p-stream/src/components/player/atoms/settings/LanguageSelectionView.tsx
Pas 685deb6d0e Refactor captions selection to group by language
Captions selection UI now groups subtitles by language, allowing users to select a language first and then choose a specific subtitle. Added LanguageSelectionView and LanguageSubtitlesView components, updated SettingsOverlay and CaptionsView to support the new flow, and enhanced ChevronLink to show selection state. This improves usability for users with multiple subtitle options per language.
2025-12-05 22:50:04 -07:00

109 lines
3.3 KiB
TypeScript

import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { FlagIcon } from "@/components/FlagIcon";
import { Menu } from "@/components/player/internals/ContextMenu";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { usePlayerStore } from "@/stores/player/store";
import { getPrettyLanguageNameFromLocale } from "@/utils/language";
export interface LanguageSelectionViewProps {
id: string;
onChoose?: (language: string) => void;
}
export function LanguageSelectionView({
id,
onChoose,
}: LanguageSelectionViewProps) {
const { t } = useTranslation();
const router = useOverlayRouter(id);
// Get all captions
const captionList = usePlayerStore((s) => s.captionList);
const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList);
const isLoadingExternalSubtitles = usePlayerStore(
(s) => s.isLoadingExternalSubtitles,
);
// Get combined caption list
const captions = useMemo(
() =>
captionList.length !== 0 ? captionList : (getHlsCaptionList?.() ?? []),
[captionList, getHlsCaptionList],
);
// Group captions by language
const groupedCaptions = useMemo(() => {
const groups: Record<string, typeof captions> = {};
captions.forEach((caption) => {
const lang = caption.language;
if (!groups[lang]) {
groups[lang] = [];
}
groups[lang].push(caption);
});
// Sort languages
const sortedGroups: Array<{
language: string;
captions: typeof captions;
languageName: string;
}> = [];
Object.entries(groups).forEach(([lang, captionsForLang]) => {
const languageName =
getPrettyLanguageNameFromLocale(lang) ||
t("player.menus.subtitles.unknownLanguage");
sortedGroups.push({
language: lang,
captions: captionsForLang,
languageName,
});
});
return sortedGroups.sort((a, b) =>
a.languageName.localeCompare(b.languageName),
);
}, [captions, t]);
return (
<>
<Menu.BackLink onClick={() => router.navigate("/captions")}>
{t("player.menus.subtitles.selectLanguage")}
</Menu.BackLink>
<Menu.Section className="pb-4">
{groupedCaptions.length > 0 ? (
groupedCaptions.map(
({ language, languageName, captions: captionsForLang }) => (
<Menu.ChevronLink
key={language}
rightText={captionsForLang.length.toString()}
onClick={() => {
onChoose?.(language);
router.navigate("/captions/languages/language");
}}
>
<span className="flex items-center">
<FlagIcon langCode={language} />
<span className="ml-3">{languageName}</span>
</span>
</Menu.ChevronLink>
),
)
) : (
<div className="text-center text-video-context-type-secondary py-2">
{t("player.menus.subtitles.notFound")}
</div>
)}
</Menu.Section>
{/* Loading indicator */}
{isLoadingExternalSubtitles && (
<div className="text-center text-video-context-type-secondary py-4">
{t("player.menus.subtitles.loadingExternal") ||
"Loading external subtitles..."}
</div>
)}
</>
);
}