Merge pull request #922 from marcoslor/868-add-appearance-preview

Add appearance preview
This commit is contained in:
William Oldham 2024-02-26 21:13:06 +00:00 committed by GitHub
commit 843ec84936
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 63 additions and 14 deletions

View File

@ -9,6 +9,7 @@ import {
} from "react"; } from "react";
import { SubtitleStyling } from "@/stores/subtitles"; import { SubtitleStyling } from "@/stores/subtitles";
import { usePreviewThemeStore } from "@/stores/theme";
export function useDerived<T>( export function useDerived<T>(
initial: T, initial: T,
@ -56,6 +57,11 @@ export function useSettingsState(
const [backendUrlState, setBackendUrl, resetBackendUrl, backendUrlChanged] = const [backendUrlState, setBackendUrl, resetBackendUrl, backendUrlChanged] =
useDerived(backendUrl); useDerived(backendUrl);
const [themeState, setTheme, resetTheme, themeChanged] = useDerived(theme); const [themeState, setTheme, resetTheme, themeChanged] = useDerived(theme);
const setPreviewTheme = usePreviewThemeStore((s) => s.setPreviewTheme);
const resetPreviewTheme = useCallback(
() => setPreviewTheme(theme),
[setPreviewTheme, theme],
);
const [ const [
appLanguageState, appLanguageState,
setAppLanguage, setAppLanguage,
@ -81,6 +87,7 @@ export function useSettingsState(
function reset() { function reset() {
resetTheme(); resetTheme();
resetPreviewTheme();
resetAppLanguage(); resetAppLanguage();
resetSubStyling(); resetSubStyling();
resetProxyUrls(); resetProxyUrls();

View File

@ -1,5 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use"; import { useAsyncFn } from "react-use";
@ -33,7 +33,7 @@ import { AccountWithToken, useAuthStore } from "@/stores/auth";
import { useLanguageStore } from "@/stores/language"; import { useLanguageStore } from "@/stores/language";
import { usePreferencesStore } from "@/stores/preferences"; import { usePreferencesStore } from "@/stores/preferences";
import { useSubtitleStore } from "@/stores/subtitles"; import { useSubtitleStore } from "@/stores/subtitles";
import { useThemeStore } from "@/stores/theme"; import { usePreviewThemeStore, useThemeStore } from "@/stores/theme";
import { SubPageLayout } from "./layouts/SubPageLayout"; import { SubPageLayout } from "./layouts/SubPageLayout";
import { PreferencesPart } from "./parts/settings/PreferencesPart"; import { PreferencesPart } from "./parts/settings/PreferencesPart";
@ -102,8 +102,10 @@ export function AccountSettings(props: {
export function SettingsPage() { export function SettingsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const activeTheme = useThemeStore((s) => s.theme); const activeTheme = useThemeStore((s) => s.theme) ?? "default";
const setTheme = useThemeStore((s) => s.setTheme); const setTheme = useThemeStore((s) => s.setTheme);
const previewTheme = usePreviewThemeStore((s) => s.previewTheme) ?? "default";
const setPreviewTheme = usePreviewThemeStore((s) => s.setPreviewTheme);
const appLanguage = useLanguageStore((s) => s.language); const appLanguage = useLanguageStore((s) => s.language);
const setAppLanguage = useLanguageStore((s) => s.setLanguage); const setAppLanguage = useLanguageStore((s) => s.setLanguage);
@ -144,6 +146,22 @@ export function SettingsPage() {
enableThumbnails, enableThumbnails,
); );
// Reset the preview theme when the settings page is unmounted
useEffect(
() => () => {
setPreviewTheme(null);
},
[setPreviewTheme],
);
const setThemeWithPreview = useCallback(
(v: string | null) => {
state.theme.set(v === "default" ? null : v);
setPreviewTheme(v);
},
[state.theme, setPreviewTheme],
);
const saveChanges = useCallback(async () => { const saveChanges = useCallback(async () => {
if (account && backendUrl) { if (account && backendUrl) {
if ( if (
@ -242,7 +260,11 @@ export function SettingsPage() {
/> />
</div> </div>
<div id="settings-appearance" className="mt-48"> <div id="settings-appearance" className="mt-48">
<ThemePart active={state.theme.state} setTheme={state.theme.set} /> <ThemePart
active={previewTheme}
inUse={activeTheme}
setTheme={setThemeWithPreview}
/>
</div> </div>
<div id="settings-captions" className="mt-48"> <div id="settings-captions" className="mt-48">
<CaptionsPart <CaptionsPart

View File

@ -10,13 +10,13 @@ export function BlurEllipsis(props: { positionClass?: string }) {
<div <div
className={classNames( className={classNames(
props.positionClass ?? "fixed", props.positionClass ?? "fixed",
"top-0 -right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentA blur-[100px] pointer-events-none opacity-25", "top-0 -right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentA blur-[100px] pointer-events-none opacity-25 transition-colors duration-75",
)} )}
/> />
<div <div
className={classNames( className={classNames(
props.positionClass ?? "fixed", props.positionClass ?? "fixed",
"top-0 right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentB blur-[100px] pointer-events-none opacity-25", "top-0 right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentB blur-[100px] pointer-events-none opacity-25 transition-colors duration-75",
)} )}
/> />
</> </>

View File

@ -5,6 +5,10 @@ import { Icon, Icons } from "@/components/Icon";
import { Heading1 } from "@/components/utils/Text"; import { Heading1 } from "@/components/utils/Text";
const availableThemes = [ const availableThemes = [
{
id: "default",
key: "settings.appearance.themes.default",
},
{ {
id: "blue", id: "blue",
key: "settings.appearance.themes.blue", key: "settings.appearance.themes.blue",
@ -26,6 +30,7 @@ const availableThemes = [
function ThemePreview(props: { function ThemePreview(props: {
selector?: string; selector?: string;
active?: boolean; active?: boolean;
inUse?: boolean;
name: string; name: string;
onClick?: () => void; onClick?: () => void;
}) { }) {
@ -105,7 +110,7 @@ function ThemePreview(props: {
<span <span
className={classNames( className={classNames(
"inline-block px-3 py-1 leading-tight text-sm transition-opacity duration-150 rounded-full bg-pill-activeBackground text-white/85", "inline-block px-3 py-1 leading-tight text-sm transition-opacity duration-150 rounded-full bg-pill-activeBackground text-white/85",
props.active ? "opacity-100" : "opacity-0 pointer-events-none", props.inUse ? "opacity-100" : "opacity-0 pointer-events-none",
)} )}
> >
{t("settings.appearance.activeTheme")} {t("settings.appearance.activeTheme")}
@ -117,6 +122,7 @@ function ThemePreview(props: {
export function ThemePart(props: { export function ThemePart(props: {
active: string | null; active: string | null;
inUse: string | null;
setTheme: (theme: string | null) => void; setTheme: (theme: string | null) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -126,16 +132,11 @@ export function ThemePart(props: {
<Heading1 border>{t("settings.appearance.title")}</Heading1> <Heading1 border>{t("settings.appearance.title")}</Heading1>
<div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-6 max-w-[700px]"> <div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-6 max-w-[700px]">
{/* default theme */} {/* default theme */}
<ThemePreview
name={t("settings.appearance.themes.default")}
selector="theme-default"
active={props.active === null}
onClick={() => props.setTheme(null)}
/>
{availableThemes.map((v) => ( {availableThemes.map((v) => (
<ThemePreview <ThemePreview
selector={`theme-${v.id}`} selector={`theme-${v.id}`}
active={props.active === v.id} active={props.active === v.id}
inUse={props.inUse === v.id}
name={t(v.key)} name={t(v.key)}
key={v.id} key={v.id}
onClick={() => props.setTheme(v.id)} onClick={() => props.setTheme(v.id)}

View File

@ -25,12 +25,31 @@ export const useThemeStore = create(
), ),
); );
export interface PreviewThemeStore {
previewTheme: string | null;
setPreviewTheme(v: string | null): void;
}
export const usePreviewThemeStore = create(
immer<PreviewThemeStore>((set) => ({
previewTheme: null,
setPreviewTheme(v) {
set((s) => {
s.previewTheme = v;
});
},
})),
);
export function ThemeProvider(props: { export function ThemeProvider(props: {
children?: ReactNode; children?: ReactNode;
applyGlobal?: boolean; applyGlobal?: boolean;
}) { }) {
const previewTheme = usePreviewThemeStore((s) => s.previewTheme);
const theme = useThemeStore((s) => s.theme); const theme = useThemeStore((s) => s.theme);
const themeSelector = theme ? `theme-${theme}` : undefined;
const themeToDisplay = previewTheme ?? theme;
const themeSelector = themeToDisplay ? `theme-${themeToDisplay}` : undefined;
return ( return (
<div className={themeSelector}> <div className={themeSelector}>