mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-07 17:35:28 +00:00
Merge pull request #188 from brecert/theme_shop
This commit is contained in:
@@ -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) {
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
{/*<Checkbox
|
||||
checked={props.settings.theme?.ligatures === true}
|
||||
onChange={() =>
|
||||
setTheme({
|
||||
ligatures: !props.settings.theme?.ligatures,
|
||||
})
|
||||
}>
|
||||
Use the system theme
|
||||
</Checkbox>*/}
|
||||
|
||||
{isExperimentEnabled('theme_shop') && <Link to="/settings/theme_shop">
|
||||
<CategoryButton icon={<Store size={24} />} action="chevron" hover>
|
||||
<Text id="app.settings.pages.theme_shop.title" />
|
||||
</CategoryButton>
|
||||
</Link>}
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.accent_selector" />
|
||||
|
||||
@@ -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) => (
|
||||
<Checkbox
|
||||
key={key}
|
||||
checked={(props.options?.enabled ?? []).indexOf(key) > -1}
|
||||
checked={isExperimentEnabled(key, props.options)}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: enabled
|
||||
|
||||
178
src/pages/settings/panes/ThemeShop.tsx
Normal file
178
src/pages/settings/panes/ThemeShop.tsx
Normal file
@@ -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<Manifest> =>
|
||||
fetch(`${import.meta.env.VITE_THEMES_URL}/manifest.json`).then(res => res.json())
|
||||
|
||||
export const fetchTheme = (slug: string): Promise<Theme> =>
|
||||
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<string, ThemeMetadata>
|
||||
}
|
||||
|
||||
// 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<JSX.HTMLAttributes<SVGSVGElement>, "as"> & {
|
||||
slug?: string,
|
||||
theme?: Theme
|
||||
onThemeLoaded?: (theme: Theme) => void
|
||||
};
|
||||
|
||||
const ThemePreview = ({ theme, ...props }: ThemePreviewProps) => {
|
||||
return <ThemedSVG {...props} theme={theme} width="323" height="202" aria-hidden="true" data-loaded={!!theme}>
|
||||
<use href={`${previewPath}#preview`} width="100%" height="100%" />
|
||||
</ThemedSVG >
|
||||
}
|
||||
|
||||
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<Record<string, Theme>>({});
|
||||
|
||||
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 (<ThemeShopRoot>
|
||||
<Tip warning>This section is under construction.</Tip>
|
||||
<ThemeList>
|
||||
{themeList?.map(([slug, theme]) => (
|
||||
<ThemeInfo key={slug} data-loaded={Reflect.has(themeData, slug)}>
|
||||
<h2 class="name">{theme.name}</h2>
|
||||
{/* Maybe id's of the users should be included as well / instead? */}
|
||||
<div class="creator">by {theme.creator}</div>
|
||||
<div class="description">{theme.description}</div>
|
||||
<button
|
||||
class="preview"
|
||||
onClick={() => dispatch({
|
||||
type: "SETTINGS_SET_THEME",
|
||||
theme: {
|
||||
custom: themeData[slug],
|
||||
}
|
||||
})}
|
||||
>
|
||||
<ThemePreview
|
||||
slug={slug}
|
||||
theme={themeData[slug]}
|
||||
/>
|
||||
</button>
|
||||
</ThemeInfo>
|
||||
))}
|
||||
</ThemeList>
|
||||
</ThemeShopRoot>)
|
||||
}
|
||||
Reference in New Issue
Block a user