enhance(frontend): シンタックスハイライトにテーマを適用できるように (#13175)
* enhance(frontend): シンタックスハイライトにテーマを適用できるように * Update Changelog * こっちも * テーマの値がディープマージされるように * 常にテーマ設定に準じるように * テーマ更新時に新しいshikiテーマを読み込むように
This commit is contained in:
parent
2f54a53062
commit
16eccad492
16 changed files with 202 additions and 42 deletions
|
@ -49,6 +49,7 @@
|
||||||
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
|
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
|
||||||
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
|
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
|
||||||
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
|
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
|
||||||
|
- Enhance: コードのシンタックスハイライトにテーマを適用できるように
|
||||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||||
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
||||||
|
|
|
@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }]" v-html="html"></div>
|
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }, (darkMode ? $style.dark : $style.light)]" v-html="html"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { bundledLanguagesInfo } from 'shiki';
|
import { bundledLanguagesInfo } from 'shiki';
|
||||||
import type { BuiltinLanguage } from 'shiki';
|
import type { BuiltinLanguage } from 'shiki';
|
||||||
import { getHighlighter } from '@/scripts/code-highlighter.js';
|
import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
code: string;
|
code: string;
|
||||||
|
@ -21,11 +22,23 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const highlighter = await getHighlighter();
|
const highlighter = await getHighlighter();
|
||||||
|
const darkMode = defaultStore.reactiveState.darkMode;
|
||||||
const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
|
const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
|
||||||
|
|
||||||
|
const [lightThemeName, darkThemeName] = await Promise.all([
|
||||||
|
getTheme('light', true),
|
||||||
|
getTheme('dark', true),
|
||||||
|
]);
|
||||||
|
|
||||||
const html = computed(() => highlighter.codeToHtml(props.code, {
|
const html = computed(() => highlighter.codeToHtml(props.code, {
|
||||||
lang: codeLang.value,
|
lang: codeLang.value,
|
||||||
theme: 'dark-plus',
|
themes: {
|
||||||
|
fallback: 'dark-plus',
|
||||||
|
light: lightThemeName,
|
||||||
|
dark: darkThemeName,
|
||||||
|
},
|
||||||
|
defaultColor: false,
|
||||||
|
cssVariablePrefix: '--shiki-',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function fetchLanguage(to: string): Promise<void> {
|
async function fetchLanguage(to: string): Promise<void> {
|
||||||
|
@ -64,6 +77,15 @@ watch(() => props.lang, (to) => {
|
||||||
margin: .5em 0;
|
margin: .5em 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--divider);
|
||||||
|
|
||||||
|
color: var(--shiki-fallback);
|
||||||
|
background-color: var(--shiki-fallback-bg);
|
||||||
|
|
||||||
|
& span {
|
||||||
|
color: var(--shiki-fallback);
|
||||||
|
background-color: var(--shiki-fallback-bg);
|
||||||
|
}
|
||||||
|
|
||||||
& pre,
|
& pre,
|
||||||
& code {
|
& code {
|
||||||
|
@ -71,6 +93,26 @@ watch(() => props.lang, (to) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.light.codeBlockRoot :global(.shiki) {
|
||||||
|
color: var(--shiki-light);
|
||||||
|
background-color: var(--shiki-light-bg);
|
||||||
|
|
||||||
|
& span {
|
||||||
|
color: var(--shiki-light);
|
||||||
|
background-color: var(--shiki-light-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.codeBlockRoot :global(.shiki) {
|
||||||
|
color: var(--shiki-dark);
|
||||||
|
background-color: var(--shiki-dark-bg);
|
||||||
|
|
||||||
|
& span {
|
||||||
|
color: var(--shiki-dark);
|
||||||
|
background-color: var(--shiki-dark-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.codeBlockRoot.codeEditor {
|
.codeBlockRoot.codeEditor {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -79,6 +121,7 @@ watch(() => props.lang, (to) => {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
min-height: 130px;
|
min-height: 130px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
min-width: calc(100% - 24px);
|
min-width: calc(100% - 24px);
|
||||||
|
@ -90,6 +133,11 @@ watch(() => props.lang, (to) => {
|
||||||
text-rendering: inherit;
|
text-rendering: inherit;
|
||||||
text-transform: inherit;
|
text-transform: inherit;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
|
||||||
|
& span {
|
||||||
|
display: inline-block;
|
||||||
|
min-height: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -53,7 +53,6 @@ function copy() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.codeBlockCopyButton {
|
.codeBlockCopyButton {
|
||||||
color: #D4D4D4;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
|
@ -67,8 +66,7 @@ function copy() {
|
||||||
.codeBlockFallbackRoot {
|
.codeBlockFallbackRoot {
|
||||||
display: block;
|
display: block;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
color: #D4D4D4;
|
background: var(--bg);
|
||||||
background: #1E1E1E;
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin: .5em 0;
|
margin: .5em 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
@ -93,8 +91,8 @@ function copy() {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: #D4D4D4;
|
color: var(--fg);
|
||||||
background: #1E1E1E;
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.codePlaceholderContainer {
|
.codePlaceholderContainer {
|
||||||
|
|
|
@ -196,10 +196,11 @@ watch(v, newValue => {
|
||||||
resize: none;
|
resize: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
caret-color: rgb(225, 228, 232);
|
caret-color: var(--fg);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
min-width: calc(100% - 24px);
|
min-width: calc(100% - 24px);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -210,6 +211,6 @@ watch(v, newValue => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea::selection {
|
.textarea::selection {
|
||||||
color: #fff;
|
color: var(--bg);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,8 +18,7 @@ const props = defineProps<{
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
color: #D4D4D4;
|
background: var(--bg);
|
||||||
background: #1E1E1E;
|
|
||||||
padding: .1em;
|
padding: .1em;
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.caption"><slot name="caption"></slot></div>
|
<div :class="$style.caption"><slot name="caption"></slot></div>
|
||||||
|
|
||||||
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -138,6 +138,7 @@ function show() {
|
||||||
active: computed(() => v.value === option.props?.value),
|
active: computed(() => v.value === option.props?.value),
|
||||||
action: () => {
|
action: () => {
|
||||||
v.value = option.props?.value;
|
v.value = option.props?.value;
|
||||||
|
changed.value = true;
|
||||||
emit('changeByUser', v.value);
|
emit('changeByUser', v.value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -288,6 +289,10 @@ function show() {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save {
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chevron {
|
.chevron {
|
||||||
transition: transform 0.1s ease-out;
|
transition: transform 0.1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,18 @@ import { uniqueBy } from '@/scripts/array.js';
|
||||||
import { fetchThemes, getThemes } from '@/theme-store.js';
|
import { fetchThemes, getThemes } from '@/theme-store.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
|
async function reloadAsk() {
|
||||||
|
const { canceled } = await os.confirm({
|
||||||
|
type: 'info',
|
||||||
|
text: i18n.ts.reloadToApplySetting,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
unisonReload();
|
||||||
|
}
|
||||||
|
|
||||||
const installedThemes = ref(getThemes());
|
const installedThemes = ref(getThemes());
|
||||||
const builtinThemes = getBuiltinThemesRef();
|
const builtinThemes = getBuiltinThemesRef();
|
||||||
|
@ -124,6 +136,7 @@ const lightThemeId = computed({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||||
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
||||||
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
|
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
|
||||||
|
@ -141,7 +154,7 @@ watch(wallpaper, () => {
|
||||||
} else {
|
} else {
|
||||||
miLocalStorage.setItem('wallpaper', wallpaper.value);
|
miLocalStorage.setItem('wallpaper', wallpaper.value);
|
||||||
}
|
}
|
||||||
location.reload();
|
reloadAsk();
|
||||||
});
|
});
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { get, set } from '@/scripts/idb-proxy.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
|
import { deepMerge } from '@/scripts/merge.js';
|
||||||
|
|
||||||
type StateDef = Record<string, {
|
type StateDef = Record<string, {
|
||||||
where: 'account' | 'device' | 'deviceAccount';
|
where: 'account' | 'device' | 'deviceAccount';
|
||||||
|
@ -84,29 +85,9 @@ export class Storage<T extends StateDef> {
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* valueにないキーをdefからもらう(再帰的)\
|
|
||||||
* nullはそのまま、undefinedはdefの値
|
|
||||||
**/
|
|
||||||
private mergeObject<X>(value: X, def: X): X {
|
|
||||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
|
||||||
const result = structuredClone(value) as X;
|
|
||||||
for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
|
|
||||||
result[k] = v;
|
|
||||||
} else if (this.isPureObject(v) && this.isPureObject(result[k])) {
|
|
||||||
const child = structuredClone(result[k]) as X[keyof X] & Record<string | number | symbol, unknown>;
|
|
||||||
result[k] = this.mergeObject<typeof v>(child, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private mergeState<X>(value: X, def: X): X {
|
private mergeState<X>(value: X, def: X): X {
|
||||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
if (this.isPureObject(value) && this.isPureObject(def)) {
|
||||||
const merged = this.mergeObject(value, def);
|
const merged = deepMerge(value, def);
|
||||||
|
|
||||||
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
|
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,10 @@ class MainRouterProxy implements IRouter {
|
||||||
return this.supplier().resolve(path);
|
return this.supplier().resolve(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.supplier().init();
|
||||||
|
}
|
||||||
|
|
||||||
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
|
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
|
||||||
return this.supplier().eventNames();
|
return this.supplier().eventNames();
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,13 @@
|
||||||
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
|
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
|
||||||
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
|
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
|
||||||
|
|
||||||
type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[];
|
export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[];
|
||||||
|
|
||||||
export function deepClone<T extends Cloneable>(x: T): T {
|
export function deepClone<T extends Cloneable>(x: T): T {
|
||||||
if (typeof x === 'object') {
|
if (typeof x === 'object') {
|
||||||
if (x === null) return x;
|
if (x === null) return x;
|
||||||
if (Array.isArray(x)) return x.map(deepClone) as T;
|
if (Array.isArray(x)) return x.map(deepClone) as T;
|
||||||
const obj = {} as Record<string, Cloneable>;
|
const obj = {} as Record<string | number | symbol, Cloneable>;
|
||||||
for (const [k, v] of Object.entries(x)) {
|
for (const [k, v] of Object.entries(x)) {
|
||||||
obj[k] = v === undefined ? undefined : deepClone(v);
|
obj[k] = v === undefined ? undefined : deepClone(v);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,51 @@
|
||||||
|
import { bundledThemesInfo } from 'shiki';
|
||||||
import { getHighlighterCore, loadWasm } from 'shiki/core';
|
import { getHighlighterCore, loadWasm } from 'shiki/core';
|
||||||
import darkPlus from 'shiki/themes/dark-plus.mjs';
|
import darkPlus from 'shiki/themes/dark-plus.mjs';
|
||||||
import type { Highlighter, LanguageRegistration } from 'shiki';
|
import { unique } from './array.js';
|
||||||
|
import { deepClone } from './clone.js';
|
||||||
|
import { deepMerge } from './merge.js';
|
||||||
|
import type { Highlighter, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki';
|
||||||
|
import { ColdDeviceStorage } from '@/store.js';
|
||||||
|
import lightTheme from '@/themes/_light.json5';
|
||||||
|
import darkTheme from '@/themes/_dark.json5';
|
||||||
|
|
||||||
let _highlighter: Highlighter | null = null;
|
let _highlighter: Highlighter | null = null;
|
||||||
|
|
||||||
|
export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>;
|
||||||
|
export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>;
|
||||||
|
export async function getTheme(mode: 'light' | 'dark', getName = false): Promise<ThemeRegistration | ThemeRegistrationRaw | string | null> {
|
||||||
|
const theme = deepClone(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme'));
|
||||||
|
|
||||||
|
if (theme.base) {
|
||||||
|
const base = [lightTheme, darkTheme].find(x => x.id === theme.base);
|
||||||
|
if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theme.codeHighlighter) {
|
||||||
|
let _res: ThemeRegistration = {};
|
||||||
|
if (theme.codeHighlighter.base === '_none_') {
|
||||||
|
_res = deepClone(theme.codeHighlighter.overrides);
|
||||||
|
} else {
|
||||||
|
const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus;
|
||||||
|
_res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base);
|
||||||
|
}
|
||||||
|
if (_res.name == null) {
|
||||||
|
_res.name = theme.id;
|
||||||
|
}
|
||||||
|
_res.type = mode;
|
||||||
|
|
||||||
|
if (getName) {
|
||||||
|
return _res.name;
|
||||||
|
}
|
||||||
|
return _res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getName) {
|
||||||
|
return 'dark-plus';
|
||||||
|
}
|
||||||
|
return darkPlus;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getHighlighter(): Promise<Highlighter> {
|
export async function getHighlighter(): Promise<Highlighter> {
|
||||||
if (!_highlighter) {
|
if (!_highlighter) {
|
||||||
return await initHighlighter();
|
return await initHighlighter();
|
||||||
|
@ -16,8 +58,14 @@ export async function initHighlighter() {
|
||||||
|
|
||||||
await loadWasm(import('shiki/onig.wasm?init'));
|
await loadWasm(import('shiki/onig.wasm?init'));
|
||||||
|
|
||||||
|
// テーマの重複を消す
|
||||||
|
const themes = unique([
|
||||||
|
darkPlus,
|
||||||
|
...(await Promise.all([getTheme('light'), getTheme('dark')])),
|
||||||
|
]);
|
||||||
|
|
||||||
const highlighter = await getHighlighterCore({
|
const highlighter = await getHighlighterCore({
|
||||||
themes: [darkPlus],
|
themes,
|
||||||
langs: [
|
langs: [
|
||||||
import('shiki/langs/javascript.mjs'),
|
import('shiki/langs/javascript.mjs'),
|
||||||
{
|
{
|
||||||
|
@ -27,6 +75,20 @@ export async function initHighlighter() {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ColdDeviceStorage.watch('lightTheme', async () => {
|
||||||
|
const newTheme = await getTheme('light');
|
||||||
|
if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
|
||||||
|
highlighter.loadTheme(newTheme);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ColdDeviceStorage.watch('darkTheme', async () => {
|
||||||
|
const newTheme = await getTheme('dark');
|
||||||
|
if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
|
||||||
|
highlighter.loadTheme(newTheme);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
_highlighter = highlighter;
|
_highlighter = highlighter;
|
||||||
|
|
||||||
return highlighter;
|
return highlighter;
|
||||||
|
|
31
packages/frontend/src/scripts/merge.ts
Normal file
31
packages/frontend/src/scripts/merge.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { deepClone } from './clone.js';
|
||||||
|
import type { Cloneable } from './clone.js';
|
||||||
|
|
||||||
|
function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* valueにないキーをdefからもらう(再帰的)\
|
||||||
|
* nullはそのまま、undefinedはdefの値
|
||||||
|
**/
|
||||||
|
export function deepMerge<X extends Record<string | number | symbol, unknown>>(value: X, def: X): X {
|
||||||
|
if (isPureObject(value) && isPureObject(def)) {
|
||||||
|
const result = deepClone(value as Cloneable) as X;
|
||||||
|
for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
|
||||||
|
result[k] = v;
|
||||||
|
} else if (isPureObject(v) && isPureObject(result[k])) {
|
||||||
|
const child = deepClone(result[k] as Cloneable) as X[keyof X] & Record<string | number | symbol, unknown>;
|
||||||
|
result[k] = deepMerge<typeof v>(child, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { deepClone } from './clone.js';
|
import { deepClone } from './clone.js';
|
||||||
|
import type { BuiltinTheme } from 'shiki';
|
||||||
import { globalEvents } from '@/events.js';
|
import { globalEvents } from '@/events.js';
|
||||||
import lightTheme from '@/themes/_light.json5';
|
import lightTheme from '@/themes/_light.json5';
|
||||||
import darkTheme from '@/themes/_dark.json5';
|
import darkTheme from '@/themes/_dark.json5';
|
||||||
|
@ -18,6 +19,13 @@ export type Theme = {
|
||||||
desc?: string;
|
desc?: string;
|
||||||
base?: 'dark' | 'light';
|
base?: 'dark' | 'light';
|
||||||
props: Record<string, string>;
|
props: Record<string, string>;
|
||||||
|
codeHighlighter?: {
|
||||||
|
base: BuiltinTheme;
|
||||||
|
overrides?: Record<string, any>;
|
||||||
|
} | {
|
||||||
|
base: '_none_';
|
||||||
|
overrides: Record<string, any>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
||||||
|
@ -53,7 +61,7 @@ export const getBuiltinThemesRef = () => {
|
||||||
return builtinThemes;
|
return builtinThemes;
|
||||||
};
|
};
|
||||||
|
|
||||||
let timeout = null;
|
let timeout: number | null = null;
|
||||||
|
|
||||||
export function applyTheme(theme: Theme, persist = true) {
|
export function applyTheme(theme: Theme, persist = true) {
|
||||||
if (timeout) window.clearTimeout(timeout);
|
if (timeout) window.clearTimeout(timeout);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { markRaw, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { miLocalStorage } from './local-storage.js';
|
import { miLocalStorage } from './local-storage.js';
|
||||||
import type { SoundType } from '@/scripts/sound.js';
|
import type { SoundType } from '@/scripts/sound.js';
|
||||||
|
import type { BuiltinTheme as ShikiBuiltinTheme } from 'shiki';
|
||||||
import { Storage } from '@/pizzax.js';
|
import { Storage } from '@/pizzax.js';
|
||||||
import { hemisphere } from '@/scripts/intl-const.js';
|
import { hemisphere } from '@/scripts/intl-const.js';
|
||||||
|
|
||||||
|
|
|
@ -94,4 +94,8 @@
|
||||||
X16: ':alpha<0.7<@panel',
|
X16: ':alpha<0.7<@panel',
|
||||||
X17: ':alpha<0.8<@bg',
|
X17: ':alpha<0.8<@bg',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
codeHighlighter: {
|
||||||
|
base: 'one-dark-pro',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,4 +94,8 @@
|
||||||
X16: ':alpha<0.7<@panel',
|
X16: ':alpha<0.7<@panel',
|
||||||
X17: ':alpha<0.8<@bg',
|
X17: ':alpha<0.8<@bg',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
codeHighlighter: {
|
||||||
|
base: 'catppuccin-latte',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue