mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-11 17:55:33 +00:00
Begin work on translate subtitle view
This commit is contained in:
parent
50216a10d9
commit
6bc4907399
7 changed files with 161 additions and 5 deletions
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue