1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-29 16:07:40 +01:00

caption keyboard shortcut + searchbar for captions + enabled toggle for keyboard + subtitle padding

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-10-22 21:11:40 +02:00
parent ca2bab30a4
commit 9ce0e6a099
6 changed files with 179 additions and 88 deletions

View file

@ -1,17 +1,15 @@
import { ReactNode } from "react";
import Fuse from "fuse.js";
import { ReactNode, useState } from "react";
import { useAsync, useAsyncFn } from "react-use";
import {
downloadSrt,
languageIdToName,
searchSubtitles,
} from "@/backend/helpers/subs";
import { languageIdToName } from "@/backend/helpers/subs";
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 { usePlayerStore } from "@/stores/player/store";
import { useSubtitleStore } from "@/stores/subtitles";
export function CaptionOption(props: {
countryCode?: string;
@ -48,40 +46,22 @@ export function CaptionOption(props: {
}
// TODO cache like everything in this view
// TODO make quick settings for caption language
// TODO fix language names, some are unknown
// TODO add search bar for languages
// TODO sort languages by common usage
export function CaptionsView({ id }: { id: string }) {
const router = useOverlayRouter(id);
const setCaption = usePlayerStore((s) => s.setCaption);
const lang = usePlayerStore((s) => s.caption.selected?.language);
const setLanguage = useSubtitleStore((s) => s.setLanguage);
const meta = usePlayerStore((s) => s.meta);
const { search, download, disable } = useCaptions();
const req = useAsync(async () => {
if (!meta) throw new Error("No meta");
return searchSubtitles(meta);
}, [meta]);
const [searchQuery, setSearchQuery] = useState("");
const req = useAsync(async () => search(), [search]);
const [downloadReq, startDownload] = useAsyncFn(
async (subtitleId: string, language: string) => {
const srtData = await downloadSrt(subtitleId);
setCaption({
language,
srtData,
url: "", // TODO remove url
});
setLanguage(language);
},
[setCaption, setLanguage]
(subtitleId: string, language: string) => download(subtitleId, language),
[download]
);
function disableCaption() {
setCaption(null);
setLanguage(null);
}
let downloadProgress: ReactNode = null;
if (downloadReq.loading) downloadProgress = <p>downloading...</p>;
else if (downloadReq.error) downloadProgress = <p>failed to download...</p>;
@ -89,19 +69,43 @@ export function CaptionsView({ id }: { id: string }) {
let content: ReactNode = null;
if (req.loading) content = <p>loading...</p>;
else if (req.error) content = <p>errored!</p>;
else if (req.value)
content = req.value.map((v) => (
<CaptionOption
key={v.id}
countryCode={v.attributes.language}
selected={lang === v.attributes.language}
onClick={() =>
startDownload(v.attributes.legacy_subtitle_id, v.attributes.language)
}
>
{languageIdToName(v.attributes.language) ?? "unknown"}
</CaptionOption>
));
else if (req.value) {
const subs = req.value.map((v) => {
const languageName = languageIdToName(v.attributes.language) ?? "unknown";
return {
...v,
languageName,
};
});
let results = subs;
if (searchQuery.trim().length > 0) {
const fuse = new Fuse(subs, {
includeScore: true,
keys: ["languageName"],
});
results = fuse.search(searchQuery).map((res) => res.item);
}
content = results.map((v) => {
return (
<CaptionOption
key={v.id}
countryCode={v.attributes.language}
selected={lang === v.attributes.language}
onClick={() =>
startDownload(
v.attributes.legacy_subtitle_id,
v.attributes.language
)
}
>
{v.languageName}
</CaptionOption>
);
});
}
return (
<>
@ -118,9 +122,10 @@ export function CaptionsView({ id }: { id: string }) {
>
Captions
</Menu.BackLink>
<Menu.Section>
<Menu.Section className="pb-6">
<Input value={searchQuery} onInput={setSearchQuery} />
{downloadProgress}
<CaptionOption onClick={() => disableCaption()} selected={!lang}>
<CaptionOption onClick={() => disable()} selected={!lang}>
Off
</CaptionOption>
{content}

View file

@ -3,6 +3,7 @@ import { useMemo } from "react";
import { languageIdToName } from "@/backend/helpers/subs";
import { Toggle } from "@/components/buttons/Toggle";
import { Icon, Icons } from "@/components/Icon";
import { useCaptions } from "@/components/player/hooks/useCaptions";
import { Menu } from "@/components/player/internals/ContextMenu";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { usePlayerStore } from "@/stores/player/store";
@ -13,27 +14,16 @@ import { providers } from "@/utils/providers";
export function SettingsMenu({ id }: { id: string }) {
const router = useOverlayRouter(id);
const currentQuality = usePlayerStore((s) => s.currentQuality);
const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
const selectedCaptionLanguage = usePlayerStore(
(s) => s.caption.selected?.language
);
const subtitlesEnabled = useSubtitleStore((s) => s.enabled);
const setSubtitleLanguage = useSubtitleStore((s) => s.setLanguage);
const currentSourceId = usePlayerStore((s) => s.sourceId);
const setCaption = usePlayerStore((s) => s.setCaption);
const sourceName = useMemo(() => {
if (!currentSourceId) return "...";
return providers.getMetadata(currentSourceId)?.name ?? "...";
}, [currentSourceId]);
// TODO actually scrape subtitles to load
function toggleSubtitles() {
if (!subtitlesEnabled) setSubtitleLanguage(lastSelectedLanguage ?? "en");
else {
setSubtitleLanguage(null);
setCaption(null);
}
}
const { toggleLastUsed } = useCaptions();
const selectedLanguagePretty = selectedCaptionLanguage
? languageIdToName(selectedCaptionLanguage) ?? "unknown"
@ -77,7 +67,7 @@ export function SettingsMenu({ id }: { id: string }) {
rightSide={
<Toggle
enabled={subtitlesEnabled}
onClick={() => toggleSubtitles()}
onClick={() => toggleLastUsed().catch(() => {})}
/>
}
>

View file

@ -0,0 +1,63 @@
import { useCallback } from "react";
import { downloadSrt, searchSubtitles } from "@/backend/helpers/subs";
import { usePlayerStore } from "@/stores/player/store";
import { useSubtitleStore } from "@/stores/subtitles";
export function useCaptions() {
const setLanguage = useSubtitleStore((s) => s.setLanguage);
const enabled = useSubtitleStore((s) => s.enabled);
const setCaption = usePlayerStore((s) => s.setCaption);
const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
const meta = usePlayerStore((s) => s.meta);
const download = useCallback(
async (subtitleId: string, language: string) => {
const srtData = await downloadSrt(subtitleId);
setCaption({
language,
srtData,
url: "", // TODO remove url
});
setLanguage(language);
},
[setCaption, setLanguage]
);
const search = useCallback(async () => {
if (!meta) throw new Error("No meta");
return searchSubtitles(meta);
}, [meta]);
const disable = useCallback(async () => {
setCaption(null);
setLanguage(null);
}, [setCaption, setLanguage]);
const downloadLastUsed = useCallback(async () => {
const language = lastSelectedLanguage ?? "en";
const searchResult = await search();
const languageResult = searchResult.find(
(v) => v.attributes.language === language
);
if (!languageResult) return false;
await download(
languageResult.attributes.legacy_subtitle_id,
languageResult.attributes.language
);
return true;
}, [lastSelectedLanguage, search, download]);
const toggleLastUsed = useCallback(async () => {
if (!enabled) await downloadLastUsed();
else disable();
}, [downloadLastUsed, disable, enabled]);
return {
download,
search,
disable,
downloadLastUsed,
toggleLastUsed,
};
}

View file

@ -0,0 +1,21 @@
import { Icon, Icons } from "@/components/Icon";
export function Input(props: {
value: string;
onInput: (str: string) => void;
}) {
return (
<div className="w-full relative mb-6">
<Icon
className="pointer-events-none absolute top-1/2 left-3 transform -translate-y-1/2 text-video-context-inputPlaceholder"
icon={Icons.SEARCH}
/>
<input
placeholder="Search"
className="w-full py-2 px-3 pl-[calc(0.75rem+24px)] bg-video-context-inputBg rounded placeholder:text-video-context-inputPlaceholder"
value={props.value}
onInput={(e) => props.onInput(e.currentTarget.value)}
/>
</div>
);
}

View file

@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from "react";
import { useCaptions } from "@/components/player/hooks/useCaptions";
import { useVolume } from "@/components/player/hooks/useVolume";
import { usePlayerStore } from "@/stores/player/store";
import { useEmpheralVolumeStore } from "@/stores/volume";
@ -10,6 +11,7 @@ export function KeyboardEvents() {
const time = usePlayerStore((s) => s.progress.time);
const { setVolume, toggleMute } = useVolume();
const { toggleLastUsed } = useCaptions();
const setShowVolume = useEmpheralVolumeStore((s) => s.setShowVolume);
const [isRolling, setIsRolling] = useState(false);
@ -20,6 +22,7 @@ export function KeyboardEvents() {
setVolume,
toggleMute,
setIsRolling,
toggleLastUsed,
display,
mediaPlaying,
isRolling,
@ -31,6 +34,7 @@ export function KeyboardEvents() {
setVolume,
toggleMute,
setIsRolling,
toggleLastUsed,
display,
mediaPlaying,
isRolling,
@ -41,6 +45,7 @@ export function KeyboardEvents() {
setVolume,
toggleMute,
setIsRolling,
toggleLastUsed,
display,
mediaPlaying,
isRolling,
@ -49,6 +54,9 @@ export function KeyboardEvents() {
useEffect(() => {
const keyEventHandler = (evt: KeyboardEvent) => {
if (evt.target && (evt.target as HTMLInputElement).nodeName === "INPUT")
return;
const k = evt.key;
// Volume
@ -83,6 +91,9 @@ export function KeyboardEvents() {
dataRef.current.mediaPlaying.isPaused ? "play" : "pause"
]();
// captions
if (k === "c") dataRef.current.toggleLastUsed().catch(() => {}); // ignore errors
// Do a barrell roll!
if (k === "r") {
if (dataRef.current.isRolling || evt.ctrlKey || evt.metaKey) return;

View file

@ -26,23 +26,23 @@ module.exports = {
"ash-400": "#3D394D",
"ash-300": "#2C293A",
"ash-200": "#2B2836",
"ash-100": "#1E1C26",
"ash-100": "#1E1C26"
},
/* fonts */
fontFamily: {
"open-sans": "'Open Sans'",
"open-sans": "'Open Sans'"
},
/* animations */
keyframes: {
"loading-pin": {
"0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" },
"20%": { height: "1em", "background-color": "white" },
},
"20%": { height: "1em", "background-color": "white" }
}
},
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" },
},
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" }
}
},
plugins: [
require("tailwind-scrollbar"),
@ -52,31 +52,31 @@ module.exports = {
colors: {
// Branding
pill: {
background: "#1C1C36",
background: "#1C1C36"
},
// meta data for the theme itself
global: {
accentA: "#505DBD",
accentB: "#3440A1",
accentB: "#3440A1"
},
// light bar
lightBar: {
light: "#2A2A71",
light: "#2A2A71"
},
// Buttons
buttons: {
toggle: "#8D44D6",
toggleDisabled: "#202836",
toggleDisabled: "#202836"
},
// only used for body colors/textures
background: {
main: "#0A0A10",
accentA: "#6E3B80",
accentB: "#1F1F50",
accentB: "#1F1F50"
},
// typography
@ -85,7 +85,7 @@ module.exports = {
text: "#73739D",
dimmed: "#926CAD",
divider: "#262632",
secondary: "#64647B",
secondary: "#64647B"
},
// search bar
@ -94,7 +94,7 @@ module.exports = {
focused: "#24243C",
placeholder: "#4A4A71",
icon: "#545476",
text: "#FFFFFF",
text: "#FFFFFF"
},
// media cards
@ -106,7 +106,7 @@ module.exports = {
barColor: "#4B4B63",
barFillColor: "#BA7FD6",
badge: "#151522",
badgeText: "#5F5F7A",
badgeText: "#5F5F7A"
},
// video player
@ -118,17 +118,17 @@ module.exports = {
error: "#E44F4F",
success: "#40B44B",
loading: "#B759D8",
noresult: "#64647B",
noresult: "#64647B"
},
progress: {
background: "#8787A8",
preloaded: "#8787A8",
watched: "#A75FC9",
watched: "#A75FC9"
},
audio: {
set: "#A75FC9",
set: "#A75FC9"
},
buttons: {
@ -137,7 +137,7 @@ module.exports = {
secondaryHover: "#1B262E",
primary: "#fff",
primaryText: "#000",
primaryHover: "#dedede",
primaryHover: "#dedede"
},
context: {
@ -148,30 +148,31 @@ module.exports = {
buttonFocus: "#202836",
flagBg: "#202836",
inputBg: "#202836",
inputPlaceholder: "#374A56",
cardBorder: "#1B262E",
slider: "#8787A8",
sliderFilled: "#A75FC9",
download: {
button: "#6b298a",
hover: "#7f35a1",
hover: "#7f35a1"
},
buttons: {
list: "#161C26",
active: "#0D1317",
active: "#0D1317"
},
type: {
main: "#617A8A",
secondary: "#374A56",
accent: "#A570FA",
},
},
},
},
},
},
}),
],
accent: "#A570FA"
}
}
}
}
}
}
})
]
};