You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Desktop: Load themes as CSS variables for use in custom themes and internal components
This commit is contained in:
		| @@ -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); | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user