diff --git a/src/components/player/atoms/settings/CaptionsView.tsx b/src/components/player/atoms/settings/CaptionsView.tsx
index ab8c8102..f553de9d 100644
--- a/src/components/player/atoms/settings/CaptionsView.tsx
+++ b/src/components/player/atoms/settings/CaptionsView.tsx
@@ -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 =
downloading...
;
else if (downloadReq.error) downloadProgress = failed to download...
;
@@ -89,19 +69,43 @@ export function CaptionsView({ id }: { id: string }) {
let content: ReactNode = null;
if (req.loading) content = loading...
;
else if (req.error) content = errored!
;
- else if (req.value)
- content = req.value.map((v) => (
-
- startDownload(v.attributes.legacy_subtitle_id, v.attributes.language)
- }
- >
- {languageIdToName(v.attributes.language) ?? "unknown"}
-
- ));
+ 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 (
+
+ startDownload(
+ v.attributes.legacy_subtitle_id,
+ v.attributes.language
+ )
+ }
+ >
+ {v.languageName}
+
+ );
+ });
+ }
return (
<>
@@ -118,9 +122,10 @@ export function CaptionsView({ id }: { id: string }) {
>
Captions
-
+
+
{downloadProgress}
- disableCaption()} selected={!lang}>
+ disable()} selected={!lang}>
Off
{content}
diff --git a/src/components/player/atoms/settings/SettingsMenu.tsx b/src/components/player/atoms/settings/SettingsMenu.tsx
index 57313fcf..1ff5f9c2 100644
--- a/src/components/player/atoms/settings/SettingsMenu.tsx
+++ b/src/components/player/atoms/settings/SettingsMenu.tsx
@@ -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={
toggleSubtitles()}
+ onClick={() => toggleLastUsed().catch(() => {})}
/>
}
>
diff --git a/src/components/player/hooks/useCaptions.ts b/src/components/player/hooks/useCaptions.ts
new file mode 100644
index 00000000..77e773d3
--- /dev/null
+++ b/src/components/player/hooks/useCaptions.ts
@@ -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,
+ };
+}
diff --git a/src/components/player/internals/ContextMenu/Input.tsx b/src/components/player/internals/ContextMenu/Input.tsx
new file mode 100644
index 00000000..db404778
--- /dev/null
+++ b/src/components/player/internals/ContextMenu/Input.tsx
@@ -0,0 +1,21 @@
+import { Icon, Icons } from "@/components/Icon";
+
+export function Input(props: {
+ value: string;
+ onInput: (str: string) => void;
+}) {
+ return (
+
+
+ props.onInput(e.currentTarget.value)}
+ />
+
+ );
+}
diff --git a/src/components/player/internals/KeyboardEvents.tsx b/src/components/player/internals/KeyboardEvents.tsx
index ed29ce60..4b3e002d 100644
--- a/src/components/player/internals/KeyboardEvents.tsx
+++ b/src/components/player/internals/KeyboardEvents.tsx
@@ -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;
diff --git a/tailwind.config.js b/tailwind.config.js
index 24c3e35c..2542482b 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -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"
+ }
+ }
+ }
+ }
+ }
+ }
+ })
+ ]
};