implement theme setting
This commit is contained in:
@@ -12,7 +12,10 @@ export default class SettingsModel {
|
||||
theme: string;
|
||||
|
||||
// storage
|
||||
private storeSetting(pathName: keyof typeof filePaths.settingsModel, value: string): void {
|
||||
private storeSetting(
|
||||
pathName: keyof typeof filePaths.settingsModel,
|
||||
value: string,
|
||||
): void {
|
||||
const path = StorageModel.getPath(
|
||||
StorageModelSubPaths.SettingsModel,
|
||||
filePaths.settingsModel[pathName],
|
||||
@@ -41,7 +44,9 @@ export default class SettingsModel {
|
||||
}
|
||||
|
||||
// load
|
||||
private readSetting(pathName: keyof typeof filePaths.settingsModel): string {
|
||||
private readSetting(
|
||||
pathName: keyof typeof filePaths.settingsModel,
|
||||
): string {
|
||||
const path = StorageModel.getPath(
|
||||
StorageModelSubPaths.SettingsModel,
|
||||
filePaths.settingsModel[pathName],
|
||||
@@ -76,6 +81,7 @@ export default class SettingsModel {
|
||||
this.loadUsername();
|
||||
this.loadFirstDayofWeek();
|
||||
this.loadLanguage();
|
||||
this.loadTheme();
|
||||
}
|
||||
|
||||
static getSystemLanguage(): Languages {
|
||||
@@ -100,4 +106,4 @@ export enum ThemeSettings {
|
||||
Dark = "dark",
|
||||
Light = "light",
|
||||
System = "system",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
export function OptionButton<T>(
|
||||
text: string,
|
||||
value: T,
|
||||
selection: React.State<T>,
|
||||
) {
|
||||
function select() {
|
||||
selection.value = value;
|
||||
}
|
||||
const isSelected = React.createProxyState(
|
||||
[selection],
|
||||
() => selection.value == value,
|
||||
);
|
||||
|
||||
return (
|
||||
<button class="standard" toggle:selected={isSelected} on:click={select}>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "bloatless-react";
|
||||
import { OptionButton } from "./optionButton";
|
||||
|
||||
export function OptionButtonList<T>(
|
||||
options: React.ListState<[string, T]>,
|
||||
selection: React.State<T>,
|
||||
) {
|
||||
function OptionToView(option: [string, T]) {
|
||||
const [text, value] = option;
|
||||
return OptionButton(text, value, selection);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class="flex-column gap"
|
||||
children:append={[options, OptionToView]}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import { SplitModal } from "../Components/splitModal";
|
||||
import { InfoTile } from "../Components/infoTile";
|
||||
import { Option } from "../Components/option";
|
||||
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
||||
import { Languages } from "../../Model/Global/settingsModel";
|
||||
import { Languages, ThemeSettings } from "../../Model/Global/settingsModel";
|
||||
import { OptionButtonList } from "../Components/optionButtonList";
|
||||
import { stringify } from "../../Model/Utility/utility";
|
||||
|
||||
export function SettingsModal(
|
||||
coreViewModel: CoreViewModel,
|
||||
@@ -19,7 +21,10 @@ export function SettingsModal(
|
||||
() => {
|
||||
switch (settingsViewModel.selectedModalPage.value) {
|
||||
case SettingsModalPages.Appearance:
|
||||
return <div>Appearance</div>;
|
||||
return SettingsAppearancePane(
|
||||
coreViewModel,
|
||||
settingsViewModel,
|
||||
);
|
||||
case SettingsModalPages.Regional:
|
||||
return SettingsRegionalPane(
|
||||
coreViewModel,
|
||||
@@ -161,8 +166,7 @@ function SettingsRegionalPane(
|
||||
Option(
|
||||
languageNames[language],
|
||||
language,
|
||||
language ==
|
||||
settingsViewModel.language.value,
|
||||
language == settingsViewModel.language.value,
|
||||
),
|
||||
)}
|
||||
</select>
|
||||
@@ -172,3 +176,32 @@ function SettingsRegionalPane(
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsAppearancePane(
|
||||
coreViewModel: CoreViewModel,
|
||||
settingsViewModel: SettingsViewModel,
|
||||
) {
|
||||
return (
|
||||
<div class="slide-up">
|
||||
<h2>{coreViewModel.translations.settings.pages.appearance}</h2>
|
||||
<hr></hr>
|
||||
{OptionButtonList(
|
||||
new React.ListState<[string, ThemeSettings]>([
|
||||
[
|
||||
coreViewModel.translations.settings.themes.dark,
|
||||
ThemeSettings.Dark,
|
||||
],
|
||||
[
|
||||
coreViewModel.translations.settings.themes.light,
|
||||
ThemeSettings.Light,
|
||||
],
|
||||
[
|
||||
coreViewModel.translations.settings.themes.system,
|
||||
ThemeSettings.System,
|
||||
],
|
||||
]),
|
||||
settingsViewModel.theme,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,11 @@ const englishTranslations = {
|
||||
regional: "Language & Region",
|
||||
info: "About Comms",
|
||||
},
|
||||
themes: {
|
||||
dark: "Dark",
|
||||
light: "Light",
|
||||
system: "Device theme",
|
||||
},
|
||||
|
||||
version: "Version",
|
||||
language: "Language",
|
||||
@@ -375,6 +380,11 @@ export const allTranslations: {
|
||||
regional: "Sprache & Region",
|
||||
info: "Über Comms",
|
||||
},
|
||||
themes: {
|
||||
dark: "Dunkel",
|
||||
light: "Hell",
|
||||
system: "Geräteeinstellung",
|
||||
},
|
||||
|
||||
version: "Version",
|
||||
language: "Sprache",
|
||||
@@ -639,6 +649,11 @@ export const allTranslations: {
|
||||
regional: "Idioma y Región",
|
||||
info: "Sobre Comms",
|
||||
},
|
||||
themes: {
|
||||
dark: "Oscuro",
|
||||
light: "Claro",
|
||||
system: "Según dispositivo",
|
||||
},
|
||||
|
||||
version: "Versión",
|
||||
language: "Idioma",
|
||||
@@ -832,6 +847,6 @@ export const languageNames = {
|
||||
en: "English",
|
||||
de: "Deutsch",
|
||||
es: "Español",
|
||||
}
|
||||
};
|
||||
|
||||
export type Translations = typeof englishTranslations;
|
||||
export type Translations = typeof englishTranslations;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import CoreViewModel from "./coreViewModel";
|
||||
import SettingsModel from "../../Model/Global/settingsModel";
|
||||
import { Languages } from "../../View/translations";
|
||||
import { Languages, ThemeSettings } from "../../Model/Global/settingsModel";
|
||||
|
||||
export default class SettingsViewModel {
|
||||
// state
|
||||
@@ -15,7 +14,12 @@ export default class SettingsViewModel {
|
||||
requiresReload = new React.State<boolean>(false);
|
||||
|
||||
firstDayOfWeek: React.State<string> = new React.State("0");
|
||||
language: React.State<Languages|string> = new React.State(Languages.English);
|
||||
language: React.State<Languages | string> = new React.State(
|
||||
Languages.English,
|
||||
);
|
||||
theme: React.State<ThemeSettings | string> = new React.State(
|
||||
ThemeSettings.System,
|
||||
);
|
||||
|
||||
// guards
|
||||
cannotSetName: React.State<boolean> = React.createProxyState(
|
||||
@@ -54,19 +58,49 @@ export default class SettingsViewModel {
|
||||
this.selectedModalPage.value = page;
|
||||
};
|
||||
|
||||
// view
|
||||
applyTheme = (): void => {
|
||||
let theme = this.theme.value;
|
||||
if (theme == ThemeSettings.System) {
|
||||
theme = SettingsViewModel.getSystemTheme();
|
||||
}
|
||||
document.body.setAttribute("theme", theme);
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(public coreViewModel: CoreViewModel) {
|
||||
this.username.value = coreViewModel.settingsModel.username;
|
||||
this.usernameInput.value = coreViewModel.settingsModel.username;
|
||||
this.firstDayOfWeek.value = coreViewModel.settingsModel.firstDayOfWeek;
|
||||
this.language.value = coreViewModel.settingsModel.language;
|
||||
this.theme.value = coreViewModel.settingsModel.theme;
|
||||
|
||||
// subscriptions
|
||||
this.firstDayOfWeek.subscribe(this.setFirstDayofWeek);
|
||||
this.language.subscribeSilent((newValue) => {
|
||||
this.coreViewModel.settingsModel.setLanguage(newValue);
|
||||
this.requiresReload.value = true;
|
||||
})
|
||||
});
|
||||
this.theme.subscribeSilent((newValue) => {
|
||||
this.coreViewModel.settingsModel.setTheme(newValue);
|
||||
});
|
||||
|
||||
// set theme
|
||||
this.theme.subscribe(() => {
|
||||
this.applyTheme();
|
||||
});
|
||||
SettingsViewModel.generateThemeMedia().addEventListener("change", () =>
|
||||
this.applyTheme(),
|
||||
);
|
||||
}
|
||||
|
||||
static generateThemeMedia(): MediaQueryList {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)");
|
||||
}
|
||||
|
||||
static getSystemTheme(): string {
|
||||
const media = SettingsViewModel.generateThemeMedia();
|
||||
return media.matches == true ? ThemeSettings.Dark : ThemeSettings.Light;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-19
@@ -76,23 +76,21 @@ main {
|
||||
);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#background-wrapper {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#background-wrapper #sky {
|
||||
--color-1: hsl(188, 100%, 30%);
|
||||
--color-2: hsl(194, 72%, 24%);
|
||||
}
|
||||
|
||||
#background-wrapper #grass-1 {
|
||||
--color-1: hsl(120, 30%, 30%);
|
||||
--color-2: hsl(120, 30%, 24%);
|
||||
}
|
||||
|
||||
#background-wrapper #grass-2 {
|
||||
--color-1: hsl(110, 60%, 30%);
|
||||
--color-2: hsl(110, 50%, 24%);
|
||||
}
|
||||
body[theme="dark"] #background-wrapper {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
body[theme="dark"] #background-wrapper #sky {
|
||||
--color-1: hsl(188, 100%, 30%);
|
||||
--color-2: hsl(194, 72%, 24%);
|
||||
}
|
||||
|
||||
body[theme="dark"] #background-wrapper #grass-1 {
|
||||
--color-1: hsl(120, 30%, 30%);
|
||||
--color-2: hsl(120, 30%, 24%);
|
||||
}
|
||||
|
||||
body[theme="dark"] #background-wrapper #grass-2 {
|
||||
--color-1: hsl(110, 60%, 30%);
|
||||
--color-2: hsl(110, 50%, 24%);
|
||||
}
|
||||
|
||||
+21
-29
@@ -12,15 +12,13 @@
|
||||
--surface-active: hsla(var(--hue), 100%, 5%, 15%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
[color]:not([color="standard"]) {
|
||||
--accent-luma: var(--accent-luma-dark);
|
||||
--accent-text-luma: var(--accent-text-luma-dark);
|
||||
body[theme="dark"] [color]:not([color="standard"]) {
|
||||
--accent-luma: var(--accent-luma-dark);
|
||||
--accent-text-luma: var(--accent-text-luma-dark);
|
||||
|
||||
--surface: hsla(var(--hue), 100%, 95%, 5%);
|
||||
--surface-hover: hsla(var(--hue), 100%, 95%, 10%);
|
||||
--surface-active: hsla(var(--hue), 100%, 95%, 15%);
|
||||
}
|
||||
--surface: hsla(var(--hue), 100%, 95%, 5%);
|
||||
--surface-hover: hsla(var(--hue), 100%, 95%, 10%);
|
||||
--surface-active: hsla(var(--hue), 100%, 95%, 15%);
|
||||
}
|
||||
|
||||
/* tile */
|
||||
@@ -44,16 +42,14 @@
|
||||
--text-normal: hsl(var(--hue), 100%, var(--text-luma));
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
[color]:not([color="standard"]).tile {
|
||||
--gradient-luma-1: 20%;
|
||||
--gradient-luma-2: 15%;
|
||||
body[theme="dark"] [color]:not([color="standard"]).tile {
|
||||
--gradient-luma-1: 20%;
|
||||
--gradient-luma-2: 15%;
|
||||
|
||||
--hover-luma: 25%;
|
||||
--active-luma: 30%;
|
||||
--hover-luma: 25%;
|
||||
--active-luma: 30%;
|
||||
|
||||
--text-luma: 90%;
|
||||
}
|
||||
--text-luma: 90%;
|
||||
}
|
||||
|
||||
/* fill */
|
||||
@@ -74,11 +70,9 @@
|
||||
background: hsl(var(--hue), var(--sat), var(--luma), 40%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
[color]:not([color="standard"]).background {
|
||||
--luma: var(--background-luma-dark);
|
||||
--sat: 100%;
|
||||
}
|
||||
body[theme="dark"] [color]:not([color="standard"]).background {
|
||||
--luma: var(--background-luma-dark);
|
||||
--sat: 100%;
|
||||
}
|
||||
|
||||
[color="standard"].background {
|
||||
@@ -92,15 +86,13 @@
|
||||
background: hsl(var(--hue), 100%, var(--luma), 50%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
[color]:not([color="standard"]).subtle-background {
|
||||
--luma: var(--subtle-luma-dark);
|
||||
}
|
||||
body[theme="dark"] [color]:not([color="standard"]).subtle-background {
|
||||
--luma: var(--subtle-luma-dark);
|
||||
}
|
||||
|
||||
@media (display-mode: standalone) {
|
||||
[color]:not([color="standard"]).subtle-background {
|
||||
--luma: 0%;
|
||||
}
|
||||
@media (display-mode: standalone) {
|
||||
[color]:not([color="standard"]).subtle-background {
|
||||
--luma: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user