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/Settings.tsx b/src/components/player/atoms/Settings.tsx index b7cc8456..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); @@ -85,6 +86,22 @@ function SettingsOverlay({ id }: { id: string }) { + + + + + + {/* This is used by the captions shortcut in bottomControls of player */} + + + + + diff --git a/src/components/player/atoms/settings/CaptionsView.tsx b/src/components/player/atoms/settings/CaptionsView.tsx index a72a41df..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,35 +82,6 @@ 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, backLink, @@ -130,14 +92,12 @@ export function CaptionsView({ 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; @@ -165,42 +125,10 @@ export function CaptionsView({ 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 ( <> @@ -257,9 +185,6 @@ export function CaptionsView({ }} onDrop={(event) => onDrop(event)} > -
- -
disable()} @@ -268,7 +193,21 @@ export function CaptionsView({ {t("player.menus.subtitles.offChoice")} - + router.navigate( + backLink ? "/captions/source" : "/captions/sourceOverlay", + ) + } + rightText={ + useSubtitleStore((s) => s.isOpenSubtitles) + ? "" + : selectedLanguagePretty + } + > + {t("player.menus.subtitles.SourceChoice")} + + router.navigate( backLink @@ -276,33 +215,14 @@ export function CaptionsView({ : "/captions/opensubtitlesOverlay", ) } - selected={useSubtitleStore((s) => s.isOpenSubtitles)} - chevron + rightText={ + useSubtitleStore((s) => s.isOpenSubtitles) + ? selectedLanguagePretty + : "" + } > {t("player.menus.subtitles.OpenSubtitlesChoice")} - - {content.length === 0 ? ( -
-
- {t("player.menus.subtitles.empty")} - -
-
- ) : ( - content - )} +
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/Opensubtitles.tsx b/src/components/player/atoms/settings/SourceCaptionsView.tsx similarity index 51% rename from src/components/player/atoms/settings/Opensubtitles.tsx rename to src/components/player/atoms/settings/SourceCaptionsView.tsx index 3ead05cd..3d24a366 100644 --- a/src/components/player/atoms/settings/Opensubtitles.tsx +++ b/src/components/player/atoms/settings/SourceCaptionsView.tsx @@ -3,11 +3,9 @@ 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"; @@ -16,45 +14,17 @@ import { 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} - - - ); -} +import { CaptionOption } from "./CaptionsView"; -function useSubtitleList(subs: CaptionListItem[], searchQuery: string) { +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, - })) - .filter((x) => x.opensubtitles); + 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); @@ -73,12 +43,12 @@ function useSubtitleList(subs: CaptionListItem[], searchQuery: string) { }, [subs, searchQuery, unknownChoice]); } -export function OpenSubtitlesCaptionView({ +export function SourceCaptionsView({ id, overlayBackLink, }: { id: string; - overlayBackLink?: boolean; + overlayBackLink?: true; }) { const { t } = useTranslation(); const router = useOverlayRouter(id); @@ -97,7 +67,10 @@ export function OpenSubtitlesCaptionView({ ); const [searchQuery, setSearchQuery] = useState(""); - const subtitleList = useSubtitleList(captions, searchQuery); + const subtitleList = useSubtitleList( + captions.filter((x) => !x.opensubtitles), + searchQuery, + ); const [downloadReq, startDownload] = useAsyncFn( async (captionId: string) => { @@ -107,25 +80,27 @@ export function OpenSubtitlesCaptionView({ [selectCaptionById, setCurrentlyDownloading], ); - const content = subtitleList.map((v) => { - return ( - startDownload(v.id)} - > - {v.languageName} - - ); - }); + const content = subtitleList.length + ? subtitleList.map((v) => { + return ( + startDownload(v.id)} + > + {v.languageName} + + ); + }) + : t("player.menus.subtitles.notFound"); return ( <> @@ -135,17 +110,40 @@ export function OpenSubtitlesCaptionView({ router.navigate(overlayBackLink ? "/captionsOverlay" : "/captions") } > - {t("player.menus.subtitles.OpenSubtitlesChoice")} + {t("player.menus.subtitles.SourceChoice")} -
- -
+ {captionList.filter((x) => !x.opensubtitles).length ? ( +
+ +
+ ) : null} - {content} + {!captionList.filter((x) => !x.opensubtitles).length ? ( +
+
+ {t("player.menus.subtitles.empty")} + +
+
+ ) : ( +
{content}
+ )}
); } -export default OpenSubtitlesCaptionView; +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)