summaryrefslogtreecommitdiff
path: root/packages/frontend/src/deck.ts
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-03-09 12:34:08 +0900
committerGitHub <noreply@github.com>2025-03-09 12:34:08 +0900
commitd30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e (patch)
treec0c87a30037d3ffc11784627e67a1965b262c336 /packages/frontend/src/deck.ts
parent[skip ci] Update CHANGELOG.md (prepend template) (diff)
downloadsharkey-d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e.tar.gz
sharkey-d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e.tar.bz2
sharkey-d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e.zip
Refine preferences (#15597)
* 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 <dev@taichan.site> Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com> Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
Diffstat (limited to 'packages/frontend/src/deck.ts')
-rw-r--r--packages/frontend/src/deck.ts306
1 files changed, 306 insertions, 0 deletions
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<string, any>;
+};
+
+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<string[]> {
+ return await misskeyApi('i/registry/keys', {
+ scope: ['client', 'deck', 'profiles'],
+ });
+}
+
+export async function deleteProfile(key: string): Promise<void> {
+ 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<Column>) {
+ 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();
+}