diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index ff1cef8c..a41f5374 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -36,6 +36,7 @@ export enum Icons { CASTING = "casting", CIRCLE_EXCLAMATION = "circle_exclamation", DOWNLOAD = "download", + SETTINGS = "settings", } export interface IconProps { @@ -79,6 +80,7 @@ const iconList: Record = { circle_exclamation: ``, casting: "", download: ``, + settings: ``, }; function ChromeCastButton() { diff --git a/src/setup/App.tsx b/src/setup/App.tsx index e82f57d7..7b197cfd 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -1,6 +1,7 @@ import { Redirect, Route, Switch } from "react-router-dom"; import { BookmarkContextProvider } from "@/state/bookmark"; import { WatchedContextProvider } from "@/state/watched"; +import { SettingsProvider } from "@/state/settings"; import { NotFoundPage } from "@/views/notfound/NotFoundView"; import { MediaView } from "@/views/media/MediaView"; @@ -11,50 +12,54 @@ import { DeveloperView } from "@/views/developer/DeveloperView"; import { VideoTesterView } from "@/views/developer/VideoTesterView"; import { ProviderTesterView } from "@/views/developer/ProviderTesterView"; import { EmbedTesterView } from "@/views/developer/EmbedTesterView"; +import { SettingsView } from "@/views/settings/SettingsView"; import { BannerContextProvider } from "@/hooks/useBanner"; import { Layout } from "@/setup/Layout"; function App() { return ( - - - - - - {/* functional routes */} - - - - + + + + + + + {/* functional routes */} + + + + - {/* pages */} - - - + {/* pages */} + + + + - {/* other */} - - - - - - - - - - + {/* other */} + + + + + + + + + + + ); } diff --git a/src/state/settings/context.tsx b/src/state/settings/context.tsx new file mode 100644 index 00000000..f0f19057 --- /dev/null +++ b/src/state/settings/context.tsx @@ -0,0 +1,100 @@ +import { useStore } from "@/utils/storage"; +import { createContext, ReactNode, useContext, useMemo } from "react"; +import { SettingsStore } from "./store"; +import { MWSettingsData } from "./types"; + +interface MWSettingsDataSetters { + setLanguage(language: string): void; + setCaptionDelay(delay: number): void; + setCaptionColor(color: string): void; + setCaptionFontSize(size: number): void; + setCaptionFontFamily(fontFamily: string): void; + setCaptionTextShadow(textShadow: string): void; + setCaptionBackgroundColor(backgroundColor: string): void; +} +type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters; +const SettingsContext = createContext(null as any); +export function SettingsProvider(props: { children: ReactNode }) { + function enforceRange(min: number, value: number, max: number) { + return Math.max(min, Math.min(value, max)); + } + const [settings, setSettings] = useStore(SettingsStore); + + const context: MWSettingsDataWrapper = useMemo(() => { + const settingsContext: MWSettingsDataWrapper = { + ...settings, + setLanguage(language) { + setSettings((oldSettings) => { + return { + ...oldSettings, + language, + }; + }); + }, + setCaptionDelay(delay: number) { + setSettings((oldSettings) => { + const captionSettings = oldSettings.captionSettings; + captionSettings.delay = enforceRange( + -10 * 1000, + delay / 1000, + 10 * 1000 + ); + const newSettings = oldSettings; + return newSettings; + }); + }, + setCaptionColor(color) { + setSettings((oldSettings) => { + const style = oldSettings.captionSettings.style; + style.color = color; + const newSettings = oldSettings; + return newSettings; + }); + }, + setCaptionFontSize(size) { + setSettings((oldSettings) => { + const style = oldSettings.captionSettings.style; + style.fontSize = enforceRange(10, size, 30); + const newSettings = oldSettings; + return newSettings; + }); + }, + setCaptionFontFamily(fontFamily) { + setSettings((oldSettings) => { + const captionStyle = oldSettings.captionSettings.style; + captionStyle.fontFamily = fontFamily; + const newSettings = oldSettings; + return newSettings; + }); + }, + setCaptionTextShadow(textShadow) { + setSettings((oldSettings) => { + const captionStyle = oldSettings.captionSettings.style; + captionStyle.textShadow = textShadow; + const newSettings = oldSettings; + return newSettings; + }); + }, + setCaptionBackgroundColor(backgroundColor) { + setSettings((oldSettings) => { + const style = oldSettings.captionSettings.style; + style.backgroundColor = backgroundColor; + const newSettings = oldSettings; + return newSettings; + }); + }, + }; + return settingsContext; + }, [settings, setSettings]); + return ( + + {props.children} + + ); +} + +export function useSettings() { + return useContext(SettingsContext); +} + +export default SettingsContext; diff --git a/src/state/settings/index.ts b/src/state/settings/index.ts new file mode 100644 index 00000000..2edd280c --- /dev/null +++ b/src/state/settings/index.ts @@ -0,0 +1 @@ +export * from "./context"; diff --git a/src/state/settings/store.ts b/src/state/settings/store.ts new file mode 100644 index 00000000..e71647d5 --- /dev/null +++ b/src/state/settings/store.ts @@ -0,0 +1,24 @@ +import { createVersionedStore } from "@/utils/storage"; +import { MWSettingsData } from "./types"; + +export const SettingsStore = createVersionedStore() + .setKey("mw-settings") + .addVersion({ + version: 0, + create() { + return { + language: "en", + captionSettings: { + delay: 0, + style: { + color: "white", + fontSize: 20, + fontFamily: "inherit", + textShadow: "2px 2px 2px black", + backgroundColor: "black", + }, + }, + } as MWSettingsData; + }, + }) + .build(); diff --git a/src/state/settings/types.ts b/src/state/settings/types.ts new file mode 100644 index 00000000..9107d955 --- /dev/null +++ b/src/state/settings/types.ts @@ -0,0 +1,23 @@ +export interface CaptionStyleSettings { + color: string; + /** + * Range is [10, 30] + */ + fontSize: number; + fontFamily: string; + textShadow: string; + backgroundColor: string; +} + +export interface CaptionSettings { + /** + * Range is [-10, 10]s + */ + delay: number; + style: CaptionStyleSettings; +} + +export interface MWSettingsData { + language: string; + captionSettings: CaptionSettings; +}