settings modal

This commit is contained in:
frost768 2023-04-06 01:48:07 +03:00
parent c2b52d3db8
commit 9e961223f6
9 changed files with 228 additions and 20 deletions

View File

@ -37,7 +37,7 @@ export function Dropdown(props: DropdownProps) {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute bottom-11 left-0 right-0 z-10 mt-1 max-h-60 overflow-auto rounded-md bg-denim-500 py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-denim-400 scrollbar-thumb-denim-200 focus:outline-none sm:bottom-10 sm:text-sm"> <Listbox.Options className="absolute top-10 left-0 right-0 z-10 mt-1 max-h-60 overflow-auto rounded-md bg-denim-500 py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-denim-400 scrollbar-thumb-denim-200 focus:outline-none sm:top-10 sm:text-sm">
{props.options.map((opt) => ( {props.options.map((opt) => (
<Listbox.Option <Listbox.Option
className={({ active }) => className={({ active }) =>

View File

@ -35,9 +35,14 @@ export function Modal(props: Props) {
); );
} }
export function ModalCard(props: { children?: ReactNode }) { export function ModalCard(props: { className?: string; children?: ReactNode }) {
return ( return (
<div className="relative mx-2 max-w-[600px] overflow-hidden rounded-lg bg-denim-200 px-10 py-10"> <div
className={[
"relative mx-2 max-w-[600px] overflow-hidden rounded-lg bg-denim-200 px-10 py-10",
props.className,
].join(" ")}
>
{props.children} {props.children}
</div> </div>
); );

View File

@ -1,9 +1,10 @@
import { ReactNode } from "react"; import { ReactNode, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { IconPatch } from "@/components/buttons/IconPatch"; import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon"; import { Icons } from "@/components/Icon";
import { conf } from "@/setup/config"; import { conf } from "@/setup/config";
import { useBannerSize } from "@/hooks/useBanner"; import { useBannerSize } from "@/hooks/useBanner";
import SettingsModal from "@/views/SettingsModal";
import { BrandPill } from "./BrandPill"; import { BrandPill } from "./BrandPill";
export interface NavigationProps { export interface NavigationProps {
@ -13,7 +14,7 @@ export interface NavigationProps {
export function Navigation(props: NavigationProps) { export function Navigation(props: NavigationProps) {
const bannerHeight = useBannerSize(); const bannerHeight = useBannerSize();
const [showModal, setShowModal] = useState(false);
return ( return (
<div <div
className="fixed left-0 right-0 top-0 z-20 min-h-[150px] bg-gradient-to-b from-denim-300 via-denim-300 to-transparent sm:from-transparent" className="fixed left-0 right-0 top-0 z-20 min-h-[150px] bg-gradient-to-b from-denim-300 via-denim-300 to-transparent sm:from-transparent"
@ -42,6 +43,14 @@ export function Navigation(props: NavigationProps) {
props.children ? "hidden sm:flex" : "flex" props.children ? "hidden sm:flex" : "flex"
} relative flex-row gap-4`} } relative flex-row gap-4`}
> >
<IconPatch
className="text-2xl text-white"
icon={Icons.GEAR}
clickable
onClick={() => {
setShowModal(true);
}}
/>
<a <a
href={conf().DISCORD_LINK} href={conf().DISCORD_LINK}
target="_blank" target="_blank"
@ -60,6 +69,7 @@ export function Navigation(props: NavigationProps) {
</a> </a>
</div> </div>
</div> </div>
<SettingsModal show={showModal} onClose={() => setShowModal(false)} />
</div> </div>
); );
} }

View File

@ -99,6 +99,11 @@
"fatalError": "The video player encounted a fatal error, please report it to the <0>Discord server</0> or on <1>GitHub</1>." "fatalError": "The video player encounted a fatal error, please report it to the <0>Discord server</0> or on <1>GitHub</1>."
} }
}, },
"settings": {
"title": "Settings",
"language":"Language",
"captionLanguage": "Caption Language"
},
"v3": { "v3": {
"newSiteTitle": "New version now released!", "newSiteTitle": "New version now released!",
"newDomain": "https://movie-web.app", "newDomain": "https://movie-web.app",

View File

@ -1,14 +1,15 @@
import { useStore } from "@/utils/storage"; import { useStore } from "@/utils/storage";
import { createContext, ReactNode, useContext, useMemo } from "react"; import { createContext, ReactNode, useContext, useMemo } from "react";
import { LangCode } from "@/setup/iso6391";
import { SettingsStore } from "./store"; import { SettingsStore } from "./store";
import { MWSettingsData } from "./types"; import { MWSettingsData } from "./types";
interface MWSettingsDataSetters { interface MWSettingsDataSetters {
setLanguage(language: string): void; setCaptionLanguage(language: LangCode): void;
setCaptionDelay(delay: number): void; setCaptionDelay(delay: number): void;
setCaptionColor(color: string): void; setCaptionColor(color: string): void;
setCaptionFontSize(size: number): void; setCaptionFontSize(size: number): void;
setCaptionBackgroundColor(backgroundColor: string): void; setCaptionBackgroundColor(backgroundColor: number): void;
} }
type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters; type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters;
const SettingsContext = createContext<MWSettingsDataWrapper>(null as any); const SettingsContext = createContext<MWSettingsDataWrapper>(null as any);
@ -17,16 +18,15 @@ export function SettingsProvider(props: { children: ReactNode }) {
return Math.max(min, Math.min(value, max)); return Math.max(min, Math.min(value, max));
} }
const [settings, setSettings] = useStore(SettingsStore); const [settings, setSettings] = useStore(SettingsStore);
const context: MWSettingsDataWrapper = useMemo(() => { const context: MWSettingsDataWrapper = useMemo(() => {
const settingsContext: MWSettingsDataWrapper = { const settingsContext: MWSettingsDataWrapper = {
...settings, ...settings,
setLanguage(language) { setCaptionLanguage(language) {
setSettings((oldSettings) => { setSettings((oldSettings) => {
return { const captionSettings = oldSettings.captionSettings;
...oldSettings, captionSettings.language = language;
language, const newSettings = oldSettings;
}; return newSettings;
}); });
}, },
setCaptionDelay(delay: number) { setCaptionDelay(delay: number) {
@ -56,7 +56,10 @@ export function SettingsProvider(props: { children: ReactNode }) {
setCaptionBackgroundColor(backgroundColor) { setCaptionBackgroundColor(backgroundColor) {
setSettings((oldSettings) => { setSettings((oldSettings) => {
const style = oldSettings.captionSettings.style; const style = oldSettings.captionSettings.style;
style.backgroundColor = backgroundColor; style.backgroundColor = `${style.backgroundColor.substring(
0,
7
)}${backgroundColor.toString(16).padStart(2, "0")}`;
const newSettings = oldSettings; const newSettings = oldSettings;
return newSettings; return newSettings;
}); });

View File

@ -1,11 +1,11 @@
import { createVersionedStore } from "@/utils/storage"; import { createVersionedStore } from "@/utils/storage";
import { MWSettingsData } from "./types"; import { MWSettingsData, MWSettingsDataV1 } from "./types";
export const SettingsStore = createVersionedStore<MWSettingsData>() export const SettingsStore = createVersionedStore<MWSettingsData>()
.setKey("mw-settings") .setKey("mw-settings")
.addVersion({ .addVersion({
version: 0, version: 0,
create(): MWSettingsData { create(): MWSettingsDataV1 {
return { return {
language: "en", language: "en",
captionSettings: { captionSettings: {
@ -18,5 +18,29 @@ export const SettingsStore = createVersionedStore<MWSettingsData>()
}, },
}; };
}, },
migrate(data: MWSettingsDataV1): MWSettingsData {
return {
captionSettings: {
language: "none",
...data.captionSettings,
},
};
},
})
.addVersion({
version: 1,
create(): MWSettingsData {
return {
captionSettings: {
delay: 0,
language: "none",
style: {
color: "#ffffff",
fontSize: 25,
backgroundColor: "#00000096",
},
},
};
},
}) })
.build(); .build();

View File

@ -1,3 +1,5 @@
import { LangCode } from "@/setup/iso6391";
export interface CaptionStyleSettings { export interface CaptionStyleSettings {
color: string; color: string;
/** /**
@ -7,7 +9,7 @@ export interface CaptionStyleSettings {
backgroundColor: string; backgroundColor: string;
} }
export interface CaptionSettings { export interface CaptionSettingsV1 {
/** /**
* Range is [-10, 10]s * Range is [-10, 10]s
*/ */
@ -15,7 +17,19 @@ export interface CaptionSettings {
style: CaptionStyleSettings; style: CaptionStyleSettings;
} }
export interface CaptionSettings {
language: LangCode;
/**
* Range is [-10, 10]s
*/
delay: number;
style: CaptionStyleSettings;
}
export interface MWSettingsDataV1 {
language: LangCode;
captionSettings: CaptionSettingsV1;
}
export interface MWSettingsData { export interface MWSettingsData {
language: string;
captionSettings: CaptionSettings; captionSettings: CaptionSettings;
} }

View File

@ -8,7 +8,7 @@ import { useVideoPlayerDescriptor } from "../../state/hooks";
import { useProgress } from "../../state/logic/progress"; import { useProgress } from "../../state/logic/progress";
import { useSource } from "../../state/logic/source"; import { useSource } from "../../state/logic/source";
function CaptionCue({ text }: { text?: string }) { export function CaptionCue({ text, scale }: { text?: string; scale?: number }) {
const { captionSettings } = useSettings(); const { captionSettings } = useSettings();
const textWithNewlines = (text || "").replaceAll(/\r?\n/g, "<br />"); const textWithNewlines = (text || "").replaceAll(/\r?\n/g, "<br />");
@ -22,9 +22,14 @@ function CaptionCue({ text }: { text?: string }) {
return ( return (
<p <p
className="pointer-events-none mb-1 select-none rounded px-4 py-1 text-center [text-shadow:0_2px_4px_rgba(0,0,0,0.5)]" className={[
"pointer-events-none mb-1 select-none rounded px-4 py-1 text-center [text-shadow:0_2px_4px_rgba(0,0,0,0.5)]",
].join(" ")}
style={{ style={{
...captionSettings.style, ...captionSettings.style,
fontSize: !scale
? captionSettings.style.fontSize
: captionSettings.style.fontSize * scale,
}} }}
> >
<span <span

142
src/views/SettingsModal.tsx Normal file
View File

@ -0,0 +1,142 @@
import { Dropdown } from "@/components/Dropdown";
import { Icon, Icons } from "@/components/Icon";
import { Modal, ModalCard } from "@/components/layout/Modal";
import { useSettings } from "@/state/settings";
import { useTranslation } from "react-i18next";
import { CaptionCue } from "@/video/components/actions/CaptionRendererAction";
import { Slider } from "@/video/components/popouts/CaptionSettingsPopout";
import { appLanguageOptions } from "@/setup/i18n";
import { LangCode, captionLanguages } from "@/setup/iso6391";
import { useMemo } from "react";
export default function SettingsModal(props: {
onClose: () => void;
show: boolean;
}) {
const {
captionSettings,
setCaptionLanguage,
setCaptionBackgroundColor,
setCaptionColor,
setCaptionFontSize,
} = useSettings();
const { t, i18n } = useTranslation();
const colors = ["#ffffff", "#00ffff", "#ffff00"];
const selectedCaptionLanguage = useMemo(
() => captionLanguages.find((l) => l.id === captionSettings.language)!,
[captionSettings.language]
);
const captionBackgroundOpacity = (
(parseInt(captionSettings.style.backgroundColor.substring(7, 9), 16) /
255) *
100
).toFixed(0);
return (
<Modal show={props.show}>
<ModalCard className="max-w-[800px] bg-ash-300 text-white">
<div className="flex w-full flex-row justify-between">
<span className="text-xl font-bold">{t("settings.title")}</span>
<div onClick={() => props.onClose()} className="hover:cursor-pointer">
<Icon icon={Icons.X} />
</div>
</div>
<div className="flex justify-between gap-10 max-sm:flex-col">
<div className="flex flex-col justify-between">
<label className="text-md font-semibold">
{t("settings.language")}
</label>
<Dropdown
selectedItem={
appLanguageOptions.find((l) => l.id === i18n.language)!
}
setSelectedItem={(val) => {
i18n.changeLanguage(val.id);
}}
options={appLanguageOptions}
/>
</div>
<div className="flex flex-col justify-between">
<label className="text-md font-semibold">
{t("settings.captionLanguage")}
</label>
<Dropdown
selectedItem={selectedCaptionLanguage}
setSelectedItem={(val) => {
setCaptionLanguage(val.id as LangCode);
}}
options={captionLanguages}
/>
</div>
</div>
<div className="flex justify-between gap-10 rounded max-md:flex-col">
<div className="flex flex-col justify-between">
<Slider
label="Size"
min={14}
step={1}
max={60}
value={captionSettings.style.fontSize}
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
/>
<Slider
label={t("videoPlayer.popouts.captionPreferences.opacity")}
step={1}
min={0}
max={255}
valueDisplay={`${captionBackgroundOpacity}%`}
value={parseInt(
captionSettings.style.backgroundColor.substring(7, 9),
16
)}
onChange={(e) =>
setCaptionBackgroundColor(e.target.valueAsNumber)
}
/>
<div className="flex flex-row justify-between">
<label className="font-bold" htmlFor="color">
{t("videoPlayer.popouts.captionPreferences.color")}
</label>
<div className="flex flex-row gap-2">
{colors.map((color) => (
<div
className={`flex h-8 w-8 items-center justify-center rounded transition-[background-color,transform] duration-100 hover:bg-[#1c161b79] active:scale-110 ${
color === captionSettings.style.color
? "bg-[#1C161B]"
: ""
}`}
onClick={() => setCaptionColor(color)}
>
<div
className="h-4 w-4 cursor-pointer appearance-none rounded-full"
style={{
backgroundColor: color,
}}
/>
<Icon
className={[
"absolute text-xs text-[#1C161B]",
color === captionSettings.style.color ? "" : "hidden",
].join(" ")}
icon={Icons.CHECKMARK}
/>
</div>
))}
</div>
</div>
</div>
<div className="flex aspect-video h-[200px] flex-col justify-end rounded bg-zinc-800">
{selectedCaptionLanguage.id !== "none" ? (
<div className="pointer-events-none flex w-full flex-col items-center transition-[bottom]">
<CaptionCue
scale={0.4}
text={selectedCaptionLanguage.nativeName}
/>
</div>
) : null}
</div>
</div>
</ModalCard>
</Modal>
);
}