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/frontend/src/pages/settings | |
| parent | [skip ci] Update CHANGELOG.md (prepend template) (diff) | |
| download | misskey-d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e.tar.gz misskey-d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e.tar.bz2 misskey-d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e.zip | |
Refine preferences (#15597)
* wip
* wip
* wip
* test
* wip rollup pluginでsearchIndexの情報生成
* wip
* SPDX
* wip: markerIdを自動付与
* rollupでビルド時・devモード時に毎回uuidを生成するように
* 開発サーバーでだけ必要な挙動は開発サーバーのみで
* 条件が逆
* wip: childrenの生成
* update comment
* update comment
* rename auto generated file
* hashをパスと行数から決定
* Update privacy.vue
* Update privacy.vue
* wip
* Update general.vue
* Update general.vue
* wip
* wip
* Update SearchMarker.vue
* wip
* Update profile.vue
* Update mute-block.vue
* Update mute-block.vue
* Update general.vue
* Update general.vue
* childrenがduplicate key errorを吐く問題をいったん解決
* マーカーの形を成形
* loggerを置きかえ
* とりあえず省略記法に対応
* Refactor and Format codes
* wip
* Update settings-search-index.ts
* wip
* wip
* とりあえず不確定要因の仮置きidを削除
* hashの生成を正規化(絶対パスになっていたのを緩和)
* pathの入力を省略可能に
* adminでもパス生成できるように
* Update settings-search-index.ts
* Update privacy.vue
* wip
* build searchIndex
* wip
* build
* Update general.vue
* build
* Update sounds.vue
* build
* build
* Update sounds.vue
* 🎨
* 🎨
* Update privacy.vue
* Update privacy.vue
* Update security.vue
* create-search-indexを多少改善
* build
* Update 2fa.vue
* wip
* 必ずtransformCodeCacheを利用するように, キャッシュの明確な受け渡しを定義
* キャッシュはdevServerでなくても更新
* Revert "wip"
This reverts commit 41bffd3a13f55618bf939dc1c9acb2a77ead4054.
* inlining
* wip
* Update theme.vue
* 🎨
* wip normalize
* Update theme.vue
* キャッシュのパス変換
* build
* wip
* wip
* Update SearchMarker.vue
* i18n.ts['key'] の形式が取り出せない問題のFix
* build
* 仮でpath入れ
* 必ず絶対パスが使われるように
* wip
* 🎨
* storybookビルド時はcreateSearchIndexをしない
* inliningの構造化
* format code
* Update index.vue
* wip
* wip
* 🎨
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* clean up
* wip
* wip
* wip
* Update rollup-plugin-unwind-css-module-class-name.test.ts
* Update navbar.vue
* clean up
* wip
* wip
* wip
* wip
* wip
* Update preferences-backups.vue
* Update common.ts
* Update preferences.ts
* wip
* wip
* wip
* wip
* Update MkPreferenceContainer.vue
* Update MkPreferenceContainer.vue
* Update MkPreferenceContainer.vue
* enhance: 検索で上下矢印を使用することで検索結果を移動できるように
* Update main-boot.ts
* refactor
* wip
* Update sounds.vue
* fix(frontend): PageWindowでSearchMarkerが動作するように
* enhance(frontend): SearchMarkerの点滅を一定時間で止める
* wip
* lint fix
* fix: 子要素監視が抜けていたのを修正
* アニメーションの回数はCSSで制御するように
* refactor
* enhance(frontend): 検索インデックス作成時のログを削減
* revert
* fix
* fix
* Update preferences.ts
* Update preferences.ts
* wip
* Update preferences.ts
* wip
* 🎨
* wip
* Update MkPreferenceContainer.vue
* wip
* Update preferences.ts
* wip
* Update preferences.ts
* Update preferences.ts
* wip
* wip
* Update preferences.ts
* wip
* wip
* Update preferences.ts
* Update CHANGELOG.md
* Update preferences.ts
* Update deck-store.ts
* deckStoreをdefaultStoreに統合
* wip
* defaultStore -> store
* Update profile.ts
* wip
* refactor
* wip: plugin
* plugin
* plugin
* plugin
* Update plugin.ts
* wip
* Update plugin.vue
* Update preferences.ts
* Update main-boot.ts
* wip
* fix test
* Update plugin.vue
* Update plugin.vue
* Update utility.ts
* wip
* wip
* Update utility.ts
* wip
* wip
* clean up
* Update utility.ts
---------
Co-authored-by: tai-cha <dev@taichan.site>
Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com>
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
Diffstat (limited to 'packages/frontend/src/pages/settings')
21 files changed, 622 insertions, 947 deletions
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(); }); |