summaryrefslogtreecommitdiff
path: root/packages/frontend/src/deck.ts
diff options
context:
space:
mode:
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();
+}