Begin work on translate subtitle view

This commit is contained in:
vlOd2 2025-12-26 01:00:09 +02:00
parent 50216a10d9
commit 6bc4907399
7 changed files with 161 additions and 5 deletions

View file

@ -9,9 +9,12 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[type.scriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View file

@ -83,6 +83,7 @@ export enum Icons {
RELOAD = "reload",
REPEAT = "repeat",
PLUS = "plus",
TRANSLATE = "translate",
}
export interface IconProps {
@ -183,6 +184,7 @@ const iconList: Record<Icons, string> = {
reload: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 640 640" fill="currentColor"><!--!Font Awesome Free v7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M544.1 256L552 256C565.3 256 576 245.3 576 232L576 88C576 78.3 570.2 69.5 561.2 65.8C552.2 62.1 541.9 64.2 535 71L483.3 122.8C439 86.1 382 64 320 64C191 64 84.3 159.4 66.6 283.5C64.1 301 76.2 317.2 93.7 319.7C111.2 322.2 127.4 310 129.9 292.6C143.2 199.5 223.3 128 320 128C364.4 128 405.2 143 437.7 168.3L391 215C384.1 221.9 382.1 232.2 385.8 241.2C389.5 250.2 398.3 256 408 256L544.1 256zM573.5 356.5C576 339 563.8 322.8 546.4 320.3C529 317.8 512.7 330 510.2 347.4C496.9 440.4 416.8 511.9 320.1 511.9C275.7 511.9 234.9 496.9 202.4 471.6L249 425C255.9 418.1 257.9 407.8 254.2 398.8C250.5 389.8 241.7 384 232 384L88 384C74.7 384 64 394.7 64 408L64 552C64 561.7 69.8 570.5 78.8 574.2C87.8 577.9 98.1 575.8 105 569L156.8 517.2C201 553.9 258 576 320 576C449 576 555.7 480.6 573.4 356.5z"/></svg>`,
repeat: `<svg viewBox="0 0 24 24" width="1em" height="1em" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="css-i6dzq1"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>`,
plus: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="1em" height="1em" fill="currentColor"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M352 128C352 110.3 337.7 96 320 96C302.3 96 288 110.3 288 128L288 288L128 288C110.3 288 96 302.3 96 320C96 337.7 110.3 352 128 352L288 352L288 512C288 529.7 302.3 544 320 544C337.7 544 352 529.7 352 512L352 352L512 352C529.7 352 544 337.7 544 320C544 302.3 529.7 288 512 288L352 288L352 128z"/></svg>`,
translate: `<svg width="1em" height="1em" fill="currentColor" width="800px" height="800px" viewBox="0 0 52 52" data-name="Layer 1" id="Layer_1" xmlns="http://www.w3.org/2000/svg"><path d="M39,18.67H35.42l-4.2,11.12A29,29,0,0,1,20.6,24.91a28.76,28.76,0,0,0,7.11-14.49h5.21a2,2,0,0,0,0-4H19.67V2a2,2,0,1,0-4,0V6.42H2.41a2,2,0,0,0,0,4H7.63a28.73,28.73,0,0,0,7.1,14.49A29.51,29.51,0,0,1,3.27,30a2,2,0,0,0,.43,4,1.61,1.61,0,0,0,.44-.05,32.56,32.56,0,0,0,13.53-6.25,32,32,0,0,0,12.13,5.9L22.83,52H28l2.7-7.76H43.64L46.37,52h5.22Zm-15.3-8.25a23.76,23.76,0,0,1-6,11.86,23.71,23.71,0,0,1-6-11.86Zm8.68,29.15,4.83-13.83L42,39.57Z"/></svg>`,
};
export const Icon = memo((props: IconProps) => {

View file

@ -12,6 +12,7 @@ import {
import { VideoPlayerButton } from "@/components/player/internals/Button";
import { Menu } from "@/components/player/internals/ContextMenu";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { CaptionListItem } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
import { AudioView } from "./settings/AudioView";
@ -23,11 +24,14 @@ import { PlaybackSettingsView } from "./settings/PlaybackSettingsView";
import { QualityView } from "./settings/QualityView";
import { SettingsMenu } from "./settings/SettingsMenu";
import { TranscriptView } from "./settings/TranscriptView";
import { TranslateSubtitleView } from "./settings/TranslateSubtitleView";
import { WatchPartyView } from "./settings/WatchPartyView";
function SettingsOverlay({ id }: { id: string }) {
const [chosenSourceId, setChosenSourceId] = useState<string | null>(null);
const [chosenLanguage, setChosenLanguage] = useState<string | null>(null);
const [captionToTranslate, setCaptionToTranslate] =
useState<CaptionListItem | null>(null);
const router = useOverlayRouter(id);
// reset source id and language when going to home or closing overlay
@ -84,6 +88,23 @@ function SettingsOverlay({ id }: { id: string }) {
<LanguageSubtitlesView
id={id}
language={chosenLanguage}
onTranslateSubtitle={setCaptionToTranslate}
overlayBackLink
/>
)}
</Menu.CardWithScrollable>
</OverlayPage>
<OverlayPage
id={id}
path="/captionsOverlay/translateSubtitleOverlay"
width={343}
height={452}
>
<Menu.CardWithScrollable>
{captionToTranslate && (
<TranslateSubtitleView
id={id}
caption={captionToTranslate}
overlayBackLink
/>
)}
@ -138,7 +159,23 @@ function SettingsOverlay({ id }: { id: string }) {
>
<Menu.CardWithScrollable>
{chosenLanguage && (
<LanguageSubtitlesView id={id} language={chosenLanguage} />
<LanguageSubtitlesView
id={id}
language={chosenLanguage}
onTranslateSubtitle={setCaptionToTranslate}
/>
)}
</Menu.CardWithScrollable>
</OverlayPage>
<OverlayPage
id={id}
path="/captions/translateSubtitle"
width={343}
height={452}
>
<Menu.CardWithScrollable>
{captionToTranslate && (
<TranslateSubtitleView id={id} caption={captionToTranslate} />
)}
</Menu.CardWithScrollable>
</OverlayPage>

View file

@ -9,6 +9,7 @@ import { subtitleTypeList } from "@/backend/helpers/subs";
import { FileDropHandler } from "@/components/DropFile";
import { FlagIcon } from "@/components/FlagIcon";
import { Icon, Icons } from "@/components/Icon";
import { Spinner } from "@/components/layout/Spinner";
import { useCaptions } from "@/components/player/hooks/useCaptions";
import { Menu } from "@/components/player/internals/ContextMenu";
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
@ -26,7 +27,8 @@ import {
sortLangCodes,
} from "@/utils/language";
export function CaptionOption(props: {
/* eslint-disable react/no-unused-prop-types */
export interface CaptionOptionProps {
countryCode?: string;
children: React.ReactNode;
selected?: boolean;
@ -41,7 +43,62 @@ export function CaptionOption(props: {
subtitleEncoding?: string;
isHearingImpaired?: boolean;
onDoubleClick?: () => void;
}) {
onTranslate?: () => void;
}
/* eslint-enable react/no-unused-prop-types */
function CaptionOptionRightSide(props: CaptionOptionProps) {
if (props.loading) {
// should override selected and error and not show translate button
return <Spinner className="text-lg" />;
}
function translateBtn(margin: boolean) {
return (
props.countryCode && (
<span
className={classNames(
"text-buttons-secondaryText px-2 py-1 rounded bg-opacity-0",
"transition duration-300 ease-in-out",
"hover:bg-opacity-100 hover:bg-buttons-primaryHover",
"hover:text-buttons-primaryText",
{
"mr-3": margin,
},
)}
onClick={(e) => {
e.stopPropagation();
props.onTranslate?.();
}}
>
<Icon icon={Icons.TRANSLATE} className="text-lg" />
</span>
)
);
}
if (props.selected || props.error) {
return (
<div className="flex items-center">
{translateBtn(true)}
{props.error ? (
<span className="text-video-context-error">
<Icon className="ml-2" icon={Icons.WARNING} />
</span>
) : (
<Icon
icon={Icons.CIRCLE_CHECK}
className="text-xl text-video-context-type-accent"
/>
)}
</div>
);
}
return translateBtn(false);
}
export function CaptionOption(props: CaptionOptionProps) {
const [showTooltip, setShowTooltip] = useState(false);
const tooltipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const { t } = useTranslation();
@ -110,6 +167,7 @@ export function CaptionOption(props: {
error={props.error}
onClick={props.onClick}
onDoubleClick={props.onDoubleClick}
rightSide={<CaptionOptionRightSide {...props} />}
>
<span
data-active-link={props.selected ? true : undefined}

View file

@ -17,12 +17,14 @@ export interface LanguageSubtitlesViewProps {
id: string;
language: string;
overlayBackLink?: boolean;
onTranslateSubtitle?: (caption: CaptionListItem) => void;
}
export function LanguageSubtitlesView({
id,
language,
overlayBackLink,
onTranslateSubtitle,
}: LanguageSubtitlesViewProps) {
const { t } = useTranslation();
const router = useOverlayRouter(id);
@ -130,6 +132,14 @@ export function LanguageSubtitlesView({
: undefined
}
onClick={() => startDownload(v.id)}
onTranslate={() => {
onTranslateSubtitle?.(v);
router.navigate(
overlayBackLink
? "/captions/translateSubtitle"
: "/captionsOverlay/translateSubtitleOverlay",
);
}}
onDoubleClick={handleDoubleClick}
flag
subtitleUrl={v.url}

View file

@ -0,0 +1,46 @@
import { useTranslation } from "react-i18next";
import { FlagIcon } from "@/components/FlagIcon";
import { Menu } from "@/components/player/internals/ContextMenu";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { CaptionListItem } from "@/stores/player/slices/source";
export interface LanguageSubtitlesViewProps {
id: string;
caption: CaptionListItem;
overlayBackLink?: boolean;
}
export function TranslateSubtitleView({
id,
caption,
overlayBackLink,
}: LanguageSubtitlesViewProps) {
const { t } = useTranslation();
const router = useOverlayRouter(id);
return (
<>
<Menu.BackLink
onClick={() =>
router.navigate(
overlayBackLink
? "/captionsOverlay/languagesOverlay"
: "/captions/languages",
)
}
>
<span className="flex items-center">
<FlagIcon langCode={caption.language} />
<span className="ml-3">Translate from {caption.id}</span>
</span>
</Menu.BackLink>
<div className="!pt-1 mt-2 pb-3">
<div className="text-center text-video-context-type-secondary py-2">
{t("player.menus.subtitles.notFound")}
</div>
</div>
</>
);
}

View file

@ -176,7 +176,7 @@ async function decompressStr(byteArray: ArrayBuffer): Promise<string> {
const writer = cs.writable.getWriter();
writer.write(byteArray);
writer.close();
return new Response(cs.readable).arrayBuffer().then(function (arrayBuffer) {
return new Response(cs.readable).arrayBuffer().then((arrayBuffer) => {
return new TextDecoder().decode(arrayBuffer);
});
}