diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx index a2fbd2d1..07228d48 100644 --- a/src/context/Theme.tsx +++ b/src/context/Theme.tsx @@ -278,14 +278,18 @@ export const PRESETS: Record = { const keys = Object.keys(PRESETS.dark); const GlobalTheme = createGlobalStyle<{ theme: Theme }>` :root { - ${(props) => - (Object.keys(props.theme) as Variables[]).map((key) => { - if (!keys.includes(key)) return; - return `--${key}: ${props.theme[key]};`; - })} + ${(props) => generateVariables(props.theme)} } `; +export const generateVariables = (theme: Theme) => { + const mergedTheme = { ...PRESETS[theme.light ? 'light' : 'dark'], ...theme } + return (Object.keys(mergedTheme) as Variables[]).map((key) => { + if (!keys.includes(key)) return; + return `--${key}: ${mergedTheme[key]};`; + }) +} + // Load the default default them and apply extras later export const ThemeContext = createContext(PRESETS["dark"]); diff --git a/src/env.d.ts b/src/env.d.ts index 25be5fe2..bcd82398 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -2,3 +2,7 @@ interface ImportMetaEnv { VITE_API_URL: string; VITE_THEMES_URL: string; } + +interface ImportMeta { + env: ImportMetaEnv +} \ No newline at end of file diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index 0b44170e..bf27eab4 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -16,6 +16,7 @@ import { User, Megaphone, Speaker, + Store, } from "@styled-icons/boxicons-solid"; import { Route, Switch, useHistory } from "react-router-dom"; import { LIBRARY_VERSION } from "revolt.js"; @@ -48,6 +49,8 @@ import { Notifications } from "./panes/Notifications"; import { Profile } from "./panes/Profile"; import { Sessions } from "./panes/Sessions"; import { Sync } from "./panes/Sync"; +import { ThemeShop } from "./panes/ThemeShop"; +import { isExperimentEnabled } from "../../redux/reducers/experiments"; export default function Settings() { const history = useHistory(); @@ -123,12 +126,19 @@ export default function Settings() { title: , }, { - divider: true, + divider: !isExperimentEnabled('theme_shop'), category: "revolt", id: "bots", icon: , title: , }, + { + hidden: !isExperimentEnabled('theme_shop'), + divider: true, + id: "theme_shop", + icon: , + title: , + }, { id: "feedback", icon: , @@ -169,6 +179,9 @@ export default function Settings() { + {isExperimentEnabled('theme_shop') && + + } diff --git a/src/pages/settings/assets/preview.svg b/src/pages/settings/assets/preview.svg new file mode 100644 index 00000000..d6c922f4 --- /dev/null +++ b/src/pages/settings/assets/preview.svg @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/settings/panes/Appearance.tsx b/src/pages/settings/panes/Appearance.tsx index 478acb1e..bd61029b 100644 --- a/src/pages/settings/panes/Appearance.tsx +++ b/src/pages/settings/panes/Appearance.tsx @@ -1,5 +1,5 @@ import { Reset, Import } from "@styled-icons/boxicons-regular"; -import { Pencil } from "@styled-icons/boxicons-solid"; +import { Pencil, Store } from "@styled-icons/boxicons-solid"; // @ts-expect-error shade-blend-color does not have typings. import pSBC from "shade-blend-color"; @@ -8,12 +8,16 @@ import { Text } from "preact-i18n"; import { useCallback, useContext, useEffect, useState } from "preact/hooks"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; +import CategoryButton from "../../../components/ui/fluent/CategoryButton"; + + import { debounce } from "../../../lib/debounce"; import { dispatch } from "../../../redux"; import { connectState } from "../../../redux/connector"; import { EmojiPacks, Settings } from "../../../redux/reducers/settings"; + import { DEFAULT_FONT, DEFAULT_MONO_FONT, @@ -42,6 +46,8 @@ import mutantSVG from "../assets/mutant_emoji.svg"; import notoSVG from "../assets/noto_emoji.svg"; import openmojiSVG from "../assets/openmoji_emoji.svg"; import twemojiSVG from "../assets/twemoji_emoji.svg"; +import { Link } from "react-router-dom"; +import { isExperimentEnabled } from "../../../redux/reducers/experiments"; interface Props { settings: Settings; @@ -131,15 +137,12 @@ export function Component(props: Props) { - {/* - setTheme({ - ligatures: !props.settings.theme?.ligatures, - }) - }> - Use the system theme - */} + + {isExperimentEnabled('theme_shop') && + } action="chevron" hover> + + + }

diff --git a/src/pages/settings/panes/Experiments.tsx b/src/pages/settings/panes/Experiments.tsx index abac7cc3..7e50c892 100644 --- a/src/pages/settings/panes/Experiments.tsx +++ b/src/pages/settings/panes/Experiments.tsx @@ -7,6 +7,7 @@ import { AVAILABLE_EXPERIMENTS, ExperimentOptions, EXPERIMENTS, + isExperimentEnabled, } from "../../../redux/reducers/experiments"; import Checkbox from "../../../components/ui/Checkbox"; @@ -24,7 +25,7 @@ export function Component(props: Props) { {AVAILABLE_EXPERIMENTS.map((key) => ( -1} + checked={isExperimentEnabled(key, props.options)} onChange={(enabled) => dispatch({ type: enabled diff --git a/src/pages/settings/panes/ThemeShop.tsx b/src/pages/settings/panes/ThemeShop.tsx new file mode 100644 index 00000000..205f0960 --- /dev/null +++ b/src/pages/settings/panes/ThemeShop.tsx @@ -0,0 +1,178 @@ +import { useEffect, useState } from "preact/hooks" +import styled from "styled-components" +import Tip from "../../../components/ui/Tip" +import { Theme, generateVariables } from '../../../context/Theme' +import { dispatch } from "../../../redux" + +export const fetchManifest = (): Promise => + fetch(`${import.meta.env.VITE_THEMES_URL}/manifest.json`).then(res => res.json()) + +export const fetchTheme = (slug: string): Promise => + fetch(`${import.meta.env.VITE_THEMES_URL}/theme_${slug}.json`).then(res => res.json()) + + +interface ThemeMetadata { + name: string, + creator: string, + description: string +} + +type Manifest = { + generated: string, + themes: Record +} + +// TODO: ability to preview / display the settings set like in the appearance pane +const ThemeInfo = styled.article` + display: grid; + grid: + "preview name creator" min-content + "preview desc desc" 1fr + / 200px 1fr 1fr; + + gap: 0.5rem 1rem; + padding: 1rem; + border-radius: var(--border-radius); + background: var(--secondary-background); + + &[data-loaded] { + .preview { + opacity: 1; + } + } + + .preview { + grid-area: preview; + aspect-ratio: 323 / 202; + + background-color: var(--secondary-background); + border-radius: calc(var(--border-radius) / 2); + + // prep style for later + outline: 3px solid transparent; + + // hide random svg parts, crop border on firefox + overflow: hidden; + + // hide until loaded + opacity: 0; + + // style button + border: 0; + margin: 0; + padding: 0; + + transition: 0.25s opacity, 0.25s outline; + + > * { + grid-area: 1 / 1; + } + + svg { + height: 100%; + width: 100%; + object-fit: contain; + } + + &:hover, &:active, &:focus-visible { + outline: 3px solid var(--tertiary-background); + } + } + + .name { + grid-area: name; + margin: 0; + } + + .creator { + grid-area: creator; + justify-self: end; + font-size: 0.75rem; + } + + .description { + grid-area: desc; + } +` + +const ThemeList = styled.div` + display: grid; + gap: 1rem; +` + +import previewPath from '../assets/preview.svg' + +const ThemedSVG = styled.svg<{ theme: Theme }>` + ${props => props.theme && generateVariables(props.theme)} +` + +type ThemePreviewProps = Omit, "as"> & { + slug?: string, + theme?: Theme + onThemeLoaded?: (theme: Theme) => void +}; + +const ThemePreview = ({ theme, ...props }: ThemePreviewProps) => { + return +} + +const ThemeShopRoot = styled.div` + display: grid; + gap: 1rem; +` + +export function ThemeShop() { + // setThemeList is for adding more / lazy loading in the future + const [themeList, setThemeList] = useState<[string, ThemeMetadata][] | null>(null); + const [themeData, setThemeData] = useState>({}); + + async function fetchThemeList() { + const manifest = await fetchManifest() + setThemeList(Object.entries(manifest.themes)) + } + + async function getTheme(slug: string) { + const theme = await fetchTheme(slug); + setThemeData(data => ({ ...data, [slug]: theme })) + } + + useEffect(() => { + fetchThemeList() + }, []) + + useEffect(() => { + themeList?.forEach(([slug]) => { + getTheme(slug) + }) + }, [themeList]) + + return ( + This section is under construction. + + {themeList?.map(([slug, theme]) => ( + +

{theme.name}

+ {/* Maybe id's of the users should be included as well / instead? */} +
by {theme.creator}
+
{theme.description}
+ +
+ ))} +
+
) +} \ No newline at end of file diff --git a/src/redux/reducers/experiments.ts b/src/redux/reducers/experiments.ts index c9da28d3..7d081f86 100644 --- a/src/redux/reducers/experiments.ts +++ b/src/redux/reducers/experiments.ts @@ -1,5 +1,9 @@ -export type Experiments = "search"; -export const AVAILABLE_EXPERIMENTS: Experiments[] = []; +import { getState } from ".."; + +export type Experiments = "search" | "theme_shop"; + +export const AVAILABLE_EXPERIMENTS: Experiments[] = ["theme_shop"]; + export const EXPERIMENTS: { [key in Experiments]: { title: string; description: string }; } = { @@ -7,6 +11,10 @@ export const EXPERIMENTS: { title: "Search", description: "Allows you to search for messages in channels.", }, + theme_shop: { + title: "Theme Shop", + description: "Allows you to access and set user submitted themes.", + }, }; export interface ExperimentOptions { @@ -50,3 +58,10 @@ export function experiments( return state; } } + +export function isExperimentEnabled( + name: Experiments, + experiments: ExperimentOptions = getState().experiments, +) { + return experiments.enabled?.includes(name) ?? false; +}