From 627d66eced7087e66d9e12be336cdecc2a4606eb Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:54:45 -0700 Subject: [PATCH] add paste and copy subtitle options --- src/assets/locales/en.json | 2 + .../player/atoms/settings/CaptionsView.tsx | 144 +++++++++++++++--- .../player/internals/ContextMenu/Links.tsx | 4 + 3 files changed, 125 insertions(+), 25 deletions(-) diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index b2ccaeb2..f9fd3941 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -736,6 +736,8 @@ }, "subtitles": { "customChoice": "Drop or upload file", + "pasteChoice": "Paste subtitle data", + "doubleClickToCopy": "Double click to copy subtitle data", "customizeLabel": "Customize", "previewLabel": "Subtitle preview:", "offChoice": "Off", diff --git a/src/components/player/atoms/settings/CaptionsView.tsx b/src/components/player/atoms/settings/CaptionsView.tsx index 5eefe195..2736b036 100644 --- a/src/components/player/atoms/settings/CaptionsView.tsx +++ b/src/components/player/atoms/settings/CaptionsView.tsx @@ -40,9 +40,11 @@ export function CaptionOption(props: { subtitleSource?: string; subtitleEncoding?: string; isHearingImpaired?: boolean; + onDoubleClick?: () => void; }) { const [showTooltip, setShowTooltip] = useState(false); const tooltipTimeoutRef = useRef(null); + const { t } = useTranslation(); const tooltipContent = useMemo(() => { if (!props.subtitleUrl && !props.subtitleSource) return null; @@ -107,6 +109,7 @@ export function CaptionOption(props: { loading={props.loading} error={props.error} onClick={props.onClick} + onDoubleClick={props.onDoubleClick} > {tooltipContent && showTooltip && ( -
+
{tooltipContent} + {props.onDoubleClick && ( + + {t("player.menus.subtitles.doubleClickToCopy")} + + )}
)}
@@ -219,6 +227,63 @@ export function CustomCaptionOption() { ); } +export function PasteCaptionOption() { + const { t } = useTranslation(); + const setCaption = usePlayerStore((s) => s.setCaption); + const setCustomSubs = useSubtitleStore((s) => s.setCustomSubs); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handlePaste = async () => { + setIsLoading(true); + setError(null); + + try { + const clipboardText = await navigator.clipboard.readText(); + const parsedData = JSON.parse(clipboardText); + + // Validate the structure + if (!parsedData.id || !parsedData.url || !parsedData.language) { + throw new Error("Invalid subtitle data format"); + } + + // Check for CORS restrictions + if (parsedData.hasCorsRestrictions) { + throw new Error("Protected subtitle url, cannot be used"); + } + + // Fetch the subtitle content + const response = await fetch(parsedData.url); + if (!response.ok) { + throw new Error(`Failed to fetch subtitle: ${response.status}`); + } + + const subtitleText = await response.text(); + + // Convert to SRT format + const converted = convert(subtitleText, "srt"); + + setCaption({ + language: parsedData.language, + srtData: converted, + id: parsedData.id, + }); + setCustomSubs(); + } catch (err) { + console.error("Failed to paste subtitle:", err); + setError(err instanceof Error ? err.message : "Failed to paste subtitle"); + } finally { + setIsLoading(false); + } + }; + + return ( + + {t("player.menus.subtitles.pasteChoice")} + + ); +} + export function CaptionsView({ id, backLink, @@ -315,28 +380,54 @@ export function CaptionsView({ // Render subtitle option const renderSubtitleOption = ( v: CaptionListItem & { languageName: string }, - ) => ( - { + const handleDoubleClick = async () => { + const copyData = { + id: v.id, + url: v.url, + language: v.language, + type: v.type, + hasCorsRestrictions: v.needsProxy, + opensubtitles: v.opensubtitles, + display: v.display, + media: v.media, + isHearingImpaired: v.isHearingImpaired, + source: v.source, + encoding: v.encoding, + }; + + try { + await navigator.clipboard.writeText(JSON.stringify(copyData, null, 2)); + // Could add a toast notification here if needed + } catch (err) { + console.error("Failed to copy subtitle data:", err); } - onClick={() => startDownload(v.id)} - flag - subtitleUrl={v.url} - subtitleType={v.type} - subtitleSource={v.source} - subtitleEncoding={v.encoding} - isHearingImpaired={v.isHearingImpaired} - > - {v.languageName} - - ); + }; + + return ( + startDownload(v.id)} + onDoubleClick={handleDoubleClick} + flag + subtitleUrl={v.url} + subtitleType={v.type} + subtitleSource={v.source} + subtitleEncoding={v.encoding} + isHearingImpaired={v.isHearingImpaired} + > + {v.languageName} + + ); + }; return ( <> @@ -431,11 +522,14 @@ export function CaptionsView({ {/* Custom upload option */} + {/* Paste subtitle option */} + + +
+ {/* Search input */} {(sourceCaptions.length || externalCaptions.length) > 0 && ( -
- -
+ )} {/* No subtitles available message */} diff --git a/src/components/player/internals/ContextMenu/Links.tsx b/src/components/player/internals/ContextMenu/Links.tsx index caed159c..7d53e264 100644 --- a/src/components/player/internals/ContextMenu/Links.tsx +++ b/src/components/player/internals/ContextMenu/Links.tsx @@ -80,6 +80,7 @@ export function Link(props: { clickable?: boolean; active?: boolean; onClick?: () => void; + onDoubleClick?: () => void; children?: ReactNode; className?: string; box?: boolean; @@ -126,6 +127,7 @@ export function Link(props: { className={classes} style={props.box ? {} : styles} onClick={props.onClick} + onDoubleClick={props.onDoubleClick} data-active-link={props.active ? true : undefined} disabled={props.disabled} > @@ -162,6 +164,7 @@ export function SelectableLink(props: { selected?: boolean; loading?: boolean; onClick?: () => void; + onDoubleClick?: () => void; children?: ReactNode; disabled?: boolean; error?: ReactNode; @@ -187,6 +190,7 @@ export function SelectableLink(props: { return (