Improvements to theme selection (#3506)

This commit is contained in:
Dimitri Mitropoulos authored and GitHub committed 2021-07-13 23:49:32 +00:00
1 parent 769d6c16d4
commit ca0f6eb9d0
9 files changed
+434 -405

No files matched your search

+2 -5
View File
@@ -68,17 +68,14 @@ interface BaseSettings {
}
export type Settings = BaseModel & BaseSettings;
export const name = 'Settings';
export const type = 'Settings';
export const prefix = 'set';
export const canDuplicate = false;
export const canSync = false;
export type ThemeSettings = Pick<Settings, 'autoDetectColorScheme' | 'lightTheme' | 'darkTheme' | 'theme'>;
export const isSettings = (model: Pick<BaseModel, 'type'>): model is Settings => (
model.type === type
);
+21 -11
View File
@@ -1,9 +1,10 @@
import Color from 'color';
import { render, THROW_ON_ERROR } from '../common/render';
import { getThemes } from './index';
import { ColorScheme, getThemes } from './index';
import type { Theme } from './index';
import { getAppDefaultTheme } from '../common/constants';
import { Settings } from '../models/settings';
import { ThemeSettings } from '../models/settings';
import { unreachableCase } from 'ts-assert-unreachable';
interface ThemeBlock {
background?: {
@@ -222,8 +223,8 @@ function wrapStyles(theme: string, selector: string, styles: string) {
].join('\n');
}
export function getColorScheme(settings: Settings) {
if (!settings.autoDetectColorScheme) {
export function getColorScheme({ autoDetectColorScheme }: ThemeSettings): ColorScheme {
if (!autoDetectColorScheme) {
return 'default';
}
@@ -238,15 +239,24 @@ export function getColorScheme(settings: Settings) {
return 'default';
}
export async function applyColorScheme(settings: Settings) {
export async function applyColorScheme(settings: ThemeSettings) {
const scheme = getColorScheme(settings);
if (scheme === 'light') {
await setTheme(settings.lightTheme);
} else if (scheme === 'dark') {
await setTheme(settings.darkTheme);
} else {
await setTheme(settings.theme);
switch (scheme) {
case 'light':
await setTheme(settings.lightTheme);
break;
case 'dark':
await setTheme(settings.darkTheme);
break;
case 'default':
await setTheme(settings.theme);
break;
default:
unreachableCase(scheme);
}
}
@@ -1,4 +1,4 @@
import React, { ButtonHTMLAttributes, PureComponent, ReactNode } from 'react';
import React, { ButtonHTMLAttributes, CSSProperties, PureComponent, ReactNode } from 'react';
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { AUTOBIND_CFG } from '../../../common/constants';
@@ -14,6 +14,7 @@ export interface ButtonProps<T> {
id?: string,
// TODO(TSCONVERSION) figure out why so many components pass this yet it isn't used
title?: string;
style?: CSSProperties;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
@@ -32,7 +33,7 @@ class Button<T> extends PureComponent<ButtonProps<T>> {
}
render() {
const { children, disabled, tabIndex, className, type, id } = this.props;
const { children, disabled, tabIndex, className, type, id, style } = this.props;
return (
<button
disabled={disabled}
@@ -41,6 +42,7 @@ class Button<T> extends PureComponent<ButtonProps<T>> {
tabIndex={tabIndex}
className={className}
onClick={this._handleClick}
style={style}
>
{children}
</button>
@@ -10,14 +10,14 @@ import SettingsShortcuts from '../settings/shortcuts';
import General from '../settings/general';
import { ImportExport } from '../settings/import-export';
import Plugins from '../settings/plugins';
import Theme from '../settings/theme';
import { ThemePanel } from '../settings/theme-panel';
import * as models from '../../../models/index';
import { Curl } from 'node-libcurl';
import Tooltip from '../tooltip';
import { applyColorScheme } from '../../../plugins/misc';
import * as session from '../../../account/session';
import Account from '../settings/account';
import { showModal } from './index';
import { Settings } from '../../../models/settings';
export const TAB_INDEX_EXPORT = 1;
export const TAB_INDEX_SHORTCUTS = 3;
@@ -25,7 +25,7 @@ export const TAB_INDEX_THEMES = 2;
export const TAB_INDEX_PLUGINS = 5;
interface Props {
settings: any;
settings: Settings;
}
interface State {
@@ -50,53 +50,6 @@ class SettingsModal extends PureComponent<Props, State> {
});
}
hideSettingsModal() {
this.modal?.hide();
}
async _handleChangeTheme(themeName, colorScheme, persist = true) {
const { settings } = this.props;
let patch;
switch (colorScheme) {
case 'light':
patch = {
lightTheme: themeName,
};
break;
case 'dark':
patch = {
darkTheme: themeName,
};
break;
case 'default':
default:
patch = {
theme: themeName,
};
break;
}
applyColorScheme({ ...settings, ...patch });
if (persist) {
await models.settings.update(settings, patch);
}
}
async _handleAutoDetectColorSchemeChange(autoDetectColorScheme, persist = true) {
const { settings } = this.props;
applyColorScheme({ ...settings, autoDetectColorScheme });
if (persist) {
models.settings.update(settings, {
autoDetectColorScheme,
});
}
}
async _handleUpdateKeyBindings(hotKeyRegistry) {
models.settings.update(this.props.settings, {
hotKeyRegistry,
@@ -165,18 +118,11 @@ class SettingsModal extends PureComponent<Props, State> {
</TabPanel>
<TabPanel className="react-tabs__tab-panel pad scrollable">
<ImportExport
hideSettingsModal={this.hideSettingsModal}
hideSettingsModal={this.hide}
/>
</TabPanel>
<TabPanel className="react-tabs__tab-panel pad scrollable">
<Theme
handleChangeTheme={this._handleChangeTheme}
activeTheme={settings.theme}
handleAutoDetectColorSchemeChange={this._handleAutoDetectColorSchemeChange}
autoDetectColorScheme={settings.autoDetectColorScheme}
activeLightTheme={settings.lightTheme}
activeDarkTheme={settings.darkTheme}
/>
<ThemePanel />
</TabPanel>
<TabPanel className="react-tabs__tab-panel pad scrollable">
<SettingsShortcuts
@@ -0,0 +1,318 @@
import React, { FC } from 'react';
import styled from 'styled-components';
import { ColorScheme } from '../../../plugins';
import { PluginTheme } from '../../../plugins/misc';
import HelpTooltip from '../help-tooltip';
import { useThemes } from '../../hooks/theme';
const THEMES_PER_ROW = 5;
const isDark = (mode: 'dark' | 'light') => mode === 'dark';
const isLight = (mode: 'dark' | 'light') => mode === 'light';
const RootWrapper = styled.div({});
const CheckboxWrapper = styled.div({
marginLeft: 'var(--padding-md)',
});
const Themes = styled.div({
display: 'flex',
flexWrap: 'wrap',
});
const ThemeButton = styled.div<{ $isActive: boolean; $isInOsThemeMode: boolean }>(({ $isActive, $isInOsThemeMode }) => ({
position: 'relative',
margin: 'var(--padding-md) var(--padding-md)',
fontSize: 0,
borderRadius: 'var(--radius-md)',
overflow: 'hidden',
boxShadow: '0 0 0 1px var(--hl-sm)',
transition: 'all 150ms ease-out',
...($isActive ? {
boxShadow: '0 0 0 var(--padding-xs) var(--color-surprise)',
transform: 'scale(1.05)',
} : {}),
'&:hover': {
transform: 'scale(1.05)',
...($isInOsThemeMode ? { boxShadow: 'none' } : {}),
},
...($isInOsThemeMode ? {
'&:hover .overlay-wrapper': {
display: 'flex',
},
'&:hover .theme-preview': {
visibility: 'hidden', // this prevents alpha-blending problems with the underlying svg bleeding through
},
} : {}),
}));
const ThemeTitle = styled.h2({
marginTop: 0,
marginBottom: 'var(--padding-xs)',
fontSize: 'var(--font-size-md)',
});
const ThemeWrapper = styled.div({
maxWidth: `${100 / THEMES_PER_ROW}%`,
minWidth: 110,
marginBottom: 'var(--padding-md)',
marginTop: 'var(--padding-md)',
textAlign: 'center',
});
const ColorSchemeBadge = styled.div<{ $theme: 'dark' | 'light'}>(({ $theme }) => ({
position: 'absolute',
top: 0,
width: 12,
height: 12,
fill: 'white',
padding: 4,
background: 'var(--color-surprise)',
...(isDark($theme) ? {
right: 0,
borderBottomLeftRadius: 'var(--radius-md)',
} : {}),
...(isLight($theme) ? {
left: 0,
borderBottomRightRadius: 'var(--radius-md)',
} : {}),
}));
const OverlayWrapper = styled.div({
position: 'absolute',
height: '100%',
width: '100%',
alignItems: 'center',
flexWrap: 'nowrap',
justifyContent: 'space-evenly',
boxSizing: 'content-box',
display: 'none', // controlled by hover on the ThemeWrapper
});
const OverlaySide = styled.div<{ $theme: 'dark' | 'light' }>(({ $theme }) => ({
display: 'flex',
cursor: 'pointer',
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
height: '100%',
background: 'var(--color-bg)',
'& svg': {
opacity: 0.4,
fill: 'var(--color-fg)',
height: 20,
},
'&:hover svg': {
opacity: 1,
fill: 'var(--color-surprise)',
},
boxSizing: 'border-box',
border: '1px solid var(--hl-sm)',
...(isDark($theme) ? {
borderTopRightRadius: 'var(--radius-md)',
borderBottomRightRadius: 'var(--radius-md)',
borderLeftStyle: 'none',
} : {}),
...(isLight($theme) ? {
borderTopLeftRadius: 'var(--radius-md)',
borderBottomLeftRadius: 'var(--radius-md)',
borderRightStyle: 'none',
} : {}),
'&:hover': {
border: '1px solid var(--color-surprise)',
},
}));
const SunSvg = () => (
<svg viewBox="0 0 14 14">
<circle cx="7" cy="7" r="4" />
<rect x="6.5" y="12" width="1" height="2" rx="0.5" />
<rect x="6.5" width="1" height="2" rx="0.5" />
<rect x="2" y="6.5" width="1" height="2" rx="0.5" transform="rotate(90 2 6.5)" />
<rect x="14" y="6.5" width="1" height="2" rx="0.5" transform="rotate(90 14 6.5)" />
<rect x="3.81799" y="3.11084" width="1" height="2" rx="0.5" transform="rotate(135 3.81799 3.11084)" />
<rect x="12.3033" y="11.5962" width="1" height="2" rx="0.5" transform="rotate(135 12.3033 11.5962)" />
<rect x="10.889" y="3.81799" width="1" height="2" rx="0.5" transform="rotate(-135 10.889 3.81799)" />
<rect x="2.40381" y="12.3033" width="1" height="2" rx="0.5" transform="rotate(-135 2.40381 12.3033)" />
</svg>
);
const MoonSvg = () => (
<svg viewBox="0 0 14 14">
<path
d="M4.00005 11C7.86604 11 11 7.86604 11 4.00005C11 2.83978 10.7178 1.74545 10.2181 0.781982C12.4649 1.94714 14 4.29432 14 7.00005C14 10.866 10.866 14 7.00005 14C4.29432 14 1.94714 12.4649 0.781982 10.2181C1.74545 10.7178 2.83978 11 4.00005 11Z"
/>
</svg>
);
const ThemePreview: FC<{ theme: PluginTheme }> = ({ theme: { name: themeName } }) => (
<svg
/* @ts-expect-error -- TSCONVERSION */
theme={themeName}
className="theme-preview"
width="100%"
height="100%"
viewBox="0 0 500 300"
>
{/*
A WORD TO THE WISE: If you, dear traveler from the future, are here
for the purpose of theming things due to changes in the app structure,
please remember to add `--sub` to your classes or else the selected class'
theme variables will apply to all theme previews. Search your codebase
for `--sub` to see more.
*/}
{/* @ts-expect-error -- TSCONVERSION */}
<g subtheme={themeName}>
{/* App Header */}
<g className="theme--app-header--sub">
<rect x="0" y="0" width="100%" height="10%" style={{ fill: 'var(--color-bg)' }} />
</g>
{/* Panes */}
<g className="theme--pane--sub">
{/* Response Area */}
<rect x="0" y="10%" width="100%" height="100%" style={{ fill: 'var(--color-bg)' }} />
{/* URL Bars */}
<rect
x="25%"
y="10%"
width="100%"
height="10%"
className="theme--pane__header--sub"
style={{ fill: 'var(--color-bg)' }}
/>
{/* Send Button */}
<g>
<rect x="53%" y="10%" width="9%" height="10%" style={{ fill: 'var(--color-surprise)' }} />
</g>
</g>
{/* Sidebar */}
<g className="theme--sidebar--sub">
<rect x="0" y="10%" width="25%" height="100%" style={{ fill: 'var(--color-bg)' }} />
</g>
{/* Lines */}
<line x1="0%" x2="100%" y1="10%" y2="10%" strokeWidth="1" style={{ stroke: 'var(--hl-md)' }} />
<line x1="25%" x2="100%" y1="20%" y2="20%" strokeWidth="1" style={{ stroke: 'var(--hl-md)' }} />
<line x1="62%" x2="62%" y1="10%" y2="100%" strokeWidth="1" style={{ stroke: 'var(--hl-md)' }} />
<line x1="25%" x2="25%" y1="10%" y2="100%" strokeWidth="1" style={{ stroke: 'var(--hl-md)' }} />
{/* Color Squares */}
<rect x="40%" y="85%" width="5%" height="8%" style={{ fill: 'var(--color-success)' }} />
<rect x="50%" y="85%" width="5%" height="8%" style={{ fill: 'var(--color-info)' }} />
<rect x="60%" y="85%" width="5%" height="8%" style={{ fill: 'var(--color-warning)' }} />
<rect x="70%" y="85%" width="5%" height="8%" style={{ fill: 'var(--color-danger)' }} />
<rect x="80%" y="85%" width="5%" height="8%" style={{ fill: 'var(--color-surprise)' }} />
<rect x="90%" y="85%" width="5%" height="8%" style={{ fill: 'var(--color-info)' }} />
</g>
</svg>
);
const IndividualTheme: FC<{
isActive: boolean;
isDark: boolean;
isLight: boolean;
isInOsThemeMode: boolean;
onChangeTheme: (name: string, mode: ColorScheme) => Promise<void>;
theme: PluginTheme
}> = ({
isActive,
isDark,
isLight,
isInOsThemeMode,
onChangeTheme,
theme,
}) => {
const { displayName, name } = theme;
const onClickThemeButton = () => {
if (isInOsThemeMode) {
// The overlays handle this behavior in OS theme mode.
// React's event bubbling means that this will be fired when you click on an overlay, so we need to turn it off when in this mode.
// Even still, we don't want to risk some potnetial subpixel or z-index nonsense accidentally setting the default when know we shouldn't.
return;
}
return onChangeTheme(name, 'default');
};
return (
<ThemeWrapper>
<ThemeTitle>{displayName}</ThemeTitle>
<ThemeButton
onClick={onClickThemeButton}
$isActive={isActive}
$isInOsThemeMode={isInOsThemeMode}
>
{isInOsThemeMode ? (
<>
<OverlayWrapper className="overlay-wrapper">
<OverlaySide $theme="light" onClick={() => { onChangeTheme(name, 'light'); }}><SunSvg /></OverlaySide>
<OverlaySide $theme="dark" onClick={() => { onChangeTheme(name, 'dark'); }}><MoonSvg /></OverlaySide>
</OverlayWrapper>
{isActive && isDark ? (
<ColorSchemeBadge $theme="dark"><MoonSvg /></ColorSchemeBadge>
) : null}
{isActive && isLight ? (
<ColorSchemeBadge $theme="light"><SunSvg /></ColorSchemeBadge>
) : null}
</>
) : null}
<ThemePreview theme={theme} />
</ThemeButton>
</ThemeWrapper>
);
};
export const ThemePanel: FC = () => {
const {
themes,
activate,
changeAutoDetect,
isActive,
isActiveDark,
isActiveLight,
autoDetectColorScheme,
} = useThemes();
return (
<RootWrapper>
<CheckboxWrapper className="form-control form-control--thin">
<label className="inline-block">
Use OS color scheme
<HelpTooltip className="space-left">
Pick your prefered themes for light and dark
</HelpTooltip>
<input
type="checkbox"
name="autoDetectColorScheme"
checked={autoDetectColorScheme}
onChange={changeAutoDetect}
/>
</label>
</CheckboxWrapper>
<Themes>
{themes.map(theme => (
<IndividualTheme
key={theme.name}
theme={theme}
isActive={isActive(theme)}
onChangeTheme={activate}
isDark={isActiveDark(theme)}
isLight={isActiveLight(theme)}
isInOsThemeMode={autoDetectColorScheme}
/>
))}
</Themes>
</RootWrapper>
);
};
@@ -1,218 +0,0 @@
import React, { PureComponent } from 'react';
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { AUTOBIND_CFG } from '../../../common/constants';
import Button from '../base/button';
import type { Theme as ThemeType, ColorScheme } from '../../../plugins';
import { getThemes } from '../../../plugins';
import HelpTooltip from '../help-tooltip';
const THEMES_PER_ROW = 5;
interface Props {
handleChangeTheme: (arg0: string, arg1: ColorScheme) => void;
activeTheme: string;
handleAutoDetectColorSchemeChange: (arg0: boolean) => void;
autoDetectColorScheme: boolean;
activeLightTheme: string;
activeDarkTheme: string;
}
interface State {
themes: ThemeType[];
}
@autoBindMethodsForReact(AUTOBIND_CFG)
class Theme extends PureComponent<Props, State> {
state: State = {
themes: [],
}
componentDidMount() {
this._loadThemes();
}
async _loadThemes() {
const themes = await getThemes();
this.setState({
themes,
});
}
renderThemePreview(themeName: string) {
return (
/* @ts-expect-error -- TSCONVERSION */
<svg theme={themeName} width="100%" height="100%" viewBox="0 0 500 300">
{/*
A WORD TO THE WISE: If you, dear traveler from the future, are here
for the purpose of theming things due to changes in the app structure,
please remember to add `--sub` to your classes or else the selected class'
theme variables will apply to all theme previews. Search your codebase
for `--sub` to see more.
*/}
{/* @ts-expect-error -- TSCONVERSION */}
<g subtheme={themeName}>
{/* App Header */}
<g className="theme--app-header--sub">
<rect x="0" y="0" width="100%" height="10%" className="bg-fill" />
</g>
{/* Panes */}
<g className="theme--pane--sub">
{/* Response Area */}
<rect x="0" y="10%" width="100%" height="100%" className="bg-fill" />
{/* URL Bars */}
<rect
x="25%"
y="10%"
width="100%"
height="10%"
className="theme--pane__header--sub bg-fill"
/>
{/* Send Button */}
<g>
<rect x="53%" y="10%" width="9%" height="10%" className="surprise-fill" />
</g>
</g>
{/* Sidebar */}
<g className="theme--sidebar--sub">
<rect x="0" y="10%" width="25%" height="100%" className="bg-fill" />
</g>
{/* Lines */}
<line x1="0%" x2="100%" y1="10%" y2="10%" strokeWidth="1" className="hl-stroke" />
<line x1="25%" x2="100%" y1="20%" y2="20%" strokeWidth="1" className="hl-stroke" />
<line x1="62%" x2="62%" y1="10%" y2="100%" strokeWidth="1" className="hl-stroke" />
<line x1="25%" x2="25%" y1="10%" y2="100%" strokeWidth="1" className="hl-stroke" />
{/* Color Squares */}
<rect x="40%" y="85%" width="5%" height="8%" className="success-fill" />
<rect x="50%" y="85%" width="5%" height="8%" className="info-fill" />
<rect x="60%" y="85%" width="5%" height="8%" className="warning-fill" />
<rect x="70%" y="85%" width="5%" height="8%" className="danger-fill" />
<rect x="80%" y="85%" width="5%" height="8%" className="surprise-fill" />
<rect x="90%" y="85%" width="5%" height="8%" className="info-fill" />
</g>
</svg>
);
}
renderTheme(theme: ThemeType) {
const { handleChangeTheme, activeTheme, autoDetectColorScheme } = this.props;
const isActive = activeTheme === theme.theme.name;
const disabled = autoDetectColorScheme;
return (
<div
key={theme.theme.name}
className="themes__theme"
style={{
maxWidth: `${100 / THEMES_PER_ROW}%`,
}}>
<h2 className="txt-lg">{theme.theme.displayName}</h2>
<Button
disabled={disabled}
onClick={() => handleChangeTheme(theme.theme.name, 'default')}
className={isActive ? 'active' : ''}>
{this.renderThemePreview(theme.theme.name)}
</Button>
</div>
);
}
renderThemeRows() {
const { themes } = this.state;
const rows: ThemeType[][] = [];
let row: ThemeType[] = [];
for (const theme of themes) {
row.push(theme);
if (row.length === THEMES_PER_ROW) {
rows.push(row);
row = [];
}
}
// Push the last row if it wasn't finished
if (row.length) {
rows.push(row);
}
return rows.map((row, i) => (
<div key={i} className="themes__row">
{row.map(this.renderTheme)}
</div>
));
}
renderThemeSelect(scheme: 'light' | 'dark') {
const {
activeLightTheme,
activeDarkTheme,
handleChangeTheme,
autoDetectColorScheme,
} = this.props;
const { themes } = this.state;
const activeColorTheme =
scheme === 'light' ? activeLightTheme : activeDarkTheme;
return (
<div>
<div className="form-control form-control--outlined">
<label>
Preferred {scheme} color scheme
<HelpTooltip className="space-left">
Lets you choose the color scheme that is used in {scheme} mode.
</HelpTooltip>
<select
value={activeColorTheme}
onChange={(e) => handleChangeTheme(e.target.value, scheme)}
>
{themes.map((theme) => (
<option key={theme.theme.name} value={theme.theme.name}>
{theme.theme.displayName}
</option>
))}
</select>
</label>
</div>
<div className="themes select__preview">
<div className="themes__theme" style={{ maxWidth: '50%' }}>
<Button disabled={!autoDetectColorScheme}>
{this.renderThemePreview(activeColorTheme)}
</Button>
</div>
</div>
</div>
);
}
render() {
const { autoDetectColorScheme, handleAutoDetectColorSchemeChange } = this.props;
return (
<>
<div className="themes general__preview">{this.renderThemeRows()}</div>
<div className="form-control form-control--thin">
<label className="inline-block">
Use OS color scheme
<HelpTooltip className="space-left">
Lets you choose one theme for light mode and one for dark mode.
</HelpTooltip>
<input
type="checkbox"
name="autoDetectColorScheme"
checked={autoDetectColorScheme}
onChange={e => handleAutoDetectColorSchemeChange(e.target.checked)}
/>
</label>
</div>
<div className="form-row">
{this.renderThemeSelect('light')}
{this.renderThemeSelect('dark')}
</div>
</>
);
}
}
export default Theme;
@@ -1,109 +0,0 @@
.themes {
&.general__preview {
padding-top: var(--padding-lg);
}
&.select__preview {
padding-top: var(--padding-xxs);
padding-bottom: var(--padding-xxs);
display: flex;
align-items: center;
justify-content: center;
}
.themes__row {
display: flex;
flex-direction: row;
margin-bottom: var(--padding-lg);
margin-left: calc(var(--padding-md) * -1);
margin-right: calc(var(--padding-md) * -1);
}
.themes__theme {
text-align: center;
h2 {
margin-top: 0;
margin-bottom: var(--padding-xs);
font-size: var(--font-size-md) !important;
}
svg {
.bg-fill {
fill: var(--color-bg);
}
.success-fill {
fill: var(--color-success);
}
.notice-fill {
fill: var(--color-notice);
}
.warning-fill {
fill: var(--color-warning);
}
.danger-fill {
fill: var(--color-danger);
}
.surprise-fill {
fill: var(--color-surprise);
}
.info-fill {
fill: var(--color-info);
}
.fg-fill {
fill: var(--color-fg);
}
.hl-stroke {
stroke: var(--hl-md);
}
}
}
button {
position: relative;
margin: var(--padding-md) var(--padding-md);
font-size: 0;
border-radius: var(--radius-md);
overflow: hidden;
box-shadow: 0 0 0 1px var(--hl-sm);
&.active:enabled {
box-shadow: 0 0 0 var(--padding-xs) var(--color-surprise);
}
&.active:enabled,
&:hover:enabled {
transform: scale(1.05);
}
&:active {
transition: all 50ms ease-out;
}
&::after {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-weight: bold;
position: absolute;
font-size: var(--font-size-md);
text-transform: uppercase;
color: var(--hl-xl);
left: 0;
right: 0;
top: 0;
bottom: 0;
visibility: hidden;
}
}
}
@@ -55,7 +55,6 @@
@import 'components/unit-tests';
@import 'components/tabs';
@import 'components/tag';
@import 'components/themes';
@import 'components/toast';
@import 'components/request-url-bar';
@import 'components/workspace-dropdown';
@@ -0,0 +1,84 @@
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { unreachableCase } from 'ts-assert-unreachable';
import * as models from '../../models';
import { ThemeSettings } from '../../models/settings';
import { ColorScheme, getThemes } from '../../plugins';
import { applyColorScheme, PluginTheme } from '../../plugins/misc';
import { selectSettings } from '../redux/selectors';
export const useThemes = () => {
const {
lightTheme,
darkTheme,
autoDetectColorScheme,
theme,
pluginConfig,
} = useSelector(selectSettings);
const [themes, setThemes] = useState<PluginTheme[]>([]);
// Reload themes if pluginConfig changes
useEffect(() => {
const func = async () => {
const pluginThemes = await getThemes();
setThemes(pluginThemes.map(({ theme }) => theme));
};
func();
}, [pluginConfig]);
// Check if the theme is active
const isActiveDark = useCallback(({ name }: PluginTheme) => name === darkTheme, [darkTheme]);
const isActiveLight = useCallback(({ name }: PluginTheme) => name === lightTheme, [lightTheme]);
const isActive = useCallback((pluginTheme : PluginTheme) => {
if (autoDetectColorScheme) {
return isActiveLight(pluginTheme) || isActiveDark(pluginTheme);
}
return pluginTheme.name === theme;
}, [autoDetectColorScheme, isActiveDark, isActiveLight, theme]);
// Apply the theme and update settings
const apply = useCallback(async (patch: Partial<ThemeSettings>) => {
applyColorScheme({
theme,
autoDetectColorScheme,
darkTheme,
lightTheme,
...patch,
});
await models.settings.patch(patch);
}, [autoDetectColorScheme, darkTheme, lightTheme, theme]);
const changeAutoDetect = useCallback(({ target: { checked } }: ChangeEvent<HTMLInputElement>) => apply({ autoDetectColorScheme: checked }), [apply]);
// Activate the theme for the selected color scheme
const activate = useCallback(async (themeName: string, colorScheme: ColorScheme) => {
switch (colorScheme) {
case 'light':
await apply({ lightTheme: themeName });
break;
case 'dark':
await apply({ darkTheme: themeName });
break;
case 'default':
await apply({ theme: themeName });
break;
default:
unreachableCase(colorScheme);
}
}, [apply]);
return {
themes,
isActive,
isActiveLight,
isActiveDark,
activate,
changeAutoDetect,
autoDetectColorScheme,
};
};