feat(mobx): continue implementing themes; performance work on settings
258
src/components/settings/AppearanceShims.tsx
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
import { Store } from "@styled-icons/boxicons-regular";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
import TextAreaAutoSize from "../../lib/TextAreaAutoSize";
|
||||||
|
|
||||||
|
import { useApplicationState } from "../../mobx/State";
|
||||||
|
import { EmojiPack } from "../../mobx/stores/Settings";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Fonts,
|
||||||
|
FONTS,
|
||||||
|
FONT_KEYS,
|
||||||
|
MonospaceFonts,
|
||||||
|
MONOSPACE_FONTS,
|
||||||
|
MONOSPACE_FONT_KEYS,
|
||||||
|
} from "../../context/Theme";
|
||||||
|
|
||||||
|
import Checkbox from "../ui/Checkbox";
|
||||||
|
import ColourSwatches from "../ui/ColourSwatches";
|
||||||
|
import ComboBox from "../ui/ComboBox";
|
||||||
|
import Radio from "../ui/Radio";
|
||||||
|
import CategoryButton from "../ui/fluent/CategoryButton";
|
||||||
|
import mutantSVG from "./mutant_emoji.svg";
|
||||||
|
import notoSVG from "./noto_emoji.svg";
|
||||||
|
import openmojiSVG from "./openmoji_emoji.svg";
|
||||||
|
import twemojiSVG from "./twemoji_emoji.svg";
|
||||||
|
|
||||||
|
import { ThemeBaseSelector } from "./appearance/ThemeBaseSelector";
|
||||||
|
|
||||||
|
export const ThemeBaseSelectorShim = observer(() => {
|
||||||
|
const theme = useApplicationState().settings.theme;
|
||||||
|
return (
|
||||||
|
<ThemeBaseSelector value={theme.getBase()} setValue={theme.setBase} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ThemeShopShim = () => {
|
||||||
|
if (!useApplicationState().experiments.isEnabled("theme_shop")) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to="/settings/theme_shop" replace>
|
||||||
|
<CategoryButton icon={<Store size={24} />} action="chevron" hover>
|
||||||
|
<Text id="app.settings.pages.theme_shop.title" />
|
||||||
|
</CategoryButton>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ThemeAccentShim = observer(() => {
|
||||||
|
const theme = useApplicationState().settings.theme;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.accent_selector" />
|
||||||
|
</h3>
|
||||||
|
<ColourSwatches
|
||||||
|
value={theme.getVariable("accent")}
|
||||||
|
onChange={(colour) => theme.setVariable("accent", colour)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ThemeCustomCSSShim = observer(() => {
|
||||||
|
const theme = useApplicationState().settings.theme;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.custom_css" />
|
||||||
|
</h3>
|
||||||
|
<TextAreaAutoSize
|
||||||
|
maxRows={20}
|
||||||
|
minHeight={480}
|
||||||
|
code
|
||||||
|
value={theme.getCSS() ?? ""}
|
||||||
|
onChange={(ev) => theme.setCSS(ev.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DisplayCompactShim = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.message_display" />
|
||||||
|
</h3>
|
||||||
|
<div /* className={styles.display} */>
|
||||||
|
<Radio
|
||||||
|
description={
|
||||||
|
<Text id="app.settings.pages.appearance.display.default_description" />
|
||||||
|
}
|
||||||
|
checked>
|
||||||
|
<Text id="app.settings.pages.appearance.display.default" />
|
||||||
|
</Radio>
|
||||||
|
<Radio
|
||||||
|
description={
|
||||||
|
<Text id="app.settings.pages.appearance.display.compact_description" />
|
||||||
|
}
|
||||||
|
disabled>
|
||||||
|
<Text id="app.settings.pages.appearance.display.compact" />
|
||||||
|
</Radio>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DisplayFontShim = observer(() => {
|
||||||
|
const theme = useApplicationState().settings.theme;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.font" />
|
||||||
|
</h3>
|
||||||
|
<ComboBox
|
||||||
|
value={theme.getFont()}
|
||||||
|
onChange={(e) => theme.setFont(e.currentTarget.value as Fonts)}>
|
||||||
|
{FONT_KEYS.map((key) => (
|
||||||
|
<option value={key} key={key}>
|
||||||
|
{FONTS[key as keyof typeof FONTS].name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</ComboBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DisplayMonospaceFontShim = observer(() => {
|
||||||
|
const theme = useApplicationState().settings.theme;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.mono_font" />
|
||||||
|
</h3>
|
||||||
|
<ComboBox
|
||||||
|
value={theme.getMonospaceFont()}
|
||||||
|
onChange={(e) =>
|
||||||
|
theme.setMonospaceFont(
|
||||||
|
e.currentTarget.value as MonospaceFonts,
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
{MONOSPACE_FONT_KEYS.map((key) => (
|
||||||
|
<option value={key} key={key}>
|
||||||
|
{
|
||||||
|
MONOSPACE_FONTS[key as keyof typeof MONOSPACE_FONTS]
|
||||||
|
.name
|
||||||
|
}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</ComboBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DisplayLigaturesShim = observer(() => {
|
||||||
|
const settings = useApplicationState().settings;
|
||||||
|
if (settings.theme.getFont() !== "Inter") return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
<Checkbox
|
||||||
|
checked={settings.get("appearance:ligatures") ?? false}
|
||||||
|
onChange={(v) => settings.set("appearance:ligatures", v)}
|
||||||
|
description={
|
||||||
|
<Text id="app.settings.pages.appearance.ligatures_desc" />
|
||||||
|
}>
|
||||||
|
<Text id="app.settings.pages.appearance.ligatures" />
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DisplayEmojiShim = observer(() => {
|
||||||
|
const settings = useApplicationState().settings;
|
||||||
|
const emojiPack = settings.get("appearance:emoji");
|
||||||
|
const setPack = (v: EmojiPack) => () => settings.set("appearance:emoji", v);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.emoji_pack" />
|
||||||
|
</h3>
|
||||||
|
<div /* className={styles.emojiPack} */>
|
||||||
|
<div /* className={styles.row} */>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
/* className={styles.button} */
|
||||||
|
onClick={setPack("mutant")}
|
||||||
|
data-active={emojiPack === "mutant"}>
|
||||||
|
<img
|
||||||
|
loading="eager"
|
||||||
|
src={mutantSVG}
|
||||||
|
draggable={false}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h4>
|
||||||
|
Mutant Remix{" "}
|
||||||
|
<a
|
||||||
|
href="https://mutant.revolt.chat"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer">
|
||||||
|
(by Revolt)
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
/* className={styles.button} */
|
||||||
|
onClick={setPack("twemoji")}
|
||||||
|
data-active={emojiPack === "twemoji"}>
|
||||||
|
<img
|
||||||
|
loading="eager"
|
||||||
|
src={twemojiSVG}
|
||||||
|
draggable={false}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h4>Twemoji</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div /* className={styles.row} */>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
/* className={styles.button} */
|
||||||
|
onClick={setPack("openmoji")}
|
||||||
|
data-active={emojiPack === "openmoji"}>
|
||||||
|
<img
|
||||||
|
loading="eager"
|
||||||
|
src={openmojiSVG}
|
||||||
|
draggable={false}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h4>Openmoji</h4>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
/* className={styles.button} */
|
||||||
|
onClick={setPack("noto")}
|
||||||
|
data-active={emojiPack === "noto"}>
|
||||||
|
<img
|
||||||
|
loading="eager"
|
||||||
|
src={notoSVG}
|
||||||
|
draggable={false}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h4>Noto Emoji</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
84
src/components/settings/appearance/ThemeBaseSelector.tsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
|
import darkSVG from "./dark.svg";
|
||||||
|
import lightSVG from "./light.svg";
|
||||||
|
|
||||||
|
const List = styled.div`
|
||||||
|
gap: 8px;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transition: border 0.3s;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&[data-active="true"] {
|
||||||
|
cursor: default;
|
||||||
|
border: 3px solid var(--accent);
|
||||||
|
&:hover {
|
||||||
|
border: 3px solid var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 3px solid var(--tertiary-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value?: "light" | "dark";
|
||||||
|
setValue: (base: "light" | "dark") => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeBaseSelector({ value, setValue }: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Text id="app.settings.pages.appearance.theme" />
|
||||||
|
</h3>
|
||||||
|
<List>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
loading="eager"
|
||||||
|
src={lightSVG}
|
||||||
|
draggable={false}
|
||||||
|
data-active={value === "light"}
|
||||||
|
onClick={() => value !== "light" && setValue("light")}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
/>
|
||||||
|
<h4>
|
||||||
|
<Text id="app.settings.pages.appearance.color.light" />
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
loading="eager"
|
||||||
|
src={darkSVG}
|
||||||
|
draggable={false}
|
||||||
|
data-active={value === "dark"}
|
||||||
|
onClick={() => value !== "dark" && setValue("dark")}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
/>
|
||||||
|
<h4>
|
||||||
|
<Text id="app.settings.pages.appearance.color.dark" />
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
@ -7,9 +7,9 @@ interface Props {
|
||||||
children: Children;
|
children: Children;
|
||||||
description?: Children;
|
description?: Children;
|
||||||
|
|
||||||
checked: boolean;
|
checked?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onSelect: () => void;
|
onSelect?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
|
@ -87,9 +87,10 @@ const RadioDescription = styled.span<BaseProps>`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Radio(props: Props) {
|
export default function Radio(props: Props) {
|
||||||
|
const selected = props.checked ?? false;
|
||||||
return (
|
return (
|
||||||
<RadioBase
|
<RadioBase
|
||||||
selected={props.checked}
|
selected={selected}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
!props.disabled && props.onSelect && props.onSelect()
|
!props.disabled && props.onSelect && props.onSelect()
|
||||||
|
@ -101,7 +102,7 @@ export default function Radio(props: Props) {
|
||||||
<span>
|
<span>
|
||||||
<span>{props.children}</span>
|
<span>{props.children}</span>
|
||||||
{props.description && (
|
{props.description && (
|
||||||
<RadioDescription selected={props.checked}>
|
<RadioDescription selected={selected}>
|
||||||
{props.description}
|
{props.description}
|
||||||
</RadioDescription>
|
</RadioDescription>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { createGlobalStyle } from "styled-components";
|
import { createGlobalStyle } from "styled-components";
|
||||||
|
|
||||||
import { createContext } from "preact";
|
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
|
|
||||||
import { useApplicationState } from "../mobx/State";
|
import { useApplicationState } from "../mobx/State";
|
||||||
|
@ -316,7 +316,7 @@ export const generateVariables = (theme: Theme) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Theme() {
|
export default observer(() => {
|
||||||
const settings = useApplicationState().settings;
|
const settings = useApplicationState().settings;
|
||||||
const theme = settings.theme;
|
const theme = settings.theme;
|
||||||
|
|
||||||
|
@ -359,4 +359,4 @@ export default function Theme() {
|
||||||
<style dangerouslySetInnerHTML={{ __html: theme.getCSS() ?? "" }} />
|
<style dangerouslySetInnerHTML={{ __html: theme.getCSS() ?? "" }} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
76
src/mobx/stores/Cache.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||||
|
|
||||||
|
import { mapToRecord } from "../../lib/conversion";
|
||||||
|
|
||||||
|
import { StoredTheme } from "../../redux/reducers/themes";
|
||||||
|
|
||||||
|
import Persistent from "../interfaces/Persistent";
|
||||||
|
import Store from "../interfaces/Store";
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
themes: Record<string, StoredTheme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache data store for temporary, long-lived data.
|
||||||
|
*/
|
||||||
|
export default class Cache implements Store, Persistent<Data> {
|
||||||
|
private themes: ObservableMap<string, StoredTheme>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct new Cache store.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.themes = new ObservableMap();
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return "draft";
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
themes: JSON.parse(JSON.stringify(mapToRecord(this.themes))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@action hydrate(data: Data) {
|
||||||
|
Object.keys(data.themes).forEach((key) =>
|
||||||
|
this.themes.set(key, data.themes[key]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache a given theme.
|
||||||
|
* @param theme Theme
|
||||||
|
*/
|
||||||
|
@action cacheTheme(theme: StoredTheme) {
|
||||||
|
this.themes.set(theme.slug, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a cached theme.
|
||||||
|
* @param slug String
|
||||||
|
*/
|
||||||
|
@action removeTheme(slug: string) {
|
||||||
|
this.themes.delete(slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a cached theme by its slug.
|
||||||
|
* @param slug Theme slug
|
||||||
|
* @returns Theme, if found
|
||||||
|
*/
|
||||||
|
@computed getTheme(slug: string) {
|
||||||
|
return this.themes.get(slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all cached themes.
|
||||||
|
* @returns Themes
|
||||||
|
*/
|
||||||
|
@computed getThemes() {
|
||||||
|
return [...this.themes.values()];
|
||||||
|
}
|
||||||
|
}
|
|
@ -80,6 +80,10 @@ export default class Settings implements Store, Persistent<ISettings> {
|
||||||
return this.data.get(key) as ISettings[T] | undefined;
|
return this.data.get(key) as ISettings[T] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action remove<T extends keyof ISettings>(key: T) {
|
||||||
|
this.data.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a value in settings without type-checking.
|
* Set a value in settings without type-checking.
|
||||||
* @param key Colon-divided key
|
* @param key Colon-divided key
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { makeAutoObservable, computed } from "mobx";
|
import { makeAutoObservable, computed, action } from "mobx";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Theme,
|
Theme,
|
||||||
|
@ -6,6 +6,8 @@ import {
|
||||||
Variables,
|
Variables,
|
||||||
DEFAULT_FONT,
|
DEFAULT_FONT,
|
||||||
DEFAULT_MONO_FONT,
|
DEFAULT_MONO_FONT,
|
||||||
|
Fonts,
|
||||||
|
MonospaceFonts,
|
||||||
} from "../../../context/Theme";
|
} from "../../../context/Theme";
|
||||||
|
|
||||||
import Settings from "../Settings";
|
import Settings from "../Settings";
|
||||||
|
@ -23,6 +25,7 @@ export default class STheme {
|
||||||
constructor(settings: Settings) {
|
constructor(settings: Settings) {
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
this.setBase = this.setBase.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,6 +59,13 @@ export default class STheme {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action setVariable(key: Variables, value: string) {
|
||||||
|
this.settings.set("appearance:theme:overrides", {
|
||||||
|
...this.settings.get("appearance:theme:overrides"),
|
||||||
|
[key]: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a specific value of a variable by its key.
|
* Get a specific value of a variable by its key.
|
||||||
* @param key Variable
|
* @param key Variable
|
||||||
|
@ -66,6 +76,10 @@ export default class STheme {
|
||||||
PRESETS[this.getBase()])[key]!;
|
PRESETS[this.getBase()])[key]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action setFont(font: Fonts) {
|
||||||
|
this.settings.set("appearance:theme:font", font);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current applied font.
|
* Get the current applied font.
|
||||||
* @returns Current font
|
* @returns Current font
|
||||||
|
@ -74,6 +88,10 @@ export default class STheme {
|
||||||
return this.settings.get("appearance:theme:font") ?? DEFAULT_FONT;
|
return this.settings.get("appearance:theme:font") ?? DEFAULT_FONT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action setMonospaceFont(font: MonospaceFonts) {
|
||||||
|
this.settings.set("appearance:theme:monoFont", font);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current applied monospace font.
|
* Get the current applied monospace font.
|
||||||
* @returns Current monospace font
|
* @returns Current monospace font
|
||||||
|
@ -84,6 +102,14 @@ export default class STheme {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action setCSS(value: string) {
|
||||||
|
if (value.length > 0) {
|
||||||
|
this.settings.set("appearance:theme:css", value);
|
||||||
|
} else {
|
||||||
|
this.settings.remove("appearance:theme:css");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the currently applied CSS snippet.
|
* Get the currently applied CSS snippet.
|
||||||
* @returns CSS string
|
* @returns CSS string
|
||||||
|
@ -91,4 +117,12 @@ export default class STheme {
|
||||||
@computed getCSS() {
|
@computed getCSS() {
|
||||||
return this.settings.get("appearance:theme:css");
|
return this.settings.get("appearance:theme:css");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action setBase(base?: "light" | "dark") {
|
||||||
|
if (base) {
|
||||||
|
this.settings.set("appearance:theme:base", base);
|
||||||
|
} else {
|
||||||
|
this.settings.remove("appearance:theme:base");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,61 +1,50 @@
|
||||||
import { Reset, Import } from "@styled-icons/boxicons-regular";
|
import { Reset, Import } from "@styled-icons/boxicons-regular";
|
||||||
import { Pencil, Store } from "@styled-icons/boxicons-solid";
|
import { Pencil } from "@styled-icons/boxicons-solid";
|
||||||
import { Link } from "react-router-dom";
|
import { observer } from "mobx-react-lite";
|
||||||
// @ts-expect-error shade-blend-color does not have typings.
|
// @ts-expect-error shade-blend-color does not have typings.
|
||||||
import pSBC from "shade-blend-color";
|
import pSBC from "shade-blend-color";
|
||||||
|
|
||||||
import styles from "./Panes.module.scss";
|
import styles from "./Panes.module.scss";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
|
||||||
import { debounce } from "../../../lib/debounce";
|
import { debounce } from "../../../lib/debounce";
|
||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
import { dispatch } from "../../../redux";
|
import { dispatch } from "../../../redux";
|
||||||
import { connectState } from "../../../redux/connector";
|
import { connectState } from "../../../redux/connector";
|
||||||
import { isExperimentEnabled } from "../../../redux/reducers/experiments";
|
|
||||||
import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
|
import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
|
||||||
|
|
||||||
import {
|
import { Theme, ThemeOptions } from "../../../context/Theme";
|
||||||
DEFAULT_FONT,
|
|
||||||
DEFAULT_MONO_FONT,
|
|
||||||
Fonts,
|
|
||||||
FONTS,
|
|
||||||
FONT_KEYS,
|
|
||||||
MonospaceFonts,
|
|
||||||
MONOSPACE_FONTS,
|
|
||||||
MONOSPACE_FONT_KEYS,
|
|
||||||
Theme,
|
|
||||||
ThemeOptions,
|
|
||||||
} from "../../../context/Theme";
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
|
|
||||||
import CollapsibleSection from "../../../components/common/CollapsibleSection";
|
import CollapsibleSection from "../../../components/common/CollapsibleSection";
|
||||||
import Tooltip from "../../../components/common/Tooltip";
|
import Tooltip from "../../../components/common/Tooltip";
|
||||||
import Button from "../../../components/ui/Button";
|
import Button from "../../../components/ui/Button";
|
||||||
import Checkbox from "../../../components/ui/Checkbox";
|
|
||||||
import ColourSwatches from "../../../components/ui/ColourSwatches";
|
|
||||||
import ComboBox from "../../../components/ui/ComboBox";
|
|
||||||
import InputBox from "../../../components/ui/InputBox";
|
import InputBox from "../../../components/ui/InputBox";
|
||||||
import CategoryButton from "../../../components/ui/fluent/CategoryButton";
|
|
||||||
import darkSVG from "../assets/dark.svg";
|
import {
|
||||||
import lightSVG from "../assets/light.svg";
|
ThemeBaseSelectorShim,
|
||||||
import mutantSVG from "../assets/mutant_emoji.svg";
|
ThemeShopShim,
|
||||||
import notoSVG from "../assets/noto_emoji.svg";
|
ThemeAccentShim,
|
||||||
import openmojiSVG from "../assets/openmoji_emoji.svg";
|
DisplayCompactShim,
|
||||||
import twemojiSVG from "../assets/twemoji_emoji.svg";
|
DisplayFontShim,
|
||||||
|
DisplayMonospaceFontShim,
|
||||||
|
DisplayLigaturesShim,
|
||||||
|
DisplayEmojiShim,
|
||||||
|
ThemeCustomCSSShim,
|
||||||
|
} from "../../../components/settings/AppearanceShims";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ! FIXME: code needs to be rewritten to fix jittering
|
// ! FIXME: code needs to be rewritten to fix jittering
|
||||||
export function Component(props: Props) {
|
export const Component = observer((props: Props) => {
|
||||||
const theme = useApplicationState().settings.theme;
|
//const theme = useApplicationState().settings.theme;
|
||||||
const { writeClipboard, openScreen } = useIntermediate();
|
const { writeClipboard, openScreen } = useIntermediate();
|
||||||
|
|
||||||
function setTheme(theme: ThemeOptions) {
|
/*function setTheme(theme: ThemeOptions) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SETTINGS_SET_THEME",
|
type: "SETTINGS_SET_THEME",
|
||||||
theme,
|
theme,
|
||||||
|
@ -95,191 +84,18 @@ export function Component(props: Props) {
|
||||||
|
|
||||||
useEffect(() => setOverride({ css }), [setOverride, css]);
|
useEffect(() => setOverride({ css }), [setOverride, css]);
|
||||||
|
|
||||||
const selected = props.settings.theme?.base ?? "dark";
|
const selected = theme.getBase();*/
|
||||||
return (
|
return (
|
||||||
<div className={styles.appearance}>
|
<div className={styles.appearance}>
|
||||||
<h3>
|
<ThemeBaseSelectorShim />
|
||||||
<Text id="app.settings.pages.appearance.theme" />
|
<ThemeShopShim />
|
||||||
</h3>
|
<ThemeAccentShim />
|
||||||
<div className={styles.themes}>
|
{/*<DisplayCompactShim />
|
||||||
<div className={styles.theme}>
|
<DisplayFontShim />
|
||||||
<img
|
<DisplayLigaturesShim />
|
||||||
loading="eager"
|
<DisplayEmojiShim />*/}
|
||||||
src={lightSVG}
|
|
||||||
draggable={false}
|
|
||||||
data-active={selected === "light"}
|
|
||||||
onClick={() =>
|
|
||||||
selected !== "light" && setTheme({ base: "light" })
|
|
||||||
}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
/>
|
|
||||||
<h4>
|
|
||||||
<Text id="app.settings.pages.appearance.color.light" />
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div className={styles.theme}>
|
|
||||||
<img
|
|
||||||
loading="eager"
|
|
||||||
src={darkSVG}
|
|
||||||
draggable={false}
|
|
||||||
data-active={selected === "dark"}
|
|
||||||
onClick={() =>
|
|
||||||
selected !== "dark" && setTheme({ base: "dark" })
|
|
||||||
}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
/>
|
|
||||||
<h4>
|
|
||||||
<Text id="app.settings.pages.appearance.color.dark" />
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isExperimentEnabled("theme_shop") && (
|
{/*<CollapsibleSection
|
||||||
<Link to="/settings/theme_shop" replace>
|
|
||||||
<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" />
|
|
||||||
</h3>
|
|
||||||
<ColourSwatches
|
|
||||||
value={theme.getVariable("accent")}
|
|
||||||
onChange={setAccent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*<h3>
|
|
||||||
<Text id="app.settings.pages.appearance.message_display" />
|
|
||||||
</h3>
|
|
||||||
<div className={styles.display}>
|
|
||||||
<Radio
|
|
||||||
description={
|
|
||||||
<Text id="app.settings.pages.appearance.display.default_description" />
|
|
||||||
}
|
|
||||||
checked
|
|
||||||
>
|
|
||||||
<Text id="app.settings.pages.appearance.display.default" />
|
|
||||||
</Radio>
|
|
||||||
<Radio
|
|
||||||
description={
|
|
||||||
<Text id="app.settings.pages.appearance.display.compact_description" />
|
|
||||||
}
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<Text id="app.settings.pages.appearance.display.compact" />
|
|
||||||
</Radio>
|
|
||||||
</div>*/}
|
|
||||||
|
|
||||||
<h3>
|
|
||||||
<Text id="app.settings.pages.appearance.font" />
|
|
||||||
</h3>
|
|
||||||
<ComboBox
|
|
||||||
value={theme.getFont()}
|
|
||||||
onChange={(e) =>
|
|
||||||
pushOverride({ font: e.currentTarget.value as Fonts })
|
|
||||||
}>
|
|
||||||
{FONT_KEYS.map((key) => (
|
|
||||||
<option value={key} key={key}>
|
|
||||||
{FONTS[key as keyof typeof FONTS].name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</ComboBox>
|
|
||||||
{/* TOFIX: Only show when a font with ligature support is selected, i.e.: Inter.*/}
|
|
||||||
<p>
|
|
||||||
<Checkbox
|
|
||||||
checked={props.settings.theme?.ligatures === true}
|
|
||||||
onChange={() =>
|
|
||||||
setTheme({
|
|
||||||
ligatures: !props.settings.theme?.ligatures,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<Text id="app.settings.pages.appearance.ligatures_desc" />
|
|
||||||
}>
|
|
||||||
<Text id="app.settings.pages.appearance.ligatures" />
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>
|
|
||||||
<Text id="app.settings.pages.appearance.emoji_pack" />
|
|
||||||
</h3>
|
|
||||||
<div className={styles.emojiPack}>
|
|
||||||
<div className={styles.row}>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => setEmojiPack("mutant")}
|
|
||||||
data-active={emojiPack === "mutant"}>
|
|
||||||
<img
|
|
||||||
loading="eager"
|
|
||||||
src={mutantSVG}
|
|
||||||
draggable={false}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h4>
|
|
||||||
Mutant Remix{" "}
|
|
||||||
<a
|
|
||||||
href="https://mutant.revolt.chat"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer">
|
|
||||||
(by Revolt)
|
|
||||||
</a>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => setEmojiPack("twemoji")}
|
|
||||||
data-active={emojiPack === "twemoji"}>
|
|
||||||
<img
|
|
||||||
loading="eager"
|
|
||||||
src={twemojiSVG}
|
|
||||||
draggable={false}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h4>Twemoji</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.row}>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => setEmojiPack("openmoji")}
|
|
||||||
data-active={emojiPack === "openmoji"}>
|
|
||||||
<img
|
|
||||||
loading="eager"
|
|
||||||
src={openmojiSVG}
|
|
||||||
draggable={false}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h4>Openmoji</h4>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => setEmojiPack("noto")}
|
|
||||||
data-active={emojiPack === "noto"}>
|
|
||||||
<img
|
|
||||||
loading="eager"
|
|
||||||
src={notoSVG}
|
|
||||||
draggable={false}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h4>Noto Emoji</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CollapsibleSection
|
|
||||||
defaultValue={false}
|
defaultValue={false}
|
||||||
id="settings_overrides"
|
id="settings_overrides"
|
||||||
summary={<Text id="app.settings.pages.appearance.overrides" />}>
|
summary={<Text id="app.settings.pages.appearance.overrides" />}>
|
||||||
|
@ -300,7 +116,6 @@ export function Component(props: Props) {
|
||||||
onClick={() => writeClipboard(JSON.stringify(theme))}>
|
onClick={() => writeClipboard(JSON.stringify(theme))}>
|
||||||
<Tooltip content={<Text id="app.special.copy" />}>
|
<Tooltip content={<Text id="app.special.copy" />}>
|
||||||
{" "}
|
{" "}
|
||||||
{/*TOFIX: Try to put the tooltip above the .code div without messing up the css challenge */}
|
|
||||||
{JSON.stringify(theme)}
|
{JSON.stringify(theme)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -410,48 +225,18 @@ export function Component(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>*/}
|
||||||
|
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
id="settings_advanced_appearance"
|
id="settings_advanced_appearance"
|
||||||
defaultValue={false}
|
defaultValue={false}
|
||||||
summary={<Text id="app.settings.pages.appearance.advanced" />}>
|
summary={<Text id="app.settings.pages.appearance.advanced" />}>
|
||||||
<h3>
|
<DisplayMonospaceFontShim />
|
||||||
<Text id="app.settings.pages.appearance.mono_font" />
|
<ThemeCustomCSSShim />
|
||||||
</h3>
|
|
||||||
<ComboBox
|
|
||||||
value={theme.getMonospaceFont()}
|
|
||||||
onChange={(e) =>
|
|
||||||
pushOverride({
|
|
||||||
monospaceFont: e.currentTarget
|
|
||||||
.value as MonospaceFonts,
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
{MONOSPACE_FONT_KEYS.map((key) => (
|
|
||||||
<option value={key} key={key}>
|
|
||||||
{
|
|
||||||
MONOSPACE_FONTS[
|
|
||||||
key as keyof typeof MONOSPACE_FONTS
|
|
||||||
].name
|
|
||||||
}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<h3>
|
|
||||||
<Text id="app.settings.pages.appearance.custom_css" />
|
|
||||||
</h3>
|
|
||||||
<TextAreaAutoSize
|
|
||||||
maxRows={20}
|
|
||||||
minHeight={480}
|
|
||||||
code
|
|
||||||
value={css}
|
|
||||||
onChange={(ev) => setCSS(ev.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export const Appearance = connectState(Component, (state) => {
|
export const Appearance = connectState(Component, (state) => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -150,38 +150,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.appearance {
|
.appearance {
|
||||||
.theme {
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.themes {
|
|
||||||
gap: 8px;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
img {
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
transition: border 0.3s;
|
|
||||||
border: 3px solid transparent;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&[data-active="true"] {
|
|
||||||
cursor: default;
|
|
||||||
border: 3px solid var(--accent);
|
|
||||||
&:hover {
|
|
||||||
border: 3px solid var(--accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: 3px solid var(--tertiary-background);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
details {
|
details {
|
||||||
summary {
|
summary {
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
|
|