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

View file

@ -3,6 +3,7 @@ import { useMemo } from "react";
import { languageIdToName } from "@/backend/helpers/subs"; import { languageIdToName } from "@/backend/helpers/subs";
import { Toggle } from "@/components/buttons/Toggle"; import { Toggle } from "@/components/buttons/Toggle";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { useCaptions } from "@/components/player/hooks/useCaptions";
import { Menu } from "@/components/player/internals/ContextMenu"; import { Menu } from "@/components/player/internals/ContextMenu";
import { useOverlayRouter } from "@/hooks/useOverlayRouter"; import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";
@ -13,27 +14,16 @@ import { providers } from "@/utils/providers";
export function SettingsMenu({ id }: { id: string }) { export function SettingsMenu({ id }: { id: string }) {
const router = useOverlayRouter(id); const router = useOverlayRouter(id);
const currentQuality = usePlayerStore((s) => s.currentQuality); const currentQuality = usePlayerStore((s) => s.currentQuality);
const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
const selectedCaptionLanguage = usePlayerStore( const selectedCaptionLanguage = usePlayerStore(
(s) => s.caption.selected?.language (s) => s.caption.selected?.language
); );
const subtitlesEnabled = useSubtitleStore((s) => s.enabled); const subtitlesEnabled = useSubtitleStore((s) => s.enabled);
const setSubtitleLanguage = useSubtitleStore((s) => s.setLanguage);
const currentSourceId = usePlayerStore((s) => s.sourceId); const currentSourceId = usePlayerStore((s) => s.sourceId);
const setCaption = usePlayerStore((s) => s.setCaption);
const sourceName = useMemo(() => { const sourceName = useMemo(() => {
if (!currentSourceId) return "..."; if (!currentSourceId) return "...";
return providers.getMetadata(currentSourceId)?.name ?? "..."; return providers.getMetadata(currentSourceId)?.name ?? "...";
}, [currentSourceId]); }, [currentSourceId]);
const { toggleLastUsed } = useCaptions();
// TODO actually scrape subtitles to load
function toggleSubtitles() {
if (!subtitlesEnabled) setSubtitleLanguage(lastSelectedLanguage ?? "en");
else {
setSubtitleLanguage(null);
setCaption(null);
}
}
const selectedLanguagePretty = selectedCaptionLanguage const selectedLanguagePretty = selectedCaptionLanguage
? languageIdToName(selectedCaptionLanguage) ?? "unknown" ? languageIdToName(selectedCaptionLanguage) ?? "unknown"
@ -77,7 +67,7 @@ export function SettingsMenu({ id }: { id: string }) {
rightSide={ rightSide={
<Toggle <Toggle
enabled={subtitlesEnabled} 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 { useEffect, useRef, useState } from "react";
import { useCaptions } from "@/components/player/hooks/useCaptions";
import { useVolume } from "@/components/player/hooks/useVolume"; import { useVolume } from "@/components/player/hooks/useVolume";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";
import { useEmpheralVolumeStore } from "@/stores/volume"; import { useEmpheralVolumeStore } from "@/stores/volume";
@ -10,6 +11,7 @@ export function KeyboardEvents() {
const time = usePlayerStore((s) => s.progress.time); const time = usePlayerStore((s) => s.progress.time);
const { setVolume, toggleMute } = useVolume(); const { setVolume, toggleMute } = useVolume();
const { toggleLastUsed } = useCaptions();
const setShowVolume = useEmpheralVolumeStore((s) => s.setShowVolume); const setShowVolume = useEmpheralVolumeStore((s) => s.setShowVolume);
const [isRolling, setIsRolling] = useState(false); const [isRolling, setIsRolling] = useState(false);
@ -20,6 +22,7 @@ export function KeyboardEvents() {
setVolume, setVolume,
toggleMute, toggleMute,
setIsRolling, setIsRolling,
toggleLastUsed,
display, display,
mediaPlaying, mediaPlaying,
isRolling, isRolling,
@ -31,6 +34,7 @@ export function KeyboardEvents() {
setVolume, setVolume,
toggleMute, toggleMute,
setIsRolling, setIsRolling,
toggleLastUsed,
display, display,
mediaPlaying, mediaPlaying,
isRolling, isRolling,
@ -41,6 +45,7 @@ export function KeyboardEvents() {
setVolume, setVolume,
toggleMute, toggleMute,
setIsRolling, setIsRolling,
toggleLastUsed,
display, display,
mediaPlaying, mediaPlaying,
isRolling, isRolling,
@ -49,6 +54,9 @@ export function KeyboardEvents() {
useEffect(() => { useEffect(() => {
const keyEventHandler = (evt: KeyboardEvent) => { const keyEventHandler = (evt: KeyboardEvent) => {
if (evt.target && (evt.target as HTMLInputElement).nodeName === "INPUT")
return;
const k = evt.key; const k = evt.key;
// Volume // Volume
@ -83,6 +91,9 @@ export function KeyboardEvents() {
dataRef.current.mediaPlaying.isPaused ? "play" : "pause" dataRef.current.mediaPlaying.isPaused ? "play" : "pause"
](); ]();
// captions
if (k === "c") dataRef.current.toggleLastUsed().catch(() => {}); // ignore errors
// Do a barrell roll! // Do a barrell roll!
if (k === "r") { if (k === "r") {
if (dataRef.current.isRolling || evt.ctrlKey || evt.metaKey) return; if (dataRef.current.isRolling || evt.ctrlKey || evt.metaKey) return;

View file

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