diff --git a/CHANGELOG.md b/CHANGELOG.md index c089aae27927508904b6ab7b139c78efe056caf1..c39a447c594308d43ff77f2f006acbcc86afcaf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ - Enhance: MFMã®å±žæ€§ã§ã‚ªãƒ¼ãƒˆã‚³ãƒ³ãƒ—リートãŒä½¿ç”¨ã§ãるよã†ã« #12735 - Enhance: 絵文å—編集ダイアãƒã‚°ã‚’モーダルã§ã¯ãªãウィンドウã§è¡¨ç¤ºã™ã‚‹ã‚ˆã†ã« - Enhance: リモートã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ãƒ¡ãƒ‹ãƒ¥ãƒ¼ã‹ã‚‰ç›´æŽ¥ãƒªãƒ¢ãƒ¼ãƒˆã§è¡¨ç¤ºã§ãるよã†ã« +- Enhance: コードã®ã‚·ãƒ³ã‚¿ãƒƒã‚¯ã‚¹ãƒã‚¤ãƒ©ã‚¤ãƒˆã«ãƒ†ãƒ¼ãƒžã‚’é©ç”¨ã§ãるよã†ã« - Fix: ãƒã‚¤ãƒ†ã‚£ãƒ–モードã®çµµæ–‡å—ãŒãƒ¢ãƒŽã‚¯ãƒã«ãªã‚‰ãªã„よã†ã« - Fix: v2023.12.0ã§è¿½åŠ ã•ã‚ŒãŸã€Œãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ãŒãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã‚¢ã‚¤ã‚³ãƒ³ã‚‚ã—ãã¯ãƒãƒŠãƒ¼ç”»åƒã‚’未è¨å®šçŠ¶æ…‹ã«ã§ãる機能ã€ãŒç®¡ç†ç”»é¢ä¸Šã§æ£ã—ã表示ã•ã‚Œã¦ã„ãªã„å•é¡Œã‚’ä¿®æ£ - Fix: AiScriptã®`readline`関数ãŒä¸æ£ãªå€¤ã‚’è¿”ã™ã“ã¨ãŒã‚ã‚‹å•é¡Œã®v2023.12.0時点ã§ã®ä¿®æ£ãŒPlay以外ã«é©ç”¨ã•ã‚Œã¦ã„ãªã„ã®ã‚’ä¿®æ£ diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue index c655ff416ef3d74a58fce17262c84a1a5c14d8c6..68c50c4c690cad17b2840e5eb0805b10634aa577 100644 --- a/packages/frontend/src/components/MkCode.core.vue +++ b/packages/frontend/src/components/MkCode.core.vue @@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- eslint-disable vue/no-v-html --> <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> <script lang="ts" setup> import { ref, computed, watch } from 'vue'; import { bundledLanguagesInfo } 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<{ code: string; @@ -21,11 +22,23 @@ const props = defineProps<{ }>(); const highlighter = await getHighlighter(); - +const darkMode = defaultStore.reactiveState.darkMode; 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, { 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> { @@ -64,6 +77,15 @@ watch(() => props.lang, (to) => { margin: .5em 0; overflow: auto; 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, & 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 { min-width: 100%; height: 100%; @@ -79,6 +121,7 @@ watch(() => props.lang, (to) => { padding: 12px; margin: 0; border-radius: 6px; + border: none; min-height: 130px; pointer-events: none; min-width: calc(100% - 24px); @@ -90,6 +133,11 @@ watch(() => props.lang, (to) => { text-rendering: inherit; text-transform: inherit; white-space: pre; + + & span { + display: inline-block; + min-height: 1em; + } } } </style> diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index 251e6ade00b9763fd6747ea1de48a9849fa72249..6c14738937f2efb0ef54269c4e69ae6a56f73bd2 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -53,7 +53,6 @@ function copy() { } .codeBlockCopyButton { - color: #D4D4D4; position: absolute; top: 8px; right: 8px; @@ -67,8 +66,7 @@ function copy() { .codeBlockFallbackRoot { display: block; overflow-wrap: anywhere; - color: #D4D4D4; - background: #1E1E1E; + background: var(--bg); padding: 1em; margin: .5em 0; overflow: auto; @@ -93,8 +91,8 @@ function copy() { border-radius: 8px; padding: 24px; margin-top: 4px; - color: #D4D4D4; - background: #1E1E1E; + color: var(--fg); + background: var(--bg); } .codePlaceholderContainer { diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index c8c3deb610a1185ebb70b5e76094bcf2f1497e71..3cf8234e72da55ecda45cfb4e2e044de1290e07f 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -196,10 +196,11 @@ watch(v, newValue => { resize: none; text-align: left; color: transparent; - caret-color: rgb(225, 228, 232); + caret-color: var(--fg); background-color: transparent; border: 0; border-radius: 6px; + box-sizing: border-box; outline: 0; min-width: calc(100% - 24px); height: 100%; @@ -210,6 +211,6 @@ watch(v, newValue => { } .textarea::selection { - color: #fff; + color: var(--bg); } </style> diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue index 5340c1fd5fa30a713fa54e70dbfb096cae73276e..6a9d97ab5a3ee0eb0abe74ef30da48b5d6aaf554 100644 --- a/packages/frontend/src/components/MkCodeInline.vue +++ b/packages/frontend/src/components/MkCodeInline.vue @@ -18,8 +18,7 @@ const props = defineProps<{ display: inline-block; font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; overflow-wrap: anywhere; - color: #D4D4D4; - background: #1E1E1E; + background: var(--bg); padding: .1em; border-radius: .3em; } diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 8dd2b9129d6449a23bba66526c7481938b0695a1..3e8ff9938790dd393e6e2c1b8037b1cf798202bf 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only </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> </template> @@ -138,6 +138,7 @@ function show() { active: computed(() => v.value === option.props?.value), action: () => { v.value = option.props?.value; + changed.value = true; emit('changeByUser', v.value); }, }); @@ -288,6 +289,10 @@ function show() { padding-left: 6px; } +.save { + margin: 8px 0 0 0; +} + .chevron { transition: transform 0.1s ease-out; } diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue index dedac10270a7e8cba42f2744158d779c8847a220..1d6fec52900810d926b91e11ed13984d7f63460c 100644 --- a/packages/frontend/src/pages/settings/theme.vue +++ b/packages/frontend/src/pages/settings/theme.vue @@ -88,6 +88,18 @@ import { uniqueBy } from '@/scripts/array.js'; import { fetchThemes, getThemes } from '@/theme-store.js'; import { definePageMetadata } from '@/scripts/page-metadata.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 builtinThemes = getBuiltinThemesRef(); @@ -124,6 +136,7 @@ const lightThemeId = computed({ } }, }); + const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode')); const wallpaper = ref(miLocalStorage.getItem('wallpaper')); @@ -141,7 +154,7 @@ watch(wallpaper, () => { } else { miLocalStorage.setItem('wallpaper', wallpaper.value); } - location.reload(); + reloadAsk(); }); onActivated(() => { diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts index 68c36ca1b4b6d63f036c4a762042b56f6014e91c..043b6efd73a54c0cd474532557501e597758347b 100644 --- a/packages/frontend/src/pizzax.ts +++ b/packages/frontend/src/pizzax.ts @@ -13,6 +13,7 @@ import { get, set } from '@/scripts/idb-proxy.js'; import { defaultStore } from '@/store.js'; import { useStream } from '@/stream.js'; import { deepClone } from '@/scripts/clone.js'; +import { deepMerge } from '@/scripts/merge.js'; type StateDef = Record<string, { where: 'account' | 'device' | 'deviceAccount'; @@ -84,29 +85,9 @@ export class Storage<T extends StateDef> { 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 { 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); diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts index 5adb3f606f4356070c67a8b031e08c2f4391bfff..c6a520e9132bb76aa1d0e0b8edccc788c942466c 100644 --- a/packages/frontend/src/router/main.ts +++ b/packages/frontend/src/router/main.ts @@ -80,6 +80,10 @@ class MainRouterProxy implements IRouter { return this.supplier().resolve(path); } + init(): void { + this.supplier().init(); + } + eventNames(): Array<EventEmitter.EventNames<RouterEvent>> { return this.supplier().eventNames(); } diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/scripts/clone.ts index ac38faefaa272d0d14d146a21ef62d7d26a1ed74..6d3a1c8c79765b587433d79b31f5fee6e95e63e5 100644 --- a/packages/frontend/src/scripts/clone.ts +++ b/packages/frontend/src/scripts/clone.ts @@ -8,13 +8,13 @@ // ã‚ã¨ã€Vue Refã‚’IndexedDBã«ä¿å˜ã—よã†ã¨ã—ã¦structredCloneを使ã£ãŸã‚‰ã‚¨ãƒ©ãƒ¼ã«ãªã£ãŸ // 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 { if (typeof x === 'object') { if (x === null) return x; 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)) { obj[k] = v === undefined ? undefined : deepClone(v); } diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index bc05ec94d54cc83c346aacdcc6e066f6dc9b8fcf..b11dfed41a60b945edb09322706748fd3e26fe38 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -1,9 +1,51 @@ +import { bundledThemesInfo } from 'shiki'; import { getHighlighterCore, loadWasm } from 'shiki/core'; 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; +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> { if (!_highlighter) { return await initHighlighter(); @@ -13,11 +55,17 @@ export async function getHighlighter(): Promise<Highlighter> { export async function initHighlighter() { const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json'); - + await loadWasm(import('shiki/onig.wasm?init')); + // テーマã®é‡è¤‡ã‚’消㙠+ const themes = unique([ + darkPlus, + ...(await Promise.all([getTheme('light'), getTheme('dark')])), + ]); + const highlighter = await getHighlighterCore({ - themes: [darkPlus], + themes, langs: [ 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; return highlighter; diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts new file mode 100644 index 0000000000000000000000000000000000000000..60097051fab66e003c00e86e90c5164eeea75575 --- /dev/null +++ b/packages/frontend/src/scripts/merge.ts @@ -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; +} diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index 21ef85fe7ac906310537a079af05edafe8536b67..d3bd9ba4bc30af419d835bf711b7e06697c23801 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -6,6 +6,7 @@ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; import { deepClone } from './clone.js'; +import type { BuiltinTheme } from 'shiki'; import { globalEvents } from '@/events.js'; import lightTheme from '@/themes/_light.json5'; import darkTheme from '@/themes/_dark.json5'; @@ -18,6 +19,13 @@ export type Theme = { desc?: string; base?: 'dark' | 'light'; 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')); @@ -53,7 +61,7 @@ export const getBuiltinThemesRef = () => { return builtinThemes; }; -let timeout = null; +let timeout: number | null = null; export function applyTheme(theme: Theme, persist = true) { if (timeout) window.clearTimeout(timeout); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index afc35bb825db551dc090a927d40e88f39bac66b1..641a506679c4edfaec0c777aefe63c1e32f612c5 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -7,6 +7,7 @@ import { markRaw, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { miLocalStorage } from './local-storage.js'; import type { SoundType } from '@/scripts/sound.js'; +import type { BuiltinTheme as ShikiBuiltinTheme } from 'shiki'; import { Storage } from '@/pizzax.js'; import { hemisphere } from '@/scripts/intl-const.js'; diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5 index 3f5822977abcb6d978b37de025cdab8c00449c68..c82a95686821a8128be43a3490566962681abc3b 100644 --- a/packages/frontend/src/themes/_dark.json5 +++ b/packages/frontend/src/themes/_dark.json5 @@ -94,4 +94,8 @@ X16: ':alpha<0.7<@panel', X17: ':alpha<0.8<@bg', }, + + codeHighlighter: { + base: 'one-dark-pro', + }, } diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend/src/themes/_light.json5 index 6ebfcaafeb5319b3b557ff02e83af2c3ebaad587..63bc030916d81ff096f1a1f6a3d06d2819bc2b8e 100644 --- a/packages/frontend/src/themes/_light.json5 +++ b/packages/frontend/src/themes/_light.json5 @@ -94,4 +94,8 @@ X16: ':alpha<0.7<@panel', X17: ':alpha<0.8<@bg', }, + + codeHighlighter: { + base: 'catppuccin-latte', + }, }