mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-14 21:35:38 +02:00
enhance(frontend): テーマの適用管理を改善 (#17376)
* wip * add test * use themeManager.currentCompiledTheme for obtaining theme variables / reduce getComputedStyle usage * fix * fix: better error handling on theme installation * Update Changelog * chore: remove frontend-shared builds as it is currently working as a stub package * fix: broken lockfile * fix * fix lint * fix
This commit is contained in:
@@ -13,7 +13,7 @@ import type { App } from 'vue';
|
||||
import widgets from '@/widgets/index.js';
|
||||
import directives from '@/directives/index.js';
|
||||
import components from '@/components/index.js';
|
||||
import { applyTheme } from '@/theme.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { refreshCurrentAccount, login } from '@/accounts.js';
|
||||
@@ -161,7 +161,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
||||
|
||||
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
|
||||
// NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため)
|
||||
// NOTE: この処理は必ずサーバーテーマ適用処理より後に来ること(二重applyTheme発火を防ぐため)
|
||||
// NOTE: この処理は必ずサーバーテーマ適用処理より後に来ること(二重発火を防ぐため)
|
||||
// see: https://github.com/misskey-dev/misskey/issues/16562
|
||||
watch(store.r.darkMode, (darkMode) => {
|
||||
const theme = (() => {
|
||||
@@ -172,7 +172,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
||||
}
|
||||
})();
|
||||
|
||||
applyTheme(theme);
|
||||
themeManager.updateTheme(theme);
|
||||
}, { immediate: true });
|
||||
|
||||
window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
|
||||
@@ -180,13 +180,13 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
||||
if (!isSafeMode) {
|
||||
watch(prefer.r.darkTheme, (theme) => {
|
||||
if (store.s.darkMode) {
|
||||
applyTheme(theme ?? defaultDarkTheme);
|
||||
themeManager.updateTheme(theme ?? defaultDarkTheme);
|
||||
}
|
||||
});
|
||||
|
||||
watch(prefer.r.lightTheme, (theme) => {
|
||||
if (!store.s.darkMode) {
|
||||
applyTheme(theme ?? defaultLightTheme);
|
||||
themeManager.updateTheme(theme ?? defaultLightTheme);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js';
|
||||
|
||||
// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
|
||||
@@ -192,13 +192,13 @@ function tick() {
|
||||
tick();
|
||||
|
||||
function calcColors() {
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark();
|
||||
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
|
||||
const themeValue = themeManager.currentCompiledTheme!;
|
||||
const dark = tinycolor(themeValue.bg).isDark();
|
||||
const accent = tinycolor(themeValue.accent).toHexString();
|
||||
majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
|
||||
//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
|
||||
mHandColor.value = tinycolor(computedStyle.getPropertyValue('--MI_THEME-fg')).toHexString();
|
||||
mHandColor.value = tinycolor(themeValue.fg).toHexString();
|
||||
hHandColor.value = accent;
|
||||
nowColor.value = accent;
|
||||
}
|
||||
@@ -207,13 +207,13 @@ calcColors();
|
||||
|
||||
onMounted(() => {
|
||||
defaultIdlingRenderScheduler.add(tick);
|
||||
globalEvents.on('themeChanged', calcColors);
|
||||
themeManager.on('themeChanged', calcColors);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
enabled = false;
|
||||
defaultIdlingRenderScheduler.delete(tick);
|
||||
globalEvents.off('themeChanged', calcColors);
|
||||
themeManager.off('themeChanged', calcColors);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import * as Misskey from 'misskey-js';
|
||||
import Cropper from 'cropperjs';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -105,10 +105,10 @@ onMounted(() => {
|
||||
cropper = new Cropper(imgEl.value, {
|
||||
});
|
||||
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
const themeValue = themeManager.currentCompiledTheme!;
|
||||
|
||||
const selection = cropper.getCropperSelection()!;
|
||||
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
|
||||
selection.themeColor = tinycolor(themeValue.accent).toHexString();
|
||||
if (props.aspectRatio != null) selection.aspectRatio = props.aspectRatio;
|
||||
selection.initialAspectRatio = props.aspectRatio ?? 1;
|
||||
selection.outlined = true;
|
||||
|
||||
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { getBgColor } from '@/utility/get-bg-color.js';
|
||||
|
||||
const miLocalStoragePrefix = 'ui:folder:' as const;
|
||||
@@ -92,11 +92,11 @@ function updateBgColor() {
|
||||
|
||||
onMounted(() => {
|
||||
updateBgColor();
|
||||
globalEvents.on('themeChanging', updateBgColor);
|
||||
themeManager.on('themeChanging', updateBgColor);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
globalEvents.off('themeChanging', updateBgColor);
|
||||
themeManager.off('themeChanging', updateBgColor);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ import { nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { getBgColor } from '@/utility/get-bg-color.js';
|
||||
import { pageFolderTeleportCount, popup } from '@/os.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import MkFolderPage from '@/components/MkFolderPage.vue';
|
||||
import { deviceKind } from '@/utility/device-kind.js';
|
||||
|
||||
@@ -192,9 +193,9 @@ async function toggle(ev: PointerEvent) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
const themeValue = themeManager.currentCompiledTheme!;
|
||||
const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent';
|
||||
const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
|
||||
const myBg = themeValue.panel;
|
||||
bgSame.value = parentBg === myBg;
|
||||
});
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
||||
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
|
||||
initChart();
|
||||
|
||||
@@ -186,7 +187,7 @@ function createDoughnut(chartEl: HTMLCanvasElement, tooltip: ReturnType<typeof u
|
||||
labels: data.map(x => x.name),
|
||||
datasets: [{
|
||||
backgroundColor: data.map(x => x.color),
|
||||
borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
|
||||
borderColor: themeManager.currentCompiledTheme!.panel,
|
||||
borderWidth: 2,
|
||||
hoverOffset: 0,
|
||||
data: data.map(x => x.value),
|
||||
|
||||
@@ -33,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref } from 'vue';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
|
||||
@@ -47,8 +48,7 @@ const polylinePoints = ref('');
|
||||
const polygonPoints = ref('');
|
||||
const headX = ref<number | null>(null);
|
||||
const headY = ref<number | null>(null);
|
||||
const clock = ref<number | null>(null);
|
||||
const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent'));
|
||||
const accent = tinycolor(themeManager.currentCompiledTheme!.accent);
|
||||
const color = accent.toRgbString();
|
||||
|
||||
function draw(): void {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Chart } from 'chart.js';
|
||||
import type { ScatterDataPoint } from 'chart.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { store } from '@/store.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/utility/chart-vline.js';
|
||||
import { alpha } from '@/utility/color.js';
|
||||
@@ -51,7 +52,7 @@ onMounted(async () => {
|
||||
|
||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent'));
|
||||
const accent = tinycolor(themeManager.currentCompiledTheme!.accent);
|
||||
const color = accent.toHex();
|
||||
|
||||
if (chartEl.value == null) return;
|
||||
|
||||
@@ -16,11 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch, onBeforeUnmount, ref, useTemplateRef } from 'vue';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
const loaded = !!window.TagCanvas;
|
||||
const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
|
||||
const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
|
||||
const available = ref(false);
|
||||
@@ -33,7 +33,7 @@ watch(available, () => {
|
||||
try {
|
||||
window.TagCanvas.Start(idForCanvas, idForTags, {
|
||||
textColour: '#ffffff',
|
||||
outlineColour: tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(),
|
||||
outlineColour: tinycolor(themeManager.currentCompiledTheme!.accent).toHexString(),
|
||||
outlineRadius: 10,
|
||||
initial: [-0.030, -0.010],
|
||||
frontSelect: true,
|
||||
|
||||
@@ -43,8 +43,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { ref, watch } from 'vue';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import { compile } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import { compile } from '@@/js/theme.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, useTemplateRef, ref, nextTick } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import gradient from 'chartjs-plugin-gradient';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { store } from '@/store.js';
|
||||
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/utility/chart-vline.js';
|
||||
@@ -61,8 +61,7 @@ async function renderChart() {
|
||||
|
||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
|
||||
const accent = tinycolor(themeManager.currentCompiledTheme!.accent).toHexString();
|
||||
|
||||
const colorRead = accent;
|
||||
const colorWrite = '#2ecc71';
|
||||
|
||||
@@ -324,8 +324,9 @@ function onTopHandlePointerdown(evt: PointerEvent) {
|
||||
if (main == null) return;
|
||||
|
||||
const base = getPositionY(evt);
|
||||
const height = parseInt(getComputedStyle(main, '').height, 10);
|
||||
const top = parseInt(getComputedStyle(main, '').top, 10);
|
||||
const computedStyle = getComputedStyle(main, '');
|
||||
const height = parseInt(computedStyle.height, 10);
|
||||
const top = parseInt(computedStyle.top, 10);
|
||||
|
||||
// 動かした時
|
||||
dragListen(me => {
|
||||
@@ -353,8 +354,9 @@ function onRightHandlePointerdown(evt: PointerEvent) {
|
||||
if (main == null) return;
|
||||
|
||||
const base = getPositionX(evt);
|
||||
const width = parseInt(getComputedStyle(main, '').width, 10);
|
||||
const left = parseInt(getComputedStyle(main, '').left, 10);
|
||||
const computedStyle = getComputedStyle(main, '');
|
||||
const width = parseInt(computedStyle.width, 10);
|
||||
const left = parseInt(computedStyle.left, 10);
|
||||
const browserWidth = window.innerWidth;
|
||||
|
||||
// 動かした時
|
||||
@@ -380,8 +382,9 @@ function onBottomHandlePointerdown(evt: PointerEvent) {
|
||||
if (main == null) return;
|
||||
|
||||
const base = getPositionY(evt);
|
||||
const height = parseInt(getComputedStyle(main, '').height, 10);
|
||||
const top = parseInt(getComputedStyle(main, '').top, 10);
|
||||
const computedStyle = getComputedStyle(main, '');
|
||||
const height = parseInt(computedStyle.height, 10);
|
||||
const top = parseInt(computedStyle.top, 10);
|
||||
const browserHeight = window.innerHeight;
|
||||
|
||||
// 動かした時
|
||||
@@ -407,8 +410,9 @@ function onLeftHandlePointerdown(evt: PointerEvent) {
|
||||
if (main == null) return;
|
||||
|
||||
const base = getPositionX(evt);
|
||||
const width = parseInt(getComputedStyle(main, '').width, 10);
|
||||
const left = parseInt(getComputedStyle(main, '').left, 10);
|
||||
const computedStyle = getComputedStyle(main, '');
|
||||
const width = parseInt(computedStyle.width, 10);
|
||||
const left = parseInt(computedStyle.left, 10);
|
||||
|
||||
// 動かした時
|
||||
dragListen(me => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { Directive } from 'vue';
|
||||
import { getBgColor } from '@/utility/get-bg-color.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
|
||||
const handlerMap = new WeakMap<HTMLElement, () => void>();
|
||||
|
||||
@@ -27,10 +27,10 @@ export const adaptiveBorderDirective = {
|
||||
|
||||
calc();
|
||||
|
||||
globalEvents.on('themeChanged', calc);
|
||||
themeManager.on('themeChanged', calc);
|
||||
},
|
||||
|
||||
unmounted(src) {
|
||||
globalEvents.off('themeChanged', handlerMap.get(src));
|
||||
themeManager.off('themeChanged', handlerMap.get(src));
|
||||
},
|
||||
} as Directive<HTMLElement>;
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
*/
|
||||
|
||||
import type { Directive } from 'vue';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { getBgColor } from '@/utility/get-bg-color.js';
|
||||
|
||||
export const panelDirective = {
|
||||
mounted(src) {
|
||||
const parentBg = getBgColor(src.parentElement) ?? 'transparent';
|
||||
|
||||
const myBg = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel');
|
||||
const myBg = themeManager.currentCompiledTheme!.panel;
|
||||
|
||||
if (parentBg === myBg) {
|
||||
src.style.backgroundColor = 'var(--MI_THEME-bg)';
|
||||
|
||||
@@ -8,8 +8,6 @@ import * as Misskey from 'misskey-js';
|
||||
import { onBeforeUnmount } from 'vue';
|
||||
|
||||
type Events = {
|
||||
themeChanging: () => void;
|
||||
themeChanged: () => void;
|
||||
clientNotification: (notification: Misskey.entities.Notification) => void;
|
||||
notePosted: (note: Misskey.entities.Note) => void;
|
||||
noteDeleted: (noteId: Misskey.entities.Note['id']) => void;
|
||||
|
||||
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, useTemplateRef } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
|
||||
@@ -43,7 +44,7 @@ onMounted(() => {
|
||||
labels: props.data.map(x => x.name),
|
||||
datasets: [{
|
||||
backgroundColor: props.data.map(x => x.color ?? '#000'),
|
||||
borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
|
||||
borderColor: themeManager.currentCompiledTheme!.panel,
|
||||
borderWidth: 2,
|
||||
hoverOffset: 0,
|
||||
data: props.data.map(x => x.value),
|
||||
|
||||
@@ -53,7 +53,8 @@ import FormSection from '@/components/form/section.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { parsePluginMeta, installPlugin } from '@/plugin.js';
|
||||
import { parseThemeCode, installTheme } from '@/theme.js';
|
||||
import { installTheme } from '@/theme.js';
|
||||
import { parseThemeCode } from '@@/js/theme.js';
|
||||
import { unisonReload } from '@/utility/unison-reload.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
@@ -20,7 +20,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { ref, computed } from 'vue';
|
||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { parseThemeCode, previewTheme, installTheme } from '@/theme.js';
|
||||
import { themeManager, installTheme, handleThemeInstallError } from '@/theme.js';
|
||||
import { parseThemeCode } from '@@/js/theme.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
@@ -29,6 +30,19 @@ import { useRouter } from '@/router.js';
|
||||
const router = useRouter();
|
||||
const installThemeCode = ref<string | null>(null);
|
||||
|
||||
function previewTheme(code: string): void {
|
||||
try {
|
||||
const theme = parseThemeCode(code);
|
||||
themeManager.previewTheme(theme);
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._theme.invalid,
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function install(code: string): Promise<void> {
|
||||
try {
|
||||
const theme = parseThemeCode(code);
|
||||
@@ -40,22 +54,7 @@ async function install(code: string): Promise<void> {
|
||||
installThemeCode.value = null;
|
||||
router.push('/settings/theme');
|
||||
} catch (err: any) {
|
||||
switch (err.message.toLowerCase()) {
|
||||
case 'this theme is already installed':
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: i18n.ts._theme.alreadyInstalled,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._theme.invalid,
|
||||
});
|
||||
break;
|
||||
}
|
||||
console.error(err);
|
||||
handleThemeInstallError(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,21 +27,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { getBuiltinThemesRef, getThemesRef, removeTheme } from '@/theme.js';
|
||||
import { removeTheme } from '@/theme.js';
|
||||
import { getBuiltinThemes } from '@@/js/theme.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||
import { prefer } from '@/preferences';
|
||||
|
||||
const installedThemes = prefer.r.themes;
|
||||
const builtinThemes = ref<Theme[]>([]);
|
||||
getBuiltinThemes().then(themes => {
|
||||
builtinThemes.value = themes;
|
||||
});
|
||||
|
||||
const installedThemes = getThemesRef();
|
||||
const builtinThemes = getBuiltinThemesRef();
|
||||
const {
|
||||
model: selectedThemeId,
|
||||
def: selectedThemeIdDef,
|
||||
|
||||
@@ -210,7 +210,7 @@ import JSON5 from 'json5';
|
||||
import defaultLightTheme from '@@/themes/l-light.json5';
|
||||
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
|
||||
import { isSafeMode } from '@@/js/config.js';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
@@ -218,7 +218,8 @@ import FormLink from '@/components/form/link.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkThemePreview from '@/components/MkThemePreview.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { getBuiltinThemesRef, getThemesRef, installTheme, parseThemeCode, removeTheme } from '@/theme.js';
|
||||
import { handleThemeInstallError, installTheme, removeTheme } from '@/theme.js';
|
||||
import { getBuiltinThemes } from '@@/js/theme.js';
|
||||
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
|
||||
import { store } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@@ -229,8 +230,11 @@ import { prefer } from '@/preferences.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { checkDragDataType, getDragData, getPlainDragData, setDragData, setPlainDragData } from '@/drag-and-drop.js';
|
||||
|
||||
const installedThemes = getThemesRef();
|
||||
const builtinThemes = getBuiltinThemesRef();
|
||||
const installedThemes = prefer.r.themes;
|
||||
const builtinThemes = ref<Theme[]>([]);
|
||||
getBuiltinThemes().then(themes => {
|
||||
builtinThemes.value = themes;
|
||||
});
|
||||
|
||||
const instanceDarkTheme = computed<Theme | null>(() => instance.defaultDarkTheme ? JSON5.parse(instance.defaultDarkTheme) : null);
|
||||
const installedDarkThemes = computed(() => installedThemes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
|
||||
@@ -353,7 +357,7 @@ async function onDrop(ev: DragEvent) {
|
||||
try {
|
||||
await installTheme(code);
|
||||
} catch (err) {
|
||||
// nop
|
||||
handleThemeInstallError(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,14 +79,15 @@ import JSON5 from 'json5';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
import { host } from '@@/js/config.js';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { addTheme, applyTheme } from '@/theme.js';
|
||||
import { addTheme, themeManager } from '@/theme.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import * as os from '@/os.js';
|
||||
import { store } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@@ -130,7 +131,7 @@ const theme = ref<Theme>({
|
||||
name: 'untitled',
|
||||
author: `@${$i.username}@${toUnicode(host)}`,
|
||||
base: 'light',
|
||||
props: lightTheme.props,
|
||||
props: deepClone(lightTheme.props),
|
||||
});
|
||||
const description = ref<string | null>(null);
|
||||
const themeCode = ref<string>('');
|
||||
@@ -170,7 +171,7 @@ function setFgColor(color: typeof fgColors[number]) {
|
||||
|
||||
function apply() {
|
||||
themeCode.value = JSON5.stringify(theme.value, null, '\t');
|
||||
applyTheme(theme.value, false);
|
||||
themeManager.previewTheme(theme.value);
|
||||
changed.value = true;
|
||||
}
|
||||
|
||||
@@ -201,7 +202,7 @@ async function saveAs() {
|
||||
theme.value.name = name;
|
||||
if (description.value) theme.value.desc = description.value;
|
||||
await addTheme(theme.value);
|
||||
applyTheme(theme.value);
|
||||
themeManager.updateTheme(theme.value);
|
||||
if (store.s.darkMode) {
|
||||
prefer.commit('darkTheme', theme.value);
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { hemisphere } from '@@/js/intl-const.js';
|
||||
import { DEFAULT_EMOJIS } from '@@/js/const.js';
|
||||
import { prefersReducedMotion } from '@@/js/config.js';
|
||||
import { definePreferences } from './manager.js';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import type { SoundType } from '@/utility/sound.js';
|
||||
import type { Plugin } from '@/plugin.js';
|
||||
import type { DeviceKind } from '@/utility/device-kind.js';
|
||||
|
||||
@@ -6,73 +6,183 @@
|
||||
// TODO: (可能な部分を)sharedに抽出して frontend-embed と共通化
|
||||
|
||||
import { ref, nextTick } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
import JSON5 from 'json5';
|
||||
import { version } from '@@/js/config.js';
|
||||
import type { Ref } from 'vue';
|
||||
import type { BundledTheme } from 'shiki/themes';
|
||||
import { getBuiltinThemes, parseThemeCode, themeProps, compile } from '@@/js/theme.js';
|
||||
import type { Theme, CompiledTheme } from '@@/js/theme.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { deepEqual } from '@/utility/deep-equal.js';
|
||||
|
||||
export type Theme = {
|
||||
id: string;
|
||||
name: string;
|
||||
author: string;
|
||||
desc?: string;
|
||||
base?: 'dark' | 'light';
|
||||
kind?: 'dark' | 'light'; // legacy
|
||||
props: Record<string, string>;
|
||||
codeHighlighter?: {
|
||||
base: BundledTheme;
|
||||
overrides?: Record<string, any>;
|
||||
} | {
|
||||
base: '_none_';
|
||||
overrides: Record<string, any>;
|
||||
};
|
||||
type ThemeManagerEvents = {
|
||||
'themeChanging': () => void;
|
||||
'themeChanged': () => void;
|
||||
'previewStateChanged': (isPreview: boolean) => void;
|
||||
'requestUpdateThemeCache': (theme: Theme, compiled: CompiledTheme) => void;
|
||||
};
|
||||
|
||||
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
||||
class ThemeManager extends EventEmitter<ThemeManagerEvents> {
|
||||
/** 現在常用しているテーマ */
|
||||
private _theme: Theme | null = null;
|
||||
get theme() { return this._theme; }
|
||||
private _compiledTheme: CompiledTheme | null = null;
|
||||
get compiledTheme() { return this._compiledTheme; }
|
||||
|
||||
export const getBuiltinThemes = () => Promise.all(
|
||||
[
|
||||
'l-light',
|
||||
'l-coffee',
|
||||
'l-apricot',
|
||||
'l-rainy',
|
||||
'l-botanical',
|
||||
'l-vivid',
|
||||
'l-cherry',
|
||||
'l-sushi',
|
||||
'l-u0',
|
||||
/** 現在適用中のテーマ */
|
||||
private _currentTheme: Theme | null = null;
|
||||
get currentTheme() { return this._currentTheme; }
|
||||
get currentThemeId() { return this._currentTheme?.id; }
|
||||
private _currentCompiledTheme: CompiledTheme | null = null;
|
||||
get currentCompiledTheme() { return this._currentCompiledTheme; }
|
||||
|
||||
'd-dark',
|
||||
'd-persimmon',
|
||||
'd-astro',
|
||||
'd-future',
|
||||
'd-botanical',
|
||||
'd-green-lime',
|
||||
'd-green-orange',
|
||||
'd-cherry',
|
||||
'd-ice',
|
||||
'd-u0',
|
||||
].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
|
||||
);
|
||||
/** プレビュー中かどうか */
|
||||
private _isPreviewMode = false;
|
||||
get isPreviewMode() { return this._isPreviewMode; }
|
||||
set isPreviewMode(value: boolean) {
|
||||
if (this._isPreviewMode !== value) {
|
||||
this._isPreviewMode = value;
|
||||
this.emit('previewStateChanged', value);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBuiltinThemesRef() {
|
||||
const builtinThemes = ref<Theme[]>([]);
|
||||
getBuiltinThemes().then(themes => builtinThemes.value = themes);
|
||||
return builtinThemes;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/** テーマを更新し、同時に適用します。 */
|
||||
public updateTheme(newTheme: Theme) {
|
||||
if (newTheme.id === this.theme?.id && version === miLocalStorage.getItem('themeCachedVersion')) return; // 変更なし
|
||||
|
||||
this.isPreviewMode = false;
|
||||
|
||||
// テーマを更新
|
||||
this._theme = deepClone(newTheme);
|
||||
const compiled = this.compile(newTheme);
|
||||
this._compiledTheme = compiled;
|
||||
|
||||
// 適用中のテーマも更新
|
||||
this._currentTheme = deepClone(this.theme);
|
||||
this._currentCompiledTheme = deepClone(compiled);
|
||||
|
||||
this.applyTheme();
|
||||
}
|
||||
|
||||
/** プレビュー用のテーマを適用します。 */
|
||||
public previewTheme(theme: Theme) {
|
||||
this.isPreviewMode = true;
|
||||
|
||||
// 適用中のテーマを更新
|
||||
this._currentTheme = deepClone(theme);
|
||||
this._currentCompiledTheme = this.compile(theme);
|
||||
|
||||
this.applyTheme();
|
||||
}
|
||||
|
||||
/** プレビュー状態を解除し、適用中のテーマを常用しているテーマに戻します。 */
|
||||
public clearPreview() {
|
||||
this.isPreviewMode = false;
|
||||
|
||||
// 適用中のテーマを常用しているテーマに戻す
|
||||
this._currentTheme = deepClone(this.theme);
|
||||
this._currentCompiledTheme = deepClone(this.compiledTheme);
|
||||
|
||||
this.applyTheme();
|
||||
}
|
||||
|
||||
/** 通常のテーマのコンパイルに加え、ベースとなるテーマの値を解決し代入します。 */
|
||||
private compile(theme: Theme) {
|
||||
const _theme = deepClone(theme);
|
||||
|
||||
if (_theme.base != null) {
|
||||
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
|
||||
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
|
||||
}
|
||||
|
||||
return compile(_theme);
|
||||
}
|
||||
|
||||
/** currentThemeを適用します。 */
|
||||
private applyTheme() {
|
||||
if (this.currentTheme == null || this.currentCompiledTheme == null) return;
|
||||
|
||||
// visibilityStateがhiddenな状態でstartViewTransitionするとブラウザによってはエラーになる
|
||||
// 通常hiddenな時に呼ばれることはないが、iOSのPWAだとアプリ切り替え時に(何故か)hiddenな状態で(何故か)一瞬デバイスのダークモード判定が変わりapplyThemeが呼ばれる場合がある
|
||||
if (window.document.startViewTransition != null && window.document.visibilityState === 'visible') {
|
||||
window.document.documentElement.classList.add('_themeChanging_');
|
||||
try {
|
||||
window.document.startViewTransition(async () => {
|
||||
this.updateAttributes();
|
||||
await nextTick();
|
||||
}).finished.then(() => {
|
||||
window.document.documentElement.classList.remove('_themeChanging_');
|
||||
this.emit('themeChanged');
|
||||
});
|
||||
} catch (err) {
|
||||
// 様々な理由により startViewTransition は失敗することがある
|
||||
// ref. https://github.com/misskey-dev/misskey/issues/16562
|
||||
|
||||
// FIXME: viewTransitonエラーはtry~catch貫通してそうな気配がする
|
||||
console.error(err);
|
||||
|
||||
window.document.documentElement.classList.remove('_themeChanging_');
|
||||
this.updateAttributes();
|
||||
this.emit('themeChanged');
|
||||
}
|
||||
} else {
|
||||
this.updateAttributes();
|
||||
this.emit('themeChanged');
|
||||
}
|
||||
|
||||
if (!this.isPreviewMode) {
|
||||
this.emit('requestUpdateThemeCache', this.currentTheme, this.currentCompiledTheme);
|
||||
}
|
||||
}
|
||||
|
||||
private updateAttributes() {
|
||||
if (!this.currentTheme || !this.currentCompiledTheme) return;
|
||||
|
||||
const colorScheme = this.currentTheme.base === 'dark' ? 'dark' : 'light';
|
||||
window.document.documentElement.dataset.colorScheme = colorScheme;
|
||||
|
||||
for (const tag of window.document.head.children) {
|
||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||
tag.setAttribute('content', this.currentCompiledTheme['htmlThemeColor']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of themeProps) {
|
||||
const value = this.currentCompiledTheme[key];
|
||||
if (value) {
|
||||
window.document.documentElement.style.setProperty(`--MI_THEME-${key}`, value.toString());
|
||||
} else {
|
||||
window.document.documentElement.style.removeProperty(`--MI_THEME-${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
window.document.documentElement.style.setProperty('color-scheme', colorScheme);
|
||||
|
||||
this.emit('themeChanging');
|
||||
}
|
||||
}
|
||||
|
||||
export function getThemesRef(): Ref<Theme[]> {
|
||||
return prefer.r.themes;
|
||||
}
|
||||
export const themeManager = new ThemeManager();
|
||||
export const isPreviewMode = ref(false);
|
||||
|
||||
themeManager.on('requestUpdateThemeCache', (theme, props) => {
|
||||
miLocalStorage.setItem('theme', JSON.stringify(props));
|
||||
miLocalStorage.setItem('themeId', theme.id);
|
||||
miLocalStorage.setItem('themeCachedVersion', version);
|
||||
});
|
||||
|
||||
themeManager.on('previewStateChanged', (preview) => {
|
||||
isPreviewMode.value = preview;
|
||||
});
|
||||
|
||||
export async function addTheme(theme: Theme): Promise<void> {
|
||||
if ($i == null) return;
|
||||
@@ -93,163 +203,6 @@ export async function removeTheme(theme: Theme): Promise<void> {
|
||||
prefer.commit('themes', themes);
|
||||
}
|
||||
|
||||
function applyThemeInternal(theme: Theme, persist: boolean) {
|
||||
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
|
||||
|
||||
window.document.documentElement.dataset.colorScheme = colorScheme;
|
||||
|
||||
// Deep copy
|
||||
const _theme = deepClone(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 window.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)) {
|
||||
window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
|
||||
}
|
||||
|
||||
window.document.documentElement.style.setProperty('color-scheme', colorScheme);
|
||||
|
||||
if (persist) {
|
||||
miLocalStorage.setItem('theme', JSON.stringify(props));
|
||||
miLocalStorage.setItem('themeId', theme.id);
|
||||
miLocalStorage.setItem('themeCachedVersion', version);
|
||||
miLocalStorage.setItem('colorScheme', colorScheme);
|
||||
}
|
||||
|
||||
// 色計算など再度行えるようにクライアント全体に通知
|
||||
globalEvents.emit('themeChanging');
|
||||
}
|
||||
|
||||
let timeout: number | null = null;
|
||||
let currentThemeId = miLocalStorage.getItem('themeId');
|
||||
|
||||
export function applyTheme(theme: Theme, persist = true) {
|
||||
if (timeout) {
|
||||
window.clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
|
||||
if (theme.id === currentThemeId && miLocalStorage.getItem('themeCachedVersion') === version) return;
|
||||
currentThemeId = theme.id;
|
||||
|
||||
// visibilityStateがhiddenな状態でstartViewTransitionするとブラウザによってはエラーになる
|
||||
// 通常hiddenな時に呼ばれることはないが、iOSのPWAだとアプリ切り替え時に(何故か)hiddenな状態で(何故か)一瞬デバイスのダークモード判定が変わりapplyThemeが呼ばれる場合がある
|
||||
if (window.document.startViewTransition != null && window.document.visibilityState === 'visible') {
|
||||
window.document.documentElement.classList.add('_themeChanging_');
|
||||
try {
|
||||
window.document.startViewTransition(async () => {
|
||||
applyThemeInternal(theme, persist);
|
||||
await nextTick();
|
||||
}).finished.then(() => {
|
||||
window.document.documentElement.classList.remove('_themeChanging_');
|
||||
globalEvents.emit('themeChanged');
|
||||
});
|
||||
} catch (err) {
|
||||
// 様々な理由により startViewTransition は失敗することがある
|
||||
// ref. https://github.com/misskey-dev/misskey/issues/16562
|
||||
|
||||
// FIXME: viewTransitonエラーはtry~catch貫通してそうな気配がする
|
||||
|
||||
console.error(err);
|
||||
|
||||
window.document.documentElement.classList.remove('_themeChanging_');
|
||||
applyThemeInternal(theme, persist);
|
||||
globalEvents.emit('themeChanged');
|
||||
}
|
||||
} else {
|
||||
applyThemeInternal(theme, persist);
|
||||
globalEvents.emit('themeChanged');
|
||||
}
|
||||
}
|
||||
|
||||
export 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 funcTxt = parts.shift();
|
||||
const argTxt = parts.shift();
|
||||
|
||||
if (funcTxt && argTxt) {
|
||||
const func = funcTxt.substring(1);
|
||||
const arg = parseFloat(argTxt);
|
||||
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 = {} as Record<string, string>;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
export function validateTheme(theme: Record<string, any>): boolean {
|
||||
if (theme.id == null || typeof theme.id !== 'string') return false;
|
||||
if (theme.name == null || typeof theme.name !== 'string') return false;
|
||||
if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
|
||||
if (theme.props == null || typeof theme.props !== 'object') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function parseThemeCode(code: string): Theme {
|
||||
let theme;
|
||||
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (_) {
|
||||
throw new Error('Failed to parse theme json');
|
||||
}
|
||||
if (!validateTheme(theme)) {
|
||||
throw new Error('This theme is invaild');
|
||||
}
|
||||
if (prefer.s.themes.some(t => t.id === theme.id)) {
|
||||
throw new Error('This theme is already installed');
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
export function previewTheme(code: string): void {
|
||||
const theme = parseThemeCode(code);
|
||||
if (theme != null) applyTheme(theme, false);
|
||||
}
|
||||
|
||||
export async function installTheme(code: string): Promise<void> {
|
||||
const theme = parseThemeCode(code);
|
||||
if (theme == null) return;
|
||||
@@ -261,3 +214,26 @@ export function clearAppliedThemeCache() {
|
||||
miLocalStorage.removeItem('themeId');
|
||||
miLocalStorage.removeItem('themeCachedVersion');
|
||||
}
|
||||
|
||||
export function handleThemeInstallError(err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
let message = '';
|
||||
switch (err.message.toLowerCase()) {
|
||||
case 'this theme is already installed':
|
||||
case 'already exists':
|
||||
case 'builtin theme':
|
||||
message = i18n.ts._theme.alreadyInstalled;
|
||||
break;
|
||||
default:
|
||||
message = i18n.ts._theme.invalid;
|
||||
break;
|
||||
}
|
||||
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: message,
|
||||
});
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
57
packages/frontend/src/ui/_common_/ThemePreviewing.vue
Normal file
57
packages/frontend/src/ui/_common_/ThemePreviewing.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<span :class="$style.icon">
|
||||
<i class="ti ti-info-circle"></i>
|
||||
</span>
|
||||
<span :class="$style.title">{{ i18n.ts.previewingTheme }}</span>
|
||||
<span :class="$style.body"><button class="_textButton" style="color: var(--MI_THEME-fgOnAccent);" @click="restore">{{ i18n.ts.previewingThemeRestore }}</button> | <MkA class="_textButton" style="color: var(--MI_THEME-fgOnAccent);" to="/settings/theme">{{ i18n.ts.settings }}</MkA></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { themeManager } from '@/theme.js';
|
||||
|
||||
function restore() {
|
||||
themeManager.clearPreview();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
--height: 24px;
|
||||
font-size: 0.85em;
|
||||
display: flex;
|
||||
vertical-align: bottom;
|
||||
width: 100%;
|
||||
line-height: var(--height);
|
||||
height: var(--height);
|
||||
overflow: clip;
|
||||
contain: strict;
|
||||
background: var(--MI_THEME-accent);
|
||||
color: var(--MI_THEME-fgOnAccent);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 10px;
|
||||
animation: blink 2s infinite;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 0 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.body {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: clip;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
@@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<XReloadSuggestion v-if="shouldSuggestReload"/>
|
||||
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
|
||||
<XThemePreviewing v-if="isThemePreviewMode"/>
|
||||
<XAnnouncements v-if="$i"/>
|
||||
<XStatusBars/>
|
||||
<div :class="$style.columnsWrapper">
|
||||
@@ -94,12 +95,14 @@ import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
|
||||
import XTitlebar from '@/ui/_common_/titlebar.vue';
|
||||
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
|
||||
import XReloadSuggestion from '@/ui/_common_/ReloadSuggestion.vue';
|
||||
import XThemePreviewing from '@/ui/_common_/ThemePreviewing.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { deviceKind } from '@/utility/device-kind.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { store } from '@/store.js';
|
||||
import { isPreviewMode as isThemePreviewMode } from '@/theme.js';
|
||||
import XMainColumn from '@/ui/deck/main-column.vue';
|
||||
import XTlColumn from '@/ui/deck/tl-column.vue';
|
||||
import XAntennaColumn from '@/ui/deck/antenna-column.vue';
|
||||
|
||||
@@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div>
|
||||
<XReloadSuggestion v-if="shouldSuggestReload"/>
|
||||
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
|
||||
<XThemePreviewing v-if="isThemePreviewMode"/>
|
||||
<XAnnouncements v-if="$i"/>
|
||||
<XStatusBars :class="$style.statusbars"/>
|
||||
</div>
|
||||
@@ -40,8 +41,10 @@ import type { PageMetadata } from '@/page.js';
|
||||
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
|
||||
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
|
||||
import XReloadSuggestion from '@/ui/_common_/ReloadSuggestion.vue';
|
||||
import XThemePreviewing from '@/ui/_common_/ThemePreviewing.vue';
|
||||
import XTitlebar from '@/ui/_common_/titlebar.vue';
|
||||
import XSidebar from '@/ui/_common_/navbar.vue';
|
||||
import { isPreviewMode as isThemePreviewMode } from '@/theme.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function getTheme(mode: 'light' | 'dark', getName = false): Promise
|
||||
_res.type = mode;
|
||||
|
||||
if (getName) {
|
||||
return _res.name;
|
||||
return _res.name!;
|
||||
}
|
||||
return _res;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import gradient from 'chartjs-plugin-gradient';
|
||||
import zoomPlugin from 'chartjs-plugin-zoom';
|
||||
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import { store } from '@/store.js';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
|
||||
@@ -50,7 +51,7 @@ export function initChart() {
|
||||
);
|
||||
|
||||
// フォントカラー
|
||||
Chart.defaults.color = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-fg');
|
||||
Chart.defaults.color = themeManager.currentCompiledTheme!.fg;
|
||||
|
||||
Chart.defaults.borderColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ export function popout(path: string, w?: HTMLElement) {
|
||||
url = appendQuery(url, 'zen');
|
||||
if (w) {
|
||||
const position = w.getBoundingClientRect();
|
||||
const width = parseInt(getComputedStyle(w, '').width, 10);
|
||||
const height = parseInt(getComputedStyle(w, '').height, 10);
|
||||
const computedStyle = getComputedStyle(w, '');
|
||||
const width = parseInt(computedStyle.width, 10);
|
||||
const height = parseInt(computedStyle.height, 10);
|
||||
const x = window.screenX + position.left;
|
||||
const y = window.screenY + position.top;
|
||||
window.open(url, url,
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import { genId } from '@/utility/id.js';
|
||||
|
||||
import type { Theme } from '@/theme.js';
|
||||
import { themeProps } from '@/theme.js';
|
||||
import type { Theme } from '@@/js/theme.js';
|
||||
import { themeProps } from '@@/js/theme.js';
|
||||
|
||||
export type Default = null;
|
||||
export type Color = string;
|
||||
|
||||
Reference in New Issue
Block a user