1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Desktop: Load themes as CSS variables for use in custom themes and internal components

This commit is contained in:
Laurent Cozic 2021-09-06 16:57:07 +01:00
parent b8c941d2da
commit 478d4accf1
19 changed files with 441 additions and 58 deletions

View File

@ -598,6 +598,9 @@ packages/app-desktop/gui/Sidebar/styles/index.js.map
packages/app-desktop/gui/StatusScreen/StatusScreen.d.ts
packages/app-desktop/gui/StatusScreen/StatusScreen.js
packages/app-desktop/gui/StatusScreen/StatusScreen.js.map
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.d.ts
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js.map
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
packages/app-desktop/gui/SyncWizard/Dialog.js
packages/app-desktop/gui/SyncWizard/Dialog.js.map
@ -967,6 +970,9 @@ packages/lib/fs-driver-node.js.map
packages/lib/fsDriver.test.d.ts
packages/lib/fsDriver.test.js
packages/lib/fsDriver.test.js.map
packages/lib/hooks/useAsyncEffect.d.ts
packages/lib/hooks/useAsyncEffect.js
packages/lib/hooks/useAsyncEffect.js.map
packages/lib/hooks/useElementSize.d.ts
packages/lib/hooks/useElementSize.js
packages/lib/hooks/useElementSize.js.map
@ -1525,6 +1531,21 @@ packages/lib/services/spellChecker/SpellCheckerService.js.map
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.d.ts
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.js
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.js.map
packages/lib/services/style/cssToTheme.d.ts
packages/lib/services/style/cssToTheme.js
packages/lib/services/style/cssToTheme.js.map
packages/lib/services/style/cssToTheme.test.d.ts
packages/lib/services/style/cssToTheme.test.js
packages/lib/services/style/cssToTheme.test.js.map
packages/lib/services/style/loadCssToTheme.d.ts
packages/lib/services/style/loadCssToTheme.js
packages/lib/services/style/loadCssToTheme.js.map
packages/lib/services/style/themeToCss.d.ts
packages/lib/services/style/themeToCss.js
packages/lib/services/style/themeToCss.js.map
packages/lib/services/style/themeToCss.test.d.ts
packages/lib/services/style/themeToCss.test.js
packages/lib/services/style/themeToCss.test.js.map
packages/lib/services/synchronizer/ItemUploader.d.ts
packages/lib/services/synchronizer/ItemUploader.js
packages/lib/services/synchronizer/ItemUploader.js.map
@ -1786,6 +1807,9 @@ packages/renderer/utils.js.map
packages/tools/buildServerDocker.d.ts
packages/tools/buildServerDocker.js
packages/tools/buildServerDocker.js.map
packages/tools/convertThemesToCss.d.ts
packages/tools/convertThemesToCss.js
packages/tools/convertThemesToCss.js.map
packages/tools/generate-database-types.d.ts
packages/tools/generate-database-types.js
packages/tools/generate-database-types.js.map

24
.gitignore vendored
View File

@ -583,6 +583,9 @@ packages/app-desktop/gui/Sidebar/styles/index.js.map
packages/app-desktop/gui/StatusScreen/StatusScreen.d.ts
packages/app-desktop/gui/StatusScreen/StatusScreen.js
packages/app-desktop/gui/StatusScreen/StatusScreen.js.map
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.d.ts
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js.map
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
packages/app-desktop/gui/SyncWizard/Dialog.js
packages/app-desktop/gui/SyncWizard/Dialog.js.map
@ -952,6 +955,9 @@ packages/lib/fs-driver-node.js.map
packages/lib/fsDriver.test.d.ts
packages/lib/fsDriver.test.js
packages/lib/fsDriver.test.js.map
packages/lib/hooks/useAsyncEffect.d.ts
packages/lib/hooks/useAsyncEffect.js
packages/lib/hooks/useAsyncEffect.js.map
packages/lib/hooks/useElementSize.d.ts
packages/lib/hooks/useElementSize.js
packages/lib/hooks/useElementSize.js.map
@ -1510,6 +1516,21 @@ packages/lib/services/spellChecker/SpellCheckerService.js.map
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.d.ts
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.js
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.js.map
packages/lib/services/style/cssToTheme.d.ts
packages/lib/services/style/cssToTheme.js
packages/lib/services/style/cssToTheme.js.map
packages/lib/services/style/cssToTheme.test.d.ts
packages/lib/services/style/cssToTheme.test.js
packages/lib/services/style/cssToTheme.test.js.map
packages/lib/services/style/loadCssToTheme.d.ts
packages/lib/services/style/loadCssToTheme.js
packages/lib/services/style/loadCssToTheme.js.map
packages/lib/services/style/themeToCss.d.ts
packages/lib/services/style/themeToCss.js
packages/lib/services/style/themeToCss.js.map
packages/lib/services/style/themeToCss.test.d.ts
packages/lib/services/style/themeToCss.test.js
packages/lib/services/style/themeToCss.test.js.map
packages/lib/services/synchronizer/ItemUploader.d.ts
packages/lib/services/synchronizer/ItemUploader.js
packages/lib/services/synchronizer/ItemUploader.js.map
@ -1771,6 +1792,9 @@ packages/renderer/utils.js.map
packages/tools/buildServerDocker.d.ts
packages/tools/buildServerDocker.js
packages/tools/buildServerDocker.js.map
packages/tools/convertThemesToCss.d.ts
packages/tools/convertThemesToCss.js
packages/tools/convertThemesToCss.js.map
packages/tools/generate-database-types.d.ts
packages/tools/generate-database-types.js
packages/tools/generate-database-types.js.map

View File

@ -237,7 +237,7 @@ export default class ElectronAppWrapper {
const iid = setInterval(() => {
if (this.electronApp().isReady()) {
clearInterval(iid);
resolve();
resolve(null);
}
}, 10);
});

View File

@ -20,6 +20,7 @@ import DialogTitle from './DialogTitle';
import DialogButtonRow, { ButtonSpec, ClickEvent, ClickEventHandler } from './DialogButtonRow';
import Dialog from './Dialog';
import SyncWizardDialog from './SyncWizard/Dialog';
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
const { ImportScreen } = require('./ImportScreen.min.js');
const { ResourceScreen } = require('./ResourceScreen.js');
const { Navigator } = require('./Navigator.min.js');
@ -208,6 +209,7 @@ class RootComponent extends React.Component<Props, any> {
return (
<StyleSheetManager disableVendorPrefixes>
<ThemeProvider theme={theme}>
<StyleSheetContainer themeId={this.props.themeId}></StyleSheetContainer>
<MenuBar/>
<GlobalStyle/>
<Navigator style={navigatorStyle} screens={screens} />

View File

@ -16,6 +16,7 @@ import { ShareUserStatus, StateShare, StateShareUser } from '@joplin/lib/service
import { State } from '@joplin/lib/reducer';
import { connect } from 'react-redux';
import { reg } from '@joplin/lib/registry';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
const logger = Logger.create('ShareFolderDialog');
@ -100,20 +101,6 @@ interface RecipientDeleteEvent {
shareUserId: string;
}
interface AsyncEffectEvent {
cancelled: boolean;
}
function useAsyncEffect(effect: Function, dependencies: any[]) {
useEffect(() => {
const event = { cancelled: false };
effect(event);
return () => {
event.cancelled = true;
};
}, dependencies);
}
enum ShareState {
Idle = 0,
Synchronizing = 1,

View File

@ -0,0 +1,41 @@
// This component is perhaps a bit of a hack but the approach should be
// reliable. It converts the current (JS) theme to CSS, and add it to the HEAD
// tag. The component itself doesn't render anything where it's located (just an
// empty invisible DIV), so it means it could be put anywhere and would have the
// same effect.
//
// It's still reliable because the lifecyle of adding the CSS and removing on
// unmout is handled properly. There should only be one such component on the
// page.
import { useEffect, useState } from 'react';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import themeToCss from '@joplin/lib/services/style/themeToCss';
import { themeById } from '@joplin/lib/theme';
interface Props {
themeId: any;
}
export default function(props: Props): any {
const [styleSheetContent, setStyleSheetContent] = useState('');
useAsyncEffect(async (event: AsyncEffectEvent) => {
const theme = themeById(props.themeId);
const themeCss = themeToCss(theme);
if (event.cancelled) return;
setStyleSheetContent(themeCss);
}, [props.themeId]);
useEffect(() => {
const element = document.createElement('style');
element.setAttribute('id', 'main-theme-stylesheet-container');
document.head.appendChild(element);
element.appendChild(document.createTextNode(styleSheetContent));
return () => {
document.head.removeChild(element);
};
}, [styleSheetContent]);
return <div style={{ display: 'none' }}></div>;
}

View File

@ -64,28 +64,6 @@ a {
opacity: 1;
}
/*
.note-list .list-item-container:hover {
background-color: rgba(0,160,255,0.1) !important;
}
*/
/*
.editor-toolbar .button:not(.disabled):hover,
.header .button:not(.disabled):hover {
background-color: rgba(0,160,255,0.1);
border: 1px solid rgba(0,160,255,0.5);
box-sizing: 'border-box';
}
.editor-toolbar .button:not(.disabled):active,
.header .button:not(.disabled):active {
background-color: rgba(0,160,255,0.2);
border: 1px solid rgba(0,160,255,0.7);
box-sizing: 'border-box';
}
*/
.editor-toolbar .button,
.header .button {
border: 1px solid rgba(0,160,255,0);
@ -163,11 +141,6 @@ a {
to {transform: rotate(360deg);}
}
/* .joplin-tinymce .tox-editor-header {
padding-left: 88px;
padding-right: 150px;
} */
*:focus {
outline: none;
}

View File

@ -641,7 +641,7 @@ async function initialize(dispatch: Function) {
class AppComponent extends React.Component {
constructor() {
public constructor() {
super();
this.state = {
@ -684,7 +684,7 @@ class AppComponent extends React.Component {
// https://github.com/laurent22/joplin/issues/3807
// https://discourse.joplinapp.org/t/webdav-config-encryption-config-randomly-lost-on-android/11364
// https://discourse.joplinapp.org/t/android-keeps-on-resetting-my-sync-and-theme/11443
async componentDidMount() {
public async componentDidMount() {
if (this.props.appState == 'starting') {
this.props.dispatch({
type: 'APP_STATE_SET',
@ -737,13 +737,13 @@ class AppComponent extends React.Component {
// setTimeout(() => NavService.go('EncryptionConfig'), 2000);
}
componentWillUnmount() {
public componentWillUnmount() {
AppState.removeEventListener('change', this.onAppStateChange_);
Linking.removeEventListener('url', this.handleOpenURL_);
if (this.unsubscribeNetInfoHandler_) this.unsubscribeNetInfoHandler_();
}
componentDidUpdate(prevProps: any) {
public componentDidUpdate(prevProps: any) {
if (this.props.showSideMenu !== prevProps.showSideMenu) {
Animated.timing(this.state.sideMenuContentOpacity, {
toValue: this.props.showSideMenu ? 0.5 : 0,
@ -752,7 +752,7 @@ class AppComponent extends React.Component {
}
}
async backButtonHandler() {
private async backButtonHandler() {
if (this.props.noteSelectionEnabled) {
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
return true;
@ -773,7 +773,7 @@ class AppComponent extends React.Component {
return false;
}
async handleShareData() {
private async handleShareData() {
const sharedData = await ShareExtension.data();
if (sharedData) {
reg.logger().info('Received shared data');
@ -785,14 +785,14 @@ class AppComponent extends React.Component {
}
}
UNSAFE_componentWillReceiveProps(newProps: any) {
public UNSAFE_componentWillReceiveProps(newProps: any) {
if (newProps.syncStarted != this.lastSyncStarted_) {
if (!newProps.syncStarted) FoldersScreenUtils.refreshFolders();
this.lastSyncStarted_ = newProps.syncStarted;
}
}
sideMenu_change(isOpen: boolean) {
private sideMenu_change(isOpen: boolean) {
// Make sure showSideMenu property of state is updated
// when the menu is open/closed.
this.props.dispatch({
@ -800,7 +800,7 @@ class AppComponent extends React.Component {
});
}
render() {
public render() {
if (this.props.appState != 'ready') return null;
const theme = themeStyle(this.props.themeId);

View File

@ -21,6 +21,38 @@ export default class FsDriverBase {
throw new Error('Not implemented');
}
public async readFile(_path: string, _encoding: string = 'utf8'): Promise<any> {
throw new Error('Not implemented');
}
public async copy(_source: string, _dest: string) {
throw new Error('Not implemented');
}
public async mkdir(_path: string) {
throw new Error('Not implemented');
}
public async unlink(_path: string) {
throw new Error('Not implemented');
}
public async move(_source: string, _dest: string) {
throw new Error('Not implemented');
}
public async readFileChunk(_handle: any, _length: number, _encoding: string = 'base64'): Promise<string> {
throw new Error('Not implemented');
}
public async open(_path: string, _mode: any): Promise<any> {
throw new Error('Not implemented');
}
public async close(_handle: any): Promise<any> {
throw new Error('Not implemented');
}
public async readDirStats(_path: string, _options: ReadDirStatsOptions = null): Promise<Stat[]> {
throw new Error('Not implemented');
}

View File

@ -0,0 +1,18 @@
import shim from '../shim';
const { useEffect } = shim.react();
export interface AsyncEffectEvent {
cancelled: boolean;
}
export type EffectFunction = (event: AsyncEffectEvent)=> Promise<void>;
export default function(effect: EffectFunction, dependencies: any[]) {
useEffect(() => {
const event: AsyncEffectEvent = { cancelled: false };
void effect(event);
return () => {
event.cancelled = true;
};
}, dependencies);
}

View File

@ -6,7 +6,7 @@
"packages": {
"": {
"name": "@joplin/lib",
"version": "2.4.0",
"version": "2.4.1",
"license": "ISC",
"dependencies": {
"async-mutex": "^0.1.3",
@ -1003,6 +1003,12 @@
"@babel/types": "^7.3.0"
}
},
"node_modules/@types/css": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/css/-/css-0.0.33.tgz",
"integrity": "sha512-qjeDgh86R0LIeEM588q65yatc8Yyo/VvSIYFqq8JOIHDolhGNX0rz7k/OuxqDpnpqlefoHj8X4Ai/6hT9IWtKQ==",
"dev": true
},
"node_modules/@types/fs-extra": {
"version": "9.0.11",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.11.tgz",

View File

@ -0,0 +1,28 @@
import cssToTheme from './cssToTheme';
describe('cssToTheme', function() {
it('should convert a CSS string to a theme', async () => {
const input = `
:root {
--joplin-appearence: light;
--joplin-color: #333333;
--joplin-background-color: #778899;
/* Should skip this comment and empty lines */
--joplin-background-color-transparent: rgba(255,255,255,0.9);
}`;
const expected = {
appearence: 'light',
color: '#333333',
backgroundColor: '#778899',
backgroundColorTransparent: 'rgba(255,255,255,0.9)',
};
const actual = cssToTheme(input, 'test.css');
expect(actual).toEqual(expected);
});
});

View File

@ -0,0 +1,46 @@
import { Theme } from '../../themes/type';
// Need to include it that way due to a bug in the lib:
// https://github.com/reworkcss/css/pull/146#issuecomment-740412799
const cssParse = require('css/lib/parse');
function formatCssToThemeVariable(cssVariable: string): string {
const elements = cssVariable.substr(2).split('-');
if (elements[0] !== 'joplin') throw new Error(`CSS variable name must start with "--joplin": ${cssVariable}`);
elements.splice(0, 1);
return elements.map((e, i) => {
const c = i === 0 ? e[0] : e[0].toUpperCase();
return c + e.substr(1);
}).join('');
}
// function unquoteValue(v:string):string {
// if (v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"')) return v.substr(1, v.length - 2);
// return v;
// }
export default function cssToTheme(css: string, sourceFilePath: string): Theme {
const o = cssParse(css, {
silent: false,
source: sourceFilePath,
});
if (!o?.stylesheet?.rules?.length) throw new Error(`Invalid CSS color file: ${sourceFilePath}`);
// Need "as any" because outdated TS definition file
const rootRule = o.stylesheet.rules[0];
if (!rootRule.selectors.includes(':root')) throw new Error('`:root` rule not found');
const declarations: any[] = rootRule.declarations;
const output: any = {};
for (const declaration of declarations) {
if (declaration.type !== 'declaration') continue; // Skip comment lines
output[formatCssToThemeVariable(declaration.property)] = declaration.value;
}
return output;
}

View File

@ -0,0 +1,27 @@
import { Theme } from '../../themes/type';
import { filename } from '../../path-utils';
import shim from '../../shim';
import cssToTheme from './cssToTheme';
export default async function(cssBaseDir: string): Promise<Record<string, Theme>> {
const themeDirs = (await shim.fsDriver().readDirStats(cssBaseDir)).filter((f: any) => f.isDirectory());
const output: Record<string, Theme> = {};
for (const themeDir of themeDirs) {
const themeName = filename(themeDir.path);
const cssFile = `${cssBaseDir}/${themeDir.path}/colors.css`;
const cssContent = await shim.fsDriver().readFile(cssFile, 'utf8');
let themeId = themeName;
const manifestFile = `${cssBaseDir}/${themeDir.path}/manifest.json`;
if (await shim.fsDriver().exists(manifestFile)) {
const manifest = JSON.parse(await shim.fsDriver().readFile(manifestFile, 'utf8'));
if (manifest.id) themeId = manifest.id;
}
output[themeId] = cssToTheme(cssContent, cssFile);
}
return output;
}

View File

@ -0,0 +1,105 @@
import { Theme, ThemeAppearance } from '../../themes/type';
import themeToCss from './themeToCss';
const input: Theme = {
appearance: ThemeAppearance.Light,
// Color scheme "1" is the basic one, like used to display the note
// content. It's basically dark gray text on white background
backgroundColor: '#ffffff',
backgroundColorTransparent: 'rgba(255,255,255,0.9)',
oddBackgroundColor: '#eeeeee',
color: '#32373F', // For regular text
colorError: 'red',
colorWarn: 'rgb(228,86,0)',
colorWarnUrl: '#155BDA',
colorFaded: '#7C8B9E', // For less important text
colorBright: '#000000', // For important text
dividerColor: '#dddddd',
selectedColor: '#e5e5e5',
urlColor: '#155BDA',
// Color scheme "2" is used for the sidebar. It's white text over
// dark blue background.
backgroundColor2: '#313640',
color2: '#ffffff',
selectedColor2: '#131313',
colorError2: '#ff6c6c',
colorWarn2: '#ffcb81',
// Color scheme "3" is used for the config screens for example/
// It's dark text over gray background.
backgroundColor3: '#F4F5F6',
backgroundColorHover3: '#CBDAF1',
color3: '#738598',
// Color scheme "4" is used for secondary-style buttons. It makes a white
// button with blue text.
backgroundColor4: '#ffffff',
color4: '#2D6BDC',
raisedBackgroundColor: '#e5e5e5',
raisedColor: '#222222',
searchMarkerBackgroundColor: '#F7D26E',
searchMarkerColor: 'black',
warningBackgroundColor: '#FFD08D',
tableBackgroundColor: 'rgb(247, 247, 247)',
codeBackgroundColor: 'rgb(243, 243, 243)',
codeBorderColor: 'rgb(220, 220, 220)',
codeColor: 'rgb(0,0,0)',
blockQuoteOpacity: 0.7,
codeMirrorTheme: 'default',
codeThemeCss: 'atom-one-light.css',
};
const expected = `
:root {
--joplin-appearance: light;
--joplin-background-color: #ffffff;
--joplin-background-color-transparent: rgba(255,255,255,0.9);
--joplin-odd-background-color: #eeeeee;
--joplin-color: #32373F;
--joplin-color-error: red;
--joplin-color-warn: rgb(228,86,0);
--joplin-color-warn-url: #155BDA;
--joplin-color-faded: #7C8B9E;
--joplin-color-bright: #000000;
--joplin-divider-color: #dddddd;
--joplin-selected-color: #e5e5e5;
--joplin-url-color: #155BDA;
--joplin-background-color2: #313640;
--joplin-color2: #ffffff;
--joplin-selected-color2: #131313;
--joplin-color-error2: #ff6c6c;
--joplin-color-warn2: #ffcb81;
--joplin-background-color3: #F4F5F6;
--joplin-background-color-hover3: #CBDAF1;
--joplin-color3: #738598;
--joplin-background-color4: #ffffff;
--joplin-color4: #2D6BDC;
--joplin-raised-background-color: #e5e5e5;
--joplin-raised-color: #222222;
--joplin-search-marker-background-color: #F7D26E;
--joplin-search-marker-color: black;
--joplin-warning-background-color: #FFD08D;
--joplin-table-background-color: rgb(247, 247, 247);
--joplin-code-background-color: rgb(243, 243, 243);
--joplin-code-border-color: rgb(220, 220, 220);
--joplin-code-color: rgb(0,0,0);
--joplin-block-quote-opacity: 0.7;
--joplin-code-mirror-theme: default;
--joplin-code-theme-css: atom-one-light.css;
}`;
describe('themeToCss', function() {
it('should a theme to a CSS string', async () => {
const actual = themeToCss(input);
expect(actual.trim()).toBe(expected.trim());
});
});

View File

@ -0,0 +1,24 @@
import { Theme } from '../../themes/type';
const { camelCaseToDash, formatCssSize } = require('../../string-utils');
// function quoteCssValue(name: string, value: string): string {
// const needsQuote = ['appearance', 'codeMirrorTheme', 'codeThemeCss'].includes(name);
// if (needsQuote) return `'${value}'`;
// return value;
// }
export default function(theme: Theme) {
const lines = [];
lines.push(':root {');
for (const name in theme) {
const value = (theme as any)[name];
const newName = `--joplin-${camelCaseToDash(name)}`;
const formattedValue = typeof value === 'number' && newName.indexOf('opacity') < 0 ? formatCssSize(value) : value;
lines.push(`\t${newName}: ${formattedValue};`);
}
lines.push('}');
return lines.join('\n');
}

View File

@ -23,7 +23,7 @@ const themes: any = {
[Setting.THEME_OLED_DARK]: theme_oledDark,
};
function themeById(themeId: string) {
export function themeById(themeId: string) {
if (!themes[themeId]) throw new Error(`Invalid theme ID: ${themeId}`);
const output = Object.assign({}, themes[themeId]);
@ -365,7 +365,7 @@ function addExtraStyles(style: any) {
const themeCache_: any = {};
function themeStyle(themeId: number) {
export function themeStyle(themeId: number) {
if (!themeId) throw new Error('Theme must be specified');
const zoomRatio = 1;
@ -405,7 +405,7 @@ const cachedStyles_: any = {
// cacheKey must be a globally unique key, and must change whenever
// the dependencies of the style change. If the style depends only
// on the theme, a static string can be provided as a cache key.
function buildStyle(cacheKey: any, themeId: number, callback: Function) {
export function buildStyle(cacheKey: any, themeId: number, callback: Function) {
cacheKey = Array.isArray(cacheKey) ? cacheKey.join('_') : cacheKey;
// We clear the cache whenever switching themes
@ -425,5 +425,3 @@ function buildStyle(cacheKey: any, themeId: number, callback: Function) {
return cachedStyles_.styles[cacheKey].style;
}
export { themeStyle, buildStyle, themeById };

View File

@ -6,7 +6,7 @@
"packages": {
"": {
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.4.1",
"license": "MIT",
"dependencies": {
"font-awesome-filetypes": "^2.1.0",

View File

@ -0,0 +1,48 @@
import themeToCss from '@joplin/lib/services/style/themeToCss';
import * as fs from 'fs-extra';
import { rootDir } from './tool-utils';
import { filename } from '@joplin/lib/path-utils';
function themeIdFromName(name: string) {
const nameToId: Record<string, number> = {
light: 1,
dark: 2,
oledDark: 22,
solarizedLight: 3,
solarizedDark: 4,
dracula: 5,
nord: 6,
aritimDark: 7,
};
if (!nameToId[name]) throw new Error(`Invalid name: ${name}`);
return nameToId[name];
}
async function main() {
const baseThemeDir = `${rootDir}/packages/lib/themes`;
const themeFiles = (await fs.readdir(baseThemeDir)).filter(f => f.endsWith('.js') && f !== 'type.js');
for (const themeFile of themeFiles) {
const themeName = filename(themeFile);
const themeDir = `${baseThemeDir}/${themeName}`;
await fs.mkdirp(themeDir);
const cssFile = `${themeDir}/colors.css`;
const content = require(`${baseThemeDir}/${themeFile}`).default;
const newContent = themeToCss(content);
await fs.writeFile(cssFile, newContent, 'utf8');
const manifestFile = `${themeDir}/manifest.json`;
const manifestContent = {
id: themeIdFromName(themeName),
};
await fs.writeFile(manifestFile, JSON.stringify(manifestContent, null, '\t'), 'utf8');
}
}
main().catch((error) => {
console.error(error);
process.exit(1);
});