Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance(frontend): シンタックスハイライトにテーマを適用できるように #13175

Merged
merged 6 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
- Enhance: コードのシンタックスハイライトにテーマを適用できるように
- Fix: ネイティブモードの絵文字がモノクロにならないように
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
Expand Down
4 changes: 4 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,10 @@ export interface Locale extends ILocale {
* デバイスのダークモードと同期する
*/
"syncDeviceDarkMode": string;
/**
* UIテーマの設定を使う
*/
"inheritFromTheme": string;
/**
* ドライブ
*/
Expand Down
1 change: 1 addition & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ dark: "ダーク"
lightThemes: "明るいテーマ"
darkThemes: "暗いテーマ"
syncDeviceDarkMode: "デバイスのダークモードと同期する"
inheritFromTheme: "UIテーマの設定を使う"
drive: "ドライブ"
fileName: "ファイル名"
selectFile: "ファイルを選択"
Expand Down
56 changes: 52 additions & 4 deletions packages/frontend/src/components/MkCode.core.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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> {
Expand Down Expand Up @@ -64,13 +77,42 @@ 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 {
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
}
}

.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%;
Expand All @@ -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);
Expand All @@ -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>
8 changes: 3 additions & 5 deletions packages/frontend/src/components/MkCode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ function copy() {
}

.codeBlockCopyButton {
color: #D4D4D4;
position: absolute;
top: 8px;
right: 8px;
Expand All @@ -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;
Expand All @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions packages/frontend/src/components/MkCodeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand All @@ -210,6 +211,6 @@ watch(v, newValue => {
}

.textarea::selection {
color: #fff;
color: var(--bg);
}
</style>
3 changes: 1 addition & 2 deletions packages/frontend/src/components/MkCodeInline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/frontend/src/components/MkSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down Expand Up @@ -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);
},
});
Expand Down Expand Up @@ -288,6 +289,10 @@ function show() {
padding-left: 6px;
}

.save {
margin: 8px 0 0 0;
}

.chevron {
transition: transform 0.1s ease-out;
}
Expand Down
43 changes: 42 additions & 1 deletion packages/frontend/src/pages/settings/theme.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSelect>
</div>

<div class="selects">
<MkSelect v-model="codeLightTheme" large manualSave class="select">
<template #label>{{ i18n.ts.themeForLightMode }} ({{ i18n.ts.code }})</template>
<template #prefix><i class="ti ti-sun"></i></template>
<option value="_inheritFromTheme_">{{ i18n.ts.inheritFromTheme }}</option>
<optgroup :label="i18n.ts._theme.builtinThemes">
<option v-for="x in codeBuiltinLightThemes" :key="'code:builtin:' + x.id" :value="x.id">{{ x.displayName }}</option>
</optgroup>
</MkSelect>
<MkSelect v-model="codeDarkTheme" large manualSave class="select">
<template #label>{{ i18n.ts.themeForDarkMode }} ({{ i18n.ts.code }})</template>
<template #prefix><i class="ti ti-moon"></i></template>
<option value="_inheritFromTheme_">{{ i18n.ts.inheritFromTheme }}</option>
<optgroup :label="i18n.ts._theme.builtinThemes">
<option v-for="x in codeBuiltinDarkThemes" :key="'code:builtin:' + x.id" :value="x.id">{{ x.displayName }}</option>
</optgroup>
</MkSelect>
</div>

<FormSection>
<div class="_formLinksGrid">
<FormLink to="/settings/theme/manage"><template #icon><i class="ti ti-tool"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
Expand All @@ -73,6 +92,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, onActivated, ref, watch } from 'vue';
import JSON5 from 'json5';
import { bundledThemesInfo as shikiBundledThemes } from 'shiki';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSection from '@/components/form/section.vue';
Expand All @@ -88,16 +108,30 @@ 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();

const instanceDarkTheme = computed(() => instance.defaultDarkTheme ? JSON5.parse(instance.defaultDarkTheme) : null);
const installedDarkThemes = computed(() => installedThemes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
const builtinDarkThemes = computed(() => builtinThemes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
const codeBuiltinDarkThemes = shikiBundledThemes.filter(t => t.type === 'dark');
const instanceLightTheme = computed(() => instance.defaultLightTheme ? JSON5.parse(instance.defaultLightTheme) : null);
const installedLightThemes = computed(() => installedThemes.value.filter(t => t.base === 'light' || t.kind === 'light'));
const builtinLightThemes = computed(() => builtinThemes.value.filter(t => t.base === 'light' || t.kind === 'light'));
const codeBuiltinLightThemes = shikiBundledThemes.filter(t => t.type === 'light');
const themes = computed(() => uniqueBy([instanceDarkTheme.value, instanceLightTheme.value, ...builtinThemes.value, ...installedThemes.value].filter(x => x != null), theme => theme.id));

const darkTheme = ColdDeviceStorage.ref('darkTheme');
Expand All @@ -124,6 +158,9 @@ const lightThemeId = computed({
}
},
});
const codeLightTheme = computed(ColdDeviceStorage.makeGetterSetter('codeLightTheme'));
const codeDarkTheme = computed(ColdDeviceStorage.makeGetterSetter('codeDarkTheme'));

const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
Expand All @@ -141,7 +178,11 @@ watch(wallpaper, () => {
} else {
miLocalStorage.setItem('wallpaper', wallpaper.value);
}
location.reload();
reloadAsk();
});

watch([codeLightTheme, codeDarkTheme], () => {
reloadAsk();
});

onActivated(() => {
Expand Down
4 changes: 4 additions & 0 deletions packages/frontend/src/router/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Loading
Loading