feat(mobx): rewrite appearance menu
|
@ -146,7 +146,6 @@
|
|||
"revolt.js": "5.1.0-alpha.15",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "^1.35.1",
|
||||
"shade-blend-color": "^1.0.0",
|
||||
"styled-components": "^5.3.0",
|
||||
"typescript": "^4.4.2",
|
||||
"ulid": "^2.3.0",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
162
src/components/settings/appearance/EmojiSelector.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
|
|
181
src/components/settings/appearance/ThemeOverrides.tsx
Normal 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";
|
||||
}
|
89
src/components/settings/appearance/ThemeTools.tsx
Normal 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>
|
||||
);
|
||||
}
|
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 |
|
@ -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}
|
||||
|
|
|
@ -9,6 +9,8 @@ import { useApplicationState } from "../../mobx/State";
|
|||
import { connectState } from "../../redux/connector";
|
||||
import { QueuedMessage } from "../../redux/reducers/queue";
|
||||
|
||||
import { setGlobalEmojiPack } from "../../components/common/Emoji";
|
||||
|
||||
import { AppContext } from "./RevoltClient";
|
||||
|
||||
type Props = {
|
||||
|
@ -30,6 +32,12 @@ function StateMonitor(props: Props) {
|
|||
return () => client.removeListener("message", add);
|
||||
}, [client, props.messages]);
|
||||
|
||||
// Set global emoji pack.
|
||||
useEffect(() => {
|
||||
const v = state.settings.get("appearance:emoji");
|
||||
v && setGlobalEmojiPack(v);
|
||||
}, [state.settings.get("appearance:emoji")]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
|||
|
||||
import { mapToRecord } from "../../lib/conversion";
|
||||
|
||||
import { Fonts, MonospaceFonts, Overrides, Theme } from "../../context/Theme";
|
||||
import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme";
|
||||
|
||||
import { Sounds } from "../../assets/sounds/Audio";
|
||||
import Persistent from "../interfaces/Persistent";
|
||||
|
|
|
@ -25,7 +25,42 @@ export default class STheme {
|
|||
constructor(settings: Settings) {
|
||||
this.settings = settings;
|
||||
makeAutoObservable(this);
|
||||
|
||||
this.setBase = this.setBase.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
}
|
||||
|
||||
@computed toJSON() {
|
||||
return JSON.parse(
|
||||
JSON.stringify({
|
||||
...this.getVariables(),
|
||||
css: this.getCSS(),
|
||||
font: this.getFont(),
|
||||
monospaceFont: this.getMonospaceFont(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@action hydrate(data: Partial<Theme>) {
|
||||
for (const key of Object.keys(data)) {
|
||||
const value = data[key as keyof Theme] as string;
|
||||
switch (key) {
|
||||
case "css": {
|
||||
this.setCSS(value);
|
||||
break;
|
||||
}
|
||||
case "font": {
|
||||
this.setFont(value as Fonts);
|
||||
break;
|
||||
}
|
||||
case "monospaceFont": {
|
||||
this.setMonospaceFont(value as MonospaceFonts);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.setVariable(key as Variables, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -72,8 +107,8 @@ export default class STheme {
|
|||
* @returns Value of variable
|
||||
*/
|
||||
@computed getVariable(key: Variables) {
|
||||
return (this.settings.get("appearance:theme:overrides") ??
|
||||
PRESETS[this.getBase()])[key]!;
|
||||
return (this.settings.get("appearance:theme:overrides")?.[key] ??
|
||||
PRESETS[this.getBase()]?.[key])!;
|
||||
}
|
||||
|
||||
@action setFont(font: Fonts) {
|
||||
|
@ -125,4 +160,9 @@ export default class STheme {
|
|||
this.settings.remove("appearance:theme:base");
|
||||
}
|
||||
}
|
||||
|
||||
@action reset() {
|
||||
this.settings.remove("appearance:theme:overrides");
|
||||
this.settings.remove("appearance:theme:css");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,231 +1,43 @@
|
|||
import { Reset, Import } from "@styled-icons/boxicons-regular";
|
||||
import { Pencil } from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// @ts-expect-error shade-blend-color does not have typings.
|
||||
import pSBC from "shade-blend-color";
|
||||
|
||||
import styles from "./Panes.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { debounce } from "../../../lib/debounce";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
|
||||
|
||||
import { Theme, ThemeOptions } from "../../../context/Theme";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
import CollapsibleSection from "../../../components/common/CollapsibleSection";
|
||||
import Tooltip from "../../../components/common/Tooltip";
|
||||
import Button from "../../../components/ui/Button";
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
|
||||
import {
|
||||
ThemeBaseSelectorShim,
|
||||
ThemeShopShim,
|
||||
ThemeAccentShim,
|
||||
DisplayCompactShim,
|
||||
DisplayFontShim,
|
||||
DisplayMonospaceFontShim,
|
||||
DisplayLigaturesShim,
|
||||
DisplayEmojiShim,
|
||||
ThemeCustomCSSShim,
|
||||
} from "../../../components/settings/AppearanceShims";
|
||||
import ThemeOverrides from "../../../components/settings/appearance/ThemeOverrides";
|
||||
import ThemeTools from "../../../components/settings/appearance/ThemeTools";
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
// ! FIXME: code needs to be rewritten to fix jittering
|
||||
export const Component = observer((props: Props) => {
|
||||
//const theme = useApplicationState().settings.theme;
|
||||
const { writeClipboard, openScreen } = useIntermediate();
|
||||
|
||||
/*function setTheme(theme: ThemeOptions) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME",
|
||||
theme,
|
||||
});
|
||||
}
|
||||
|
||||
const pushOverride = useCallback((custom: Partial<Theme>) => {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME_OVERRIDE",
|
||||
custom,
|
||||
});
|
||||
}, []);
|
||||
|
||||
function setAccent(accent: string) {
|
||||
setOverride({
|
||||
accent,
|
||||
"scrollbar-thumb": pSBC(-0.2, accent),
|
||||
});
|
||||
}
|
||||
|
||||
const emojiPack = props.settings.appearance?.emojiPack ?? "mutant";
|
||||
function setEmojiPack(emojiPack: EmojiPacks) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_APPEARANCE",
|
||||
options: {
|
||||
emojiPack,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const setOverride = useCallback(
|
||||
debounce(pushOverride as (...args: unknown[]) => void, 200),
|
||||
[pushOverride],
|
||||
) as (custom: Partial<Theme>) => void;
|
||||
const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? "");
|
||||
|
||||
useEffect(() => setOverride({ css }), [setOverride, css]);
|
||||
|
||||
const selected = theme.getBase();*/
|
||||
export const Appearance = observer(() => {
|
||||
return (
|
||||
<div className={styles.appearance}>
|
||||
<ThemeBaseSelectorShim />
|
||||
<ThemeShopShim />
|
||||
<ThemeAccentShim />
|
||||
{/*<DisplayCompactShim />
|
||||
|
||||
<DisplayFontShim />
|
||||
<DisplayLigaturesShim />
|
||||
<DisplayEmojiShim />*/}
|
||||
<DisplayEmojiShim />
|
||||
|
||||
{/*<CollapsibleSection
|
||||
<CollapsibleSection
|
||||
defaultValue={false}
|
||||
id="settings_overrides"
|
||||
summary={<Text id="app.settings.pages.appearance.overrides" />}>
|
||||
<div className={styles.actions}>
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.settings.pages.appearance.reset_overrides" />
|
||||
}>
|
||||
<Button
|
||||
contrast
|
||||
iconbutton
|
||||
onClick={() => setTheme({ custom: {} })}>
|
||||
<Reset size={22} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<div
|
||||
className={styles.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();
|
||||
setOverride(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 (string) =>
|
||||
setOverride(JSON.parse(string)),
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<Import size={22} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ThemeTools />
|
||||
|
||||
<h3>App</h3>
|
||||
<div className={styles.overrides}>
|
||||
{(
|
||||
[
|
||||
"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((x) => (
|
||||
<div
|
||||
className={styles.entry}
|
||||
key={x}
|
||||
style={{ backgroundColor: theme.getVariable(x) }}>
|
||||
<div className={styles.input}>
|
||||
<input
|
||||
type="color"
|
||||
value={theme.getVariable(x)}
|
||||
onChange={(v) =>
|
||||
setOverride({
|
||||
[x]: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
style={`color: ${getContrastingColour(
|
||||
theme.getVariable(x),
|
||||
theme.getVariable("primary-background"),
|
||||
)}`}>
|
||||
{x}
|
||||
</span>
|
||||
<div className={styles.override}>
|
||||
<div
|
||||
className={styles.picker}
|
||||
onClick={(e) =>
|
||||
e.currentTarget.parentElement?.parentElement
|
||||
?.querySelector("input")
|
||||
?.click()
|
||||
}>
|
||||
<Pencil size={24} />
|
||||
</div>
|
||||
<InputBox
|
||||
type="text"
|
||||
className={styles.text}
|
||||
value={theme.getVariable(x)}
|
||||
onChange={(y) =>
|
||||
setOverride({
|
||||
[x]: y.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleSection>*/}
|
||||
<ThemeOverrides />
|
||||
</CollapsibleSection>
|
||||
|
||||
<CollapsibleSection
|
||||
id="settings_advanced_appearance"
|
||||
|
@ -238,19 +50,4 @@ export const Component = observer((props: Props) => {
|
|||
);
|
||||
});
|
||||
|
||||
export const Appearance = connectState(Component, (state) => {
|
||||
return {
|
||||
settings: state.settings,
|
||||
};
|
||||
});
|
||||
|
||||
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";
|
||||
}
|
||||
// <DisplayCompactShim />
|
||||
|
|
|
@ -15,6 +15,8 @@ import Tip from "../../../components/ui/Tip";
|
|||
|
||||
const constraints = { audio: true };
|
||||
|
||||
// TODO: do not rewrite this code until voice is rewritten!
|
||||
|
||||
export function Component() {
|
||||
const [mediaStream, setMediaStream] = useState<MediaStream | undefined>(
|
||||
undefined,
|
||||
|
@ -57,11 +59,11 @@ export function Component() {
|
|||
return () => {
|
||||
if (mediaStream) {
|
||||
// close microphone access on unmount
|
||||
mediaStream.getTracks().forEach(track => {
|
||||
track.stop()
|
||||
})
|
||||
mediaStream.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [mediaStream]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -232,97 +232,6 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actions {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overrides {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sessions {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { Theme, ThemeOptions } from "../../context/Theme";
|
||||
|
||||
import { setEmojiPack } from "../../components/common/Emoji";
|
||||
import { setGlobalEmojiPack } from "../../components/common/Emoji";
|
||||
|
||||
import type { Sounds } from "../../assets/sounds/Audio";
|
||||
import type { SyncUpdateAction } from "./sync";
|
||||
|
@ -59,7 +59,7 @@ export function settings(
|
|||
state = {} as Settings,
|
||||
action: SettingsAction,
|
||||
): Settings {
|
||||
setEmojiPack(state.appearance?.emojiPack ?? "mutant");
|
||||
// setGlobalEmojiPack(state.appearance?.emojiPack ?? "mutant");
|
||||
|
||||
switch (action.type) {
|
||||
case "SETTINGS_SET_THEME":
|
||||
|
|