feat(mobx): rewrite appearance menu

This commit is contained in:
Paul
2021-12-15 18:23:05 +00:00
parent 65be047dc6
commit c7df0088fc
19 changed files with 558 additions and 403 deletions

View File

@@ -3,7 +3,7 @@ import { EmojiPacks } from "../../redux/reducers/settings";
let EMOJI_PACK = "mutant";
const REVISION = 3;
export function setEmojiPack(pack: EmojiPacks) {
export function setGlobalEmojiPack(pack: EmojiPacks) {
EMOJI_PACK = pack;
}

View File

@@ -1,13 +1,14 @@
import { Store } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom";
// @ts-expect-error shade-blend-color does not have typings.
import pSBC from "shade-blend-color";
import { Text } from "preact-i18n";
import TextAreaAutoSize from "../../lib/TextAreaAutoSize";
import { useApplicationState } from "../../mobx/State";
import { EmojiPack } from "../../mobx/stores/Settings";
import {
Fonts,
@@ -23,13 +24,13 @@ 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 { EmojiSelector } from "./appearance/EmojiSelector";
import { ThemeBaseSelector } from "./appearance/ThemeBaseSelector";
/**
* Component providing a way to switch the base theme being used.
*/
export const ThemeBaseSelectorShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
@@ -37,6 +38,11 @@ export const ThemeBaseSelectorShim = observer(() => {
);
});
/**
* Component providing a link to the theme shop.
* Only appears if experiment is enabled.
* TODO: stabilise
*/
export const ThemeShopShim = () => {
if (!useApplicationState().experiments.isEnabled("theme_shop")) return null;
@@ -49,6 +55,9 @@ export const ThemeShopShim = () => {
);
};
/**
* Component providing a way to change current accent colour.
*/
export const ThemeAccentShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
@@ -58,12 +67,18 @@ export const ThemeAccentShim = observer(() => {
</h3>
<ColourSwatches
value={theme.getVariable("accent")}
onChange={(colour) => theme.setVariable("accent", colour)}
onChange={(colour) => {
theme.setVariable("accent", colour as string);
theme.setVariable("scrollbar-thumb", pSBC(-0.2, colour));
}}
/>
</>
);
});
/**
* Component providing a way to edit custom CSS.
*/
export const ThemeCustomCSSShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
@@ -82,7 +97,15 @@ export const ThemeCustomCSSShim = observer(() => {
);
});
export const ThemeImporterShim = observer(() => {
return <a></a>;
});
/**
* Component providing a way to switch between compact and normal message view.
*/
export const DisplayCompactShim = () => {
// TODO: WIP feature
return (
<>
<h3>
@@ -108,6 +131,9 @@ export const DisplayCompactShim = () => {
);
};
/**
* Component providing a way to change primary text font.
*/
export const DisplayFontShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
@@ -128,6 +154,9 @@ export const DisplayFontShim = observer(() => {
);
});
/**
* Component providing a way to change secondary, monospace text font.
*/
export const DisplayMonospaceFontShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
@@ -155,6 +184,9 @@ export const DisplayMonospaceFontShim = observer(() => {
);
});
/**
* Component providing a way to toggle font ligatures.
*/
export const DisplayLigaturesShim = observer(() => {
const settings = useApplicationState().settings;
if (settings.theme.getFont() !== "Inter") return null;
@@ -173,86 +205,15 @@ export const DisplayLigaturesShim = observer(() => {
);
});
/**
* Component providing a way to change emoji pack.
*/
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>
</>
<EmojiSelector
value={settings.get("appearance:emoji")}
setValue={(v) => settings.set("appearance:emoji", v)}
/>
);
});

View File

@@ -0,0 +1,162 @@
import styled from "styled-components";
import { Text } from "preact-i18n";
import { EmojiPack } from "../../../mobx/stores/Settings";
import mutantSVG from "./mutant_emoji.svg";
import notoSVG from "./noto_emoji.svg";
import openmojiSVG from "./openmoji_emoji.svg";
import twemojiSVG from "./twemoji_emoji.svg";
const Container = styled.div`
gap: 12px;
display: flex;
flex-direction: column;
.row {
gap: 12px;
display: flex;
> div {
flex: 1;
display: flex;
flex-direction: column;
}
}
.button {
padding: 2rem 1.2rem;
display: grid;
place-items: center;
cursor: pointer;
transition: border 0.3s;
background: var(--hover);
border: 3px solid transparent;
border-radius: var(--border-radius);
img {
max-width: 100%;
}
&[data-active="true"] {
cursor: default;
background: var(--secondary-background);
border: 3px solid var(--accent);
&:hover {
border: 3px solid var(--accent);
}
}
&:hover {
background: var(--secondary-background);
border: 3px solid var(--tertiary-background);
}
}
h4 {
text-transform: unset;
a {
opacity: 0.7;
color: var(--accent);
font-weight: 600;
&:hover {
text-decoration: underline;
}
}
@media only screen and (max-width: 800px) {
a {
display: block;
}
}
}
`;
interface Props {
value?: EmojiPack;
setValue: (pack: EmojiPack) => void;
}
export function EmojiSelector({ value, setValue }: Props) {
return (
<>
<h3>
<Text id="app.settings.pages.appearance.emoji_pack" />
</h3>
<Container>
<div class="row">
<div>
<div
class="button"
onClick={() => setValue("mutant")}
data-active={value === "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
class="button"
onClick={() => setValue("twemoji")}
data-active={value === "twemoji"}>
<img
loading="eager"
src={twemojiSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Twemoji</h4>
</div>
</div>
<div class="row">
<div>
<div
class="button"
onClick={() => setValue("openmoji")}
data-active={value === "openmoji"}>
<img
loading="eager"
src={openmojiSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Openmoji</h4>
</div>
<div>
<div
class="button"
onClick={() => setValue("noto")}
data-active={value === "noto"}>
<img
loading="eager"
src={notoSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Noto Emoji</h4>
</div>
</div>
</Container>
</>
);
}

View File

@@ -58,7 +58,7 @@ export function ThemeBaseSelector({ value, setValue }: Props) {
src={lightSVG}
draggable={false}
data-active={value === "light"}
onClick={() => value !== "light" && setValue("light")}
onClick={() => setValue("light")}
onContextMenu={(e) => e.preventDefault()}
/>
<h4>
@@ -71,7 +71,7 @@ export function ThemeBaseSelector({ value, setValue }: Props) {
src={darkSVG}
draggable={false}
data-active={value === "dark"}
onClick={() => value !== "dark" && setValue("dark")}
onClick={() => setValue("dark")}
onContextMenu={(e) => e.preventDefault()}
/>
<h4>

View File

@@ -0,0 +1,181 @@
import { Pencil } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import styled from "styled-components";
import { useDebounceCallback } from "../../../lib/debounce";
import { useApplicationState } from "../../../mobx/State";
import { Variables } from "../../../context/Theme";
import InputBox from "../../ui/InputBox";
const Container = styled.div`
row-gap: 8px;
display: grid;
column-gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
margin-bottom: 20px;
.entry {
padding: 12px;
margin-top: 8px;
border: 1px solid black;
border-radius: var(--border-radius);
span {
flex: 1;
display: block;
font-weight: 600;
font-size: 0.875rem;
margin-bottom: 8px;
text-transform: capitalize;
background: inherit;
background-clip: text;
-webkit-background-clip: text;
}
.override {
gap: 8px;
display: flex;
.picker {
width: 38px;
height: 38px;
display: grid;
cursor: pointer;
place-items: center;
border-radius: var(--border-radius);
background: var(--primary-background);
}
input[type="text"] {
width: 0;
min-width: 0;
flex-grow: 1;
}
}
.input {
width: 0;
height: 0;
position: relative;
input {
opacity: 0;
border: none;
display: block;
cursor: pointer;
position: relative;
top: 48px;
}
}
}
`;
export default observer(() => {
const theme = useApplicationState().settings.theme;
const setVariable = useDebounceCallback(
(data) => {
const { key, value } = data as { key: Variables; value: string };
theme.setVariable(key, value);
},
[theme],
100,
);
return (
<Container>
{(
[
"accent",
"background",
"foreground",
"primary-background",
"primary-header",
"secondary-background",
"secondary-foreground",
"secondary-header",
"tertiary-background",
"tertiary-foreground",
"block",
"message-box",
"mention",
"scrollbar-thumb",
"scrollbar-track",
"status-online",
"status-away",
"status-busy",
"status-streaming",
"status-invisible",
"success",
"warning",
"error",
"hover",
] as const
).map((key) => (
<div
class="entry"
key={key}
style={{ backgroundColor: theme.getVariable(key) }}>
<div class="input">
<input
type="color"
value={theme.getVariable(key)}
onChange={(el) =>
setVariable({
key,
value: el.currentTarget.value,
})
}
/>
</div>
<span
style={{
color: getContrastingColour(
theme.getVariable(key),
theme.getVariable("primary-background"),
),
}}>
{key}
</span>
<div class="override">
<div
class="picker"
onClick={(e) =>
e.currentTarget.parentElement?.parentElement
?.querySelector("input")
?.click()
}>
<Pencil size={24} />
</div>
<InputBox
type="text"
class="text"
value={theme.getVariable(key)}
onChange={(el) =>
setVariable({
key,
value: el.currentTarget.value,
})
}
/>
</div>
</div>
))}
</Container>
);
});
function getContrastingColour(hex: string, fallback: string): string {
hex = hex.replace("#", "");
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
const cc = (r * 299 + g * 587 + b * 114) / 1000;
if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(cc))
return getContrastingColour(fallback, "#fffff");
return cc >= 175 ? "black" : "white";
}

View File

@@ -0,0 +1,89 @@
import { Import, Reset } from "@styled-icons/boxicons-regular";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { useApplicationState } from "../../../mobx/State";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import Tooltip from "../../common/Tooltip";
import Button from "../../ui/Button";
const Actions = styled.div`
gap: 8px;
display: flex;
margin: 18px 0 8px 0;
.code {
cursor: pointer;
display: flex;
align-items: center;
font-size: 0.875rem;
min-width: 0;
flex-grow: 1;
padding: 8px;
font-family: var(--monospace-font);
border-radius: var(--border-radius);
background: var(--secondary-background);
> div {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
`;
export default function ThemeTools() {
const { writeClipboard, openScreen } = useIntermediate();
const theme = useApplicationState().settings.theme;
return (
<Actions>
<Tooltip
content={
<Text id="app.settings.pages.appearance.reset_overrides" />
}>
<Button contrast iconbutton onClick={theme.reset}>
<Reset size={22} />
</Button>
</Tooltip>
<div
class="code"
onClick={() => writeClipboard(JSON.stringify(theme))}>
<Tooltip content={<Text id="app.special.copy" />}>
{" "}
{JSON.stringify(theme)}
</Tooltip>
</div>
<Tooltip
content={<Text id="app.settings.pages.appearance.import" />}>
<Button
contrast
iconbutton
onClick={async () => {
try {
const text = await navigator.clipboard.readText();
theme.hydrate(JSON.parse(text));
} catch (err) {
openScreen({
id: "_input",
question: (
<Text id="app.settings.pages.appearance.import_theme" />
),
field: (
<Text id="app.settings.pages.appearance.theme_data" />
),
callback: async (text) =>
theme.hydrate(JSON.parse(text)),
});
}
}}>
<Import size={22} />
</Button>
</Tooltip>
</Actions>
);
}

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -5,6 +5,8 @@ import styled, { css } from "styled-components";
import { RefObject } from "preact";
import { useRef } from "preact/hooks";
import { useDebounceCallback } from "../../lib/debounce";
interface Props {
value: string;
onChange: (value: string) => void;
@@ -115,6 +117,11 @@ const Rows = styled.div`
export default function ColourSwatches({ value, onChange }: Props) {
const ref = useRef<HTMLInputElement>() as RefObject<HTMLInputElement>;
const setValue = useDebounceCallback(
(value) => onChange(value as string),
[onChange],
100,
);
return (
<SwatchesBase>
@@ -122,7 +129,7 @@ export default function ColourSwatches({ value, onChange }: Props) {
type="color"
value={value}
ref={ref}
onChange={(ev) => onChange(ev.currentTarget.value)}
onChange={(ev) => setValue(ev.currentTarget.value)}
/>
<Swatch
colour={value}