diff --git a/CHANGELOG.md b/CHANGELOG.md index e0b47ff5e8..748f3aa8eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - Feat: 設定の管理が強化されました - 自動でバックアップされるように - Enhance: プラグインの管理が強化されました +- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに +- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように +- Enhance: テーマ設定画面のデザインを改善 - Fix: テーマ切り替え時に一部の色が変わらない問題を修正 ### Server diff --git a/assets/about/drive.png b/assets/about/drive.png deleted file mode 100644 index 16037aae39..0000000000 Binary files a/assets/about/drive.png and /dev/null differ diff --git a/assets/about/post.png b/assets/about/post.png deleted file mode 100644 index 3c55f66c56..0000000000 Binary files a/assets/about/post.png and /dev/null differ diff --git a/assets/about/reaction.png b/assets/about/reaction.png deleted file mode 100644 index e4e7e06bc0..0000000000 Binary files a/assets/about/reaction.png and /dev/null differ diff --git a/assets/about/ui.png b/assets/about/ui.png deleted file mode 100644 index 0601837f4c..0000000000 Binary files a/assets/about/ui.png and /dev/null differ diff --git a/assets/ss/explore.jpg b/assets/ss/explore.jpg deleted file mode 100644 index bf81d794c3..0000000000 Binary files a/assets/ss/explore.jpg and /dev/null differ diff --git a/assets/ss/user.jpg b/assets/ss/user.jpg deleted file mode 100644 index 3ec595c199..0000000000 Binary files a/assets/ss/user.jpg and /dev/null differ diff --git a/locales/index.d.ts b/locales/index.d.ts index 0cdd428c82..409ad3835b 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5310,6 +5310,72 @@ export interface Locale extends ILocale { * 復元 */ "restore": string; + "_settings": { + /** + * ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。 + */ + "driveBanner": string; + /** + * プラグインを利用するとクライアントの機能を拡張することができます。プラグインのインストール、個別の設定と管理が行えます。 + */ + "pluginBanner": string; + /** + * サーバーからの受信する通知の種類と範囲や、プッシュ通知の設定が行えます。 + */ + "notificationsBanner": string; + /** + * API + */ + "api": string; + /** + * Webhook + */ + "webhook": string; + /** + * サービス連携 + */ + "serviceConnection": string; + /** + * 外部のアプリ・サービスと連携するためのアクセストークンやWebhookの管理と設定が行えます。 + */ + "serviceConnectionBanner": string; + /** + * アカウントのデータ + */ + "accountData": string; + /** + * アカウントのデータをエクスポート/インポートして管理できます。 + */ + "accountDataBanner": string; + /** + * 非表示にするコンテンツの設定や、特定のユーザーからのアクションを制限する設定と管理を行えます。 + */ + "muteAndBlockBanner": string; + /** + * クライアントの視覚や動作に関するパーソナライズを行い、より最適に使用できるように設定できます。 + */ + "accessibilityBanner": string; + /** + * コンテンツの公開範囲、見つけやすさ、フォローの承認制などアカウントのプライバシーに関する設定を行えます。 + */ + "privacyBanner": string; + /** + * パスワード、ログイン方法、認証アプリ、パスキーなどアカウントのセキュリティに関する設定を行えます。 + */ + "securityBanner": string; + /** + * 好みに応じた、クライアントの全体的な動作の設定が行えます。 + */ + "preferencesBanner": string; + /** + * 好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。 + */ + "appearanceBanner": string; + /** + * クライアントで再生するサウンドの設定が行えます。 + */ + "soundsBanner": string; + }; "_preferencesProfile": { /** * プロファイル名 @@ -7746,6 +7812,10 @@ export interface Locale extends ILocale { * 標準のテーマ */ "builtinThemes": string; + /** + * サーバーのテーマ + */ + "instanceTheme": string; /** * そのテーマは既にインストールされています */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1e41c43864..5d24282849 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1324,6 +1324,24 @@ noName: "名前はありません" skip: "スキップ" restore: "復元" +_settings: + driveBanner: "ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。" + pluginBanner: "プラグインを利用するとクライアントの機能を拡張することができます。プラグインのインストール、個別の設定と管理が行えます。" + notificationsBanner: "サーバーからの受信する通知の種類と範囲や、プッシュ通知の設定が行えます。" + api: "API" + webhook: "Webhook" + serviceConnection: "サービス連携" + serviceConnectionBanner: "外部のアプリ・サービスと連携するためのアクセストークンやWebhookの管理と設定が行えます。" + accountData: "アカウントのデータ" + accountDataBanner: "アカウントのデータをエクスポート/インポートして管理できます。" + muteAndBlockBanner: "非表示にするコンテンツの設定や、特定のユーザーからのアクションを制限する設定と管理を行えます。" + accessibilityBanner: "クライアントの視覚や動作に関するパーソナライズを行い、より最適に使用できるように設定できます。" + privacyBanner: "コンテンツの公開範囲、見つけやすさ、フォローの承認制などアカウントのプライバシーに関する設定を行えます。" + securityBanner: "パスワード、ログイン方法、認証アプリ、パスキーなどアカウントのセキュリティに関する設定を行えます。" + preferencesBanner: "好みに応じた、クライアントの全体的な動作の設定が行えます。" + appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。" + soundsBanner: "クライアントで再生するサウンドの設定が行えます。" + _preferencesProfile: profileName: "プロファイル名" profileNameDescription: "このデバイスを識別する名前を設定してください。" @@ -2031,6 +2049,7 @@ _theme: installed: "{name}をインストールしました" installedThemes: "インストールされたテーマ" builtinThemes: "標準のテーマ" + instanceTheme: "サーバーのテーマ" alreadyInstalled: "そのテーマは既にインストールされています" invalid: "テーマの形式が間違っています" make: "テーマを作る" diff --git a/package.json b/package.json index ff891ab9a4..d6324ca38c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2025.3.2-alpha.4", + "version": "2025.3.2-alpha.6", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/frontend/assets/bell_3d.png b/packages/frontend/assets/bell_3d.png new file mode 100644 index 0000000000..2598cdd82b Binary files /dev/null and b/packages/frontend/assets/bell_3d.png differ diff --git a/packages/frontend/assets/cloud_3d.png b/packages/frontend/assets/cloud_3d.png new file mode 100644 index 0000000000..a3a1de12dd Binary files /dev/null and b/packages/frontend/assets/cloud_3d.png differ diff --git a/packages/frontend/assets/desktop_computer_3d.png b/packages/frontend/assets/desktop_computer_3d.png new file mode 100644 index 0000000000..85e92a02c0 Binary files /dev/null and b/packages/frontend/assets/desktop_computer_3d.png differ diff --git a/packages/frontend/assets/electric_plug_3d.png b/packages/frontend/assets/electric_plug_3d.png new file mode 100644 index 0000000000..431ef68c85 Binary files /dev/null and b/packages/frontend/assets/electric_plug_3d.png differ diff --git a/packages/frontend/assets/gear_3d.png b/packages/frontend/assets/gear_3d.png new file mode 100644 index 0000000000..050340b76c Binary files /dev/null and b/packages/frontend/assets/gear_3d.png differ diff --git a/packages/frontend/assets/link_3d.png b/packages/frontend/assets/link_3d.png new file mode 100644 index 0000000000..b1cb23080a Binary files /dev/null and b/packages/frontend/assets/link_3d.png differ diff --git a/packages/frontend/assets/locked_with_key_3d.png b/packages/frontend/assets/locked_with_key_3d.png new file mode 100644 index 0000000000..aae99b982d Binary files /dev/null and b/packages/frontend/assets/locked_with_key_3d.png differ diff --git a/packages/frontend/assets/mens_room_3d.png b/packages/frontend/assets/mens_room_3d.png new file mode 100644 index 0000000000..8b85ca8782 Binary files /dev/null and b/packages/frontend/assets/mens_room_3d.png differ diff --git a/packages/frontend/assets/musical_note_3d.png b/packages/frontend/assets/musical_note_3d.png new file mode 100644 index 0000000000..0b520311f6 Binary files /dev/null and b/packages/frontend/assets/musical_note_3d.png differ diff --git a/packages/frontend/assets/package_3d.png b/packages/frontend/assets/package_3d.png new file mode 100644 index 0000000000..582134fd2f Binary files /dev/null and b/packages/frontend/assets/package_3d.png differ diff --git a/packages/frontend/assets/prohibited_3d.png b/packages/frontend/assets/prohibited_3d.png new file mode 100644 index 0000000000..1f071edd06 Binary files /dev/null and b/packages/frontend/assets/prohibited_3d.png differ diff --git a/packages/frontend/assets/speaker_high_volume_3d.png b/packages/frontend/assets/speaker_high_volume_3d.png new file mode 100644 index 0000000000..b25aaa91d6 Binary files /dev/null and b/packages/frontend/assets/speaker_high_volume_3d.png differ diff --git a/packages/frontend/assets/unlocked_3d.png b/packages/frontend/assets/unlocked_3d.png new file mode 100644 index 0000000000..c6ff7a0dc2 Binary files /dev/null and b/packages/frontend/assets/unlocked_3d.png differ diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 5e2a1f45ac..c90d4da5ec 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -17,6 +17,7 @@ import { misskeyApi } from '@/utility/misskey-api.js'; import { unisonReload, reloadChannel } from '@/utility/unison-reload.js'; // TODO: 他のタブと永続化されたstateを同期 +// TODO: accountsはpreferences管理にする(tokenは別管理) type Account = Misskey.entities.MeDetailed & { token: string }; diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index bf3ab12682..86efd48c4e 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -6,9 +6,11 @@ import { createApp, defineAsyncComponent, markRaw } from 'vue'; import { ui } from '@@/js/config.js'; import * as Misskey from 'misskey-js'; +import { v4 as uuid } from 'uuid'; import { common } from './common.js'; import type { Component } from 'vue'; import type { Keymap } from '@/utility/hotkey.js'; +import type { DeckProfile } from '@/deck.js'; import { i18n } from '@/i18n.js'; import { alert, confirm, popup, post, toast } from '@/os.js'; import { useStream } from '@/stream.js'; @@ -143,12 +145,34 @@ export async function mainBoot() { if (themes.length > 0) { prefer.commit('themes', themes); } + const plugins = ColdDeviceStorage.get('plugins'); prefer.commit('plugins', plugins.map(p => ({ ...p, installId: (p as any).id, id: undefined, }))); + + prefer.commit('deck.profile', deckStore.s.profile); + misskeyApi('i/registry/keys', { + scope: ['client', 'deck', 'profiles'], + }).then(async keys => { + const profiles: DeckProfile[] = []; + for (const key of keys) { + const deck = await misskeyApi('i/registry/get', { + scope: ['client', 'deck', 'profiles'], + key: key, + }); + profiles.push({ + id: uuid(), + name: key, + columns: deck.columns, + layout: deck.layout, + }); + } + prefer.commit('deck.profiles', profiles); + }); + prefer.commit('lightTheme', ColdDeviceStorage.get('lightTheme')); prefer.commit('darkTheme', ColdDeviceStorage.get('darkTheme')); prefer.commit('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode')); @@ -223,9 +247,6 @@ export async function mainBoot() { prefer.commit('sound.on.noteMy', store.s.sound_noteMy as any); prefer.commit('sound.on.notification', store.s.sound_notification as any); prefer.commit('sound.on.reaction', store.s.sound_reaction as any); - store.set('deck.profile', deckStore.s.profile); - store.set('deck.columns', deckStore.s.columns); - store.set('deck.layout', deckStore.s.layout); store.set('menu', []); } diff --git a/packages/frontend/src/components/MkFeatureBanner.vue b/packages/frontend/src/components/MkFeatureBanner.vue new file mode 100644 index 0000000000..e990ffc8f0 --- /dev/null +++ b/packages/frontend/src/components/MkFeatureBanner.vue @@ -0,0 +1,43 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue index e749725fea..4d1787d420 100644 --- a/packages/frontend/src/components/MkPasswordDialog.vue +++ b/packages/frontend/src/components/MkPasswordDialog.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- 🔐 + 🔐
{{ i18n.ts.authenticationRequiredToContinue }}
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 6a72663157..5e379d08b7 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -265,7 +265,13 @@ const canPost = computed((): boolean => { quoteId.value != null ) && (textLength.value <= maxTextLength.value) && - (cwTextLength.value <= maxCwTextLength) && + ( + useCw.value ? + ( + cw.value != null && cw.value.trim() !== '' && + cwTextLength.value <= maxCwTextLength + ) : true + ) && (files.value.length <= 16) && (!poll.value || poll.value.choices.length >= 2); }); @@ -744,14 +750,6 @@ function isAnnoying(text: string): boolean { } async function post(ev?: MouseEvent) { - if (useCw.value && (cw.value == null || cw.value.trim() === '')) { - os.alert({ - type: 'error', - text: i18n.ts.cwNotationRequired, - }); - return; - } - if (ev) { const el = (ev.currentTarget ?? ev.target) as HTMLElement | null; diff --git a/packages/frontend/src/components/MkThemePreview.vue b/packages/frontend/src/components/MkThemePreview.vue new file mode 100644 index 0000000000..5b180b3680 --- /dev/null +++ b/packages/frontend/src/components/MkThemePreview.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/packages/frontend/src/deck.ts b/packages/frontend/src/deck.ts index f158db2f85..9df56c52df 100644 --- a/packages/frontend/src/deck.ts +++ b/packages/frontend/src/deck.ts @@ -3,13 +3,23 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { throttle } from 'throttle-debounce'; import { notificationTypes } from 'misskey-js'; +import { 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 { misskeyApi } from '@/utility/misskey-api.js'; +import type { MenuItem } from '@/types/menu.js'; import { deepClone } from '@/utility/clone.js'; -import { store } from '@/store.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; @@ -53,127 +63,132 @@ export type Column = { soundSetting?: SoundStore; }; -export const loadDeck = async () => { - let deck; +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 : []); - try { - deck = await misskeyApi('i/registry/get', { - scope: ['client', 'deck', 'profiles'], - key: store.s['deck.profile'], - }); - } catch (err) { - if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') { - // 後方互換性のため - if (store.s['deck.profile'] === 'default') { - saveDeck(); - return; - } +if (prefer.s['deck.profile'] == null) { + addProfile('Main'); +} - store.set('deck.columns', []); - store.set('deck.layout', []); - return; - } - throw err; - } +export function forceSaveCurrentDeckProfile() { + const currentProfile = prefer.s['deck.profiles'].find(p => p.name === prefer.s['deck.profile']); + if (currentProfile == null) return; - store.set('deck.columns', deck.columns); - store.set('deck.layout', deck.layout); + 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(); }; -export async function forceSaveDeck() { - await misskeyApi('i/registry/set', { - scope: ['client', 'deck', 'profiles'], - key: store.s['deck.profile'], - value: { - columns: store.r['deck.columns'].value, - layout: store.r['deck.layout'].value, - }, - }); +function switchProfile(profile: DeckProfile) { + prefer.commit('deck.profile', profile.name); + const currentProfile = deepClone(profile); + columns.value = currentProfile.columns; + layout.value = currentProfile.layout; + forceSaveCurrentDeckProfile(); } -// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する -export const saveDeck = throttle(1000, () => { - forceSaveDeck(); -}); +function addProfile(name: string) { + if (name.trim() === '') return; + if (prefer.s['deck.profiles'].find(p => p.name === name)) return; -export async function getProfiles(): Promise { - return await misskeyApi('i/registry/keys', { - scope: ['client', 'deck', 'profiles'], - }); + const newProfile: DeckProfile = { + id: uuid(), + name, + columns: [], + layout: [], + }; + prefer.commit('deck.profiles', [...prefer.s['deck.profiles'], newProfile]); + switchProfile(newProfile); } -export async function deleteProfile(key: string): Promise { - return await misskeyApi('i/registry/remove', { - scope: ['client', 'deck', 'profiles'], - key: key, - }); +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; - store.push('deck.columns', column); - store.push('deck.layout', [column.id]); - saveDeck(); + columns.value.push(column); + layout.value.push([column.id]); + saveCurrentDeckProfile(); } export function removeColumn(id: Column['id']) { - store.set('deck.columns', store.s['deck.columns'].filter(c => c.id !== id)); - store.set('deck.layout', store.s['deck.layout'] - .map(ids => ids.filter(_id => _id !== id)) - .filter(ids => ids.length > 0)); - saveDeck(); + 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 = store.s['deck.layout'].findIndex(ids => ids.indexOf(a) !== -1); - const aY = store.s['deck.layout'][aX].findIndex(id => id === a); - const bX = store.s['deck.layout'].findIndex(ids => ids.indexOf(b) !== -1); - const bY = store.s['deck.layout'][bX].findIndex(id => id === b); - const layout = deepClone(store.s['deck.layout']); - layout[aX][aY] = b; - layout[bX][bY] = a; - store.set('deck.layout', layout); - saveDeck(); + 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 layout = deepClone(store.s['deck.layout']); - store.s['deck.layout'].some((ids, i) => { + const newLayout = deepClone(layout.value); + layout.value.some((ids, i) => { if (ids.includes(id)) { - const left = store.s['deck.layout'][i - 1]; + const left = layout.value[i - 1]; if (left) { - layout[i - 1] = store.s['deck.layout'][i]; - layout[i] = left; - store.set('deck.layout', layout); + newLayout[i - 1] = layout.value[i]; + newLayout[i] = left; + layout.value = newLayout; } return true; } return false; }); - saveDeck(); + saveCurrentDeckProfile(); } export function swapRightColumn(id: Column['id']) { - const layout = deepClone(store.s['deck.layout']); - store.s['deck.layout'].some((ids, i) => { + const newLayout = deepClone(layout.value); + layout.value.some((ids, i) => { if (ids.includes(id)) { - const right = store.s['deck.layout'][i + 1]; + const right = layout.value[i + 1]; if (right) { - layout[i + 1] = store.s['deck.layout'][i]; - layout[i] = right; - store.set('deck.layout', layout); + newLayout[i + 1] = layout.value[i]; + newLayout[i] = right; + layout.value = newLayout; } return true; } return false; }); - saveDeck(); + saveCurrentDeckProfile(); } export function swapUpColumn(id: Column['id']) { - const layout = deepClone(store.s['deck.layout']); - const idsIndex = store.s['deck.layout'].findIndex(ids => ids.includes(id)); - const ids = deepClone(store.s['deck.layout'][idsIndex]); + 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]; @@ -181,20 +196,20 @@ export function swapUpColumn(id: Column['id']) { ids[i - 1] = id; ids[i] = up; - layout[idsIndex] = ids; - store.set('deck.layout', layout); + newLayout[idsIndex] = ids; + layout.value = newLayout; } return true; } return false; }); - saveDeck(); + saveCurrentDeckProfile(); } export function swapDownColumn(id: Column['id']) { - const layout = deepClone(store.s['deck.layout']); - const idsIndex = store.s['deck.layout'].findIndex(ids => ids.includes(id)); - const ids = deepClone(store.s['deck.layout'][idsIndex]); + 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]; @@ -202,105 +217,137 @@ export function swapDownColumn(id: Column['id']) { ids[i + 1] = id; ids[i] = down; - layout[idsIndex] = ids; - store.set('deck.layout', layout); + newLayout[idsIndex] = ids; + layout.value = newLayout; } return true; } return false; }); - saveDeck(); + saveCurrentDeckProfile(); } export function stackLeftColumn(id: Column['id']) { - let layout = deepClone(store.s['deck.layout']); - const i = store.s['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(); + 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 layout = deepClone(store.s['deck.layout']); - const i = store.s['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); + 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 columns = deepClone(store.s['deck.columns']); - for (const column of columns) { + const newColumns = deepClone(columns.value); + for (const column of newColumns) { if (affected.includes(column.id)) { column.active = true; } } - store.set('deck.columns', columns); + columns.value = newColumns; - saveDeck(); + saveCurrentDeckProfile(); } export function addColumnWidget(id: Column['id'], widget: ColumnWidget) { - const columns = deepClone(store.s['deck.columns']); - const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id); - const column = deepClone(store.s['deck.columns'][columnIndex]); + 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); - columns[columnIndex] = column; - store.set('deck.columns', columns); - saveDeck(); + newColumns[columnIndex] = column; + columns.value = newColumns; + saveCurrentDeckProfile(); } export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) { - const columns = deepClone(store.s['deck.columns']); - const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id); - const column = deepClone(store.s['deck.columns'][columnIndex]); + 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); - columns[columnIndex] = column; - store.set('deck.columns', columns); - saveDeck(); + newColumns[columnIndex] = column; + columns.value = newColumns; + saveCurrentDeckProfile(); } export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) { - const columns = deepClone(store.s['deck.columns']); - const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id); - const column = deepClone(store.s['deck.columns'][columnIndex]); + 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; - columns[columnIndex] = column; - store.set('deck.columns', columns); - saveDeck(); + newColumns[columnIndex] = column; + columns.value = newColumns; + saveCurrentDeckProfile(); } export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) { - const columns = deepClone(store.s['deck.columns']); - const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id); - const column = deepClone(store.s['deck.columns'][columnIndex]); + 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); - columns[columnIndex] = column; - store.set('deck.columns', columns); - saveDeck(); + newColumns[columnIndex] = column; + columns.value = newColumns; + saveCurrentDeckProfile(); } export function updateColumn(id: Column['id'], column: Partial) { - const columns = deepClone(store.s['deck.columns']); - const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id); - const currentColumn = deepClone(store.s['deck.columns'][columnIndex]); + 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; } - columns[columnIndex] = currentColumn; - store.set('deck.columns', columns); - saveDeck(); + newColumns[columnIndex] = currentColumn; + columns.value = newColumns; + saveCurrentDeckProfile(); +} + +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); } diff --git a/packages/frontend/src/pages/settings/accessibility.vue b/packages/frontend/src/pages/settings/accessibility.vue index f22e45ce1f..3dbb039a17 100644 --- a/packages/frontend/src/pages/settings/accessibility.vue +++ b/packages/frontend/src/pages/settings/accessibility.vue @@ -6,6 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only