add downloading feature for desktop

This commit is contained in:
Duplicake-fyi 2026-02-22 20:20:42 +00:00
parent f5103b45cb
commit 29e53b4b39
3 changed files with 375 additions and 25 deletions

View file

@ -294,16 +294,28 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
{t("navigation.menu.settings")}
</DropdownLink>
{isDesktopApp && (
<DropdownLink
onClick={() =>
window.dispatchEvent(
new CustomEvent("pstream-desktop-settings"),
)
}
icon={Icons.GEAR}
>
{t("navigation.menu.desktop")}
</DropdownLink>
<>
<DropdownLink
onClick={() =>
window.dispatchEvent(
new CustomEvent("pstream-desktop-settings"),
)
}
icon={Icons.GEAR}
>
{t("navigation.menu.desktop")}
</DropdownLink>
<DropdownLink
onClick={() => {
if ((window as any).__PSTREAM_OPEN_OFFLINE__) {
(window as any).__PSTREAM_OPEN_OFFLINE__();
}
}}
icon={Icons.DOWNLOAD}
>
Offline Downloads
</DropdownLink>
</>
)}
<DropdownLink href="/watch-history" icon={Icons.CLOCK}>
{t("home.watchHistory.sectionTitle")}

View file

@ -2,11 +2,13 @@ import { useCallback, useMemo } from "react";
import { Trans, useTranslation } from "react-i18next";
import { useCopyToClipboard } from "react-use";
import { downloadCaption } from "@/backend/helpers/subs";
import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
import { OverlayPage } from "@/components/overlays/OverlayPage";
import { Menu } from "@/components/player/internals/ContextMenu";
import { convertSubtitlesToSrtDataurl } from "@/components/player/utils/captions";
import { useIsDesktopApp } from "@/hooks/useIsDesktopApp";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { usePlayerStore } from "@/stores/player/store";
@ -64,6 +66,62 @@ export function DownloadView({ id }: { id: string }) {
const sourceType = usePlayerStore((s) => s.source?.type);
const selectedCaption = usePlayerStore((s) => s.caption?.selected);
const captionList = usePlayerStore((s) => s.captionList);
const meta = usePlayerStore((s) => s.meta);
const duration = usePlayerStore((s) => s.progress.duration);
const isDesktopApp = useIsDesktopApp();
const startOfflineDownload = useCallback(async () => {
if (!downloadUrl) return;
const title = meta?.title ? meta.title : "Video";
const poster = meta?.poster;
let subtitleText = null;
if (selectedCaption?.srtData) {
subtitleText = selectedCaption.srtData;
} else if (captionList.length > 0) {
// Auto-fetch the first English caption, or the first available one
const defaultCaption =
captionList.find((c) => c.language === "en") ?? captionList[0];
try {
subtitleText = await downloadCaption(defaultCaption);
} catch {
// Continue without subtitles if fetch fails
}
}
window.postMessage(
{
name: "startDownload",
relayId: Math.random().toString(36).substring(7),
instanceId: "web-player",
body: {
url: downloadUrl,
title,
poster,
subtitleText,
duration,
type: sourceType,
},
},
"*",
);
if ((window as any).__PSTREAM_OPEN_OFFLINE__) {
(window as any).__PSTREAM_OPEN_OFFLINE__();
} else {
router.navigate("/");
}
}, [
downloadUrl,
meta,
selectedCaption,
captionList,
duration,
router,
sourceType,
]);
const openSubtitleDownload = useCallback(() => {
const dataUrl = selectedCaption
? convertSubtitlesToSrtDataurl(selectedCaption?.srtData)
@ -83,21 +141,48 @@ export function DownloadView({ id }: { id: string }) {
<div className="mb-4">
{sourceType === "hls" ? (
<div className="mb-6">
<Menu.Paragraph marginClass="mb-6">
<StyleTrans k="player.menus.downloads.hlsDisclaimer" />
</Menu.Paragraph>
{isDesktopApp ? (
<>
<Menu.Paragraph marginClass="mb-6">
<Trans i18nKey="player.menus.downloads.desktopDisclaimer">
Download this video directly to your app for offline
playback.
</Trans>
</Menu.Paragraph>
<Button
className="w-full mt-2"
theme="purple"
onClick={startOfflineDownload}
>
{t(
"player.menus.downloads.offlineButton",
"Download for Offline Use",
)}
</Button>
</>
) : (
<>
<Menu.Paragraph marginClass="mb-6">
<StyleTrans k="player.menus.downloads.hlsDisclaimer" />
</Menu.Paragraph>
<Button className="w-full mt-2" theme="purple" href={hlsDownload}>
{t("player.menus.downloads.button")}
</Button>
<p className="text-xs py-4">
<Trans i18nKey="player.menus.downloads.hlsDownloader">
<a
className="text-type-link"
href="https://hls-downloader.pstream.mov/"
/>
</Trans>
</p>
<Button
className="w-full mt-2"
theme="purple"
href={hlsDownload}
>
{t("player.menus.downloads.button")}
</Button>
<p className="text-xs py-4">
<Trans i18nKey="player.menus.downloads.hlsDownloader">
<a
className="text-type-link"
href="https://hls-downloader.pstream.mov/"
/>
</Trans>
</p>
</>
)}
<Button
className="w-full mt-2"
theme="secondary"
@ -120,6 +205,42 @@ export function DownloadView({ id }: { id: string }) {
{t("player.menus.downloads.downloadSubtitle")}
</Button>
</div>
) : sourceType === "file" ? (
<div className="mb-6">
{isDesktopApp ? (
<>
<Menu.Paragraph marginClass="mb-6">
<Trans i18nKey="player.menus.downloads.desktopDisclaimer">
Download this video directly to your app for offline
playback.
</Trans>
</Menu.Paragraph>
<Button
className="w-full mt-2"
theme="purple"
onClick={startOfflineDownload}
>
{t(
"player.menus.downloads.offlineButton",
"Download for Offline Use",
)}
</Button>
</>
) : (
<Button className="w-full" href={downloadUrl} theme="purple">
{t("player.menus.downloads.downloadVideo")}
</Button>
)}
<Button
className="w-full mt-2"
onClick={openSubtitleDownload}
disabled={!selectedCaption}
theme="secondary"
download="subtitles.srt"
>
{t("player.menus.downloads.downloadSubtitle")}
</Button>
</div>
) : (
<>
<Menu.ChevronLink onClick={() => router.navigate("/download/pc")}>
@ -141,7 +262,6 @@ export function DownloadView({ id }: { id: string }) {
<Menu.Paragraph marginClass="my-6">
<StyleTrans k="player.menus.downloads.disclaimer" />
</Menu.Paragraph>
<Button className="w-full" href={downloadUrl} theme="purple">
{t("player.menus.downloads.downloadVideo")}
</Button>

File diff suppressed because one or more lines are too long