feat(downloads): in-app player for library items (E7 S4b)
Play a completed library item in-app: a Play button on Library rows (and
Shared rows with download access) opens a modal that streams the local file
into a native <video controls> (or <audio> for audio-only assets).
Rather than the YouTube IFrame PlayerModal or the Plex HLS PlexPlayer — both
bound to their own domain APIs and unable to point at a local file — this
reuses the shared Modal a11y shell (focus-trap, ESC, layer-stack, portal) and
the range-aware /api/downloads/{id}/file endpoint. S4a's universal-codec
guarantee (mp4/H.264/AAC + m4a/mp3) makes native playback always work, so seek,
volume, fullscreen and speed come for free with zero transcode.
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
Link2,
|
||||
Pause,
|
||||
Play,
|
||||
PlayCircle,
|
||||
Plus,
|
||||
RotateCcw,
|
||||
Scissors,
|
||||
@@ -26,6 +27,7 @@ const DownloadDialog = lazy(() => import("./DownloadDialog"));
|
||||
const ProfileEditor = lazy(() => import("./ProfileEditor"));
|
||||
const VideoEditor = lazy(() => import("./VideoEditor"));
|
||||
const ShareDialog = lazy(() => import("./ShareDialog"));
|
||||
const LibraryPlayer = lazy(() => import("./LibraryPlayer"));
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { useMe } from "../lib/useMe";
|
||||
@@ -50,6 +52,7 @@ export default function DownloadCenter() {
|
||||
const [manage, setManage] = useState(false);
|
||||
const [metaJob, setMetaJob] = useState<DownloadJob | null>(null);
|
||||
const [shareJob, setShareJob] = useState<DownloadJob | null>(null);
|
||||
const [playJob, setPlayJob] = useState<DownloadJob | null>(null);
|
||||
const [editJob, setEditJob] = useState<DownloadJob | null>(null);
|
||||
const [errorJob, setErrorJob] = useState<DownloadJob | null>(null);
|
||||
|
||||
@@ -265,6 +268,9 @@ export default function DownloadCenter() {
|
||||
<div className="space-y-2">
|
||||
{library.map((job) => (
|
||||
<DownloadRow key={job.id} job={job}>
|
||||
<IconBtn onClick={() => setPlayJob(job)} title={t("downloads.actions.play")}>
|
||||
<PlayCircle className="w-4 h-4" />
|
||||
</IconBtn>
|
||||
{saveBtn(job)}
|
||||
{job.can_download && (job.asset?.duration_s ?? 0) > 0 && (
|
||||
<IconBtn onClick={() => setEditJob(job)} title={t("downloads.actions.edit")}>
|
||||
@@ -333,6 +339,11 @@ export default function DownloadCenter() {
|
||||
<div className="space-y-2">
|
||||
{(sharedQ.data ?? []).map((job) => (
|
||||
<DownloadRow key={job.id} job={job}>
|
||||
{job.can_download && (
|
||||
<IconBtn onClick={() => setPlayJob(job)} title={t("downloads.actions.play")}>
|
||||
<PlayCircle className="w-4 h-4" />
|
||||
</IconBtn>
|
||||
)}
|
||||
{job.can_download && saveBtn(job)}
|
||||
{job.can_download && (job.asset?.duration_s ?? 0) > 0 && (
|
||||
<IconBtn onClick={() => setEditJob(job)} title={t("downloads.actions.edit")}>
|
||||
@@ -377,6 +388,7 @@ export default function DownloadCenter() {
|
||||
{manage && <ProfileEditor onClose={() => setManage(false)} />}
|
||||
{metaJob && <MetaEditModal job={metaJob} onClose={() => setMetaJob(null)} />}
|
||||
{shareJob && <ShareDialog job={shareJob} onClose={() => setShareJob(null)} />}
|
||||
{playJob && <LibraryPlayer job={playJob} onClose={() => setPlayJob(null)} />}
|
||||
{editJob && <VideoEditor job={editJob} onClose={() => setEditJob(null)} />}
|
||||
{errorJob && (
|
||||
<ErrorModal
|
||||
|
||||
Reference in New Issue
Block a user