Move Appearance tab to ts

This commit is contained in:
Jorik Schellekens 2020-05-28 13:55:07 +01:00
parent 0fd5714e21
commit 81922231ed
9 changed files with 295 additions and 180 deletions

View file

@ -58,8 +58,8 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs';
import { ThemeWatcher } from "../../theme";
import { FontWatcher } from '../../FontWatcher';
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from '../../settings/watchers/FontWatcher';
import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer, IDeferred } from "../../utils/promise";
import ToastStore from "../../stores/ToastStore";

View file

@ -34,7 +34,7 @@ interface IProps extends React.InputHTMLAttributes<HTMLSelectElement | HTMLInput
id?: string,
// The element to create. Defaults to "input".
// To define options for a select, use <Field><option ... /></Field>
element?: "input" | " select" | "textarea",
element?: "input" | "select" | "textarea",
// The field's type (when used as an <input>). Defaults to "text".
type?: string,
// id of a <datalist> element for suggestions

View file

@ -20,34 +20,61 @@ import React from 'react';
import {_t} from "../../../../../languageHandler";
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
import * as sdk from "../../../../../index";
import {enumerateThemes, ThemeWatcher} from "../../../../../theme";
import { enumerateThemes } from "../../../../../theme";
import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher";
import Field from "../../../elements/Field";
import Slider from "../../../elements/Slider";
import AccessibleButton from "../../../elements/AccessibleButton";
import dis from "../../../../../dispatcher/dispatcher";
import { FontWatcher } from "../../../../../FontWatcher";
import { FontWatcher } from "../../../../../settings/watchers/FontWatcher";
import { RecheckThemePayload } from '../../../../../dispatcher/payloads/RecheckThemePayload';
import { Action } from '../../../../../dispatcher/actions';
import { IValidationResult, IFieldState } from '../../../elements/Validation';
export default class AppearanceUserSettingsTab extends React.Component {
constructor() {
super();
interface IProps {
}
interface IState extends IThemeState {
// String displaying the current selected fontSize.
// Needs to be string for things like '17.' without
// trailing 0s.
fontSize: string,
customThemeUrl: string,
customThemeMessage: CustomThemeMessage,
useCustomFontSize: boolean,
}
export type CustomThemeMessage = {isError: boolean, text: string};
interface IThemeState {
theme: string,
useSystemTheme: boolean,
}
export default class AppearanceUserSettingsTab extends React.Component<IProps, IState> {
private themeTimer: NodeJS.Timeout;
constructor(props: IProps) {
super(props);
this.state = {
fontSize: SettingsStore.getValue("fontSize", null),
...this._calculateThemeState(),
fontSize: SettingsStore.getValue("fontSize", null).toString(),
...this.calculateThemeState(),
customThemeUrl: "",
customThemeMessage: {isError: false, text: ""},
useCustomFontSize: SettingsStore.getValue("useCustomFontSize"),
};
}
_calculateThemeState() {
private calculateThemeState(): IThemeState {
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
// show the right values for things.
const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
const systemThemeExplicit = SettingsStore.getValueAt(
const themeChoice: string = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
const systemThemeExplicit: boolean = SettingsStore.getValueAt(
SettingLevel.DEVICE, "use_system_theme", null, false, true);
const themeExplicit = SettingsStore.getValueAt(
const themeExplicit: string = SettingsStore.getValueAt(
SettingLevel.DEVICE, "theme", null, false, true);
// If the user has enabled system theme matching, use that.
@ -73,15 +100,15 @@ export default class AppearanceUserSettingsTab extends React.Component {
};
}
_onThemeChange = (e) => {
private onThemeChange(e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>): void {
const newTheme = e.target.value;
if (this.state.theme === newTheme) return;
// doing getValue in the .catch will still return the value we failed to set,
// so remember what the value was before we tried to set it so we can revert
const oldTheme = SettingsStore.getValue('theme');
const oldTheme: string = SettingsStore.getValue('theme');
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => {
dis.dispatch({action: 'recheck_theme'});
dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme});
this.setState({theme: oldTheme});
});
this.setState({theme: newTheme});
@ -91,23 +118,21 @@ export default class AppearanceUserSettingsTab extends React.Component {
// XXX: The local echoed value appears to be unreliable, in particular
// when settings custom themes(!) so adding forceTheme to override
// the value from settings.
dis.dispatch({action: 'recheck_theme', forceTheme: newTheme});
dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme, forceTheme: newTheme});
};
_onUseSystemThemeChanged = (checked) => {
private onUseSystemThemeChanged(checked: boolean) {
this.setState({useSystemTheme: checked});
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked);
dis.dispatch({action: 'recheck_theme'});
dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme});
};
_onFontSizeChanged = (size) => {
this.setState({fontSize: size});
private onFontSizeChanged(size: number) {
this.setState({fontSize: size.toString()});
SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size);
};
_onValidateFontSize = ({value}) => {
console.log({value});
private async onValidateFontSize({value}: Pick<IFieldState, "value">): Promise<IValidationResult> {
const parsedSize = parseFloat(value);
const min = FontWatcher.MIN_SIZE;
const max = FontWatcher.MAX_SIZE;
@ -127,17 +152,18 @@ export default class AppearanceUserSettingsTab extends React.Component {
return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
}
_onAddCustomTheme = async () => {
let currentThemes = SettingsStore.getValue("custom_themes");
private async onAddCustomTheme() {
let currentThemes: string[] = SettingsStore.getValue("custom_themes");
if (!currentThemes) currentThemes = [];
currentThemes = currentThemes.map(c => c); // cheap clone
if (this._themeTimer) {
clearTimeout(this._themeTimer);
if (this.themeTimer) {
clearTimeout(this.themeTimer);
}
try {
const r = await fetch(this.state.customThemeUrl);
// XXX: need some schema for this
const themeInfo = await r.json();
if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') {
this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}});
@ -153,42 +179,32 @@ export default class AppearanceUserSettingsTab extends React.Component {
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}});
this._themeTimer = setTimeout(() => {
this.themeTimer = setTimeout(() => {
this.setState({customThemeMessage: {text: "", isError: false}});
}, 3000);
};
_onCustomThemeChange = (e) => {
private onCustomThemeChange(e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) {
this.setState({customThemeUrl: e.target.value});
};
render() {
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Appearance")}</div>
{this._renderThemeSection()}
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this._renderFontSection() : null}
</div>
);
}
_renderThemeSection() {
private renderThemeSection() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch");
const themeWatcher = new ThemeWatcher();
let systemThemeSection;
let systemThemeSection: JSX.Element;
if (themeWatcher.isSystemThemeSupported()) {
systemThemeSection = <div>
<LabelledToggleSwitch
value={this.state.useSystemTheme}
label={SettingsStore.getDisplayName("use_system_theme")}
onChange={this._onUseSystemThemeChanged}
onChange={this.onUseSystemThemeChanged}
/>
</div>;
}
let customThemeForm;
let customThemeForm: JSX.Element;
if (SettingsStore.isFeatureEnabled("feature_custom_themes")) {
let messageElement = null;
if (this.state.customThemeMessage.text) {
@ -200,17 +216,17 @@ export default class AppearanceUserSettingsTab extends React.Component {
}
customThemeForm = (
<div className='mx_SettingsTab_section'>
<form onSubmit={this._onAddCustomTheme}>
<form onSubmit={this.onAddCustomTheme}>
<Field
label={_t("Custom theme URL")}
type='text'
id='mx_GeneralUserSettingsTab_customThemeInput'
autoComplete="off"
onChange={this._onCustomThemeChange}
onChange={this.onCustomThemeChange}
value={this.state.customThemeUrl}
/>
<AccessibleButton
onClick={this._onAddCustomTheme}
onClick={this.onAddCustomTheme}
type="submit" kind="primary_sm"
disabled={!this.state.customThemeUrl.trim()}
>{_t("Add theme")}</AccessibleButton>
@ -220,7 +236,8 @@ export default class AppearanceUserSettingsTab extends React.Component {
);
}
const themes = Object.entries(enumerateThemes())
// XXX: replace any type here
const themes = Object.entries<any>(enumerateThemes())
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
const customThemes = themes.filter(p => !builtInThemes.includes(p))
@ -232,7 +249,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
{systemThemeSection}
<Field
id="theme" label={_t("Theme")} element="select"
value={this.state.theme} onChange={this._onThemeChange}
value={this.state.theme} onChange={this.onThemeChange}
disabled={this.state.useSystemTheme}
>
{orderedThemes.map(theme => {
@ -245,7 +262,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
);
}
_renderFontSection() {
private renderFontSection() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_fontScaling">
<span className="mx_SettingsTab_subheading">{_t("Font size")}</span>
@ -253,9 +270,9 @@ export default class AppearanceUserSettingsTab extends React.Component {
<div className="mx_AppearanceUserSettingsTab_fontSlider_smallText">Aa</div>
<Slider
values={[13, 15, 16, 18, 20]}
value={this.state.fontSize}
onSelectionChange={this._onFontSizeChanged}
displayFunc={value => {}}
value={parseInt(this.state.fontSize, 10)}
onSelectionChange={this.onFontSizeChanged}
displayFunc={value => ""}
disabled={this.state.useCustomFontSize}
/>
<div className="mx_AppearanceUserSettingsTab_fontSlider_largeText">Aa</div>
@ -263,7 +280,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
<SettingsFlag
name="useCustomFontSize"
level={SettingLevel.ACCOUNT}
onChange={(checked)=> this.setState({useCustomFontSize: checked})}
onChange={(checked) => this.setState({useCustomFontSize: checked})}
/>
<Field
type="text"
@ -272,10 +289,20 @@ export default class AppearanceUserSettingsTab extends React.Component {
placeholder={this.state.fontSize.toString()}
value={this.state.fontSize.toString()}
id="font_size_field"
onValidate={this._onValidateFontSize}
onValidate={this.onValidateFontSize}
onChange={(value) => this.setState({fontSize: value.target.value})}
disabled={!this.state.useCustomFontSize}
/>
</div>;
}
render() {
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Appearance")}</div>
{this.renderThemeSection()}
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null}
</div>
);
}
}

View file

@ -43,5 +43,10 @@ export enum Action {
* Sets the current tooltip. Should be use with ViewTooltipPayload.
*/
ViewTooltip = "view_tooltip",
/**
* Forces the theme to reload. No additional payload information required.
*/
RecheckTheme = "recheck_theme",
}

View file

@ -0,0 +1,27 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ActionPayload } from "../payloads";
import { Action } from "../actions";
export interface RecheckThemePayload extends ActionPayload {
action: Action.RecheckTheme,
/**
* Optionally specify the exact theme which is to be loaded.
*/
forceTheme?: string;
}

View file

@ -14,38 +14,42 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import dis from './dispatcher/dispatcher';
import SettingsStore, {SettingLevel} from './settings/SettingsStore';
import dis from '../../dispatcher/dispatcher';
import SettingsStore, {SettingLevel} from '../SettingsStore';
import IWatcher from "./Watcher";
import { toPx } from '../../utils/units';
export class FontWatcher {
static MIN_SIZE = 13;
static MAX_SIZE = 20;
export class FontWatcher implements IWatcher {
public static readonly MIN_SIZE = 13;
public static readonly MAX_SIZE = 20;
private dispatcherRef: string;
constructor() {
this._dispatcherRef = null;
this.dispatcherRef = null;
}
start() {
this._setRootFontSize(SettingsStore.getValue("fontSize"));
this._dispatcherRef = dis.register(this._onAction);
public start() {
this.setRootFontSize(SettingsStore.getValue("fontSize"));
this.dispatcherRef = dis.register(this.onAction);
}
stop() {
dis.unregister(this._dispatcherRef);
public stop() {
dis.unregister(this.dispatcherRef);
}
_onAction = (payload) => {
private onAction = (payload) => {
if (payload.action === 'update-font-size') {
this._setRootFontSize(payload.size);
this.setRootFontSize(payload.size);
}
};
_setRootFontSize = (size) => {
private setRootFontSize = (size) => {
const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE);
if (fontSize != size) {
if (fontSize !== size) {
SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize);
}
document.querySelector(":root").style.fontSize = fontSize + "px";
(<HTMLElement>document.querySelector(":root")).style.fontSize = toPx(fontSize);
};
}

View file

@ -0,0 +1,138 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsStore, { SettingLevel } from '../SettingsStore';
import dis from '../../dispatcher/dispatcher';
import { Action } from '../../dispatcher/actions';
import ThemeController from "../controllers/ThemeController";
import { setTheme } from "../../theme";
import { ActionPayload } from '../../dispatcher/payloads';
export default class ThemeWatcher {
// XXX: I think this is unused.
static _instance = null;
private themeWatchRef: string;
private systemThemeWatchRef: string;
private dispatcherRef: string;
private preferDark: MediaQueryList;
private preferLight: MediaQueryList;
private currentTheme: string;
constructor() {
this.themeWatchRef = null;
this.systemThemeWatchRef = null;
this.dispatcherRef = null;
// we have both here as each may either match or not match, so by having both
// we can get the tristate of dark/light/unsupported
this.preferDark = (<any>global).matchMedia("(prefers-color-scheme: dark)");
this.preferLight = (<any>global).matchMedia("(prefers-color-scheme: light)");
this.currentTheme = this.getEffectiveTheme();
}
public start() {
this.themeWatchRef = SettingsStore.watchSetting("theme", null, this.onChange);
this.systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this.onChange);
if (this.preferDark.addEventListener) {
this.preferDark.addEventListener('change', this.onChange);
this.preferLight.addEventListener('change', this.onChange);
}
this.dispatcherRef = dis.register(this.onAction);
}
public stop() {
if (this.preferDark.addEventListener) {
this.preferDark.removeEventListener('change', this.onChange);
this.preferLight.removeEventListener('change', this.onChange);
}
SettingsStore.unwatchSetting(this.systemThemeWatchRef);
SettingsStore.unwatchSetting(this.themeWatchRef);
dis.unregister(this.dispatcherRef);
}
private onChange = () => {
this.recheck();
};
private onAction = (payload: ActionPayload) => {
if (payload.action === Action.RecheckTheme) {
// XXX forceTheme
this.recheck(payload.forceTheme);
}
};
// XXX: forceTheme param added here as local echo appears to be unreliable
// https://github.com/vector-im/riot-web/issues/11443
public recheck(forceTheme?: string) {
const oldTheme = this.currentTheme;
this.currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme;
if (oldTheme !== this.currentTheme) {
setTheme(this.currentTheme);
}
}
public getEffectiveTheme(): string {
// Dev note: Much of this logic is replicated in the AppearanceUserSettingsTab
// XXX: checking the isLight flag here makes checking it in the ThemeController
// itself completely redundant since we just override the result here and we're
// now effectively just using the ThemeController as a place to store the static
// variable. The system theme setting probably ought to have an equivalent
// controller that honours the same flag, although probablt better would be to
// have the theme logic in one place rather than split between however many
// different places.
if (ThemeController.isLogin) return 'light';
// If the user has specifically enabled the system matching option (excluding default),
// then use that over anything else. We pick the lowest possible level for the setting
// to ensure the ordering otherwise works.
const systemThemeExplicit = SettingsStore.getValueAt(
SettingLevel.DEVICE, "use_system_theme", null, false, true);
if (systemThemeExplicit) {
console.log("returning explicit system theme");
if (this.preferDark.matches) return 'dark';
if (this.preferLight.matches) return 'light';
}
// If the user has specifically enabled the theme (without the system matching option being
// enabled specifically and excluding the default), use that theme. We pick the lowest possible
// level for the setting to ensure the ordering otherwise works.
const themeExplicit = SettingsStore.getValueAt(
SettingLevel.DEVICE, "theme", null, false, true);
if (themeExplicit) {
console.log("returning explicit theme: " + themeExplicit);
return themeExplicit;
}
// If the user hasn't really made a preference in either direction, assume the defaults of the
// settings and use those.
if (SettingsStore.getValue('use_system_theme')) {
if (this.preferDark.matches) return 'dark';
if (this.preferLight.matches) return 'light';
}
console.log("returning theme value");
return SettingsStore.getValue('theme');
}
public isSystemThemeSupported() {
return this.preferDark.matches || this.preferLight.matches;
}
}

View file

@ -0,0 +1,20 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export default interface IWatcher {
start(): void
stop(): void
}

View file

@ -19,114 +19,8 @@ import {_t} from "./languageHandler";
export const DEFAULT_THEME = "light";
import Tinter from "./Tinter";
import dis from "./dispatcher/dispatcher";
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
import ThemeController from "./settings/controllers/ThemeController";
export class ThemeWatcher {
static _instance = null;
constructor() {
this._themeWatchRef = null;
this._systemThemeWatchRef = null;
this._dispatcherRef = null;
// we have both here as each may either match or not match, so by having both
// we can get the tristate of dark/light/unsupported
this._preferDark = global.matchMedia("(prefers-color-scheme: dark)");
this._preferLight = global.matchMedia("(prefers-color-scheme: light)");
this._currentTheme = this.getEffectiveTheme();
}
start() {
this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onChange);
this._systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this._onChange);
if (this._preferDark.addEventListener) {
this._preferDark.addEventListener('change', this._onChange);
this._preferLight.addEventListener('change', this._onChange);
}
this._dispatcherRef = dis.register(this._onAction);
}
stop() {
if (this._preferDark.addEventListener) {
this._preferDark.removeEventListener('change', this._onChange);
this._preferLight.removeEventListener('change', this._onChange);
}
SettingsStore.unwatchSetting(this._systemThemeWatchRef);
SettingsStore.unwatchSetting(this._themeWatchRef);
dis.unregister(this._dispatcherRef);
}
_onChange = () => {
this.recheck();
};
_onAction = (payload) => {
if (payload.action === 'recheck_theme') {
// XXX forceTheme
this.recheck(payload.forceTheme);
}
};
// XXX: forceTheme param added here as local echo appears to be unreliable
// https://github.com/vector-im/riot-web/issues/11443
recheck(forceTheme) {
const oldTheme = this._currentTheme;
this._currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme;
if (oldTheme !== this._currentTheme) {
setTheme(this._currentTheme);
}
}
getEffectiveTheme() {
// Dev note: Much of this logic is replicated in the AppearanceUserSettingsTab
// XXX: checking the isLight flag here makes checking it in the ThemeController
// itself completely redundant since we just override the result here and we're
// now effectively just using the ThemeController as a place to store the static
// variable. The system theme setting probably ought to have an equivalent
// controller that honours the same flag, although probablt better would be to
// have the theme logic in one place rather than split between however many
// different places.
if (ThemeController.isLogin) return 'light';
// If the user has specifically enabled the system matching option (excluding default),
// then use that over anything else. We pick the lowest possible level for the setting
// to ensure the ordering otherwise works.
const systemThemeExplicit = SettingsStore.getValueAt(
SettingLevel.DEVICE, "use_system_theme", null, false, true);
if (systemThemeExplicit) {
console.log("returning explicit system theme");
if (this._preferDark.matches) return 'dark';
if (this._preferLight.matches) return 'light';
}
// If the user has specifically enabled the theme (without the system matching option being
// enabled specifically and excluding the default), use that theme. We pick the lowest possible
// level for the setting to ensure the ordering otherwise works.
const themeExplicit = SettingsStore.getValueAt(
SettingLevel.DEVICE, "theme", null, false, true);
if (themeExplicit) {
console.log("returning explicit theme: " + themeExplicit);
return themeExplicit;
}
// If the user hasn't really made a preference in either direction, assume the defaults of the
// settings and use those.
if (SettingsStore.getValue('use_system_theme')) {
if (this._preferDark.matches) return 'dark';
if (this._preferLight.matches) return 'light';
}
console.log("returning theme value");
return SettingsStore.getValue('theme');
}
isSystemThemeSupported() {
return this._preferDark.matches || this._preferLight.matches;
}
}
import SettingsStore from "./settings/SettingsStore";
import ThemeWatcher from "./settings/watchers/ThemeWatcher";
export function enumerateThemes() {
const BUILTIN_THEMES = {