diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 55517a26..8fa82b62 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -372,6 +372,7 @@ "customChoice": "Drop or upload file", "customizeLabel": "Customize", "offChoice": "Off", + "SourceChoice": "Source Captions", "OpenSubtitlesChoice": "OpenSubtitles", "settings": { "backlink": "Custom subtitles", @@ -382,7 +383,8 @@ "unknownLanguage": "Unknown", "dropSubtitleFile": "Drop subtitle file here! >_<", "scrapeButton": "Scrape subtitles", - "empty": "There are no provided subtitles for this." + "empty": "There are no provided subtitles for this.", + "notFound": "None of the available options match your query" } }, "metadata": { diff --git a/src/components/player/atoms/Captions.tsx b/src/components/player/atoms/Captions.tsx new file mode 100644 index 00000000..89e419e0 --- /dev/null +++ b/src/components/player/atoms/Captions.tsx @@ -0,0 +1,28 @@ +import { useEffect } from "react"; + +import { Icons } from "@/components/Icon"; +import { OverlayAnchor } from "@/components/overlays/OverlayAnchor"; +import { VideoPlayerButton } from "@/components/player/internals/Button"; +import { useOverlayRouter } from "@/hooks/useOverlayRouter"; +import { usePlayerStore } from "@/stores/player/store"; + +export function Captions() { + const router = useOverlayRouter("settings"); + const setHasOpenOverlay = usePlayerStore((s) => s.setHasOpenOverlay); + + useEffect(() => { + setHasOpenOverlay(router.isRouterActive); + }, [setHasOpenOverlay, router.isRouterActive]); + + return ( + + { + router.open(); + router.navigate("/captionsOverlay"); + }} + icon={Icons.CAPTIONS} + /> + + ); +} diff --git a/src/components/player/atoms/Settings.tsx b/src/components/player/atoms/Settings.tsx index b3eb2a59..22a690a1 100644 --- a/src/components/player/atoms/Settings.tsx +++ b/src/components/player/atoms/Settings.tsx @@ -18,10 +18,11 @@ import { AudioView } from "./settings/AudioView"; import { CaptionSettingsView } from "./settings/CaptionSettingsView"; import { CaptionsView } from "./settings/CaptionsView"; import { DownloadRoutes } from "./settings/Downloads"; -import { OpenSubtitlesCaptionView } from "./settings/Opensubtitles"; +import { OpenSubtitlesCaptionView } from "./settings/OpensubtitlesCaptionsView"; import { PlaybackSettingsView } from "./settings/PlaybackSettingsView"; import { QualityView } from "./settings/QualityView"; import { SettingsMenu } from "./settings/SettingsMenu"; +import SourceCaptionsView from "./settings/SourceCaptionsView"; function SettingsOverlay({ id }: { id: string }) { const [chosenSourceId, setChosenSourceId] = useState(null); @@ -54,6 +55,12 @@ function SettingsOverlay({ id }: { id: string }) { + + + + + {/* This is used by the captions shortcut in bottomControls of player */} + @@ -68,11 +75,49 @@ function SettingsOverlay({ id }: { id: string }) { + {/* This is used by the captions shortcut in bottomControls of player */} + + + + + + + + + + + {/* This is used by the captions shortcut in bottomControls of player */} + + + + + + {/* This is used by the captions shortcut in bottomControls of player */} + + + + + diff --git a/src/components/player/atoms/index.ts b/src/components/player/atoms/index.ts index 3caf8154..6be88fd6 100644 --- a/src/components/player/atoms/index.ts +++ b/src/components/player/atoms/index.ts @@ -16,3 +16,4 @@ export * from "./VolumeChangedPopout"; export * from "./NextEpisodeButton"; export * from "./Chromecast"; export * from "./CastingNotification"; +export * from "./Captions"; diff --git a/src/components/player/atoms/settings/CaptionSettingsView.tsx b/src/components/player/atoms/settings/CaptionSettingsView.tsx index 2f942a48..884233a9 100644 --- a/src/components/player/atoms/settings/CaptionSettingsView.tsx +++ b/src/components/player/atoms/settings/CaptionSettingsView.tsx @@ -216,7 +216,13 @@ export function CaptionSetting(props: { export const colors = ["#ffffff", "#b0b0b0", "#80b1fa", "#e2e535"]; -export function CaptionSettingsView({ id }: { id: string }) { +export function CaptionSettingsView({ + id, + overlayBackLink, +}: { + id: string; + overlayBackLink?: boolean; +}) { const { t } = useTranslation(); const router = useOverlayRouter(id); const styling = useSubtitleStore((s) => s.styling); @@ -228,7 +234,11 @@ export function CaptionSettingsView({ id }: { id: string }) { return ( <> - router.navigate("/captions")}> + + router.navigate(overlayBackLink ? "/captionsOverlay" : "/captions") + } + > {t("player.menus.subtitles.settings.backlink")} diff --git a/src/components/player/atoms/settings/CaptionsView.tsx b/src/components/player/atoms/settings/CaptionsView.tsx index 709ad067..05dcb41e 100644 --- a/src/components/player/atoms/settings/CaptionsView.tsx +++ b/src/components/player/atoms/settings/CaptionsView.tsx @@ -1,8 +1,6 @@ import classNames from "classnames"; -import Fuse from "fuse.js"; -import { type DragEvent, useMemo, useRef, useState } from "react"; +import { type DragEvent, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useAsyncFn } from "react-use"; import { convert } from "subsrt-ts"; import { subtitleTypeList } from "@/backend/helpers/subs"; @@ -11,16 +9,11 @@ import { FlagIcon } from "@/components/FlagIcon"; import { Icon, Icons } from "@/components/Icon"; import { useCaptions } from "@/components/player/hooks/useCaptions"; import { Menu } from "@/components/player/internals/ContextMenu"; -import { Input } from "@/components/player/internals/ContextMenu/Input"; import { SelectableLink } from "@/components/player/internals/ContextMenu/Links"; import { useOverlayRouter } from "@/hooks/useOverlayRouter"; -import { CaptionListItem } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; import { useSubtitleStore } from "@/stores/subtitles"; -import { - getPrettyLanguageNameFromLocale, - sortLangCodes, -} from "@/utils/language"; +import { getPrettyLanguageNameFromLocale } from "@/utils/language"; export function CaptionOption(props: { countryCode?: string; @@ -29,7 +22,6 @@ export function CaptionOption(props: { loading?: boolean; onClick?: () => void; error?: React.ReactNode; - chevron?: boolean; }) { return ( s.caption.selected?.language); const setCaption = usePlayerStore((s) => s.setCaption); @@ -91,47 +82,22 @@ function CustomCaptionOption() { ); } -function useSubtitleList(subs: CaptionListItem[], searchQuery: string) { - const { t: translate } = useTranslation(); - const unknownChoice = translate("player.menus.subtitles.unknownLanguage"); - return useMemo(() => { - const input = subs - .map((t) => ({ - ...t, - languageName: - getPrettyLanguageNameFromLocale(t.language) ?? unknownChoice, - })) - .filter((x) => !x.opensubtitles); - const sorted = sortLangCodes(input.map((t) => t.language)); - let results = input.sort((a, b) => { - return sorted.indexOf(a.language) - sorted.indexOf(b.language); - }); - - if (searchQuery.trim().length > 0) { - const fuse = new Fuse(input, { - includeScore: true, - keys: ["languageName"], - }); - - results = fuse.search(searchQuery).map((res) => res.item); - } - - return results; - }, [subs, searchQuery, unknownChoice]); -} - -export function CaptionsView({ id }: { id: string }) { +export function CaptionsView({ + id, + backLink, +}: { + id: string; + backLink?: true; +}) { const { t } = useTranslation(); const router = useOverlayRouter(id); const selectedCaptionId = usePlayerStore((s) => s.caption.selected?.id); - const [currentlyDownloading, setCurrentlyDownloading] = useState< - string | null - >(null); - const { selectCaptionById, disable } = useCaptions(); - const captionList = usePlayerStore((s) => s.captionList); - const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList); + const { disable } = useCaptions(); const [dragging, setDragging] = useState(false); const setCaption = usePlayerStore((s) => s.setCaption); + const selectedCaptionLanguage = usePlayerStore( + (s) => s.caption.selected?.language, + ); function onDrop(event: DragEvent) { const files = event.dataTransfer.files; @@ -159,42 +125,10 @@ export function CaptionsView({ id }: { id: string }) { reader.readAsText(firstFile); } - const captions = useMemo( - () => - captionList.length !== 0 ? captionList : getHlsCaptionList?.() ?? [], - [captionList, getHlsCaptionList], - ); - - const [searchQuery, setSearchQuery] = useState(""); - const subtitleList = useSubtitleList(captions, searchQuery); - - const [downloadReq, startDownload] = useAsyncFn( - async (captionId: string) => { - setCurrentlyDownloading(captionId); - return selectCaptionById(captionId); - }, - [selectCaptionById, setCurrentlyDownloading], - ); - - const content = subtitleList.map((v) => { - return ( - startDownload(v.id)} - > - {v.languageName} - - ); - }); + const selectedLanguagePretty = selectedCaptionLanguage + ? getPrettyLanguageNameFromLocale(selectedCaptionLanguage) ?? + t("player.menus.subtitles.unknownLanguage") + : undefined; return ( <> @@ -213,20 +147,36 @@ export function CaptionsView({ id }: { id: string }) { - router.navigate("/")} - rightSide={ - - } - > - {t("player.menus.subtitles.title")} - + {backLink ? ( + router.navigate("/")} + rightSide={ + + } + > + {t("player.menus.subtitles.title")} + + ) : ( + router.navigate("/captions/settingsOverlay")} + className="-mr-2 -my-1 px-2 p-[0.4em] rounded tabbable hover:bg-video-context-light hover:bg-opacity-10" + > + {t("player.menus.subtitles.customizeLabel")} + + } + > + {t("player.menus.subtitles.title")} + + )} onDrop(event)} > -
- - -
disable()} @@ -253,22 +193,36 @@ export function CaptionsView({ id }: { id: string }) { {t("player.menus.subtitles.offChoice")} - {content.length === 0 ? ( -
-
- {t("player.menus.subtitles.empty")} - -
-
- ) : ( - content - )} + + router.navigate( + backLink ? "/captions/source" : "/captions/sourceOverlay", + ) + } + rightText={ + useSubtitleStore((s) => s.isOpenSubtitles) + ? "" + : selectedLanguagePretty + } + > + {t("player.menus.subtitles.SourceChoice")} + + + router.navigate( + backLink + ? "/captions/opensubtitles" + : "/captions/opensubtitlesOverlay", + ) + } + rightText={ + useSubtitleStore((s) => s.isOpenSubtitles) + ? selectedLanguagePretty + : "" + } + > + {t("player.menus.subtitles.OpenSubtitlesChoice")} +
diff --git a/src/components/player/atoms/settings/Opensubtitles.tsx b/src/components/player/atoms/settings/Opensubtitles.tsx deleted file mode 100644 index b4dd2b1b..00000000 --- a/src/components/player/atoms/settings/Opensubtitles.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import Fuse from "fuse.js"; -import { useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useAsyncFn } from "react-use"; - -import { FlagIcon } from "@/components/FlagIcon"; -import { useCaptions } from "@/components/player/hooks/useCaptions"; -import { Menu } from "@/components/player/internals/ContextMenu"; -import { Input } from "@/components/player/internals/ContextMenu/Input"; -import { SelectableLink } from "@/components/player/internals/ContextMenu/Links"; -import { useOverlayRouter } from "@/hooks/useOverlayRouter"; -import { CaptionListItem } from "@/stores/player/slices/source"; -import { usePlayerStore } from "@/stores/player/store"; -import { - getPrettyLanguageNameFromLocale, - sortLangCodes, -} from "@/utils/language"; - -export function CaptionOption(props: { - countryCode?: string; - children: React.ReactNode; - selected?: boolean; - loading?: boolean; - onClick?: () => void; - error?: React.ReactNode; -}) { - return ( - - - - - - {props.children} - - - ); -} - -function useSubtitleList(subs: CaptionListItem[], searchQuery: string) { - const { t: translate } = useTranslation(); - const unknownChoice = translate("player.menus.subtitles.unknownLanguage"); - return useMemo(() => { - const input = subs - .map((t) => ({ - ...t, - languageName: - getPrettyLanguageNameFromLocale(t.language) ?? unknownChoice, - })) - .filter((x) => x.opensubtitles); - const sorted = sortLangCodes(input.map((t) => t.language)); - let results = input.sort((a, b) => { - return sorted.indexOf(a.language) - sorted.indexOf(b.language); - }); - - if (searchQuery.trim().length > 0) { - const fuse = new Fuse(input, { - includeScore: true, - keys: ["languageName"], - }); - - results = fuse.search(searchQuery).map((res) => res.item); - } - - return results; - }, [subs, searchQuery, unknownChoice]); -} - -export function OpenSubtitlesCaptionView({ id }: { id: string }) { - const { t } = useTranslation(); - const router = useOverlayRouter(id); - const selectedCaptionId = usePlayerStore((s) => s.caption.selected?.id); - const [currentlyDownloading, setCurrentlyDownloading] = useState< - string | null - >(null); - const { selectCaptionById } = useCaptions(); - const captionList = usePlayerStore((s) => s.captionList); - const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList); - - const captions = useMemo( - () => - captionList.length !== 0 ? captionList : getHlsCaptionList?.() ?? [], - [captionList, getHlsCaptionList], - ); - - const [searchQuery, setSearchQuery] = useState(""); - const subtitleList = useSubtitleList(captions, searchQuery); - - const [downloadReq, startDownload] = useAsyncFn( - async (captionId: string) => { - setCurrentlyDownloading(captionId); - return selectCaptionById(captionId); - }, - [selectCaptionById, setCurrentlyDownloading], - ); - - const content = subtitleList.map((v) => { - return ( - startDownload(v.id)} - > - {v.languageName} - - ); - }); - - return ( - <> -
- router.navigate("/captions")}> - {t("player.menus.subtitles.OpenSubtitlesChoice")} - -
-
- -
- - {content} - - - ); -} - -export default OpenSubtitlesCaptionView; diff --git a/src/components/player/atoms/settings/OpensubtitlesCaptionsView.tsx b/src/components/player/atoms/settings/OpensubtitlesCaptionsView.tsx new file mode 100644 index 00000000..160d9009 --- /dev/null +++ b/src/components/player/atoms/settings/OpensubtitlesCaptionsView.tsx @@ -0,0 +1,104 @@ +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useAsyncFn } from "react-use"; + +import { useCaptions } from "@/components/player/hooks/useCaptions"; +import { Menu } from "@/components/player/internals/ContextMenu"; +import { Input } from "@/components/player/internals/ContextMenu/Input"; +import { useOverlayRouter } from "@/hooks/useOverlayRouter"; +import { usePlayerStore } from "@/stores/player/store"; + +import { CaptionOption } from "./CaptionsView"; +import { useSubtitleList } from "./SourceCaptionsView"; + +export function OpenSubtitlesCaptionView({ + id, + overlayBackLink, +}: { + id: string; + overlayBackLink?: true; +}) { + const { t } = useTranslation(); + const router = useOverlayRouter(id); + const selectedCaptionId = usePlayerStore((s) => s.caption.selected?.id); + const [currentlyDownloading, setCurrentlyDownloading] = useState< + string | null + >(null); + const { selectCaptionById } = useCaptions(); + const captionList = usePlayerStore((s) => s.captionList); + const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList); + + const captions = useMemo( + () => + captionList.length !== 0 ? captionList : getHlsCaptionList?.() ?? [], + [captionList, getHlsCaptionList], + ); + + const [searchQuery, setSearchQuery] = useState(""); + const subtitleList = useSubtitleList( + captions.filter((x) => x.opensubtitles), + searchQuery, + ); + + const [downloadReq, startDownload] = useAsyncFn( + async (captionId: string) => { + setCurrentlyDownloading(captionId); + return selectCaptionById(captionId); + }, + [selectCaptionById, setCurrentlyDownloading], + ); + + const content = subtitleList.length + ? subtitleList.map((v) => { + return ( + startDownload(v.id)} + > + {v.languageName} + + ); + }) + : t("player.menus.subtitles.notFound"); + + return ( + <> +
+ + router.navigate(overlayBackLink ? "/captionsOverlay" : "/captions") + } + > + {t("player.menus.subtitles.OpenSubtitlesChoice")} + +
+ {captionList.filter((x) => x.opensubtitles).length ? ( +
+ +
+ ) : null} + + {!captionList.filter((x) => x.opensubtitles).length ? ( +
+
+ {t("player.menus.subtitles.empty")} +
+
+ ) : ( +
{content}
+ )} +
+ + ); +} + +export default OpenSubtitlesCaptionView; diff --git a/src/components/player/atoms/settings/SourceCaptionsView.tsx b/src/components/player/atoms/settings/SourceCaptionsView.tsx new file mode 100644 index 00000000..3d24a366 --- /dev/null +++ b/src/components/player/atoms/settings/SourceCaptionsView.tsx @@ -0,0 +1,149 @@ +import Fuse from "fuse.js"; +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useAsyncFn } from "react-use"; + +import { useCaptions } from "@/components/player/hooks/useCaptions"; +import { Menu } from "@/components/player/internals/ContextMenu"; +import { Input } from "@/components/player/internals/ContextMenu/Input"; +import { useOverlayRouter } from "@/hooks/useOverlayRouter"; +import { CaptionListItem } from "@/stores/player/slices/source"; +import { usePlayerStore } from "@/stores/player/store"; +import { + getPrettyLanguageNameFromLocale, + sortLangCodes, +} from "@/utils/language"; + +import { CaptionOption } from "./CaptionsView"; + +export function useSubtitleList(subs: CaptionListItem[], searchQuery: string) { + const { t: translate } = useTranslation(); + const unknownChoice = translate("player.menus.subtitles.unknownLanguage"); + return useMemo(() => { + const input = subs.map((t) => ({ + ...t, + languageName: + getPrettyLanguageNameFromLocale(t.language) ?? unknownChoice, + })); + const sorted = sortLangCodes(input.map((t) => t.language)); + let results = input.sort((a, b) => { + return sorted.indexOf(a.language) - sorted.indexOf(b.language); + }); + + if (searchQuery.trim().length > 0) { + const fuse = new Fuse(input, { + includeScore: true, + keys: ["languageName"], + }); + + results = fuse.search(searchQuery).map((res) => res.item); + } + + return results; + }, [subs, searchQuery, unknownChoice]); +} + +export function SourceCaptionsView({ + id, + overlayBackLink, +}: { + id: string; + overlayBackLink?: true; +}) { + const { t } = useTranslation(); + const router = useOverlayRouter(id); + const selectedCaptionId = usePlayerStore((s) => s.caption.selected?.id); + const [currentlyDownloading, setCurrentlyDownloading] = useState< + string | null + >(null); + const { selectCaptionById } = useCaptions(); + const captionList = usePlayerStore((s) => s.captionList); + const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList); + + const captions = useMemo( + () => + captionList.length !== 0 ? captionList : getHlsCaptionList?.() ?? [], + [captionList, getHlsCaptionList], + ); + + const [searchQuery, setSearchQuery] = useState(""); + const subtitleList = useSubtitleList( + captions.filter((x) => !x.opensubtitles), + searchQuery, + ); + + const [downloadReq, startDownload] = useAsyncFn( + async (captionId: string) => { + setCurrentlyDownloading(captionId); + return selectCaptionById(captionId); + }, + [selectCaptionById, setCurrentlyDownloading], + ); + + const content = subtitleList.length + ? subtitleList.map((v) => { + return ( + startDownload(v.id)} + > + {v.languageName} + + ); + }) + : t("player.menus.subtitles.notFound"); + + return ( + <> +
+ + router.navigate(overlayBackLink ? "/captionsOverlay" : "/captions") + } + > + {t("player.menus.subtitles.SourceChoice")} + +
+ {captionList.filter((x) => !x.opensubtitles).length ? ( +
+ +
+ ) : null} + + {!captionList.filter((x) => !x.opensubtitles).length ? ( +
+
+ {t("player.menus.subtitles.empty")} + +
+
+ ) : ( +
{content}
+ )} +
+ + ); +} + +export default SourceCaptionsView; diff --git a/src/components/player/internals/ContextMenu/Links.tsx b/src/components/player/internals/ContextMenu/Links.tsx index 9b49db88..616647a9 100644 --- a/src/components/player/internals/ContextMenu/Links.tsx +++ b/src/components/player/internals/ContextMenu/Links.tsx @@ -123,34 +123,14 @@ export function SelectableLink(props: { children?: ReactNode; disabled?: boolean; error?: ReactNode; - chevron?: boolean; }) { let rightContent; if (props.selected) { - if (props.chevron) { - rightContent = ( - - - - - ); - } else { - rightContent = ( - - ); - } - } else if (props.chevron) { rightContent = ( - + ); } if (props.error) diff --git a/src/pages/parts/player/PlayerPart.tsx b/src/pages/parts/player/PlayerPart.tsx index 1f55013b..f012f8ea 100644 --- a/src/pages/parts/player/PlayerPart.tsx +++ b/src/pages/parts/player/PlayerPart.tsx @@ -111,7 +111,10 @@ export function PlayerPart(props: PlayerPartProps) { ) : null} {status === playerStatus.PLAYBACK_ERROR || status === playerStatus.PLAYING ? ( - + <> + + + ) : null} @@ -121,7 +124,12 @@ export function PlayerPart(props: PlayerPartProps) {
{status === playerStatus.PLAYING ? : null} - {status === playerStatus.PLAYING ? : null} + {status === playerStatus.PLAYING ? ( + <> + + + + ) : null}