Merge pull request #3503 from matrix-org/bwindels/custom-themes-mvp
Custom theming MVP
This commit is contained in:
commit
c8c4dc29d1
13 changed files with 299 additions and 110 deletions
|
@ -15,7 +15,7 @@ order of prioirty, are:
|
||||||
* `room-account` - The current user's account, but only when in a specific room
|
* `room-account` - The current user's account, but only when in a specific room
|
||||||
* `account` - The current user's account
|
* `account` - The current user's account
|
||||||
* `room` - A specific room (setting for all members of the room)
|
* `room` - A specific room (setting for all members of the room)
|
||||||
* `config` - Values are defined by `config.json`
|
* `config` - Values are defined by the `settingDefaults` key (usually) in `config.json`
|
||||||
* `default` - The hardcoded default for the settings
|
* `default` - The hardcoded default for the settings
|
||||||
|
|
||||||
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure
|
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure
|
||||||
|
|
|
@ -70,10 +70,10 @@ limitations under the License.
|
||||||
.mx_RoomSubList_badge {
|
.mx_RoomSubList_badge {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: $accent-fg-color;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
|
color: $roomtile-badge-fg-color;
|
||||||
background-color: $roomtile-name-color;
|
background-color: $roomtile-name-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSubList_badgeHighlight {
|
.mx_RoomSubList_badgeHighlight {
|
||||||
|
color: $accent-fg-color;
|
||||||
background-color: $warning-color;
|
background-color: $warning-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,7 @@ limitations under the License.
|
||||||
flex: 0 1 content;
|
flex: 0 1 content;
|
||||||
border-radius: 0.8em;
|
border-radius: 0.8em;
|
||||||
padding: 0 0.4em;
|
padding: 0 0.4em;
|
||||||
color: $accent-fg-color;
|
color: $roomtile-badge-fg-color;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
@ -156,6 +156,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_RoomTile_highlight .mx_RoomTile_badge,
|
.mx_RoomTile_highlight .mx_RoomTile_badge,
|
||||||
.mx_RoomTile_badge.mx_RoomTile_badgeRed {
|
.mx_RoomTile_badge.mx_RoomTile_badgeRed {
|
||||||
|
color: $accent-fg-color;
|
||||||
background-color: $warning-color;
|
background-color: $warning-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
6
res/themes/dark-custom/css/dark-custom.scss
Normal file
6
res/themes/dark-custom/css/dark-custom.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
@import "../../light/css/_paths.scss";
|
||||||
|
@import "../../light/css/_fonts.scss";
|
||||||
|
@import "../../light/css/_light.scss";
|
||||||
|
@import "../../dark/css/_dark.scss";
|
||||||
|
@import "../../light-custom/css/_custom.scss";
|
||||||
|
@import "../../../../res/css/_components.scss";
|
126
res/themes/light-custom/css/_custom.scss
Normal file
126
res/themes/light-custom/css/_custom.scss
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
//
|
||||||
|
// --accent-color
|
||||||
|
$accent-color: var(--accent-color);
|
||||||
|
$button-bg-color: var(--accent-color);
|
||||||
|
$button-link-fg-color: var(--accent-color);
|
||||||
|
$button-primary-bg-color: var(--accent-color);
|
||||||
|
$input-valid-border-color: var(--accent-color);
|
||||||
|
$reaction-row-button-selected-border-color: var(--accent-color);
|
||||||
|
$roomsublist-chevron-color: var(--accent-color);
|
||||||
|
$tab-label-active-bg-color: var(--accent-color);
|
||||||
|
$togglesw-on-color: var(--accent-color);
|
||||||
|
$username-variant3-color: var(--accent-color);
|
||||||
|
$accent-color-50pct: var(--accent-color-50pct); //still needs alpha at .5
|
||||||
|
//
|
||||||
|
// --timeline-background-color
|
||||||
|
$authpage-body-bg-color: var(--timeline-background-color);
|
||||||
|
$button-secondary-bg-color: var(--timeline-background-color);
|
||||||
|
$field-focused-label-bg-color: var(--timeline-background-color);
|
||||||
|
$lightbox-border-color: var(--timeline-background-color);
|
||||||
|
$menu-bg-color: var(--timeline-background-color);
|
||||||
|
$avatar-bg-color: var(--timeline-background-color);
|
||||||
|
$message-action-bar-bg-color: var(--timeline-background-color);
|
||||||
|
$primary-bg-color: var(--timeline-background-color);
|
||||||
|
$roomtile-focused-bg-color: var(--timeline-background-color);
|
||||||
|
$togglesw-ball-color: var(--timeline-background-color);
|
||||||
|
$droptarget-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .5
|
||||||
|
$authpage-modal-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .59
|
||||||
|
//
|
||||||
|
// --roomlist-highlights-color
|
||||||
|
$roomtile-selected-bg-color: var(--roomlist-highlights-color);
|
||||||
|
//
|
||||||
|
// --sidebar-color
|
||||||
|
$interactive-tooltip-bg-color: var(--sidebar-color);
|
||||||
|
$tagpanel-bg-color: var(--sidebar-color);
|
||||||
|
$tooltip-timeline-bg-color: var(--sidebar-color);
|
||||||
|
$dialog-backdrop-color: var(--sidebar-color-50pct);
|
||||||
|
//
|
||||||
|
// --roomlist-background-color
|
||||||
|
$event-selected-color: var(--roomlist-background-color);
|
||||||
|
$header-panel-bg-color: var(--roomlist-background-color);
|
||||||
|
$reaction-row-button-bg-color: var(--roomlist-background-color);
|
||||||
|
$panel-gradient: var(--roomlist-background-color-0pct), var(--roomlist-background-color);
|
||||||
|
// these were #f2f5f8 instead of #f3f8fd, but close enough
|
||||||
|
$dark-panel-bg-color: var(--roomlist-background-color);
|
||||||
|
$input-lighter-bg-color: var(--roomlist-background-color);
|
||||||
|
$plinth-bg-color: var(--roomlist-background-color);
|
||||||
|
$roomsublist-background: var(--roomlist-background-color);
|
||||||
|
$secondary-accent-color: var(--roomlist-background-color);
|
||||||
|
$selected-color: var(--roomlist-background-color);
|
||||||
|
$widget-menu-bar-bg-color: var(--roomlist-background-color);
|
||||||
|
$roomtile-badge-fg-color: var(--roomlist-background-color);
|
||||||
|
//
|
||||||
|
// --timeline-text-color
|
||||||
|
$message-action-bar-fg-color: var(--timeline-text-color);
|
||||||
|
$primary-fg-color: var(--timeline-text-color);
|
||||||
|
$settings-profile-overlay-placeholder-fg-color: var(--timeline-text-color);
|
||||||
|
$roomtopic-color: var(--timeline-text-color-50pct);
|
||||||
|
$tab-label-fg-color: var(--timeline-text-color);
|
||||||
|
$tab-label-icon-bg-color: var(--timeline-text-color); //was #454545
|
||||||
|
// was #212121
|
||||||
|
$topleftmenu-color: var(--timeline-text-color);
|
||||||
|
// was #45474a
|
||||||
|
$dialog-title-fg-color: var(--timeline-text-color);
|
||||||
|
$tab-label-fg-color: var(--timeline-text-color);
|
||||||
|
// was #4e5054
|
||||||
|
$authpage-lang-color: var(--timeline-text-color);
|
||||||
|
$roomheader-color: var(--timeline-text-color);
|
||||||
|
//
|
||||||
|
// --roomlist-text-color
|
||||||
|
$roomtile-notified-color: var(--roomlist-text-color);
|
||||||
|
$roomtile-selected-color: var(--roomlist-text-color);
|
||||||
|
//
|
||||||
|
// --roomlist-text-secondary-color
|
||||||
|
$roomsublist-label-fg-color: var(--roomlist-text-secondary-color);
|
||||||
|
$roomtile-name-color: var(--roomlist-text-secondary-color);
|
||||||
|
//
|
||||||
|
// --roomlist-separator-color
|
||||||
|
$input-darker-bg-color: var(--roomlist-separator-color);
|
||||||
|
$panel-divider-color: var(--roomlist-separator-color);// originally #dee1f3, but close enough
|
||||||
|
$primary-hairline-color: var(--roomlist-separator-color);// originally #e5e5e5, but close enough
|
||||||
|
//
|
||||||
|
// --timeline-text-secondary-color
|
||||||
|
$authpage-secondary-color: var(--timeline-text-secondary-color);
|
||||||
|
$memberstatus-placeholder-color: var(--timeline-text-secondary-color);
|
||||||
|
$notice-secondary-color: var(--timeline-text-secondary-color);
|
||||||
|
$pinned-color: var(--timeline-text-secondary-color);
|
||||||
|
$settings-subsection-fg-color: var(--timeline-text-secondary-color);
|
||||||
|
$roomheader-addroom-bg-color: var(--timeline-text-secondary-color);
|
||||||
|
// was #747474
|
||||||
|
$light-fg-color: var(--timeline-text-secondary-color);
|
||||||
|
// was #777777
|
||||||
|
$blockquote-fg-color: var(--timeline-text-secondary-color);
|
||||||
|
// was #888888
|
||||||
|
$greyed-fg-color: var(--timeline-text-secondary-color);
|
||||||
|
$info-plinth-fg-color: var(--timeline-text-secondary-color);
|
||||||
|
$preview-widget-fg-color: var(--timeline-text-secondary-color);
|
||||||
|
//
|
||||||
|
// --primary-color
|
||||||
|
$accent-color-alt: var(--primary-color);
|
||||||
|
$input-focused-border-color: var(--primary-color);
|
||||||
|
//
|
||||||
|
// --warning-color
|
||||||
|
$button-danger-bg-color: var(--warning-color);
|
||||||
|
$event-highlight-fg-color: var(--warning-color);
|
||||||
|
$input-invalid-border-color: var(--warning-color);
|
||||||
|
$mention-user-pill-bg-color: var(--warning-color);
|
||||||
|
$notice-primary-color: var(--warning-color);
|
||||||
|
$pinned-unread-color: var(--warning-color);
|
||||||
|
$warning-color: var(--warning-color);
|
||||||
|
$button-danger-disabled-bg-color: var(--warning-color-50pct); // still needs alpha at 0.5
|
5
res/themes/light-custom/css/light-custom.scss
Normal file
5
res/themes/light-custom/css/light-custom.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@import "../../light/css/_paths.scss";
|
||||||
|
@import "../../light/css/_fonts.scss";
|
||||||
|
@import "../../light/css/_light.scss";
|
||||||
|
@import "_custom.scss";
|
||||||
|
@import "../../../../res/css/_components.scss";
|
|
@ -167,6 +167,7 @@ $header-divider-color: #91A1C0;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$roomtile-name-color: #61708b;
|
$roomtile-name-color: #61708b;
|
||||||
|
$roomtile-badge-fg-color: $accent-fg-color;
|
||||||
$roomtile-selected-color: #212121;
|
$roomtile-selected-color: #212121;
|
||||||
$roomtile-notified-color: #212121;
|
$roomtile-notified-color: #212121;
|
||||||
$roomtile-selected-bg-color: #fff;
|
$roomtile-selected-bg-color: #fff;
|
||||||
|
@ -234,7 +235,7 @@ $tab-label-active-fg-color: #ffffff;
|
||||||
$tab-label-bg-color: transparent;
|
$tab-label-bg-color: transparent;
|
||||||
$tab-label-active-bg-color: $accent-color;
|
$tab-label-active-bg-color: $accent-color;
|
||||||
$tab-label-icon-bg-color: #454545;
|
$tab-label-icon-bg-color: #454545;
|
||||||
$tab-label-active-icon-bg-color: #ffffff;
|
$tab-label-active-icon-bg-color: $tab-label-active-fg-color;
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
$button-primary-fg-color: #ffffff;
|
$button-primary-fg-color: #ffffff;
|
||||||
|
|
|
@ -59,6 +59,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.
|
||||||
|
@ -661,7 +662,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
|
||||||
|
@ -1105,82 +1106,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
143
src/theme.js
Normal file
143
src/theme.js
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2019 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 {_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;
|
||||||
|
}
|
||||||
|
return Object.assign({}, customThemeNames, BUILTIN_THEMES);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCustomThemeVars(customTheme) {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCustomTheme(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}`);
|
||||||
|
}
|
||||||
|
return customTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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-")) {
|
||||||
|
const customTheme = getCustomTheme(theme.substr(7));
|
||||||
|
stylesheetName = customTheme.is_dark ? "dark-custom" : "light-custom";
|
||||||
|
setCustomThemeVars(customTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,24 +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"),
|
|
||||||
};
|
|
Loading…
Add table
Add a link
Reference in a new issue