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.ts362
1 files changed, 362 insertions, 0 deletions
diff --git a/packages/frontend/src/deck.ts b/packages/frontend/src/deck.ts
new file mode 100644
index 0000000000..cd6fc9bb5d
--- /dev/null
+++ b/packages/frontend/src/deck.ts
@@ -0,0 +1,362 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { notificationTypes } from 'misskey-js';
+import { ref, computed } from 'vue';
+import type { Ref } from 'vue';
+import { v4 as uuid } from 'uuid';
+import { i18n } from './i18n.js';
+import type { BasicTimelineType } from '@/timelines.js';
+import type { SoundStore } from '@/preferences/def.js';
+import type { MenuItem } from '@/types/menu.js';
+import { deepClone } from '@/utility/clone.js';
+import { prefer } from '@/preferences.js';
+import * as os from '@/os.js';
+
+export type DeckProfile = {
+ name: string;
+ id: string;
+ columns: Column[];
+ layout: Column['id'][][];
+};
+
+type ColumnWidget = {
+ name: string;
+ id: string;
+ data: Record<string, any>;
+};
+
+export const columnTypes = [
+ 'main',
+ 'widgets',
+ 'notifications',
+ 'tl',
+ 'antenna',
+ 'list',
+ 'channel',
+ 'mentions',
+ 'direct',
+ 'roleTimeline',
+ 'chat',
+ 'following',
+] 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;
+};
+
+const _currentProfile = prefer.s['deck.profiles'].find(p => p.name === prefer.s['deck.profile']);
+const __currentProfile = _currentProfile ? deepClone(_currentProfile) : null;
+export const columns = ref(__currentProfile ? __currentProfile.columns : []);
+export const layout = ref(__currentProfile ? __currentProfile.layout : []);
+
+if (prefer.s['deck.profile'] == null) {
+ addProfile('Main');
+}
+
+export function forceSaveCurrentDeckProfile() {
+ const currentProfile = prefer.s['deck.profiles'].find(p => p.name === prefer.s['deck.profile']);
+ if (currentProfile == null) return;
+
+ const newProfile = deepClone(currentProfile);
+ newProfile.columns = columns.value;
+ newProfile.layout = layout.value;
+
+ const newProfiles = prefer.s['deck.profiles'].filter(p => p.name !== prefer.s['deck.profile']);
+ newProfiles.push(newProfile);
+ prefer.commit('deck.profiles', newProfiles);
+}
+
+export const saveCurrentDeckProfile = () => {
+ forceSaveCurrentDeckProfile();
+};
+
+function switchProfile(profile: DeckProfile) {
+ prefer.commit('deck.profile', profile.name);
+ const currentProfile = deepClone(profile);
+ columns.value = currentProfile.columns;
+ layout.value = currentProfile.layout;
+ forceSaveCurrentDeckProfile();
+}
+
+function addProfile(name: string) {
+ if (name.trim() === '') return;
+ if (prefer.s['deck.profiles'].find(p => p.name === name)) return;
+
+ const newProfile: DeckProfile = {
+ id: uuid(),
+ name,
+ columns: [],
+ layout: [],
+ };
+ prefer.commit('deck.profiles', [...prefer.s['deck.profiles'], newProfile]);
+ switchProfile(newProfile);
+}
+
+function createFirstProfile() {
+ addProfile('Main');
+}
+
+export function deleteProfile(name: string): void {
+ const newProfiles = prefer.s['deck.profiles'].filter(p => p.name !== name);
+ prefer.commit('deck.profiles', newProfiles);
+
+ if (prefer.s['deck.profiles'].length === 0) {
+ createFirstProfile();
+ } else {
+ switchProfile(prefer.s['deck.profiles'][0]);
+ }
+}
+
+export function addColumn(column: Column) {
+ if (column.name === undefined) column.name = null;
+ columns.value.push(column);
+ layout.value.push([column.id]);
+ saveCurrentDeckProfile();
+}
+
+export function removeColumn(id: Column['id']) {
+ columns.value = columns.value.filter(c => c.id !== id);
+ layout.value = layout.value.map(ids => ids.filter(_id => _id !== id)).filter(ids => ids.length > 0);
+ saveCurrentDeckProfile();
+}
+
+export function swapColumn(a: Column['id'], b: Column['id']) {
+ const aX = layout.value.findIndex(ids => ids.indexOf(a) !== -1);
+ const aY = layout.value[aX].findIndex(id => id === a);
+ const bX = layout.value.findIndex(ids => ids.indexOf(b) !== -1);
+ const bY = layout.value[bX].findIndex(id => id === b);
+ const newLayout = deepClone(layout.value);
+ newLayout[aX][aY] = b;
+ newLayout[bX][bY] = a;
+ layout.value = newLayout;
+ saveCurrentDeckProfile();
+}
+
+export function swapLeftColumn(id: Column['id']) {
+ const newLayout = deepClone(layout.value);
+ layout.value.some((ids, i) => {
+ if (ids.includes(id)) {
+ const left = layout.value[i - 1];
+ if (left) {
+ newLayout[i - 1] = layout.value[i];
+ newLayout[i] = left;
+ layout.value = newLayout;
+ }
+ return true;
+ }
+ return false;
+ });
+ saveCurrentDeckProfile();
+}
+
+export function swapRightColumn(id: Column['id']) {
+ const newLayout = deepClone(layout.value);
+ layout.value.some((ids, i) => {
+ if (ids.includes(id)) {
+ const right = layout.value[i + 1];
+ if (right) {
+ newLayout[i + 1] = layout.value[i];
+ newLayout[i] = right;
+ layout.value = newLayout;
+ }
+ return true;
+ }
+ return false;
+ });
+ saveCurrentDeckProfile();
+}
+
+export function swapUpColumn(id: Column['id']) {
+ const newLayout = deepClone(layout.value);
+ const idsIndex = layout.value.findIndex(ids => ids.includes(id));
+ const ids = deepClone(layout.value[idsIndex]);
+ ids.some((x, i) => {
+ if (x === id) {
+ const up = ids[i - 1];
+ if (up) {
+ ids[i - 1] = id;
+ ids[i] = up;
+
+ newLayout[idsIndex] = ids;
+ layout.value = newLayout;
+ }
+ return true;
+ }
+ return false;
+ });
+ saveCurrentDeckProfile();
+}
+
+export function swapDownColumn(id: Column['id']) {
+ const newLayout = deepClone(layout.value);
+ const idsIndex = layout.value.findIndex(ids => ids.includes(id));
+ const ids = deepClone(layout.value[idsIndex]);
+ ids.some((x, i) => {
+ if (x === id) {
+ const down = ids[i + 1];
+ if (down) {
+ ids[i + 1] = id;
+ ids[i] = down;
+
+ newLayout[idsIndex] = ids;
+ layout.value = newLayout;
+ }
+ return true;
+ }
+ return false;
+ });
+ saveCurrentDeckProfile();
+}
+
+export function stackLeftColumn(id: Column['id']) {
+ let newLayout = deepClone(layout.value);
+ const i = layout.value.findIndex(ids => ids.includes(id));
+ newLayout = newLayout.map(ids => ids.filter(_id => _id !== id));
+ newLayout[i - 1].push(id);
+ newLayout = newLayout.filter(ids => ids.length > 0);
+ layout.value = newLayout;
+ saveCurrentDeckProfile();
+}
+
+export function popRightColumn(id: Column['id']) {
+ let newLayout = deepClone(layout.value);
+ const i = layout.value.findIndex(ids => ids.includes(id));
+ const affected = newLayout[i];
+ newLayout = newLayout.map(ids => ids.filter(_id => _id !== id));
+ newLayout.splice(i + 1, 0, [id]);
+ newLayout = newLayout.filter(ids => ids.length > 0);
+ layout.value = newLayout;
+
+ const newColumns = deepClone(columns.value);
+ for (const column of newColumns) {
+ if (affected.includes(column.id)) {
+ column.active = true;
+ }
+ }
+ columns.value = newColumns;
+
+ saveCurrentDeckProfile();
+}
+
+export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
+ const newColumns = deepClone(columns.value);
+ const columnIndex = columns.value.findIndex(c => c.id === id);
+ const column = deepClone(columns.value[columnIndex]);
+ if (column == null) return;
+ if (column.widgets == null) column.widgets = [];
+ column.widgets.unshift(widget);
+ newColumns[columnIndex] = column;
+ columns.value = newColumns;
+ saveCurrentDeckProfile();
+}
+
+export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
+ const newColumns = deepClone(columns.value);
+ const columnIndex = columns.value.findIndex(c => c.id === id);
+ const column = deepClone(columns.value[columnIndex]);
+ if (column == null) return;
+ if (column.widgets == null) column.widgets = [];
+ column.widgets = column.widgets.filter(w => w.id !== widget.id);
+ newColumns[columnIndex] = column;
+ columns.value = newColumns;
+ saveCurrentDeckProfile();
+}
+
+export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
+ const newColumns = deepClone(columns.value);
+ const columnIndex = columns.value.findIndex(c => c.id === id);
+ const column = deepClone(columns.value[columnIndex]);
+ if (column == null) return;
+ column.widgets = widgets;
+ newColumns[columnIndex] = column;
+ columns.value = newColumns;
+ saveCurrentDeckProfile();
+}
+
+export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
+ const newColumns = deepClone(columns.value);
+ const columnIndex = columns.value.findIndex(c => c.id === id);
+ const column = deepClone(columns.value[columnIndex]);
+ if (column == null) return;
+ if (column.widgets == null) column.widgets = [];
+ column.widgets = column.widgets.map(w => w.id === widgetId ? {
+ ...w,
+ data: widgetData,
+ } : w);
+ newColumns[columnIndex] = column;
+ columns.value = newColumns;
+ saveCurrentDeckProfile();
+}
+
+export function updateColumn(id: Column['id'], column: Partial<Column>) {
+ const newColumns = deepClone(columns.value);
+ const columnIndex = columns.value.findIndex(c => c.id === id);
+ const currentColumn = deepClone(columns.value[columnIndex]);
+ if (currentColumn == null) return;
+ for (const [k, v] of Object.entries(column)) {
+ currentColumn[k] = v;
+ }
+ newColumns[columnIndex] = currentColumn;
+ columns.value = newColumns;
+ saveCurrentDeckProfile();
+}
+
+export function getColumn<TColumn extends Column>(id: Column['id']): Ref<TColumn> {
+ return computed(() => {
+ return columns.value.find(c => c.id === id) as TColumn;
+ });
+}
+
+export function switchProfileMenu(ev: MouseEvent) {
+ const items: MenuItem[] = prefer.s['deck.profile'] ? [{
+ text: prefer.s['deck.profile'],
+ active: true,
+ action: () => {},
+ }] : [];
+
+ const profiles = prefer.s['deck.profiles'];
+
+ items.push(...(profiles.filter(p => p.name !== prefer.s['deck.profile']).map(p => ({
+ text: p.name,
+ action: () => {
+ switchProfile(p);
+ },
+ }))), { type: 'divider' as const }, {
+ text: i18n.ts._deck.newProfile,
+ icon: 'ti ti-plus',
+ action: async () => {
+ const { canceled, result: name } = await os.inputText({
+ title: i18n.ts._deck.profile,
+ minLength: 1,
+ });
+
+ if (canceled || name == null || name.trim() === '') return;
+
+ addProfile(name);
+ },
+ });
+
+ os.popupMenu(items, ev.currentTarget ?? ev.target);
+}