support custom themes from setting
also move theme setting code from MatrixChat to own file.
This commit is contained in:
parent
79d4434c9f
commit
558f8daeeb
6 changed files with 151 additions and 107 deletions
|
@ -56,6 +56,7 @@ import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||||
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
||||||
import DMRoomMap from '../../utils/DMRoomMap';
|
import DMRoomMap from '../../utils/DMRoomMap';
|
||||||
import { countRoomsWithNotif } from '../../RoomNotifs';
|
import { countRoomsWithNotif } from '../../RoomNotifs';
|
||||||
|
import { setTheme } from "../../theme";
|
||||||
|
|
||||||
// Disable warnings for now: we use deprecated bluebird functions
|
// Disable warnings for now: we use deprecated bluebird functions
|
||||||
// and need to migrate, but they spam the console with warnings.
|
// and need to migrate, but they spam the console with warnings.
|
||||||
|
@ -658,7 +659,7 @@ export default createReactClass({
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'set_theme':
|
case 'set_theme':
|
||||||
this._onSetTheme(payload.value);
|
setTheme(payload.value);
|
||||||
break;
|
break;
|
||||||
case 'on_logging_in':
|
case 'on_logging_in':
|
||||||
// We are now logging in, so set the state to reflect that
|
// We are now logging in, so set the state to reflect that
|
||||||
|
@ -1102,82 +1103,6 @@ export default createReactClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Called whenever someone changes the theme
|
|
||||||
*
|
|
||||||
* @param {string} theme new theme
|
|
||||||
*/
|
|
||||||
_onSetTheme: function(theme) {
|
|
||||||
if (!theme) {
|
|
||||||
theme = SettingsStore.getValue("theme");
|
|
||||||
}
|
|
||||||
|
|
||||||
// look for the stylesheet elements.
|
|
||||||
// styleElements is a map from style name to HTMLLinkElement.
|
|
||||||
const styleElements = Object.create(null);
|
|
||||||
let a;
|
|
||||||
for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
|
|
||||||
const href = a.getAttribute("href");
|
|
||||||
// shouldn't we be using the 'title' tag rather than the href?
|
|
||||||
const match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
|
|
||||||
if (match) {
|
|
||||||
styleElements[match[1]] = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(theme in styleElements)) {
|
|
||||||
throw new Error("Unknown theme " + theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
// disable all of them first, then enable the one we want. Chrome only
|
|
||||||
// bothers to do an update on a true->false transition, so this ensures
|
|
||||||
// that we get exactly one update, at the right time.
|
|
||||||
//
|
|
||||||
// ^ This comment was true when we used to use alternative stylesheets
|
|
||||||
// for the CSS. Nowadays we just set them all as disabled in index.html
|
|
||||||
// and enable them as needed. It might be cleaner to disable them all
|
|
||||||
// at the same time to prevent loading two themes simultaneously and
|
|
||||||
// having them interact badly... but this causes a flash of unstyled app
|
|
||||||
// which is even uglier. So we don't.
|
|
||||||
|
|
||||||
styleElements[theme].disabled = false;
|
|
||||||
|
|
||||||
const switchTheme = function() {
|
|
||||||
// we re-enable our theme here just in case we raced with another
|
|
||||||
// theme set request as per https://github.com/vector-im/riot-web/issues/5601.
|
|
||||||
// We could alternatively lock or similar to stop the race, but
|
|
||||||
// this is probably good enough for now.
|
|
||||||
styleElements[theme].disabled = false;
|
|
||||||
Object.values(styleElements).forEach((a) => {
|
|
||||||
if (a == styleElements[theme]) return;
|
|
||||||
a.disabled = true;
|
|
||||||
});
|
|
||||||
Tinter.setTheme(theme);
|
|
||||||
};
|
|
||||||
|
|
||||||
// turns out that Firefox preloads the CSS for link elements with
|
|
||||||
// the disabled attribute, but Chrome doesn't.
|
|
||||||
|
|
||||||
let cssLoaded = false;
|
|
||||||
|
|
||||||
styleElements[theme].onload = () => {
|
|
||||||
switchTheme();
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
|
||||||
const ss = document.styleSheets[i];
|
|
||||||
if (ss && ss.href === styleElements[theme].href) {
|
|
||||||
cssLoaded = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cssLoaded) {
|
|
||||||
styleElements[theme].onload = undefined;
|
|
||||||
switchTheme();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a chat with the welcome user, if the user doesn't already have one
|
* Starts a chat with the welcome user, if the user doesn't already have one
|
||||||
* @returns {string} The room ID of the new room, or null if no room was created
|
* @returns {string} The room ID of the new room, or null if no room was created
|
||||||
|
|
|
@ -27,7 +27,7 @@ import LanguageDropdown from "../../../elements/LanguageDropdown";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import {THEMES} from "../../../../../themes";
|
import {enumerateThemes} from "../../../../../theme";
|
||||||
import PlatformPeg from "../../../../../PlatformPeg";
|
import PlatformPeg from "../../../../../PlatformPeg";
|
||||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||||
import sdk from "../../../../..";
|
import sdk from "../../../../..";
|
||||||
|
@ -275,8 +275,8 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
|
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
|
||||||
<Field id="theme" label={_t("Theme")} element="select"
|
<Field id="theme" label={_t("Theme")} element="select"
|
||||||
value={this.state.theme} onChange={this._onThemeChange}>
|
value={this.state.theme} onChange={this._onThemeChange}>
|
||||||
{Object.entries(THEMES).map(([theme, text]) => {
|
{Object.entries(enumerateThemes()).map(([theme, text]) => {
|
||||||
return <option key={theme} value={theme}>{_t(text)}</option>;
|
return <option key={theme} value={theme}>{text}</option>;
|
||||||
})}
|
})}
|
||||||
</Field>
|
</Field>
|
||||||
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
|
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
|
||||||
|
|
|
@ -245,6 +245,10 @@ export const SETTINGS = {
|
||||||
default: "light",
|
default: "light",
|
||||||
controller: new ThemeController(),
|
controller: new ThemeController(),
|
||||||
},
|
},
|
||||||
|
"custom_themes": {
|
||||||
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
"webRtcAllowPeerToPeer": {
|
"webRtcAllowPeerToPeer": {
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||||
displayName: _td('Allow Peer-to-Peer for 1:1 calls'),
|
displayName: _td('Allow Peer-to-Peer for 1:1 calls'),
|
||||||
|
|
|
@ -16,12 +16,13 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SettingController from "./SettingController";
|
import SettingController from "./SettingController";
|
||||||
import {DEFAULT_THEME, THEMES} from "../../themes";
|
import {DEFAULT_THEME, enumerateThemes} from "../../theme";
|
||||||
|
|
||||||
export default class ThemeController extends SettingController {
|
export default class ThemeController extends SettingController {
|
||||||
getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) {
|
getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) {
|
||||||
|
const themes = enumerateThemes();
|
||||||
// Override in case some no longer supported theme is stored here
|
// Override in case some no longer supported theme is stored here
|
||||||
if (!THEMES[calculatedValue]) {
|
if (!themes[calculatedValue]) {
|
||||||
return DEFAULT_THEME;
|
return DEFAULT_THEME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
139
src/theme.js
Normal file
139
src/theme.js
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
|
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 {_t} from "./languageHandler";
|
||||||
|
|
||||||
|
export const DEFAULT_THEME = "light";
|
||||||
|
import Tinter from "./Tinter";
|
||||||
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
|
||||||
|
export function enumerateThemes() {
|
||||||
|
const BUILTIN_THEMES = {
|
||||||
|
"light": _t("Light theme"),
|
||||||
|
"dark": _t("Dark theme"),
|
||||||
|
};
|
||||||
|
const customThemes = SettingsStore.getValue("custom_themes");
|
||||||
|
const customThemeNames = {};
|
||||||
|
for (const {name} of customThemes) {
|
||||||
|
customThemeNames[`custom-${name}`] = name;
|
||||||
|
}
|
||||||
|
console.log("customThemeNames", customThemeNames);
|
||||||
|
return Object.assign({}, customThemeNames, BUILTIN_THEMES);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCustomThemeVars(themeName) {
|
||||||
|
// set css variables
|
||||||
|
const customThemes = SettingsStore.getValue("custom_themes");
|
||||||
|
if (!customThemes) {
|
||||||
|
throw new Error(`No custom themes set, can't set custom theme "${themeName}"`);
|
||||||
|
}
|
||||||
|
const customTheme = customThemes.find(t => t.name === themeName);
|
||||||
|
if (!customTheme) {
|
||||||
|
const knownNames = customThemes.map(t => t.name).join(", ");
|
||||||
|
throw new Error(`Can't find custom theme "${themeName}", only know ${knownNames}`);
|
||||||
|
}
|
||||||
|
const {style} = document.body;
|
||||||
|
if (customTheme.colors) {
|
||||||
|
for (const [name, hexColor] of Object.entries(customTheme.colors)) {
|
||||||
|
style.setProperty(`--${name}`, hexColor);
|
||||||
|
// uses #rrggbbaa to define the color with alpha values at 0% and 50%
|
||||||
|
style.setProperty(`--${name}-0pct`, hexColor + "00");
|
||||||
|
style.setProperty(`--${name}-50pct`, hexColor + "7F");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever someone changes the theme
|
||||||
|
*
|
||||||
|
* @param {string} theme new theme
|
||||||
|
*/
|
||||||
|
export function setTheme(theme) {
|
||||||
|
if (!theme) {
|
||||||
|
theme = SettingsStore.getValue("theme");
|
||||||
|
}
|
||||||
|
let stylesheetName = theme;
|
||||||
|
if (theme.startsWith("custom-")) {
|
||||||
|
stylesheetName = "light-custom";
|
||||||
|
const themeName = theme.substr(7);
|
||||||
|
setCustomThemeVars(themeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for the stylesheet elements.
|
||||||
|
// styleElements is a map from style name to HTMLLinkElement.
|
||||||
|
const styleElements = Object.create(null);
|
||||||
|
let a;
|
||||||
|
for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
|
||||||
|
const href = a.getAttribute("href");
|
||||||
|
// shouldn't we be using the 'title' tag rather than the href?
|
||||||
|
const match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
|
||||||
|
if (match) {
|
||||||
|
styleElements[match[1]] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(stylesheetName in styleElements)) {
|
||||||
|
throw new Error("Unknown theme " + stylesheetName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable all of them first, then enable the one we want. Chrome only
|
||||||
|
// bothers to do an update on a true->false transition, so this ensures
|
||||||
|
// that we get exactly one update, at the right time.
|
||||||
|
//
|
||||||
|
// ^ This comment was true when we used to use alternative stylesheets
|
||||||
|
// for the CSS. Nowadays we just set them all as disabled in index.html
|
||||||
|
// and enable them as needed. It might be cleaner to disable them all
|
||||||
|
// at the same time to prevent loading two themes simultaneously and
|
||||||
|
// having them interact badly... but this causes a flash of unstyled app
|
||||||
|
// which is even uglier. So we don't.
|
||||||
|
|
||||||
|
styleElements[stylesheetName].disabled = false;
|
||||||
|
|
||||||
|
const switchTheme = function() {
|
||||||
|
// we re-enable our theme here just in case we raced with another
|
||||||
|
// theme set request as per https://github.com/vector-im/riot-web/issues/5601.
|
||||||
|
// We could alternatively lock or similar to stop the race, but
|
||||||
|
// this is probably good enough for now.
|
||||||
|
styleElements[stylesheetName].disabled = false;
|
||||||
|
Object.values(styleElements).forEach((a) => {
|
||||||
|
if (a == styleElements[stylesheetName]) return;
|
||||||
|
a.disabled = true;
|
||||||
|
});
|
||||||
|
Tinter.setTheme(theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
// turns out that Firefox preloads the CSS for link elements with
|
||||||
|
// the disabled attribute, but Chrome doesn't.
|
||||||
|
|
||||||
|
let cssLoaded = false;
|
||||||
|
|
||||||
|
styleElements[stylesheetName].onload = () => {
|
||||||
|
switchTheme();
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||||
|
const ss = document.styleSheets[i];
|
||||||
|
if (ss && ss.href === styleElements[stylesheetName].href) {
|
||||||
|
cssLoaded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cssLoaded) {
|
||||||
|
styleElements[stylesheetName].onload = undefined;
|
||||||
|
switchTheme();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
|
||||||
|
|
||||||
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 {_td} from "./languageHandler";
|
|
||||||
|
|
||||||
export const DEFAULT_THEME = "light";
|
|
||||||
|
|
||||||
export const THEMES = {
|
|
||||||
"light": _td("Light theme"),
|
|
||||||
"dark": _td("Dark theme"),
|
|
||||||
"light-custom": _td("Custom theme (light)"),
|
|
||||||
};
|
|
Loading…
Add table
Add a link
Reference in a new issue