From d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 9 Mar 2025 12:34:08 +0900 Subject: Refine preferences (#15597) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * wip * test * wip rollup pluginでsearchIndexの情報生成 * wip * SPDX * wip: markerIdを自動付与 * rollupでビルド時・devモード時に毎回uuidを生成するように * 開発サーバーでだけ必要な挙動は開発サーバーのみで * 条件が逆 * wip: childrenの生成 * update comment * update comment * rename auto generated file * hashをパスと行数から決定 * Update privacy.vue * Update privacy.vue * wip * Update general.vue * Update general.vue * wip * wip * Update SearchMarker.vue * wip * Update profile.vue * Update mute-block.vue * Update mute-block.vue * Update general.vue * Update general.vue * childrenがduplicate key errorを吐く問題をいったん解決 * マーカーの形を成形 * loggerを置きかえ * とりあえず省略記法に対応 * Refactor and Format codes * wip * Update settings-search-index.ts * wip * wip * とりあえず不確定要因の仮置きidを削除 * hashの生成を正規化(絶対パスになっていたのを緩和) * pathの入力を省略可能に * adminでもパス生成できるように * Update settings-search-index.ts * Update privacy.vue * wip * build searchIndex * wip * build * Update general.vue * build * Update sounds.vue * build * build * Update sounds.vue * 🎨 * 🎨 * Update privacy.vue * Update privacy.vue * Update security.vue * create-search-indexを多少改善 * build * Update 2fa.vue * wip * 必ずtransformCodeCacheを利用するように, キャッシュの明確な受け渡しを定義 * キャッシュはdevServerでなくても更新 * Revert "wip" This reverts commit 41bffd3a13f55618bf939dc1c9acb2a77ead4054. * inlining * wip * Update theme.vue * 🎨 * wip normalize * Update theme.vue * キャッシュのパス変換 * build * wip * wip * Update SearchMarker.vue * i18n.ts['key'] の形式が取り出せない問題のFix * build * 仮でpath入れ * 必ず絶対パスが使われるように * wip * 🎨 * storybookビルド時はcreateSearchIndexをしない * inliningの構造化 * format code * Update index.vue * wip * wip * 🎨 * wip * wip * wip * wip * wip * wip * wip * wip * clean up * wip * wip * wip * Update rollup-plugin-unwind-css-module-class-name.test.ts * Update navbar.vue * clean up * wip * wip * wip * wip * wip * Update preferences-backups.vue * Update common.ts * Update preferences.ts * wip * wip * wip * wip * Update MkPreferenceContainer.vue * Update MkPreferenceContainer.vue * Update MkPreferenceContainer.vue * enhance: 検索で上下矢印を使用することで検索結果を移動できるように * Update main-boot.ts * refactor * wip * Update sounds.vue * fix(frontend): PageWindowでSearchMarkerが動作するように * enhance(frontend): SearchMarkerの点滅を一定時間で止める * wip * lint fix * fix: 子要素監視が抜けていたのを修正 * アニメーションの回数はCSSで制御するように * refactor * enhance(frontend): 検索インデックス作成時のログを削減 * revert * fix * fix * Update preferences.ts * Update preferences.ts * wip * Update preferences.ts * wip * 🎨 * wip * Update MkPreferenceContainer.vue * wip * Update preferences.ts * wip * Update preferences.ts * Update preferences.ts * wip * wip * Update preferences.ts * wip * wip * Update preferences.ts * Update CHANGELOG.md * Update preferences.ts * Update deck-store.ts * deckStoreをdefaultStoreに統合 * wip * defaultStore -> store * Update profile.ts * wip * refactor * wip: plugin * plugin * plugin * plugin * Update plugin.ts * wip * Update plugin.vue * Update preferences.ts * Update main-boot.ts * wip * fix test * Update plugin.vue * Update plugin.vue * Update utility.ts * wip * wip * Update utility.ts * wip * wip * clean up * Update utility.ts --------- Co-authored-by: tai-cha Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com> Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> --- packages/frontend/src/deck.ts | 306 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 packages/frontend/src/deck.ts (limited to 'packages/frontend/src/deck.ts') diff --git a/packages/frontend/src/deck.ts b/packages/frontend/src/deck.ts new file mode 100644 index 0000000000..dee9a3db78 --- /dev/null +++ b/packages/frontend/src/deck.ts @@ -0,0 +1,306 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { throttle } from 'throttle-debounce'; +import { notificationTypes } from 'misskey-js'; +import type { BasicTimelineType } from '@/timelines.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { deepClone } from '@/scripts/clone.js'; +import { store } from '@/store.js'; + +type ColumnWidget = { + name: string; + id: string; + data: Record; +}; + +export const columnTypes = [ + 'main', + 'widgets', + 'notifications', + 'tl', + 'antenna', + 'list', + 'channel', + 'mentions', + 'direct', + 'roleTimeline', +] as const; + +export type ColumnType = typeof columnTypes[number]; + +export type Column = { + id: string; + type: ColumnType; + name: string | null; + width: number; + widgets?: ColumnWidget[]; + active?: boolean; + flexible?: boolean; + antennaId?: string; + listId?: string; + channelId?: string; + roleId?: string; + excludeTypes?: typeof notificationTypes[number][]; + tl?: BasicTimelineType; + withRenotes?: boolean; + withReplies?: boolean; + withSensitive?: boolean; + onlyFiles?: boolean; + soundSetting?: SoundStore; +}; + +export const loadDeck = async () => { + let deck; + + try { + deck = await misskeyApi('i/registry/get', { + scope: ['client', 'deck', 'profiles'], + key: store.state['deck.profile'], + }); + } catch (err) { + if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') { + // 後方互換性のため + if (store.state['deck.profile'] === 'default') { + saveDeck(); + return; + } + + store.set('deck.columns', []); + store.set('deck.layout', []); + return; + } + throw err; + } + + store.set('deck.columns', deck.columns); + store.set('deck.layout', deck.layout); +}; + +export async function forceSaveDeck() { + await misskeyApi('i/registry/set', { + scope: ['client', 'deck', 'profiles'], + key: store.state['deck.profile'], + value: { + columns: store.reactiveState['deck.columns'].value, + layout: store.reactiveState['deck.layout'].value, + }, + }); +} + +// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する +export const saveDeck = throttle(1000, () => { + forceSaveDeck(); +}); + +export async function getProfiles(): Promise { + return await misskeyApi('i/registry/keys', { + scope: ['client', 'deck', 'profiles'], + }); +} + +export async function deleteProfile(key: string): Promise { + return await misskeyApi('i/registry/remove', { + scope: ['client', 'deck', 'profiles'], + key: key, + }); +} + +export function addColumn(column: Column) { + if (column.name === undefined) column.name = null; + store.push('deck.columns', column); + store.push('deck.layout', [column.id]); + saveDeck(); +} + +export function removeColumn(id: Column['id']) { + store.set('deck.columns', store.state['deck.columns'].filter(c => c.id !== id)); + store.set('deck.layout', store.state['deck.layout'] + .map(ids => ids.filter(_id => _id !== id)) + .filter(ids => ids.length > 0)); + saveDeck(); +} + +export function swapColumn(a: Column['id'], b: Column['id']) { + const aX = store.state['deck.layout'].findIndex(ids => ids.indexOf(a) !== -1); + const aY = store.state['deck.layout'][aX].findIndex(id => id === a); + const bX = store.state['deck.layout'].findIndex(ids => ids.indexOf(b) !== -1); + const bY = store.state['deck.layout'][bX].findIndex(id => id === b); + const layout = deepClone(store.state['deck.layout']); + layout[aX][aY] = b; + layout[bX][bY] = a; + store.set('deck.layout', layout); + saveDeck(); +} + +export function swapLeftColumn(id: Column['id']) { + const layout = deepClone(store.state['deck.layout']); + store.state['deck.layout'].some((ids, i) => { + if (ids.includes(id)) { + const left = store.state['deck.layout'][i - 1]; + if (left) { + layout[i - 1] = store.state['deck.layout'][i]; + layout[i] = left; + store.set('deck.layout', layout); + } + return true; + } + return false; + }); + saveDeck(); +} + +export function swapRightColumn(id: Column['id']) { + const layout = deepClone(store.state['deck.layout']); + store.state['deck.layout'].some((ids, i) => { + if (ids.includes(id)) { + const right = store.state['deck.layout'][i + 1]; + if (right) { + layout[i + 1] = store.state['deck.layout'][i]; + layout[i] = right; + store.set('deck.layout', layout); + } + return true; + } + return false; + }); + saveDeck(); +} + +export function swapUpColumn(id: Column['id']) { + const layout = deepClone(store.state['deck.layout']); + const idsIndex = store.state['deck.layout'].findIndex(ids => ids.includes(id)); + const ids = deepClone(store.state['deck.layout'][idsIndex]); + ids.some((x, i) => { + if (x === id) { + const up = ids[i - 1]; + if (up) { + ids[i - 1] = id; + ids[i] = up; + + layout[idsIndex] = ids; + store.set('deck.layout', layout); + } + return true; + } + return false; + }); + saveDeck(); +} + +export function swapDownColumn(id: Column['id']) { + const layout = deepClone(store.state['deck.layout']); + const idsIndex = store.state['deck.layout'].findIndex(ids => ids.includes(id)); + const ids = deepClone(store.state['deck.layout'][idsIndex]); + ids.some((x, i) => { + if (x === id) { + const down = ids[i + 1]; + if (down) { + ids[i + 1] = id; + ids[i] = down; + + layout[idsIndex] = ids; + store.set('deck.layout', layout); + } + return true; + } + return false; + }); + saveDeck(); +} + +export function stackLeftColumn(id: Column['id']) { + let layout = deepClone(store.state['deck.layout']); + const i = store.state['deck.layout'].findIndex(ids => ids.includes(id)); + layout = layout.map(ids => ids.filter(_id => _id !== id)); + layout[i - 1].push(id); + layout = layout.filter(ids => ids.length > 0); + store.set('deck.layout', layout); + saveDeck(); +} + +export function popRightColumn(id: Column['id']) { + let layout = deepClone(store.state['deck.layout']); + const i = store.state['deck.layout'].findIndex(ids => ids.includes(id)); + const affected = layout[i]; + layout = layout.map(ids => ids.filter(_id => _id !== id)); + layout.splice(i + 1, 0, [id]); + layout = layout.filter(ids => ids.length > 0); + store.set('deck.layout', layout); + + const columns = deepClone(store.state['deck.columns']); + for (const column of columns) { + if (affected.includes(column.id)) { + column.active = true; + } + } + store.set('deck.columns', columns); + + saveDeck(); +} + +export function addColumnWidget(id: Column['id'], widget: ColumnWidget) { + const columns = deepClone(store.state['deck.columns']); + const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id); + const column = deepClone(store.state['deck.columns'][columnIndex]); + if (column == null) return; + if (column.widgets == null) column.widgets = []; + column.widgets.unshift(widget); + columns[columnIndex] = column; + store.set('deck.columns', columns); + saveDeck(); +} + +export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) { + const columns = deepClone(store.state['deck.columns']); + const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id); + const column = deepClone(store.state['deck.columns'][columnIndex]); + if (column == null) return; + if (column.widgets == null) column.widgets = []; + column.widgets = column.widgets.filter(w => w.id !== widget.id); + columns[columnIndex] = column; + store.set('deck.columns', columns); + saveDeck(); +} + +export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) { + const columns = deepClone(store.state['deck.columns']); + const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id); + const column = deepClone(store.state['deck.columns'][columnIndex]); + if (column == null) return; + column.widgets = widgets; + columns[columnIndex] = column; + store.set('deck.columns', columns); + saveDeck(); +} + +export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) { + const columns = deepClone(store.state['deck.columns']); + const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id); + const column = deepClone(store.state['deck.columns'][columnIndex]); + if (column == null) return; + if (column.widgets == null) column.widgets = []; + column.widgets = column.widgets.map(w => w.id === widgetId ? { + ...w, + data: widgetData, + } : w); + columns[columnIndex] = column; + store.set('deck.columns', columns); + saveDeck(); +} + +export function updateColumn(id: Column['id'], column: Partial) { + const columns = deepClone(store.state['deck.columns']); + const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id); + const currentColumn = deepClone(store.state['deck.columns'][columnIndex]); + if (currentColumn == null) return; + for (const [k, v] of Object.entries(column)) { + currentColumn[k] = v; + } + columns[columnIndex] = currentColumn; + store.set('deck.columns', columns); + saveDeck(); +} -- cgit v1.2.3-freya