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 41bffd3a13.

* 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>
This commit is contained in:
syuilo
2025-03-09 12:34:08 +09:00
committed by GitHub
parent 05cdc095c0
commit d30ddd4c2e
181 changed files with 3437 additions and 2463 deletions

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkLoading v-if="!loaded"/>
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear>
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear>
<div v-show="loaded" :class="$style.root">
<img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/>
<div class="_gaps">
@@ -27,15 +27,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { version } from '@@/js/config.js';
import MkButton from '@/components/MkButton.vue';
import MkLink from '@/components/MkLink.vue';
import { version } from '@@/js/config.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js';
import { defaultStore } from '@/store.js';
import { prefer } from '@/preferences.js';
import { serverErrorImageUrl } from '@/instance.js';
const props = withDefaults(defineProps<{

View File

@@ -143,7 +143,7 @@ import MkInfo from '@/components/MkInfo.vue';
import { physics } from '@/scripts/physics.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { defaultStore } from '@/store.js';
import { store } from '@/store.js';
import * as os from '@/os.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
@@ -406,7 +406,7 @@ const easterEggEngine = ref<{ stop: () => void } | null>(null);
const containerEl = shallowRef<HTMLElement>();
function iconLoaded() {
const emojis = defaultStore.state.reactions;
const emojis = store.state.reactions;
const containerWidth = containerEl.value.offsetWidth;
for (let i = 0; i < 32; i++) {
easterEggEmojis.value.push({

View File

@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
</div>
<MkInfo v-if="!defaultStore.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
<MkInfo v-if="!store.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
{{ i18n.ts._abuseUserReport.resolveTutorial }}
</MkInfo>
@@ -68,7 +68,7 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import { defaultStore } from '@/store.js';
import { store } from '@/store.js';
const reports = shallowRef<InstanceType<typeof MkPagination>>();
@@ -93,7 +93,7 @@ function resolved(reportId) {
}
function closeTutorial() {
defaultStore.set('abusesTutorial', false);
store.set('abusesTutorial', false);
}
const headerActions = computed(() => []);

View File

@@ -78,6 +78,11 @@ SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as Misskey from 'misskey-js';
import { onMounted, ref, useCssModule } from 'vue';
import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
import type { DroppedFile } from '@/scripts/file-drop.js';
import type { GridSetting } from '@/components/grid/grid.js';
import type { GridRow } from '@/components/grid/row.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import {
emptyStrToEmptyArray,
@@ -88,7 +93,6 @@ import MkGrid from '@/components/grid/MkGrid.vue';
import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { defaultStore } from '@/store.js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
@@ -99,11 +103,7 @@ import { extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
import type { DroppedFile } from '@/scripts/file-drop.js';
import type { GridSetting } from '@/components/grid/grid.js';
import type { GridRow } from '@/components/grid/row.js';
import { prefer } from '@/preferences.js';
const MAXIMUM_EMOJI_REGISTER_COUNT = 100;
@@ -244,8 +244,8 @@ function setupGrid(): GridSetting {
const uploadFolders = ref<FolderItem[]>([]);
const gridItems = ref<GridItem[]>([]);
const selectedFolderId = ref(defaultStore.state.uploadFolder);
const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading);
const selectedFolderId = ref(prefer.s.uploadFolder);
const keepOriginalUploading = ref(prefer.s.keepOriginalUploading);
const directoryToCategory = ref<boolean>(false);
const registerButtonDisabled = ref<boolean>(false);
const requestLogs = ref<RequestLogItem[]>([]);

View File

@@ -17,7 +17,7 @@ import { onMounted, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { store } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
import { initChart } from '@/scripts/init-chart.js';
@@ -54,7 +54,7 @@ async function renderChart() {
const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const colorRead = '#3498db';
const colorWrite = '#2ecc71';

View File

@@ -27,7 +27,7 @@ import isChromatic from 'chromatic';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
import { defaultStore } from '@/store.js';
import { store } from '@/store.js';
import { alpha } from '@/scripts/color.js';
import { initChart } from '@/scripts/init-chart.js';
@@ -68,7 +68,7 @@ onMounted(async () => {
const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const succColor = '#87e000';
const failColor = '#ff4400';

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div>
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
<MkLoading v-if="fetching"/>
<div v-else :class="$style.instances">
<MkA v-for="(instance, i) in instances" :key="instance.id" v-tooltip.mfm.noDelay="`${instance.name}\n${instance.host}\n${instance.softwareName} ${instance.softwareVersion}`" :to="`/instance-info/${instance.host}`" :class="$style.instance">
@@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
import { defaultStore } from '@/store.js';
import { prefer } from '@/preferences.js';
const instances = ref<Misskey.entities.FederationInstance[]>([]);
const fetching = ref(true);

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div>
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
<MkLoading v-if="fetching"/>
<div v-else :class="$style.root" class="_panel">
<MkA v-for="user in moderators" :key="user.id" class="user" :to="`/admin/user/${user.id}`">
@@ -18,9 +18,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import * as Misskey from 'misskey-js';
import { defaultStore } from '@/store.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { prefer } from '@/preferences.js';
const moderators = ref<Misskey.entities.UserDetailed[] | null>(null);
const fetching = ref(true);

View File

@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, shallowRef } from 'vue';
import { Chart } from 'chart.js';
import { defaultStore } from '@/store.js';
import { store } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
import { alpha } from '@/scripts/color.js';
@@ -67,7 +67,7 @@ const color =
'?' as never;
onMounted(() => {
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
chartInstance = new Chart(chartEl.value, {
type: 'line',

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div>
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
<MkLoading v-if="fetching"/>
<div v-else :class="$style.root">
<div class="item _panel users">
@@ -68,7 +68,7 @@ import MkNumberDiff from '@/components/MkNumberDiff.vue';
import MkNumber from '@/components/MkNumber.vue';
import { i18n } from '@/i18n.js';
import { customEmojis } from '@/custom-emojis.js';
import { defaultStore } from '@/store.js';
import { prefer } from '@/preferences.js';
const stats = ref<Misskey.entities.StatsResponse | null>(null);
const usersComparedToThePrevDay = ref<number>();

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root">
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
<MkLoading v-if="fetching"/>
<div v-else class="users">
<MkA v-for="(user, i) in newUsers" :key="user.id" :to="`/admin/user/${user.id}`" class="user">
@@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { defaultStore } from '@/store.js';
import { prefer } from '@/preferences.js';
const newUsers = ref<Misskey.entities.UserDetailed[] | null>(null);
const fetching = ref(true);

View File

@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, shallowRef } from 'vue';
import { Chart } from 'chart.js';
import { defaultStore } from '@/store.js';
import { store } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
import { alpha } from '@/scripts/color.js';
@@ -67,7 +67,7 @@ const color =
'?' as never;
onMounted(() => {
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
chartInstance = new Chart(chartEl.value, {
type: 'line',

View File

@@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800">
<Transition
:enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''"
:leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
:enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
:leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
:enterActiveClass="prefer.s.animation ? $style.fadeEnterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.fadeLeaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.fadeEnterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.fadeLeaveTo : ''"
mode="out-in"
>
<div v-if="announcement" :key="announcement.id" class="_panel" :class="$style.announcement">
@@ -56,7 +56,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i, updateAccountPartial } from '@/account.js';
import { defaultStore } from '@/store.js';
import { prefer } from '@/preferences.js';
const props = defineProps<{
announcementId: string;

View File

@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo>
<!-- スマホタブレットの場合キーボードが表示されると投稿が見づらくなるのでデスクトップ場合のみ自動でフォーカスを当てる -->
<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
<MkPostForm v-if="$i && prefer.r.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
</div>
@@ -75,6 +75,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import type { PageHeaderItem } from '@/types/page-header.js';
import MkPostForm from '@/components/MkPostForm.vue';
import MkTimeline from '@/components/MkTimeline.vue';
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
@@ -85,16 +87,14 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { deviceKind } from '@/scripts/device-kind.js';
import MkNotes from '@/components/MkNotes.vue';
import { url } from '@@/js/config.js';
import { favoritedChannelsCache } from '@/cache.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import { defaultStore } from '@/store.js';
import { prefer } from '@/preferences.js';
import MkNote from '@/components/MkNote.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import type { PageHeaderItem } from '@/types/page-header.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { notesSearchAvailable } from '@/scripts/check-permissions.js';

View File

@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
<img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
<img v-if="store.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
<canvas ref="canvasEl" :class="$style.canvas"/>
<Transition
@@ -195,6 +195,8 @@ import { computed, onDeactivated, onMounted, onUnmounted, ref, shallowRef, watch
import * as Matter from 'matter-js';
import * as Misskey from 'misskey-js';
import { DropAndFusionGame } from 'misskey-bubble-game';
import { useInterval } from '@@/js/use-interval.js';
import { apiUrl } from '@@/js/config.js';
import type { Mono } from 'misskey-bubble-game';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
@@ -203,15 +205,14 @@ import MkNumber from '@/components/MkNumber.vue';
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
import MkButton from '@/components/MkButton.vue';
import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js';
import { store } from '@/store.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { useInterval } from '@@/js/use-interval.js';
import { apiUrl } from '@@/js/config.js';
import { $i } from '@/account.js';
import * as sound from '@/scripts/sound.js';
import MkRange from '@/components/MkRange.vue';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { prefer } from '@/preferences.js';
type FrontendMonoDefinition = {
id: string;
@@ -586,8 +587,8 @@ const showConfig = ref(false);
const replaying = ref(false);
const replayPlaybackRate = ref(1);
const currentFrame = ref(0);
const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume);
const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume);
const bgmVolume = ref(prefer.s['game.dropAndFusion'].bgmVolume);
const sfxVolume = ref(prefer.s['game.dropAndFusion'].sfxVolume);
watch(replayPlaybackRate, (newValue) => {
game.replayPlaybackRate = newValue;
@@ -623,7 +624,7 @@ function loadMonoTextures() {
if (renderer.textures[mono.img]) return;
let src = mono.img;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (monoTextureUrls[mono.img]) {
src = monoTextureUrls[mono.img];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -649,7 +650,6 @@ function loadMonoTextures() {
function getTextureImageUrl(mono: Mono) {
const def = monoDefinitions.value.find(x => x.id === mono.id)!;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (monoTextureUrls[def.img]) {
return monoTextureUrls[def.img];
@@ -853,13 +853,13 @@ function exportLog() {
}
function updateSettings<
K extends keyof typeof defaultStore.state.dropAndFusion,
V extends typeof defaultStore.state.dropAndFusion[K],
K extends keyof typeof prefer.s['game.dropAndFusion'],
V extends typeof prefer.s['game.dropAndFusion'][K],
>(key: K, value: V) {
const changes: { [P in K]?: V } = {};
changes[key] = value;
defaultStore.set('dropAndFusion', {
...defaultStore.state.dropAndFusion,
prefer.set('game.dropAndFusion', {
...prefer.s['game.dropAndFusion'],
...changes,
});
}
@@ -909,8 +909,8 @@ function getGameImageDriveFile() {
formData.append('name', `bubble-game-${Date.now()}.png`);
formData.append('isSensitive', 'false');
formData.append('i', $i.token);
if (defaultStore.state.uploadFolder) {
formData.append('folderId', defaultStore.state.uploadFolder);
if (prefer.s.uploadFolder) {
formData.append('folderId', prefer.s.uploadFolder);
}
window.fetch(apiUrl + '/drive/files/create', {

View File

@@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700">
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
<div v-if="flash" :key="flash.id">
<Transition :name="defaultStore.state.animation ? 'zoom' : ''" mode="out-in">
<Transition :name="prefer.s.animation ? 'zoom' : ''" mode="out-in">
<div v-if="started" :class="$style.started">
<div class="main _panel">
<MkAsUi v-if="root" :component="root" :components="components"/>
@@ -79,7 +79,7 @@ import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import MkFolder from '@/components/MkFolder.vue';
import MkCode from '@/components/MkCode.vue';
import { defaultStore } from '@/store.js';
import { prefer } from '@/preferences.js';
import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';

View File

@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="1000" :marginMin="16" :marginMax="32">
<div class="_root">
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
<div v-if="post" class="rkxwuolj">
<div class="files">
<div v-for="file in post.files" :key="file.id" class="file">
@@ -65,6 +65,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, ref, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import type { MenuItem } from '@/types/menu.js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -72,15 +74,13 @@ import MkContainer from '@/components/MkContainer.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
import { url } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { defaultStore } from '@/store.js';
import { prefer } from '@/preferences.js';
import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { useRouter } from '@/router/supplier.js';
import type { MenuItem } from '@/types/menu.js';
const router = useRouter();

View File

@@ -45,18 +45,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, onActivated, onDeactivated, nextTick } from 'vue';
import type { Extension } from '@/components/MkExtensionInstaller.vue';
import type { AiScriptPluginMeta } from '@/plugin.js';
import MkLoading from '@/components/global/MkLoading.vue';
import MkExtensionInstaller from '@/components/MkExtensionInstaller.vue';
import type { Extension } from '@/components/MkExtensionInstaller.vue';
import MkButton from '@/components/MkButton.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkUrl from '@/components/global/MkUrl.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js';
import type { AiScriptPluginMeta } from '@/scripts/install-plugin.js';
import { parseThemeCode, installTheme } from '@/scripts/install-theme.js';
import { parsePluginMeta, installPlugin } from '@/plugin.js';
import { parseThemeCode, installTheme } from '@/scripts/theme.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';

View File

@@ -67,15 +67,15 @@ import MkFolder from '@/components/MkFolder.vue';
import MkInput from '@/components/MkInput.vue';
import { userListsCache } from '@/cache.js';
import { signinRequired } from '@/account.js';
import { defaultStore } from '@/store.js';
import MkPagination from '@/components/MkPagination.vue';
import { mainRouter } from '@/router/main.js';
import { prefer } from '@/preferences.js';
const $i = signinRequired();
const {
enableInfiniteScroll,
} = defaultStore.reactiveState;
} = prefer.r;
const props = defineProps<{
listId: string;

View File

@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800">
<div>
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
<div v-if="note">
<div v-if="showNext" class="_margin">
<MkNotes class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/>
@@ -61,7 +61,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
import MkClipPreview from '@/components/MkClipPreview.vue';
import { defaultStore } from '@/store.js';
import { prefer } from '@/preferences.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
import { serverContext, assertServerContext } from '@/server-context.js';

View File

@@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800">
<Transition
:enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''"
:leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
:enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
:leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
:enterActiveClass="prefer.s.animation ? $style.fadeEnterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.fadeLeaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.fadeEnterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.fadeLeaveTo : ''"
mode="out-in"
>
<div v-if="page" :key="page.id" class="_gaps">
@@ -100,11 +100,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, ref, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import type { MenuItem } from '@/types/menu.js';
import XPage from '@/components/page/page.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { url } from '@@/js/config.js';
import MkMediaImage from '@/components/MkMediaImage.vue';
import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
@@ -113,7 +114,7 @@ import MkPagination from '@/components/MkPagination.vue';
import MkPagePreview from '@/components/MkPagePreview.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { pageViewInterruptors, defaultStore } from '@/store.js';
import { pageViewInterruptors } from '@/store.js';
import { deepClone } from '@/scripts/clone.js';
import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
@@ -121,7 +122,7 @@ import { instance } from '@/instance.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { useRouter } from '@/router/supplier.js';
import type { MenuItem } from '@/types/menu.js';
import { prefer } from '@/preferences.js';
const router = useRouter();

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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(() => []);

View File

@@ -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;

View File

@@ -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,
});

View File

@@ -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({

View File

@@ -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',

View File

@@ -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,

View File

@@ -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,
}));

View File

@@ -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 });

View File

@@ -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);

View File

@@ -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>
<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="_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>
<MkFolder>
<template #icon><i class="ti ti-terminal-2"></i></template>
<template #label>{{ i18n.ts._plugin.viewLog }}</template>
<div class="_gaps_s">
<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 inline @click="copy(pluginLogs.get(plugin.id)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
<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_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>
<MkCode :code="pluginLogs.get(plugin.id)?.join('\n') ?? ''"/>
</div>
</MkFolder>
<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 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>
<MkCode :code="plugin.src ?? ''" lang="is"/>
<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.installId)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
</div>
<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>
<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="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();
});

View File

@@ -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>

View File

@@ -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 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 overridedDeviceKind = computed(store.makeGetterSetter('overridedDeviceKind'));
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,
});

View File

@@ -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">
<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>
<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>
<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) {

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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(() => []);

View File

@@ -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';

View File

@@ -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();
});

View File

@@ -26,7 +26,7 @@ import MkButton from '@/components/MkButton.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
import { store } from '@/store.js';
import * as os from '@/os.js';
import { genEmbedCode } from '@/scripts/get-embed-code.js';
@@ -44,11 +44,11 @@ const pagination = {
const notes = ref<InstanceType<typeof MkNotes>>();
async function post() {
defaultStore.set('postFormHashtags', props.tag);
defaultStore.set('postFormWithHashtags', true);
store.set('postFormHashtags', props.tag);
store.set('postFormWithHashtags', true);
await os.post();
defaultStore.set('postFormHashtags', '');
defaultStore.set('postFormWithHashtags', false);
store.set('postFormHashtags', '');
store.set('postFormWithHashtags', false);
notes.value?.pagingComponent?.reload();
}

View File

@@ -78,24 +78,23 @@ import { toUnicode } from 'punycode.js';
import tinycolor from 'tinycolor2';
import { v4 as uuid } from 'uuid';
import JSON5 from 'json5';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import { host } from '@@/js/config.js';
import type { Theme } from '@/scripts/theme.js';
import MkButton from '@/components/MkButton.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkFolder from '@/components/MkFolder.vue';
import { $i } from '@/account.js';
import { applyTheme } from '@/scripts/theme.js';
import type { Theme } from '@/scripts/theme.js';
import { host } from '@@/js/config.js';
import * as os from '@/os.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { store } from '@/store.js';
import { addTheme } from '@/theme-store.js';
import { i18n } from '@/i18n.js';
import { useLeaveGuard } from '@/scripts/use-leave-guard.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { prefer } from '@/preferences.js';
const bgColors = [
{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
@@ -201,10 +200,10 @@ async function saveAs() {
if (description.value) theme.value.desc = description.value;
await addTheme(theme.value);
applyTheme(theme.value);
if (defaultStore.state.darkMode) {
ColdDeviceStorage.set('darkTheme', theme.value);
if (store.state.darkMode) {
prefer.set('darkTheme', theme.value);
} else {
ColdDeviceStorage.set('lightTheme', theme.value);
prefer.set('lightTheme', theme.value);
}
changed.value = false;
os.alert({

View File

@@ -9,10 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="800">
<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
<div :key="src" ref="rootEl">
<MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
<MkInfo v-if="isBasicTimeline(src) && !store.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
{{ i18n.ts._timelineDescription[src] }}
</MkInfo>
<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
<MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
<div :class="$style.tl">
<MkTimeline
@@ -36,25 +36,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, provide, shallowRef, ref, onMounted, onActivated } from 'vue';
import { scroll } from '@@/js/scroll.js';
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import type { MenuItem } from '@/types/menu.js';
import type { BasicTimelineType } from '@/timelines.js';
import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { scroll } from '@@/js/scroll.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js';
import { deviceKind } from '@/scripts/device-kind.js';
import { deepMerge } from '@/scripts/merge.js';
import type { MenuItem } from '@/types/menu.js';
import { miLocalStorage } from '@/local-storage.js';
import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
import type { BasicTimelineType } from '@/timelines.js';
import { prefer } from '@/preferences.js';
provide('shouldOmitHeaderTitle', true);
@@ -66,18 +67,18 @@ type TimelinePageSrc = BasicTimelineType | `list:${string}`;
const queue = ref(0);
const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global');
const src = computed<TimelinePageSrc>({
get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value),
get: () => ($i ? store.reactiveState.tl.value.src : srcWhenNotSignin.value),
set: (x) => saveSrc(x),
});
const withRenotes = computed<boolean>({
get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
get: () => store.reactiveState.tl.value.filter.withRenotes,
set: (x) => saveTlFilter('withRenotes', x),
});
// computed内での無限ループを防ぐためのフラグ
const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>(
defaultStore.reactiveState.tl.value.filter.withReplies ? 'withReplies' :
defaultStore.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' :
store.reactiveState.tl.value.filter.withReplies ? 'withReplies' :
store.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' :
false,
);
@@ -87,7 +88,7 @@ const withReplies = computed<boolean>({
if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') {
return false;
} else {
return defaultStore.reactiveState.tl.value.filter.withReplies;
return store.reactiveState.tl.value.filter.withReplies;
}
},
set: (x) => saveTlFilter('withReplies', x),
@@ -97,7 +98,7 @@ const onlyFiles = computed<boolean>({
if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') {
return false;
} else {
return defaultStore.reactiveState.tl.value.filter.onlyFiles;
return store.reactiveState.tl.value.filter.onlyFiles;
}
},
set: (x) => saveTlFilter('onlyFiles', x),
@@ -114,7 +115,7 @@ watch([withReplies, onlyFiles], ([withRepliesTo, onlyFilesTo]) => {
});
const withSensitive = computed<boolean>({
get: () => defaultStore.reactiveState.tl.value.filter.withSensitive,
get: () => store.reactiveState.tl.value.filter.withSensitive,
set: (x) => saveTlFilter('withSensitive', x),
});
@@ -195,23 +196,23 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
}
function saveSrc(newSrc: TimelinePageSrc): void {
const out = deepMerge({ src: newSrc }, defaultStore.state.tl);
const out = deepMerge({ src: newSrc }, store.state.tl);
if (newSrc.startsWith('userList:')) {
const id = newSrc.substring('userList:'.length);
out.userList = defaultStore.reactiveState.pinnedUserLists.value.find(l => l.id === id) ?? null;
out.userList = prefer.r.pinnedUserLists.value.find(l => l.id === id) ?? null;
}
defaultStore.set('tl', out);
store.set('tl', out);
if (['local', 'global'].includes(newSrc)) {
srcWhenNotSignin.value = newSrc as 'local' | 'global';
}
}
function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue: boolean) {
function saveTlFilter(key: keyof typeof store.state.tl.filter, newValue: boolean) {
if (key !== 'withReplies' || $i) {
const out = deepMerge({ filter: { [key]: newValue } }, defaultStore.state.tl);
defaultStore.set('tl', out);
const out = deepMerge({ filter: { [key]: newValue } }, store.state.tl);
store.set('tl', out);
}
}
@@ -230,9 +231,9 @@ function focus(): void {
function closeTutorial(): void {
if (!isBasicTimeline(src.value)) return;
const before = defaultStore.state.timelineTutorials;
const before = store.state.timelineTutorials;
before[src.value] = true;
defaultStore.set('timelineTutorials', before);
store.set('timelineTutorials', before);
}
function switchTlIfNeeded() {
@@ -298,7 +299,7 @@ const headerActions = computed(() => {
return tmp;
});
const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({
const headerTabs = computed(() => [...(prefer.r.pinnedUserLists.value.map(l => ({
key: 'list:' + l.id,
title: l.name,
icon: 'ti ti-star',

View File

@@ -20,7 +20,7 @@ import type { ChartDataset } from 'chart.js';
import * as Misskey from 'misskey-js';
import gradient from 'chartjs-plugin-gradient';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { store } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
import { initChart } from '@/scripts/init-chart.js';
@@ -64,7 +64,7 @@ async function renderChart() {
const raw = await misskeyApi('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const colorFollowLocal = '#008FFB';
const colorFollowRemote = '#008FFB88';

View File

@@ -20,7 +20,7 @@ import type { ChartDataset } from 'chart.js';
import * as Misskey from 'misskey-js';
import gradient from 'chartjs-plugin-gradient';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { store } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
import { initChart } from '@/scripts/init-chart.js';
@@ -64,7 +64,7 @@ async function renderChart() {
const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const colorNormal = '#008FFB';
const colorReply = '#FEB019';

View File

@@ -20,7 +20,7 @@ import type { ChartDataset } from 'chart.js';
import * as Misskey from 'misskey-js';
import gradient from 'chartjs-plugin-gradient';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { store } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
import { initChart } from '@/scripts/init-chart.js';
@@ -64,7 +64,7 @@ async function renderChart() {
const raw = await misskeyApi('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const colorUser = '#3498db';
const colorVisitor = '#2ecc71';

View File

@@ -176,7 +176,6 @@ import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { $i, iAmModerator } from '@/account.js';
import { dateString } from '@/filters/date.js';
import { confetti } from '@/scripts/confetti.js';
@@ -185,6 +184,7 @@ import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFf
import { useRouter } from '@/router/supplier.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import MkSparkle from '@/components/MkSparkle.vue';
import { prefer } from '@/preferences.js';
function calcAge(birthdate: string): number {
const date = new Date(birthdate);
@@ -236,7 +236,7 @@ watch(moderationNote, async () => {
const style = computed(() => {
if (props.user.bannerUrl == null) return {};
if (defaultStore.state.disableShowingAnimatedImages) {
if (prefer.s.disableShowingAnimatedImages) {
return {
backgroundImage: `url(${ getStaticImageUrl(props.user.bannerUrl) })`,
};