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:
parent
b8c941d2da
commit
478d4accf1
@ -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
24
.gitignore
vendored
@ -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
|
||||
|
@ -237,7 +237,7 @@ export default class ElectronAppWrapper {
|
||||
const iid = setInterval(() => {
|
||||
if (this.electronApp().isReady()) {
|
||||
clearInterval(iid);
|
||||
resolve();
|
||||
resolve(null);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
|
@ -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} />
|
||||
|
@ -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,
|
||||
|
41
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx
Normal file
41
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx
Normal 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>;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
|
18
packages/lib/hooks/useAsyncEffect.ts
Normal file
18
packages/lib/hooks/useAsyncEffect.ts
Normal 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);
|
||||
}
|
8
packages/lib/package-lock.json
generated
8
packages/lib/package-lock.json
generated
@ -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",
|
||||
|
28
packages/lib/services/style/cssToTheme.test.ts
Normal file
28
packages/lib/services/style/cssToTheme.test.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
46
packages/lib/services/style/cssToTheme.ts
Normal file
46
packages/lib/services/style/cssToTheme.ts
Normal 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;
|
||||
}
|
27
packages/lib/services/style/loadCssToTheme.ts
Normal file
27
packages/lib/services/style/loadCssToTheme.ts
Normal 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;
|
||||
}
|
105
packages/lib/services/style/themeToCss.test.ts
Normal file
105
packages/lib/services/style/themeToCss.test.ts
Normal 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());
|
||||
});
|
||||
|
||||
});
|
24
packages/lib/services/style/themeToCss.ts
Normal file
24
packages/lib/services/style/themeToCss.ts
Normal 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');
|
||||
}
|
@ -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 };
|
||||
|
2
packages/renderer/package-lock.json
generated
2
packages/renderer/package-lock.json
generated
@ -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",
|
||||
|
48
packages/tools/convertThemesToCss.ts
Normal file
48
packages/tools/convertThemesToCss.ts
Normal 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);
|
||||
});
|
Loading…
Reference in New Issue
Block a user