Merge remote-tracking branch 'origin/main' into dev

This commit is contained in:
Captain Jack Sparrow 2024-05-29 18:10:06 +00:00
commit b6293b7942
11 changed files with 1817 additions and 770 deletions

File diff suppressed because it is too large Load Diff

View File

@ -368,6 +368,7 @@
"customChoice": "Drop or upload file",
"customizeLabel": "Customize",
"offChoice": "Off",
"OpenSubtitlesChoice": "OpenSubtitles",
"settings": {
"backlink": "Custom subtitles",
"delay": "Subtitle delay",
@ -375,7 +376,9 @@
},
"title": "Subtitles",
"unknownLanguage": "Unknown",
"dropSubtitleFile": "Drop subtitle file here! >_<"
"dropSubtitleFile": "Drop subtitle file here! >_<",
"scrapeButton": "Scrape subtitles",
"empty": "There are no provided subtitles for this."
}
},
"metadata": {

View File

@ -65,6 +65,7 @@ export enum Icons {
CIRCLE_QUESTION = "circle_question",
BRUSH = "brush",
UPLOAD = "upload",
WEB = "web",
}
export interface IconProps {
@ -136,6 +137,18 @@ const iconList: Record<Icons, string> = {
circle_question: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm169.8-90.7c7.9-22.3 29.1-37.3 52.8-37.3h58.3c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24V250.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1H222.6c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>`,
brush: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M162.4 6c-1.5-3.6-5-6-8.9-6h-19c-3.9 0-7.5 2.4-8.9 6L104.9 57.7c-3.2 8-14.6 8-17.8 0L66.4 6c-1.5-3.6-5-6-8.9-6H48C21.5 0 0 21.5 0 48V224v22.4V256H9.6 374.4 384v-9.6V224 48c0-26.5-21.5-48-48-48H230.5c-3.9 0-7.5 2.4-8.9 6L200.9 57.7c-3.2 8-14.6 8-17.8 0L162.4 6zM0 288v32c0 35.3 28.7 64 64 64h64v64c0 35.3 28.7 64 64 64s64-28.7 64-64V384h64c35.3 0 64-28.7 64-64V288H0zM192 432a16 16 0 1 1 0 32 16 16 0 1 1 0-32z" fill="currentColor"/></svg>`,
upload: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path opacity="1" fill="currentColor" d="M320 480H64c-17.7 0-32-14.3-32-32V64c0-17.7 14.3-32 32-32H192V144c0 26.5 21.5 48 48 48H352V448c0 17.7-14.3 32-32 32zM240 160c-8.8 0-16-7.2-16-16V32.5c2.8 .7 5.4 2.1 7.4 4.2L347.3 152.6c2.1 2.1 3.5 4.6 4.2 7.4H240zM64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V163.9c0-12.7-5.1-24.9-14.1-33.9L254.1 14.1c-9-9-21.2-14.1-33.9-14.1H64zM208 278.6l52.7 52.7c6.2 6.2 16.4 6.2 22.6 0s6.2-16.4 0-22.6l-80-80c-6.2-6.2-16.4-6.2-22.6 0l-80 80c-6.2 6.2-6.2 16.4 0 22.6s16.4 6.2 22.6 0L176 278.6V400c0 8.8 7.2 16 16 16s16-7.2 16-16V278.6z"/></svg>`,
web: `<svg xmlns="http://www.w3.org/2000/svg" width="1.4em" height="1.4em" viewBox="0 0 24 24" fill="none">
<path d="M15.3222 10.383C15.3796 10.9457 15.4125 11.4903 15.4125 12C15.4125 12.9541 15.2972 14.0315 15.1208 15.1208C14.0315 15.2972 12.9541 15.4125 12 15.4125C11.0502 15.4125 9.97313 15.2975 8.87911 15.1205C8.70281 14.0312 8.5875 12.954 8.5875 12C8.5875 11.4905 8.62039 10.9458 8.67789 10.383C9.82608 10.5668 10.9715 10.6875 12 10.6875C13.0286 10.6875 14.174 10.5668 15.3222 10.383Z" fill="currentColor"/>
<path d="M16.8752 10.0994C16.9462 10.7579 16.9875 11.399 16.9875 12C16.9875 12.8769 16.8997 13.8389 16.7599 14.8153C18.7425 14.4016 20.575 13.8731 21.5567 13.5722C21.8739 13.475 21.9986 13.4363 22.1658 13.3694C22.2494 13.336 22.326 13.302 22.4259 13.2543C22.4748 12.843 22.5 12.4244 22.5 12C22.5 10.878 22.324 9.79714 21.9982 8.78346L21.9133 8.81017C20.8868 9.12245 18.9652 9.6745 16.8752 10.0994Z" fill="currentColor"/>
<path d="M21.4017 7.31948C20.3698 7.63221 18.579 8.14039 16.6599 8.53603C16.2178 5.84926 15.443 3.16951 15.0702 1.95598C17.8422 2.80227 20.1273 4.76467 21.4017 7.31948Z" fill="currentColor"/>
<path d="M15.1117 8.82229C14.0253 8.99781 12.9513 9.1125 12 9.1125C11.0487 9.1125 9.97477 8.99781 8.88843 8.8223C9.30471 6.28005 10.0478 3.68306 10.4278 2.44333C10.525 2.12606 10.5637 2.00144 10.6306 1.83418C10.664 1.75062 10.698 1.67398 10.7457 1.57414C11.157 1.52518 11.5756 1.5 12 1.5C12.4434 1.5 12.8803 1.52748 13.3093 1.58083C13.3184 1.61564 13.3268 1.64679 13.3351 1.67626C13.3597 1.76333 13.3982 1.8857 13.4628 2.09104L13.4696 2.11261C13.7935 3.14223 14.6519 6.01401 15.1117 8.82229Z" fill="currentColor"/>
<path d="M7.34004 8.536C7.7801 5.86107 8.54986 3.19576 8.92192 1.98181L8.92983 1.95597C6.15777 2.80225 3.8727 4.76465 2.59835 7.31946C3.63018 7.63219 5.42095 8.14036 7.34004 8.536Z" fill="currentColor"/>
<path d="M2.00184 8.78345C1.67598 9.79714 1.5 10.878 1.5 12C1.5 12.4389 1.52693 12.8715 1.57923 13.2963L1.74471 13.3515L1.74603 13.3519L1.74765 13.3525L1.74879 13.3528C1.80205 13.3705 3.36305 13.886 5.41878 14.3975C5.99886 14.5418 6.61307 14.6844 7.24006 14.8151C7.10025 13.8388 7.0125 12.8769 7.0125 12C7.0125 11.3988 7.05374 10.7577 7.12472 10.0994C5.03428 9.67436 3.11218 9.12212 2.08597 8.80989L2.07883 8.80771L2.00184 8.78345Z" fill="currentColor"/>
<path d="M12 16.9875C12.8769 16.9875 13.8389 16.8997 14.8153 16.7599C14.4016 18.7425 13.8731 20.575 13.5722 21.5566C13.475 21.8739 13.4363 21.9985 13.3694 22.1658C13.336 22.2494 13.302 22.326 13.2543 22.4259C12.843 22.4748 12.4244 22.5 12 22.5C11.5756 22.5 11.157 22.4748 10.7457 22.4259C10.698 22.326 10.664 22.2494 10.6306 22.1658C10.5637 21.9986 10.525 21.8739 10.4278 21.5567C10.1269 20.5751 9.59846 18.7427 9.18478 16.7603C10.1579 16.8996 11.1201 16.9875 12 16.9875Z" fill="currentColor"/>
<path d="M5.0385 15.9259C3.73853 15.6024 2.63135 15.2775 1.95597 15.0702C2.97258 18.4002 5.59982 21.0274 8.92983 22.044L8.92192 22.0182C8.59705 20.9582 7.96897 18.7917 7.52191 16.4784C6.6525 16.3103 5.80722 16.1171 5.0385 15.9259Z" fill="currentColor"/>
<path d="M22.0182 15.0781C20.9582 15.403 18.7915 16.0311 16.4781 16.4781C16.0311 18.7915 15.403 20.9581 15.0781 22.0182L15.0702 22.044C18.4002 21.0274 21.0274 18.4002 22.044 15.0702L22.0182 15.0781Z" fill="currentColor"/>
<path d="M1.6103 13.323C1.64665 13.3277 1.67628 13.3327 1.68611 13.3349C1.69472 13.337 1.70821 13.3406 1.7131 13.3419L1.72391 13.345L1.72973 13.3468L1.73585 13.3487L1.74098 13.3503C1.7381 13.3494 1.67976 13.3348 1.6103 13.323Z" fill="currentColor"/>
</svg>`,
};
function ChromeCastButton() {

View File

@ -18,6 +18,7 @@ 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 { PlaybackSettingsView } from "./settings/PlaybackSettingsView";
import { QualityView } from "./settings/QualityView";
import { SettingsMenu } from "./settings/SettingsMenu";
@ -57,6 +58,16 @@ function SettingsOverlay({ id }: { id: string }) {
<CaptionsView id={id} />
</Menu.CardWithScrollable>
</OverlayPage>
<OverlayPage
id={id}
path="/captions/opensubtitles"
width={343}
height={431}
>
<Menu.Card>
<OpenSubtitlesCaptionView id={id} />
</Menu.Card>
</OverlayPage>
<OverlayPage id={id} path="/captions/settings" width={343} height={450}>
<Menu.Card>
<CaptionSettingsView id={id} />

View File

@ -29,6 +29,7 @@ export function CaptionOption(props: {
loading?: boolean;
onClick?: () => void;
error?: React.ReactNode;
chevron?: boolean;
}) {
return (
<SelectableLink
@ -36,6 +37,7 @@ export function CaptionOption(props: {
loading={props.loading}
error={props.error}
onClick={props.onClick}
chevron={props.chevron}
>
<span
data-active-link={props.selected ? true : undefined}
@ -93,11 +95,13 @@ function useSubtitleList(subs: CaptionListItem[], searchQuery: string) {
const { t: translate } = useTranslation();
const unknownChoice = translate("player.menus.subtitles.unknownLanguage");
return useMemo(() => {
const input = subs.map((t) => ({
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);
@ -231,8 +235,15 @@ export function CaptionsView({ id }: { id: string }) {
}}
onDrop={(event) => onDrop(event)}
>
<div className="mt-3">
<div className="mt-3 flex flex-row gap-2">
<Input value={searchQuery} onInput={setSearchQuery} />
<button
type="button"
onClick={() => router.navigate("/captions/opensubtitles")}
className="p-[0.5em] rounded tabbable hover:bg-video-context-hoverColor hover:bg-opacity-50"
>
<Icon icon={Icons.WEB} />
</button>
</div>
<Menu.ScrollToActiveSection className="!pt-1 mt-2 pb-3">
<CaptionOption
@ -242,7 +253,22 @@ export function CaptionsView({ id }: { id: string }) {
{t("player.menus.subtitles.offChoice")}
</CaptionOption>
<CustomCaptionOption />
{content}
{content.length === 0 ? (
<div className="p-4 rounded-xl bg-video-context-light bg-opacity-10 font-medium text-center">
<div className="flex flex-col items-center justify-center gap-3">
{t("player.menus.subtitles.empty")}
<button
type="button"
onClick={() => router.navigate("/captions/opensubtitles")}
className="p-1 w-3/4 rounded tabbable duration-200 bg-opacity-10 bg-video-context-light hover:bg-opacity-20"
>
{t("player.menus.subtitles.scrapeButton")}
</button>
</div>
</div>
) : (
content
)}
</Menu.ScrollToActiveSection>
</FileDropHandler>
</>

View File

@ -0,0 +1,141 @@
import Fuse from "fuse.js";
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";
import {
getPrettyLanguageNameFromLocale,
sortLangCodes,
} from "@/utils/language";
export function CaptionOption(props: {
countryCode?: string;
children: React.ReactNode;
selected?: boolean;
loading?: boolean;
onClick?: () => void;
error?: React.ReactNode;
}) {
return (
<SelectableLink
selected={props.selected}
loading={props.loading}
error={props.error}
onClick={props.onClick}
>
<span
data-active-link={props.selected ? true : undefined}
className="flex items-center"
>
<span data-code={props.countryCode} className="mr-3 inline-flex">
<FlagIcon langCode={props.countryCode} />
</span>
<span>{props.children}</span>
</span>
</SelectableLink>
);
}
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 OpenSubtitlesCaptionView({ id }: { id: string }) {
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, searchQuery);
const [downloadReq, startDownload] = useAsyncFn(
async (captionId: string) => {
setCurrentlyDownloading(captionId);
return selectCaptionById(captionId);
},
[selectCaptionById, setCurrentlyDownloading],
);
const content = subtitleList.map((v) => {
return (
<CaptionOption
// key must use index to prevent url collisions
key={v.id}
countryCode={v.language}
selected={v.id === selectedCaptionId}
loading={v.id === currentlyDownloading && downloadReq.loading}
error={
v.id === currentlyDownloading && downloadReq.error
? downloadReq.error.toString()
: undefined
}
onClick={() => startDownload(v.id)}
>
{v.languageName}
</CaptionOption>
);
});
return (
<>
<div>
<Menu.BackLink onClick={() => router.navigate("/captions")}>
{t("player.menus.subtitles.OpenSubtitlesChoice")}
</Menu.BackLink>
</div>
<div className="mt-3">
<Input value={searchQuery} onInput={setSearchQuery} />
</div>
<Menu.ScrollToActiveSection className="!pt-1 mt-2 pb-3">
{content}
</Menu.ScrollToActiveSection>
</>
);
}
export default OpenSubtitlesCaptionView;

View File

@ -19,6 +19,7 @@ export function useCaptions() {
);
const setCaption = usePlayerStore((s) => s.setCaption);
const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
const setIsOpenSubtitles = useSubtitleStore((s) => s.setIsOpenSubtitles);
const captionList = usePlayerStore((s) => s.captionList);
const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList);
@ -77,11 +78,13 @@ export function useCaptions() {
captionToSet.srtData = srtData;
}
setIsOpenSubtitles(!!caption.opensubtitles);
setCaption(captionToSet);
resetSubtitleSpecificSettings();
setLanguage(caption.language);
},
[
setIsOpenSubtitles,
setLanguage,
captions,
setCaption,
@ -101,9 +104,10 @@ export function useCaptions() {
);
const disable = useCallback(async () => {
setIsOpenSubtitles(false);
setCaption(null);
setLanguage(null);
}, [setCaption, setLanguage]);
}, [setCaption, setLanguage, setIsOpenSubtitles]);
const selectLastUsedLanguage = useCallback(async () => {
const language = lastSelectedLanguage ?? "en";

View File

@ -123,9 +123,24 @@ export function SelectableLink(props: {
children?: ReactNode;
disabled?: boolean;
error?: ReactNode;
chevron?: boolean;
}) {
let rightContent;
if (props.selected) {
if (props.chevron) {
rightContent = (
<span className="flex items-center">
<Icon
icon={Icons.CIRCLE_CHECK}
className="text-xl text-video-context-type-accent"
/>
<Icon
className="text-white text-xl ml-1 -mr-1.5"
icon={Icons.CHEVRON_RIGHT}
/>
</span>
);
} else {
rightContent = (
<Icon
icon={Icons.CIRCLE_CHECK}
@ -133,6 +148,11 @@ export function SelectableLink(props: {
/>
);
}
} else if (props.chevron) {
rightContent = (
<Icon className="text-xl ml-1 -mr-1.5" icon={Icons.CHEVRON_RIGHT} />
);
}
if (props.error)
rightContent = (
<span className="flex items-center text-video-context-error">

View File

@ -102,5 +102,6 @@ export function convertProviderCaption(
language: v.language,
url: v.url,
needsProxy: v.hasCorsRestrictions,
opensubtitles: v.opensubtitles,
}));
}

View File

@ -54,6 +54,7 @@ export interface CaptionListItem {
url: string;
needsProxy: boolean;
hls?: boolean;
opensubtitles?: boolean;
}
export interface AudioTrack {

View File

@ -36,11 +36,13 @@ export interface SubtitleStore {
};
enabled: boolean;
lastSelectedLanguage: string | null;
isOpenSubtitles: boolean;
styling: SubtitleStyling;
overrideCasing: boolean;
delay: number;
updateStyling(newStyling: Partial<SubtitleStyling>): void;
setLanguage(language: string | null): void;
setIsOpenSubtitles(isOpenSubtitles: boolean): void;
setCustomSubs(): void;
setOverrideCasing(enabled: boolean): void;
setDelay(delay: number): void;
@ -56,6 +58,7 @@ export const useSubtitleStore = create(
lastSelectedLanguage: null,
},
lastSelectedLanguage: null,
isOpenSubtitles: false,
overrideCasing: false,
delay: 0,
styling: {
@ -96,6 +99,11 @@ export const useSubtitleStore = create(
if (lang) s.lastSelectedLanguage = lang;
});
},
setIsOpenSubtitles(isOpenSubtitles) {
set((s) => {
s.isOpenSubtitles = isOpenSubtitles;
});
},
setCustomSubs() {
set((s) => {
s.enabled = true;