diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-03-09 12:34:08 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-03-09 12:34:08 +0900 |
| commit | d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e (patch) | |
| tree | c0c87a30037d3ffc11784627e67a1965b262c336 /packages | |
| parent | [skip ci] Update CHANGELOG.md (prepend template) (diff) | |
| download | sharkey-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')
178 files changed, 3328 insertions, 2458 deletions
diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts index d000a28232..00639be642 100644 --- a/packages/frontend/.storybook/preview.ts +++ b/packages/frontend/.storybook/preview.ts @@ -68,9 +68,9 @@ queueMicrotask(() => { import('../src/directives'), import('../src/widgets'), import('../src/scripts/theme'), - import('../src/store'), + import('../src/preferences'), import('../src/os'), - ]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { defaultStore }, os]) => { + ]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { prefer }, os]) => { setup((app) => { moduleInitialized = true; if (app[appInitialized]) { @@ -83,7 +83,7 @@ queueMicrotask(() => { widgets(app); misskeyOS = os; if (isChromatic()) { - defaultStore.set('animation', false); + prefer.set('animation', false); } }); }); @@ -104,9 +104,9 @@ const preview = { } }).catch(() => {}) : Promise.resolve(); - const resetDefaultStorePromise = import('../src/store').then(({ defaultStore }) => { + const resetDefaultStorePromise = import('../src/store').then(({ store }) => { // @ts-expect-error - defaultStore.init(); + store.init(); }).catch(() => {}); Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => { initLocalStorage(); diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts index 5d8cf05fff..884ae3afac 100644 --- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts @@ -58,7 +58,7 @@ describe(normalizeClass.name, () => { it('Composition API (standard)', () => { const ast = parse(` -import { c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js'; +import { c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js'; import { M as MkContainer } from './MkContainer-!~{03M}~.js'; import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js'; import './photoswipe-!~{003}~.js'; @@ -74,7 +74,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({ let fetching = ref(true); let images = ref([]); function thumbnail(image) { - return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl; + return store.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl; } onMounted(() => { const image = [ @@ -173,7 +173,7 @@ export { index_photos as default }; `.slice(1), { ecmaVersion: 'latest', sourceType: 'module' }); unwindCssModuleClassName(ast); expect(generate(ast)).toBe(` -import {c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js'; +import {c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js'; import {M as MkContainer} from './MkContainer-!~{03M}~.js'; import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js'; import './photoswipe-!~{003}~.js'; @@ -190,7 +190,7 @@ const index_photos = defineComponent({ let fetching = ref(true); let images = ref([]); function thumbnail(image) { - return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl; + return store.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl; } onMounted(() => { const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"]; @@ -268,7 +268,7 @@ export {index_photos as default}; it('Composition API (with `useCssModule()`)', () => { const ast = parse(` import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js'; -import { d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js'; +import { d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js'; function isDebuggerEnabled(id) { try { @@ -393,7 +393,7 @@ const _sfc_main = defineComponent({ el.style.left = ""; } return () => h( - defaultStore.state.animation ? TransitionGroup : "div", + prefer.s.animation ? TransitionGroup : "div", { class: { [$style["date-separated-list"]]: true, @@ -402,7 +402,7 @@ const _sfc_main = defineComponent({ [$style["direction-down"]]: props.direction === "down", [$style["direction-up"]]: props.direction === "up" }, - ...defaultStore.state.animation ? { + ...prefer.s.animation ? { name: "list", tag: "div", onBeforeLeave, @@ -441,7 +441,7 @@ export { MkDateSeparatedList as M }; unwindCssModuleClassName(ast); expect(generate(ast)).toBe(` import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js'; -import {d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js'; +import {d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js'; function isDebuggerEnabled(id) { try { return localStorage.getItem(\`DEBUG_\${id}\`) !== null; @@ -555,7 +555,7 @@ const _sfc_main = defineComponent({ el.style.top = ""; el.style.left = ""; } - return () => h(defaultStore.state.animation ? TransitionGroup : "div", { + return () => h(prefer.s.animation ? TransitionGroup : "div", { class: { [$style["date-separated-list"]]: true, [$style["date-separated-list-nogap"]]: props.noGap, @@ -563,7 +563,7 @@ const _sfc_main = defineComponent({ [$style["direction-down"]]: props.direction === "down", [$style["direction-up"]]: props.direction === "up" }, - ...defaultStore.state.animation ? { + ...prefer.s.animation ? { name: "list", tag: "div", onBeforeLeave, diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index d09b98efe0..7b35d12a80 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -6,6 +6,8 @@ import { computed, watch, version as vueVersion } from 'vue'; import { compareVersions } from 'compare-versions'; import { version, lang, updateLocale, locale } from '@@/js/config.js'; +import defaultLightTheme from '@@/themes/l-light.json5'; +import defaultDarkTheme from '@@/themes/d-green-lime.json5'; import type { App } from 'vue'; import widgets from '@/widgets/index.js'; import directives from '@/directives/index.js'; @@ -14,7 +16,7 @@ import { applyTheme } from '@/scripts/theme.js'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js'; import { updateI18n, i18n } from '@/i18n.js'; import { $i, refreshAccount, login } from '@/account.js'; -import { defaultStore, ColdDeviceStorage } from '@/store.js'; +import { store } from '@/store.js'; import { fetchInstance, instance } from '@/instance.js'; import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js'; import { reloadChannel } from '@/scripts/unison-reload.js'; @@ -26,6 +28,7 @@ import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; import { setupRouter } from '@/router/main.js'; import { createMainRouter } from '@/router/definition.js'; +import { prefer } from '@/preferences.js'; export async function common(createVue: () => App<Element>) { console.info(`Misskey v${version}`); @@ -38,7 +41,7 @@ export async function common(createVue: () => App<Element>) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (window as any).$i = $i; // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any).$store = defaultStore; + (window as any).$store = store; window.addEventListener('error', event => { console.error(event); @@ -123,7 +126,7 @@ export async function common(createVue: () => App<Element>) { html.setAttribute('lang', lang); //#endregion - await defaultStore.ready; + await store.ready; await deckStore.ready; const fetchInstanceMetaPromise = fetchInstance(); @@ -151,56 +154,63 @@ export async function common(createVue: () => App<Element>) { //#endregion // NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため) - watch(defaultStore.reactiveState.darkMode, (darkMode) => { - applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); + watch(store.reactiveState.darkMode, (darkMode) => { + applyTheme(darkMode + ? (prefer.s.darkTheme ?? defaultDarkTheme) + : (prefer.s.lightTheme ?? defaultLightTheme), + ); }, { immediate: miLocalStorage.getItem('theme') == null }); - document.documentElement.dataset.colorScheme = defaultStore.state.darkMode ? 'dark' : 'light'; + document.documentElement.dataset.colorScheme = store.state.darkMode ? 'dark' : 'light'; - const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); - const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); + const darkTheme = prefer.model('darkTheme'); + const lightTheme = prefer.model('lightTheme'); watch(darkTheme, (theme) => { - if (defaultStore.state.darkMode) { - applyTheme(theme); + if (store.state.darkMode) { + applyTheme(theme ?? defaultDarkTheme); } }); watch(lightTheme, (theme) => { - if (!defaultStore.state.darkMode) { - applyTheme(theme); + if (!store.state.darkMode) { + applyTheme(theme ?? defaultLightTheme); } }); //#region Sync dark mode - if (ColdDeviceStorage.get('syncDeviceDarkMode')) { - defaultStore.set('darkMode', isDeviceDarkmode()); + if (prefer.s.syncDeviceDarkMode) { + store.set('darkMode', isDeviceDarkmode()); } window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => { - if (ColdDeviceStorage.get('syncDeviceDarkMode')) { - defaultStore.set('darkMode', mql.matches); + if (prefer.s.syncDeviceDarkMode) { + store.set('darkMode', mql.matches); } }); //#endregion + if (prefer.s.darkTheme && store.state.darkMode) { + if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme); + } else if (prefer.s.lightTheme && !store.state.darkMode) { + if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme); + } + fetchInstanceMetaPromise.then(() => { - if (defaultStore.state.themeInitial) { - if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme)); - if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme)); - defaultStore.set('themeInitial', false); - } + // TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア + if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.set('lightTheme', JSON.parse(instance.defaultLightTheme)); + if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.set('darkTheme', JSON.parse(instance.defaultDarkTheme)); }); - watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => { + watch(store.reactiveState.overridedDeviceKind, (kind) => { updateDeviceKind(kind); }, { immediate: true }); - watch(defaultStore.reactiveState.useBlurEffectForModal, v => { + watch(prefer.r.useBlurEffectForModal, v => { document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none'); }, { immediate: true }); - watch(defaultStore.reactiveState.useBlurEffect, v => { + watch(prefer.r.useBlurEffect, v => { if (v) { document.documentElement.style.removeProperty('--MI-blur'); } else { @@ -214,7 +224,7 @@ export async function common(createVue: () => App<Element>) { navigator.wakeLock.request('screen'); } }); - if (defaultStore.state.keepScreenOn && 'wakeLock' in navigator) { + if (prefer.s.keepScreenOn && 'wakeLock' in navigator) { navigator.wakeLock.request('screen') .then(onVisibilityChange) .catch(() => { diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 3a43c6794b..0ebe55ed1d 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -5,26 +5,29 @@ import { createApp, defineAsyncComponent, markRaw } from 'vue'; import { ui } from '@@/js/config.js'; -import { common } from './common.js'; import * as Misskey from 'misskey-js'; +import { common } from './common.js'; import type { Component } from 'vue'; +import type { Keymap } from '@/scripts/hotkey.js'; import { i18n } from '@/i18n.js'; import { alert, confirm, popup, post, toast } from '@/os.js'; import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; import { $i, signout, updateAccountPartial } from '@/account.js'; import { instance } from '@/instance.js'; -import { ColdDeviceStorage, defaultStore } from '@/store.js'; +import { ColdDeviceStorage, store } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; import { initializeSw } from '@/scripts/initialize-sw.js'; -import { deckStore } from '@/ui/deck/deck-store.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mainRouter } from '@/router/main.js'; import { makeHotkey } from '@/scripts/hotkey.js'; -import type { Keymap } from '@/scripts/hotkey.js'; import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js'; +import { prefer } from '@/preferences.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { deckStore } from '@/ui/deck/deck-store.js'; +import { launchPlugin } from '@/plugin.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => { @@ -34,7 +37,7 @@ export async function mainBoot() { if (!$i) uiStyle = 'visitor'; if (searchParams.has('zen')) uiStyle = 'zen'; - if (uiStyle === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') uiStyle = 'zen'; + if (uiStyle === 'deck' && prefer.s['deck.useSimpleUiForNonRootPages'] && location.pathname !== '/') uiStyle = 'zen'; if (searchParams.has('ui')) uiStyle = searchParams.get('ui'); @@ -73,9 +76,9 @@ export async function mainBoot() { let reloadDialogShowing = false; stream.on('_disconnected_', async () => { - if (defaultStore.state.serverDisconnectedBehavior === 'reload') { + if (prefer.s.serverDisconnectedBehavior === 'reload') { location.reload(); - } else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { + } else if (prefer.s.serverDisconnectedBehavior === 'dialog') { if (reloadDialogShowing) return; reloadDialogShowing = true; const { canceled } = await confirm({ @@ -102,18 +105,14 @@ export async function mainBoot() { removeCustomEmojis(emojiData.emojis); }); - for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { - import('@/plugin.js').then(async ({ install }) => { - // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 - await new Promise(r => setTimeout(r, 0)); - install(plugin); - }); + for (const plugin of prefer.s.plugins.filter(p => p.active)) { + launchPlugin(plugin); } try { - if (defaultStore.state.enableSeasonalScreenEffect) { + if (prefer.s.enableSeasonalScreenEffect) { const month = new Date().getMonth() + 1; - if (defaultStore.state.hemisphere === 'S') { + if (prefer.s.hemisphere === 'S') { // ▼南半球 if (month === 7 || month === 8) { const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect; @@ -138,8 +137,99 @@ export async function mainBoot() { } if ($i) { - defaultStore.loaded.then(() => { - if (defaultStore.state.accountSetupWizard !== -1) { + store.loaded.then(async () => { + // prefereces migration + // TODO: そのうち消す + if (store.state.menu.length > 0) { + const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []); + if (themes.length > 0) { + prefer.set('themes', themes); + } + const plugins = ColdDeviceStorage.get('plugins'); + prefer.set('plugins', plugins.map(p => ({ + ...p, + installId: (p as any).id, + id: undefined, + }))); + prefer.set('lightTheme', ColdDeviceStorage.get('lightTheme')); + prefer.set('darkTheme', ColdDeviceStorage.get('darkTheme')); + prefer.set('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode')); + prefer.set('keepCw', store.state.keepCw); + prefer.set('collapseRenotes', store.state.collapseRenotes); + prefer.set('rememberNoteVisibility', store.state.rememberNoteVisibility); + prefer.set('uploadFolder', store.state.uploadFolder); + prefer.set('keepOriginalUploading', store.state.keepOriginalUploading); + prefer.set('menu', store.state.menu); + prefer.set('statusbars', store.state.statusbars); + prefer.set('pinnedUserLists', store.state.pinnedUserLists); + prefer.set('serverDisconnectedBehavior', store.state.serverDisconnectedBehavior); + prefer.set('nsfw', store.state.nsfw); + prefer.set('highlightSensitiveMedia', store.state.highlightSensitiveMedia); + prefer.set('animation', store.state.animation); + prefer.set('animatedMfm', store.state.animatedMfm); + prefer.set('advancedMfm', store.state.advancedMfm); + prefer.set('showReactionsCount', store.state.showReactionsCount); + prefer.set('enableQuickAddMfmFunction', store.state.enableQuickAddMfmFunction); + prefer.set('loadRawImages', store.state.loadRawImages); + prefer.set('imageNewTab', store.state.imageNewTab); + prefer.set('disableShowingAnimatedImages', store.state.disableShowingAnimatedImages); + prefer.set('emojiStyle', store.state.emojiStyle); + prefer.set('menuStyle', store.state.menuStyle); + prefer.set('useBlurEffectForModal', store.state.useBlurEffectForModal); + prefer.set('useBlurEffect', store.state.useBlurEffect); + prefer.set('showFixedPostForm', store.state.showFixedPostForm); + prefer.set('showFixedPostFormInChannel', store.state.showFixedPostFormInChannel); + prefer.set('enableInfiniteScroll', store.state.enableInfiniteScroll); + prefer.set('useReactionPickerForContextMenu', store.state.useReactionPickerForContextMenu); + prefer.set('showGapBetweenNotesInTimeline', store.state.showGapBetweenNotesInTimeline); + prefer.set('instanceTicker', store.state.instanceTicker); + prefer.set('emojiPickerScale', store.state.emojiPickerScale); + prefer.set('emojiPickerWidth', store.state.emojiPickerWidth); + prefer.set('emojiPickerHeight', store.state.emojiPickerHeight); + prefer.set('emojiPickerStyle', store.state.emojiPickerStyle); + prefer.set('reportError', store.state.reportError); + prefer.set('squareAvatars', store.state.squareAvatars); + prefer.set('showAvatarDecorations', store.state.showAvatarDecorations); + prefer.set('numberOfPageCache', store.state.numberOfPageCache); + prefer.set('showNoteActionsOnlyHover', store.state.showNoteActionsOnlyHover); + prefer.set('showClipButtonInNoteFooter', store.state.showClipButtonInNoteFooter); + prefer.set('reactionsDisplaySize', store.state.reactionsDisplaySize); + prefer.set('limitWidthOfReaction', store.state.limitWidthOfReaction); + prefer.set('forceShowAds', store.state.forceShowAds); + prefer.set('aiChanMode', store.state.aiChanMode); + prefer.set('devMode', store.state.devMode); + prefer.set('mediaListWithOneImageAppearance', store.state.mediaListWithOneImageAppearance); + prefer.set('notificationPosition', store.state.notificationPosition); + prefer.set('notificationStackAxis', store.state.notificationStackAxis); + prefer.set('enableCondensedLine', store.state.enableCondensedLine); + prefer.set('keepScreenOn', store.state.keepScreenOn); + prefer.set('disableStreamingTimeline', store.state.disableStreamingTimeline); + prefer.set('useGroupedNotifications', store.state.useGroupedNotifications); + prefer.set('dataSaver', store.state.dataSaver); + prefer.set('enableSeasonalScreenEffect', store.state.enableSeasonalScreenEffect); + prefer.set('enableHorizontalSwipe', store.state.enableHorizontalSwipe); + prefer.set('useNativeUiForVideoAudioPlayer', store.state.useNativeUIForVideoAudioPlayer); + prefer.set('keepOriginalFilename', store.state.keepOriginalFilename); + prefer.set('alwaysConfirmFollow', store.state.alwaysConfirmFollow); + prefer.set('confirmWhenRevealingSensitiveMedia', store.state.confirmWhenRevealingSensitiveMedia); + prefer.set('contextMenu', store.state.contextMenu); + prefer.set('skipNoteRender', store.state.skipNoteRender); + prefer.set('showSoftWordMutedWord', store.state.showSoftWordMutedWord); + prefer.set('confirmOnReact', store.state.confirmOnReact); + prefer.set('sound.masterVolume', store.state.sound_masterVolume); + prefer.set('sound.notUseSound', store.state.sound_notUseSound); + prefer.set('sound.useSoundOnlyWhenActive', store.state.sound_useSoundOnlyWhenActive); + prefer.set('sound.on.note', store.state.sound_note as any); + prefer.set('sound.on.noteMy', store.state.sound_noteMy as any); + prefer.set('sound.on.notification', store.state.sound_notification as any); + prefer.set('sound.on.reaction', store.state.sound_reaction as any); + store.set('deck.profile', deckStore.state.profile); + store.set('deck.columns', deckStore.state.columns); + store.set('deck.layout', deckStore.state.layout); + store.set('menu', []); + } + + if (store.state.accountSetupWizard !== -1) { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, { closed: () => dispose(), }); @@ -154,7 +244,7 @@ export async function mainBoot() { }); } - function onAnnouncementCreated (ev: { announcement: Misskey.entities.Announcement }) { + function onAnnouncementCreated(ev: { announcement: Misskey.entities.Announcement }) { const announcement = ev.announcement; if (announcement.display === 'dialog') { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { @@ -412,7 +502,7 @@ export async function mainBoot() { post(); }, 'd': () => { - defaultStore.set('darkMode', !defaultStore.state.darkMode); + store.set('darkMode', !store.state.darkMode); }, 's': () => { mainRouter.push('/search'); diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 33495c8af6..1a68353331 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -54,17 +54,18 @@ import contains from '@/scripts/contains.js'; import { acct } from '@/filters/user.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; import { searchEmoji } from '@/scripts/search-emoji.js'; +import { prefer } from '@/preferences.js'; const lib = emojilist.filter(x => x.category !== 'flags'); const emojiDb = computed(() => { //#region Unicode Emoji - const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; + const char2path = prefer.r.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({ emoji: x.char, @@ -72,7 +73,7 @@ const emojiDb = computed(() => { url: char2path(x.char), })); - for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { + for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) { for (const [emoji, keywords] of Object.entries(index)) { for (const k of keywords) { unicodeEmojiDB.push({ @@ -154,10 +155,10 @@ function complete(type: string, value: any) { emit('done', { type, value }); emit('closed'); if (type === 'emoji') { - let recents = defaultStore.state.recentlyUsedEmojis; + let recents = store.state.recentlyUsedEmojis; recents = recents.filter((emoji: any) => emoji !== value); recents.unshift(value); - defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); + store.set('recentlyUsedEmojis', recents.splice(0, 32)); } } @@ -237,7 +238,7 @@ function exec() { } else if (props.type === 'emoji') { if (!props.q || props.q === '') { // 最近使った絵文字をサジェスト - emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[]; + emojis.value = store.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[]; return; } diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index 134f8226d4..05b8264a83 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; // APIs provided by Captcha services // see: https://docs.hcaptcha.com/configuration/#javascript-api @@ -154,7 +154,7 @@ async function requestRender() { captchaWidgetId.value = captcha.value.render(elem, { sitekey: props.sitekey, - theme: defaultStore.state.darkMode ? 'dark' : 'light', + theme: store.state.darkMode ? 'dark' : 'light', callback: callback, 'expired-callback': () => callback(undefined), 'error-callback': () => callback(undefined), diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index d05f4921f6..c8685f8618 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -54,7 +54,7 @@ import { onMounted, ref, shallowRef, watch } from 'vue'; import { Chart } from 'chart.js'; import * as Misskey from 'misskey-js'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; import { alpha } from '@/scripts/color.js'; @@ -161,7 +161,7 @@ const render = () => { chartInstance.destroy(); } - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y))); diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue index 0d7a67eaec..a9001b376a 100644 --- a/packages/frontend/src/components/MkCode.core.vue +++ b/packages/frontend/src/components/MkCode.core.vue @@ -13,7 +13,7 @@ import { computed, ref, watch } from 'vue'; import { bundledLanguagesInfo } from 'shiki/langs'; import type { BundledLanguage } from 'shiki/langs'; import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; const props = defineProps<{ code: string; @@ -22,7 +22,7 @@ const props = defineProps<{ }>(); const highlighter = await getHighlighter(); -const darkMode = defaultStore.reactiveState.darkMode; +const darkMode = store.reactiveState.darkMode; const codeLang = ref<BundledLanguage | 'aiscript'>('js'); const [lightThemeName, darkThemeName] = await Promise.all([ diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index cb82bfd98b..06e4d32878 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only </button> <Suspense> <template #fallback> - <MkLoading /> + <MkLoading/> </template> <XCode v-if="show && lang" :code="code" :lang="lang"/> <pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre> @@ -28,9 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, ref } from 'vue'; import * as os from '@/os.js'; import MkLoading from '@/components/global/MkLoading.vue'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ code: string; @@ -42,7 +42,7 @@ const props = withDefaults(defineProps<{ forceShow: false, }); -const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code); +const show = ref(props.forceShow === true ? true : !prefer.s.dataSaver.code); const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue')); diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index 6a278250fa..169f3df7a4 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </header> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''" @enter="enter" @afterEnter="afterEnter" @leave="leave" @@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue index f51fefa0c0..ed9e5127b0 100644 --- a/packages/frontend/src/components/MkContextMenu.vue +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <Transition appear - :enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''" > <div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> <MkMenu :items="items" :align="'left'" @close="emit('closed')"/> @@ -22,7 +22,7 @@ import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue'; import MkMenu from './MkMenu.vue'; import type { MenuItem } from '@/types/menu.js'; import contains from '@/scripts/contains.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import * as os from '@/os.js'; const props = defineProps<{ diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 0186cfc2c0..9f9c5b32e2 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -35,13 +35,13 @@ import { onMounted, shallowRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import Cropper from 'cropperjs'; import tinycolor from 'tinycolor2'; +import { apiUrl } from '@@/js/config.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import * as os from '@/os.js'; import { $i } from '@/account.js'; -import { defaultStore } from '@/store.js'; -import { apiUrl } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; +import { prefer } from '@/preferences.js'; const emit = defineEmits<{ (ev: 'ok', cropped: Misskey.entities.DriveFile): void; @@ -81,8 +81,8 @@ const ok = async () => { formData.append('i', $i!.token); if (props.uploadFolder) { formData.append('folderId', props.uploadFolder); - } else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) { - formData.append('folderId', defaultStore.state.uploadFolder); + } else if (props.uploadFolder !== null && prefer.s.uploadFolder) { + formData.append('folderId', prefer.s.uploadFolder); } window.fetch(apiUrl + '/drive/files/create', { diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 0d5a16126b..b5842876ac 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -6,13 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts"> import { defineComponent, h, TransitionGroup, useCssModule } from 'vue'; import type { PropType } from 'vue'; +import type { MisskeyEntity } from '@/types/date-separated-list.js'; import MkAd from '@/components/global/MkAd.vue'; import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { instance } from '@/instance.js'; -import { defaultStore } from '@/store.js'; -import type { MisskeyEntity } from '@/types/date-separated-list.js'; +import { prefer } from '@/preferences.js'; export default defineComponent({ props: { @@ -150,7 +150,7 @@ export default defineComponent({ [$style['direction-up']]: props.direction === 'up', }; - return () => defaultStore.state.animation ? h(TransitionGroup, { + return () => prefer.s.animation ? h(TransitionGroup, { class: classes, name: 'list', tag: 'div', diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 44e3b59ade..f91b248d0c 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="!hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template> {{ folder.name }} </p> - <p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload"> + <p v-if="prefer.s.uploadFolder == folder.id" :class="$style.upload"> {{ i18n.ts.uploadFolder }} </p> <button v-if="selectMode" class="_button" :class="$style.checkboxWrapper" @click.prevent.stop="checkboxClicked"> @@ -40,9 +40,9 @@ import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -244,8 +244,8 @@ function deleteFolder() { misskeyApi('drive/folders/delete', { folderId: props.folder.id, }).then(() => { - if (defaultStore.state.uploadFolder === props.folder.id) { - defaultStore.set('uploadFolder', null); + if (prefer.s.uploadFolder === props.folder.id) { + prefer.set('uploadFolder', null); } }).catch(err => { switch (err.id) { @@ -266,7 +266,7 @@ function deleteFolder() { } function setAsUploadFolder() { - defaultStore.set('uploadFolder', props.folder.id); + prefer.set('uploadFolder', props.folder.id); } function onContextmenu(ev: MouseEvent) { @@ -295,7 +295,7 @@ function onContextmenu(ev: MouseEvent) { danger: true, action: deleteFolder, }]; - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menu = menu.concat([{ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyFolderId, diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 8be6d6f53d..6863e65b05 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -106,10 +106,10 @@ import XFile from '@/components/MkDrive.file.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { uploadFile, uploads } from '@/scripts/upload.js'; import { claimAchievement } from '@/scripts/achievements.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ initialFolder?: Misskey.entities.DriveFolder; @@ -142,7 +142,7 @@ const selectedFiles = ref<Misskey.entities.DriveFile[]>([]); const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]); const uploadings = uploads; const connection = useStream().useChannel('drive'); -const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい +const keepOriginal = ref<boolean>(prefer.s.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい // ドロップされようとしているか const draghover = ref(false); @@ -716,7 +716,7 @@ function onContextmenu(ev: MouseEvent) { } onMounted(() => { - if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) { + if (prefer.s.enableInfiniteScroll && loadMoreFiles.value) { nextTick(() => { ilFilesObserver.observe(loadMoreFiles.value?.$el); }); @@ -737,7 +737,7 @@ onMounted(() => { }); onActivated(() => { - if (defaultStore.state.enableInfiniteScroll) { + if (prefer.s.enableInfiniteScroll) { nextTick(() => { ilFilesObserver.observe(loadMoreFiles.value?.$el); }); diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 62a1000674..3c5b3133da 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -134,14 +134,15 @@ import * as os from '@/os.js'; import { isTouchUsing } from '@/scripts/touch.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js'; import { $i } from '@/account.js'; import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ showPinned?: boolean; - pinnedEmojis?: string[]; + pinnedEmojis?: string[]; maxHeight?: number; asDrawer?: boolean; asWindow?: boolean; @@ -163,8 +164,9 @@ const { emojiPickerScale, emojiPickerWidth, emojiPickerHeight, - recentlyUsedEmojis, -} = defaultStore.reactiveState; +} = prefer.r; + +const recentlyUsedEmojis = store.reactiveState.recentlyUsedEmojis; const recentlyUsedEmojisDef = computed(() => { return recentlyUsedEmojis.value.map(getDef); @@ -317,7 +319,7 @@ watch(q, () => { } if (matches.size >= max) return matches; - for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { + for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) { for (const emoji of emojis) { if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) { matches.add(emoji); @@ -334,7 +336,7 @@ watch(q, () => { } if (matches.size >= max) return matches; - for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { + for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) { for (const emoji of emojis) { if (index[emoji.char].some(k => k.startsWith(newQ))) { matches.add(emoji); @@ -351,7 +353,7 @@ watch(q, () => { } if (matches.size >= max) return matches; - for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { + for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) { for (const emoji of emojis) { if (index[emoji.char].some(k => k.includes(newQ))) { matches.add(emoji); @@ -413,7 +415,7 @@ function computeButtonTitle(ev: MouseEvent): void { function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) { const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; - if (el && defaultStore.state.animation) { + if (el && prefer.s.animation) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); @@ -427,10 +429,10 @@ function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, // 最近使った絵文字更新 if (!pinned.value?.includes(key)) { - let recents = defaultStore.state.recentlyUsedEmojis; + let recents = store.state.recentlyUsedEmojis; recents = recents.filter((emoji) => emoji !== key); recents.unshift(key); - defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); + store.set('recentlyUsedEmojis', recents.splice(0, 32)); } } diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 21c712b441..6d7062b41c 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="modal" v-slot="{ type, maxHeight }" :zPriority="'middle'" - :preferType="defaultStore.state.emojiPickerStyle" + :preferType="prefer.s.emojiPickerStyle" :hasInteractionWithOtherFocusTrappedEls="true" :transparentBg="true" :manualShowing="manualShowing" @@ -40,16 +40,16 @@ import * as Misskey from 'misskey-js'; import { shallowRef } from 'vue'; import MkModal from '@/components/MkModal.vue'; import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ manualShowing?: boolean | null; src?: HTMLElement; showPinned?: boolean; - pinnedEmojis?: string[], + pinnedEmojis?: string[], asReactionPicker?: boolean; targetNote?: Misskey.entities.Note; - choseAndClose?: boolean; + choseAndClose?: boolean; }>(), { manualShowing: null, showPinned: true, diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue index fb1b5220fb..08b8d60d09 100644 --- a/packages/frontend/src/components/MkFoldableSection.vue +++ b/packages/frontend/src/components/MkFoldableSection.vue @@ -14,10 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only </button> </header> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.folderToggleEnterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.folderToggleLeaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.folderToggleEnterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.folderToggleLeaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.folderToggleEnterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.folderToggleLeaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.folderToggleEnterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.folderToggleLeaveTo : ''" @enter="enter" @afterEnter="afterEnter" @leave="leave" @@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref, shallowRef, watch } from 'vue'; import { miLocalStorage } from '@/local-storage.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { getBgColor } from '@/scripts/get-bg-color.js'; const miLocalStoragePrefix = 'ui:folder:' as const; diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 384c0c0b34..1f27e9e221 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -27,10 +27,10 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened"> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''" @enter="enter" @afterEnter="afterEnter" @leave="leave" @@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { nextTick, onMounted, ref, shallowRef } from 'vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { getBgColor } from '@/scripts/get-bg-color.js'; const props = withDefaults(defineProps<{ diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index c1dc67f776..bd45df843a 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -45,7 +45,8 @@ import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import { $i } from '@/account.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ user: Misskey.entities.UserDetailed, @@ -100,7 +101,7 @@ async function onClick() { userId: props.user.id, }); } else { - if (defaultStore.state.alwaysConfirmFollow) { + if (prefer.s.alwaysConfirmFollow) { const { canceled } = await os.confirm({ type: 'question', text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }), @@ -120,11 +121,11 @@ async function onClick() { } else { await misskeyApi('following/create', { userId: props.user.id, - withReplies: defaultStore.state.defaultWithReplies, + withReplies: store.state.defaultWithReplies, }); emit('update:user', { ...props.user, - withReplies: defaultStore.state.defaultWithReplies, + withReplies: store.state.defaultWithReplies, }); hasPendingFollowRequestFromYou.value = true; diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue index 22f8355acf..49a6c65170 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.vue +++ b/packages/frontend/src/components/MkGalleryPostPreview.vue @@ -35,14 +35,14 @@ SPDX-License-Identifier: AGPL-3.0-only import * as Misskey from 'misskey-js'; import { computed, ref } from 'vue'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ post: Misskey.entities.GalleryPost; }>(); const hover = ref(false); -const safe = computed(() => defaultStore.state.nsfw === 'ignore' || defaultStore.state.nsfw === 'respect' && !props.post.isSensitive); +const safe = computed(() => prefer.s.nsfw === 'ignore' || prefer.s.nsfw === 'respect' && !props.post.isSensitive); const show = computed(() => safe.value || hover.value); function enterHover(): void { diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue index 0cc0df9911..058a2d7c78 100644 --- a/packages/frontend/src/components/MkHeatmap.vue +++ b/packages/frontend/src/components/MkHeatmap.vue @@ -17,7 +17,7 @@ import { onMounted, nextTick, watch, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { alpha } from '@/scripts/color.js'; import { initChart } from '@/scripts/init-chart.js'; @@ -106,7 +106,7 @@ async function renderChart() { await nextTick(); - const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300'; + const color = store.state.darkMode ? '#b4e900' : '#86b300'; // 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3; diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue index 196c962a06..ca597b43cb 100644 --- a/packages/frontend/src/components/MkHorizontalSwipe.vue +++ b/packages/frontend/src/components/MkHorizontalSwipe.vue @@ -28,12 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, shallowRef, computed, nextTick, watch } from 'vue'; import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; -import { defaultStore } from '@/store.js'; import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js'; +import { prefer } from '@/preferences.js'; const rootEl = shallowRef<HTMLDivElement>(); -// eslint-disable-next-line no-undef const tabModel = defineModel<string>('tab'); const props = defineProps<{ @@ -44,7 +43,7 @@ const emit = defineEmits<{ (ev: 'swiped', newKey: string, direction: 'left' | 'right'): void; }>(); -const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontalSwipe.value || defaultStore.reactiveState.animation.value); +const shouldAnimate = computed(() => prefer.r.enableHorizontalSwipe.value || prefer.r.animation.value); // ▼ しきい値 ▼ // @@ -72,7 +71,7 @@ const isSwipingForClass = ref(false); let swipeAborted = false; function touchStart(event: TouchEvent) { - if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return; + if (!prefer.r.enableHorizontalSwipe.value) return; if (event.touches.length !== 1) return; @@ -83,7 +82,7 @@ function touchStart(event: TouchEvent) { } function touchMove(event: TouchEvent) { - if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return; + if (!prefer.r.enableHorizontalSwipe.value) return; if (event.touches.length !== 1) return; @@ -134,7 +133,7 @@ function touchEnd(event: TouchEvent) { return; } - if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return; + if (!prefer.r.enableHorizontalSwipe.value) return; if (event.touches.length !== 0) return; diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index c04d0864fb..37cbc5d06b 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -6,13 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''"> <TransitionGroup - :duration="defaultStore.state.animation && props.transition?.duration || undefined" - :enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined" - :leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined" - :enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined" - :leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined" - :enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined" - :leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined" + :duration="prefer.s.animation && props.transition?.duration || undefined" + :enterActiveClass="prefer.s.animation && props.transition?.enterActiveClass || undefined" + :leaveActiveClass="prefer.s.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined" + :enterFromClass="prefer.s.animation && props.transition?.enterFromClass || undefined" + :leaveToClass="prefer.s.animation && props.transition?.leaveToClass || undefined" + :enterToClass="prefer.s.animation && props.transition?.enterToClass || undefined" + :leaveFromClass="prefer.s.animation && props.transition?.leaveFromClass || undefined" > <canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/> <img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/> @@ -60,7 +60,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue'; import { v4 as uuid } from 'uuid'; import { render } from 'buraha'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ transition?: { diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue index 32c1a2d172..9538979dd1 100644 --- a/packages/frontend/src/components/MkLaunchPad.vue +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only import { shallowRef } from 'vue'; import MkModal from '@/components/MkModal.vue'; import { navbarItemDef } from '@/navbar.js'; -import { defaultStore } from '@/store.js'; import { deviceKind } from '@/scripts/device-kind.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ src?: HTMLElement; @@ -50,7 +50,7 @@ const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'pop const modal = shallowRef<InstanceType<typeof MkModal>>(); -const menu = defaultStore.state.menu; +const menu = prefer.s.menu; const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({ type: def.to ? 'link' : 'button', diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index 3d2795b37a..83ce212efd 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -10,20 +10,20 @@ SPDX-License-Identifier: AGPL-3.0-only tabindex="0" :class="[ $style.audioContainer, - (audio.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive, + (audio.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive, ]" @contextmenu.stop @keydown.stop > <button v-if="hide" :class="$style.hidden" @click="show"> <div :class="$style.hiddenTextWrapper"> - <b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b> - <b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b> + <b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ti ti-music"></i> {{ prefer.s.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b> <span style="display: block;">{{ i18n.ts.clickToShow }}</span> </div> </button> - <div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.nativeAudioContainer"> + <div v-else-if="prefer.s.useNativeUiForVideoAudioPlayer" :class="$style.nativeAudioContainer"> <audio ref="audioEl" preload="metadata" @@ -93,13 +93,13 @@ import * as Misskey from 'misskey-js'; import type { MenuItem } from '@/types/menu.js'; import type { Keymap } from '@/scripts/hotkey.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import bytes from '@/filters/bytes.js'; import { hms } from '@/filters/hms.js'; import MkMediaRange from '@/components/MkMediaRange.vue'; import { $i, iAmModerator } from '@/account.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ audio: Misskey.entities.DriveFile; @@ -155,10 +155,10 @@ const playerEl = shallowRef<HTMLDivElement>(); const audioEl = shallowRef<HTMLAudioElement>(); // eslint-disable-next-line vue/no-setup-props-reactivity-loss -const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore')); +const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore')); async function show() { - if (props.audio.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { + if (props.audio.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { const { canceled } = await os.confirm({ type: 'question', text: i18n.ts.sensitiveMediaRevealConfirm, @@ -240,7 +240,7 @@ function showMenu(ev: MouseEvent) { menu.push({ type: 'divider' }, ...details); } - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menu.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyFileId, @@ -407,7 +407,7 @@ onDeactivated(() => { elapsedTimeMs.value = 0; durationMs.value = 0; bufferedEnd.value = 0; - hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'); + hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore'); stopAudioElWatch(); onceInit = false; if (mediaTickFrameId) { diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 3e521e0a03..f23cf507fb 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -27,9 +27,9 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; import MkMediaAudio from '@/components/MkMediaAudio.vue'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ media: Misskey.entities.DriveFile; @@ -38,7 +38,7 @@ const props = defineProps<{ const hide = ref(true); async function show() { - if (props.media.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { + if (props.media.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { const { canceled } = await os.confirm({ type: 'question', text: i18n.ts.sensitiveMediaRevealConfirm, diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 8ab990b926..8206e3d8f6 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" @click="onclick"> +<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="onclick"> <component :is="disableImageLink ? 'div' : 'a'" v-bind="disableImageLink ? { @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <ImgWithBlurhash :hash="image.blurhash" - :src="(defaultStore.state.dataSaver.media && hide) ? null : url" + :src="(prefer.s.dataSaver.media && hide) ? null : url" :forceBlurhash="hide" :cover="hide || cover" :alt="image.comment || image.name" @@ -32,8 +32,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="hide"> <div :class="$style.hiddenText"> <div :class="$style.hiddenTextWrapper"> - <b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> - <b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b> + <b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ti ti-photo"></i> {{ prefer.s.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b> <span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span> </div> </div> @@ -58,10 +58,10 @@ import { copyToClipboard } from '@/scripts/copy-to-clipboard'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import bytes from '@/filters/bytes.js'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { $i, iAmModerator } from '@/account.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ image: Misskey.entities.DriveFile; @@ -77,9 +77,9 @@ const props = withDefaults(defineProps<{ const hide = ref(true); -const url = computed(() => (props.raw || defaultStore.state.loadRawImages) +const url = computed(() => (props.raw || prefer.s.loadRawImages) ? props.image.url - : defaultStore.state.disableShowingAnimatedImages + : prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(props.image.url) : props.image.thumbnailUrl, ); @@ -91,7 +91,7 @@ async function onclick(ev: MouseEvent) { if (hide.value) { ev.stopPropagation(); - if (props.image.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { + if (props.image.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { const { canceled } = await os.confirm({ type: 'question', text: i18n.ts.sensitiveMediaRevealConfirm, @@ -105,7 +105,7 @@ async function onclick(ev: MouseEvent) { // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする watch(() => props.image, () => { - hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore'); + hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.image.isSensitive && prefer.s.nsfw !== 'ignore'); }, { deep: true, immediate: true, @@ -166,7 +166,7 @@ function showMenu(ev: MouseEvent) { menuItems.push({ type: 'divider' }, ...details); } - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyFileId, diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 32766f2029..1c9494f8d6 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -12,9 +12,9 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[ $style.medias, count === 1 ? [$style.n1, { - [$style.n116_9]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '16_9', - [$style.n11_1]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '1_1', - [$style.n12_3]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '2_3', + [$style.n116_9]: prefer.s.mediaListWithOneImageAppearance === '16_9', + [$style.n11_1]: prefer.s.mediaListWithOneImageAppearance === '1_1', + [$style.n12_3]: prefer.s.mediaListWithOneImageAppearance === '2_3', }] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany, ]" > @@ -33,13 +33,13 @@ import * as Misskey from 'misskey-js'; import PhotoSwipeLightbox from 'photoswipe/lightbox'; import PhotoSwipe from 'photoswipe'; import 'photoswipe/style.css'; +import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js'; import XBanner from '@/components/MkMediaBanner.vue'; import XImage from '@/components/MkMediaImage.vue'; import XVideo from '@/components/MkMediaVideo.vue'; import * as os from '@/os.js'; -import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js'; -import { defaultStore } from '@/store.js'; import { focusParent } from '@/scripts/focus.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ mediaList: Misskey.entities.DriveFile[]; @@ -75,7 +75,7 @@ async function calcAspectRatio() { return `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`; }; - switch (defaultStore.state.mediaListWithOneImageAppearance) { + switch (prefer.s.mediaListWithOneImageAppearance) { case '16_9': gallery.value.style.aspectRatio = ratioMax(16 / 9); break; diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 682da22711..1433b17dc0 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[ $style.videoContainer, controlsShowing && $style.active, - (video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive, + (video.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive, ]" @mouseover="onMouseOver" @mouseleave="onMouseLeave" @@ -20,13 +20,13 @@ SPDX-License-Identifier: AGPL-3.0-only > <button v-if="hide" :class="$style.hidden" @click="show"> <div :class="$style.hiddenTextWrapper"> - <b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> - <b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b> + <b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ti ti-movie"></i> {{ prefer.s.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b> <span style="display: block;">{{ i18n.ts.clickToShow }}</span> </div> </button> - <div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.videoRoot"> + <div v-else-if="prefer.s.useNativeUiForVideoAudioPlayer" :class="$style.videoRoot"> <video ref="videoEl" :class="$style.video" @@ -116,13 +116,13 @@ import type { Keymap } from '@/scripts/hotkey.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard'; import bytes from '@/filters/bytes.js'; import { hms } from '@/filters/hms.js'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js'; import hasAudio from '@/scripts/media-has-audio.js'; import MkMediaRange from '@/components/MkMediaRange.vue'; import { $i, iAmModerator } from '@/account.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ video: Misskey.entities.DriveFile; @@ -175,10 +175,10 @@ function hasFocus() { } // eslint-disable-next-line vue/no-setup-props-reactivity-loss -const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore')); +const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore')); async function show() { - if (props.video.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { + if (props.video.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { const { canceled } = await os.confirm({ type: 'question', text: i18n.ts.sensitiveMediaRevealConfirm, @@ -265,7 +265,7 @@ function showMenu(ev: MouseEvent) { menu.push({ type: 'divider' }, ...details); } - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menu.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyFileId, @@ -502,7 +502,7 @@ onDeactivated(() => { elapsedTimeMs.value = 0; durationMs.value = 0; bufferedEnd.value = 0; - hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'); + hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore'); stopVideoElWatch(); onceInit = false; if (mediaTickFrameId) { diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue index 87c82f0a89..ff3442abe0 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -19,8 +19,8 @@ import { computed } from 'vue'; import { host as localHost } from '@@/js/config.js'; import type { MkABehavior } from '@/components/global/MkA.vue'; import { $i } from '@/account.js'; -import { defaultStore } from '@/store.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ username: string; @@ -36,7 +36,7 @@ const isMe = $i && ( `@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase() ); -const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar +const avatarUrl = computed(() => prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar ? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`) : `/avatar/@${props.username}@${props.host}`, ); diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index 19588003fa..521c851d8b 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -43,13 +43,13 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch, ref, shallowRef, computed } from 'vue'; +import type { Keymap } from '@/scripts/hotkey.js'; import * as os from '@/os.js'; import { isTouchUsing } from '@/scripts/touch.js'; -import { defaultStore } from '@/store.js'; import { deviceKind } from '@/scripts/device-kind.js'; -import type { Keymap } from '@/scripts/hotkey.js'; import { focusTrap } from '@/scripts/focus-trap.js'; import { focusParent } from '@/scripts/focus.js'; +import { prefer } from '@/preferences.js'; function getFixedContainer(el: Element | null): Element | null { if (el == null || el.tagName === 'BODY') return null; @@ -106,7 +106,7 @@ const zIndex = os.claimZIndex(props.zPriority); const useSendAnime = ref(false); const type = computed<ModalTypes>(() => { if (props.preferType === 'auto') { - if ((defaultStore.state.menuStyle === 'drawer') || (defaultStore.state.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) { + if ((prefer.s.menuStyle === 'drawer') || (prefer.s.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) { return 'drawer'; } else { return props.src != null ? 'popup' : 'dialog'; @@ -117,7 +117,7 @@ const type = computed<ModalTypes>(() => { }); const isEnableBgTransparent = computed(() => props.transparentBg && (type.value === 'popup')); const transitionName = computed((() => - defaultStore.state.animation + prefer.s.animation ? useSendAnime.value ? 'send' : type.value === 'drawer' diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 8e1d854660..c88b5c16ee 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-show="!isDeleted" ref="rootEl" v-hotkey="keymap" - :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender }]" + :class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]" :tabindex="isDeleted ? '-1' : '0'" > <MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> @@ -130,9 +130,9 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ti ti-plus"></i> - <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> + <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> </button> - <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()"> + <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()"> <i class="ti ti-paperclip"></i> </button> <button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()"> @@ -178,13 +178,15 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, inject, onMounted, ref, shallowRef, watch, provide } from 'vue'; -import type { Ref } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; import { shouldCollapsed } from '@@/js/collapsed.js'; import { host } from '@@/js/config.js'; +import type { Ref } from 'vue'; import type { MenuItem } from '@/types/menu.js'; +import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; +import type { Keymap } from '@/scripts/hotkey.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; @@ -197,7 +199,6 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import { pleaseLogin } from '@/scripts/please-login.js'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { notePage } from '@/filters/note.js'; import { userPage } from '@/filters/user.js'; @@ -205,7 +206,7 @@ import number from '@/filters/number.js'; import * as os from '@/os.js'; import * as sound from '@/scripts/sound.js'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { defaultStore, noteViewInterruptors } from '@/store.js'; +import { noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; @@ -219,9 +220,9 @@ import { getNoteSummary } from '@/scripts/get-note-summary.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { isEnabledUrlPreview } from '@/instance.js'; -import type { Keymap } from '@/scripts/hotkey.js'; import { focusPrev, focusNext } from '@/scripts/focus.js'; import { getAppearNote } from '@/scripts/get-appear-note.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -284,13 +285,13 @@ const collapsed = ref(appearNote.value.cw == null && isLong); const isDeleted = ref(false); const muted = ref(checkMute(appearNote.value, $i?.mutedWords)); const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true)); -const showSoftWordMutedWord = computed(() => defaultStore.state.showSoftWordMutedWord); +const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord); const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null); const translating = ref(false); -const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); +const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id)); const renoteCollapsed = ref( - defaultStore.state.collapseRenotes && isRenote && ( + prefer.s.collapseRenotes && isRenote && ( ($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131 (appearNote.value.myReaction != null) ), @@ -345,7 +346,7 @@ const keymap = { }, 'c': () => { if (renoteCollapsed.value) return; - if (!defaultStore.state.showClipButtonInNoteFooter) return; + if (!prefer.s.showClipButtonInNoteFooter) return; clip(); }, 'o': () => { @@ -479,7 +480,7 @@ function react(): void { reaction: '❤️', }); const el = reactButton.value; - if (el && defaultStore.state.animation) { + if (el && prefer.s.animation) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); @@ -490,7 +491,7 @@ function react(): void { } else { blur(); reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => { - if (defaultStore.state.confirmOnReact) { + if (prefer.s.confirmOnReact) { const confirm = await os.confirm({ type: 'question', text: i18n.tsx.reactAreYouSure({ emoji: reaction.replace('@.', '') }), @@ -549,7 +550,7 @@ function onContextmenu(ev: MouseEvent): void { if (ev.target && isLink(ev.target as HTMLElement)) return; if (window.getSelection()?.toString() !== '') return; - if (defaultStore.state.useReactionPickerForContextMenu) { + if (prefer.s.useReactionPickerForContextMenu) { ev.preventDefault(); react(); } else { diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 27d8a399cc..75c08087f3 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -146,9 +146,9 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ti ti-plus"></i> - <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> + <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> </button> - <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()"> + <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()"> <i class="ti ti-paperclip"></i> </button> <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()"> @@ -215,6 +215,9 @@ import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; import { host } from '@@/js/config.js'; +import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; +import type { Paging } from '@/components/MkPagination.vue'; +import type { Keymap } from '@/scripts/hotkey.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; @@ -226,7 +229,6 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import { pleaseLogin } from '@/scripts/please-login.js'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import { notePage } from '@/filters/note.js'; @@ -234,7 +236,7 @@ import number from '@/filters/number.js'; import * as os from '@/os.js'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import * as sound from '@/scripts/sound.js'; -import { defaultStore, noteViewInterruptors } from '@/store.js'; +import { noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; @@ -248,12 +250,11 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkPagination from '@/components/MkPagination.vue'; -import type { Paging } from '@/components/MkPagination.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; import { isEnabledUrlPreview } from '@/instance.js'; import { getAppearNote } from '@/scripts/get-appear-note.js'; -import type { Keymap } from '@/scripts/hotkey.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -303,7 +304,7 @@ const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null); const translating = ref(false); const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null; -const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); +const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance); const conversation = ref<Misskey.entities.Note[]>([]); const replies = ref<Misskey.entities.Note[]>([]); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id); @@ -319,7 +320,7 @@ const keymap = { 'q': () => renote(), 'm': () => showMenu(), 'c': () => { - if (!defaultStore.state.showClipButtonInNoteFooter) return; + if (!prefer.s.showClipButtonInNoteFooter) return; clip(); }, 'o': () => galleryEl.value?.openGallery(), @@ -442,7 +443,7 @@ function react(): void { reaction: '❤️', }); const el = reactButton.value; - if (el && defaultStore.state.animation) { + if (el && prefer.s.animation) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); @@ -453,7 +454,7 @@ function react(): void { } else { blur(); reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => { - if (defaultStore.state.confirmOnReact) { + if (prefer.s.confirmOnReact) { const confirm = await os.confirm({ type: 'question', text: i18n.tsx.reactAreYouSure({ emoji: reaction.replace('@.', '') }), @@ -497,7 +498,7 @@ function onContextmenu(ev: MouseEvent): void { if (ev.target && isLink(ev.target as HTMLElement)) return; if (window.getSelection()?.toString() !== '') return; - if (defaultStore.state.useReactionPickerForContextMenu) { + if (prefer.s.useReactionPickerForContextMenu) { ev.preventDefault(); react(); } else { diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 750e32a9ff..8e5adec1a3 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -40,7 +40,6 @@ import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import { notePage } from '@/filters/note.js'; import { userPage } from '@/filters/user.js'; -import { defaultStore } from '@/store.js'; defineProps<{ note: Misskey.entities.Note; diff --git a/packages/frontend/src/components/MkNoteMediaGrid.vue b/packages/frontend/src/components/MkNoteMediaGrid.vue index e51ea5a2de..764d9f6a32 100644 --- a/packages/frontend/src/components/MkNoteMediaGrid.vue +++ b/packages/frontend/src/components/MkNoteMediaGrid.vue @@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-for="file in note.files"> <div v-if="((( - (defaultStore.state.nsfw === 'force' || file.isSensitive) && - defaultStore.state.nsfw !== 'ignore' - ) || (defaultStore.state.dataSaver.media && file.type.startsWith('image/'))) && + (prefer.s.nsfw === 'force' || file.isSensitive) && + prefer.s.nsfw !== 'ignore' + ) || (prefer.s.dataSaver.media && file.type.startsWith('image/'))) && !showingFiles.has(file.id) )" :class="[$style.filePreview, { [$style.square]: square }]" @@ -18,15 +18,15 @@ SPDX-License-Identifier: AGPL-3.0-only <MkDriveFileThumbnail :file="file" fit="cover" - :highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia" + :highlightWhenSensitive="prefer.s.highlightSensitiveMedia" :forceBlurhash="true" :large="true" :class="$style.file" /> <div :class="$style.sensitive"> <div> - <div v-if="file.isSensitive"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media && file.size ? ` (${bytes(file.size)})` : '' }}</div> - <div v-else><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && file.size ? bytes(file.size) : i18n.ts.image }}</div> + <div v-if="file.isSensitive"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media && file.size ? ` (${bytes(file.size)})` : '' }}</div> + <div v-else><i class="ti ti-photo"></i> {{ prefer.s.dataSaver.media && file.size ? bytes(file.size) : i18n.ts.image }}</div> <div>{{ i18n.ts.clickToShow }}</div> </div> </div> @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkDriveFileThumbnail :file="file" fit="cover" - :highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia" + :highlightWhenSensitive="prefer.s.highlightSensitiveMedia" :large="true" :class="$style.file" /> @@ -45,10 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; import { notePage } from '@/filters/note.js'; import { i18n } from '@/i18n.js'; -import * as Misskey from 'misskey-js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import bytes from '@/filters/bytes.js'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 470837ace5..4275262429 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -25,17 +25,17 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue'; +import * as Misskey from 'misskey-js'; +import type { notificationTypes } from '@@/js/const.js'; import MkPagination from '@/components/MkPagination.vue'; import XNotification from '@/components/MkNotification.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkNote from '@/components/MkNote.vue'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; -import type { notificationTypes } from '@@/js/const.js'; import { infoImageUrl } from '@/instance.js'; -import { defaultStore } from '@/store.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; -import * as Misskey from 'misskey-js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ excludeTypes?: typeof notificationTypes[number][]; @@ -43,7 +43,7 @@ const props = defineProps<{ const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); -const pagination = computed(() => defaultStore.reactiveState.useGroupedNotifications.value ? { +const pagination = computed(() => prefer.r.useGroupedNotifications.value ? { endpoint: 'i/notifications-grouped' as const, limit: 20, params: computed(() => ({ diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index d9135ab517..d4b0180ef9 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''" mode="out-in" > <MkLoading v-if="fetching"/> @@ -44,15 +44,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts"> import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue'; -import type { ComputedRef } from 'vue'; import * as Misskey from 'misskey-js'; import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js'; -import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import type { ComputedRef } from 'vue'; import type { MisskeyEntity } from '@/types/date-separated-list.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; const SECOND_FETCH_LIMIT = 30; const TOLERANCE = 16; @@ -140,7 +139,7 @@ const empty = computed(() => items.value.size === 0); const error = ref(false); const { enableInfiniteScroll, -} = defaultStore.reactiveState; +} = prefer.r; const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value); const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : document.body); diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index b39a4ad708..6aa460b506 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> <div v-show="useCw" :class="$style.cwOuter"> <input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd"> - <div v-if="maxCwTextLength - cwTextLength < 20" :class="['_acrylic', $style.cwTextCount, { [$style.cwTextOver]: cwTextLength > maxCwTextLength }]">{{ maxCwTextLength - cwTextLength }}</div> + <div v-if="maxCwTextLength - cwTextLength < 20" :class="['_acrylic', $style.cwTextCount, { [$style.cwTextOver]: cwTextLength > maxCwTextLength }]">{{ maxCwTextLength - cwTextLength }}</div> </div> <div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div> @@ -123,7 +123,7 @@ import { Autocomplete } from '@/scripts/autocomplete.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { selectFiles } from '@/scripts/select-file.js'; -import { defaultStore, notePostInterruptors, postFormActions } from '@/store.js'; +import { store, notePostInterruptors, postFormActions } from '@/store.js'; import MkInfo from '@/components/MkInfo.vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; @@ -135,6 +135,7 @@ import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; +import { prefer } from '@/preferences.js'; const $i = signinRequired(); @@ -174,19 +175,18 @@ const text = ref(props.initialText ?? ''); const files = ref(props.initialFiles ?? []); const poll = ref<PollEditorModelValue | null>(null); const useCw = ref<boolean>(!!props.initialCw); -const showPreview = ref(defaultStore.state.showPreview); -watch(showPreview, () => defaultStore.set('showPreview', showPreview.value)); -const showAddMfmFunction = ref(defaultStore.state.enableQuickAddMfmFunction); -watch(showAddMfmFunction, () => defaultStore.set('enableQuickAddMfmFunction', showAddMfmFunction.value)); +const showPreview = ref(store.state.showPreview); +watch(showPreview, () => store.set('showPreview', showPreview.value)); +const showAddMfmFunction = ref(prefer.s.enableQuickAddMfmFunction); +watch(showAddMfmFunction, () => prefer.set('enableQuickAddMfmFunction', showAddMfmFunction.value)); const cw = ref<string | null>(props.initialCw ?? null); -const localOnly = ref(props.initialLocalOnly ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly)); -const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility)); +const localOnly = ref(props.initialLocalOnly ?? (prefer.s.rememberNoteVisibility ? store.state.localOnly : prefer.s.defaultNoteLocalOnly)); +const visibility = ref(props.initialVisibility ?? (prefer.s.rememberNoteVisibility ? store.state.visibility : prefer.s.defaultNoteVisibility)); const visibleUsers = ref<Misskey.entities.UserDetailed[]>([]); if (props.initialVisibleUsers) { props.initialVisibleUsers.forEach(u => pushVisibleUser(u)); } -const reactionAcceptance = ref(defaultStore.state.reactionAcceptance); -const autocomplete = ref(null); +const reactionAcceptance = ref(store.state.reactionAcceptance); const draghover = ref(false); const quoteId = ref<string | null>(null); const hasNotSpecifiedMentions = ref(false); @@ -268,8 +268,8 @@ const canPost = computed((): boolean => { (!poll.value || poll.value.choices.length >= 2); }); -const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags')); -const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags')); +const withHashtags = computed(store.makeGetterSetter('postFormWithHashtags')); +const hashtags = computed(store.makeGetterSetter('postFormHashtags')); watch(text, () => { checkMissingMention(); @@ -357,7 +357,7 @@ if (props.specified) { } // keep cw when reply -if (defaultStore.state.keepCw && props.reply && props.reply.cw) { +if (prefer.s.keepCw && props.reply && props.reply.cw) { useCw.value = true; cw.value = props.reply.cw; } @@ -456,7 +456,7 @@ function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities function upload(file: File, name?: string): void { if (props.mock) return; - uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { + uploadFile(file, prefer.s.uploadFolder, name).then(res => { files.value.push(res); }); } @@ -477,8 +477,8 @@ function setVisibility() { }, { changeVisibility: v => { visibility.value = v; - if (defaultStore.state.rememberNoteVisibility) { - defaultStore.set('visibility', visibility.value); + if (prefer.s.rememberNoteVisibility) { + store.set('visibility', visibility.value); } }, closed: () => dispose(), @@ -525,8 +525,8 @@ async function toggleLocalOnly() { } localOnly.value = !localOnly.value; - if (defaultStore.state.rememberNoteVisibility) { - defaultStore.set('localOnly', localOnly.value); + if (prefer.s.rememberNoteVisibility) { + store.set('localOnly', localOnly.value); } } @@ -594,6 +594,8 @@ function onCompositionEnd(ev: CompositionEvent) { justEndedComposition.value = true; } +const pastedFileName = 'yyyy-MM-dd HH-mm-ss [{{number}}]'; + async function onPaste(ev: ClipboardEvent) { if (props.mock) return; if (!ev.clipboardData) return; @@ -604,7 +606,7 @@ async function onPaste(ev: ClipboardEvent) { if (!file) continue; const lio = file.name.lastIndexOf('.'); const ext = lio >= 0 ? file.name.slice(lio) : ''; - const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; + const formatted = `${formatTimeString(new Date(file.lastModified), pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; upload(file, formatted); } } @@ -638,7 +640,7 @@ async function onPaste(ev: ClipboardEvent) { return; } - const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, '0'); + const fileName = formatTimeString(new Date(), pastedFileName).replace(/{{number}}/g, '0'); const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' }); upload(file, `${fileName}.txt`); }); @@ -751,7 +753,7 @@ async function post(ev?: MouseEvent) { if (ev) { const el = (ev.currentTarget ?? ev.target) as HTMLElement | null; - if (el && defaultStore.state.animation) { + if (el && prefer.s.animation) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index c7774d50b2..83d0d66cc3 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -36,12 +36,12 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, inject } from 'vue'; import * as Misskey from 'misskey-js'; import type { MenuItem } from '@/types/menu'; -import { defaultStore } from '@/store'; import { copyToClipboard } from '@/scripts/copy-to-clipboard'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -198,7 +198,7 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar action: () => { detachAndDeleteMedia(file); }, }); - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyFileId, diff --git a/packages/frontend/src/components/MkPreferenceContainer.vue b/packages/frontend/src/components/MkPreferenceContainer.vue new file mode 100644 index 0000000000..85fab462cd --- /dev/null +++ b/packages/frontend/src/components/MkPreferenceContainer.vue @@ -0,0 +1,94 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root"> + <div :class="$style.body"> + <slot></slot> + </div> + <div :class="$style.menu"> + <i v-if="isAccountOverrided" class="ti ti-user-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i> + <div :class="$style.buttons"> + <button class="_button" style="color: var(--MI_THEME-fg)" @click="showMenu"><i class="ti ti-dots"></i></button> + </div> + </div> +</div> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import type { PREF_DEF } from '@/preferences/def.js'; +import * as os from '@/os.js'; +import { profileManager } from '@/preferences.js'; + +const props = withDefaults(defineProps<{ + k: keyof typeof PREF_DEF; +}>(), { +}); + +const isAccountOverrided = ref(profileManager.isAccountOverrided(props.k)); + +function showMenu(ev: MouseEvent) { + const i = window.setInterval(() => { + isAccountOverrided.value = profileManager.isAccountOverrided(props.k); + }, 100); + os.popupMenu(profileManager.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, { + onClosing: () => { + window.clearInterval(i); + }, + }); +} +</script> + +<style lang="scss" module> +.root { + position: relative; + display: flex; + + &:hover { + &::after { + content: ''; + position: absolute; + top: -8px; + left: -8px; + width: calc(100% + 16px); + height: calc(100% + 16px); + border-radius: 8px; + background: light-dark(rgba(0, 0, 0, 0.02), rgba(255, 255, 255, 0.02)); + pointer-events: none; + } + + .menu { + .buttons { + opacity: 0.7; + } + } + } + + .body { + flex: 1; + } + + .menu { + display: flex; + gap: 8px; + align-items: center; + margin-left: 12px; + font-size: 12px; + padding-left: 8px; + border-left: solid 1px var(--MI_THEME-divider); + + &:hover { + .buttons { + opacity: 1; + } + } + + .buttons { + opacity: 0.3; + } + } +} +</style> diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 41e475eade..2e453aeb8f 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -8,11 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only ref="buttonEl" v-ripple="canToggle" class="_button" - :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]" + :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large' }]" @click="toggleReaction()" @contextmenu.prevent.stop="menu" > - <MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> + <MkReactionIcon :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> <span :class="$style.count">{{ count }}</span> </button> </template> @@ -30,11 +30,11 @@ import { useTooltip } from '@/scripts/use-tooltip.js'; import { $i } from '@/account.js'; import MkReactionEffect from '@/components/MkReactionEffect.vue'; import { claimAchievement } from '@/scripts/achievements.js'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as sound from '@/scripts/sound.js'; import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; import { customEmojisMap } from '@/custom-emojis.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ reaction: string; @@ -90,7 +90,7 @@ async function toggleReaction() { } }); } else { - if (defaultStore.state.confirmOnReact) { + if (prefer.s.confirmOnReact) { const confirm = await os.confirm({ type: 'question', text: i18n.tsx.reactAreYouSure({ emoji: props.reaction.replace('@.', '') }), @@ -135,7 +135,7 @@ async function menu(ev) { } function anime() { - if (document.hidden || !defaultStore.state.animation || buttonEl.value == null) return; + if (document.hidden || !prefer.s.animation || buttonEl.value == null) return; const rect = buttonEl.value.getBoundingClientRect(); const x = rect.left + 16; diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index 63b202f9f3..bb60db8d34 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <TransitionGroup - :enterActiveClass="defaultStore.state.animation ? $style.transition_x_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_x_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_x_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_x_leaveTo : ''" - :moveClass="defaultStore.state.animation ? $style.transition_x_move : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''" + :moveClass="prefer.s.animation ? $style.transition_x_move : ''" tag="div" :class="$style.root" > <XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/> @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only import * as Misskey from 'misskey-js'; import { inject, watch, ref } from 'vue'; import XReaction from '@/components/MkReactionsViewer.reaction.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue index 64b573c4d3..3ca680d8f2 100644 --- a/packages/frontend/src/components/MkRetentionHeatmap.vue +++ b/packages/frontend/src/components/MkRetentionHeatmap.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, nextTick, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { alpha } from '@/scripts/color.js'; import { initChart } from '@/scripts/init-chart.js'; @@ -75,7 +75,7 @@ async function renderChart() { await nextTick(); - const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300'; + const color = store.state.darkMode ? '#b4e900' : '#86b300'; const getYYYYMMDD = (date: Date) => { const y = date.getFullYear().toString().padStart(2, '0'); diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue index d41793b0fa..13cee1b1cd 100644 --- a/packages/frontend/src/components/MkRetentionLineChart.vue +++ b/packages/frontend/src/components/MkRetentionLineChart.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, shallowRef } from 'vue'; import { Chart } from 'chart.js'; import tinycolor from 'tinycolor2'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; import { alpha } from '@/scripts/color.js'; @@ -42,7 +42,7 @@ const getDate = (ymd: string) => { onMounted(async () => { let raw = await misskeyApi('retention', { }); - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent')); const color = accent.toHex(); diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 7bae240ddd..3cc9b69341 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-if="paginationQuery" ref="tlComponent" :pagination="paginationQuery" - :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" + :noGap="!prefer.s.showGapBetweenNotesInTimeline" @queue="emit('queue', $event)" @status="prComponent?.setDisabled($event)" /> @@ -20,14 +20,14 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import type { BasicTimelineType } from '@/timelines.js'; +import type { Paging } from '@/components/MkPagination.vue'; import MkNotes from '@/components/MkNotes.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; import { $i } from '@/account.js'; import { instance } from '@/instance.js'; -import { defaultStore } from '@/store.js'; -import type { Paging } from '@/components/MkPagination.vue'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; @@ -239,7 +239,7 @@ function updatePaginationQuery() { } function refreshEndpointAndChannel() { - if (!defaultStore.state.disableStreamingTimeline) { + if (!prefer.s.disableStreamingTimeline) { disconnectChannel(); connectChannel(); } diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue index e256640649..ac795e312c 100644 --- a/packages/frontend/src/components/MkToast.vue +++ b/packages/frontend/src/components/MkToast.vue @@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_toast_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_toast_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_toast_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_toast_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_toast_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_toast_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_toast_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_toast_leaveTo : ''" appear @afterLeave="emit('closed')" > <div v-if="showing" class="_acrylic" :class="$style.root" :style="{ zIndex }"> @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref } from 'vue'; import * as os from '@/os.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; defineProps<{ message: string; diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue index 10365d29b1..c30ca1ffe6 100644 --- a/packages/frontend/src/components/MkTooltip.vue +++ b/packages/frontend/src/components/MkTooltip.vue @@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_tooltip_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_tooltip_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_tooltip_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_tooltip_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_tooltip_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_tooltip_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_tooltip_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_tooltip_leaveTo : ''" appear @afterLeave="emit('closed')" > <div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }"> @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { nextTick, onMounted, onUnmounted, shallowRef } from 'vue'; import * as os from '@/os.js'; import { calcPopupPosition } from '@/scripts/popup-position.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ showing: boolean; diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 063e5dcad2..850dd2d257 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin" scrolling="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }" - :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`" + :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`" ></iframe> </div> <div :class="$style.action"> @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div v-else> <component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> - <div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="defaultStore.state.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`"> + <div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="prefer.s.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`"> </div> <article :class="$style.body"> <header :class="$style.header"> @@ -92,7 +92,8 @@ import * as os from '@/os.js'; import { deviceKind } from '@/scripts/device-kind.js'; import MkButton from '@/components/MkButton.vue'; import { transformPlayerUrl } from '@/scripts/player-url-transform.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; +import { prefer } from '@/preferences.js'; type SummalyResult = Awaited<ReturnType<typeof summaly>>; diff --git a/packages/frontend/src/components/MkUrlPreviewPopup.vue b/packages/frontend/src/components/MkUrlPreviewPopup.vue index e972973dba..fd36d6a82b 100644 --- a/packages/frontend/src/components/MkUrlPreviewPopup.vue +++ b/packages/frontend/src/components/MkUrlPreviewPopup.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }"> - <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')"> + <Transition :name="prefer.s.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')"> <MkUrlPreview v-if="showing" class="_popup _shadow" :url="url" :showActions="false"/> </Transition> </div> @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, ref } from 'vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import * as os from '@/os.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ showing: boolean; diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index 0164515a8a..9abf61e893 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_panel" :class="$style.root"> - <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div> + <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div> <MkAvatar :class="$style.avatar" :user="user" indicator/> <div :class="$style.title"> <MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> @@ -42,7 +42,7 @@ import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; defineProps<{ user: Misskey.entities.UserDetailed; diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index 740202f28b..4027893a8e 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -5,15 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_popup_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_popup_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_popup_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_popup_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_popup_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_popup_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_popup_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_popup_leaveTo : ''" appear @afterLeave="emit('closed')" > <div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }"> <div v-if="user != null"> - <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"> + <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"> <span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span> </div> <svg viewBox="0 0 128 128" :class="$style.avatarBack"> @@ -64,7 +64,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import number from '@/filters/number.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { $i } from '@/account.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index 1e93d9dbea..2c5757e24e 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -68,7 +68,7 @@ import MkInput from '@/components/MkInput.vue'; import FormSplit from '@/components/form/split.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import { instance } from '@/instance.js'; @@ -128,10 +128,10 @@ async function ok() { dialogEl.value?.close(); // 最近使ったユーザー更新 - let recents = defaultStore.state.recentlyUsedUsers; + let recents = store.state.recentlyUsedUsers; recents = recents.filter(x => x !== selected.value?.id); recents.unshift(selected.value.id); - defaultStore.set('recentlyUsedUsers', recents.splice(0, 16)); + store.set('recentlyUsedUsers', recents.splice(0, 16)); } function cancel() { @@ -141,7 +141,7 @@ function cancel() { onMounted(() => { misskeyApi('users/show', { - userIds: defaultStore.state.recentlyUsedUsers, + userIds: store.state.recentlyUsedUsers, }).then(foundUsers => { let _users = foundUsers; _users = _users.filter((u) => { diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index b7261129ef..cba6f6fd90 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -139,7 +139,7 @@ import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import { host } from '@@/js/config.js'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import * as os from '@/os.js'; const emit = defineEmits<{ @@ -149,10 +149,10 @@ const emit = defineEmits<{ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); // eslint-disable-next-line vue/no-setup-props-reactivity-loss -const page = ref(defaultStore.state.accountSetupWizard); +const page = ref(store.state.accountSetupWizard); watch(page, () => { - defaultStore.set('accountSetupWizard', page.value); + store.set('accountSetupWizard', page.value); }); async function close(skip: boolean) { @@ -165,11 +165,11 @@ async function close(skip: boolean) { } dialog.value?.close(); - defaultStore.set('accountSetupWizard', -1); + store.set('accountSetupWizard', -1); } function setupComplete() { - defaultStore.set('accountSetupWizard', -1); + store.set('accountSetupWizard', -1); dialog.value?.close(); } @@ -194,7 +194,7 @@ async function later(later: boolean) { } dialog.value?.close(); - defaultStore.set('accountSetupWizard', 0); + store.set('accountSetupWizard', 0); } </script> diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue index d098dad9a1..ac48b11c3f 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue @@ -18,7 +18,7 @@ import { Chart } from 'chart.js'; import gradient from 'chartjs-plugin-gradient'; import tinycolor from 'tinycolor2'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; import { initChart } from '@/scripts/init-chart.js'; @@ -59,7 +59,7 @@ async function renderChart() { await nextTick(); - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const computedStyle = getComputedStyle(document.documentElement); const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(); diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index 2953f656d4..4759d217e8 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_window_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_window_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_window_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_window_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_window_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_window_leaveTo : ''" appear @afterLeave="emit('closed')" > @@ -58,7 +58,7 @@ import type { MenuItem } from '@/types/menu.js'; import contains from '@/scripts/contains.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; type WindowButton = { title: string; diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue index 1122976436..1c069d65fc 100644 --- a/packages/frontend/src/components/MkYouTubePlayer.vue +++ b/packages/frontend/src/components/MkYouTubePlayer.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div class="poamfof"> - <Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in"> + <Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in"> <div v-if="player.url && (player.url.startsWith('http://') || player.url.startsWith('https://'))" class="player"> <iframe v-if="!fetching" :src="transformPlayerUrl(player.url)" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe> </div> @@ -25,10 +25,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import MkWindow from '@/components/MkWindow.vue'; import { versatileLang } from '@@/js/intl-const.js'; +import MkWindow from '@/components/MkWindow.vue'; import { transformPlayerUrl } from '@/scripts/player-url-transform.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ url: string; diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index 08a78c8d81..4f9bfd02bd 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -45,9 +45,10 @@ import { url as local, host } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import MkButton from '@/components/MkButton.vue'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import * as os from '@/os.js'; import { $i } from '@/account.js'; +import { prefer } from '@/preferences.js'; type Ad = (typeof instance)['ads'][number]; @@ -66,7 +67,7 @@ const choseAd = (): Ad | null => { return props.specify; } - const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? { + const allAds = instance.ads.map(ad => store.state.mutedAds.includes(ad.id) ? { ...ad, ratio: 0, } : ad); @@ -107,12 +108,12 @@ const chosen = ref(choseAd()); const self = computed(() => chosen.value?.url.startsWith(local)); -const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null)); +const shouldHide = ref(!prefer.s.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null)); function reduceFrequency(): void { if (chosen.value == null) return; - if (defaultStore.state.mutedAds.includes(chosen.value.id)) return; - defaultStore.push('mutedAds', chosen.value.id); + if (store.state.mutedAds.includes(chosen.value.id)) return; + store.push('mutedAds', chosen.value.id); os.success(); chosen.value = choseAd(); showMenu.value = false; diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 35c07bc80c..71c309fd89 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -48,11 +48,10 @@ import MkA from './MkA.vue'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { acct, userPage } from '@/filters/user.js'; import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; -const animation = ref(defaultStore.state.animation); -const squareAvatars = ref(defaultStore.state.squareAvatars); -const useBlurEffect = ref(defaultStore.state.useBlurEffect); +const animation = ref(prefer.s.animation); +const squareAvatars = ref(prefer.s.squareAvatars); const props = withDefaults(defineProps<{ user: Misskey.entities.User; @@ -75,7 +74,7 @@ const emit = defineEmits<{ (ev: 'click', v: MouseEvent): void; }>(); -const showDecoration = props.forceShowDecoration || defaultStore.state.showAvatarDecorations; +const showDecoration = props.forceShowDecoration || prefer.s.showAvatarDecorations; const bound = computed(() => props.link ? { to: userPage(props.user), target: props.target } @@ -83,7 +82,7 @@ const bound = computed(() => props.link const url = computed(() => { if (props.user.avatarUrl == null) return null; - if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl); + if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl); return props.user.avatarUrl; }); @@ -93,7 +92,7 @@ function onClick(ev: MouseEvent): void { } function getDecorationUrl(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { - if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(decoration.url); + if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(decoration.url); return decoration.url; } diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index ec1d859080..82c5e2a1dc 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -28,7 +28,6 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, defineAsyncComponent, inject, ref } from 'vue'; import type { MenuItem } from '@/types/menu.js'; import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js'; -import { defaultStore } from '@/store.js'; import { customEmojisMap } from '@/custom-emojis.js'; import * as os from '@/os.js'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; @@ -37,6 +36,7 @@ import * as sound from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue'; import { $i } from '@/account.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ name: string; @@ -77,7 +77,7 @@ const url = computed(() => { false, true, ); - return defaultStore.reactiveState.disableShowingAnimatedImages.value + return prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(proxied) : proxied; }); diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index f0acd3bc27..a00004fdb8 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -12,12 +12,12 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, inject } from 'vue'; import { colorizeEmoji, getEmojiName } from '@@/js/emojilist.js'; import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@@/js/emoji-base.js'; -import { defaultStore } from '@/store.js'; +import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as sound from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ emoji: string; @@ -27,9 +27,9 @@ const props = defineProps<{ const react = inject<((name: string) => void) | null>('react', null); -const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; +const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; -const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native'); +const useOsNativeEmojis = computed(() => prefer.s.emojiStyle === 'native'); const url = computed(() => char2path(props.emoji)); const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji)); diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue index c594cc752b..b07e0775a3 100644 --- a/packages/frontend/src/components/global/MkError.vue +++ b/packages/frontend/src/components/global/MkError.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear> +<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear> <div :class="$style.root"> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p> @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { serverErrorImageUrl } from '@/instance.js'; const emit = defineEmits<{ diff --git a/packages/frontend/src/components/global/MkFooterSpacer.vue b/packages/frontend/src/components/global/MkFooterSpacer.vue index 1a75855fa1..d08645aaf3 100644 --- a/packages/frontend/src/components/global/MkFooterSpacer.vue +++ b/packages/frontend/src/components/global/MkFooterSpacer.vue @@ -4,11 +4,11 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[$style.spacer, defaultStore.reactiveState.darkMode.value ? $style.dark : $style.light]"></div> +<div :class="[$style.spacer, store.reactiveState.darkMode.value ? $style.dark : $style.light]"></div> </template> <script lang="ts" setup> -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts index 48d7e34d76..337e326ccd 100644 --- a/packages/frontend/src/components/global/MkMfm.ts +++ b/packages/frontend/src/components/global/MkMfm.ts @@ -3,11 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { h, provide } from 'vue'; -import type { VNode, SetupContext } from 'vue'; +import { h } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import { host } from '@@/js/config.js'; +import type { VNode, SetupContext } from 'vue'; +import type { MkABehavior } from '@/components/global/MkA.vue'; import MkUrl from '@/components/global/MkUrl.vue'; import MkTime from '@/components/global/MkTime.vue'; import MkLink from '@/components/MkLink.vue'; @@ -19,8 +20,7 @@ import MkCodeInline from '@/components/MkCodeInline.vue'; import MkGoogle from '@/components/MkGoogle.vue'; import MkSparkle from '@/components/MkSparkle.vue'; import MkA from '@/components/global/MkA.vue'; -import type { MkABehavior } from '@/components/global/MkA.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; function safeParseFloat(str: unknown): number | null { if (typeof str !== 'string' || str === '') return null; @@ -81,7 +81,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven return c.match(/^[0-9a-f]{3,6}$/i) ? c : null; }; - const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm; + const useAnim = prefer.s.advancedMfm && prefer.s.animatedMfm; /** * Gen Vue Elements from MFM AST @@ -188,17 +188,17 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'x2': { return h('span', { - class: defaultStore.state.advancedMfm ? 'mfm-x2' : '', + class: prefer.s.advancedMfm ? 'mfm-x2' : '', }, genEl(token.children, scale * 2)); } case 'x3': { return h('span', { - class: defaultStore.state.advancedMfm ? 'mfm-x3' : '', + class: prefer.s.advancedMfm ? 'mfm-x3' : '', }, genEl(token.children, scale * 3)); } case 'x4': { return h('span', { - class: defaultStore.state.advancedMfm ? 'mfm-x4' : '', + class: prefer.s.advancedMfm ? 'mfm-x4' : '', }, genEl(token.children, scale * 4)); } case 'font': { @@ -241,14 +241,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven break; } case 'position': { - if (!defaultStore.state.advancedMfm) break; + if (!prefer.s.advancedMfm) break; const x = safeParseFloat(token.props.args.x) ?? 0; const y = safeParseFloat(token.props.args.y) ?? 0; style = `transform: translateX(${x}em) translateY(${y}em);`; break; } case 'scale': { - if (!defaultStore.state.advancedMfm) { + if (!prefer.s.advancedMfm) { style = ''; break; } diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index aaef8b8fca..8a5e556293 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -8,13 +8,13 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.tabsInner"> <button v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title" - class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: defaultStore.reactiveState.animation.value }]" + class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: prefer.s.animation }]" @mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)" > <div :class="$style.tabInner"> <i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i> <div - v-if="!t.iconOnly || (!defaultStore.reactiveState.animation.value && t.key === tab)" + v-if="!t.iconOnly || (!prefer.s.animation && t.key === tab)" :class="$style.tabTitle" > {{ t.title }} @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div ref="tabHighlightEl" - :class="[$style.tabHighlight, { [$style.animate]: defaultStore.reactiveState.animation.value }]" + :class="[$style.tabHighlight, { [$style.animate]: prefer.s.animation }]" ></div> </div> </template> @@ -41,20 +41,20 @@ export type Tab = { onClick?: (ev: MouseEvent) => void; } & ( | { - iconOnly?: false; - title: string; - icon?: string; - } + iconOnly?: false; + title: string; + icon?: string; + } | { - iconOnly: true; - icon: string; - } + iconOnly: true; + icon: string; + } ); </script> <script lang="ts" setup> import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ tabs?: Tab[]; diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index 3ab3d10a40..2e963daa27 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <KeepAlive - :max="defaultStore.state.numberOfPageCache" + :max="prefer.s.numberOfPageCache" :exclude="pageCacheController" > <Suspense :timeout="0"> @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue'; import type { IRouter, Resolved, RouteDef } from '@/nirax.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { globalEvents } from '@/events.js'; import MkLoadingPage from '@/pages/_loading_.vue'; diff --git a/packages/frontend/src/components/global/SearchMarker.vue b/packages/frontend/src/components/global/SearchMarker.vue index c5ec626cf4..66a78cb7fd 100644 --- a/packages/frontend/src/components/global/SearchMarker.vue +++ b/packages/frontend/src/components/global/SearchMarker.vue @@ -36,7 +36,7 @@ const rootEl = useTemplateRef('root'); const rootElMutationObserver = new MutationObserver(() => { checkChildren(); }); -const injectedSearchMarkerId = inject<Ref<string | null>>('inAppSearchMarkerId'); +const injectedSearchMarkerId = inject<Ref<string | null> | null>('inAppSearchMarkerId', null); const searchMarkerId = computed(() => injectedSearchMarkerId?.value ?? window.location.hash.slice(1)); const highlighted = ref(props.markerId === searchMarkerId.value); 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(); +} diff --git a/packages/frontend/src/directives/click-anime.ts b/packages/frontend/src/directives/click-anime.ts index 60242837f2..c34f351fb3 100644 --- a/packages/frontend/src/directives/click-anime.ts +++ b/packages/frontend/src/directives/click-anime.ts @@ -4,11 +4,11 @@ */ import type { Directive } from 'vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; export default { mounted(el: HTMLElement, binding, vn) { - if (!defaultStore.state.animation) return; + if (!prefer.s.animation) return; const target = el.children[0]; diff --git a/packages/frontend/src/directives/ripple.ts b/packages/frontend/src/directives/ripple.ts index 99845c57c3..614cd37011 100644 --- a/packages/frontend/src/directives/ripple.ts +++ b/packages/frontend/src/directives/ripple.ts @@ -4,14 +4,14 @@ */ import MkRippleEffect from '@/components/MkRippleEffect.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { popup } from '@/os.js'; export default { mounted(el, binding, vn) { // 明示的に false であればバインドしない if (binding.value === false) return; - if (!defaultStore.state.animation) return; + if (!prefer.s.animation) return; el.addEventListener('click', () => { const rect = el.getBoundingClientRect(); diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 9c8863f863..3977edb91b 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -19,7 +19,6 @@ export type Keys = ( 'drafts' | 'hashtags' | 'wallpaper' | - 'theme' | 'colorScheme' | 'useSystemFont' | 'fontSize' | @@ -28,13 +27,17 @@ export type Keys = ( 'locale' | 'localeVersion' | 'theme' | + 'themeId' | 'customCss' | 'message_drafts' | 'scratchpad' | 'debug' | + 'preferences' | + 'latestPreferencesUpdate' | + 'hidePreferencesRestoreSuggestion' | `miux:${string}` | `ui:folder:${string}` | - `themes:${string}` | + `themes:${string}` | // DEPRECATED `aiscript:${string}` | 'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~) 'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~); diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 6e48366092..2142432953 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -6,15 +6,15 @@ // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue'; -import type { Component, Ref } from 'vue'; import { EventEmitter } from 'eventemitter3'; import * as Misskey from 'misskey-js'; +import type { Component, Ref } from 'vue'; import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/scripts/form.js'; import type { MenuItem } from '@/types/menu.js'; import type { PostFormProps } from '@/types/post-form.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { i18n } from '@/i18n.js'; import MkPostFormDialog from '@/components/MkPostFormDialog.vue'; import MkWaitingDialog from '@/components/MkWaitingDialog.vue'; @@ -626,7 +626,7 @@ export async function selectRole(params: { }): Promise< { canceled: true; result: undefined; } | { canceled: false; result: Misskey.entities.Role[] } -> { + > { return new Promise((resolve) => { popup(defineAsyncComponent(() => import('@/components/MkRoleSelectDialog.vue')), params, { done: roles => { @@ -699,8 +699,8 @@ export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | n export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> { if ( - defaultStore.state.contextMenu === 'native' || - (defaultStore.state.contextMenu === 'appWithShift' && !ev.shiftKey) + prefer.s.contextMenu === 'native' || + (prefer.s.contextMenu === 'appWithShift' && !ev.shiftKey) ) { return Promise.resolve(); } diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue index f09a8e4285..9e3a6777f1 100644 --- a/packages/frontend/src/pages/_error_.vue +++ b/packages/frontend/src/pages/_error_.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkLoading v-if="!loaded"/> -<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear> +<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear> <div v-show="loaded" :class="$style.root"> <img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/> <div class="_gaps"> @@ -27,15 +27,15 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; +import { version } from '@@/js/config.js'; import MkButton from '@/components/MkButton.vue'; import MkLink from '@/components/MkLink.vue'; -import { version } from '@@/js/config.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { serverErrorImageUrl } from '@/instance.js'; const props = withDefaults(defineProps<{ diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 29772ae00a..6e5b52ea8a 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -143,7 +143,7 @@ import MkInfo from '@/components/MkInfo.vue'; import { physics } from '@/scripts/physics.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import * as os from '@/os.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; @@ -406,7 +406,7 @@ const easterEggEngine = ref<{ stop: () => void } | null>(null); const containerEl = shallowRef<HTMLElement>(); function iconLoaded() { - const emojis = defaultStore.state.reactions; + const emojis = store.state.reactions; const containerWidth = containerEl.value.offsetWidth; for (let i = 0; i < 32; i++) { easterEggEmojis.value.push({ diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue index 22173bb888..c7a45a502e 100644 --- a/packages/frontend/src/pages/admin/abuses.vue +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton> </div> - <MkInfo v-if="!defaultStore.reactiveState.abusesTutorial.value" closable @close="closeTutorial()"> + <MkInfo v-if="!store.reactiveState.abusesTutorial.value" closable @close="closeTutorial()"> {{ i18n.ts._abuseUserReport.resolveTutorial }} </MkInfo> @@ -68,7 +68,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; const reports = shallowRef<InstanceType<typeof MkPagination>>(); @@ -93,7 +93,7 @@ function resolved(reportId) { } function closeTutorial() { - defaultStore.set('abusesTutorial', false); + store.set('abusesTutorial', false); } const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue index d6ee8ea49c..6537b844c3 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue @@ -78,6 +78,11 @@ SPDX-License-Identifier: AGPL-3.0-only /* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as Misskey from 'misskey-js'; import { onMounted, ref, useCssModule } from 'vue'; +import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; +import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; +import type { DroppedFile } from '@/scripts/file-drop.js'; +import type { GridSetting } from '@/components/grid/grid.js'; +import type { GridRow } from '@/components/grid/row.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { emptyStrToEmptyArray, @@ -88,7 +93,6 @@ import MkGrid from '@/components/grid/MkGrid.vue'; import { i18n } from '@/i18n.js'; import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; -import { defaultStore } from '@/store.js'; import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; @@ -99,11 +103,7 @@ import { extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js'; -import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; -import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; -import type { DroppedFile } from '@/scripts/file-drop.js'; -import type { GridSetting } from '@/components/grid/grid.js'; -import type { GridRow } from '@/components/grid/row.js'; +import { prefer } from '@/preferences.js'; const MAXIMUM_EMOJI_REGISTER_COUNT = 100; @@ -244,8 +244,8 @@ function setupGrid(): GridSetting { const uploadFolders = ref<FolderItem[]>([]); const gridItems = ref<GridItem[]>([]); -const selectedFolderId = ref(defaultStore.state.uploadFolder); -const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading); +const selectedFolderId = ref(prefer.s.uploadFolder); +const keepOriginalUploading = ref(prefer.s.keepOriginalUploading); const directoryToCategory = ref<boolean>(false); const registerButtonDisabled = ref<boolean>(false); const requestLogs = ref<RequestLogItem[]>([]); diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue index 79dd6fd5fd..1a72f9050c 100644 --- a/packages/frontend/src/pages/admin/overview.active-users.vue +++ b/packages/frontend/src/pages/admin/overview.active-users.vue @@ -17,7 +17,7 @@ import { onMounted, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import gradient from 'chartjs-plugin-gradient'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; import { initChart } from '@/scripts/init-chart.js'; @@ -54,7 +54,7 @@ async function renderChart() { const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' }); - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const colorRead = '#3498db'; const colorWrite = '#2ecc71'; diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue index 570fcddc07..197f792755 100644 --- a/packages/frontend/src/pages/admin/overview.ap-requests.vue +++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue @@ -27,7 +27,7 @@ import isChromatic from 'chromatic'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { alpha } from '@/scripts/color.js'; import { initChart } from '@/scripts/init-chart.js'; @@ -68,7 +68,7 @@ onMounted(async () => { const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' }); - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const succColor = '#87e000'; const failColor = '#ff4400'; diff --git a/packages/frontend/src/pages/admin/overview.instances.vue b/packages/frontend/src/pages/admin/overview.instances.vue index 292e2e1dbc..67c0297dec 100644 --- a/packages/frontend/src/pages/admin/overview.instances.vue +++ b/packages/frontend/src/pages/admin/overview.instances.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> - <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in"> + <Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in"> <MkLoading v-if="fetching"/> <div v-else :class="$style.instances"> <MkA v-for="(instance, i) in instances" :key="instance.id" v-tooltip.mfm.noDelay="`${instance.name}\n${instance.host}\n${instance.softwareName} ${instance.softwareVersion}`" :to="`/instance-info/${instance.host}`" :class="$style.instance"> @@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import * as Misskey from 'misskey-js'; import { useInterval } from '@@/js/use-interval.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const instances = ref<Misskey.entities.FederationInstance[]>([]); const fetching = ref(true); diff --git a/packages/frontend/src/pages/admin/overview.moderators.vue b/packages/frontend/src/pages/admin/overview.moderators.vue index f0691534c8..4d503c0035 100644 --- a/packages/frontend/src/pages/admin/overview.moderators.vue +++ b/packages/frontend/src/pages/admin/overview.moderators.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> - <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in"> + <Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in"> <MkLoading v-if="fetching"/> <div v-else :class="$style.root" class="_panel"> <MkA v-for="user in moderators" :key="user.id" class="user" :to="`/admin/user/${user.id}`"> @@ -18,9 +18,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref } from 'vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import * as Misskey from 'misskey-js'; -import { defaultStore } from '@/store.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { prefer } from '@/preferences.js'; const moderators = ref<Misskey.entities.UserDetailed[] | null>(null); const fetching = ref(true); diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue index 2efc17c888..7b83641fbd 100644 --- a/packages/frontend/src/pages/admin/overview.queue.chart.vue +++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, shallowRef } from 'vue'; import { Chart } from 'chart.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; import { alpha } from '@/scripts/color.js'; @@ -67,7 +67,7 @@ const color = '?' as never; onMounted(() => { - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; chartInstance = new Chart(chartEl.value, { type: 'line', diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue index 222e9f4673..72b93ee97c 100644 --- a/packages/frontend/src/pages/admin/overview.stats.vue +++ b/packages/frontend/src/pages/admin/overview.stats.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> - <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in"> + <Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in"> <MkLoading v-if="fetching"/> <div v-else :class="$style.root"> <div class="item _panel users"> @@ -68,7 +68,7 @@ import MkNumberDiff from '@/components/MkNumberDiff.vue'; import MkNumber from '@/components/MkNumber.vue'; import { i18n } from '@/i18n.js'; import { customEmojis } from '@/custom-emojis.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const stats = ref<Misskey.entities.StatsResponse | null>(null); const usersComparedToThePrevDay = ref<number>(); diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue index 8c9d7a8197..8de4f298ce 100644 --- a/packages/frontend/src/pages/admin/overview.users.vue +++ b/packages/frontend/src/pages/admin/overview.users.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root"> - <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in"> + <Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in"> <MkLoading v-if="fetching"/> <div v-else class="users"> <MkA v-for="(user, i) in newUsers" :key="user.id" :to="`/admin/user/${user.id}`" class="user"> @@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import * as Misskey from 'misskey-js'; import { useInterval } from '@@/js/use-interval.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const newUsers = ref<Misskey.entities.UserDetailed[] | null>(null); const fetching = ref(true); diff --git a/packages/frontend/src/pages/admin/queue.chart.chart.vue b/packages/frontend/src/pages/admin/queue.chart.chart.vue index cc18898172..debb48a15a 100644 --- a/packages/frontend/src/pages/admin/queue.chart.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.chart.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, shallowRef } from 'vue'; import { Chart } from 'chart.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; import { alpha } from '@/scripts/color.js'; @@ -67,7 +67,7 @@ const color = '?' as never; onMounted(() => { - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; chartInstance = new Chart(chartEl.value, { type: 'line', diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue index 56c10fb292..b8903a8ea6 100644 --- a/packages/frontend/src/pages/announcement.vue +++ b/packages/frontend/src/pages/announcement.vue @@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="800"> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.fadeEnterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.fadeLeaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.fadeEnterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.fadeLeaveTo : ''" mode="out-in" > <div v-if="announcement" :key="announcement.id" class="_panel" :class="$style.announcement"> @@ -56,7 +56,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { $i, updateAccountPartial } from '@/account.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ announcementId: string; diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 4a91165d50..d333204ff8 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo> <!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる --> - <MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/> + <MkPostForm v-if="$i && prefer.r.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/> <MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/> </div> @@ -75,6 +75,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { url } from '@@/js/config.js'; +import type { PageHeaderItem } from '@/types/page-header.js'; import MkPostForm from '@/components/MkPostForm.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import XChannelFollowButton from '@/components/MkChannelFollowButton.vue'; @@ -85,16 +87,14 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { deviceKind } from '@/scripts/device-kind.js'; import MkNotes from '@/components/MkNotes.vue'; -import { url } from '@@/js/config.js'; import { favoritedChannelsCache } from '@/cache.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import MkNote from '@/components/MkNote.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; -import type { PageHeaderItem } from '@/types/page-header.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { notesSearchAvailable } from '@/scripts/check-permissions.js'; diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue index 10099e6291..55c984e1bf 100644 --- a/packages/frontend/src/pages/drop-and-fusion.game.vue +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove"> - <img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/> + <img v-if="store.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/> <img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/> <canvas ref="canvasEl" :class="$style.canvas"/> <Transition @@ -195,6 +195,8 @@ import { computed, onDeactivated, onMounted, onUnmounted, ref, shallowRef, watch import * as Matter from 'matter-js'; import * as Misskey from 'misskey-js'; import { DropAndFusionGame } from 'misskey-bubble-game'; +import { useInterval } from '@@/js/use-interval.js'; +import { apiUrl } from '@@/js/config.js'; import type { Mono } from 'misskey-bubble-game'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; @@ -203,15 +205,14 @@ import MkNumber from '@/components/MkNumber.vue'; import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; import MkButton from '@/components/MkButton.vue'; import { claimAchievement } from '@/scripts/achievements.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@@/js/use-interval.js'; -import { apiUrl } from '@@/js/config.js'; import { $i } from '@/account.js'; import * as sound from '@/scripts/sound.js'; import MkRange from '@/components/MkRange.vue'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { prefer } from '@/preferences.js'; type FrontendMonoDefinition = { id: string; @@ -586,8 +587,8 @@ const showConfig = ref(false); const replaying = ref(false); const replayPlaybackRate = ref(1); const currentFrame = ref(0); -const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume); -const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume); +const bgmVolume = ref(prefer.s['game.dropAndFusion'].bgmVolume); +const sfxVolume = ref(prefer.s['game.dropAndFusion'].sfxVolume); watch(replayPlaybackRate, (newValue) => { game.replayPlaybackRate = newValue; @@ -623,7 +624,7 @@ function loadMonoTextures() { if (renderer.textures[mono.img]) return; let src = mono.img; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (monoTextureUrls[mono.img]) { src = monoTextureUrls[mono.img]; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -649,7 +650,6 @@ function loadMonoTextures() { function getTextureImageUrl(mono: Mono) { const def = monoDefinitions.value.find(x => x.id === mono.id)!; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (monoTextureUrls[def.img]) { return monoTextureUrls[def.img]; @@ -853,13 +853,13 @@ function exportLog() { } function updateSettings< - K extends keyof typeof defaultStore.state.dropAndFusion, - V extends typeof defaultStore.state.dropAndFusion[K], + K extends keyof typeof prefer.s['game.dropAndFusion'], + V extends typeof prefer.s['game.dropAndFusion'][K], >(key: K, value: V) { const changes: { [P in K]?: V } = {}; changes[key] = value; - defaultStore.set('dropAndFusion', { - ...defaultStore.state.dropAndFusion, + prefer.set('game.dropAndFusion', { + ...prefer.s['game.dropAndFusion'], ...changes, }); } @@ -909,8 +909,8 @@ function getGameImageDriveFile() { formData.append('name', `bubble-game-${Date.now()}.png`); formData.append('isSensitive', 'false'); formData.append('i', $i.token); - if (defaultStore.state.uploadFolder) { - formData.append('folderId', defaultStore.state.uploadFolder); + if (prefer.s.uploadFolder) { + formData.append('folderId', prefer.s.uploadFolder); } window.fetch(apiUrl + '/drive/files/create', { diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index 6294a3f4a2..532ec87dac 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700"> - <Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in"> + <Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in"> <div v-if="flash" :key="flash.id"> - <Transition :name="defaultStore.state.animation ? 'zoom' : ''" mode="out-in"> + <Transition :name="prefer.s.animation ? 'zoom' : ''" mode="out-in"> <div v-if="started" :class="$style.started"> <div class="main _panel"> <MkAsUi v-if="root" :component="root" :components="components"/> @@ -79,7 +79,7 @@ import { registerAsUiLib } from '@/scripts/aiscript/ui.js'; import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; import MkFolder from '@/components/MkFolder.vue'; import MkCode from '@/components/MkCode.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { $i } from '@/account.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index feb4c60611..7e1b7be16c 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="1000" :marginMin="16" :marginMax="32"> <div class="_root"> - <Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in"> + <Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in"> <div v-if="post" class="rkxwuolj"> <div class="files"> <div v-for="file in post.files" :key="file.id" class="file"> @@ -65,6 +65,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, ref, defineAsyncComponent } from 'vue'; import * as Misskey from 'misskey-js'; +import { url } from '@@/js/config.js'; +import type { MenuItem } from '@/types/menu.js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; @@ -72,15 +74,13 @@ import MkContainer from '@/components/MkContainer.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; -import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { $i } from '@/account.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { useRouter } from '@/router/supplier.js'; -import type { MenuItem } from '@/types/menu.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue index 58f8b865bb..423bd611e6 100644 --- a/packages/frontend/src/pages/install-extensions.vue +++ b/packages/frontend/src/pages/install-extensions.vue @@ -45,18 +45,18 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed, onActivated, onDeactivated, nextTick } from 'vue'; +import type { Extension } from '@/components/MkExtensionInstaller.vue'; +import type { AiScriptPluginMeta } from '@/plugin.js'; import MkLoading from '@/components/global/MkLoading.vue'; import MkExtensionInstaller from '@/components/MkExtensionInstaller.vue'; -import type { Extension } from '@/components/MkExtensionInstaller.vue'; import MkButton from '@/components/MkButton.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkUrl from '@/components/global/MkUrl.vue'; import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js'; -import type { AiScriptPluginMeta } from '@/scripts/install-plugin.js'; -import { parseThemeCode, installTheme } from '@/scripts/install-theme.js'; +import { parsePluginMeta, installPlugin } from '@/plugin.js'; +import { parseThemeCode, installTheme } from '@/scripts/theme.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index 69e404bd85..acedeeeb7d 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -67,15 +67,15 @@ import MkFolder from '@/components/MkFolder.vue'; import MkInput from '@/components/MkInput.vue'; import { userListsCache } from '@/cache.js'; import { signinRequired } from '@/account.js'; -import { defaultStore } from '@/store.js'; import MkPagination from '@/components/MkPagination.vue'; import { mainRouter } from '@/router/main.js'; +import { prefer } from '@/preferences.js'; const $i = signinRequired(); const { enableInfiniteScroll, -} = defaultStore.reactiveState; +} = prefer.r; const props = defineProps<{ listId: string; diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index 0791c1343b..eaeef2e566 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="800"> <div> - <Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in"> + <Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in"> <div v-if="note"> <div v-if="showNext" class="_margin"> <MkNotes class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/> @@ -61,7 +61,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import { dateString } from '@/filters/date.js'; import MkClipPreview from '@/components/MkClipPreview.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import { getAppearNote } from '@/scripts/get-appear-note.js'; import { serverContext, assertServerContext } from '@/server-context.js'; diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index d9ad7babb7..3444ef49aa 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="800"> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.fadeEnterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.fadeLeaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.fadeEnterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.fadeLeaveTo : ''" mode="out-in" > <div v-if="page" :key="page.id" class="_gaps"> @@ -100,11 +100,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, ref, defineAsyncComponent } from 'vue'; import * as Misskey from 'misskey-js'; +import { url } from '@@/js/config.js'; +import type { MenuItem } from '@/types/menu.js'; import XPage from '@/components/page/page.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { url } from '@@/js/config.js'; import MkMediaImage from '@/components/MkMediaImage.vue'; import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; @@ -113,7 +114,7 @@ import MkPagination from '@/components/MkPagination.vue'; import MkPagePreview from '@/components/MkPagePreview.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { pageViewInterruptors, defaultStore } from '@/store.js'; +import { pageViewInterruptors } from '@/store.js'; import { deepClone } from '@/scripts/clone.js'; import { $i } from '@/account.js'; import { isSupportShare } from '@/scripts/navigator.js'; @@ -121,7 +122,7 @@ import { instance } from '@/instance.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { useRouter } from '@/router/supplier.js'; -import type { MenuItem } from '@/types/menu.js'; +import { prefer } from '@/preferences.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/settings/accessibility.vue b/packages/frontend/src/pages/settings/accessibility.vue index b703be1fe1..f464f728ff 100644 --- a/packages/frontend/src/pages/settings/accessibility.vue +++ b/packages/frontend/src/pages/settings/accessibility.vue @@ -8,49 +8,63 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <div class="_gaps_s"> <SearchMarker :keywords="['animation', 'motion', 'reduce']"> - <MkSwitch v-model="reduceAnimation"> - <template #label><SearchLabel>{{ i18n.ts.reduceUiAnimation }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="animation"> + <MkSwitch v-model="reduceAnimation"> + <template #label><SearchLabel>{{ i18n.ts.reduceUiAnimation }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif']"> - <MkSwitch v-model="disableShowingAnimatedImages"> - <template #label><SearchLabel>{{ i18n.ts.disableShowingAnimatedImages }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="disableShowingAnimatedImages"> + <MkSwitch v-model="disableShowingAnimatedImages"> + <template #label><SearchLabel>{{ i18n.ts.disableShowingAnimatedImages }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['mfm', 'enable', 'show', 'animated']"> - <MkSwitch v-model="animatedMfm"> - <template #label><SearchLabel>{{ i18n.ts.enableAnimatedMfm }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="animatedMfm"> + <MkSwitch v-model="animatedMfm"> + <template #label><SearchLabel>{{ i18n.ts.enableAnimatedMfm }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['swipe', 'horizontal', 'tab']"> - <MkSwitch v-model="enableHorizontalSwipe"> - <template #label><SearchLabel>{{ i18n.ts.enableHorizontalSwipe }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="enableHorizontalSwipe"> + <MkSwitch v-model="enableHorizontalSwipe"> + <template #label><SearchLabel>{{ i18n.ts.enableHorizontalSwipe }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['keep', 'screen', 'display', 'on']"> - <MkSwitch v-model="keepScreenOn"> - <template #label><SearchLabel>{{ i18n.ts.keepScreenOn }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="keepScreenOn"> + <MkSwitch v-model="keepScreenOn"> + <template #label><SearchLabel>{{ i18n.ts.keepScreenOn }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['native', 'system', 'video', 'audio', 'player', 'media']"> - <MkSwitch v-model="useNativeUIForVideoAudioPlayer"> - <template #label><SearchLabel>{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="useNativeUiForVideoAudioPlayer"> + <MkSwitch v-model="useNativeUiForVideoAudioPlayer"> + <template #label><SearchLabel>{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> </div> <SearchMarker :keywords="['contextmenu', 'system', 'native']"> - <MkSelect v-model="contextMenu"> - <template #label><SearchLabel>{{ i18n.ts._contextMenu.title }}</SearchLabel></template> - <option value="app">{{ i18n.ts._contextMenu.app }}</option> - <option value="appWithShift">{{ i18n.ts._contextMenu.appWithShift }}</option> - <option value="native">{{ i18n.ts._contextMenu.native }}</option> - </MkSelect> + <MkPreferenceContainer k="contextMenu"> + <MkSelect v-model="contextMenu"> + <template #label><SearchLabel>{{ i18n.ts._contextMenu.title }}</SearchLabel></template> + <option value="app">{{ i18n.ts._contextMenu.app }}</option> + <option value="appWithShift">{{ i18n.ts._contextMenu.appWithShift }}</option> + <option value="native">{{ i18n.ts._contextMenu.native }}</option> + </MkSelect> + </MkPreferenceContainer> </SearchMarker> </div> </SearchMarker> @@ -60,18 +74,19 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, ref, watch } from 'vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { reloadAsk } from '@/scripts/reload-ask.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue'; -const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v)); -const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm')); -const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages')); -const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn')); -const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe')); -const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer')); -const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu')); +const reduceAnimation = prefer.model('animation', v => !v, v => !v); +const animatedMfm = prefer.model('animatedMfm'); +const disableShowingAnimatedImages = prefer.model('disableShowingAnimatedImages'); +const keepScreenOn = prefer.model('keepScreenOn'); +const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe'); +const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer'); +const contextMenu = prefer.model('contextMenu'); watch([ keepScreenOn, diff --git a/packages/frontend/src/pages/settings/appearance.vue b/packages/frontend/src/pages/settings/appearance.vue index 465c2a38c2..b23f32aec4 100644 --- a/packages/frontend/src/pages/settings/appearance.vue +++ b/packages/frontend/src/pages/settings/appearance.vue @@ -10,73 +10,85 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <div class="_gaps_s"> <SearchMarker :keywords="['blur']"> - <MkSwitch v-model="useBlurEffect"> - <template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="useBlurEffect"> + <MkSwitch v-model="useBlurEffect"> + <template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['blur', 'modal']"> - <MkSwitch v-model="useBlurEffectForModal"> - <template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="useBlurEffectForModal"> + <MkSwitch v-model="useBlurEffectForModal"> + <template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail']"> - <MkSwitch v-model="highlightSensitiveMedia"> - <template #label><SearchLabel>{{ i18n.ts.highlightSensitiveMedia }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="highlightSensitiveMedia"> + <MkSwitch v-model="highlightSensitiveMedia"> + <template #label><SearchLabel>{{ i18n.ts.highlightSensitiveMedia }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['avatar', 'icon', 'square']"> - <MkSwitch v-model="squareAvatars"> - <template #label><SearchLabel>{{ i18n.ts.squareAvatars }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="squareAvatars"> + <MkSwitch v-model="squareAvatars"> + <template #label><SearchLabel>{{ i18n.ts.squareAvatars }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['avatar', 'icon', 'decoration', 'show']"> - <MkSwitch v-model="showAvatarDecorations"> - <template #label><SearchLabel>{{ i18n.ts.showAvatarDecorations }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="showAvatarDecorations"> + <MkSwitch v-model="showAvatarDecorations"> + <template #label><SearchLabel>{{ i18n.ts.showAvatarDecorations }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['note', 'timeline', 'gap']"> - <MkSwitch v-model="showGapBetweenNotesInTimeline"> - <template #label><SearchLabel>{{ i18n.ts.showGapBetweenNotesInTimeline }}</SearchLabel></template> - </MkSwitch> - </SearchMarker> - - <SearchMarker :keywords="['font', 'system', 'native']"> - <MkSwitch v-model="useSystemFont"> - <template #label><SearchLabel>{{ i18n.ts.useSystemFont }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="showGapBetweenNotesInTimeline"> + <MkSwitch v-model="showGapBetweenNotesInTimeline"> + <template #label><SearchLabel>{{ i18n.ts.showGapBetweenNotesInTimeline }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['effect', 'show']"> - <MkSwitch v-model="enableSeasonalScreenEffect"> - <template #label><SearchLabel>{{ i18n.ts.seasonalScreenEffect }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="enableSeasonalScreenEffect"> + <MkSwitch v-model="enableSeasonalScreenEffect"> + <template #label><SearchLabel>{{ i18n.ts.seasonalScreenEffect }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> </div> <SearchMarker :keywords="['menu', 'style', 'popup', 'drawer']"> - <MkSelect v-model="menuStyle"> - <template #label><SearchLabel>{{ i18n.ts.menuStyle }}</SearchLabel></template> - <option value="auto">{{ i18n.ts.auto }}</option> - <option value="popup">{{ i18n.ts.popup }}</option> - <option value="drawer">{{ i18n.ts.drawer }}</option> - </MkSelect> + <MkPreferenceContainer k="menuStyle"> + <MkSelect v-model="menuStyle"> + <template #label><SearchLabel>{{ i18n.ts.menuStyle }}</SearchLabel></template> + <option value="auto">{{ i18n.ts.auto }}</option> + <option value="popup">{{ i18n.ts.popup }}</option> + <option value="drawer">{{ i18n.ts.drawer }}</option> + </MkSelect> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']"> - <div> - <MkRadios v-model="emojiStyle"> - <template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template> - <option value="native">{{ i18n.ts.native }}</option> - <option value="fluentEmoji">Fluent Emoji</option> - <option value="twemoji">Twemoji</option> - </MkRadios> - <div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div> - </div> + <MkPreferenceContainer k="emojiStyle"> + <div> + <MkRadios v-model="emojiStyle"> + <template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template> + <option value="native">{{ i18n.ts.native }}</option> + <option value="fluentEmoji">Fluent Emoji</option> + <option value="twemoji">Twemoji</option> + </MkRadios> + <div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div> + </div> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['font', 'size']"> @@ -88,6 +100,12 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="3"><span style="font-size: 17px;">Aa</span></option> </MkRadios> </SearchMarker> + + <SearchMarker :keywords="['font', 'system', 'native']"> + <MkSwitch v-model="useSystemFont"> + <template #label><SearchLabel>{{ i18n.ts.useSystemFont }}</SearchLabel></template> + </MkSwitch> + </SearchMarker> </div> </FormSection> @@ -97,46 +115,56 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <SearchMarker :keywords="['reaction', 'size', 'scale', 'display']"> - <MkRadios v-model="reactionsDisplaySize"> - <template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template> - <option value="small">{{ i18n.ts.small }}</option> - <option value="medium">{{ i18n.ts.medium }}</option> - <option value="large">{{ i18n.ts.large }}</option> - </MkRadios> + <MkPreferenceContainer k="reactionsDisplaySize"> + <MkRadios v-model="reactionsDisplaySize"> + <template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template> + <option value="small">{{ i18n.ts.small }}</option> + <option value="medium">{{ i18n.ts.medium }}</option> + <option value="large">{{ i18n.ts.large }}</option> + </MkRadios> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['reaction', 'size', 'scale', 'display', 'width', 'limit']"> - <MkSwitch v-model="limitWidthOfReaction"> - <template #label><SearchLabel>{{ i18n.ts.limitWidthOfReaction }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="limitWidthOfReaction"> + <MkSwitch v-model="limitWidthOfReaction"> + <template #label><SearchLabel>{{ i18n.ts.limitWidthOfReaction }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height']"> - <MkRadios v-model="mediaListWithOneImageAppearance"> - <template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template> - <option value="expand">{{ i18n.ts.default }}</option> - <option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option> - <option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option> - <option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option> - </MkRadios> + <MkPreferenceContainer k="mediaListWithOneImageAppearance"> + <MkRadios v-model="mediaListWithOneImageAppearance"> + <template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template> + <option value="expand">{{ i18n.ts.default }}</option> + <option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option> + <option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option> + <option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option> + </MkRadios> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation']"> - <MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker"> - <template #label><SearchLabel>{{ i18n.ts.instanceTicker }}</SearchLabel></template> - <option value="none">{{ i18n.ts._instanceTicker.none }}</option> - <option value="remote">{{ i18n.ts._instanceTicker.remote }}</option> - <option value="always">{{ i18n.ts._instanceTicker.always }}</option> - </MkSelect> + <MkPreferenceContainer k="instanceTicker"> + <MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker"> + <template #label><SearchLabel>{{ i18n.ts.instanceTicker }}</SearchLabel></template> + <option value="none">{{ i18n.ts._instanceTicker.none }}</option> + <option value="remote">{{ i18n.ts._instanceTicker.remote }}</option> + <option value="always">{{ i18n.ts._instanceTicker.always }}</option> + </MkSelect> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility']"> - <MkSelect v-model="nsfw"> - <template #label><SearchLabel>{{ i18n.ts.displayOfSensitiveMedia }}</SearchLabel></template> - <option value="respect">{{ i18n.ts._displayOfSensitiveMedia.respect }}</option> - <option value="ignore">{{ i18n.ts._displayOfSensitiveMedia.ignore }}</option> - <option value="force">{{ i18n.ts._displayOfSensitiveMedia.force }}</option> - </MkSelect> + <MkPreferenceContainer k="nsfw"> + <MkSelect v-model="nsfw"> + <template #label><SearchLabel>{{ i18n.ts.displayOfSensitiveMedia }}</SearchLabel></template> + <option value="respect">{{ i18n.ts._displayOfSensitiveMedia.respect }}</option> + <option value="ignore">{{ i18n.ts._displayOfSensitiveMedia.ignore }}</option> + <option value="force">{{ i18n.ts._displayOfSensitiveMedia.force }}</option> + </MkSelect> + </MkPreferenceContainer> </SearchMarker> </div> </FormSection> @@ -148,21 +176,25 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <SearchMarker :keywords="['position']"> - <MkRadios v-model="notificationPosition"> - <template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template> - <option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option> - <option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option> - <option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option> - <option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option> - </MkRadios> + <MkPreferenceContainer k="notificationPosition"> + <MkRadios v-model="notificationPosition"> + <template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template> + <option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option> + <option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option> + <option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option> + <option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option> + </MkRadios> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['stack', 'axis', 'direction']"> - <MkRadios v-model="notificationStackAxis"> - <template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template> - <option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option> - <option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option> - </MkRadios> + <MkPreferenceContainer k="notificationStackAxis"> + <MkRadios v-model="notificationStackAxis"> + <template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template> + <option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option> + <option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option> + </MkRadios> + </MkPreferenceContainer> </SearchMarker> <MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton> @@ -183,7 +215,7 @@ import * as Misskey from 'misskey-js'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkRadios from '@/components/MkRadios.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { reloadAsk } from '@/scripts/reload-ask.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -194,26 +226,27 @@ import { claimAchievement } from '@/scripts/achievements.js'; import MkButton from '@/components/MkButton.vue'; import FormSection from '@/components/form/section.vue'; import { instance } from '@/instance.js'; +import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue'; const fontSize = ref(miLocalStorage.getItem('fontSize')); const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null); -const showAvatarDecorations = computed(defaultStore.makeGetterSetter('showAvatarDecorations')); -const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle')); -const menuStyle = computed(defaultStore.makeGetterSetter('menuStyle')); -const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal')); -const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect')); -const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia')); -const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars')); -const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect')); -const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline')); -const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance')); -const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize')); -const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction')); -const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition')); -const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis')); -const nsfw = computed(defaultStore.makeGetterSetter('nsfw')); -const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker')); +const showAvatarDecorations = prefer.model('showAvatarDecorations'); +const emojiStyle = prefer.model('emojiStyle'); +const menuStyle = prefer.model('menuStyle'); +const useBlurEffectForModal = prefer.model('useBlurEffectForModal'); +const useBlurEffect = prefer.model('useBlurEffect'); +const highlightSensitiveMedia = prefer.model('highlightSensitiveMedia'); +const squareAvatars = prefer.model('squareAvatars'); +const enableSeasonalScreenEffect = prefer.model('enableSeasonalScreenEffect'); +const showGapBetweenNotesInTimeline = prefer.model('showGapBetweenNotesInTimeline'); +const mediaListWithOneImageAppearance = prefer.model('mediaListWithOneImageAppearance'); +const reactionsDisplaySize = prefer.model('reactionsDisplaySize'); +const limitWidthOfReaction = prefer.model('limitWidthOfReaction'); +const notificationPosition = prefer.model('notificationPosition'); +const notificationStackAxis = prefer.model('notificationStackAxis'); +const nsfw = prefer.model('nsfw'); +const instanceTicker = prefer.model('instanceTicker'); watch(fontSize, () => { if (fontSize.value == null) { diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue index e574ec7dc0..e1965b6d36 100644 --- a/packages/frontend/src/pages/settings/deck.vue +++ b/packages/frontend/src/pages/settings/deck.vue @@ -23,14 +23,14 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed } from 'vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkRadios from '@/components/MkRadios.vue'; -import { deckStore } from '@/ui/deck/deck-store.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { prefer } from '@/preferences.js'; -const navWindow = computed(deckStore.makeGetterSetter('navWindow')); -const useSimpleUiForNonRootPages = computed(deckStore.makeGetterSetter('useSimpleUiForNonRootPages')); -const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn')); -const columnAlign = computed(deckStore.makeGetterSetter('columnAlign')); +const navWindow = prefer.model('deck.navWindow'); +const useSimpleUiForNonRootPages = prefer.model('deck.useSimpleUiForNonRootPages'); +const alwaysShowMainColumn = prefer.model('deck.alwaysShowMainColumn'); +const columnAlign = prefer.model('deck.columnAlign'); const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index 0138aac1c5..8e61b11dbe 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -50,17 +50,21 @@ SPDX-License-Identifier: AGPL-3.0-only </FormLink> <SearchMarker :keywords="['keep', 'original', 'raw', 'upload']"> - <MkSwitch v-model="keepOriginalUploading"> - <template #label><SearchLabel>{{ i18n.ts.keepOriginalUploading }}</SearchLabel></template> - <template #caption><SearchKeyword>{{ i18n.ts.keepOriginalUploadingDescription }}</SearchKeyword></template> - </MkSwitch> + <MkPreferenceContainer k="keepOriginalUploading"> + <MkSwitch v-model="keepOriginalUploading"> + <template #label><SearchLabel>{{ i18n.ts.keepOriginalUploading }}</SearchLabel></template> + <template #caption><SearchKeyword>{{ i18n.ts.keepOriginalUploadingDescription }}</SearchKeyword></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['keep', 'original', 'filename']"> - <MkSwitch v-model="keepOriginalFilename"> - <template #label><SearchLabel>{{ i18n.ts.keepOriginalFilename }}</SearchLabel></template> - <template #caption><SearchKeyword>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchKeyword></template> - </MkSwitch> + <MkPreferenceContainer k="keepOriginalFilename"> + <MkSwitch v-model="keepOriginalFilename"> + <template #label><SearchLabel>{{ i18n.ts.keepOriginalFilename }}</SearchLabel></template> + <template #caption><SearchKeyword>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchKeyword></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file']"> @@ -93,11 +97,12 @@ import FormSplit from '@/components/form/split.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import bytes from '@/filters/bytes.js'; -import { defaultStore } from '@/store.js'; import MkChart from '@/components/MkChart.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { signinRequired } from '@/account.js'; +import { prefer } from '@/preferences.js'; +import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue'; const $i = signinRequired(); @@ -120,8 +125,8 @@ const meterStyle = computed(() => { }; }); -const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading')); -const keepOriginalFilename = computed(defaultStore.makeGetterSetter('keepOriginalFilename')); +const keepOriginalUploading = prefer.model('keepOriginalUploading'); +const keepOriginalFilename = prefer.model('keepOriginalFilename'); misskeyApi('drive').then(info => { capacity.value = info.capacity; @@ -129,9 +134,9 @@ misskeyApi('drive').then(info => { fetching.value = false; }); -if (defaultStore.state.uploadFolder) { +if (prefer.s.uploadFolder) { misskeyApi('drive/folders/show', { - folderId: defaultStore.state.uploadFolder, + folderId: prefer.s.uploadFolder, }).then(response => { uploadFolder.value = response; }); @@ -139,11 +144,11 @@ if (defaultStore.state.uploadFolder) { function chooseUploadFolder() { os.selectDriveFolder(false).then(async folder => { - defaultStore.set('uploadFolder', folder[0] ? folder[0].id : null); + prefer.set('uploadFolder', folder[0] ? folder[0].id : null); os.success(); - if (defaultStore.state.uploadFolder) { + if (prefer.s.uploadFolder) { uploadFolder.value = await misskeyApi('drive/folders/show', { - folderId: defaultStore.state.uploadFolder, + folderId: prefer.s.uploadFolder, }); } else { uploadFolder.value = null; diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue index b16c943676..d2060a9112 100644 --- a/packages/frontend/src/pages/settings/emoji-picker.vue +++ b/packages/frontend/src/pages/settings/emoji-picker.vue @@ -89,37 +89,45 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.emojiPickerDisplay }}</template> <div class="_gaps_m"> - <MkRadios v-model="emojiPickerScale"> - <template #label>{{ i18n.ts.size }}</template> - <option :value="1">{{ i18n.ts.small }}</option> - <option :value="2">{{ i18n.ts.medium }}</option> - <option :value="3">{{ i18n.ts.large }}</option> - </MkRadios> + <MkPreferenceContainer k="emojiPickerScale"> + <MkRadios v-model="emojiPickerScale"> + <template #label>{{ i18n.ts.size }}</template> + <option :value="1">{{ i18n.ts.small }}</option> + <option :value="2">{{ i18n.ts.medium }}</option> + <option :value="3">{{ i18n.ts.large }}</option> + </MkRadios> + </MkPreferenceContainer> - <MkRadios v-model="emojiPickerWidth"> - <template #label>{{ i18n.ts.numberOfColumn }}</template> - <option :value="1">5</option> - <option :value="2">6</option> - <option :value="3">7</option> - <option :value="4">8</option> - <option :value="5">9</option> - </MkRadios> + <MkPreferenceContainer k="emojiPickerWidth"> + <MkRadios v-model="emojiPickerWidth"> + <template #label>{{ i18n.ts.numberOfColumn }}</template> + <option :value="1">5</option> + <option :value="2">6</option> + <option :value="3">7</option> + <option :value="4">8</option> + <option :value="5">9</option> + </MkRadios> + </MkPreferenceContainer> - <MkRadios v-model="emojiPickerHeight"> - <template #label>{{ i18n.ts.height }}</template> - <option :value="1">{{ i18n.ts.small }}</option> - <option :value="2">{{ i18n.ts.medium }}</option> - <option :value="3">{{ i18n.ts.large }}</option> - <option :value="4">{{ i18n.ts.large }}+</option> - </MkRadios> + <MkPreferenceContainer k="emojiPickerHeight"> + <MkRadios v-model="emojiPickerHeight"> + <template #label>{{ i18n.ts.height }}</template> + <option :value="1">{{ i18n.ts.small }}</option> + <option :value="2">{{ i18n.ts.medium }}</option> + <option :value="3">{{ i18n.ts.large }}</option> + <option :value="4">{{ i18n.ts.large }}+</option> + </MkRadios> + </MkPreferenceContainer> - <MkSelect v-model="emojiPickerStyle"> - <template #label>{{ i18n.ts.style }}</template> - <template #caption>{{ i18n.ts.needReloadToApply }}</template> - <option value="auto">{{ i18n.ts.auto }}</option> - <option value="popup">{{ i18n.ts.popup }}</option> - <option value="drawer">{{ i18n.ts.drawer }}</option> - </MkSelect> + <MkPreferenceContainer k="emojiPickerStyle"> + <MkSelect v-model="emojiPickerStyle"> + <template #label>{{ i18n.ts.style }}</template> + <template #caption>{{ i18n.ts.needReloadToApply }}</template> + <option value="auto">{{ i18n.ts.auto }}</option> + <option value="popup">{{ i18n.ts.popup }}</option> + <option value="drawer">{{ i18n.ts.drawer }}</option> + </MkSelect> + </MkPreferenceContainer> </div> </FormSection> </div> @@ -127,14 +135,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref, watch } from 'vue'; -import type { Ref } from 'vue'; import Sortable from 'vuedraggable'; +import type { Ref } from 'vue'; import MkRadios from '@/components/MkRadios.vue'; import MkButton from '@/components/MkButton.vue'; import FormSection from '@/components/form/section.vue'; import MkSelect from '@/components/MkSelect.vue'; import * as os from '@/os.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { deepClone } from '@/scripts/clone.js'; @@ -143,14 +151,16 @@ import { emojiPicker } from '@/scripts/emoji-picker.js'; import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; import MkEmoji from '@/components/global/MkEmoji.vue'; import MkFolder from '@/components/MkFolder.vue'; +import { prefer } from '@/preferences.js'; +import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue'; -const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(defaultStore.state.reactions)); -const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmojis)); +const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(store.state.reactions)); +const pinnedEmojis: Ref<string[]> = ref(deepClone(store.state.pinnedEmojis)); -const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale')); -const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth')); -const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight')); -const emojiPickerStyle = computed(defaultStore.makeGetterSetter('emojiPickerStyle')); +const emojiPickerScale = prefer.model('emojiPickerScale'); +const emojiPickerWidth = prefer.model('emojiPickerWidth'); +const emojiPickerHeight = prefer.model('emojiPickerHeight'); +const emojiPickerStyle = prefer.model('emojiPickerStyle'); const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev); const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev); @@ -210,7 +220,7 @@ async function setDefault(itemsRef: Ref<string[]>) { }); if (canceled) return; - itemsRef.value = deepClone(defaultStore.def.reactions.default); + itemsRef.value = deepClone(store.def.reactions.default); } async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) { @@ -230,13 +240,13 @@ function getHTMLElement(ev: MouseEvent): HTMLElement { } watch(pinnedEmojisForReaction, () => { - defaultStore.set('reactions', pinnedEmojisForReaction.value); + store.set('reactions', pinnedEmojisForReaction.value); }, { deep: true, }); watch(pinnedEmojis, () => { - defaultStore.set('pinnedEmojis', pinnedEmojis.value); + store.set('pinnedEmojis', pinnedEmojis.value); }, { deep: true, }); diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index 6b67a9a1a8..949f9019d9 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -155,11 +155,11 @@ import { selectFile } from '@/scripts/select-file.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { $i } from '@/account.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; const excludeMutingUsers = ref(false); const excludeInactiveUsers = ref(false); -const withReplies = ref(defaultStore.state.defaultWithReplies); +const withReplies = ref(store.state.defaultWithReplies); const onExportSuccess = () => { os.alert({ diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 458605d545..6203e7f698 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -12,6 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="!narrow || currentPage?.route.name == null" class="nav"> <div class="baaadecd"> <MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> + <MkInfo v-if="!store.reactiveState.enablePreferencesAutoCloudBackup.value && store.reactiveState.showPreferencesAutoCloudBackupSuggestion.value" class="info"> + <div>{{ i18n.ts._preferencesBackup.autoPreferencesBackupIsNotEnabledForThisDevice }}</div> + <div><button class="_textButton" @click="enableAutoBackup">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipAutoBackup">{{ i18n.ts.skip }}</button></div> + </MkInfo> <MkSuperMenu :def="menuDef" :grid="narrow" :searchIndex="SETTING_INDEX"></MkSuperMenu> </div> </div> @@ -41,6 +45,8 @@ import { definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } import * as os from '@/os.js'; import { useRouter } from '@/router/supplier.js'; import { searchIndexes } from '@/scripts/autogen/settings-search-index.js'; +import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utility.js'; +import { store } from '@/store.js'; const SETTING_INDEX = searchIndexes; // TODO: lazy load @@ -65,6 +71,10 @@ const ro = new ResizeObserver((entries, observer) => { narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD; }); +function skipAutoBackup() { + store.set('showPreferencesAutoCloudBackupSuggestion', false); +} + const menuDef = computed<SuperMenuDef[]>(() => [{ items: [{ icon: 'ti ti-user', @@ -168,10 +178,12 @@ const menuDef = computed<SuperMenuDef[]>(() => [{ }], }, { items: [{ - icon: 'ti ti-device-floppy', - text: i18n.ts.preferencesBackups, - to: '/settings/preferences-backups', - active: currentPage.value?.route.name === 'preferences-backups', + type: 'button', + icon: 'ti ti-settings-2', + text: i18n.ts.preferencesProfile, + action: async (ev: MouseEvent) => { + os.popupMenu(getPreferencesProfileMenu(), ev.currentTarget ?? ev.target); + }, }, { type: 'button', icon: 'ti ti-trash', diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index 4aac2a25bd..3620c05ca8 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -186,8 +186,8 @@ import { signinRequired } from '@/account.js'; import MkInfo from '@/components/MkInfo.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; -import { defaultStore } from '@/store'; import { reloadAsk } from '@/scripts/reload-ask.js'; +import { prefer } from '@/preferences.js'; const $i = signinRequired(); @@ -210,7 +210,7 @@ const expandedRenoteMuteItems = ref([]); const expandedMuteItems = ref([]); const expandedBlockItems = ref([]); -const showSoftWordMutedWord = computed(defaultStore.makeGetterSetter('showSoftWordMutedWord')); +const showSoftWordMutedWord = prefer.model('showSoftWordMutedWord'); watch([ showSoftWordMutedWord, diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index c38cdc4fc2..5e1dedd709 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -53,22 +53,24 @@ import FormSlot from '@/components/form/slot.vue'; import MkContainer from '@/components/MkContainer.vue'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { reloadAsk } from '@/scripts/reload-ask.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { prefer } from '@/preferences.js'; +import { PREF_DEF } from '@/preferences/def.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); -const items = ref(defaultStore.state.menu.map(x => ({ +const items = ref(prefer.s.menu.map(x => ({ id: Math.random().toString(), type: x, }))); -const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); +const menuDisplay = computed(store.makeGetterSetter('menuDisplay')); async function addItem() { - const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k)); + const menu = Object.keys(navbarItemDef).filter(k => !prefer.s.menu.includes(k)); const { canceled, result: item } = await os.select({ title: i18n.ts.addItem, items: [...menu.map(k => ({ @@ -89,12 +91,12 @@ function removeItem(index: number) { } async function save() { - defaultStore.set('menu', items.value.map(x => x.type)); + prefer.set('menu', items.value.map(x => x.type)); await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); } function reset() { - items.value = defaultStore.def.menu.default.map(x => ({ + items.value = PREF_DEF.menu.default.map(x => ({ id: Math.random().toString(), type: x, })); diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 9742c548e7..21133d72e7 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -117,20 +117,21 @@ import MkKeyValue from '@/components/MkKeyValue.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { signout, signinRequired } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { reloadAsk } from '@/scripts/reload-ask.js'; import FormSection from '@/components/form/section.vue'; +import { prefer } from '@/preferences.js'; const $i = signinRequired(); -const reportError = computed(defaultStore.makeGetterSetter('reportError')); -const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine')); -const skipNoteRender = computed(defaultStore.makeGetterSetter('skipNoteRender')); -const devMode = computed(defaultStore.makeGetterSetter('devMode')); -const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); +const reportError = prefer.model('reportError'); +const enableCondensedLine = prefer.model('enableCondensedLine'); +const skipNoteRender = prefer.model('skipNoteRender'); +const devMode = prefer.model('devMode'); +const defaultWithReplies = computed(store.makeGetterSetter('defaultWithReplies')); watch(skipNoteRender, async () => { await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue index 3ab26e80d9..c79aec91c0 100644 --- a/packages/frontend/src/pages/settings/plugin.install.vue +++ b/packages/frontend/src/pages/settings/plugin.install.vue @@ -23,10 +23,10 @@ import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkButton from '@/components/MkButton.vue'; import FormInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; -import { installPlugin } from '@/scripts/install-plugin.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { installPlugin } from '@/plugin.js'; const code = ref<string | null>(null); diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue index 3c3dcfe41e..fe57812a1d 100644 --- a/packages/frontend/src/pages/settings/plugin.vue +++ b/packages/frontend/src/pages/settings/plugin.vue @@ -4,72 +4,92 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="_gaps_m"> - <FormLink to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink> +<SearchMarker path="/settings/plugin" :label="i18n.ts.plugins" :keywords="['plugin']" icon="ti ti-plug"> + <div class="_gaps_m"> + <FormLink to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink> - <FormSection> - <template #label>{{ i18n.ts.manage }}</template> - <div class="_gaps_s"> - <div v-for="plugin in plugins" :key="plugin.id" class="_panel _gaps_m" style="padding: 20px;"> - <div class="_gaps_s"> - <span style="display: flex; align-items: center;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span> - <MkSwitch :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch> - </div> + <FormSection> + <template #label>{{ i18n.ts.manage }}</template> + <div class="_gaps_s"> + <MkFolder v-for="plugin in plugins" :key="plugin.installId"> + <template #icon><i class="ti ti-plug"></i></template> + <template #suffix> + <i v-if="plugin.active" class="ti ti-player-play" style="color: var(--MI_THEME-accent);"></i> + <i v-else class="ti ti-player-pause" style="opacity: 0.7;"></i> + </template> + <template #label> + <div :style="plugin.active ? '' : 'opacity: 0.7;'"> + {{ plugin.name }} + <span style="margin-left: 1em; opacity: 0.7;">v{{ plugin.version }}</span> + </div> + </template> + <template #caption> + {{ plugin.description }} + </template> + <template #footer> + <div class="_buttons"> + <MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton> + <MkButton inline danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton> + </div> + </template> - <div class="_gaps_s"> - <MkKeyValue> - <template #key>{{ i18n.ts.author }}</template> - <template #value>{{ plugin.author }}</template> - </MkKeyValue> - <MkKeyValue> - <template #key>{{ i18n.ts.description }}</template> - <template #value>{{ plugin.description }}</template> - </MkKeyValue> - <MkKeyValue> - <template #key>{{ i18n.ts.permission }}</template> - <template #value> - <ul style="margin-top: 0; margin-bottom: 0;"> - <li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li> - <li v-if="!plugin.permissions || plugin.permissions.length === 0">{{ i18n.ts.none }}</li> - </ul> - </template> - </MkKeyValue> - </div> + <div class="_gaps_m"> + <div class="_gaps_s"> + <span style="display: flex; align-items: center;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span> + <MkSwitch :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch> + </div> - <div class="_buttons"> - <MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton> - <MkButton inline danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton> - </div> + <div class="_gaps_s"> + <MkKeyValue> + <template #key>{{ i18n.ts.author }}</template> + <template #value>{{ plugin.author }}</template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts.description }}</template> + <template #value>{{ plugin.description }}</template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts.permission }}</template> + <template #value> + <ul style="margin-top: 0; margin-bottom: 0;"> + <li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li> + <li v-if="!plugin.permissions || plugin.permissions.length === 0">{{ i18n.ts.none }}</li> + </ul> + </template> + </MkKeyValue> + </div> - <MkFolder> - <template #icon><i class="ti ti-terminal-2"></i></template> - <template #label>{{ i18n.ts._plugin.viewLog }}</template> + <MkFolder> + <template #icon><i class="ti ti-terminal-2"></i></template> + <template #label>{{ i18n.ts._plugin.viewLog }}</template> - <div class="_gaps_s"> - <div class="_buttons"> - <MkButton inline @click="copy(pluginLogs.get(plugin.id)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> - </div> + <div class="_gaps_s"> + <div class="_buttons"> + <MkButton inline @click="copy(pluginLogs.get(plugin.installId)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> + </div> - <MkCode :code="pluginLogs.get(plugin.id)?.join('\n') ?? ''"/> - </div> - </MkFolder> + <MkCode :code="pluginLogs.get(plugin.installId)?.join('\n') ?? ''"/> + </div> + </MkFolder> - <MkFolder> - <template #icon><i class="ti ti-code"></i></template> - <template #label>{{ i18n.ts._plugin.viewSource }}</template> + <MkFolder> + <template #icon><i class="ti ti-code"></i></template> + <template #label>{{ i18n.ts._plugin.viewSource }}</template> - <div class="_gaps_s"> - <div class="_buttons"> - <MkButton inline @click="copy(plugin.src)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> - </div> + <div class="_gaps_s"> + <div class="_buttons"> + <MkButton inline @click="copy(plugin.src)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> + </div> - <MkCode :code="plugin.src ?? ''" lang="is"/> + <MkCode :code="plugin.src ?? ''" lang="ais"/> + </div> + </MkFolder> </div> </MkFolder> </div> - </div> - </FormSection> -</div> + </FormSection> + </div> +</SearchMarker> </template> <script lang="ts" setup> @@ -83,19 +103,16 @@ import MkFolder from '@/components/MkFolder.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import * as os from '@/os.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { ColdDeviceStorage } from '@/store.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { pluginLogs } from '@/plugin.js'; +import { changePluginActive, configPlugin, pluginLogs, uninstallPlugin } from '@/plugin.js'; +import { prefer } from '@/preferences.js'; -const plugins = ref(ColdDeviceStorage.get('plugins')); +const plugins = prefer.r.plugins; async function uninstall(plugin) { - ColdDeviceStorage.set('plugins', plugins.value.filter(x => x.id !== plugin.id)); - await os.apiWithDialog('i/revoke-token', { - token: plugin.token, - }); + await uninstallPlugin(plugin); nextTick(() => { unisonReload(); }); @@ -106,30 +123,15 @@ function copy(text) { os.success(); } -// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする async function config(plugin) { - const config = plugin.config; - for (const key in plugin.configData) { - config[key].default = plugin.configData[key]; - } - - const { canceled, result } = await os.form(plugin.name, config); - if (canceled) return; - - const coldPlugins = ColdDeviceStorage.get('plugins'); - coldPlugins.find(p => p.id === plugin.id)!.configData = result; - ColdDeviceStorage.set('plugins', coldPlugins); - + await configPlugin(plugin); nextTick(() => { location.reload(); }); } function changeActive(plugin, active) { - const coldPlugins = ColdDeviceStorage.get('plugins'); - coldPlugins.find(p => p.id === plugin.id)!.active = active; - ColdDeviceStorage.set('plugins', coldPlugins); - + changePluginActive(plugin, active); nextTick(() => { location.reload(); }); diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue deleted file mode 100644 index 7388e014ed..0000000000 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ /dev/null @@ -1,465 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<div class="_gaps_m"> - <div :class="$style.buttons"> - <MkButton inline primary @click="saveNew">{{ i18n.ts._preferencesBackups.saveNew }}</MkButton> - <MkButton inline @click="loadFile">{{ i18n.ts._preferencesBackups.loadFile }}</MkButton> - </div> - - <FormSection> - <template #label>{{ i18n.ts._preferencesBackups.list }}</template> - <template v-if="profiles && Object.keys(profiles).length > 0"> - <div class="_gaps_s"> - <div - v-for="(profile, id) in profiles" - :key="id" - class="_panel" - :class="$style.profile" - @click="$event => menu($event, id)" - @contextmenu.prevent.stop="$event => menu($event, id)" - > - <div :class="$style.profileName">{{ profile.name }}</div> - <div :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.createdAt({ date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div> - <div v-if="profile.updatedAt" :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.updatedAt({ date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div> - </div> - </div> - </template> - <div v-else-if="profiles"> - <MkInfo>{{ i18n.ts._preferencesBackups.noBackups }}</MkInfo> - </div> - <MkLoading v-else/> - </FormSection> -</div> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, ref } from 'vue'; -import { v4 as uuid } from 'uuid'; -import { version, host } from '@@/js/config.js'; -import FormSection from '@/components/form/section.vue'; -import MkButton from '@/components/MkButton.vue'; -import MkInfo from '@/components/MkInfo.vue'; -import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { ColdDeviceStorage, defaultStore } from '@/store.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; -import { useStream } from '@/stream.js'; -import { $i } from '@/account.js'; -import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { miLocalStorage } from '@/local-storage.js'; - -const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ - 'collapseRenotes', - 'menu', - 'visibility', - 'localOnly', - 'statusbars', - 'widgets', - 'tl', - 'pinnedUserLists', - 'overridedDeviceKind', - 'serverDisconnectedBehavior', - 'nsfw', - 'highlightSensitiveMedia', - 'animation', - 'animatedMfm', - 'advancedMfm', - 'showReactionsCount', - 'loadRawImages', - 'imageNewTab', - 'dataSaver', - 'disableShowingAnimatedImages', - 'emojiStyle', - 'menuStyle', - 'useBlurEffectForModal', - 'useBlurEffect', - 'showFixedPostForm', - 'showFixedPostFormInChannel', - 'enableInfiniteScroll', - 'useReactionPickerForContextMenu', - 'showGapBetweenNotesInTimeline', - 'instanceTicker', - 'emojiPickerScale', - 'emojiPickerWidth', - 'emojiPickerHeight', - 'emojiPickerStyle', - 'defaultSideView', - 'menuDisplay', - 'reportError', - 'squareAvatars', - 'showAvatarDecorations', - 'numberOfPageCache', - 'showNoteActionsOnlyHover', - 'showClipButtonInNoteFooter', - 'reactionsDisplaySize', - 'forceShowAds', - 'aiChanMode', - 'devMode', - 'mediaListWithOneImageAppearance', - 'notificationPosition', - 'notificationStackAxis', - 'keepScreenOn', - 'defaultWithReplies', - 'disableStreamingTimeline', - 'useGroupedNotifications', - 'sound_masterVolume', - 'sound_note', - 'sound_noteMy', - 'sound_notification', -]; -const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [ - 'lightTheme', - 'darkTheme', - 'syncDeviceDarkMode', - 'plugins', -]; - -const scope = ['clientPreferencesProfiles']; - -const profileProps = ['name', 'createdAt', 'updatedAt', 'misskeyVersion', 'settings', 'host']; - -type Profile = { - name: string; - createdAt: string; - updatedAt: string | null; - misskeyVersion: string; - host: string; - settings: { - hot: Record<keyof typeof defaultStoreSaveKeys, unknown>; - cold: Record<keyof typeof coldDeviceStorageSaveKeys, unknown>; - fontSize: string | null; - useSystemFont: 't' | null; - wallpaper: string | null; - }; -}; - -const connection = $i && useStream().useChannel('main'); - -const profiles = ref<Record<string, Profile> | null>(null); - -misskeyApi('i/registry/get-all', { scope }) - .then(res => { - profiles.value = res || {}; - }); - -function isObject(value: unknown): value is Record<string, unknown> { - return value != null && typeof value === 'object' && !Array.isArray(value); -} - -function validate(profile: any): void { - if (!isObject(profile)) throw new Error('not an object'); - - // Check if unnecessary properties exist - if (Object.keys(profile).some(key => !profileProps.includes(key))) throw new Error('Unnecessary properties exist'); - - if (!profile.name) throw new Error('Missing required prop: name'); - if (!profile.misskeyVersion) throw new Error('Missing required prop: misskeyVersion'); - - // Check if createdAt and updatedAt is Date - // https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date - if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt as any).getTime())) throw new Error('createdAt is falsy or not Date'); - if (profile.updatedAt) { - if (Number.isNaN(new Date(profile.updatedAt as any).getTime())) { - throw new Error('updatedAt is not Date'); - } - } else if (profile.updatedAt !== null) { - throw new Error('updatedAt is not null'); - } - - if (!profile.settings) throw new Error('Missing required prop: settings'); - if (!isObject(profile.settings)) throw new Error('Invalid prop: settings'); -} - -function getSettings(): Profile['settings'] { - const hot = {} as Record<keyof typeof defaultStoreSaveKeys, unknown>; - for (const key of defaultStoreSaveKeys) { - hot[key] = defaultStore.state[key]; - } - - const cold = {} as Record<keyof typeof coldDeviceStorageSaveKeys, unknown>; - for (const key of coldDeviceStorageSaveKeys) { - cold[key] = ColdDeviceStorage.get(key); - } - - return { - hot, - cold, - fontSize: miLocalStorage.getItem('fontSize'), - useSystemFont: miLocalStorage.getItem('useSystemFont') as 't' | null, - wallpaper: miLocalStorage.getItem('wallpaper'), - }; -} - -async function saveNew(): Promise<void> { - if (!profiles.value) return; - - const { canceled, result: name } = await os.inputText({ - title: i18n.ts._preferencesBackups.inputName, - default: '', - }); - if (canceled) return; - - if (Object.values(profiles.value).some(x => x.name === name)) { - return os.alert({ - title: i18n.ts._preferencesBackups.cannotSave, - text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }), - }); - } - - const id = uuid(); - const profile: Profile = { - name, - createdAt: (new Date()).toISOString(), - updatedAt: null, - misskeyVersion: version, - host, - settings: getSettings(), - }; - await os.apiWithDialog('i/registry/set', { scope, key: id, value: profile }); -} - -function loadFile(): void { - const input = document.createElement('input'); - input.type = 'file'; - input.multiple = false; - input.onchange = async () => { - if (!profiles.value) return; - if (!input.files || input.files.length === 0) return; - - const file = input.files[0]; - - if (file.type !== 'application/json') { - return os.alert({ - type: 'error', - title: i18n.ts._preferencesBackups.cannotLoad, - text: i18n.ts._preferencesBackups.invalidFile, - }); - } - - let profile: Profile; - try { - profile = JSON.parse(await file.text()) as unknown as Profile; - validate(profile); - } catch (err) { - return os.alert({ - type: 'error', - title: i18n.ts._preferencesBackups.cannotLoad, - text: (err as any)?.message ?? '', - }); - } - - const id = uuid(); - await os.apiWithDialog('i/registry/set', { scope, key: id, value: profile }); - - // 一応廃棄 - (window as any).__misskey_input_ref__ = null; - }; - - // https://qiita.com/fukasawah/items/b9dc732d95d99551013d - // iOS Safari で正常に動かす為のおまじない - (window as any).__misskey_input_ref__ = input; - - input.click(); -} - -async function applyProfile(id: string): Promise<void> { - if (!profiles.value) return; - - const profile = profiles.value[id]; - - const { canceled: cancel1 } = await os.confirm({ - type: 'warning', - title: i18n.ts._preferencesBackups.apply, - text: i18n.tsx._preferencesBackups.applyConfirm({ name: profile.name }), - }); - if (cancel1) return; - - // TODO: バージョン or ホストが違ったらさらに警告を表示 - - const settings = profile.settings; - - // defaultStore - for (const key of defaultStoreSaveKeys) { - if (settings.hot[key] !== undefined) { - defaultStore.set(key, settings.hot[key]); - } - } - - // coldDeviceStorage - for (const key of coldDeviceStorageSaveKeys) { - if (settings.cold[key] !== undefined) { - ColdDeviceStorage.set(key, settings.cold[key]); - } - } - - // fontSize - if (settings.fontSize) { - miLocalStorage.setItem('fontSize', settings.fontSize); - } else { - miLocalStorage.removeItem('fontSize'); - } - - // useSystemFont - if (settings.useSystemFont) { - miLocalStorage.setItem('useSystemFont', settings.useSystemFont); - } else { - miLocalStorage.removeItem('useSystemFont'); - } - - // wallpaper - if (settings.wallpaper != null) { - miLocalStorage.setItem('wallpaper', settings.wallpaper); - } else { - miLocalStorage.removeItem('wallpaper'); - } - - const { canceled: cancel2 } = await os.confirm({ - type: 'info', - text: i18n.ts.reloadToApplySetting, - }); - if (cancel2) return; - - unisonReload(); -} - -async function deleteProfile(id: string): Promise<void> { - if (!profiles.value) return; - - const { canceled } = await os.confirm({ - type: 'info', - title: i18n.ts.delete, - text: i18n.tsx.deleteAreYouSure({ x: profiles.value[id].name }), - }); - if (canceled) return; - - await os.apiWithDialog('i/registry/remove', { scope, key: id }); - delete profiles.value[id]; -} - -async function save(id: string): Promise<void> { - if (!profiles.value) return; - - const { name, createdAt } = profiles.value[id]; - - const { canceled } = await os.confirm({ - type: 'info', - title: i18n.ts._preferencesBackups.save, - text: i18n.tsx._preferencesBackups.saveConfirm({ name }), - }); - if (canceled) return; - - const profile: Profile = { - name, - createdAt, - updatedAt: (new Date()).toISOString(), - misskeyVersion: version, - host, - settings: getSettings(), - }; - await os.apiWithDialog('i/registry/set', { scope, key: id, value: profile }); -} - -async function rename(id: string): Promise<void> { - if (!profiles.value) return; - - const { canceled: cancel1, result: name } = await os.inputText({ - title: i18n.ts._preferencesBackups.inputName, - default: '', - }); - if (cancel1 || profiles.value[id].name === name) return; - - if (Object.values(profiles.value).some(x => x.name === name)) { - return os.alert({ - title: i18n.ts._preferencesBackups.cannotSave, - text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }), - }); - } - - const registry = Object.assign({}, { ...profiles.value[id] }); - - const { canceled: cancel2 } = await os.confirm({ - type: 'info', - title: i18n.ts.rename, - text: i18n.tsx._preferencesBackups.renameConfirm({ old: registry.name, new: name }), - }); - if (cancel2) return; - - registry.name = name; - await os.apiWithDialog('i/registry/set', { scope, key: id, value: registry }); -} - -function menu(ev: MouseEvent, profileId: string) { - if (!profiles.value) return; - - return os.popupMenu([{ - text: i18n.ts._preferencesBackups.apply, - icon: 'ti ti-check', - action: () => applyProfile(profileId), - }, { - type: 'a', - text: i18n.ts.download, - icon: 'ti ti-download', - href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })), - download: `${profiles.value[profileId].name}.json`, - }, { type: 'divider' }, { - text: i18n.ts.rename, - icon: 'ti ti-forms', - action: () => rename(profileId), - }, { - text: i18n.ts._preferencesBackups.save, - icon: 'ti ti-device-floppy', - action: () => save(profileId), - }, { type: 'divider' }, { - text: i18n.ts.delete, - icon: 'ti ti-trash', - action: () => deleteProfile(profileId), - danger: true, - }], (ev.currentTarget ?? ev.target ?? undefined) as unknown as HTMLElement | undefined); -} - -onMounted(() => { - // streamingのuser storage updateイベントを監視して更新 - connection?.on('registryUpdated', ({ scope: recievedScope, key, value }) => { - if (!recievedScope || recievedScope.length !== scope.length || recievedScope[0] !== scope[0]) return; - if (!profiles.value) return; - - profiles.value[key] = value; - }); -}); - -onUnmounted(() => { - connection?.off('registryUpdated'); -}); - -definePageMetadata(() => ({ - title: i18n.ts.preferencesBackups, - icon: 'ti ti-device-floppy', -})); -</script> - -<style lang="scss" module> -.buttons { - display: flex; - gap: var(--MI-margin); - flex-wrap: wrap; -} - -.profile { - padding: 20px; - cursor: pointer; - - &Name { - font-weight: 700; - } - - &Time { - font-size: .85em; - opacity: .7; - } -} -</style> diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue index fe718bfa69..2df621eaa6 100644 --- a/packages/frontend/src/pages/settings/preferences.vue +++ b/packages/frontend/src/pages/settings/preferences.vue @@ -33,30 +33,36 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <div class="_gaps_s"> <SearchMarker :keywords="['post', 'form', 'timeline']"> - <MkSwitch v-model="showFixedPostForm"> - <template #label><SearchLabel>{{ i18n.ts.showFixedPostForm }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="showFixedPostForm"> + <MkSwitch v-model="showFixedPostForm"> + <template #label><SearchLabel>{{ i18n.ts.showFixedPostForm }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['post', 'form', 'timeline', 'channel']"> - <MkSwitch v-model="showFixedPostFormInChannel"> - <template #label><SearchLabel>{{ i18n.ts.showFixedPostFormInChannel }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="showFixedPostFormInChannel"> + <MkSwitch v-model="showFixedPostFormInChannel"> + <template #label><SearchLabel>{{ i18n.ts.showFixedPostFormInChannel }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['pinned', 'list']"> <MkFolder> <template #label><SearchLabel>{{ i18n.ts.pinnedList }}</SearchLabel></template> <!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ --> - <MkButton v-if="defaultStore.reactiveState.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton> + <MkButton v-if="prefer.r.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton> <MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> </MkFolder> </SearchMarker> <SearchMarker :keywords="['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn']"> - <MkSwitch v-model="enableQuickAddMfmFunction"> - <template #label><SearchLabel>{{ i18n.ts.enableQuickAddMfmFunction }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="enableQuickAddMfmFunction"> + <MkSwitch v-model="enableQuickAddMfmFunction"> + <template #label><SearchLabel>{{ i18n.ts.enableQuickAddMfmFunction }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> </div> </FormSection> @@ -68,40 +74,52 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <div class="_gaps_s"> <SearchMarker :keywords="['renote']"> - <MkSwitch v-model="collapseRenotes"> - <template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template> - <template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template> - </MkSwitch> + <MkPreferenceContainer k="collapseRenotes"> + <MkSwitch v-model="collapseRenotes"> + <template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template> + <template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['hover', 'show', 'footer', 'action']"> - <MkSwitch v-model="showNoteActionsOnlyHover"> - <template #label><SearchLabel>{{ i18n.ts.showNoteActionsOnlyHover }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="showNoteActionsOnlyHover"> + <MkSwitch v-model="showNoteActionsOnlyHover"> + <template #label><SearchLabel>{{ i18n.ts.showNoteActionsOnlyHover }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['footer', 'action', 'clip', 'show']"> - <MkSwitch v-model="showClipButtonInNoteFooter"> - <template #label><SearchLabel>{{ i18n.ts.showClipButtonInNoteFooter }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="showClipButtonInNoteFooter"> + <MkSwitch v-model="showClipButtonInNoteFooter"> + <template #label><SearchLabel>{{ i18n.ts.showClipButtonInNoteFooter }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['mfm', 'enable', 'show', 'advanced']"> - <MkSwitch v-model="advancedMfm"> - <template #label><SearchLabel>{{ i18n.ts.enableAdvancedMfm }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="advancedMfm"> + <MkSwitch v-model="advancedMfm"> + <template #label><SearchLabel>{{ i18n.ts.enableAdvancedMfm }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['reaction', 'count', 'show']"> - <MkSwitch v-model="showReactionsCount"> - <template #label><SearchLabel>{{ i18n.ts.showReactionsCount }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="showReactionsCount"> + <MkSwitch v-model="showReactionsCount"> + <template #label><SearchLabel>{{ i18n.ts.showReactionsCount }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment']"> - <MkSwitch v-model="loadRawImages"> - <template #label><SearchLabel>{{ i18n.ts.loadRawImages }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="loadRawImages"> + <MkSwitch v-model="loadRawImages"> + <template #label><SearchLabel>{{ i18n.ts.loadRawImages }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> </div> </div> @@ -114,9 +132,11 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <SearchMarker :keywords="['group']"> - <MkSwitch v-model="useGroupedNotifications"> - <template #label><SearchLabel>{{ i18n.ts.useGroupedNotifications }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="useGroupedNotifications"> + <MkSwitch v-model="useGroupedNotifications"> + <template #label><SearchLabel>{{ i18n.ts.useGroupedNotifications }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> </div> </FormSection> @@ -129,62 +149,88 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <div class="_gaps_s"> <SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab']"> - <MkSwitch v-model="imageNewTab"> - <template #label><SearchLabel>{{ i18n.ts.openImageInNewTab }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="imageNewTab"> + <MkSwitch v-model="imageNewTab"> + <template #label><SearchLabel>{{ i18n.ts.openImageInNewTab }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['reaction', 'picker', 'contextmenu', 'open']"> - <MkSwitch v-model="useReactionPickerForContextMenu"> - <template #label><SearchLabel>{{ i18n.ts.useReactionPickerForContextMenu }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="useReactionPickerForContextMenu"> + <MkSwitch v-model="useReactionPickerForContextMenu"> + <template #label><SearchLabel>{{ i18n.ts.useReactionPickerForContextMenu }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['load', 'auto', 'more']"> - <MkSwitch v-model="enableInfiniteScroll"> - <template #label><SearchLabel>{{ i18n.ts.enableInfiniteScroll }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="enableInfiniteScroll"> + <MkSwitch v-model="enableInfiniteScroll"> + <template #label><SearchLabel>{{ i18n.ts.enableInfiniteScroll }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['disable', 'streaming', 'timeline']"> - <MkSwitch v-model="disableStreamingTimeline"> - <template #label><SearchLabel>{{ i18n.ts.disableStreamingTimeline }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="disableStreamingTimeline"> + <MkSwitch v-model="disableStreamingTimeline"> + <template #label><SearchLabel>{{ i18n.ts.disableStreamingTimeline }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['follow', 'confirm', 'always']"> - <MkSwitch v-model="alwaysConfirmFollow"> - <template #label><SearchLabel>{{ i18n.ts.alwaysConfirmFollow }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="alwaysConfirmFollow"> + <MkSwitch v-model="alwaysConfirmFollow"> + <template #label><SearchLabel>{{ i18n.ts.alwaysConfirmFollow }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm']"> - <MkSwitch v-model="confirmWhenRevealingSensitiveMedia"> - <template #label><SearchLabel>{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="confirmWhenRevealingSensitiveMedia"> + <MkSwitch v-model="confirmWhenRevealingSensitiveMedia"> + <template #label><SearchLabel>{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['reaction', 'confirm']"> - <MkSwitch v-model="confirmOnReact"> - <template #label><SearchLabel>{{ i18n.ts.confirmOnReact }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="confirmOnReact"> + <MkSwitch v-model="confirmOnReact"> + <template #label><SearchLabel>{{ i18n.ts.confirmOnReact }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> + </SearchMarker> + + <SearchMarker :keywords="['remember', 'keep', 'note', 'cw']"> + <MkPreferenceContainer k="keepCw"> + <MkSwitch v-model="keepCw"> + <template #label><SearchLabel>{{ i18n.ts.keepCw }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> </div> <SearchMarker :keywords="['server', 'disconnect', 'reconnect', 'reload', 'streaming']"> - <MkSelect v-model="serverDisconnectedBehavior"> - <template #label><SearchLabel>{{ i18n.ts.whenServerDisconnected }}</SearchLabel></template> - <option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option> - <option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option> - <option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option> - </MkSelect> + <MkPreferenceContainer k="serverDisconnectedBehavior"> + <MkSelect v-model="serverDisconnectedBehavior"> + <template #label><SearchLabel>{{ i18n.ts.whenServerDisconnected }}</SearchLabel></template> + <option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option> + <option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option> + <option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option> + </MkSelect> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['cache', 'page']"> - <MkRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing> - <template #label><SearchLabel>{{ i18n.ts.numberOfPageCache }}</SearchLabel></template> - <template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template> - </MkRange> + <MkPreferenceContainer k="numberOfPageCache"> + <MkRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing> + <template #label><SearchLabel>{{ i18n.ts.numberOfPageCache }}</SearchLabel></template> + <template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template> + </MkRange> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :label="i18n.ts.dataSaver" :keywords="['datasaver']"> @@ -229,18 +275,22 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <SearchMarker :keywords="['ad', 'show']"> - <MkSwitch v-model="forceShowAds"> - <template #label><SearchLabel>{{ i18n.ts.forceShowAds }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="forceShowAds"> + <MkSwitch v-model="forceShowAds"> + <template #label><SearchLabel>{{ i18n.ts.forceShowAds }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker> - <MkRadios v-model="hemisphere"> - <template #label><SearchLabel>{{ i18n.ts.hemisphere }}</SearchLabel></template> - <option value="N">{{ i18n.ts._hemisphere.N }}</option> - <option value="S">{{ i18n.ts._hemisphere.S }}</option> - <template #caption>{{ i18n.ts._hemisphere.caption }}</template> - </MkRadios> + <MkPreferenceContainer k="hemisphere"> + <MkRadios v-model="hemisphere"> + <template #label><SearchLabel>{{ i18n.ts.hemisphere }}</SearchLabel></template> + <option value="N">{{ i18n.ts._hemisphere.N }}</option> + <option value="S">{{ i18n.ts._hemisphere.S }}</option> + <template #caption>{{ i18n.ts._hemisphere.caption }}</template> + </MkRadios> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['emoji', 'dictionary', 'additional', 'extra']"> @@ -248,8 +298,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label><SearchLabel>{{ i18n.ts.additionalEmojiDictionary }}</SearchLabel></template> <div class="_buttons"> <template v-for="lang in emojiIndexLangs" :key="lang"> - <MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton> - <MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton> + <MkButton v-if="store.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton> + <MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ store.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton> </template> </div> </MkFolder> @@ -272,7 +322,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref, watch } from 'vue'; -import * as Misskey from 'misskey-js'; import { langs } from '@@/js/config.js'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; @@ -284,41 +333,44 @@ import FormSection from '@/components/form/section.vue'; import FormLink from '@/components/form/link.vue'; import MkLink from '@/components/MkLink.vue'; import MkInfo from '@/components/MkInfo.vue'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import * as os from '@/os.js'; -import { instance } from '@/instance.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { reloadAsk } from '@/scripts/reload-ask.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; +import { prefer } from '@/preferences.js'; +import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue'; const lang = ref(miLocalStorage.getItem('lang')); -const dataSaver = ref(defaultStore.state.dataSaver); +const dataSaver = ref(prefer.s.dataSaver); + +const overridedDeviceKind = computed(store.makeGetterSetter('overridedDeviceKind')); -const hemisphere = computed(defaultStore.makeGetterSetter('hemisphere')); -const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind')); -const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior')); -const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover')); -const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter')); -const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes')); -const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm')); -const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount')); -const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction')); -const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds')); -const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages')); -const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab')); -const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm')); -const showFixedPostFormInChannel = computed(defaultStore.makeGetterSetter('showFixedPostFormInChannel')); -const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache')); -const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll')); -const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu')); -const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline')); -const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications')); -const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow')); -const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia')); -const confirmOnReact = computed(defaultStore.makeGetterSetter('confirmOnReact')); -const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu')); +const keepCw = prefer.model('keepCw'); +const serverDisconnectedBehavior = prefer.model('serverDisconnectedBehavior'); +const hemisphere = prefer.model('hemisphere'); +const showNoteActionsOnlyHover = prefer.model('showNoteActionsOnlyHover'); +const showClipButtonInNoteFooter = prefer.model('showClipButtonInNoteFooter'); +const collapseRenotes = prefer.model('collapseRenotes'); +const advancedMfm = prefer.model('advancedMfm'); +const showReactionsCount = prefer.model('showReactionsCount'); +const enableQuickAddMfmFunction = prefer.model('enableQuickAddMfmFunction'); +const forceShowAds = prefer.model('forceShowAds'); +const loadRawImages = prefer.model('loadRawImages'); +const imageNewTab = prefer.model('imageNewTab'); +const showFixedPostForm = prefer.model('showFixedPostForm'); +const showFixedPostFormInChannel = prefer.model('showFixedPostFormInChannel'); +const numberOfPageCache = prefer.model('numberOfPageCache'); +const enableInfiniteScroll = prefer.model('enableInfiniteScroll'); +const useReactionPickerForContextMenu = prefer.model('useReactionPickerForContextMenu'); +const disableStreamingTimeline = prefer.model('disableStreamingTimeline'); +const useGroupedNotifications = prefer.model('useGroupedNotifications'); +const alwaysConfirmFollow = prefer.model('alwaysConfirmFollow'); +const confirmWhenRevealingSensitiveMedia = prefer.model('confirmWhenRevealingSensitiveMedia'); +const confirmOnReact = prefer.model('confirmOnReact'); +const contextMenu = prefer.model('contextMenu'); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); @@ -356,7 +408,7 @@ function getEmojiIndexLangName(targetLang: typeof emojiIndexLangs[number]) { function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) { async function main() { - const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes; + const currentIndexes = store.state.additionalUnicodeEmojiIndexes; function download() { switch (lang) { @@ -368,7 +420,7 @@ function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) { } currentIndexes[lang] = await download(); - await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes); + await store.set('additionalUnicodeEmojiIndexes', currentIndexes); } os.promiseDialog(main()); @@ -376,9 +428,9 @@ function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) { function removeEmojiIndex(lang: string) { async function main() { - const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes; + const currentIndexes = store.state.additionalUnicodeEmojiIndexes; delete currentIndexes[lang]; - await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes); + await store.set('additionalUnicodeEmojiIndexes', currentIndexes); } os.promiseDialog(main()); @@ -393,16 +445,17 @@ async function setPinnedList() { })), }); if (canceled) return; + if (list == null) return; - defaultStore.set('pinnedUserLists', [list]); + prefer.set('pinnedUserLists', [list]); } function removePinnedList() { - defaultStore.set('pinnedUserLists', []); + prefer.set('pinnedUserLists', []); } function enableAllDataSaver() { - const g = { ...defaultStore.state.dataSaver }; + const g = { ...prefer.s.dataSaver }; Object.keys(g).forEach((key) => { g[key] = true; }); @@ -410,7 +463,7 @@ function enableAllDataSaver() { } function disableAllDataSaver() { - const g = { ...defaultStore.state.dataSaver }; + const g = { ...prefer.s.dataSaver }; Object.keys(g).forEach((key) => { g[key] = false; }); @@ -418,7 +471,7 @@ function disableAllDataSaver() { } watch(dataSaver, (to) => { - defaultStore.set('dataSaver', to); + prefer.set('dataSaver', to); }, { deep: true, }); diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index cd0d54a73b..792b4147da 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -172,38 +172,41 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <div class="_gaps_m"> <SearchMarker :keywords="['remember', 'keep', 'note', 'visibility']"> - <MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()"> - <template #label><SearchLabel>{{ i18n.ts.rememberNoteVisibility }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="rememberNoteVisibility"> + <MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()"> + <template #label><SearchLabel>{{ i18n.ts.rememberNoteVisibility }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['default', 'note', 'visibility']"> - <MkFolder v-if="!rememberNoteVisibility"> - <template #label><SearchLabel>{{ i18n.ts.defaultNoteVisibility }}</SearchLabel></template> - <template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template> - <template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template> - <template v-else-if="defaultNoteVisibility === 'followers'" #suffix>{{ i18n.ts._visibility.followers }}</template> - <template v-else-if="defaultNoteVisibility === 'specified'" #suffix>{{ i18n.ts._visibility.specified }}</template> + <MkDisableSection :disabled="rememberNoteVisibility"> + <MkFolder> + <template #label><SearchLabel>{{ i18n.ts.defaultNoteVisibility }}</SearchLabel></template> + <template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template> + <template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template> + <template v-else-if="defaultNoteVisibility === 'followers'" #suffix>{{ i18n.ts._visibility.followers }}</template> + <template v-else-if="defaultNoteVisibility === 'specified'" #suffix>{{ i18n.ts._visibility.specified }}</template> + + <div class="_gaps_m"> + <MkPreferenceContainer k="defaultNoteVisibility"> + <MkSelect v-model="defaultNoteVisibility"> + <option value="public">{{ i18n.ts._visibility.public }}</option> + <option value="home">{{ i18n.ts._visibility.home }}</option> + <option value="followers">{{ i18n.ts._visibility.followers }}</option> + <option value="specified">{{ i18n.ts._visibility.specified }}</option> + </MkSelect> + </MkPreferenceContainer> - <div class="_gaps_m"> - <MkSelect v-model="defaultNoteVisibility"> - <option value="public">{{ i18n.ts._visibility.public }}</option> - <option value="home">{{ i18n.ts._visibility.home }}</option> - <option value="followers">{{ i18n.ts._visibility.followers }}</option> - <option value="specified">{{ i18n.ts._visibility.specified }}</option> - </MkSelect> - <MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch> - </div> - </MkFolder> + <MkPreferenceContainer k="defaultNoteLocalOnly"> + <MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch> + </MkPreferenceContainer> + </div> + </MkFolder> + </MkDisableSection> </SearchMarker> </div> </FormSection> - - <SearchMarker :keywords="['remember', 'keep', 'note', 'cw']"> - <MkSwitch v-model="keepCw" @update:modelValue="save()"> - <template #label><SearchLabel>{{ i18n.ts.keepCw }}</SearchLabel></template> - </MkSwitch> - </SearchMarker> </div> </SearchMarker> </template> @@ -215,7 +218,6 @@ import MkSelect from '@/components/MkSelect.vue'; import FormSection from '@/components/form/section.vue'; import MkFolder from '@/components/MkFolder.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import { signinRequired } from '@/account.js'; @@ -225,6 +227,8 @@ import { formatDateTimeString } from '@/scripts/format-time-string.js'; import MkInput from '@/components/MkInput.vue'; import * as os from '@/os.js'; import MkDisableSection from '@/components/MkDisableSection.vue'; +import { prefer } from '@/preferences.js'; +import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue'; const $i = signinRequired(); @@ -241,10 +245,9 @@ const publicReactions = ref($i.publicReactions); const followingVisibility = ref($i.followingVisibility); const followersVisibility = ref($i.followersVisibility); -const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); -const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); -const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility')); -const keepCw = computed(defaultStore.makeGetterSetter('keepCw')); +const defaultNoteVisibility = prefer.model('defaultNoteVisibility'); +const defaultNoteLocalOnly = prefer.model('defaultNoteLocalOnly'); +const rememberNoteVisibility = prefer.model('rememberNoteVisibility'); const makeNotesFollowersOnlyBefore_type = computed(() => { if (makeNotesFollowersOnlyBefore.value == null) { diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 51148a1f72..f9ddbbc9ed 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -168,7 +168,7 @@ import { signinRequired } from '@/account.js'; import { langmap } from '@/scripts/langmap.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { claimAchievement } from '@/scripts/achievements.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { globalEvents } from '@/events.js'; import MkInfo from '@/components/MkInfo.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -177,7 +177,7 @@ const $i = signinRequired(); const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); -const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance')); +const reactionAcceptance = computed(store.makeGetterSetter('reactionAcceptance')); function assertVaildLang(lang: string | null): lang is keyof typeof langmap { return lang != null && lang in langmap; diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index 1df2d89277..808ae06f41 100644 --- a/packages/frontend/src/pages/settings/sounds.vue +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -7,21 +7,27 @@ SPDX-License-Identifier: AGPL-3.0-only <SearchMarker path="/settings/sounds" :label="i18n.ts.sounds" :keywords="['sounds']" icon="ti ti-music"> <div class="_gaps_m"> <SearchMarker :keywords="['mute']"> - <MkSwitch v-model="notUseSound"> - <template #label><SearchLabel>{{ i18n.ts.notUseSound }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="sound.notUseSound"> + <MkSwitch v-model="notUseSound"> + <template #label><SearchLabel>{{ i18n.ts.notUseSound }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['active', 'mute']"> - <MkSwitch v-model="useSoundOnlyWhenActive"> - <template #label><SearchLabel>{{ i18n.ts.useSoundOnlyWhenActive }}</SearchLabel></template> - </MkSwitch> + <MkPreferenceContainer k="sound.useSoundOnlyWhenActive"> + <MkSwitch v-model="useSoundOnlyWhenActive"> + <template #label><SearchLabel>{{ i18n.ts.useSoundOnlyWhenActive }}</SearchLabel></template> + </MkSwitch> + </MkPreferenceContainer> </SearchMarker> <SearchMarker :keywords="['volume', 'master']"> - <MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`"> - <template #label><SearchLabel>{{ i18n.ts.masterVolume }}</SearchLabel></template> - </MkRange> + <MkPreferenceContainer k="sound.masterVolume"> + <MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`"> + <template #label><SearchLabel>{{ i18n.ts.masterVolume }}</SearchLabel></template> + </MkRange> + </MkPreferenceContainer> </SearchMarker> <FormSection> @@ -52,7 +58,8 @@ import { computed, ref } from 'vue'; import XSound from './sounds.sound.vue'; import type { Ref } from 'vue'; import type { SoundType, OperationType } from '@/scripts/sound.js'; -import type { SoundStore } from '@/store.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { prefer } from '@/preferences.js'; import MkRange from '@/components/MkRange.vue'; import MkButton from '@/components/MkButton.vue'; import FormSection from '@/components/form/section.vue'; @@ -60,18 +67,19 @@ import MkFolder from '@/components/MkFolder.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { operationTypes } from '@/scripts/sound.js'; -import { defaultStore } from '@/store.js'; import MkSwitch from '@/components/MkSwitch.vue'; +import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue'; +import { PREF_DEF } from '@/preferences/def.js'; -const notUseSound = computed(defaultStore.makeGetterSetter('sound_notUseSound')); -const useSoundOnlyWhenActive = computed(defaultStore.makeGetterSetter('sound_useSoundOnlyWhenActive')); -const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume')); +const notUseSound = prefer.model('sound.notUseSound'); +const useSoundOnlyWhenActive = prefer.model('sound.useSoundOnlyWhenActive'); +const masterVolume = prefer.model('sound.masterVolume'); const sounds = ref<Record<OperationType, Ref<SoundStore>>>({ - note: defaultStore.reactiveState.sound_note, - noteMy: defaultStore.reactiveState.sound_noteMy, - notification: defaultStore.reactiveState.sound_notification, - reaction: defaultStore.reactiveState.sound_reaction, + note: prefer.r['sound.on.note'], + noteMy: prefer.r['sound.on.noteMy'], + notification: prefer.r['sound.on.notification'], + reaction: prefer.r['sound.on.reaction'], }); function getSoundTypeName(f: SoundType): string { @@ -93,14 +101,14 @@ async function updated(type: keyof typeof sounds.value, sound) { volume: sound.volume, }; - defaultStore.set(`sound_${type}`, v); + prefer.set(`sound.on.${type}`, v); sounds.value[type] = v; } function reset() { for (const sound of Object.keys(sounds.value) as Array<keyof typeof sounds.value>) { - const v = defaultStore.def[`sound_${sound}`].default; - defaultStore.set(`sound_${sound}`, v); + const v = PREF_DEF[`sound.on.${sound}`].default; + prefer.set(`sound.on.${sound}`, v); sounds.value[sound] = v; } } diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue index 140b6beb14..ede395e51e 100644 --- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue +++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue @@ -94,17 +94,17 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkButton from '@/components/MkButton.vue'; import MkRange from '@/components/MkRange.vue'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import { deepClone } from '@/scripts/clone.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ _id: string; userLists: Misskey.entities.UserList[] | null; }>(); -const statusbar = reactive(deepClone(defaultStore.state.statusbars.find(x => x.id === props._id))); +const statusbar = reactive(deepClone(prefer.s.statusbars.find(x => x.id === props._id))); watch(() => statusbar.type, () => { if (statusbar.type === 'rss') { @@ -134,13 +134,13 @@ watch(() => statusbar.type, () => { watch(statusbar, save); async function save() { - const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id); - const statusbars = deepClone(defaultStore.state.statusbars); + const i = prefer.s.statusbars.findIndex(x => x.id === props._id); + const statusbars = deepClone(prefer.s.statusbars); statusbars[i] = deepClone(statusbar); - defaultStore.set('statusbars', statusbars); + prefer.set('statusbars', statusbars); } function del() { - defaultStore.set('statusbars', defaultStore.state.statusbars.filter(x => x.id !== props._id)); + prefer.set('statusbars', prefer.s.statusbars.filter(x => x.id !== props._id)); } </script> diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue index 1ae3de7994..068d28bc4e 100644 --- a/packages/frontend/src/pages/settings/statusbar.vue +++ b/packages/frontend/src/pages/settings/statusbar.vue @@ -22,11 +22,11 @@ import XStatusbar from './statusbar.statusbar.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { prefer } from '@/preferences.js'; -const statusbars = defaultStore.reactiveState.statusbars; +const statusbars = prefer.r.statusbars; const userLists = ref<Misskey.entities.UserList[] | null>(null); @@ -37,13 +37,13 @@ onMounted(() => { }); async function add() { - defaultStore.push('statusbars', { + prefer.set('statusbars', [...statusbars.value, { id: uuid(), type: null, black: false, size: 'medium', props: {}, - }); + }]); } const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue index 4f05d3784c..b19b12aaab 100644 --- a/packages/frontend/src/pages/settings/theme.install.vue +++ b/packages/frontend/src/pages/settings/theme.install.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed } from 'vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkButton from '@/components/MkButton.vue'; -import { parseThemeCode, previewTheme, installTheme } from '@/scripts/install-theme.js'; +import { parseThemeCode, previewTheme, installTheme } from '@/scripts/theme.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue index b0e4ce13d5..41de2aa6a6 100644 --- a/packages/frontend/src/pages/settings/theme.vue +++ b/packages/frontend/src/pages/settings/theme.vue @@ -75,6 +75,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, onActivated, ref, watch } from 'vue'; import JSON5 from 'json5'; +import defaultLightTheme from '@@/themes/l-light.json5'; +import defaultDarkTheme from '@@/themes/d-green-lime.json5'; import type { MkSelectItem } from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; @@ -84,15 +86,15 @@ import MkButton from '@/components/MkButton.vue'; import { getBuiltinThemesRef } from '@/scripts/theme.js'; import { selectFile } from '@/scripts/select-file.js'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js'; -import { ColdDeviceStorage, defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import { uniqueBy } from '@/scripts/array.js'; -import { fetchThemes, getThemes } from '@/theme-store.js'; +import { getThemes } from '@/theme-store.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; import { reloadAsk } from '@/scripts/reload-ask.js'; -import * as os from '@/os.js'; +import { prefer } from '@/preferences.js'; const installedThemes = ref(getThemes()); const builtinThemes = getBuiltinThemesRef(); @@ -169,39 +171,39 @@ const darkThemeSelectorItems = computed(() => { return items; }); -const darkTheme = ColdDeviceStorage.ref('darkTheme'); +const darkTheme = prefer.r.darkTheme; const darkThemeId = computed({ get() { - return darkTheme.value.id; + return darkTheme.value ? darkTheme.value.id : defaultDarkTheme.id; }, set(id) { const t = themes.value.find(x => x.id === id); if (t) { // テーマエディタでテーマを作成したときなどは、themesに反映されないため undefined になる - ColdDeviceStorage.set('darkTheme', t); + prefer.set('darkTheme', t); } }, }); -const lightTheme = ColdDeviceStorage.ref('lightTheme'); +const lightTheme = prefer.r.lightTheme; const lightThemeId = computed({ get() { - return lightTheme.value.id; + return lightTheme.value ? lightTheme.value.id : defaultLightTheme.id; }, set(id) { const t = themes.value.find(x => x.id === id); if (t) { // テーマエディタでテーマを作成したときなどは、themesに反映されないため undefined になる - ColdDeviceStorage.set('lightTheme', t); + prefer.set('lightTheme', t); } }, }); -const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); -const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode')); +const darkMode = computed(store.makeGetterSetter('darkMode')); +const syncDeviceDarkMode = prefer.model('syncDeviceDarkMode'); const wallpaper = ref(miLocalStorage.getItem('wallpaper')); const themesCount = installedThemes.value.length; watch(syncDeviceDarkMode, () => { if (syncDeviceDarkMode.value) { - defaultStore.set('darkMode', isDeviceDarkmode()); + store.set('darkMode', isDeviceDarkmode()); } }); @@ -215,12 +217,6 @@ watch(wallpaper, async () => { }); onActivated(() => { - fetchThemes().then(() => { - installedThemes.value = getThemes(); - }); -}); - -fetchThemes().then(() => { installedThemes.value = getThemes(); }); diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index b669e25179..5fc921c55c 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -26,7 +26,7 @@ import MkButton from '@/components/MkButton.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import * as os from '@/os.js'; import { genEmbedCode } from '@/scripts/get-embed-code.js'; @@ -44,11 +44,11 @@ const pagination = { const notes = ref<InstanceType<typeof MkNotes>>(); async function post() { - defaultStore.set('postFormHashtags', props.tag); - defaultStore.set('postFormWithHashtags', true); + store.set('postFormHashtags', props.tag); + store.set('postFormWithHashtags', true); await os.post(); - defaultStore.set('postFormHashtags', ''); - defaultStore.set('postFormWithHashtags', false); + store.set('postFormHashtags', ''); + store.set('postFormWithHashtags', false); notes.value?.pagingComponent?.reload(); } diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index 76567cc403..c701030f9e 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -78,24 +78,23 @@ import { toUnicode } from 'punycode.js'; import tinycolor from 'tinycolor2'; import { v4 as uuid } from 'uuid'; import JSON5 from 'json5'; - import lightTheme from '@@/themes/_light.json5'; import darkTheme from '@@/themes/_dark.json5'; +import { host } from '@@/js/config.js'; +import type { Theme } from '@/scripts/theme.js'; import MkButton from '@/components/MkButton.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkFolder from '@/components/MkFolder.vue'; - import { $i } from '@/account.js'; import { applyTheme } from '@/scripts/theme.js'; -import type { Theme } from '@/scripts/theme.js'; -import { host } from '@@/js/config.js'; import * as os from '@/os.js'; -import { ColdDeviceStorage, defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { addTheme } from '@/theme-store.js'; import { i18n } from '@/i18n.js'; import { useLeaveGuard } from '@/scripts/use-leave-guard.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { prefer } from '@/preferences.js'; const bgColors = [ { color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' }, @@ -201,10 +200,10 @@ async function saveAs() { if (description.value) theme.value.desc = description.value; await addTheme(theme.value); applyTheme(theme.value); - if (defaultStore.state.darkMode) { - ColdDeviceStorage.set('darkTheme', theme.value); + if (store.state.darkMode) { + prefer.set('darkTheme', theme.value); } else { - ColdDeviceStorage.set('lightTheme', theme.value); + prefer.set('lightTheme', theme.value); } changed.value = false; os.alert({ diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 044a1908ab..4c15194672 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -9,10 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="800"> <MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin"> <div :key="src" ref="rootEl"> - <MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()"> + <MkInfo v-if="isBasicTimeline(src) && !store.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()"> {{ i18n.ts._timelineDescription[src] }} </MkInfo> - <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/> + <MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/> <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> <div :class="$style.tl"> <MkTimeline @@ -36,25 +36,26 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, provide, shallowRef, ref, onMounted, onActivated } from 'vue'; +import { scroll } from '@@/js/scroll.js'; import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; +import type { MenuItem } from '@/types/menu.js'; +import type { BasicTimelineType } from '@/timelines.js'; import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkPostForm from '@/components/MkPostForm.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; -import { scroll } from '@@/js/scroll.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { deepMerge } from '@/scripts/merge.js'; -import type { MenuItem } from '@/types/menu.js'; import { miLocalStorage } from '@/local-storage.js'; import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; -import type { BasicTimelineType } from '@/timelines.js'; +import { prefer } from '@/preferences.js'; provide('shouldOmitHeaderTitle', true); @@ -66,18 +67,18 @@ type TimelinePageSrc = BasicTimelineType | `list:${string}`; const queue = ref(0); const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global'); const src = computed<TimelinePageSrc>({ - get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value), + get: () => ($i ? store.reactiveState.tl.value.src : srcWhenNotSignin.value), set: (x) => saveSrc(x), }); const withRenotes = computed<boolean>({ - get: () => defaultStore.reactiveState.tl.value.filter.withRenotes, + get: () => store.reactiveState.tl.value.filter.withRenotes, set: (x) => saveTlFilter('withRenotes', x), }); // computed内での無限ループを防ぐためのフラグ const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>( - defaultStore.reactiveState.tl.value.filter.withReplies ? 'withReplies' : - defaultStore.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' : + store.reactiveState.tl.value.filter.withReplies ? 'withReplies' : + store.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' : false, ); @@ -87,7 +88,7 @@ const withReplies = computed<boolean>({ if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') { return false; } else { - return defaultStore.reactiveState.tl.value.filter.withReplies; + return store.reactiveState.tl.value.filter.withReplies; } }, set: (x) => saveTlFilter('withReplies', x), @@ -97,7 +98,7 @@ const onlyFiles = computed<boolean>({ if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') { return false; } else { - return defaultStore.reactiveState.tl.value.filter.onlyFiles; + return store.reactiveState.tl.value.filter.onlyFiles; } }, set: (x) => saveTlFilter('onlyFiles', x), @@ -114,7 +115,7 @@ watch([withReplies, onlyFiles], ([withRepliesTo, onlyFilesTo]) => { }); const withSensitive = computed<boolean>({ - get: () => defaultStore.reactiveState.tl.value.filter.withSensitive, + get: () => store.reactiveState.tl.value.filter.withSensitive, set: (x) => saveTlFilter('withSensitive', x), }); @@ -195,23 +196,23 @@ async function chooseChannel(ev: MouseEvent): Promise<void> { } function saveSrc(newSrc: TimelinePageSrc): void { - const out = deepMerge({ src: newSrc }, defaultStore.state.tl); + const out = deepMerge({ src: newSrc }, store.state.tl); if (newSrc.startsWith('userList:')) { const id = newSrc.substring('userList:'.length); - out.userList = defaultStore.reactiveState.pinnedUserLists.value.find(l => l.id === id) ?? null; + out.userList = prefer.r.pinnedUserLists.value.find(l => l.id === id) ?? null; } - defaultStore.set('tl', out); + store.set('tl', out); if (['local', 'global'].includes(newSrc)) { srcWhenNotSignin.value = newSrc as 'local' | 'global'; } } -function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue: boolean) { +function saveTlFilter(key: keyof typeof store.state.tl.filter, newValue: boolean) { if (key !== 'withReplies' || $i) { - const out = deepMerge({ filter: { [key]: newValue } }, defaultStore.state.tl); - defaultStore.set('tl', out); + const out = deepMerge({ filter: { [key]: newValue } }, store.state.tl); + store.set('tl', out); } } @@ -230,9 +231,9 @@ function focus(): void { function closeTutorial(): void { if (!isBasicTimeline(src.value)) return; - const before = defaultStore.state.timelineTutorials; + const before = store.state.timelineTutorials; before[src.value] = true; - defaultStore.set('timelineTutorials', before); + store.set('timelineTutorials', before); } function switchTlIfNeeded() { @@ -298,7 +299,7 @@ const headerActions = computed(() => { return tmp; }); -const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({ +const headerTabs = computed(() => [...(prefer.r.pinnedUserLists.value.map(l => ({ key: 'list:' + l.id, title: l.name, icon: 'ti ti-star', diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue index 7b74ea67ca..64d20f7945 100644 --- a/packages/frontend/src/pages/user/activity.following.vue +++ b/packages/frontend/src/pages/user/activity.following.vue @@ -20,7 +20,7 @@ import type { ChartDataset } from 'chart.js'; import * as Misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; import { initChart } from '@/scripts/init-chart.js'; @@ -64,7 +64,7 @@ async function renderChart() { const raw = await misskeyApi('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' }); - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const colorFollowLocal = '#008FFB'; const colorFollowRemote = '#008FFB88'; diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue index 8c7484ae08..cb171928eb 100644 --- a/packages/frontend/src/pages/user/activity.notes.vue +++ b/packages/frontend/src/pages/user/activity.notes.vue @@ -20,7 +20,7 @@ import type { ChartDataset } from 'chart.js'; import * as Misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; import { initChart } from '@/scripts/init-chart.js'; @@ -64,7 +64,7 @@ async function renderChart() { const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const colorNormal = '#008FFB'; const colorReply = '#FEB019'; diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue index a073626cbb..2804211f95 100644 --- a/packages/frontend/src/pages/user/activity.pv.vue +++ b/packages/frontend/src/pages/user/activity.pv.vue @@ -20,7 +20,7 @@ import type { ChartDataset } from 'chart.js'; import * as Misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; import { initChart } from '@/scripts/init-chart.js'; @@ -64,7 +64,7 @@ async function renderChart() { const raw = await misskeyApi('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' }); - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const colorUser = '#3498db'; const colorVisitor = '#2ecc71'; diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 8ebcf975b7..932d944dde 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -176,7 +176,6 @@ import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; import { $i, iAmModerator } from '@/account.js'; import { dateString } from '@/filters/date.js'; import { confetti } from '@/scripts/confetti.js'; @@ -185,6 +184,7 @@ import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFf import { useRouter } from '@/router/supplier.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import MkSparkle from '@/components/MkSparkle.vue'; +import { prefer } from '@/preferences.js'; function calcAge(birthdate: string): number { const date = new Date(birthdate); @@ -236,7 +236,7 @@ watch(moderationNote, async () => { const style = computed(() => { if (props.user.bannerUrl == null) return {}; - if (defaultStore.state.disableShowingAnimatedImages) { + if (prefer.s.disableShowingAnimatedImages) { return { backgroundImage: `url(${ getStaticImageUrl(props.user.bannerUrl) })`, }; diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts index 918b81b204..778d5c1981 100644 --- a/packages/frontend/src/pizzax.ts +++ b/packages/frontend/src/pizzax.ts @@ -11,7 +11,7 @@ import type { Ref } from 'vue'; import { $i } from '@/account.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { get, set } from '@/scripts/idb-proxy.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { useStream } from '@/stream.js'; import { deepClone } from '@/scripts/clone.js'; import { deepMerge } from '@/scripts/merge.js'; @@ -148,7 +148,7 @@ export class Storage<T extends StateDef> { if ($i) { // api関数と循環参照なので一応setTimeoutしておく window.setTimeout(async () => { - await defaultStore.ready; + await store.ready; misskeyApi('i/registry/get-all', { scope: ['client', this.key] }) .then(kvs => { diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index e319a8c398..609c819053 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -3,39 +3,208 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ref } from 'vue'; +import { ref, defineAsyncComponent } from 'vue'; import { Interpreter, Parser, utils, values } from '@syuilo/aiscript'; +import { compareVersions } from 'compare-versions'; +import { v4 as uuid } from 'uuid'; import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; -import { inputText } from '@/os.js'; -import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js'; -import type { Plugin } from '@/store.js'; +import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors, store } from '@/store.js'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; + +export type Plugin = { + installId: string; + name: string; + active: boolean; + config?: Record<string, { default: any }>; + configData: Record<string, any>; + src: string | null; + version: string; + author?: string; + description?: string; + permissions?: string[]; +}; + +export type AiScriptPluginMeta = { + name: string; + version: string; + author: string; + description?: string; + permissions?: string[]; + config?: Record<string, any>; +}; const parser = new Parser(); + +export function isSupportedAiScriptVersion(version: string): boolean { + try { + return (compareVersions(version, '0.12.0') >= 0); + } catch (err) { + return false; + } +} + +export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta> { + if (!code) { + throw new Error('code is required'); + } + + const lv = utils.getLangVersion(code); + if (lv == null) { + throw new Error('No language version annotation found'); + } else if (!isSupportedAiScriptVersion(lv)) { + throw new Error(`Aiscript version '${lv}' is not supported`); + } + + let ast; + try { + ast = parser.parse(code); + } catch (err) { + throw new Error('Aiscript syntax error'); + } + + const meta = Interpreter.collectMetadata(ast); + if (meta == null) { + throw new Error('Meta block not found'); + } + + const metadata = meta.get(null); + if (metadata == null) { + throw new Error('Metadata not found'); + } + + const { name, version, author, description, permissions, config } = metadata; + if (name == null || version == null || author == null) { + throw new Error('Required property not found'); + } + + return { + name, + version, + author, + description, + permissions, + config, + }; +} + +export async function authorizePlugin(plugin: Plugin) { + if (plugin.permissions == null || plugin.permissions.length === 0) return; + if (Object.hasOwn(store.state.pluginTokens, plugin.installId)) return; + + const token = await new Promise<string>((res, rej) => { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), { + title: i18n.ts.tokenRequested, + information: i18n.ts.pluginTokenRequestedDescription, + initialName: plugin.name, + initialPermissions: plugin.permissions, + }, { + done: async result => { + const { name, permissions } = result; + const { token } = await misskeyApi('miauth/gen-token', { + session: null, + name: name, + permission: permissions, + }); + res(token); + }, + closed: () => dispose(), + }); + }); + + store.set('pluginTokens', { + ...store.state.pluginTokens, + [plugin.installId]: token, + }); +} + +export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { + if (!code) return; + + let realMeta: AiScriptPluginMeta; + if (!meta) { + realMeta = await parsePluginMeta(code); + } else { + realMeta = meta; + } + + const installId = uuid(); + + const plugin = { + ...realMeta, + installId, + active: true, + configData: {}, + src: code, + }; + + prefer.set('plugins', prefer.s.plugins.concat(plugin)); + + await authorizePlugin(plugin); +} + +export async function uninstallPlugin(plugin: Plugin) { + prefer.set('plugins', prefer.s.plugins.filter(x => x.installId !== plugin.installId)); + if (Object.hasOwn(store.state.pluginTokens, plugin.installId)) { + await os.apiWithDialog('i/revoke-token', { + token: store.state.pluginTokens[plugin.installId], + }); + const pluginTokens = { ...store.state.pluginTokens }; + delete pluginTokens[plugin.installId]; + store.set('pluginTokens', pluginTokens); + } +} + +export async function configPlugin(plugin: Plugin) { + if (plugin.config == null) { + throw new Error('This plugin does not have a config'); + } + + const config = plugin.config; + for (const key in plugin.configData) { + config[key].default = plugin.configData[key]; + } + + const { canceled, result } = await os.form(plugin.name, config); + if (canceled) return; + + prefer.set('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, configData: result } : x)); +} + +export function changePluginActive(plugin: Plugin, active: boolean) { + prefer.set('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, active } : x)); +} + const pluginContexts = new Map<string, Interpreter>(); export const pluginLogs = ref(new Map<string, string[]>()); -export async function install(plugin: Plugin): Promise<void> { +export async function launchPlugin(plugin: Plugin): Promise<void> { // 後方互換性のため if (plugin.src == null) return; + await authorizePlugin(plugin); + const aiscript = new Interpreter(createPluginEnv({ plugin: plugin, - storageKey: 'plugins:' + plugin.id, + storageKey: 'plugins:' + plugin.installId, }), { in: aiScriptReadline, out: (value): void => { console.log(value); - pluginLogs.value.get(plugin.id).push(utils.reprValue(value)); + pluginLogs.value.get(plugin.installId).push(utils.reprValue(value)); }, log: (): void => { }, err: (err): void => { - pluginLogs.value.get(plugin.id).push(`${err}`); + pluginLogs.value.get(plugin.installId).push(`${err}`); throw err; // install時のtry-catchに反応させる }, }); - initPlugin({ plugin, aiscript }); + pluginContexts.set(plugin.installId, aiscript); + pluginLogs.value.set(plugin.installId, []); aiscript.exec(parser.parse(plugin.src)).then( () => { @@ -49,47 +218,36 @@ export async function install(plugin: Plugin): Promise<void> { } function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> { + const id = opts.plugin.installId; + const config = new Map<string, values.Value>(); for (const [k, v] of Object.entries(opts.plugin.config ?? {})) { config.set(k, utils.jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default)); } return { - ...createAiScriptEnv({ ...opts, token: opts.plugin.token }), - //#region Deprecated - 'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => { - utils.assertString(title); - registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => { - utils.assertString(title); - registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => { - utils.assertString(title); - registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler }); - }), - //#endregion + ...createAiScriptEnv({ ...opts, token: store.state.pluginTokens[id] }), + 'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => { utils.assertString(title); - registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler }); + registerPostFormAction({ pluginId: id, title: title.value, handler }); }), 'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => { utils.assertString(title); - registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler }); + registerUserAction({ pluginId: id, title: title.value, handler }); }), 'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => { utils.assertString(title); - registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler }); + registerNoteAction({ pluginId: id, title: title.value, handler }); }), 'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => { - registerNoteViewInterruptor({ pluginId: opts.plugin.id, handler }); + registerNoteViewInterruptor({ pluginId: id, handler }); }), 'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => { - registerNotePostInterruptor({ pluginId: opts.plugin.id, handler }); + registerNotePostInterruptor({ pluginId: id, handler }); }), 'Plugin:register_page_view_interruptor': values.FN_NATIVE(([handler]) => { - registerPageViewInterruptor({ pluginId: opts.plugin.id, handler }); + registerPageViewInterruptor({ pluginId: id, handler }); }), 'Plugin:open_url': values.FN_NATIVE(([url]) => { utils.assertString(url); @@ -99,11 +257,6 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s }; } -function initPlugin({ plugin, aiscript }): void { - pluginContexts.set(plugin.id, aiscript); - pluginLogs.value.set(plugin.id, []); -} - function registerPostFormAction({ pluginId, title, handler }): void { postFormActions.push({ title, handler: (form, update) => { diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts new file mode 100644 index 0000000000..03da3be465 --- /dev/null +++ b/packages/frontend/src/preferences.ts @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { v4 as uuid } from 'uuid'; +import type { PreferencesProfile } from '@/preferences/profile.js'; +import { cloudBackup } from '@/preferences/utility.js'; +import { miLocalStorage } from '@/local-storage.js'; +import { ProfileManager } from '@/preferences/profile.js'; +import { store } from '@/store.js'; +import { $i } from '@/account.js'; + +const TAB_ID = uuid(); + +function createProfileManager() { + let profile: PreferencesProfile; + + const savedProfileRaw = miLocalStorage.getItem('preferences'); + if (savedProfileRaw == null) { + profile = ProfileManager.newProfile(); + miLocalStorage.setItem('preferences', JSON.stringify(profile)); + } else { + profile = ProfileManager.normalizeProfile(JSON.parse(savedProfileRaw)); + } + + return new ProfileManager(profile); +} + +export const profileManager = createProfileManager(); +profileManager.addListener('updated', ({ profile: p }) => { + miLocalStorage.setItem('preferences', JSON.stringify(p)); + miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`); +}); +export const prefer = profileManager.store; + +let latestSyncedAt = Date.now(); + +function syncBetweenTabs() { + const latest = miLocalStorage.getItem('latestPreferencesUpdate'); + if (latest == null) return; + + const latestTab = latest.split('/')[0]; + const latestAt = parseInt(latest.split('/')[1]); + + if (latestTab === TAB_ID) return; + if (latestAt <= latestSyncedAt) return; + + profileManager.rewriteProfile(ProfileManager.normalizeProfile(JSON.parse(miLocalStorage.getItem('preferences')!))); + + latestSyncedAt = Date.now(); + + if (_DEV_) console.log('prefer:synced'); +} + +window.setInterval(syncBetweenTabs, 5000); + +document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + syncBetweenTabs(); + } +}); + +let latestBackupAt = 0; + +window.setInterval(() => { + if ($i == null) return; + if (!store.state.enablePreferencesAutoCloudBackup) return; + if (document.visibilityState !== 'visible') return; // 同期されていない古い値がバックアップされるのを防ぐ + if (profileManager.profile.modifiedAt <= latestBackupAt) return; + + cloudBackup().then(() => { + latestBackupAt = Date.now(); + }); +}, 1000 * 60 * 3); + +if (_DEV_) { + (window as any).profileManager = profileManager; + (window as any).prefer = prefer; + (window as any).cloudBackup = cloudBackup; +} diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts new file mode 100644 index 0000000000..19284de6a1 --- /dev/null +++ b/packages/frontend/src/preferences/def.ts @@ -0,0 +1,308 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { hemisphere } from '@@/js/intl-const.js'; +import type { Theme } from '@/scripts/theme.js'; +import type { SoundType } from '@/scripts/sound.js'; +import type { Plugin } from '@/plugin.js'; +import { DEFAULT_DEVICE_KIND } from '@/scripts/device-kind.js'; + +/** サウンド設定 */ +export type SoundStore = { + type: Exclude<SoundType, '_driveFile_'>; + volume: number; +} | { + type: '_driveFile_'; + + /** ドライブのファイルID */ + fileId: string; + + /** ファイルURL(こちらが優先される) */ + fileUrl: string; + + volume: number; +}; + +export const PREF_DEF = { + pinnedUserLists: { + accountDependent: true, + default: [] as Misskey.entities.UserList[], + }, + uploadFolder: { + accountDependent: true, + default: null as string | null, + }, + + themes: { + default: [] as Theme[], + }, + lightTheme: { + default: null as Theme | null, + }, + darkTheme: { + default: null as Theme | null, + }, + syncDeviceDarkMode: { + default: true, + }, + defaultNoteVisibility: { + default: 'public' as (typeof Misskey.noteVisibilities)[number], + }, + defaultNoteLocalOnly: { + default: false, + }, + keepCw: { + default: true, + }, + keepOriginalUploading: { + default: false, + }, + rememberNoteVisibility: { + default: false, + }, + reportError: { + default: false, + }, + collapseRenotes: { + default: true, + }, + menu: { + default: [ + 'notifications', + 'clips', + 'drive', + 'followRequests', + '-', + 'explore', + 'announcements', + 'search', + '-', + 'ui', + ], + }, + statusbars: { + default: [] as { + name: string; + id: string; + type: string; + size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge'; + black: boolean; + props: Record<string, any>; + }[], + }, + serverDisconnectedBehavior: { + default: 'quiet' as 'quiet' | 'reload' | 'dialog', + }, + nsfw: { + default: 'respect' as 'respect' | 'force' | 'ignore', + }, + highlightSensitiveMedia: { + default: false, + }, + animation: { + default: !window.matchMedia('(prefers-reduced-motion)').matches, + }, + animatedMfm: { + default: !window.matchMedia('(prefers-reduced-motion)').matches, + }, + advancedMfm: { + default: true, + }, + showReactionsCount: { + default: false, + }, + enableQuickAddMfmFunction: { + default: false, + }, + loadRawImages: { + default: false, + }, + imageNewTab: { + default: false, + }, + disableShowingAnimatedImages: { + default: window.matchMedia('(prefers-reduced-motion)').matches, + }, + emojiStyle: { + default: 'twemoji', // twemoji / fluentEmoji / native + }, + menuStyle: { + default: 'auto' as 'auto' | 'popup' | 'drawer', + }, + useBlurEffectForModal: { + default: DEFAULT_DEVICE_KIND === 'desktop', + }, + useBlurEffect: { + default: DEFAULT_DEVICE_KIND === 'desktop', + }, + showFixedPostForm: { + default: false, + }, + showFixedPostFormInChannel: { + default: false, + }, + enableInfiniteScroll: { + default: true, + }, + useReactionPickerForContextMenu: { + default: false, + }, + showGapBetweenNotesInTimeline: { + default: false, + }, + instanceTicker: { + default: 'remote' as 'none' | 'remote' | 'always', + }, + emojiPickerScale: { + default: 1, + }, + emojiPickerWidth: { + default: 1, + }, + emojiPickerHeight: { + default: 2, + }, + emojiPickerStyle: { + default: 'auto' as 'auto' | 'popup' | 'drawer', + }, + squareAvatars: { + default: false, + }, + showAvatarDecorations: { + default: true, + }, + numberOfPageCache: { + default: 3, + }, + showNoteActionsOnlyHover: { + default: false, + }, + showClipButtonInNoteFooter: { + default: false, + }, + reactionsDisplaySize: { + default: 'medium' as 'small' | 'medium' | 'large', + }, + limitWidthOfReaction: { + default: true, + }, + forceShowAds: { + default: false, + }, + aiChanMode: { + default: false, + }, + devMode: { + default: false, + }, + mediaListWithOneImageAppearance: { + default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3', + }, + notificationPosition: { + default: 'rightBottom' as 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom', + }, + notificationStackAxis: { + default: 'horizontal' as 'vertical' | 'horizontal', + }, + enableCondensedLine: { + default: true, + }, + keepScreenOn: { + default: false, + }, + disableStreamingTimeline: { + default: false, + }, + useGroupedNotifications: { + default: true, + }, + dataSaver: { + default: { + media: false, + avatar: false, + urlPreview: false, + code: false, + } as Record<string, boolean>, + }, + hemisphere: { + default: hemisphere as 'N' | 'S', + }, + enableSeasonalScreenEffect: { + default: false, + }, + enableHorizontalSwipe: { + default: true, + }, + useNativeUiForVideoAudioPlayer: { + default: false, + }, + keepOriginalFilename: { + default: true, + }, + alwaysConfirmFollow: { + default: true, + }, + confirmWhenRevealingSensitiveMedia: { + default: false, + }, + contextMenu: { + default: 'app' as 'app' | 'appWithShift' | 'native', + }, + skipNoteRender: { + default: true, + }, + showSoftWordMutedWord: { + default: false, + }, + confirmOnReact: { + default: false, + }, + plugins: { + default: [] as Plugin[], + }, + 'sound.masterVolume': { + default: 0.3, + }, + 'sound.notUseSound': { + default: false, + }, + 'sound.useSoundOnlyWhenActive': { + default: false, + }, + 'sound.on.note': { + default: { type: 'syuilo/n-aec', volume: 1 } as SoundStore, + }, + 'sound.on.noteMy': { + default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore, + }, + 'sound.on.notification': { + default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore, + }, + 'sound.on.reaction': { + default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore, + }, + 'deck.alwaysShowMainColumn': { + default: true, + }, + 'deck.navWindow': { + default: true, + }, + 'deck.useSimpleUiForNonRootPages': { + default: true, + }, + 'deck.columnAlign': { + default: 'left' as 'left' | 'right' | 'center', + }, + 'game.dropAndFusion': { + default: { + bgmVolume: 0.25, + sfxVolume: 1, + }, + }, +} satisfies Record<string, { + default: any; + accountDependent?: boolean; +}>; diff --git a/packages/frontend/src/preferences/profile.ts b/packages/frontend/src/preferences/profile.ts new file mode 100644 index 0000000000..516fa2b557 --- /dev/null +++ b/packages/frontend/src/preferences/profile.ts @@ -0,0 +1,236 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ref, watch } from 'vue'; +import { v4 as uuid } from 'uuid'; +import { host, version } from '@@/js/config.js'; +import { EventEmitter } from 'eventemitter3'; +import { PREF_DEF } from './def.js'; +import { Store } from './store.js'; +import type { MenuItem } from '@/types/menu.js'; +import { $i } from '@/account.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { i18n } from '@/i18n.js'; + +// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない + +//type DottedToNested<T extends Record<string, any>> = { +// [K in keyof T as K extends string ? K extends `${infer A}.${infer B}` ? A : K : K]: K extends `${infer A}.${infer B}` ? DottedToNested<{ [key in B]: T[K] }> : T[K]; +//}; + +type PREF = typeof PREF_DEF; +type ValueOf<K extends keyof PREF> = PREF[K]['default']; +type Account = string; // <host>/<userId> + +type Cond = { + server: string | null; // 将来のため + account: Account | null; + device: string | null; // 将来のため +}; + +export type PreferencesProfile = { + id: string; + version: string; + type: 'main'; + modifiedAt: number; + name: string; + preferences: { + [K in keyof PREF]: [Cond, ValueOf<K>][]; + }; + syncByAccount: [Account, keyof PREF][], +}; + +export class ProfileManager extends EventEmitter<{ + updated: (ctx: { + profile: PreferencesProfile + }) => void; +}> { + public profile: PreferencesProfile; + public store: Store<{ + [K in keyof PREF]: ValueOf<K>; + }>; + + constructor(profile: PreferencesProfile) { + super(); + this.profile = profile; + + const states = this.genStates(); + + this.store = new Store(states); + this.store.addListener('updated', ({ key, value }) => { + console.log('prefer:set', key, value); + + const record = this.getMatchedRecord(key); + if (record[0].account == null && PREF_DEF[key].accountDependent) { + this.profile.preferences[key].push([{ + server: null, + account: `${host}/${$i!.id}`, + device: null, + }, value]); + this.save(); + return; + } + + record[1] = value; + this.save(); + }); + } + + private genStates() { + const states = {} as { [K in keyof PREF]: ValueOf<K> }; + let key: keyof PREF; + for (key in PREF_DEF) { + const record = this.getMatchedRecord(key); + states[key] = record[1]; + } + + return states; + } + + public static newProfile(): PreferencesProfile { + const data = {} as PreferencesProfile['preferences']; + let key: keyof PREF; + for (key in PREF_DEF) { + data[key] = [[{ + server: null, + account: null, + device: null, + }, PREF_DEF[key].default]]; + } + return { + id: uuid(), + version: version, + type: 'main', + modifiedAt: Date.now(), + name: '', + preferences: data, + syncByAccount: [], + }; + } + + public static normalizeProfile(profile: any): PreferencesProfile { + const data = {} as PreferencesProfile['preferences']; + let key: keyof PREF; + for (key in PREF_DEF) { + const records = profile.preferences[key]; + if (records == null || records.length === 0) { + data[key] = [[{ + server: null, + account: null, + device: null, + }, PREF_DEF[key].default]]; + continue; + } else { + data[key] = records; + } + } + + return { + ...profile, + preferences: data, + }; + } + + public save() { + this.profile.modifiedAt = Date.now(); + this.profile.version = version; + this.emit('updated', { profile: this.profile }); + } + + public getMatchedRecord<K extends keyof PREF>(key: K): [Cond, ValueOf<K>] { + const records = this.profile.preferences[key]; + + if ($i == null) return records.find(([cond, v]) => cond.account == null)!; + + const accountOverrideRecord = records.find(([cond, v]) => cond.account === `${host}/${$i!.id}`); + if (accountOverrideRecord) return accountOverrideRecord; + + const record = records.find(([cond, v]) => cond.account == null); + return record!; + } + + public isAccountOverrided<K extends keyof PREF>(key: K): boolean { + if ($i == null) return false; + return this.profile.preferences[key].some(([cond, v]) => cond.account === `${host}/${$i!.id}`) ?? false; + } + + public setAccountOverride<K extends keyof PREF>(key: K) { + if ($i == null) return; + if (PREF_DEF[key].accountDependent) throw new Error('already account-dependent'); + if (this.isAccountOverrided(key)) return; + + const records = this.profile.preferences[key]; + records.push([{ + server: null, + account: `${host}/${$i!.id}`, + device: null, + }, this.store.s[key]]); + + this.save(); + } + + public clearAccountOverride<K extends keyof PREF>(key: K) { + if ($i == null) return; + if (PREF_DEF[key].accountDependent) throw new Error('cannot clear override for this account-dependent property'); + + const records = this.profile.preferences[key]; + + const index = records.findIndex(([cond, v]) => cond.account === `${host}/${$i!.id}`); + if (index === -1) return; + + records.splice(index, 1); + + this.store.rewrite(key, this.getMatchedRecord(key)[1]); + + this.save(); + } + + public renameProfile(name: string) { + this.profile.name = name; + this.save(); + } + + public rewriteProfile(profile: PreferencesProfile) { + this.profile = profile; + const states = this.genStates(); + for (const key in states) { + this.store.rewrite(key, states[key]); + } + } + + public getPerPrefMenu<K extends keyof PREF>(key: K): MenuItem[] { + const overrideByAccount = ref(this.isAccountOverrided(key)); + + watch(overrideByAccount, () => { + if (overrideByAccount.value) { + this.setAccountOverride(key); + } else { + this.clearAccountOverride(key); + } + }); + + return [{ + icon: 'ti ti-copy', + text: i18n.ts.copyPreferenceId, + action: () => { + copyToClipboard(key); + }, + }, { + icon: 'ti ti-refresh', + text: i18n.ts.resetToDefaultValue, + danger: true, + action: () => { + this.store.set(key, PREF_DEF[key].default); + }, + }, { + type: 'divider', + }, { + type: 'switch', + icon: 'ti ti-user-cog', + text: i18n.ts.overrideByAccount, + ref: overrideByAccount, + }]; + } +} diff --git a/packages/frontend/src/preferences/store.ts b/packages/frontend/src/preferences/store.ts new file mode 100644 index 0000000000..1029346d81 --- /dev/null +++ b/packages/frontend/src/preferences/store.ts @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { computed, onUnmounted, ref, watch } from 'vue'; +import { EventEmitter } from 'eventemitter3'; +import type { Ref, WritableComputedRef } from 'vue'; + +// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない + +//type DottedToNested<T extends Record<string, any>> = { +// [K in keyof T as K extends string ? K extends `${infer A}.${infer B}` ? A : K : K]: K extends `${infer A}.${infer B}` ? DottedToNested<{ [key in B]: T[K] }> : T[K]; +//}; + +type StoreEvent<Data extends Record<string, any>> = { + updated: <K extends keyof Data>(ctx: { + key: K; + value: Data[K]; + }) => void; +}; + +export class Store<Data extends Record<string, any>> extends EventEmitter<StoreEvent<Data>> { + /** + * static の略 (static が予約語のため) + */ + public s = {} as { + [K in keyof Data]: Data[K]; + }; + + /** + * reactive の略 + */ + public r = {} as { + [K in keyof Data]: Ref<Data[K]>; + }; + + constructor(data: { [K in keyof Data]: Data[K] }) { + super(); + + for (const key in data) { + this.s[key] = data[key]; + this.r[key] = ref(this.s[key]); + } + } + + public set<K extends keyof Data>(key: K, value: Data[K]) { + this.r[key].value = this.s[key] = value; + this.emit('updated', { key, value }); + } + + public rewrite<K extends keyof Data>(key: K, value: Data[K]) { + this.r[key].value = this.s[key] = value; + } + + /** + * 特定のキーの、簡易的なcomputed refを作ります + * 主にvue上で設定コントロールのmodelとして使う用 + */ + public model<K extends keyof Data, V extends Data[K] = Data[K]>( + key: K, + getter?: (v: Data[K]) => V, + setter?: (v: V) => Data[K], + ): WritableComputedRef<V> { + const valueRef = ref(this.s[key]); + + const stop = watch(this.r[key], val => { + valueRef.value = val; + }); + + // NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする + onUnmounted(() => { + stop(); + }); + + // TODO: VueのcustomRef使うと良い感じになるかも + return computed({ + get: () => { + if (getter) { + return getter(valueRef.value); + } else { + return valueRef.value; + } + }, + set: (value) => { + const val = setter ? setter(value) : value; + this.set(key, val); + valueRef.value = val; + }, + }); + } +} diff --git a/packages/frontend/src/preferences/utility.ts b/packages/frontend/src/preferences/utility.ts new file mode 100644 index 0000000000..7979695231 --- /dev/null +++ b/packages/frontend/src/preferences/utility.ts @@ -0,0 +1,222 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ref, watch } from 'vue'; +import type { PreferencesProfile } from './profile.js'; +import type { MenuItem } from '@/types/menu.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { i18n } from '@/i18n.js'; +import { miLocalStorage } from '@/local-storage.js'; +import { prefer, profileManager } from '@/preferences.js'; +import * as os from '@/os.js'; +import { store } from '@/store.js'; +import { $i } from '@/account.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { unisonReload } from '@/scripts/unison-reload.js'; + +export function getPreferencesProfileMenu(): MenuItem[] { + const autoBackupEnabled = ref(store.state.enablePreferencesAutoCloudBackup); + + watch(autoBackupEnabled, () => { + if (autoBackupEnabled.value) { + if (profileManager.profile.name == null || profileManager.profile.name.trim() === '') { + autoBackupEnabled.value = false; + os.alert({ + type: 'warning', + title: i18n.ts._preferencesBackup.youNeedToNameYourProfileToEnableAutoBackup, + }); + return; + } + + store.set('enablePreferencesAutoCloudBackup', true); + } else { + store.set('enablePreferencesAutoCloudBackup', false); + } + }); + + const menu: MenuItem[] = [{ + type: 'label', + text: profileManager.profile.name || `(${i18n.ts.noName})`, + }, { + text: i18n.ts.rename, + icon: 'ti ti-pencil', + action: () => { + renameProfile(); + }, + }, { + type: 'switch', + icon: 'ti ti-cloud-up', + text: i18n.ts._preferencesBackup.autoBackup, + ref: autoBackupEnabled, + }, { + text: i18n.ts.export, + icon: 'ti ti-download', + action: () => { + exportCurrentProfile(); + }, + }, { + type: 'divider', + }, { + text: i18n.ts._preferencesBackup.restoreFromBackup, + icon: 'ti ti-cloud-down', + action: () => { + restoreFromCloudBackup(); + }, + }, { + text: i18n.ts.import, + icon: 'ti ti-upload', + action: () => { + importProfile(); + }, + }]; + + if (prefer.s.devMode) { + menu.push({ + type: 'divider', + }, { + text: 'Copy profile as text', + icon: 'ti ti-clipboard', + action: () => { + copyToClipboard(JSON.stringify(profileManager.profile, null, '\t')); + }, + }); + } + + return menu; +} + +async function renameProfile() { + const { canceled, result: name } = await os.inputText({ + title: i18n.ts._preferencesProfile.profileName, + text: i18n.ts._preferencesProfile.profileNameDescription + '\n' + i18n.ts._preferencesProfile.profileNameDescription2, + placeholder: profileManager.profile.name || null, + default: profileManager.profile.name || null, + }); + if (canceled || name == null || name.trim() === '') return; + + profileManager.renameProfile(name); +} + +function exportCurrentProfile() { + const p = profileManager.profile; + const txtBlob = new Blob([JSON.stringify(p)], { type: 'text/plain' }); + const dummya = document.createElement('a'); + dummya.href = URL.createObjectURL(txtBlob); + dummya.download = `${p.name || p.id}.misskeypreferences`; + dummya.click(); +} + +function importProfile() { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.misskeypreferences'; + input.onchange = async () => { + if (input.files == null || input.files.length === 0) return; + + const file = input.files[0]; + const txt = await file.text(); + const profile = JSON.parse(txt) as PreferencesProfile; + + miLocalStorage.setItem('preferences', JSON.stringify(profile)); + miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true'); + shouldSuggestRestoreBackup.value = false; + unisonReload(); + }; + + input.click(); +} + +export async function cloudBackup() { + if ($i == null) return; + if (profileManager.profile.name == null || profileManager.profile.name.trim() === '') { + throw new Error('Profile name is not set'); + } + + await misskeyApi('i/registry/set', { + scope: ['client', 'preferences', 'backups'], + key: profileManager.profile.name, + value: profileManager.profile, + }); +} + +export async function restoreFromCloudBackup() { + if ($i == null) return; + + // TODO: 更新日時でソートして取得したい + const keys = await misskeyApi('i/registry/keys', { + scope: ['client', 'preferences', 'backups'], + }); + + console.log(keys); + + if (keys.length === 0) { + os.alert({ + type: 'warning', + title: i18n.ts._preferencesBackup.noBackupsFoundTitle, + text: i18n.ts._preferencesBackup.noBackupsFoundDescription, + }); + return; + } + + const select = await os.select({ + title: i18n.ts._preferencesBackup.selectBackupToRestore, + items: keys.map(k => ({ + text: k, + value: k, + })), + }); + if (select.canceled) return; + if (select.result == null) return; + + const profile = await misskeyApi('i/registry/get', { + scope: ['client', 'preferences', 'backups'], + key: select.result, + }); + + console.log(profile); + + miLocalStorage.setItem('preferences', JSON.stringify(profile)); + miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true'); + store.set('enablePreferencesAutoCloudBackup', true); + shouldSuggestRestoreBackup.value = false; + unisonReload(); +} + +export async function enableAutoBackup() { + if (profileManager.profile.name == null || profileManager.profile.name.trim() === '') { + await renameProfile(); + } + + if (profileManager.profile.name == null || profileManager.profile.name.trim() === '') { + return; + } + + store.set('enablePreferencesAutoCloudBackup', true); +} + +export const shouldSuggestRestoreBackup = ref(false); + +if ($i != null) { + if (new Date($i.createdAt).getTime() < (Date.now() - 1000 * 60 * 30)) { // アカウント作成直後は意味ないので除外 + miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true'); + } else { + if (miLocalStorage.getItem('hidePreferencesRestoreSuggestion') !== 'true') { + misskeyApi('i/registry/keys', { + scope: ['client', 'preferences', 'backups'], + }).then(keys => { + if (keys.length === 0) { + miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true'); + } else { + shouldSuggestRestoreBackup.value = true; + } + }); + } + } +} + +export function hideRestoreBackupSuggestion() { + miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true'); + shouldSuggestRestoreBackup.value = false; +} diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index c6ee128f5f..7e0efa2e89 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -166,10 +166,6 @@ const routes: RouteDef[] = [{ name: 'deck', component: page(() => import('@/pages/settings/deck.vue')), }, { - path: '/preferences-backups', - name: 'preferences-backups', - component: page(() => import('@/pages/settings/preferences-backups.vue')), - }, { path: '/custom-css', name: 'preferences', component: page(() => import('@/pages/settings/custom-css.vue')), diff --git a/packages/frontend/src/scripts/autogen/settings-search-index.ts b/packages/frontend/src/scripts/autogen/settings-search-index.ts index c62272b271..983bc07d38 100644 --- a/packages/frontend/src/scripts/autogen/settings-search-index.ts +++ b/packages/frontend/src/scripts/autogen/settings-search-index.ts @@ -57,12 +57,12 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['mute'], }, { - id: 'xy5OOBB4A', + id: 'oALW4ja7U', label: i18n.ts.useSoundOnlyWhenActive, keywords: ['active', 'mute'], }, { - id: '9MxYVIf7k', + id: 'BbJK2SKT2', label: i18n.ts.masterVolume, keywords: ['volume', 'master'], }, @@ -267,15 +267,10 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['remember', 'keep', 'note', 'visibility'], }, { - id: 'rhKwScbVS', + id: '1u5HZuujV', label: i18n.ts.defaultNoteVisibility, keywords: ['default', 'note', 'visibility'], }, - { - id: '3EmXVyevo', - label: i18n.ts.keepCw, - keywords: ['remember', 'keep', 'note', 'cw'], - }, ], label: i18n.ts.privacy, keywords: ['privacy'], @@ -301,50 +296,50 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['post', 'form', 'timeline'], }, { - id: '9ra14w32V', + id: 'snyCQ5oKE', label: i18n.ts.showFixedPostFormInChannel, keywords: ['post', 'form', 'timeline', 'channel'], }, { - id: '84MdeDWL1', + id: '8j36S4Ev6', label: i18n.ts.pinnedList, keywords: ['pinned', 'list'], }, { - id: 'fYdWhBbrN', + id: 'CWpyT9vLK', label: i18n.ts.enableQuickAddMfmFunction, keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'], }, { - id: '4huRldNp5', + id: 'puIqj1a8b', children: [ { - id: 'puIqj1a8b', + id: '1x3JNXj8N', label: i18n.ts.collapseRenotes, keywords: ['renote', i18n.ts.collapseRenotesDescription], }, { - id: 'wqpOC22Zm', + id: 'c98gbF9c6', label: i18n.ts.showNoteActionsOnlyHover, keywords: ['hover', 'show', 'footer', 'action'], }, { - id: 'cjfAtxMzP', + id: '4LxdiOMNh', label: i18n.ts.showClipButtonInNoteFooter, keywords: ['footer', 'action', 'clip', 'show'], }, { - id: 'khzxoCjtp', + id: '9gTCaLkIf', label: i18n.ts.enableAdvancedMfm, keywords: ['mfm', 'enable', 'show', 'advanced'], }, { - id: 'uJkoVjTmF', + id: '6kMj4HVOg', label: i18n.ts.showReactionsCount, keywords: ['reaction', 'count', 'show'], }, { - id: '9gTCaLkIf', + id: 'dPersnkzh', label: i18n.ts.loadRawImages, keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'], }, @@ -353,10 +348,10 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['note'], }, { - id: '5G6O6qdis', + id: '5XhS6ukl8', children: [ { - id: 'sYTvqUbhP', + id: '3GcWIaZf8', label: i18n.ts.useGroupedNotifications, keywords: ['group'], }, @@ -365,55 +360,60 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['notification'], }, { - id: 'c3xhLyXZ5', + id: 'dSGDnj2PA', children: [ { - id: 'FbhoeuRAD', + id: '1LHOhDKGW', label: i18n.ts.openImageInNewTab, keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'], }, { - id: 'qixh85g2N', + id: 'DSzwvTp7i', label: i18n.ts.useReactionPickerForContextMenu, keywords: ['reaction', 'picker', 'contextmenu', 'open'], }, { - id: 'd2H4E5ys6', + id: '5QTUzrpT3', label: i18n.ts.enableInfiniteScroll, keywords: ['load', 'auto', 'more'], }, { - id: 'jC7LtTnmc', + id: '7Uf8ksn3q', label: i18n.ts.disableStreamingTimeline, keywords: ['disable', 'streaming', 'timeline'], }, { - id: '8xazEqlgZ', + id: 'whKYKvaQB', label: i18n.ts.alwaysConfirmFollow, keywords: ['follow', 'confirm', 'always'], }, { - id: 'wZqrDQZar', + id: 'nf4kcPeYw', label: i18n.ts.confirmWhenRevealingSensitiveMedia, keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'], }, { - id: '5QTUzrpT3', + id: 'rRisK1YYQ', label: i18n.ts.confirmOnReact, keywords: ['reaction', 'confirm'], }, { - id: 'nygexkaUk', + id: '6AH0lnjf1', + label: i18n.ts.keepCw, + keywords: ['remember', 'keep', 'note', 'cw'], + }, + { + id: 'uHcTVSGDv', label: i18n.ts.whenServerDisconnected, keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'], }, { - id: 'whKYKvaQB', + id: 'fzPca1Gk9', label: i18n.ts.numberOfPageCache, keywords: ['cache', 'page'], }, { - id: 'lBbtAg0Hm', + id: 'mNU5IBln7', label: i18n.ts.dataSaver, keywords: ['datasaver'], }, @@ -422,20 +422,20 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['behavior'], }, { - id: 'y2v7CV9zs', + id: 'C3psHYdZn', children: [ { - id: 'k1qTdyfzM', + id: 'iCEiAg4Wg', label: i18n.ts.forceShowAds, keywords: ['ad', 'show'], }, { - id: 'e9As4Us48', + id: 'qj9eChQ5B', label: i18n.ts.hemisphere, keywords: [], }, { - id: 'zvM13vl26', + id: 'uItIge5hw', label: i18n.ts.additionalEmojiDictionary, keywords: ['emoji', 'dictionary', 'additional', 'extra'], }, @@ -450,6 +450,13 @@ export const searchIndexes: SearchIndexItem[] = [ icon: 'ti ti-adjustments', }, { + id: 'mwkwtw83Y', + label: i18n.ts.plugins, + keywords: ['plugin'], + path: '/settings/plugin', + icon: 'ti ti-plug', + }, + { id: 'F1uK9ssiY', children: [ { @@ -626,17 +633,17 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['keep', 'original', 'raw', 'upload', i18n.ts.keepOriginalUploadingDescription], }, { - id: 'oqUiI5w0s', + id: 'D8HUTGWE1', label: i18n.ts.keepOriginalFilename, keywords: ['keep', 'original', 'filename', i18n.ts.keepOriginalFilenameDescription], }, { - id: 'Aszkikq9n', + id: '6xAvsWSZi', label: i18n.ts.alwaysMarkSensitive, keywords: ['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file'], }, { - id: 'iGlVjsfVj', + id: 'csNNPF1KX', label: i18n.ts.enableAutoSensitive, keywords: ['auto', 'nsfw', 'sensitive', 'media', 'file', i18n.ts.enableAutoSensitiveDescription], }, @@ -662,80 +669,80 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['blur'], }, { - id: 'vbZvyLDC1', + id: 'C05WQNSIJ', label: i18n.ts.useBlurEffectForModal, keywords: ['blur', 'modal'], }, { - id: '6fLNMTwNt', + id: 'snVKNr7Bw', label: i18n.ts.highlightSensitiveMedia, keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'], }, { - id: 'hhvF8Z4pF', + id: 'DsS2CwjYE', label: i18n.ts.squareAvatars, keywords: ['avatar', 'icon', 'square'], }, { - id: 'DsS2CwjYE', + id: 'xCcTDl651', label: i18n.ts.showAvatarDecorations, keywords: ['avatar', 'icon', 'decoration', 'show'], }, { - id: 'pWZ0ypy2g', + id: '3dHw723VD', label: i18n.ts.showGapBetweenNotesInTimeline, keywords: ['note', 'timeline', 'gap'], }, { - id: 'AfRMcC6IM', - label: i18n.ts.useSystemFont, - keywords: ['font', 'system', 'native'], - }, - { - id: 'jD0qbxlzN', + id: 'AWi72xbrl', label: i18n.ts.seasonalScreenEffect, keywords: ['effect', 'show'], }, { - id: 'EdYo3hOK', + id: 'Ces8FsJws', label: i18n.ts.menuStyle, keywords: ['menu', 'style', 'popup', 'drawer'], }, { - id: '9mSlX0EkD', + id: 'wDr9xSXCv', label: i18n.ts.emojiStyle, keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'], }, { - id: '44UmMwmUe', + id: 'vFB0pLzck', label: i18n.ts.fontSize, keywords: ['font', 'size'], }, { - id: 'vFB0pLzck', + id: '23BhvYXPC', + label: i18n.ts.useSystemFont, + keywords: ['font', 'system', 'native'], + }, + { + id: 'EeNLndAOa', children: [ { - id: 'pc7IpPEU4', + id: 'rAAPoaodS', label: i18n.ts.reactionsDisplaySize, keywords: ['reaction', 'size', 'scale', 'display'], }, { - id: 'siOW5aSwp', + id: 'qTLAvNWsc', label: i18n.ts.limitWidthOfReaction, keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'], }, { - id: 'dDUvhk13F', + id: '2lWgzAm13', label: i18n.ts.mediaListWithOneImageAppearance, keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'], }, { - id: 'CLxNL1Rp0', + id: 'EU7HbxOR5', label: i18n.ts.instanceTicker, keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'], }, { - id: 'dP2KWDYzD', + id: 'AEtM0FAp1', label: i18n.ts.displayOfSensitiveMedia, keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'], }, @@ -744,15 +751,15 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['note', 'display'], }, { - id: 'dVOzi22IW', + id: 'A1FMC2Zon', children: [ { - id: 'aoF4ufUwn', + id: 'CB37G5ZDo', label: i18n.ts.position, keywords: ['position'], }, { - id: 'sKK2XSS69', + id: 'gGS2i19hS', label: i18n.ts.stackAxis, keywords: ['stack', 'axis', 'direction'], }, @@ -775,32 +782,32 @@ export const searchIndexes: SearchIndexItem[] = [ keywords: ['animation', 'motion', 'reduce'], }, { - id: 'RhYwm8At', + id: 'cXr3tFdpa', label: i18n.ts.disableShowingAnimatedImages, keywords: ['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif'], }, { - id: '5mZxz2cru', + id: 'Ok1UBwtP', label: i18n.ts.enableAnimatedMfm, keywords: ['mfm', 'enable', 'show', 'animated'], }, { - id: 'bgjamYEis', + id: 'yPEpJigqY', label: i18n.ts.enableHorizontalSwipe, keywords: ['swipe', 'horizontal', 'tab'], }, { - id: 'yPEpJigqY', + id: 'h7iZtdTU3', label: i18n.ts.keepScreenOn, keywords: ['keep', 'screen', 'display', 'on'], }, { - id: 'oxwiGKMu0', + id: 'gP1BY3PDy', label: i18n.ts.useNativeUIForVideoAudioPlayer, keywords: ['native', 'system', 'video', 'audio', 'player', 'media'], }, { - id: 'n90tffyiU', + id: 'jnMK3M6rs', label: i18n.ts._contextMenu.title, keywords: ['contextmenu', 'system', 'native'], }, diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index 4d57dcd944..4f2aff9d4c 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -10,18 +10,20 @@ import { bundledThemesInfo } from 'shiki/themes'; import { bundledLanguagesInfo } from 'shiki/langs'; import lightTheme from '@@/themes/_light.json5'; import darkTheme from '@@/themes/_dark.json5'; +import defaultLightTheme from '@@/themes/l-light.json5'; +import defaultDarkTheme from '@@/themes/d-green-lime.json5'; import { unique } from './array.js'; import { deepClone } from './clone.js'; import { deepMerge } from './merge.js'; import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core'; -import { ColdDeviceStorage } from '@/store.js'; +import { prefer } from '@/preferences.js'; let _highlighter: HighlighterCore | null = null; export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>; export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>; export async function getTheme(mode: 'light' | 'dark', getName = false): Promise<ThemeRegistration | ThemeRegistrationRaw | string | null> { - const theme = deepClone(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme')); + const theme = deepClone(mode === 'light' ? prefer.s.lightTheme ?? defaultLightTheme : prefer.s.darkTheme ?? defaultDarkTheme); if (theme.base) { const base = [lightTheme, darkTheme].find(x => x.id === theme.base); @@ -77,19 +79,19 @@ async function initHighlighter() { ], }); - ColdDeviceStorage.watch('lightTheme', async () => { - const newTheme = await getTheme('light'); - if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { - highlighter.loadTheme(newTheme); - } - }); - - ColdDeviceStorage.watch('darkTheme', async () => { - const newTheme = await getTheme('dark'); - if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { - highlighter.loadTheme(newTheme); - } - }); + // TODO + //watch('lightTheme', async () => { + // const newTheme = await getTheme('light'); + // if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { + // highlighter.loadTheme(newTheme); + // } + //}); + //watch('darkTheme', async () => { + // const newTheme = await getTheme('dark'); + // if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { + // highlighter.loadTheme(newTheme); + // } + //}); _highlighter = highlighter; diff --git a/packages/frontend/src/scripts/emoji-picker.ts b/packages/frontend/src/scripts/emoji-picker.ts index e704b5fd6f..7ff5863243 100644 --- a/packages/frontend/src/scripts/emoji-picker.ts +++ b/packages/frontend/src/scripts/emoji-picker.ts @@ -6,7 +6,7 @@ import { defineAsyncComponent, ref } from 'vue'; import type { Ref } from 'vue'; import { popup } from '@/os.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; /** * 絵文字ピッカーを表示する。 @@ -25,7 +25,7 @@ class EmojiPicker { } public async init() { - const emojisRef = defaultStore.reactiveState.pinnedEmojis; + const emojisRef = store.reactiveState.pinnedEmojis; await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { src: this.src, pinnedEmojis: emojisRef, diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index c8ab9238d3..6ac0684170 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -5,12 +5,12 @@ import * as Misskey from 'misskey-js'; import { defineAsyncComponent } from 'vue'; +import type { MenuItem } from '@/types/menu.js'; import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import type { MenuItem } from '@/types/menu.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; function rename(file: Misskey.entities.DriveFile) { os.inputText({ @@ -148,7 +148,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss action: () => deleteFile(file), }); - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyFileId, diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index 84122c0e60..8ce4a81bd4 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -15,7 +15,7 @@ import { instance } from '@/instance.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { defaultStore, noteActions } from '@/store.js'; +import { store, noteActions } from '@/store.js'; import { miLocalStorage } from '@/local-storage.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import { clipsCache, favoritedChannelsCache } from '@/cache.js'; @@ -23,6 +23,7 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { isSupportShare } from '@/scripts/navigator.js'; import { getAppearNote } from '@/scripts/get-appear-note.js'; import { genEmbedCode } from '@/scripts/get-embed-code.js'; +import { prefer } from '@/preferences.js'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -507,7 +508,7 @@ export function getNoteMenu(props: { }))); } - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyNoteId, @@ -558,7 +559,7 @@ export function getRenoteMenu(props: { icon: 'ti ti-repeat', action: () => { const el = props.renoteButton.value; - if (el && defaultStore.state.animation) { + if (el && prefer.s.animation) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); @@ -596,7 +597,7 @@ export function getRenoteMenu(props: { icon: 'ti ti-repeat', action: () => { const el = props.renoteButton.value; - if (el && defaultStore.state.animation) { + if (el && prefer.s.animation) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); @@ -605,8 +606,8 @@ export function getRenoteMenu(props: { }); } - const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility; - const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly; + const configuredVisibility = prefer.s.rememberNoteVisibility ? store.state.visibility : prefer.s.defaultNoteVisibility; + const localOnly = prefer.s.rememberNoteVisibility ? store.state.localOnly : prefer.s.defaultNoteLocalOnly; let visibility = appearNote.visibility; visibility = smallerVisibility(visibility, configuredVisibility); @@ -647,7 +648,7 @@ export function getRenoteMenu(props: { text: channel.name, action: () => { const el = props.renoteButton.value; - if (el && defaultStore.state.animation) { + if (el && prefer.s.animation) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 8f7c3ba3be..6892c3a4e4 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -6,19 +6,20 @@ import { toUnicode } from 'punycode.js'; import { defineAsyncComponent, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; +import { host, url } from '@@/js/config.js'; +import type { IRouter } from '@/nirax.js'; +import type { MenuItem } from '@/types/menu.js'; import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { host, url } from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore, userActions } from '@/store.js'; +import { userActions } from '@/store.js'; import { $i, iAmModerator } from '@/account.js'; import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js'; -import type { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; import { mainRouter } from '@/router/main.js'; import { genEmbedCode } from '@/scripts/get-embed-code.js'; -import type { MenuItem } from '@/types/menu.js'; +import { prefer } from '@/preferences.js'; export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { const meId = $i ? $i.id : null; @@ -251,7 +252,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter listId: list.id, userId: user.id, }).then(() => { - list.userIds?.splice(list.userIds?.indexOf(user.id), 1); + list.userIds?.splice(list.userIds.indexOf(user.id), 1); }); } })); @@ -398,7 +399,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); } - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyUserId, diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/scripts/init-chart.ts index 41e1636aa7..037b0d9567 100644 --- a/packages/frontend/src/scripts/init-chart.ts +++ b/packages/frontend/src/scripts/init-chart.ts @@ -24,7 +24,7 @@ import { import gradient from 'chartjs-plugin-gradient'; import zoomPlugin from 'chartjs-plugin-zoom'; import { MatrixController, MatrixElement } from 'chartjs-chart-matrix'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import 'chartjs-adapter-date-fns'; export function initChart() { @@ -52,7 +52,7 @@ export function initChart() { // フォントカラー Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg'); - Chart.defaults.borderColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + Chart.defaults.borderColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; Chart.defaults.animation = false; } diff --git a/packages/frontend/src/scripts/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts deleted file mode 100644 index 37f473b6de..0000000000 --- a/packages/frontend/src/scripts/install-plugin.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { defineAsyncComponent } from 'vue'; -import { compareVersions } from 'compare-versions'; -import { v4 as uuid } from 'uuid'; -import { Interpreter, Parser, utils } from '@syuilo/aiscript'; -import type { Plugin } from '@/store.js'; -import { ColdDeviceStorage } from '@/store.js'; -import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { i18n } from '@/i18n.js'; - -export type AiScriptPluginMeta = { - name: string; - version: string; - author: string; - description?: string; - permissions?: string[]; - config?: Record<string, any>; -}; - -const parser = new Parser(); - -export function savePlugin({ id, meta, src, token }: { - id: string; - meta: AiScriptPluginMeta; - src: string; - token: string; -}) { - ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({ - ...meta, - id, - active: true, - configData: {}, - token: token, - src: src, - } as Plugin)); -} - -export function isSupportedAiScriptVersion(version: string): boolean { - try { - return (compareVersions(version, '0.12.0') >= 0); - } catch (err) { - return false; - } -} - -export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta> { - if (!code) { - throw new Error('code is required'); - } - - const lv = utils.getLangVersion(code); - if (lv == null) { - throw new Error('No language version annotation found'); - } else if (!isSupportedAiScriptVersion(lv)) { - throw new Error(`Aiscript version '${lv}' is not supported`); - } - - let ast; - try { - ast = parser.parse(code); - } catch (err) { - throw new Error('Aiscript syntax error'); - } - - const meta = Interpreter.collectMetadata(ast); - if (meta == null) { - throw new Error('Meta block not found'); - } - - const metadata = meta.get(null); - if (metadata == null) { - throw new Error('Metadata not found'); - } - - const { name, version, author, description, permissions, config } = metadata; - if (name == null || version == null || author == null) { - throw new Error('Required property not found'); - } - - return { - name, - version, - author, - description, - permissions, - config, - }; -} - -export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { - if (!code) return; - - let realMeta: AiScriptPluginMeta; - if (!meta) { - realMeta = await parsePluginMeta(code); - } else { - realMeta = meta; - } - - const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), { - title: i18n.ts.tokenRequested, - information: i18n.ts.pluginTokenRequestedDescription, - initialName: realMeta.name, - initialPermissions: realMeta.permissions, - }, { - done: async result => { - const { name, permissions } = result; - const { token } = await misskeyApi('miauth/gen-token', { - session: null, - name: name, - permission: permissions, - }); - res(token); - }, - closed: () => dispose(), - }); - }); - - savePlugin({ - id: uuid(), - meta: realMeta, - token, - src: code, - }); -} diff --git a/packages/frontend/src/scripts/install-theme.ts b/packages/frontend/src/scripts/install-theme.ts deleted file mode 100644 index cc32adcc6a..0000000000 --- a/packages/frontend/src/scripts/install-theme.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import JSON5 from 'json5'; -import { addTheme, getThemes } from '@/theme-store.js'; -import { applyTheme, validateTheme } from '@/scripts/theme.js'; -import type { Theme } from '@/scripts/theme.js'; - -export function parseThemeCode(code: string): Theme { - let theme; - - try { - theme = JSON5.parse(code); - } catch (err) { - throw new Error('Failed to parse theme json'); - } - if (!validateTheme(theme)) { - throw new Error('This theme is invaild'); - } - if (getThemes().some(t => t.id === theme.id)) { - throw new Error('This theme is already installed'); - } - - return theme; -} - -export function previewTheme(code: string): void { - const theme = parseThemeCode(code); - if (theme) applyTheme(theme, false); -} - -export async function installTheme(code: string): Promise<void> { - const theme = parseThemeCode(code); - if (!theme) return; - await addTheme(theme); -} diff --git a/packages/frontend/src/scripts/reaction-picker.ts b/packages/frontend/src/scripts/reaction-picker.ts index c142b3ed2a..81f6c02dcf 100644 --- a/packages/frontend/src/scripts/reaction-picker.ts +++ b/packages/frontend/src/scripts/reaction-picker.ts @@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js'; import { defineAsyncComponent, ref } from 'vue'; import type { Ref } from 'vue'; import { popup } from '@/os.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; class ReactionPicker { private src: Ref<HTMLElement | null> = ref(null); @@ -21,7 +21,7 @@ class ReactionPicker { } public async init() { - const reactionsRef = defaultStore.reactiveState.reactions; + const reactionsRef = store.reactiveState.reactions; await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { src: this.src, pinnedEmojis: reactionsRef, diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index c25b4d73bd..42b34f54f5 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -9,8 +9,8 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; import { uploadFile } from '@/scripts/upload.js'; +import { prefer } from '@/preferences.js'; export function chooseFileFromPc( multiple: boolean, @@ -20,8 +20,8 @@ export function chooseFileFromPc( nameConverter?: (file: File) => string | undefined; }, ): Promise<Misskey.entities.DriveFile[]> { - const uploadFolder = options?.uploadFolder ?? defaultStore.state.uploadFolder; - const keepOriginal = options?.keepOriginal ?? defaultStore.state.keepOriginalUploading; + const uploadFolder = options?.uploadFolder ?? prefer.s.uploadFolder; + const keepOriginal = options?.keepOriginal ?? prefer.s.keepOriginalUploading; const nameConverter = options?.nameConverter ?? (() => undefined); return new Promise((res, rej) => { @@ -82,7 +82,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> { misskeyApi('drive/files/upload-from-url', { url: url, - folderId: defaultStore.state.uploadFolder, + folderId: prefer.s.uploadFolder, marker, }); @@ -96,7 +96,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> { function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> { return new Promise((res, rej) => { - const keepOriginal = ref(defaultStore.state.keepOriginalUploading); + const keepOriginal = ref(prefer.s.keepOriginalUploading); os.popupMenu([label ? { text: label, diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 2008afe045..436c2b75f0 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -3,8 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { SoundStore } from '@/store.js'; -import { defaultStore } from '@/store.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { prefer } from '@/preferences.js'; +import { PREF_DEF } from '@/preferences/def.js'; let ctx: AudioContext; const cache = new Map<string, AudioBuffer>(); @@ -127,11 +128,11 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; }) * @param type スプライトの種類を指定 */ export function playMisskeySfx(operationType: OperationType) { - const sound = defaultStore.state[`sound_${operationType}`]; + const sound = prefer.s[`sound.on.${operationType}`]; playMisskeySfxFile(sound).then((succeed) => { if (!succeed && sound.type === '_driveFile_') { // ドライブファイルが存在しない場合はデフォルトのサウンドを再生する - const soundName = defaultStore.def[`sound_${operationType}`].default.type as Exclude<SoundType, '_driveFile_'>; + const soundName = PREF_DEF[`sound_${operationType}`].default.type as Exclude<SoundType, '_driveFile_'>; if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`); playMisskeySfxFileInternal({ type: soundName, @@ -166,7 +167,7 @@ async function playMisskeySfxFileInternal(soundStore: SoundStore): Promise<boole if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { return false; } - const masterVolume = defaultStore.state.sound_masterVolume; + const masterVolume = prefer.s['sound.masterVolume']; if (isMute() || masterVolume === 0 || soundStore.volume === 0) { return true; // ミュート時は成功として扱う } @@ -198,10 +199,10 @@ export function createSourceNode(buffer: AudioBuffer, opts: { pan?: number; playbackRate?: number; }): { - soundSource: AudioBufferSourceNode; - panNode: StereoPannerNode; - gainNode: GainNode; -} { + soundSource: AudioBufferSourceNode; + panNode: StereoPannerNode; + gainNode: GainNode; + } { const panNode = ctx.createStereoPanner(); panNode.pan.value = opts.pan ?? 0; @@ -242,13 +243,13 @@ export async function getSoundDuration(file: string): Promise<number> { * ミュートすべきかどうかを判断する */ export function isMute(): boolean { - if (defaultStore.state.sound_notUseSound) { + if (prefer.s['sound.notUseSound']) { // サウンドを出力しない return true; } // noinspection RedundantIfStatementJS - if (defaultStore.state.sound_useSoundOnlyWhenActive && document.visibilityState === 'hidden') { + if (prefer.s['sound.useSoundOnlyWhenActive'] && document.visibilityState === 'hidden') { // ブラウザがアクティブな時のみサウンドを出力する return true; } diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index 1a3909c132..851ba41e61 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -7,10 +7,12 @@ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; import lightTheme from '@@/themes/_light.json5'; import darkTheme from '@@/themes/_dark.json5'; +import JSON5 from 'json5'; import { deepClone } from './clone.js'; import type { BundledTheme } from 'shiki/themes'; import { globalEvents } from '@/events.js'; import { miLocalStorage } from '@/local-storage.js'; +import { addTheme, getThemes } from '@/theme-store.js'; export type Theme = { id: string; @@ -101,6 +103,7 @@ export function applyTheme(theme: Theme, persist = true) { if (persist) { miLocalStorage.setItem('theme', JSON.stringify(props)); + miLocalStorage.setItem('themeId', theme.id); miLocalStorage.setItem('colorScheme', colorScheme); } @@ -155,3 +158,32 @@ export function validateTheme(theme: Record<string, any>): boolean { if (theme.props == null || typeof theme.props !== 'object') return false; return true; } + +export function parseThemeCode(code: string): Theme { + let theme; + + try { + theme = JSON5.parse(code); + } catch (err) { + throw new Error('Failed to parse theme json'); + } + if (!validateTheme(theme)) { + throw new Error('This theme is invaild'); + } + if (getThemes().some(t => t.id === theme.id)) { + throw new Error('This theme is already installed'); + } + + return theme; +} + +export function previewTheme(code: string): void { + const theme = parseThemeCode(code); + if (theme) applyTheme(theme, false); +} + +export async function installTheme(code: string): Promise<void> { + const theme = parseThemeCode(code); + if (!theme) return; + await addTheme(theme); +} diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index 713573a377..d105a318a7 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -7,13 +7,13 @@ import { reactive, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { v4 as uuid } from 'uuid'; import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; -import { getCompressionConfig } from './upload/compress-config.js'; -import { defaultStore } from '@/store.js'; import { apiUrl } from '@@/js/config.js'; +import { getCompressionConfig } from './upload/compress-config.js'; import { $i } from '@/account.js'; import { alert } from '@/os.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; +import { prefer } from '@/preferences.js'; type Uploading = { id: string; @@ -34,7 +34,7 @@ export function uploadFile( file: File, folder?: string | Misskey.entities.DriveFolder, name?: string, - keepOriginal: boolean = defaultStore.state.keepOriginalUploading, + keepOriginal: boolean = prefer.s.keepOriginalUploading, ): Promise<Misskey.entities.DriveFile> { if ($i == null) throw new Error('Not logged in'); @@ -59,7 +59,7 @@ export function uploadFile( const ctx = reactive<Uploading>({ id, - name: defaultStore.state.keepOriginalFilename ? filename : id + extension, + name: prefer.s.keepOriginalFilename ? filename : id + extension, progressMax: undefined, progressValue: undefined, img: window.URL.createObjectURL(file), diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 128fce315f..6e4b4cd0c1 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -5,15 +5,15 @@ import { markRaw, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { hemisphere } from '@@/js/intl-const.js'; import lightTheme from '@@/themes/l-light.json5'; import darkTheme from '@@/themes/d-green-lime.json5'; -import type { SoundType } from '@/scripts/sound.js'; -import type { Ast } from '@syuilo/aiscript'; +import { hemisphere } from '@@/js/intl-const.js'; import type { DeviceKind } from '@/scripts/device-kind.js'; -import { DEFAULT_DEVICE_KIND } from '@/scripts/device-kind.js'; +import type { Plugin } from '@/plugin.js'; +import type { Column } from '@/deck.js'; import { miLocalStorage } from '@/local-storage.js'; import { Storage } from '@/pizzax.js'; +import { DEFAULT_DEVICE_KIND } from '@/scripts/device-kind.js'; interface PostFormAction { title: string, @@ -42,22 +42,6 @@ interface PageViewInterruptor { handler: (page: Misskey.entities.Page) => unknown; } -/** サウンド設定 */ -export type SoundStore = { - type: Exclude<SoundType, '_driveFile_'>; - volume: number; -} | { - type: '_driveFile_'; - - /** ドライブのファイルID */ - fileId: string; - - /** ファイルURL(こちらが優先される) */ - fileUrl: string; - - volume: number; -}; - export const postFormActions: PostFormAction[] = []; export const userActions: UserAction[] = []; export const noteActions: NoteAction[] = []; @@ -65,9 +49,10 @@ export const noteViewInterruptors: NoteViewInterruptor[] = []; export const notePostInterruptors: NotePostInterruptor[] = []; export const pageViewInterruptors: PageViewInterruptor[] = []; -// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう) -// あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない -export const defaultStore = markRaw(new Storage('base', { +/** + * 「状態」を管理するストア(not「設定」) + */ +export const store = markRaw(new Storage('base', { accountSetupWizard: { where: 'account', default: 0, @@ -85,38 +70,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: false, }, - keepCw: { - where: 'account', - default: true, - }, - collapseRenotes: { - where: 'account', - default: true, - }, - rememberNoteVisibility: { - where: 'account', - default: false, - }, - defaultNoteVisibility: { - where: 'account', - default: 'public' as (typeof Misskey.noteVisibilities)[number], - }, - defaultNoteLocalOnly: { - where: 'account', - default: false, - }, - uploadFolder: { - where: 'account', - default: null as string | null, - }, - pastedFileName: { - where: 'account', - default: 'yyyy-MM-dd HH-mm-ss [{{number}}]', - }, - keepOriginalUploading: { - where: 'account', - default: false, - }, memo: { where: 'account', default: null, @@ -137,22 +90,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: [] as string[], }, - - menu: { - where: 'deviceAccount', - default: [ - 'notifications', - 'clips', - 'drive', - 'followRequests', - '-', - 'explore', - 'announcements', - 'search', - '-', - 'ui', - ], - }, visibility: { where: 'deviceAccount', default: 'public' as (typeof Misskey.noteVisibilities)[number], @@ -165,17 +102,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, - statusbars: { - where: 'deviceAccount', - default: [] as { - name: string; - id: string; - type: string; - size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge'; - black: boolean; - props: Record<string, any>; - }[], - }, widgets: { where: 'account', default: [] as { @@ -198,14 +124,119 @@ export const defaultStore = markRaw(new Storage('base', { }, }, }, - pinnedUserLists: { + overridedDeviceKind: { + where: 'device', + default: null as DeviceKind | null, + }, + darkMode: { + where: 'device', + default: false, + }, + recentlyUsedEmojis: { + where: 'device', + default: [] as string[], + }, + recentlyUsedUsers: { + where: 'device', + default: [] as string[], + }, + menuDisplay: { + where: 'device', + default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top', + }, + postFormWithHashtags: { + where: 'device', + default: false, + }, + postFormHashtags: { + where: 'device', + default: '', + }, + additionalUnicodeEmojiIndexes: { + where: 'device', + default: {} as Record<string, Record<string, string[]>>, + }, + defaultWithReplies: { + where: 'account', + default: false, + }, + pluginTokens: { where: 'deviceAccount', - default: [] as Misskey.entities.UserList[], + default: {} as Record<string, string>, // plugin id, token + }, + 'deck.profile': { + where: 'deviceAccount', + default: 'default', + }, + 'deck.columns': { + where: 'deviceAccount', + default: [] as Column[], + }, + 'deck.layout': { + where: 'deviceAccount', + default: [] as Column['id'][][], }, - overridedDeviceKind: { + enablePreferencesAutoCloudBackup: { where: 'device', - default: null as DeviceKind | null, + default: false, + }, + showPreferencesAutoCloudBackupSuggestion: { + where: 'device', + default: true, + }, + + //#region TODO: そのうち消す (preferに移行済み) + defaultSideView: { + where: 'device', + default: false, + }, + defaultNoteVisibility: { + where: 'account', + default: 'public' as (typeof Misskey.noteVisibilities)[number], + }, + defaultNoteLocalOnly: { + where: 'account', + default: false, + }, + keepCw: { + where: 'account', + default: true, + }, + collapseRenotes: { + where: 'account', + default: true, + }, + rememberNoteVisibility: { + where: 'account', + default: false, + }, + uploadFolder: { + where: 'account', + default: null as string | null, + }, + keepOriginalUploading: { + where: 'account', + default: false, + }, + menu: { + where: 'deviceAccount', + default: [], + }, + statusbars: { + where: 'deviceAccount', + default: [] as { + name: string; + id: string; + type: string; + size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge'; + black: boolean; + props: Record<string, any>; + }[], + }, + pinnedUserLists: { + where: 'deviceAccount', + default: [] as Misskey.entities.UserList[], }, serverDisconnectedBehavior: { where: 'device', @@ -287,10 +318,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, - darkMode: { - where: 'device', - default: false, - }, instanceTicker: { where: 'device', default: 'remote' as 'none' | 'remote' | 'always', @@ -311,22 +338,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'auto' as 'auto' | 'popup' | 'drawer', }, - recentlyUsedEmojis: { - where: 'device', - default: [] as string[], - }, - recentlyUsedUsers: { - where: 'device', - default: [] as string[], - }, - defaultSideView: { - where: 'device', - default: false, - }, - menuDisplay: { - where: 'device', - default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top', - }, reportError: { where: 'device', default: false, @@ -339,18 +350,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: true, }, - postFormWithHashtags: { - where: 'device', - default: false, - }, - postFormHashtags: { - where: 'device', - default: '', - }, - themeInitial: { - where: 'device', - default: true, - }, numberOfPageCache: { where: 'device', default: 3, @@ -399,18 +398,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: true, }, - additionalUnicodeEmojiIndexes: { - where: 'device', - default: {} as Record<string, Record<string, string[]>>, - }, keepScreenOn: { where: 'device', default: false, }, - defaultWithReplies: { - where: 'account', - default: false, - }, disableStreamingTimeline: { where: 'device', default: false, @@ -432,17 +423,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, - dropAndFusion: { - where: 'device', - default: { - bgmVolume: 0.25, - sfxVolume: 1, - }, - }, - hemisphere: { - where: 'device', - default: hemisphere as 'N' | 'S', - }, enableHorizontalSwipe: { where: 'device', default: true, @@ -479,7 +459,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, - + hemisphere: { + where: 'device', + default: hemisphere as 'N' | 'S', + }, sound_masterVolume: { where: 'device', default: 0.3, @@ -494,56 +477,49 @@ export const defaultStore = markRaw(new Storage('base', { }, sound_note: { where: 'device', - default: { type: 'syuilo/n-aec', volume: 1 } as SoundStore, + default: { type: 'syuilo/n-aec', volume: 1 }, }, sound_noteMy: { where: 'device', - default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore, + default: { type: 'syuilo/n-cea-4va', volume: 1 }, }, sound_notification: { where: 'device', - default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore, + default: { type: 'syuilo/n-ea', volume: 1 }, }, sound_reaction: { where: 'device', - default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore, + default: { type: 'syuilo/bubble2', volume: 1 }, }, + dropAndFusion: { + where: 'device', + default: { + bgmVolume: 0.25, + sfxVolume: 1, + }, + }, + //#endregion })); // TODO: 他のタブと永続化されたstateを同期 const PREFIX = 'miux:' as const; -export type Plugin = { - id: string; - name: string; - active: boolean; - config?: Record<string, { default: any }>; - configData: Record<string, any>; - token: string; - src: string | null; - version: string; - ast: Ast.Node[]; - author?: string; - description?: string; - permissions?: string[]; -}; - interface Watcher { key: string; callback: (value: unknown) => void; } +// TODO: 消す(preferに移行済みのため) /** * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ) */ - export class ColdDeviceStorage { public static default = { - lightTheme, - darkTheme, - syncDeviceDarkMode: true, - plugins: [] as Plugin[], + lightTheme, // TODO: 消す(preferに移行済みのため) + darkTheme, // TODO: 消す(preferに移行済みのため) + syncDeviceDarkMode: true, // TODO: 消す(preferに移行済みのため) + plugins: [] as Plugin[], // TODO: 消す(preferに移行済みのため) }; public static watchers: Watcher[] = []; diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts index fb010ae426..09c665a2ab 100644 --- a/packages/frontend/src/theme-store.ts +++ b/packages/frontend/src/theme-store.ts @@ -3,29 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { getBuiltinThemes } from '@/scripts/theme.js'; import type { Theme } from '@/scripts/theme.js'; -import { miLocalStorage } from '@/local-storage.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { getBuiltinThemes } from '@/scripts/theme.js'; import { $i } from '@/account.js'; - -const lsCacheKey = $i ? `themes:${$i.id}` as const : null; +import { prefer } from '@/preferences.js'; export function getThemes(): Theme[] { if ($i == null) return []; - return JSON.parse(miLocalStorage.getItem(lsCacheKey!) ?? '[]'); -} - -export async function fetchThemes(): Promise<void> { - if ($i == null) return; - - try { - const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }); - miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes)); - } catch (err) { - if (err.code === 'NO_SUCH_KEY') return; - throw err; - } + return prefer.s.themes; } export async function addTheme(theme: Theme): Promise<void> { @@ -34,15 +19,15 @@ export async function addTheme(theme: Theme): Promise<void> { if (builtinThemes.some(t => t.id === theme.id)) { throw new Error('builtin theme'); } - await fetchThemes(); - const themes = getThemes().concat(theme); - await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); - miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes)); + const themes = getThemes(); + if (themes.some(t => t.id === theme.id)) { + throw new Error('already exists'); + } + prefer.set('themes', [...themes, theme]); } export async function removeTheme(theme: Theme): Promise<void> { if ($i == null) return; const themes = getThemes().filter(t => t.id !== theme.id); - await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); - miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes)); + prefer.set('themes', themes); } diff --git a/packages/frontend/src/ui/_common_/PreferenceRestore.vue b/packages/frontend/src/ui/_common_/PreferenceRestore.vue new file mode 100644 index 0000000000..0412733350 --- /dev/null +++ b/packages/frontend/src/ui/_common_/PreferenceRestore.vue @@ -0,0 +1,73 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root"> + <span :class="$style.icon"> + <i class="ti ti-info-circle"></i> + </span> + <span :class="$style.title">{{ i18n.ts._preferencesBackup.backupFound }}</span> + <span :class="$style.body"><button class="_textButton" @click="restore">{{ i18n.ts.restore }}</button> | <button class="_textButton" @click="skip">{{ i18n.ts.skip }}</button></span> +</div> +</template> + +<script lang="ts" setup> +import { $i } from '@/account.js'; +import { i18n } from '@/i18n.js'; +import { hideRestoreBackupSuggestion, restoreFromCloudBackup } from '@/preferences/utility.js'; + +function restore() { + restoreFromCloudBackup(); +} + +function skip() { + hideRestoreBackupSuggestion(); +} +</script> + +<style lang="scss" module> +.root { + --height: 24px; + font-size: 0.85em; + display: flex; + vertical-align: bottom; + width: 100%; + line-height: var(--height); + height: var(--height); + overflow: clip; + contain: strict; + background: var(--MI_THEME-panel); + + @container (max-width: 1000px) { + display: block; + text-align: center; + + > .body { + display: none; + } + } +} + +.icon { + margin-left: 10px; +} + +.title { + padding: 0 10px; + font-weight: bold; + + &:empty { + display: none; + } +} + +.body { + min-width: 0; + flex: 1; + overflow: clip; + white-space: nowrap; + text-overflow: ellipsis; +} +</style> diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index d145b9b6c6..51645f9676 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -17,18 +17,18 @@ SPDX-License-Identifier: AGPL-3.0-only <TransitionGroup tag="div" :class="[$style.notifications, { - [$style.notificationsPosition_leftTop]: defaultStore.state.notificationPosition === 'leftTop', - [$style.notificationsPosition_leftBottom]: defaultStore.state.notificationPosition === 'leftBottom', - [$style.notificationsPosition_rightTop]: defaultStore.state.notificationPosition === 'rightTop', - [$style.notificationsPosition_rightBottom]: defaultStore.state.notificationPosition === 'rightBottom', - [$style.notificationsStackAxis_vertical]: defaultStore.state.notificationStackAxis === 'vertical', - [$style.notificationsStackAxis_horizontal]: defaultStore.state.notificationStackAxis === 'horizontal', + [$style.notificationsPosition_leftTop]: prefer.s.notificationPosition === 'leftTop', + [$style.notificationsPosition_leftBottom]: prefer.s.notificationPosition === 'leftBottom', + [$style.notificationsPosition_rightTop]: prefer.s.notificationPosition === 'rightTop', + [$style.notificationsPosition_rightBottom]: prefer.s.notificationPosition === 'rightBottom', + [$style.notificationsStackAxis_vertical]: prefer.s.notificationStackAxis === 'vertical', + [$style.notificationsStackAxis_horizontal]: prefer.s.notificationStackAxis === 'horizontal', }]" - :moveClass="defaultStore.state.animation ? $style.transition_notification_move : ''" - :enterActiveClass="defaultStore.state.animation ? $style.transition_notification_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_notification_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_notification_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_notification_leaveTo : ''" + :moveClass="prefer.s.animation ? $style.transition_notification_move : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_notification_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_notification_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_notification_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_notification_leaveTo : ''" > <div v-for="notification in notifications" :key="notification.id" :class="$style.notification"> <XNotification :notification="notification"/> @@ -56,7 +56,7 @@ import * as sound from '@/scripts/sound.js'; import { $i } from '@/account.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { globalEvents } from '@/events.js'; const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue')); diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index 44253e93bd..698e9d8d47 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -54,11 +54,11 @@ import { openInstanceMenu } from './common.js'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -const menu = toRef(defaultStore.state, 'menu'); +const menu = toRef(prefer.s, 'menu'); const otherMenuItemIndicated = computed(() => { for (const def in navbarItemDef) { if (menu.value.includes(def)) continue; diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index fec8666dc1..1fb99f9f22 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -94,20 +94,21 @@ import { openInstanceMenu } from './common.js'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; import { useRouter } from '@/router/supplier.js'; +import { prefer } from '@/preferences.js'; const router = useRouter(); const forceIconOnly = ref(window.innerWidth <= 1279); const iconOnly = computed(() => { - return forceIconOnly.value || (defaultStore.reactiveState.menuDisplay.value === 'sideIcon'); + return forceIconOnly.value || (store.reactiveState.menuDisplay.value === 'sideIcon'); }); -const menu = computed(() => defaultStore.state.menu); +const menu = computed(() => prefer.s.menu); const otherMenuItemIndicated = computed(() => { for (const def in navbarItemDef) { if (menu.value.includes(def)) continue; @@ -122,12 +123,12 @@ function calcViewState() { window.addEventListener('resize', calcViewState); -watch(defaultStore.reactiveState.menuDisplay, () => { +watch(store.reactiveState.menuDisplay, () => { calcViewState(); }); function toggleIconOnly() { - defaultStore.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon'); + store.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon'); } function openAccountMenu(ev: MouseEvent) { diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue index ed881bef22..a8d87599e6 100644 --- a/packages/frontend/src/ui/_common_/statusbars.vue +++ b/packages/frontend/src/ui/_common_/statusbars.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root"> <div - v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" :class="[$style.item, { [$style.black]: x.black, + v-for="x in prefer.r.statusbars.value" :key="x.id" :class="[$style.item, { [$style.black]: x.black, [$style.verySmall]: x.size === 'verySmall', [$style.small]: x.size === 'small', [$style.large]: x.size === 'large', @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent } from 'vue'; import { instance } from '@/instance.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const XRss = defineAsyncComponent(() => import('./statusbar-rss.vue')); const XFederation = defineAsyncComponent(() => import('./statusbar-federation.vue')); const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')); diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue index cc62a28b14..1eb809d198 100644 --- a/packages/frontend/src/ui/_common_/stream-indicator.vue +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div v-if="hasDisconnected && defaultStore.state.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected"> +<div v-if="hasDisconnected && prefer.s.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected"> <div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.disconnectedFromServer }}</div> <div :class="$style.command" class="_buttons"> <MkButton small primary @click="reload">{{ i18n.ts.reload }}</MkButton> @@ -19,7 +19,7 @@ import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const zIndex = os.claimZIndex('high'); diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue index f4633314ae..39b40754ff 100644 --- a/packages/frontend/src/ui/classic.header.vue +++ b/packages/frontend/src/ui/classic.header.vue @@ -53,15 +53,16 @@ import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; import { openAccountMenu as openAccountMenu_, $i } from '@/account.js'; import MkButton from '@/components/MkButton.vue'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; const WINDOW_THRESHOLD = 1400; const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD); -const menu = ref(defaultStore.state.menu); -// const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); +const menu = ref(prefer.s.menu); +// const menuDisplay = computed(store.makeGetterSetter('menuDisplay')); const otherNavItemIndicated = computed<boolean>(() => { for (const def in navbarItemDef) { if (menu.value.includes(def)) continue; diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue index 5acef0bef8..c11771f028 100644 --- a/packages/frontend/src/ui/classic.sidebar.vue +++ b/packages/frontend/src/ui/classic.sidebar.vue @@ -59,14 +59,15 @@ import MkButton from '@/components/MkButton.vue'; // import { StickySidebar } from '@/scripts/sticky-sidebar.js'; // import { mainRouter } from '@/router.js'; //import MisskeyLogo from '@assets/client/misskey.svg'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; const WINDOW_THRESHOLD = 1400; -const menu = ref(defaultStore.state.menu); -const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); +const menu = ref(prefer.s.menu); +const menuDisplay = computed(store.makeGetterSetter('menuDisplay')); const otherNavItemIndicated = computed<boolean>(() => { for (const def in navbarItemDef) { if (menu.value.includes(def)) continue; @@ -99,7 +100,7 @@ function openAccountMenu(ev: MouseEvent) { }, ev); } -watch(defaultStore.reactiveState.menuDisplay, () => { +watch(store.reactiveState.menuDisplay, () => { calcViewState(); }); diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index da5059bb59..63c60a3d6f 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <Transition :name="defaultStore.state.animation ? 'tray-back' : ''"> + <Transition :name="prefer.s.animation ? 'tray-back' : ''"> <div v-if="widgetsShowing" class="tray-back _modalBg" @@ -35,11 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only ></div> </Transition> - <Transition :name="defaultStore.state.animation ? 'tray' : ''"> + <Transition :name="prefer.s.animation ? 'tray' : ''"> <XWidgets v-if="widgetsShowing" class="tray"/> </Transition> - <iframe v-if="defaultStore.state.aiChanMode" ref="live2d" class="ivnzpscs" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe> + <iframe v-if="prefer.s.aiChanMode" ref="live2d" class="ivnzpscs" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe> <XCommon/> </div> @@ -47,18 +47,19 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, onMounted, provide, ref, computed, shallowRef } from 'vue'; +import { instanceName } from '@@/js/config.js'; +import { isLink } from '@@/js/is-link.js'; import XSidebar from './classic.sidebar.vue'; import XCommon from './_common_/common.vue'; -import { instanceName } from '@@/js/config.js'; +import type { PageMetadata } from '@/scripts/page-metadata.js'; import { StickySidebar } from '@/scripts/sticky-sidebar.js'; import * as os from '@/os.js'; import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; -import type { PageMetadata } from '@/scripts/page-metadata.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; import { mainRouter } from '@/router/main.js'; -import { isLink } from '@@/js/is-link.js'; +import { prefer } from '@/preferences.js'; const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue')); const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); @@ -74,7 +75,7 @@ const widgetsShowing = ref(false); const fullView = ref(false); const globalHeaderHeight = ref(0); const wallpaper = miLocalStorage.getItem('wallpaper') != null; -const showMenuOnTop = computed(() => defaultStore.state.menuDisplay === 'top'); +const showMenuOnTop = computed(() => store.state.menuDisplay === 'top'); const live2d = shallowRef<HTMLIFrameElement>(); const widgetsLeft = ref<HTMLElement>(); const widgetsRight = ref<HTMLElement>(); @@ -96,7 +97,7 @@ provide('shouldHeaderThin', showMenuOnTop.value); provide('forceSpacerMin', true); function attachSticky(el: HTMLElement) { - const sticky = new StickySidebar(el, 0, defaultStore.state.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す + const sticky = new StickySidebar(el, 0, store.state.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す window.addEventListener('scroll', () => { sticky.calc(window.scrollY); }, { passive: true }); @@ -142,9 +143,9 @@ if (window.innerWidth < 1024) { document.documentElement.style.overflowY = 'scroll'; -defaultStore.loaded.then(() => { - if (defaultStore.state.widgets.length === 0) { - defaultStore.set('widgets', [{ +store.loaded.then(() => { + if (store.state.widgets.length === 0) { + store.set('widgets', [{ name: 'calendar', id: 'a', place: null, data: {}, }, { @@ -162,7 +163,7 @@ onMounted(() => { isDesktop.value = (window.innerWidth >= DESKTOP_THRESHOLD); }, { passive: true }); - if (defaultStore.state.aiChanMode) { + if (prefer.s.aiChanMode) { const iframeRect = live2d.value.getBoundingClientRect(); window.addEventListener('mousemove', ev => { live2d.value.contentWindow.postMessage({ diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 8e99c457ad..2107f6f53a 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.main"> <XAnnouncements v-if="$i"/> <XStatusBars/> - <div ref="columnsEl" :class="[$style.sections, { [$style.center]: deckStore.reactiveState.columnAlign.value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel"> + <div ref="columnsEl" :class="[$style.sections, { [$style.center]: prefer.r['deck.columnAlign'].value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel"> <!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> <section v-for="ids in layout" @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.sideMenu"> <div :class="$style.sideMenuTop"> - <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button> + <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${store.state['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button> <button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button> </div> <div :class="$style.sideMenuMiddle"> @@ -62,10 +62,10 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveTo : ''" > <div v-if="drawerMenuShowing" @@ -77,10 +77,10 @@ SPDX-License-Identifier: AGPL-3.0-only </Transition> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_menuDrawer_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawer_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_menuDrawer_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''" > <div v-if="drawerMenuShowing" :class="$style.menu"> <XDrawerMenu/> @@ -95,8 +95,6 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue'; import { v4 as uuid } from 'uuid'; import XCommon from './_common_/common.vue'; -import { deckStore, columnTypes, addColumn as addColumnToStore, forceSaveDeck, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js'; -import type { ColumnType } from './deck/deck-store.js'; import type { MenuItem } from '@/types/menu.js'; import XSidebar from '@/ui/_common_/navbar.vue'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; @@ -107,7 +105,7 @@ import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { deviceKind } from '@/scripts/device-kind.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import XMainColumn from '@/ui/deck/main-column.vue'; import XTlColumn from '@/ui/deck/tl-column.vue'; import XAntennaColumn from '@/ui/deck/antenna-column.vue'; @@ -119,6 +117,8 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue'; import XDirectColumn from '@/ui/deck/direct-column.vue'; import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; import { mainRouter } from '@/router/main.js'; +import { store } from '@/store.js'; +import { columnTypes, forceSaveDeck, getProfiles, loadDeck, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); @@ -137,8 +137,8 @@ const columnComponents = { mainRouter.navHook = (path, flag): boolean => { if (flag === 'forcePage') return false; - const noMainColumn = !deckStore.state.columns.some(x => x.type === 'main'); - if (deckStore.state.navWindow || noMainColumn) { + const noMainColumn = !store.state['deck.columns'].some(x => x.type === 'main'); + if (prefer.s['deck.navWindow'] || noMainColumn) { os.pageWindow(path); return true; } @@ -160,8 +160,8 @@ watch(route, () => { }); */ -const columns = deckStore.reactiveState.columns; -const layout = deckStore.reactiveState.layout; +const columns = store.reactiveState['deck.columns']; +const layout = store.reactiveState['deck.layout']; const menuIndicated = computed(() => { if ($i == null) return false; for (const def in navbarItemDef) { @@ -214,15 +214,15 @@ loadDeck(); function changeProfile(ev: MouseEvent) { let items: MenuItem[] = [{ - text: deckStore.state.profile, + text: store.state['deck.profile'], active: true, action: () => {}, }]; getProfiles().then(profiles => { - items.push(...(profiles.filter(k => k !== deckStore.state.profile).map(k => ({ + items.push(...(profiles.filter(k => k !== store.state['deck.profile']).map(k => ({ text: k, action: () => { - deckStore.set('profile', k); + store.set('deck.profile', k); unisonReload(); }, }))), { type: 'divider' as const }, { @@ -237,7 +237,7 @@ function changeProfile(ev: MouseEvent) { if (canceled || name == null) return; os.promiseDialog((async () => { - await deckStore.set('profile', name); + await store.set('deck.profile', name); await forceSaveDeck(); })(), () => { unisonReload(); @@ -252,19 +252,19 @@ function changeProfile(ev: MouseEvent) { async function deleteProfile() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.tsx.deleteAreYouSure({ x: deckStore.state.profile }), + text: i18n.tsx.deleteAreYouSure({ x: store.state['deck.profile'] }), }); if (canceled) return; os.promiseDialog((async () => { - if (deckStore.state.profile === 'default') { - await deckStore.set('columns', []); - await deckStore.set('layout', []); + if (store.state['deck.profile'] === 'default') { + await store.set('deck.columns', []); + await store.set('deck.layout', []); await forceSaveDeck(); } else { - await deleteProfile_(deckStore.state.profile); + await deleteProfile_(store.state['deck.profile']); } - await deckStore.set('profile', 'default'); + await store.set('deck.profile', 'default'); })(), () => { unisonReload(); }); diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index b79cd8408b..5f0c607edb 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -15,17 +15,17 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref, shallowRef, watch, defineAsyncComponent } from 'vue'; -import type { entities as MisskeyEntities } from 'misskey-js'; import XColumn from './column.vue'; -import { updateColumn } from './deck-store.js'; -import type { Column } from './deck-store.js'; +import type { entities as MisskeyEntities } from 'misskey-js'; +import type { Column } from '@/deck.js'; +import type { MenuItem } from '@/types/menu.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { updateColumn } from '@/deck.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.js'; import { antennasCache } from '@/cache.js'; -import type { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index 9e07c06639..f002f655f7 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -22,16 +22,16 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, ref, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import XColumn from './column.vue'; -import { updateColumn } from './deck-store.js'; -import type { Column } from './deck-store.js'; +import type { Column } from '@/deck.js'; +import type { MenuItem } from '@/types/menu.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { updateColumn } from '@/deck.js'; import MkTimeline from '@/components/MkTimeline.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { favoritedChannelsCache } from '@/cache.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.js'; -import type { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index f23e33c748..fc208197a0 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -43,11 +43,11 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onBeforeUnmount, onMounted, provide, watch, shallowRef, ref, computed } from 'vue'; -import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from './deck-store.js'; +import type { Column } from '@/deck.js'; +import type { MenuItem } from '@/types/menu.js'; +import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from '@/deck.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import type { Column } from './deck-store.js'; -import type { MenuItem } from '@/types/menu.js'; provide('shouldHeaderThin', true); provide('shouldOmitHeaderTitle', true); diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 9055ea6d43..bdca513a7a 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -3,57 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { throttle } from 'throttle-debounce'; import { markRaw } from 'vue'; -import { notificationTypes } from 'misskey-js'; -import type { BasicTimelineType } from '@/timelines.js'; +import type { Column } from '@/deck.js'; import { Storage } from '@/pizzax.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { deepClone } from '@/scripts/clone.js'; -import type { SoundStore } 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; -}; +// TODO: 消す(移行済みのため) export const deckStore = markRaw(new Storage('deck', { profile: { where: 'deviceAccount', @@ -67,272 +21,4 @@ export const deckStore = markRaw(new Storage('deck', { where: 'deviceAccount', default: [] as Column['id'][][], }, - columnAlign: { - where: 'deviceAccount', - default: 'left' as 'left' | 'right' | 'center', - }, - alwaysShowMainColumn: { - where: 'deviceAccount', - default: true, - }, - navWindow: { - where: 'deviceAccount', - default: true, - }, - useSimpleUiForNonRootPages: { - where: 'deviceAccount', - default: true, - }, })); - -export const loadDeck = async () => { - let deck; - - try { - deck = await misskeyApi('i/registry/get', { - scope: ['client', 'deck', 'profiles'], - key: deckStore.state.profile, - }); - } catch (err) { - if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') { - // 後方互換性のため - if (deckStore.state.profile === 'default') { - saveDeck(); - return; - } - - deckStore.set('columns', []); - deckStore.set('layout', []); - return; - } - throw err; - } - - deckStore.set('columns', deck.columns); - deckStore.set('layout', deck.layout); -}; - -export async function forceSaveDeck() { - await misskeyApi('i/registry/set', { - scope: ['client', 'deck', 'profiles'], - key: deckStore.state.profile, - value: { - columns: deckStore.reactiveState.columns.value, - layout: deckStore.reactiveState.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; - deckStore.push('columns', column); - deckStore.push('layout', [column.id]); - saveDeck(); -} - -export function removeColumn(id: Column['id']) { - deckStore.set('columns', deckStore.state.columns.filter(c => c.id !== id)); - deckStore.set('layout', deckStore.state.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 = deckStore.state.layout.findIndex(ids => ids.indexOf(a) !== -1); - const aY = deckStore.state.layout[aX].findIndex(id => id === a); - const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1); - const bY = deckStore.state.layout[bX].findIndex(id => id === b); - const layout = deepClone(deckStore.state.layout); - layout[aX][aY] = b; - layout[bX][bY] = a; - deckStore.set('layout', layout); - saveDeck(); -} - -export function swapLeftColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - deckStore.state.layout.some((ids, i) => { - if (ids.includes(id)) { - const left = deckStore.state.layout[i - 1]; - if (left) { - layout[i - 1] = deckStore.state.layout[i]; - layout[i] = left; - deckStore.set('layout', layout); - } - return true; - } - return false; - }); - saveDeck(); -} - -export function swapRightColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - deckStore.state.layout.some((ids, i) => { - if (ids.includes(id)) { - const right = deckStore.state.layout[i + 1]; - if (right) { - layout[i + 1] = deckStore.state.layout[i]; - layout[i] = right; - deckStore.set('layout', layout); - } - return true; - } - return false; - }); - saveDeck(); -} - -export function swapUpColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id)); - const ids = deepClone(deckStore.state.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; - deckStore.set('layout', layout); - } - return true; - } - return false; - }); - saveDeck(); -} - -export function swapDownColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id)); - const ids = deepClone(deckStore.state.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; - deckStore.set('layout', layout); - } - return true; - } - return false; - }); - saveDeck(); -} - -export function stackLeftColumn(id: Column['id']) { - let layout = deepClone(deckStore.state.layout); - const i = deckStore.state.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); - deckStore.set('layout', layout); - saveDeck(); -} - -export function popRightColumn(id: Column['id']) { - let layout = deepClone(deckStore.state.layout); - const i = deckStore.state.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); - deckStore.set('layout', layout); - - const columns = deepClone(deckStore.state.columns); - for (const column of columns) { - if (affected.includes(column.id)) { - column.active = true; - } - } - deckStore.set('columns', columns); - - saveDeck(); -} - -export function addColumnWidget(id: Column['id'], widget: ColumnWidget) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; - if (column.widgets == null) column.widgets = []; - column.widgets.unshift(widget); - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.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; - deckStore.set('columns', columns); - saveDeck(); -} - -export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; - column.widgets = widgets; - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.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; - deckStore.set('columns', columns); - saveDeck(); -} - -export function updateColumn(id: Column['id'], column: Partial<Column>) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const currentColumn = deepClone(deckStore.state.columns[columnIndex]); - if (currentColumn == null) return; - for (const [k, v] of Object.entries(column)) { - currentColumn[k] = v; - } - columns[columnIndex] = currentColumn; - deckStore.set('columns', columns); - saveDeck(); -} diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue index 2cecd6c669..772188d773 100644 --- a/packages/frontend/src/ui/deck/direct-column.vue +++ b/packages/frontend/src/ui/deck/direct-column.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import XColumn from './column.vue'; -import type { Column } from './deck-store.js'; +import type { Column } from '@/deck.js'; import MkNotes from '@/components/MkNotes.vue'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index 83961d02bc..f627ab5262 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -15,16 +15,16 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { watch, shallowRef, ref, onMounted } from 'vue'; -import type { entities as MisskeyEntities } from 'misskey-js'; import XColumn from './column.vue'; -import { updateColumn } from './deck-store.js'; -import type { Column } from './deck-store.js'; +import type { entities as MisskeyEntities } from 'misskey-js'; +import type { Column } from '@/deck.js'; +import type { MenuItem } from '@/types/menu.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { updateColumn } from '@/deck.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.js'; -import type { SoundStore } from '@/store.js'; import { userListsCache } from '@/cache.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index 45c39a5cad..906e94f0b3 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn v-if="deckStore.state.alwaysShowMainColumn || mainRouter.currentRoute.value.name !== 'index'" :column="column" :isStacked="isStacked"> +<XColumn v-if="prefer.s['deck.alwaysShowMainColumn'] || mainRouter.currentRoute.value.name !== 'index'" :column="column" :isStacked="isStacked"> <template #header> <template v-if="pageMetadata"> <i :class="pageMetadata.icon"></i> @@ -20,17 +20,17 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { provide, shallowRef, ref } from 'vue'; +import { getScrollContainer } from '@@/js/scroll.js'; +import { isLink } from '@@/js/is-link.js'; import XColumn from './column.vue'; -import { deckStore } from '@/ui/deck/deck-store.js'; -import type { Column } from '@/ui/deck/deck-store.js'; +import type { Column } from '@/deck.js'; +import type { PageMetadata } from '@/scripts/page-metadata.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; -import type { PageMetadata } from '@/scripts/page-metadata.js'; import { useScrollPositionManager } from '@/nirax.js'; -import { getScrollContainer } from '@@/js/scroll.js'; -import { isLink } from '@@/js/is-link.js'; import { mainRouter } from '@/router/main.js'; +import { prefer } from '@/preferences.js'; defineProps<{ column: Column; diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue index 233fba554b..ffd0307940 100644 --- a/packages/frontend/src/ui/deck/mentions-column.vue +++ b/packages/frontend/src/ui/deck/mentions-column.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import XColumn from './column.vue'; -import type { Column } from './deck-store.js'; +import type { Column } from '@/deck.js'; import MkNotes from '@/components/MkNotes.vue'; import { i18n } from '../../i18n.js'; diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue index c0303e86dc..0a2c0e9952 100644 --- a/packages/frontend/src/ui/deck/notifications-column.vue +++ b/packages/frontend/src/ui/deck/notifications-column.vue @@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, shallowRef } from 'vue'; import XColumn from './column.vue'; -import { updateColumn } from './deck-store.js'; -import type { Column } from './deck-store.js'; +import { updateColumn } from '@/deck.js'; +import type { Column } from '@/deck.js'; import XNotifications from '@/components/MkNotifications.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index 5b1420570d..4d1be08706 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -16,14 +16,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, onMounted, ref, shallowRef, watch } from 'vue'; import XColumn from './column.vue'; -import { updateColumn } from './deck-store.js'; -import type { Column } from './deck-store.js'; +import type { Column } from '@/deck.js'; +import type { MenuItem } from '@/types/menu.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { updateColumn } from '@/deck.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.js'; -import type { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index b9b3746abf..96da944398 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -34,15 +34,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, watch, ref, shallowRef, computed } from 'vue'; import XColumn from './column.vue'; -import { removeColumn, updateColumn } from './deck-store.js'; -import type { Column } from './deck-store.js'; +import type { Column } from '@/deck.js'; import type { MenuItem } from '@/types/menu.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { removeColumn, updateColumn } from '@/deck.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { hasWithReplies, isAvailableBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; -import { instance } from '@/instance.js'; -import type { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/ui/deck/tl-note-notification.ts b/packages/frontend/src/ui/deck/tl-note-notification.ts index 03d4b3a580..277ebb0511 100644 --- a/packages/frontend/src/ui/deck/tl-note-notification.ts +++ b/packages/frontend/src/ui/deck/tl-note-notification.ts @@ -5,8 +5,8 @@ import * as Misskey from 'misskey-js'; import type { Ref } from 'vue'; -import type { SoundStore } from '@/store.js'; import type { SoundType } from '@/scripts/sound.js'; +import type { SoundStore } from '@/preferences/def.js'; import { getSoundDuration, playMisskeySfxFile, soundsTypes } from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue index 20284d8c9f..4e84ef0ba0 100644 --- a/packages/frontend/src/ui/deck/widgets-column.vue +++ b/packages/frontend/src/ui/deck/widgets-column.vue @@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import XColumn from './column.vue'; -import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store.js'; -import type { Column } from './deck-store.js'; +import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from '@/deck.js'; +import type { Column } from '@/deck.js'; import XWidgets from '@/components/MkWidgets.vue'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 25f47a2d55..bddb62dc60 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;" @contextmenu.stop="onContextmenu"> <template #header> <div> + <XPreferenceRestore v-if="shouldSuggestRestoreBackup"/> <XAnnouncements v-if="$i"/> <XStatusBars :class="$style.statusbars"/> </div> @@ -38,10 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveTo : ''" > <div v-if="drawerMenuShowing" @@ -53,10 +54,10 @@ SPDX-License-Identifier: AGPL-3.0-only </Transition> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_menuDrawer_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawer_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_menuDrawer_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''" > <div v-if="drawerMenuShowing" :class="$style.menuDrawer"> <XDrawerMenu/> @@ -64,10 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only </Transition> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''" > <div v-if="widgetsShowing" @@ -79,10 +80,10 @@ SPDX-License-Identifier: AGPL-3.0-only </Transition> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveTo : ''" > <div v-if="widgetsShowing" :class="$style.widgetsDrawer"> <button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button> @@ -105,7 +106,7 @@ import type MkStickyContainer from '@/components/global/MkStickyContainer.vue'; import type { PageMetadata } from '@/scripts/page-metadata.js'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import * as os from '@/os.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { navbarItemDef } from '@/navbar.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; @@ -114,11 +115,14 @@ import { deviceKind } from '@/scripts/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; import { useScrollPositionManager } from '@/nirax.js'; import { mainRouter } from '@/router/main.js'; +import { prefer } from '@/preferences.js'; +import { shouldSuggestRestoreBackup } from '@/preferences/utility.js'; const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue')); const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); +const XPreferenceRestore = defineAsyncComponent(() => import('@/ui/_common_/PreferenceRestore.vue')); const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index'); @@ -174,9 +178,9 @@ if (window.innerWidth > 1024) { } } -defaultStore.loaded.then(() => { - if (defaultStore.state.widgets.length === 0) { - defaultStore.set('widgets', [{ +store.loaded.then(() => { + if (store.state.widgets.length === 0) { + store.set('widgets', [{ name: 'calendar', id: 'a', place: 'right', data: {}, }, { diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue index fc0a4475d2..42b870cbb5 100644 --- a/packages/frontend/src/ui/universal.widgets.vue +++ b/packages/frontend/src/ui/universal.widgets.vue @@ -19,7 +19,7 @@ const editMode = ref(false); <script lang="ts" setup> import XWidgets from '@/components/MkWidgets.vue'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; const props = withDefaults(defineProps<{ // null = 全てのウィジェットを表示 @@ -31,24 +31,24 @@ const props = withDefaults(defineProps<{ }); const widgets = computed(() => { - if (props.place === null) return defaultStore.reactiveState.widgets.value; - if (props.place === 'left') return defaultStore.reactiveState.widgets.value.filter(w => w.place === 'left'); - return defaultStore.reactiveState.widgets.value.filter(w => w.place !== 'left'); + if (props.place === null) return store.reactiveState.widgets.value; + if (props.place === 'left') return store.reactiveState.widgets.value.filter(w => w.place === 'left'); + return store.reactiveState.widgets.value.filter(w => w.place !== 'left'); }); function addWidget(widget) { - defaultStore.set('widgets', [{ + store.set('widgets', [{ ...widget, place: props.place, - }, ...defaultStore.state.widgets]); + }, ...store.state.widgets]); } function removeWidget(widget) { - defaultStore.set('widgets', defaultStore.state.widgets.filter(w => w.id !== widget.id)); + store.set('widgets', store.state.widgets.filter(w => w.id !== widget.id)); } function updateWidget({ id, data }) { - defaultStore.set('widgets', defaultStore.state.widgets.map(w => w.id === id ? { + store.set('widgets', store.state.widgets.map(w => w.id === id ? { ...w, data, place: props.place, @@ -57,18 +57,18 @@ function updateWidget({ id, data }) { function updateWidgets(thisWidgets) { if (props.place === null) { - defaultStore.set('widgets', thisWidgets); + store.set('widgets', thisWidgets); return; } if (props.place === 'left') { - defaultStore.set('widgets', [ + store.set('widgets', [ ...thisWidgets.map(w => ({ ...w, place: 'left' })), - ...defaultStore.state.widgets.filter(w => w.place !== 'left' && !thisWidgets.some(t => w.id === t.id)), + ...store.state.widgets.filter(w => w.place !== 'left' && !thisWidgets.some(t => w.id === t.id)), ]); return; } - defaultStore.set('widgets', [ - ...defaultStore.state.widgets.filter(w => w.place === 'left' && !thisWidgets.some(t => w.id === t.id)), + store.set('widgets', [ + ...store.state.widgets.filter(w => w.place === 'left' && !thisWidgets.some(t => w.id === t.id)), ...thisWidgets.map(w => ({ ...w, place: 'right' })), ]); } diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index 8bcb260677..9ba9786ca8 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -70,15 +70,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, provide, ref, computed } from 'vue'; -import XCommon from './_common_/common.vue'; import { instanceName } from '@@/js/config.js'; +import XCommon from './_common_/common.vue'; +import type { PageMetadata } from '@/scripts/page-metadata.js'; import * as os from '@/os.js'; import { instance } from '@/instance.js'; import XSigninDialog from '@/components/MkSigninDialog.vue'; import XSignupDialog from '@/components/MkSignupDialog.vue'; -import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; -import type { PageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue'; import { mainRouter } from '@/router/main.js'; @@ -108,7 +107,7 @@ const announcements = { limit: 10, }; -const isTimelineAvailable = ref(instance.policies?.ltlAvailable || instance.policies?.gtlAvailable); +const isTimelineAvailable = ref(instance.policies.ltlAvailable || instance.policies.gtlAvailable); const showMenu = ref(false); const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); @@ -116,10 +115,6 @@ const narrow = ref(window.innerWidth < 1280); const keymap = computed(() => { return { - 'd': () => { - if (ColdDeviceStorage.get('syncDeviceDarkMode')) return; - defaultStore.set('darkMode', !defaultStore.state.darkMode); - }, 's': () => { mainRouter.push('/search'); }, diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index 89716575a9..6e56c9115e 100644 --- a/packages/frontend/src/widgets/WidgetFederation.vue +++ b/packages/frontend/src/widgets/WidgetFederation.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="wbrkwalb"> <MkLoading v-if="fetching"/> - <TransitionGroup v-else tag="div" :name="defaultStore.state.animation ? 'chart' : ''" class="instances"> + <TransitionGroup v-else tag="div" :name="prefer.s.animation ? 'chart' : ''" class="instances"> <div v-for="(instance, i) in instances" :key="instance.id" class="instance"> <img :src="getInstanceIcon(instance)" alt=""/> <div class="body"> @@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const name = 'federation'; diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index 84ba05b5d3..00d0ec7fa5 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -59,7 +59,7 @@ import { useStream } from '@/stream.js'; import kmg from '@/filters/kmg.js'; import * as sound from '@/scripts/sound.js'; import { deepClone } from '@/scripts/clone.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const name = 'jobQueue'; @@ -104,7 +104,7 @@ const prev = reactive({} as typeof current); const jammedAudioBuffer = ref<AudioBuffer | null>(null); const jammedSoundNodePlaying = ref<boolean>(false); -if (defaultStore.state.sound_masterVolume) { +if (prefer.s['sound.masterVolume']) { sound.loadAudio('/client-assets/sounds/syuilo/queue-jammed.mp3').then(buf => { if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer'); jammedAudioBuffer.value = buf; diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue index 65ab7a7075..4705de2016 100644 --- a/packages/frontend/src/widgets/WidgetMemo.vue +++ b/packages/frontend/src/widgets/WidgetMemo.vue @@ -21,7 +21,7 @@ import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; const name = 'memo'; @@ -48,12 +48,12 @@ const { widgetProps, configure } = useWidgetPropsManager(name, emit, ); -const text = ref<string | null>(defaultStore.state.memo); +const text = ref<string | null>(store.state.memo); const changed = ref(false); let timeoutId; const saveMemo = () => { - defaultStore.set('memo', text.value); + store.set('memo', text.value); changed.value = false; }; @@ -63,7 +63,7 @@ const onChange = () => { timeoutId = window.setTimeout(saveMemo, 1000); }; -watch(() => defaultStore.reactiveState.memo, newText => { +watch(() => store.reactiveState.memo, newText => { text.value = newText.value; }); diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue index 6d13ba09cc..6c7c82f229 100644 --- a/packages/frontend/src/widgets/WidgetPhotos.vue +++ b/packages/frontend/src/widgets/WidgetPhotos.vue @@ -31,7 +31,7 @@ import { useStream } from '@/stream.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { i18n } from '@/i18n.js'; const name = 'photos'; @@ -70,7 +70,7 @@ const onDriveFileCreated = (file) => { }; const thumbnail = (image: Misskey.entities.DriveFile): string => { - return defaultStore.state.disableShowingAnimatedImages + return prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl ?? image.url; }; diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue index 3354912c07..1e3ca74d40 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="wbrkwala"> <MkLoading v-if="fetching"/> - <TransitionGroup v-else tag="div" :name="defaultStore.state.animation ? 'chart' : ''" class="tags"> + <TransitionGroup v-else tag="div" :name="prefer.s.animation ? 'chart' : ''" class="tags"> <div v-for="stat in stats" :key="stat.tag"> <div class="tag"> <MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA> @@ -26,15 +26,15 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const name = 'hashtags'; diff --git a/packages/frontend/test/emoji.test.ts b/packages/frontend/test/emoji.test.ts index cf686efd0d..ffdc858b75 100644 --- a/packages/frontend/test/emoji.test.ts +++ b/packages/frontend/test/emoji.test.ts @@ -5,7 +5,7 @@ import { describe, test, assert, afterEach } from 'vitest'; import { render, cleanup, type RenderResult } from '@testing-library/vue'; -import { defaultStoreState } from './init.js'; +import { preferState } from './init.js'; import { getEmojiName } from '@@/js/emojilist.js'; import { components } from '@/components/index.js'; import { directives } from '@/directives/index.js'; @@ -21,12 +21,12 @@ describe('Emoji', () => { afterEach(() => { cleanup(); - defaultStoreState.emojiStyle = ''; + preferState.emojiStyle = ''; }); describe('MkEmoji', () => { test('Should render selector-less heart with color in native mode', async () => { - defaultStoreState.emojiStyle = 'native'; + preferState.emojiStyle = 'native'; const mkEmoji = await renderEmoji('\u2764'); // monochrome heart assert.ok(mkEmoji.queryByText('\u2764\uFE0F')); // colored heart assert.ok(!mkEmoji.queryByText('\u2764')); diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts index 0cde571dcb..3b6b4d581b 100644 --- a/packages/frontend/test/init.ts +++ b/packages/frontend/test/init.ts @@ -17,7 +17,7 @@ updateI18n(locales['en-US']); // XXX: misskey-js panics if WebSocket is not defined vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; }); -export const defaultStoreState: Record<string, unknown> = { +export const preferState: Record<string, unknown> = { // なんかtestがうまいこと動かないのでここに書く dataSaver: { @@ -29,11 +29,11 @@ export const defaultStoreState: Record<string, unknown> = { }; -// XXX: defaultStore somehow becomes undefined in vitest? -vi.mock('@/store.js', () => { +// XXX: store somehow becomes undefined in vitest? +vi.mock('@/preferences.js', () => { return { - defaultStore: { - state: defaultStoreState, + prefer: { + s: preferState, }, }; }); |