diff --git a/.vscode/settings.json b/.vscode/settings.json index cb78ef1f..77974714 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" } } diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index ad256aa0..d2a3aad1 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -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 = { reload: ``, repeat: ``, plus: ``, + translate: ``, }; export const Icon = memo((props: IconProps) => { diff --git a/src/components/player/atoms/Settings.tsx b/src/components/player/atoms/Settings.tsx index 355510c9..07042d2e 100644 --- a/src/components/player/atoms/Settings.tsx +++ b/src/components/player/atoms/Settings.tsx @@ -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(null); const [chosenLanguage, setChosenLanguage] = useState(null); + const [captionToTranslate, setCaptionToTranslate] = + useState(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 }) { + )} + + + + + {captionToTranslate && ( + )} @@ -138,7 +159,23 @@ function SettingsOverlay({ id }: { id: string }) { > {chosenLanguage && ( - + + )} + + + + + {captionToTranslate && ( + )} diff --git a/src/components/player/atoms/settings/CaptionsView.tsx b/src/components/player/atoms/settings/CaptionsView.tsx index 6681dbe4..b2f523f9 100644 --- a/src/components/player/atoms/settings/CaptionsView.tsx +++ b/src/components/player/atoms/settings/CaptionsView.tsx @@ -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 ; + } + + function translateBtn(margin: boolean) { + return ( + props.countryCode && ( + { + e.stopPropagation(); + props.onTranslate?.(); + }} + > + + + ) + ); + } + + if (props.selected || props.error) { + return ( +
+ {translateBtn(true)} + {props.error ? ( + + + + ) : ( + + )} +
+ ); + } + + return translateBtn(false); +} + +export function CaptionOption(props: CaptionOptionProps) { const [showTooltip, setShowTooltip] = useState(false); const tooltipTimeoutRef = useRef(null); const { t } = useTranslation(); @@ -110,6 +167,7 @@ export function CaptionOption(props: { error={props.error} onClick={props.onClick} onDoubleClick={props.onDoubleClick} + rightSide={} > 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} diff --git a/src/components/player/atoms/settings/TranslateSubtitleView.tsx b/src/components/player/atoms/settings/TranslateSubtitleView.tsx new file mode 100644 index 00000000..bd0d702b --- /dev/null +++ b/src/components/player/atoms/settings/TranslateSubtitleView.tsx @@ -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 ( + <> + + router.navigate( + overlayBackLink + ? "/captionsOverlay/languagesOverlay" + : "/captions/languages", + ) + } + > + + + Translate from {caption.id} + + + +
+
+ {t("player.menus.subtitles.notFound")} +
+
+ + ); +} diff --git a/src/components/player/utils/captionstranslation.ts b/src/components/player/utils/captionstranslation.ts index 85b729df..db0fc1bc 100644 --- a/src/components/player/utils/captionstranslation.ts +++ b/src/components/player/utils/captionstranslation.ts @@ -176,7 +176,7 @@ async function decompressStr(byteArray: ArrayBuffer): Promise { 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); }); }