feat(mobx): rewrite appearance menu
|
@ -146,7 +146,6 @@
|
||||||
"revolt.js": "5.1.0-alpha.15",
|
"revolt.js": "5.1.0-alpha.15",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "^1.35.1",
|
"sass": "^1.35.1",
|
||||||
"shade-blend-color": "^1.0.0",
|
|
||||||
"styled-components": "^5.3.0",
|
"styled-components": "^5.3.0",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^4.4.2",
|
||||||
"ulid": "^2.3.0",
|
"ulid": "^2.3.0",
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { EmojiPacks } from "../../redux/reducers/settings";
|
||||||
let EMOJI_PACK = "mutant";
|
let EMOJI_PACK = "mutant";
|
||||||
const REVISION = 3;
|
const REVISION = 3;
|
||||||
|
|
||||||
export function setEmojiPack(pack: EmojiPacks) {
|
export function setGlobalEmojiPack(pack: EmojiPacks) {
|
||||||
EMOJI_PACK = pack;
|
EMOJI_PACK = pack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { Store } from "@styled-icons/boxicons-regular";
|
import { Store } from "@styled-icons/boxicons-regular";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Link } from "react-router-dom";
|
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 { Text } from "preact-i18n";
|
||||||
|
|
||||||
import TextAreaAutoSize from "../../lib/TextAreaAutoSize";
|
import TextAreaAutoSize from "../../lib/TextAreaAutoSize";
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
import { EmojiPack } from "../../mobx/stores/Settings";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Fonts,
|
Fonts,
|
||||||
|
@ -23,13 +24,13 @@ import ColourSwatches from "../ui/ColourSwatches";
|
||||||
import ComboBox from "../ui/ComboBox";
|
import ComboBox from "../ui/ComboBox";
|
||||||
import Radio from "../ui/Radio";
|
import Radio from "../ui/Radio";
|
||||||
import CategoryButton from "../ui/fluent/CategoryButton";
|
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";
|
import { ThemeBaseSelector } from "./appearance/ThemeBaseSelector";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component providing a way to switch the base theme being used.
|
||||||
|
*/
|
||||||
export const ThemeBaseSelectorShim = observer(() => {
|
export const ThemeBaseSelectorShim = observer(() => {
|
||||||
const theme = useApplicationState().settings.theme;
|
const theme = useApplicationState().settings.theme;
|
||||||
return (
|
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 = () => {
|
export const ThemeShopShim = () => {
|
||||||
if (!useApplicationState().experiments.isEnabled("theme_shop")) return null;
|
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(() => {
|
export const ThemeAccentShim = observer(() => {
|
||||||
const theme = useApplicationState().settings.theme;
|
const theme = useApplicationState().settings.theme;
|
||||||
return (
|
return (
|
||||||
|
@ -58,12 +67,18 @@ export const ThemeAccentShim = observer(() => {
|
||||||
</h3>
|
</h3>
|
||||||
<ColourSwatches
|
<ColourSwatches
|
||||||
value={theme.getVariable("accent")}
|
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(() => {
|
export const ThemeCustomCSSShim = observer(() => {
|
||||||
const theme = useApplicationState().settings.theme;
|
const theme = useApplicationState().settings.theme;
|
||||||
return (
|
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 = () => {
|
export const DisplayCompactShim = () => {
|
||||||
|
// TODO: WIP feature
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3>
|
<h3>
|
||||||
|
@ -108,6 +131,9 @@ export const DisplayCompactShim = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component providing a way to change primary text font.
|
||||||
|
*/
|
||||||
export const DisplayFontShim = observer(() => {
|
export const DisplayFontShim = observer(() => {
|
||||||
const theme = useApplicationState().settings.theme;
|
const theme = useApplicationState().settings.theme;
|
||||||
return (
|
return (
|
||||||
|
@ -128,6 +154,9 @@ export const DisplayFontShim = observer(() => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component providing a way to change secondary, monospace text font.
|
||||||
|
*/
|
||||||
export const DisplayMonospaceFontShim = observer(() => {
|
export const DisplayMonospaceFontShim = observer(() => {
|
||||||
const theme = useApplicationState().settings.theme;
|
const theme = useApplicationState().settings.theme;
|
||||||
return (
|
return (
|
||||||
|
@ -155,6 +184,9 @@ export const DisplayMonospaceFontShim = observer(() => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component providing a way to toggle font ligatures.
|
||||||
|
*/
|
||||||
export const DisplayLigaturesShim = observer(() => {
|
export const DisplayLigaturesShim = observer(() => {
|
||||||
const settings = useApplicationState().settings;
|
const settings = useApplicationState().settings;
|
||||||
if (settings.theme.getFont() !== "Inter") return null;
|
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(() => {
|
export const DisplayEmojiShim = observer(() => {
|
||||||
const settings = useApplicationState().settings;
|
const settings = useApplicationState().settings;
|
||||||
const emojiPack = settings.get("appearance:emoji");
|
|
||||||
const setPack = (v: EmojiPack) => () => settings.set("appearance:emoji", v);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<EmojiSelector
|
||||||
<h3>
|
value={settings.get("appearance:emoji")}
|
||||||
<Text id="app.settings.pages.appearance.emoji_pack" />
|
setValue={(v) => settings.set("appearance:emoji", v)}
|
||||||
</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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
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}
|
src={lightSVG}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
data-active={value === "light"}
|
data-active={value === "light"}
|
||||||
onClick={() => value !== "light" && setValue("light")}
|
onClick={() => setValue("light")}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
/>
|
/>
|
||||||
<h4>
|
<h4>
|
||||||
|
@ -71,7 +71,7 @@ export function ThemeBaseSelector({ value, setValue }: Props) {
|
||||||
src={darkSVG}
|
src={darkSVG}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
data-active={value === "dark"}
|
data-active={value === "dark"}
|
||||||
onClick={() => value !== "dark" && setValue("dark")}
|
onClick={() => setValue("dark")}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
/>
|
/>
|
||||||
<h4>
|
<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 { RefObject } from "preact";
|
||||||
import { useRef } from "preact/hooks";
|
import { useRef } from "preact/hooks";
|
||||||
|
|
||||||
|
import { useDebounceCallback } from "../../lib/debounce";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
@ -115,6 +117,11 @@ const Rows = styled.div`
|
||||||
|
|
||||||
export default function ColourSwatches({ value, onChange }: Props) {
|
export default function ColourSwatches({ value, onChange }: Props) {
|
||||||
const ref = useRef<HTMLInputElement>() as RefObject<HTMLInputElement>;
|
const ref = useRef<HTMLInputElement>() as RefObject<HTMLInputElement>;
|
||||||
|
const setValue = useDebounceCallback(
|
||||||
|
(value) => onChange(value as string),
|
||||||
|
[onChange],
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SwatchesBase>
|
<SwatchesBase>
|
||||||
|
@ -122,7 +129,7 @@ export default function ColourSwatches({ value, onChange }: Props) {
|
||||||
type="color"
|
type="color"
|
||||||
value={value}
|
value={value}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onChange={(ev) => onChange(ev.currentTarget.value)}
|
onChange={(ev) => setValue(ev.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<Swatch
|
<Swatch
|
||||||
colour={value}
|
colour={value}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { useApplicationState } from "../../mobx/State";
|
||||||
import { connectState } from "../../redux/connector";
|
import { connectState } from "../../redux/connector";
|
||||||
import { QueuedMessage } from "../../redux/reducers/queue";
|
import { QueuedMessage } from "../../redux/reducers/queue";
|
||||||
|
|
||||||
|
import { setGlobalEmojiPack } from "../../components/common/Emoji";
|
||||||
|
|
||||||
import { AppContext } from "./RevoltClient";
|
import { AppContext } from "./RevoltClient";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -30,6 +32,12 @@ function StateMonitor(props: Props) {
|
||||||
return () => client.removeListener("message", add);
|
return () => client.removeListener("message", add);
|
||||||
}, [client, props.messages]);
|
}, [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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||||
|
|
||||||
import { mapToRecord } from "../../lib/conversion";
|
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 { Sounds } from "../../assets/sounds/Audio";
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
|
|
|
@ -25,7 +25,42 @@ 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);
|
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
|
* @returns Value of variable
|
||||||
*/
|
*/
|
||||||
@computed getVariable(key: Variables) {
|
@computed getVariable(key: Variables) {
|
||||||
return (this.settings.get("appearance:theme:overrides") ??
|
return (this.settings.get("appearance:theme:overrides")?.[key] ??
|
||||||
PRESETS[this.getBase()])[key]!;
|
PRESETS[this.getBase()]?.[key])!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action setFont(font: Fonts) {
|
@action setFont(font: Fonts) {
|
||||||
|
@ -125,4 +160,9 @@ export default class STheme {
|
||||||
this.settings.remove("appearance:theme:base");
|
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";
|
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 styles from "./Panes.module.scss";
|
||||||
import { Text } from "preact-i18n";
|
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 CollapsibleSection from "../../../components/common/CollapsibleSection";
|
||||||
import Tooltip from "../../../components/common/Tooltip";
|
|
||||||
import Button from "../../../components/ui/Button";
|
|
||||||
import InputBox from "../../../components/ui/InputBox";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ThemeBaseSelectorShim,
|
ThemeBaseSelectorShim,
|
||||||
ThemeShopShim,
|
ThemeShopShim,
|
||||||
ThemeAccentShim,
|
ThemeAccentShim,
|
||||||
DisplayCompactShim,
|
|
||||||
DisplayFontShim,
|
DisplayFontShim,
|
||||||
DisplayMonospaceFontShim,
|
DisplayMonospaceFontShim,
|
||||||
DisplayLigaturesShim,
|
DisplayLigaturesShim,
|
||||||
DisplayEmojiShim,
|
DisplayEmojiShim,
|
||||||
ThemeCustomCSSShim,
|
ThemeCustomCSSShim,
|
||||||
} from "../../../components/settings/AppearanceShims";
|
} from "../../../components/settings/AppearanceShims";
|
||||||
|
import ThemeOverrides from "../../../components/settings/appearance/ThemeOverrides";
|
||||||
|
import ThemeTools from "../../../components/settings/appearance/ThemeTools";
|
||||||
|
|
||||||
interface Props {
|
export const Appearance = observer(() => {
|
||||||
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();*/
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.appearance}>
|
<div className={styles.appearance}>
|
||||||
<ThemeBaseSelectorShim />
|
<ThemeBaseSelectorShim />
|
||||||
<ThemeShopShim />
|
<ThemeShopShim />
|
||||||
<ThemeAccentShim />
|
<ThemeAccentShim />
|
||||||
{/*<DisplayCompactShim />
|
|
||||||
<DisplayFontShim />
|
<DisplayFontShim />
|
||||||
<DisplayLigaturesShim />
|
<DisplayLigaturesShim />
|
||||||
<DisplayEmojiShim />*/}
|
<DisplayEmojiShim />
|
||||||
|
|
||||||
{/*<CollapsibleSection
|
<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" />}>
|
||||||
<div className={styles.actions}>
|
<ThemeTools />
|
||||||
<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>
|
|
||||||
<h3>App</h3>
|
<h3>App</h3>
|
||||||
<div className={styles.overrides}>
|
<ThemeOverrides />
|
||||||
{(
|
</CollapsibleSection>
|
||||||
[
|
|
||||||
"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>*/}
|
|
||||||
|
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
id="settings_advanced_appearance"
|
id="settings_advanced_appearance"
|
||||||
|
@ -238,19 +50,4 @@ export const Component = observer((props: Props) => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Appearance = connectState(Component, (state) => {
|
// <DisplayCompactShim />
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,6 +15,8 @@ import Tip from "../../../components/ui/Tip";
|
||||||
|
|
||||||
const constraints = { audio: true };
|
const constraints = { audio: true };
|
||||||
|
|
||||||
|
// TODO: do not rewrite this code until voice is rewritten!
|
||||||
|
|
||||||
export function Component() {
|
export function Component() {
|
||||||
const [mediaStream, setMediaStream] = useState<MediaStream | undefined>(
|
const [mediaStream, setMediaStream] = useState<MediaStream | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -57,11 +59,11 @@ export function Component() {
|
||||||
return () => {
|
return () => {
|
||||||
if (mediaStream) {
|
if (mediaStream) {
|
||||||
// close microphone access on unmount
|
// close microphone access on unmount
|
||||||
mediaStream.getTracks().forEach(track => {
|
mediaStream.getTracks().forEach((track) => {
|
||||||
track.stop()
|
track.stop();
|
||||||
})
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}, [mediaStream]);
|
}, [mediaStream]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -232,97 +232,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.sessions {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Theme, ThemeOptions } from "../../context/Theme";
|
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 { Sounds } from "../../assets/sounds/Audio";
|
||||||
import type { SyncUpdateAction } from "./sync";
|
import type { SyncUpdateAction } from "./sync";
|
||||||
|
@ -59,7 +59,7 @@ export function settings(
|
||||||
state = {} as Settings,
|
state = {} as Settings,
|
||||||
action: SettingsAction,
|
action: SettingsAction,
|
||||||
): Settings {
|
): Settings {
|
||||||
setEmojiPack(state.appearance?.emojiPack ?? "mutant");
|
// setGlobalEmojiPack(state.appearance?.emojiPack ?? "mutant");
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "SETTINGS_SET_THEME":
|
case "SETTINGS_SET_THEME":
|
||||||
|
|