diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx index 07228d48..347d1f01 100644 --- a/src/context/Theme.tsx +++ b/src/context/Theme.tsx @@ -7,6 +7,8 @@ import { useEffect } from "preact/hooks"; import { connectState } from "../redux/connector"; import { Children } from "../types/Preact"; +import { fetchManifest, fetchTheme } from "../pages/settings/panes/ThemeShop"; +import { getState } from "../redux"; export type Variables = | "accent" @@ -72,7 +74,7 @@ export type Theme = { }; export interface ThemeOptions { - preset?: string; + base?: string; ligatures?: boolean; custom?: Partial; } @@ -275,6 +277,27 @@ export const PRESETS: Record = { }, }; +// todo: store used themes locally +export function getBaseTheme(name: string): Theme { + if (name in PRESETS) { + return PRESETS[name] + } + + const themes = getState().themes + + if (name in themes) { + const { theme } = themes[name]; + + return { + ...PRESETS[theme.light ? 'light' : 'dark'], + ...theme + } + } + + // how did we get here + return PRESETS['dark'] +} + const keys = Object.keys(PRESETS.dark); const GlobalTheme = createGlobalStyle<{ theme: Theme }>` :root { @@ -283,10 +306,9 @@ const GlobalTheme = createGlobalStyle<{ theme: Theme }>` `; export const generateVariables = (theme: Theme) => { - const mergedTheme = { ...PRESETS[theme.light ? 'light' : 'dark'], ...theme } - return (Object.keys(mergedTheme) as Variables[]).map((key) => { + return (Object.keys(theme) as Variables[]).map((key) => { if (!keys.includes(key)) return; - return `--${key}: ${mergedTheme[key]};`; + return `--${key}: ${theme[key]};`; }) } @@ -300,8 +322,7 @@ interface Props { function Theme({ children, options }: Props) { const theme: Theme = { - ...PRESETS["dark"], - ...PRESETS[options?.preset ?? ""], + ...getBaseTheme(options?.base ?? 'dark'), ...options?.custom, }; diff --git a/src/pages/settings/panes/Appearance.tsx b/src/pages/settings/panes/Appearance.tsx index bd61029b..6d060e74 100644 --- a/src/pages/settings/panes/Appearance.tsx +++ b/src/pages/settings/panes/Appearance.tsx @@ -98,7 +98,7 @@ export function Component(props: Props) { useEffect(() => setOverride({ css }), [setOverride, css]); - const selected = props.settings.theme?.preset ?? "dark"; + const selected = props.settings.theme?.base ?? "dark"; return (

@@ -113,7 +113,7 @@ export function Component(props: Props) { data-active={selected === "light"} onClick={() => selected !== "light" && - setTheme({ preset: "light" }) + setTheme({ base: "light" }) } onContextMenu={(e) => e.preventDefault()} /> @@ -128,7 +128,7 @@ export function Component(props: Props) { draggable={false} data-active={selected === "dark"} onClick={() => - selected !== "dark" && setTheme({ preset: "dark" }) + selected !== "dark" && setTheme({ base: "dark" }) } onContextMenu={(e) => e.preventDefault()} /> diff --git a/src/pages/settings/panes/ThemeShop.tsx b/src/pages/settings/panes/ThemeShop.tsx index a23dcc93..47a4db79 100644 --- a/src/pages/settings/panes/ThemeShop.tsx +++ b/src/pages/settings/panes/ThemeShop.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "preact/hooks"; import { dispatch } from "../../../redux"; -import { Theme, generateVariables } from "../../../context/Theme"; +import { Theme, generateVariables, ThemeOptions } from "../../../context/Theme"; import Tip from "../../../components/ui/Tip"; import previewPath from "../assets/preview.svg"; @@ -21,14 +21,14 @@ export const fetchTheme = (slug: string): Promise => res.json(), ); -interface ThemeMetadata { +export interface ThemeMetadata { name: string; creator: string; commit?: string; description: string; } -type Manifest = { +export type Manifest = { generated: string; themes: Record; }; @@ -189,14 +189,21 @@ export function ThemeShop() {
{theme.description}
diff --git a/src/redux/index.ts b/src/redux/index.ts index 3edab32a..2e5b7d84 100644 --- a/src/redux/index.ts +++ b/src/redux/index.ts @@ -14,6 +14,7 @@ import { QueuedMessage } from "./reducers/queue"; import { SectionToggle } from "./reducers/section_toggle"; import { Settings } from "./reducers/settings"; import { SyncOptions } from "./reducers/sync"; +import { Themes } from "./reducers/themes"; import { TrustedLinks } from "./reducers/trusted_links"; import { Unreads } from "./reducers/unreads"; @@ -31,6 +32,7 @@ export type State = { notifications: Notifications; sectionToggle: SectionToggle; trustedLinks: TrustedLinks; + themes: Themes; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -62,6 +64,7 @@ store.subscribe(() => { notifications, sectionToggle, trustedLinks, + themes, } = store.getState() as State; localForage.setItem("state", { @@ -78,6 +81,7 @@ store.subscribe(() => { notifications, sectionToggle, trustedLinks, + themes, }); }); diff --git a/src/redux/reducers/index.ts b/src/redux/reducers/index.ts index c7040552..aa8be8f4 100644 --- a/src/redux/reducers/index.ts +++ b/src/redux/reducers/index.ts @@ -12,6 +12,7 @@ import { sectionToggle, SectionToggleAction } from "./section_toggle"; import { config, ConfigAction } from "./server_config"; import { settings, SettingsAction } from "./settings"; import { sync, SyncAction } from "./sync"; +import { themes, ThemesAction } from "./themes"; import { trustedLinks, TrustedLinksAction } from "./trusted_links"; import { unreads, UnreadsAction } from "./unreads"; @@ -29,6 +30,7 @@ export default combineReducers({ notifications, sectionToggle, trustedLinks, + themes, }); export type Action = @@ -45,4 +47,5 @@ export type Action = | NotificationsAction | SectionToggleAction | TrustedLinksAction + | ThemesAction | { type: "__INIT"; state: State }; diff --git a/src/redux/reducers/themes.ts b/src/redux/reducers/themes.ts new file mode 100644 index 00000000..89b9c840 --- /dev/null +++ b/src/redux/reducers/themes.ts @@ -0,0 +1,33 @@ +import { Theme } from "../../context/Theme"; + +import { ThemeMetadata } from "../../pages/settings/panes/ThemeShop"; + +export interface StoredTheme { + slug: string; + meta: ThemeMetadata; + theme: Theme; +} + +export type Themes = Record; + +export type ThemesAction = + | { type: undefined } + | { type: "THEMES_SET_THEME"; theme: StoredTheme } + | { type: "THEMES_REMOVE_THEME"; slug: string } + | { type: "RESET" }; + +export function themes(state: Themes = {}, action: ThemesAction) { + switch (action.type) { + case "THEMES_SET_THEME": + return { + ...state, + [action.theme.slug]: action.theme, + }; + case "THEMES_REMOVE_THEME": + return { ...state, [action.slug]: null }; + case "RESET": + return {}; + default: + return state; + } +}