enhance(frontend): テーマをドラッグ&ドロップできるように

This commit is contained in:
syuilo 2025-09-26 15:36:25 +09:00
parent d1446d195a
commit 0b7634b126
3 changed files with 66 additions and 8 deletions

View File

@ -14,6 +14,7 @@
- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました - Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
- Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし)を追加 - Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし)を追加
- Enhance: ウォーターマークにアカウントのQRコードを追加できるように - Enhance: ウォーターマークにアカウントのQRコードを追加できるように
- Enhance: テーマをドラッグ&ドロップできるように
- Enhance: 絵文字ピッカーのサイズをより大きくできるように - Enhance: 絵文字ピッカーのサイズをより大きくできるように
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上 - Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
- Fix: iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正 - Fix: iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正

View File

@ -23,6 +23,15 @@ export function setDragData<T extends keyof DragDataMap>(
event.dataTransfer.setData(`misskey/${type}`.toLowerCase(), JSON.stringify(data)); event.dataTransfer.setData(`misskey/${type}`.toLowerCase(), JSON.stringify(data));
} }
export function setPlainDragData(
event: DragEvent,
data: string,
) {
if (event.dataTransfer == null) return;
event.dataTransfer.setData('text/plain', data);
}
export function getDragData<T extends keyof DragDataMap>( export function getDragData<T extends keyof DragDataMap>(
event: DragEvent, event: DragEvent,
type: T, type: T,
@ -35,6 +44,17 @@ export function getDragData<T extends keyof DragDataMap>(
return JSON.parse(data); return JSON.parse(data);
} }
export function getPlainDragData(
event: DragEvent,
): string | null {
if (event.dataTransfer == null) return null;
const data = event.dataTransfer.getData('text/plain');
if (data == null || data === '') return null;
return data;
}
export function checkDragDataType( export function checkDragDataType(
event: DragEvent, event: DragEvent,
types: (keyof DragDataMap)[], types: (keyof DragDataMap)[],

View File

@ -5,7 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<SearchMarker path="/settings/theme" :label="i18n.ts.theme" :keywords="['theme']" icon="ti ti-palette"> <SearchMarker path="/settings/theme" :label="i18n.ts.theme" :keywords="['theme']" icon="ti ti-palette">
<div class="_gaps_m"> <div
class="_gaps_m"
@dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop"
>
<div v-adaptive-border class="rfqxtzch _panel"> <div v-adaptive-border class="rfqxtzch _panel">
<div class="toggle"> <div class="toggle">
<div class="toggleWrapper"> <div class="toggleWrapper">
@ -58,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.themeRadio" :class="$style.themeRadio"
:value="instanceLightTheme.id" :value="instanceLightTheme.id"
/> />
<label :for="`themeRadio_${instanceLightTheme.id}`" :class="$style.themeItemRoot" class="_button" @contextmenu.prevent.stop="onThemeContextmenu(instanceLightTheme, $event)"> <label :for="`themeRadio_${instanceLightTheme.id}`" :class="$style.themeItemRoot" class="_button" draggable="true" @dragstart="onThemeDragstart($event, instanceLightTheme)" @contextmenu.prevent.stop="onThemeContextmenu(instanceLightTheme, $event)">
<MkThemePreview :theme="instanceLightTheme" :class="$style.themeItemPreview"/> <MkThemePreview :theme="instanceLightTheme" :class="$style.themeItemPreview"/>
<div :class="$style.themeItemCaption">{{ instanceLightTheme.name }}</div> <div :class="$style.themeItemCaption">{{ instanceLightTheme.name }}</div>
</label> </label>
@ -78,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.themeRadio" :class="$style.themeRadio"
:value="theme.id" :value="theme.id"
/> />
<label :for="`themeRadio_${theme.id}`" :class="$style.themeItemRoot" class="_button" @contextmenu.prevent.stop="onThemeContextmenu(theme, $event)"> <label :for="`themeRadio_${theme.id}`" :class="$style.themeItemRoot" class="_button" draggable="true" @dragstart="onThemeDragstart($event, theme)" @contextmenu.prevent.stop="onThemeContextmenu(theme, $event)">
<MkThemePreview :theme="theme" :class="$style.themeItemPreview"/> <MkThemePreview :theme="theme" :class="$style.themeItemPreview"/>
<div :class="$style.themeItemCaption">{{ theme.name }}</div> <div :class="$style.themeItemCaption">{{ theme.name }}</div>
</label> </label>
@ -98,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.themeRadio" :class="$style.themeRadio"
:value="theme.id" :value="theme.id"
/> />
<label :for="`themeRadio_${theme.id}`" :class="$style.themeItemRoot" class="_button" @contextmenu.prevent.stop="onThemeContextmenu(theme, $event)"> <label :for="`themeRadio_${theme.id}`" :class="$style.themeItemRoot" class="_button" draggable="true" @dragstart="onThemeDragstart($event, theme)" @contextmenu.prevent.stop="onThemeContextmenu(theme, $event)">
<MkThemePreview :theme="theme" :class="$style.themeItemPreview"/> <MkThemePreview :theme="theme" :class="$style.themeItemPreview"/>
<div :class="$style.themeItemCaption">{{ theme.name }}</div> <div :class="$style.themeItemCaption">{{ theme.name }}</div>
</label> </label>
@ -129,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.themeRadio" :class="$style.themeRadio"
:value="instanceDarkTheme.id" :value="instanceDarkTheme.id"
/> />
<label :for="`themeRadio_${instanceDarkTheme.id}`" :class="$style.themeItemRoot" class="_button" @contextmenu.prevent.stop="onThemeContextmenu(instanceDarkTheme, $event)"> <label :for="`themeRadio_${instanceDarkTheme.id}`" :class="$style.themeItemRoot" class="_button" draggable="true" @dragstart="onThemeDragstart($event, instanceDarkTheme)" @contextmenu.prevent.stop="onThemeContextmenu(instanceDarkTheme, $event)">
<MkThemePreview :theme="instanceDarkTheme" :class="$style.themeItemPreview"/> <MkThemePreview :theme="instanceDarkTheme" :class="$style.themeItemPreview"/>
<div :class="$style.themeItemCaption">{{ instanceDarkTheme.name }}</div> <div :class="$style.themeItemCaption">{{ instanceDarkTheme.name }}</div>
</label> </label>
@ -149,7 +153,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.themeRadio" :class="$style.themeRadio"
:value="theme.id" :value="theme.id"
/> />
<label :for="`themeRadio_${theme.id}`" :class="$style.themeItemRoot" class="_button" @contextmenu.prevent.stop="onThemeContextmenu(theme, $event)"> <label :for="`themeRadio_${theme.id}`" :class="$style.themeItemRoot" class="_button" draggable="true" @dragstart="onThemeDragstart($event, theme)" @contextmenu.prevent.stop="onThemeContextmenu(theme, $event)">
<MkThemePreview :theme="theme" :class="$style.themeItemPreview"/> <MkThemePreview :theme="theme" :class="$style.themeItemPreview"/>
<div :class="$style.themeItemCaption">{{ theme.name }}</div> <div :class="$style.themeItemCaption">{{ theme.name }}</div>
</label> </label>
@ -169,7 +173,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.themeRadio" :class="$style.themeRadio"
:value="theme.id" :value="theme.id"
/> />
<label :for="`themeRadio_${theme.id}`" :class="$style.themeItemRoot" class="_button" @contextmenu.prevent.stop="onThemeContextmenu(theme, $event)"> <label :for="`themeRadio_${theme.id}`" :class="$style.themeItemRoot" class="_button" draggable="true" @dragstart="onThemeDragstart($event, theme)" @contextmenu.prevent.stop="onThemeContextmenu(theme, $event)">
<MkThemePreview :theme="theme" :class="$style.themeItemPreview"/> <MkThemePreview :theme="theme" :class="$style.themeItemPreview"/>
<div :class="$style.themeItemCaption">{{ theme.name }}</div> <div :class="$style.themeItemCaption">{{ theme.name }}</div>
</label> </label>
@ -214,7 +218,7 @@ import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkThemePreview from '@/components/MkThemePreview.vue'; import MkThemePreview from '@/components/MkThemePreview.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import { getBuiltinThemesRef, getThemesRef, removeTheme } from '@/theme.js'; import { getBuiltinThemesRef, getThemesRef, installTheme, parseThemeCode, removeTheme } from '@/theme.js';
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js'; import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -223,6 +227,7 @@ import { uniqueBy } from '@/utility/array.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { checkDragDataType, getDragData, getPlainDragData, setDragData, setPlainDragData } from '@/drag-and-drop.js';
const installedThemes = getThemesRef(); const installedThemes = getThemesRef();
const builtinThemes = getBuiltinThemesRef(); const builtinThemes = getBuiltinThemesRef();
@ -321,6 +326,38 @@ function onThemeContextmenu(theme: Theme, ev: MouseEvent) {
}], ev); }], ev);
} }
function onThemeDragstart(ev: DragEvent, theme: Theme) {
if (!ev.dataTransfer) return;
ev.dataTransfer.effectAllowed = 'copy';
setPlainDragData(ev, JSON5.stringify(theme, null, '\t'));
}
function onDragover(ev: DragEvent) {
if (!ev.dataTransfer) return;
if (ev.dataTransfer.types[0] === 'text/plain') {
ev.dataTransfer.dropEffect = 'copy';
} else {
ev.dataTransfer.dropEffect = 'none';
}
return false;
}
async function onDrop(ev: DragEvent) {
if (!ev.dataTransfer) return;
const code = getPlainDragData(ev);
if (code != null) {
try {
await installTheme(code);
} catch (err) {
// nop
}
}
}
const headerActions = computed(() => []); const headerActions = computed(() => []);
const headerTabs = computed(() => []); const headerTabs = computed(() => []);