insomnia/packages/insomnia-app/app/plugins/misc.ts
2021-12-14 09:26:36 -05:00

335 lines
8.5 KiB
TypeScript

import Color from 'color';
import { unreachableCase } from 'ts-assert-unreachable';
import { getAppDefaultTheme } from '../common/constants';
import { render, THROW_ON_ERROR } from '../common/render';
import { ThemeSettings } from '../models/settings';
import type { Theme } from './index';
import { ColorScheme, getThemes } from './index';
interface ThemeBlock {
background?: {
default: string;
success?: string;
notice?: string;
warning?: string;
danger?: string;
surprise?: string;
info?: string;
};
foreground?: {
default: string;
success?: string;
notice?: string;
warning?: string;
danger?: string;
surprise?: string;
info?: string;
};
highlight?: {
default: string;
xxs?: string;
xs?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
};
}
type ThemeInner = ThemeBlock & {
rawCss?: string;
styles?:
| {
dialog?: ThemeBlock;
dialogFooter?: ThemeBlock;
dialogHeader?: ThemeBlock;
dropdown?: ThemeBlock;
editor?: ThemeBlock;
link?: ThemeBlock;
overlay?: ThemeBlock;
pane?: ThemeBlock;
paneHeader?: ThemeBlock;
sidebar?: ThemeBlock;
sidebarHeader?: ThemeBlock;
sidebarList?: ThemeBlock;
tooltip?: ThemeBlock;
transparentOverlay?: ThemeBlock;
}
| null;
};
export interface PluginTheme {
name: string;
displayName: string;
theme: ThemeInner;
}
export async function generateThemeCSS(theme: PluginTheme) {
const renderedTheme: ThemeInner = await render(
theme.theme,
theme.theme,
null,
THROW_ON_ERROR,
theme.name,
);
const n = theme.name;
let css = '';
// For the top-level variables, merge with the base theme to ensure that
// we have everything we need.
css += wrapStyles(
n,
'',
getThemeBlockCSS({
...renderedTheme,
background: { ..._baseTheme.background, ...renderedTheme.background },
foreground: { ..._baseTheme.foreground, ...renderedTheme.foreground },
highlight: { ..._baseTheme.highlight, ...renderedTheme.highlight },
}),
);
if (renderedTheme.styles) {
const styles = renderedTheme.styles;
// Dropdown Menus
css += wrapStyles(
n,
'.theme--dropdown__menu',
getThemeBlockCSS(styles.dropdown || styles.dialog),
);
// Tooltips
css += wrapStyles(n, '.theme--tooltip', getThemeBlockCSS(styles.tooltip || styles.dialog));
// Overlay
css += wrapStyles(
n,
'.theme--transparent-overlay',
getThemeBlockCSS(styles.transparentOverlay),
);
// Dialogs
css += wrapStyles(n, '.theme--dialog', getThemeBlockCSS(styles.dialog));
css += wrapStyles(n, '.theme--dialog__header', getThemeBlockCSS(styles.dialogHeader));
css += wrapStyles(n, '.theme--dialog__footer', getThemeBlockCSS(styles.dialogFooter));
// Panes
css += wrapStyles(n, '.theme--pane', getThemeBlockCSS(styles.pane));
css += wrapStyles(n, '.theme--pane__header', getThemeBlockCSS(styles.paneHeader));
// @ts-expect-error -- TSCONVERSION
css += wrapStyles(n, '.theme--app-header', getThemeBlockCSS(styles.appHeader));
// Sidebar Styles
css += wrapStyles(n, '.theme--sidebar', getThemeBlockCSS(styles.sidebar));
css += wrapStyles(n, '.theme--sidebar__list', getThemeBlockCSS(styles.sidebarList));
css += wrapStyles(n, '.theme--sidebar__header', getThemeBlockCSS(styles.sidebarHeader));
// Link
css += wrapStyles(n, '.theme--link', getThemeBlockCSS(styles.link));
// Code Editors
css += wrapStyles(n, '.theme--editor', getThemeBlockCSS(styles.editor));
// HACK: Dialog styles for CodeMirror dialogs too
css += wrapStyles(n, '.CodeMirror-info', getThemeBlockCSS(styles.dialog));
}
return css;
}
function getThemeBlockCSS(block?: ThemeBlock) {
if (!block) {
return '';
}
const indent = '\t';
let css = '';
const addColorVar = (variable: string, value?: string) => {
if (!value) {
return;
}
try {
const parsedColor = Color(value);
const rgb = parsedColor.rgb();
addVar(variable, rgb.string());
addVar(`${variable}-rgb`, rgb.array().join(', '));
} catch (err) {
console.log('Failed to parse theme color', value);
}
};
const addVar = (variable: string, value?: string) => {
if (!value) {
return;
}
css += `${indent}--${variable}: ${value};\n`;
};
const addComment = comment => {
css += `${indent}/* ${comment} */\n`;
};
const addNewLine = () => {
css += '\n';
};
if (block.background) {
const { background } = block;
addComment('Background');
addColorVar('color-bg', background.default);
addColorVar('color-success', background.success);
addColorVar('color-notice', background.notice);
addColorVar('color-warning', background.warning);
addColorVar('color-danger', background.danger);
addColorVar('color-surprise', background.surprise);
addColorVar('color-info', background.info);
addNewLine();
}
if (block.foreground) {
const { foreground } = block;
addComment('Foreground');
addColorVar('color-font', foreground.default);
addColorVar('color-font-success', foreground.success);
addColorVar('color-font-notice', foreground.notice);
addColorVar('color-font-warning', foreground.warning);
addColorVar('color-font-danger', foreground.danger);
addColorVar('color-font-surprise', foreground.surprise);
addColorVar('color-font-info', foreground.info);
addNewLine();
}
if (block.highlight) {
const { highlight } = block;
addComment('Highlight');
addColorVar('hl', highlight.default);
addColorVar('hl-xxs', highlight.xxs);
addColorVar('hl-xs', highlight.xs);
addColorVar('hl-sm', highlight.sm);
addColorVar('hl-md', highlight.md);
addColorVar('hl-lg', highlight.lg);
addColorVar('hl-xl', highlight.xl);
addNewLine();
}
return css.replace(/\s+$/, '');
}
function wrapStyles(theme: string, selector: string, styles: string) {
if (!styles) {
return '';
}
return [
`[theme="${theme}"] ${selector}, `,
`[subtheme="${theme}"] ${selector ? selector + '--sub' : ''} {`,
styles,
'}',
'',
'',
].join('\n');
}
export function getColorScheme({ autoDetectColorScheme }: ThemeSettings): ColorScheme {
if (!autoDetectColorScheme) {
return 'default';
}
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
return 'light';
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'default';
}
export async function applyColorScheme(settings: ThemeSettings) {
const scheme = getColorScheme(settings);
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);
}
}
export async function setTheme(themeName: string) {
if (!document) {
return;
}
const head = document.head;
const body = document.body;
if (!head || !body) {
return;
}
const themes: Theme[] = await getThemes();
// If theme isn't installed for some reason, set to the default
if (!themes.find(t => t.theme.name === themeName)) {
console.log(`[theme] Theme not found ${themeName}`);
themeName = getAppDefaultTheme();
}
body.setAttribute('theme', themeName);
for (const theme of themes) {
let themeCSS = (await generateThemeCSS(theme.theme)) + '\n';
const { name } = theme.theme;
const { rawCss } = theme.theme.theme;
let s = document.querySelector(`style[data-theme-name="${name}"]`);
if (!s) {
s = document.createElement('style');
s.setAttribute('data-theme-name', name);
head.appendChild(s);
}
if (typeof rawCss === 'string' && name === themeName) {
themeCSS += '\n\n' + rawCss;
}
s.innerHTML = themeCSS;
}
}
const _baseTheme = {
background: {
default: '#fff',
success: '#75ba24',
notice: '#d8c84d',
warning: '#ec8702',
danger: '#e15251',
surprise: '#6030BF',
info: '#20aed9',
},
foreground: {
default: '#666',
success: '#fff',
notice: '#fff',
warning: '#fff',
danger: '#fff',
surprise: '#fff',
info: '#fff',
},
highlight: {
default: 'rgba(130, 130, 130, 1)',
xxs: 'rgba(130, 130, 130, 0.05)',
xs: 'rgba(130, 130, 130, 0.1)',
sm: 'rgba(130, 130, 130, 0.25)',
md: 'rgba(130, 130, 130, 0.35)',
lg: 'rgba(130, 130, 130, 0.5)',
xl: 'rgba(130, 130, 130, 0.8)',
},
};