wip
This commit is contained in:
		@@ -3,17 +3,6 @@
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * BOOT LOADER
 | 
			
		||||
 * サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで、以下の役割を持ちます。
 | 
			
		||||
 * - 翻訳ファイルをフェッチする。
 | 
			
		||||
 * - バージョンに基づいて適切なメインスクリプトを読み込む。
 | 
			
		||||
 * - キャッシュされたコンパイル済みテーマを適用する。
 | 
			
		||||
 * - クライアントの設定値に基づいて対応するHTMLクラス等を設定する。
 | 
			
		||||
 * テーマをこの段階で設定するのは、メインスクリプトが読み込まれる間もテーマを適用したいためです。
 | 
			
		||||
 * 注: webpackは介さないため、このファイルではrequireやimportは使えません。
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
 | 
			
		||||
@@ -320,6 +309,6 @@
 | 
			
		||||
			#errorInfo {
 | 
			
		||||
				width: 50%;
 | 
			
		||||
			}
 | 
			
		||||
		}`)
 | 
			
		||||
		}`);
 | 
			
		||||
	}
 | 
			
		||||
})();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										102
									
								
								packages/frontend-embed/src/theme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								packages/frontend-embed/src/theme.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
import lightTheme from '@@/themes/_light.json5';
 | 
			
		||||
import darkTheme from '@@/themes/_dark.json5';
 | 
			
		||||
import type { BundledTheme } from 'shiki/themes';
 | 
			
		||||
 | 
			
		||||
export type Theme = {
 | 
			
		||||
	id: string;
 | 
			
		||||
	name: string;
 | 
			
		||||
	author: string;
 | 
			
		||||
	desc?: string;
 | 
			
		||||
	base?: 'dark' | 'light';
 | 
			
		||||
	props: Record<string, string>;
 | 
			
		||||
	codeHighlighter?: {
 | 
			
		||||
		base: BundledTheme;
 | 
			
		||||
		overrides?: Record<string, any>;
 | 
			
		||||
	} | {
 | 
			
		||||
		base: '_none_';
 | 
			
		||||
		overrides: Record<string, any>;
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
let timeout: number | null = null;
 | 
			
		||||
 | 
			
		||||
export function applyTheme(theme: Theme, persist = true) {
 | 
			
		||||
	if (timeout) window.clearTimeout(timeout);
 | 
			
		||||
 | 
			
		||||
	document.documentElement.classList.add('_themeChanging_');
 | 
			
		||||
 | 
			
		||||
	timeout = window.setTimeout(() => {
 | 
			
		||||
		document.documentElement.classList.remove('_themeChanging_');
 | 
			
		||||
	}, 1000);
 | 
			
		||||
 | 
			
		||||
	const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
 | 
			
		||||
 | 
			
		||||
	// Deep copy
 | 
			
		||||
	const _theme = JSON.parse(JSON.stringify(theme));
 | 
			
		||||
 | 
			
		||||
	if (_theme.base) {
 | 
			
		||||
		const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
 | 
			
		||||
		if (base) _theme.props = Object.assign({}, base.props, _theme.props);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const props = compile(_theme);
 | 
			
		||||
 | 
			
		||||
	for (const tag of document.head.children) {
 | 
			
		||||
		if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
 | 
			
		||||
			tag.setAttribute('content', props['htmlThemeColor']);
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for (const [k, v] of Object.entries(props)) {
 | 
			
		||||
		document.documentElement.style.setProperty(`--${k}`, v.toString());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	document.documentElement.style.setProperty('color-scheme', colorScheme);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function compile(theme: Theme): Record<string, string> {
 | 
			
		||||
	function getColor(val: string): tinycolor.Instance {
 | 
			
		||||
		if (val[0] === '@') { // ref (prop)
 | 
			
		||||
			return getColor(theme.props[val.substring(1)]);
 | 
			
		||||
		} else if (val[0] === '$') { // ref (const)
 | 
			
		||||
			return getColor(theme.props[val]);
 | 
			
		||||
		} else if (val[0] === ':') { // func
 | 
			
		||||
			const parts = val.split('<');
 | 
			
		||||
			const func = parts.shift().substring(1);
 | 
			
		||||
			const arg = parseFloat(parts.shift());
 | 
			
		||||
			const color = getColor(parts.join('<'));
 | 
			
		||||
 | 
			
		||||
			switch (func) {
 | 
			
		||||
				case 'darken': return color.darken(arg);
 | 
			
		||||
				case 'lighten': return color.lighten(arg);
 | 
			
		||||
				case 'alpha': return color.setAlpha(arg);
 | 
			
		||||
				case 'hue': return color.spin(arg);
 | 
			
		||||
				case 'saturate': return color.saturate(arg);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// other case
 | 
			
		||||
		return tinycolor(val);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const props = {};
 | 
			
		||||
 | 
			
		||||
	for (const [k, v] of Object.entries(theme.props)) {
 | 
			
		||||
		if (k.startsWith('$')) continue; // ignore const
 | 
			
		||||
 | 
			
		||||
		props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return props;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function genValue(c: tinycolor.Instance): string {
 | 
			
		||||
	return c.toRgbString();
 | 
			
		||||
}
 | 
			
		||||
@@ -23,7 +23,8 @@
 | 
			
		||||
		"useDefineForClassFields": true,
 | 
			
		||||
		"baseUrl": ".",
 | 
			
		||||
		"paths": {
 | 
			
		||||
			"@/*": ["./src/*"]
 | 
			
		||||
			"@/*": ["./src/*"],
 | 
			
		||||
			"@@/*": ["../frontend-shared/*"]
 | 
			
		||||
		},
 | 
			
		||||
		"typeRoots": [
 | 
			
		||||
			"./@types",
 | 
			
		||||
 
 | 
			
		||||
@@ -72,6 +72,7 @@ export function getConfig(): UserConfig {
 | 
			
		||||
			extensions,
 | 
			
		||||
			alias: {
 | 
			
		||||
				'@/': __dirname + '/src/',
 | 
			
		||||
				'@@/': __dirname + '/../frontend-shared/',
 | 
			
		||||
				'/client-assets/': __dirname + '/assets/',
 | 
			
		||||
				'/static-assets/': __dirname + '/../backend/assets/'
 | 
			
		||||
			},
 | 
			
		||||
 
 | 
			
		||||
@@ -79,6 +79,8 @@ import tinycolor from 'tinycolor2';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import JSON5 from 'json5';
 | 
			
		||||
 | 
			
		||||
import lightTheme from '@@/themes/_light.json5';
 | 
			
		||||
import darkTheme from '@@/themes/_dark.json5';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
 | 
			
		||||
import MkTextarea from '@/components/MkTextarea.vue';
 | 
			
		||||
@@ -86,8 +88,6 @@ import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
import { Theme, applyTheme } from '@/scripts/theme.js';
 | 
			
		||||
import lightTheme from '@/themes/_light.json5';
 | 
			
		||||
import darkTheme from '@/themes/_dark.json5';
 | 
			
		||||
import { host } from '@/config.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
 | 
			
		||||
 
 | 
			
		||||
@@ -7,13 +7,13 @@ import { getHighlighterCore, loadWasm } from 'shiki/core';
 | 
			
		||||
import darkPlus from 'shiki/themes/dark-plus.mjs';
 | 
			
		||||
import { bundledThemesInfo } from 'shiki/themes';
 | 
			
		||||
import { bundledLanguagesInfo } from 'shiki/langs';
 | 
			
		||||
import lightTheme from '@@/themes/_light.json5';
 | 
			
		||||
import darkTheme from '@@/themes/_dark.json5';
 | 
			
		||||
import { unique } from './array.js';
 | 
			
		||||
import { deepClone } from './clone.js';
 | 
			
		||||
import { deepMerge } from './merge.js';
 | 
			
		||||
import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core';
 | 
			
		||||
import { ColdDeviceStorage } from '@/store.js';
 | 
			
		||||
import lightTheme from '@/themes/_light.json5';
 | 
			
		||||
import darkTheme from '@/themes/_dark.json5';
 | 
			
		||||
 | 
			
		||||
let _highlighter: HighlighterCore | null = null;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,11 @@
 | 
			
		||||
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
import lightTheme from '@@/themes/_light.json5';
 | 
			
		||||
import darkTheme from '@@/themes/_dark.json5';
 | 
			
		||||
import { deepClone } from './clone.js';
 | 
			
		||||
import type { BundledTheme } from 'shiki/themes';
 | 
			
		||||
import { globalEvents } from '@/events.js';
 | 
			
		||||
import lightTheme from '@/themes/_light.json5';
 | 
			
		||||
import darkTheme from '@/themes/_dark.json5';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage.js';
 | 
			
		||||
 | 
			
		||||
export type Theme = {
 | 
			
		||||
 
 | 
			
		||||
@@ -520,8 +520,8 @@ interface Watcher {
 | 
			
		||||
/**
 | 
			
		||||
 * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
 | 
			
		||||
 */
 | 
			
		||||
import lightTheme from '@/themes/l-light.json5';
 | 
			
		||||
import darkTheme from '@/themes/d-green-lime.json5';
 | 
			
		||||
import lightTheme from '@@/themes/l-light.json5';
 | 
			
		||||
import darkTheme from '@@/themes/d-green-lime.json5';
 | 
			
		||||
 | 
			
		||||
export class ColdDeviceStorage {
 | 
			
		||||
	public static default = {
 | 
			
		||||
@@ -558,7 +558,7 @@ export class ColdDeviceStorage {
 | 
			
		||||
	public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
 | 
			
		||||
		// 呼び出し側のバグ等で undefined が来ることがある
 | 
			
		||||
		// undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
 | 
			
		||||
		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
			
		||||
		 
 | 
			
		||||
		if (value === undefined) {
 | 
			
		||||
			console.error(`attempt to store undefined value for key '${key}'`);
 | 
			
		||||
			return;
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,8 @@
 | 
			
		||||
		"useDefineForClassFields": true,
 | 
			
		||||
		"baseUrl": ".",
 | 
			
		||||
		"paths": {
 | 
			
		||||
			"@/*": ["./src/*"]
 | 
			
		||||
			"@/*": ["./src/*"],
 | 
			
		||||
			"@@/*": ["../frontend-shared/*"]
 | 
			
		||||
		},
 | 
			
		||||
		"typeRoots": [
 | 
			
		||||
			"./@types",
 | 
			
		||||
 
 | 
			
		||||
@@ -87,6 +87,7 @@ export function getConfig(): UserConfig {
 | 
			
		||||
			extensions,
 | 
			
		||||
			alias: {
 | 
			
		||||
				'@/': __dirname + '/src/',
 | 
			
		||||
				'@@/': __dirname + '/../frontend-shared/',
 | 
			
		||||
				'/client-assets/': __dirname + '/assets/',
 | 
			
		||||
				'/static-assets/': __dirname + '/../backend/assets/',
 | 
			
		||||
				'/fluent-emojis/': __dirname + '/../../fluent-emojis/dist/',
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user