summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-03-06 23:15:19 +0900
committerGitHub <noreply@github.com>2025-03-06 23:15:19 +0900
commit0214a0001fee2355a6d48da8ae5790c24650be33 (patch)
tree64b8c3cd23dcfcdea9efc8b12ba583b949823149 /packages/frontend/src
parent[skip ci] Update CHANGELOG.md (prepend template) (diff)
downloadmisskey-0214a0001fee2355a6d48da8ae5790c24650be33.tar.gz
misskey-0214a0001fee2355a6d48da8ae5790c24650be33.tar.bz2
misskey-0214a0001fee2355a6d48da8ae5790c24650be33.zip
feat(frontend): 設定の検索 (#15505)
* wip * wip * wip * test * wip rollup pluginでsearchIndexの情報生成 * wip * SPDX * wip: markerIdを自動付与 * rollupでビルド時・devモード時に毎回uuidを生成するように * 開発サーバーでだけ必要な挙動は開発サーバーのみで * 条件が逆 * wip: childrenの生成 * update comment * update comment * rename auto generated file * hashをパスと行数から決定 * Update privacy.vue * Update privacy.vue * wip * Update general.vue * Update general.vue * wip * wip * Update SearchMarker.vue * wip * Update profile.vue * Update mute-block.vue * Update mute-block.vue * Update general.vue * Update general.vue * childrenがduplicate key errorを吐く問題をいったん解決 * マーカーの形を成形 * loggerを置きかえ * とりあえず省略記法に対応 * Refactor and Format codes * wip * Update settings-search-index.ts * wip * wip * とりあえず不確定要因の仮置きidを削除 * hashの生成を正規化(絶対パスになっていたのを緩和) * pathの入力を省略可能に * adminでもパス生成できるように * Update settings-search-index.ts * Update privacy.vue * wip * build searchIndex * wip * build * Update general.vue * build * Update sounds.vue * build * build * Update sounds.vue * 🎨 * 🎨 * Update privacy.vue * Update privacy.vue * Update security.vue * create-search-indexを多少改善 * build * Update 2fa.vue * wip * 必ずtransformCodeCacheを利用するように, キャッシュの明確な受け渡しを定義 * キャッシュはdevServerでなくても更新 * Revert "wip" This reverts commit 41bffd3a13f55618bf939dc1c9acb2a77ead4054. * inlining * wip * Update theme.vue * 🎨 * wip normalize * Update theme.vue * キャッシュのパス変換 * build * wip * wip * Update SearchMarker.vue * i18n.ts['key'] の形式が取り出せない問題のFix * build * 仮でpath入れ * 必ず絶対パスが使われるように * wip * 🎨 * storybookビルド時はcreateSearchIndexをしない * inliningの構造化 * format code * Update index.vue * wip * wip * 🎨 * wip * wip * wip * wip * wip * wip * wip * wip * clean up * Update navbar.vue * enhance: 検索で上下矢印を使用することで検索結果を移動できるように * refactor * fix(frontend): PageWindowでSearchMarkerが動作するように * enhance(frontend): SearchMarkerの点滅を一定時間で止める * lint fix * fix: 子要素監視が抜けていたのを修正 * アニメーションの回数はCSSで制御するように * refactor * enhance(frontend): 検索インデックス作成時のログを削減 * revert * fix * fix --------- Co-authored-by: tai-cha <dev@taichan.site> Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com> Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/components/MkDisableSection.vue41
-rw-r--r--packages/frontend/src/components/MkPageWindow.vue12
-rw-r--r--packages/frontend/src/components/MkSuperMenu.vue208
-rw-r--r--packages/frontend/src/components/global/SearchKeyword.vue14
-rw-r--r--packages/frontend/src/components/global/SearchLabel.vue14
-rw-r--r--packages/frontend/src/components/global/SearchMarker.vue116
-rw-r--r--packages/frontend/src/components/index.ts13
-rw-r--r--packages/frontend/src/pages/settings/2fa.vue126
-rw-r--r--packages/frontend/src/pages/settings/accessibility.vue91
-rw-r--r--packages/frontend/src/pages/settings/appearance.vue287
-rw-r--r--packages/frontend/src/pages/settings/avatar-decoration.vue58
-rw-r--r--packages/frontend/src/pages/settings/drive.vue123
-rw-r--r--packages/frontend/src/pages/settings/email.vue88
-rw-r--r--packages/frontend/src/pages/settings/general.vue492
-rw-r--r--packages/frontend/src/pages/settings/import-export.vue245
-rw-r--r--packages/frontend/src/pages/settings/index.vue41
-rw-r--r--packages/frontend/src/pages/settings/migration.vue6
-rw-r--r--packages/frontend/src/pages/settings/mute-block.vue255
-rw-r--r--packages/frontend/src/pages/settings/other.vue158
-rw-r--r--packages/frontend/src/pages/settings/preferences.vue434
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue313
-rw-r--r--packages/frontend/src/pages/settings/profile.vue227
-rw-r--r--packages/frontend/src/pages/settings/security.vue67
-rw-r--r--packages/frontend/src/pages/settings/sounds.vue70
-rw-r--r--packages/frontend/src/pages/settings/theme.vue106
-rw-r--r--packages/frontend/src/router/definition.ts20
-rw-r--r--packages/frontend/src/scripts/autogen/settings-search-index.ts815
-rw-r--r--packages/frontend/src/ui/_common_/navbar.vue126
-rw-r--r--packages/frontend/src/ui/universal.vue6
29 files changed, 3202 insertions, 1370 deletions
diff --git a/packages/frontend/src/components/MkDisableSection.vue b/packages/frontend/src/components/MkDisableSection.vue
new file mode 100644
index 0000000000..f596d5e3b5
--- /dev/null
+++ b/packages/frontend/src/components/MkDisableSection.vue
@@ -0,0 +1,41 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="[$style.root]">
+ <div :inert="disabled" :class="[{ [$style.disabled]: disabled }]">
+ <slot></slot>
+ </div>
+ <div v-if="disabled" :class="[$style.cover]"></div>
+</div>
+</template>
+
+<script lang="ts" setup>
+defineProps<{
+ disabled?: boolean;
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+ position: relative;
+}
+
+.disabled {
+ opacity: 0.7;
+}
+
+.cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ cursor: not-allowed;
+ --color: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
+ background-size: auto auto;
+ background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
+}
+</style>
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index e725d2a15d..c3fc1961eb 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -91,6 +91,14 @@ const buttonsRight = computed(() => {
});
const reloadCount = ref(0);
+function getSearchMarker(path: string) {
+ const hash = path.split('#')[1];
+ if (hash == null) return null;
+ return hash;
+}
+
+const searchMarkerId = ref<string | null>(getSearchMarker(props.initialPath));
+
windowRouter.addListener('push', ctx => {
history.value.push({ path: ctx.path, key: ctx.key });
});
@@ -101,7 +109,8 @@ windowRouter.addListener('replace', ctx => {
});
windowRouter.addListener('change', ctx => {
- console.log('windowRouter: change', ctx.path);
+ if (_DEV_) console.log('windowRouter: change', ctx.path);
+ searchMarkerId.value = getSearchMarker(ctx.path);
analytics.page({
path: ctx.path,
title: ctx.path,
@@ -111,6 +120,7 @@ windowRouter.addListener('change', ctx => {
windowRouter.init();
provide('router', windowRouter);
+provide('inAppSearchMarkerId', searchMarkerId);
provideMetadataReceiver((metadataGetter) => {
const info = metadataGetter();
pageMetadata.value = info;
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 397aa68ed6..d8dec3aa2f 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -4,27 +4,60 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="rrevdjwu" :class="{ grid }">
- <div v-for="group in def" class="group">
- <div v-if="group.title" class="title">{{ group.title }}</div>
+<div ref="rootEl" class="rrevdjwu" :class="{ grid }">
+ <MkInput
+ v-model="search"
+ :placeholder="i18n.ts.search"
+ type="search"
+ style="margin-bottom: 16px;"
+ @keydown="searchOnKeyDown"
+ >
+ <template #prefix><i class="ti ti-search"></i></template>
+ </MkInput>
- <div class="items">
- <template v-for="(item, i) in group.items">
- <a v-if="item.type === 'a'" :href="item.href" :target="item.target" class="_button item" :class="{ danger: item.danger, active: item.active }">
- <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
- <span class="text">{{ item.text }}</span>
- </a>
- <button v-else-if="item.type === 'button'" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
- <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
- <span class="text">{{ item.text }}</span>
- </button>
- <MkA v-else :to="item.to" class="_button item" :class="{ danger: item.danger, active: item.active }">
- <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
- <span class="text">{{ item.text }}</span>
- </MkA>
- </template>
+ <template v-if="search == ''">
+ <div v-for="group in def" class="group">
+ <div v-if="group.title" class="title">{{ group.title }}</div>
+
+ <div class="items">
+ <template v-for="(item, i) in group.items">
+ <a v-if="item.type === 'a'" :href="item.href" :target="item.target" class="_button item" :class="{ danger: item.danger, active: item.active }">
+ <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
+ <span class="text">{{ item.text }}</span>
+ </a>
+ <button v-else-if="item.type === 'button'" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
+ <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
+ <span class="text">{{ item.text }}</span>
+ </button>
+ <MkA v-else :to="item.to" class="_button item" :class="{ danger: item.danger, active: item.active }">
+ <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
+ <span class="text">{{ item.text }}</span>
+ </MkA>
+ </template>
+ </div>
+ </div>
+ </template>
+ <template v-else>
+ <div v-for="item, index in searchResult">
+ <MkA
+ :to="item.path + '#' + item.id"
+ class="_button searchResultItem"
+ :class="{ selected: searchSelectedIndex !== null && searchSelectedIndex === index }"
+ >
+ <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
+ <span class="text">
+ <template v-if="item.isRoot">
+ {{ item.label }}
+ </template>
+ <template v-else>
+ <span style="opacity: 0.7; font-size: 90%;">{{ item.parentLabels.join(' > ') }}</span>
+ <br>
+ <span>{{ item.label }}</span>
+ </template>
+ </span>
+ </MkA>
</div>
- </div>
+ </template>
</div>
</template>
@@ -58,10 +91,98 @@ export type SuperMenuDef = {
</script>
<script lang="ts" setup>
-defineProps<{
+import { useTemplateRef, ref, watch, nextTick } from 'vue';
+import type { SearchIndexItem } from '@/scripts/autogen/settings-search-index.js';
+import MkInput from '@/components/MkInput.vue';
+import { i18n } from '@/i18n.js';
+import { getScrollContainer } from '@@/js/scroll.js';
+import { useRouter } from '@/router/supplier.js';
+
+const props = defineProps<{
def: SuperMenuDef[];
grid?: boolean;
+ searchIndex: SearchIndexItem[];
}>();
+
+const router = useRouter();
+const rootEl = useTemplateRef('rootEl');
+
+const search = ref('');
+const searchSelectedIndex = ref<null | number>(null);
+const searchResult = ref<{
+ id: string;
+ path: string;
+ label: string;
+ icon?: string;
+ isRoot: boolean;
+ parentLabels: string[];
+}[]>([]);
+
+watch(search, (value) => {
+ searchResult.value = [];
+ searchSelectedIndex.value = null;
+
+ if (value === '') {
+ return;
+ }
+
+ const dive = (items: SearchIndexItem[], parents: SearchIndexItem[] = []) => {
+ for (const item of items) {
+ const matched =
+ item.label.includes(value.toLowerCase()) ||
+ item.keywords.some((x) => x.toLowerCase().includes(value.toLowerCase()));
+
+ if (matched) {
+ searchResult.value.push({
+ id: item.id,
+ path: item.path ?? parents.find((x) => x.path != null)?.path,
+ label: item.label,
+ parentLabels: parents.map((x) => x.label).toReversed(),
+ icon: item.icon ?? parents.find((x) => x.icon != null)?.icon,
+ isRoot: parents.length === 0,
+ });
+ }
+
+ if (item.children) {
+ dive(item.children, [item, ...parents]);
+ }
+ }
+ };
+
+ dive(props.searchIndex);
+});
+
+function searchOnKeyDown(ev: KeyboardEvent) {
+ if (ev.isComposing) return;
+
+ if (ev.key === 'Enter' && searchSelectedIndex.value != null) {
+ ev.preventDefault();
+ router.push(searchResult.value[searchSelectedIndex.value].path + '#' + searchResult.value[searchSelectedIndex.value].id);
+ } else if (ev.key === 'ArrowDown') {
+ ev.preventDefault();
+ const current = searchSelectedIndex.value ?? -1;
+ searchSelectedIndex.value = current + 1 >= searchResult.value.length ? 0 : current + 1;
+ } else if (ev.key === 'ArrowUp') {
+ ev.preventDefault();
+ const current = searchSelectedIndex.value ?? 0;
+ searchSelectedIndex.value = current - 1 < 0 ? searchResult.value.length - 1 : current - 1;
+ }
+
+ if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
+ nextTick(() => {
+ if (!rootEl.value) return;
+ const selectedEl = rootEl.value.querySelector<HTMLElement>('.searchResultItem.selected');
+ if (selectedEl != null) {
+ const scrollContainer = getScrollContainer(selectedEl);
+ if (!scrollContainer) return;
+ scrollContainer.scrollTo({
+ top: selectedEl.offsetTop - scrollContainer.clientHeight / 2 + selectedEl.clientHeight / 2,
+ behavior: 'instant',
+ });
+ }
+ });
+ }
+}
</script>
<style lang="scss" scoped>
@@ -184,5 +305,52 @@ defineProps<{
}
}
}
+
+ .searchResultItem {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 9px 16px 9px 8px;
+ border-radius: 9px;
+ font-size: 0.9em;
+
+ &:hover {
+ text-decoration: none;
+ background: var(--MI_THEME-panelHighlight);
+ }
+
+ &.selected {
+ outline: 2px solid var(--MI_THEME-focus);
+ }
+
+ &:focus-visible,
+ &.selected {
+ outline-offset: -2px;
+ }
+
+ &.active {
+ color: var(--MI_THEME-accent);
+ background: var(--MI_THEME-accentedBg);
+ }
+
+ &.danger {
+ color: var(--MI_THEME-error);
+ }
+
+ > .icon {
+ width: 32px;
+ margin-right: 2px;
+ flex-shrink: 0;
+ text-align: center;
+ opacity: 0.8;
+ }
+
+ > .text {
+ white-space: normal;
+ padding-right: 12px;
+ flex-shrink: 1;
+ }
+ }
}
</style>
diff --git a/packages/frontend/src/components/global/SearchKeyword.vue b/packages/frontend/src/components/global/SearchKeyword.vue
new file mode 100644
index 0000000000..27a284faf0
--- /dev/null
+++ b/packages/frontend/src/components/global/SearchKeyword.vue
@@ -0,0 +1,14 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<slot></slot>
+</template>
+
+<script lang="ts" setup>
+</script>
+
+<style lang="scss" module>
+</style>
diff --git a/packages/frontend/src/components/global/SearchLabel.vue b/packages/frontend/src/components/global/SearchLabel.vue
new file mode 100644
index 0000000000..27a284faf0
--- /dev/null
+++ b/packages/frontend/src/components/global/SearchLabel.vue
@@ -0,0 +1,14 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<slot></slot>
+</template>
+
+<script lang="ts" setup>
+</script>
+
+<style lang="scss" module>
+</style>
diff --git a/packages/frontend/src/components/global/SearchMarker.vue b/packages/frontend/src/components/global/SearchMarker.vue
new file mode 100644
index 0000000000..c5ec626cf4
--- /dev/null
+++ b/packages/frontend/src/components/global/SearchMarker.vue
@@ -0,0 +1,116 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div ref="root" :class="[$style.root, { [$style.highlighted]: highlighted }]">
+ <slot></slot>
+</div>
+</template>
+
+<script lang="ts" setup>
+import {
+ onActivated,
+ onDeactivated,
+ onMounted,
+ onBeforeUnmount,
+ watch,
+ computed,
+ ref,
+ useTemplateRef,
+ inject,
+} from 'vue';
+import type { Ref } from 'vue';
+
+const props = defineProps<{
+ markerId?: string;
+ label?: string;
+ icon?: string;
+ keywords?: string[];
+ children?: string[];
+ inlining?: string[];
+}>();
+
+const rootEl = useTemplateRef('root');
+const rootElMutationObserver = new MutationObserver(() => {
+ checkChildren();
+});
+const injectedSearchMarkerId = inject<Ref<string | null>>('inAppSearchMarkerId');
+const searchMarkerId = computed(() => injectedSearchMarkerId?.value ?? window.location.hash.slice(1));
+const highlighted = ref(props.markerId === searchMarkerId.value);
+
+function checkChildren() {
+ if (props.children?.includes(searchMarkerId.value)) {
+ const el = document.querySelector(`[data-in-app-search-marker-id="${searchMarkerId.value}"]`);
+ highlighted.value = el == null;
+ }
+}
+
+watch([
+ searchMarkerId,
+ () => props.children,
+], () => {
+ if (props.children != null && props.children.length > 0) {
+ checkChildren();
+ }
+}, { flush: 'post' });
+
+function init() {
+ checkChildren();
+
+ if (highlighted.value) {
+ rootEl.value?.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ });
+ }
+
+ if (rootEl.value != null) {
+ rootElMutationObserver.observe(rootEl.value, {
+ childList: true,
+ subtree: true,
+ });
+ }
+}
+
+function dispose() {
+ rootElMutationObserver.disconnect();
+}
+
+onMounted(init);
+onActivated(init);
+onDeactivated(dispose);
+onBeforeUnmount(dispose);
+</script>
+
+<style lang="scss" module>
+.root {
+ position: relative;
+}
+
+.highlighted {
+ &::after {
+ content: '';
+ position: absolute;
+ top: -8px;
+ left: -8px;
+ width: calc(100% + 16px);
+ height: calc(100% + 16px);
+ border-radius: 6px;
+ animation: blink 1s 3.5;
+ pointer-events: none;
+ }
+}
+
+@keyframes blink {
+ 0%, 100% {
+ background: color(from var(--MI_THEME-accent) srgb r g b / 0.05);
+ border: 1px solid color(from var(--MI_THEME-accent) srgb r g b / 0.7);
+ }
+ 50% {
+ background: transparent;
+ border: 1px solid transparent;
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index 0252bf0252..ebbad3e5b8 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -3,8 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import type { App } from 'vue';
-
import Mfm from './global/MkMfm.js';
import MkA from './global/MkA.vue';
import MkAcct from './global/MkAcct.vue';
@@ -26,6 +24,11 @@ import MkSpacer from './global/MkSpacer.vue';
import MkFooterSpacer from './global/MkFooterSpacer.vue';
import MkStickyContainer from './global/MkStickyContainer.vue';
import MkLazy from './global/MkLazy.vue';
+import SearchMarker from './global/SearchMarker.vue';
+import SearchLabel from './global/SearchLabel.vue';
+import SearchKeyword from './global/SearchKeyword.vue';
+
+import type { App } from 'vue';
export default function(app: App) {
for (const [key, value] of Object.entries(components)) {
@@ -55,6 +58,9 @@ export const components = {
MkFooterSpacer: MkFooterSpacer,
MkStickyContainer: MkStickyContainer,
MkLazy: MkLazy,
+ SearchMarker: SearchMarker,
+ SearchLabel: SearchLabel,
+ SearchKeyword: SearchKeyword,
};
declare module '@vue/runtime-core' {
@@ -80,5 +86,8 @@ declare module '@vue/runtime-core' {
MkFooterSpacer: typeof MkFooterSpacer;
MkStickyContainer: typeof MkStickyContainer;
MkLazy: typeof MkLazy;
+ SearchMarker: typeof SearchMarker;
+ SearchLabel: typeof SearchLabel;
+ SearchKeyword: typeof SearchKeyword;
}
}
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index 776f59dda3..806599e801 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -4,74 +4,82 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<FormSection :first="first">
- <template #label>{{ i18n.ts['2fa'] }}</template>
+<SearchMarker markerId="2fa" :keywords="['2fa']">
+ <FormSection :first="first">
+ <template #label><SearchLabel>{{ i18n.ts['2fa'] }}</SearchLabel></template>
- <div v-if="$i" class="_gaps_s">
- <MkInfo v-if="$i.twoFactorEnabled && $i.twoFactorBackupCodesStock === 'partial'" warn>
- {{ i18n.ts._2fa.backupCodeUsedWarning }}
- </MkInfo>
- <MkInfo v-if="$i.twoFactorEnabled && $i.twoFactorBackupCodesStock === 'none'" warn>
- {{ i18n.ts._2fa.backupCodesExhaustedWarning }}
- </MkInfo>
+ <div v-if="$i" class="_gaps_s">
+ <MkInfo v-if="$i.twoFactorEnabled && $i.twoFactorBackupCodesStock === 'partial'" warn>
+ {{ i18n.ts._2fa.backupCodeUsedWarning }}
+ </MkInfo>
+ <MkInfo v-if="$i.twoFactorEnabled && $i.twoFactorBackupCodesStock === 'none'" warn>
+ {{ i18n.ts._2fa.backupCodesExhaustedWarning }}
+ </MkInfo>
- <MkFolder :defaultOpen="true">
- <template #icon><i class="ti ti-shield-lock"></i></template>
- <template #label>{{ i18n.ts.totp }}</template>
- <template #caption>{{ i18n.ts.totpDescription }}</template>
- <template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
+ <SearchMarker :keywords="['totp', 'app']">
+ <MkFolder :defaultOpen="true">
+ <template #icon><i class="ti ti-shield-lock"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.totp }}</SearchLabel></template>
+ <template #caption><SearchKeyword>{{ i18n.ts.totpDescription }}</SearchKeyword></template>
+ <template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
- <div v-if="$i.twoFactorEnabled" class="_gaps_s">
- <div v-text="i18n.ts._2fa.alreadyRegistered"/>
- <template v-if="$i.securityKeysList.length > 0">
- <MkButton @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
- <MkInfo>{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
- </template>
- <MkButton v-else danger @click="unregisterTOTP">{{ i18n.ts.unregister }}</MkButton>
- </div>
+ <div v-if="$i.twoFactorEnabled" class="_gaps_s">
+ <div v-text="i18n.ts._2fa.alreadyRegistered"/>
+ <template v-if="$i.securityKeysList.length > 0">
+ <MkButton @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
+ <MkInfo>{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
+ </template>
+ <MkButton v-else danger @click="unregisterTOTP">{{ i18n.ts.unregister }}</MkButton>
+ </div>
- <div v-else-if="!$i.twoFactorEnabled" class="_gaps_s">
- <MkButton primary gradate @click="registerTOTP">{{ i18n.ts._2fa.registerTOTP }}</MkButton>
- <MkLink url="https://misskey-hub.net/docs/for-users/stepped-guides/how-to-enable-2fa/" target="_blank"><i class="ti ti-help-circle"></i> {{ i18n.ts.learnMore }}</MkLink>
- </div>
- </MkFolder>
+ <div v-else-if="!$i.twoFactorEnabled" class="_gaps_s">
+ <MkButton primary gradate @click="registerTOTP">{{ i18n.ts._2fa.registerTOTP }}</MkButton>
+ <MkLink url="https://misskey-hub.net/docs/for-users/stepped-guides/how-to-enable-2fa/" target="_blank"><i class="ti ti-help-circle"></i> {{ i18n.ts.learnMore }}</MkLink>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.securityKeyAndPasskey }}</template>
- <div class="_gaps_s">
- <MkInfo>
- {{ i18n.ts._2fa.securityKeyInfo }}
- </MkInfo>
+ <SearchMarker :keywords="['security', 'key', 'passkey']">
+ <MkFolder>
+ <template #icon><i class="ti ti-key"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.securityKeyAndPasskey }}</SearchLabel></template>
+ <div class="_gaps_s">
+ <MkInfo>
+ {{ i18n.ts._2fa.securityKeyInfo }}
+ </MkInfo>
- <MkInfo v-if="!webAuthnSupported()" warn>
- {{ i18n.ts._2fa.securityKeyNotSupported }}
- </MkInfo>
+ <MkInfo v-if="!webAuthnSupported()" warn>
+ {{ i18n.ts._2fa.securityKeyNotSupported }}
+ </MkInfo>
- <MkInfo v-else-if="webAuthnSupported() && !$i.twoFactorEnabled" warn>
- {{ i18n.ts._2fa.registerTOTPBeforeKey }}
- </MkInfo>
+ <MkInfo v-else-if="webAuthnSupported() && !$i.twoFactorEnabled" warn>
+ {{ i18n.ts._2fa.registerTOTPBeforeKey }}
+ </MkInfo>
- <template v-else>
- <MkButton primary @click="addSecurityKey">{{ i18n.ts._2fa.registerSecurityKey }}</MkButton>
- <MkFolder v-for="key in $i.securityKeysList" :key="key.id">
- <template #label>{{ key.name }}</template>
- <template #suffix><I18n :src="i18n.ts.lastUsedAt"><template #t><MkTime :time="key.lastUsed"/></template></I18n></template>
- <div class="_buttons">
- <MkButton @click="renameKey(key)"><i class="ti ti-forms"></i> {{ i18n.ts.rename }}</MkButton>
- <MkButton danger @click="unregisterKey(key)"><i class="ti ti-trash"></i> {{ i18n.ts.unregister }}</MkButton>
- </div>
- </MkFolder>
- </template>
- </div>
- </MkFolder>
+ <template v-else>
+ <MkButton primary @click="addSecurityKey">{{ i18n.ts._2fa.registerSecurityKey }}</MkButton>
+ <MkFolder v-for="key in $i.securityKeysList" :key="key.id">
+ <template #label>{{ key.name }}</template>
+ <template #suffix><I18n :src="i18n.ts.lastUsedAt"><template #t><MkTime :time="key.lastUsed"/></template></I18n></template>
+ <div class="_buttons">
+ <MkButton @click="renameKey(key)"><i class="ti ti-forms"></i> {{ i18n.ts.rename }}</MkButton>
+ <MkButton danger @click="unregisterKey(key)"><i class="ti ti-trash"></i> {{ i18n.ts.unregister }}</MkButton>
+ </div>
+ </MkFolder>
+ </template>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList.length === 0" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)">
- <template #label>{{ i18n.ts.passwordLessLogin }}</template>
- <template #caption>{{ i18n.ts.passwordLessLoginDescription }}</template>
- </MkSwitch>
- </div>
-</FormSection>
+ <SearchMarker :keywords="['password', 'less', 'key', 'passkey', 'login', 'signin']">
+ <MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList.length === 0" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)">
+ <template #label><SearchLabel>{{ i18n.ts.passwordLessLogin }}</SearchLabel></template>
+ <template #caption><SearchKeyword>{{ i18n.ts.passwordLessLoginDescription }}</SearchKeyword></template>
+ </MkSwitch>
+ </SearchMarker>
+ </div>
+ </FormSection>
+</SearchMarker>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/pages/settings/accessibility.vue b/packages/frontend/src/pages/settings/accessibility.vue
new file mode 100644
index 0000000000..b703be1fe1
--- /dev/null
+++ b/packages/frontend/src/pages/settings/accessibility.vue
@@ -0,0 +1,91 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<SearchMarker path="/settings/accessibility" :label="i18n.ts.accessibility" :keywords="['accessibility']" icon="ti ti-accessible">
+ <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>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif']">
+ <MkSwitch v-model="disableShowingAnimatedImages">
+ <template #label><SearchLabel>{{ i18n.ts.disableShowingAnimatedImages }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['mfm', 'enable', 'show', 'animated']">
+ <MkSwitch v-model="animatedMfm">
+ <template #label><SearchLabel>{{ i18n.ts.enableAnimatedMfm }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['swipe', 'horizontal', 'tab']">
+ <MkSwitch v-model="enableHorizontalSwipe">
+ <template #label><SearchLabel>{{ i18n.ts.enableHorizontalSwipe }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['keep', 'screen', 'display', 'on']">
+ <MkSwitch v-model="keepScreenOn">
+ <template #label><SearchLabel>{{ i18n.ts.keepScreenOn }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['native', 'system', 'video', 'audio', 'player', 'media']">
+ <MkSwitch v-model="useNativeUIForVideoAudioPlayer">
+ <template #label><SearchLabel>{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</SearchLabel></template>
+ </MkSwitch>
+ </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>
+ </SearchMarker>
+ </div>
+</SearchMarker>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from 'vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkSelect from '@/components/MkSelect.vue';
+import { defaultStore } from '@/store.js';
+import { reloadAsk } from '@/scripts/reload-ask.js';
+import { i18n } from '@/i18n.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+
+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'));
+
+watch([
+ keepScreenOn,
+ contextMenu,
+], async () => {
+ await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
+});
+
+const headerActions = computed(() => []);
+
+const headerTabs = computed(() => []);
+
+definePageMetadata(() => ({
+ title: i18n.ts.accessibility,
+ icon: 'ti ti-accessible',
+}));
+</script>
diff --git a/packages/frontend/src/pages/settings/appearance.vue b/packages/frontend/src/pages/settings/appearance.vue
new file mode 100644
index 0000000000..465c2a38c2
--- /dev/null
+++ b/packages/frontend/src/pages/settings/appearance.vue
@@ -0,0 +1,287 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<SearchMarker path="/settings/appearance" :label="i18n.ts.appearance" :keywords="['appearance']" icon="ti ti-device-desktop">
+ <div class="_gaps_m">
+ <FormSection first>
+ <div class="_gaps_m">
+ <div class="_gaps_s">
+ <SearchMarker :keywords="['blur']">
+ <MkSwitch v-model="useBlurEffect">
+ <template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['blur', 'modal']">
+ <MkSwitch v-model="useBlurEffectForModal">
+ <template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail']">
+ <MkSwitch v-model="highlightSensitiveMedia">
+ <template #label><SearchLabel>{{ i18n.ts.highlightSensitiveMedia }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['avatar', 'icon', 'square']">
+ <MkSwitch v-model="squareAvatars">
+ <template #label><SearchLabel>{{ i18n.ts.squareAvatars }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['avatar', 'icon', 'decoration', 'show']">
+ <MkSwitch v-model="showAvatarDecorations">
+ <template #label><SearchLabel>{{ i18n.ts.showAvatarDecorations }}</SearchLabel></template>
+ </MkSwitch>
+ </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>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['effect', 'show']">
+ <MkSwitch v-model="enableSeasonalScreenEffect">
+ <template #label><SearchLabel>{{ i18n.ts.seasonalScreenEffect }}</SearchLabel></template>
+ </MkSwitch>
+ </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>
+ </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>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['font', 'size']">
+ <MkRadios v-model="fontSize">
+ <template #label><SearchLabel>{{ i18n.ts.fontSize }}</SearchLabel></template>
+ <option :value="null"><span style="font-size: 14px;">Aa</span></option>
+ <option value="1"><span style="font-size: 15px;">Aa</span></option>
+ <option value="2"><span style="font-size: 16px;">Aa</span></option>
+ <option value="3"><span style="font-size: 17px;">Aa</span></option>
+ </MkRadios>
+ </SearchMarker>
+ </div>
+ </FormSection>
+
+ <SearchMarker :keywords="['note', 'display']">
+ <FormSection>
+ <template #label><SearchLabel>{{ i18n.ts.displayOfNote }}</SearchLabel></template>
+
+ <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>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['reaction', 'size', 'scale', 'display', 'width', 'limit']">
+ <MkSwitch v-model="limitWidthOfReaction">
+ <template #label><SearchLabel>{{ i18n.ts.limitWidthOfReaction }}</SearchLabel></template>
+ </MkSwitch>
+ </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>
+ </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>
+ </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>
+ </SearchMarker>
+ </div>
+ </FormSection>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['notification', 'display']">
+ <FormSection>
+ <template #label><SearchLabel>{{ i18n.ts.notificationDisplay }}</SearchLabel></template>
+
+ <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>
+ </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>
+ </SearchMarker>
+
+ <MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton>
+ </div>
+ </FormSection>
+ </SearchMarker>
+
+ <FormSection>
+ <FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
+ </FormSection>
+ </div>
+</SearchMarker>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from 'vue';
+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 { 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 FormLink from '@/components/form/link.vue';
+import { globalEvents } from '@/events.js';
+import { claimAchievement } from '@/scripts/achievements.js';
+import MkButton from '@/components/MkButton.vue';
+import FormSection from '@/components/form/section.vue';
+import { instance } from '@/instance.js';
+
+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'));
+
+watch(fontSize, () => {
+ if (fontSize.value == null) {
+ miLocalStorage.removeItem('fontSize');
+ } else {
+ miLocalStorage.setItem('fontSize', fontSize.value);
+ }
+});
+
+watch(useSystemFont, () => {
+ if (useSystemFont.value) {
+ miLocalStorage.setItem('useSystemFont', 't');
+ } else {
+ miLocalStorage.removeItem('useSystemFont');
+ }
+});
+
+watch([
+ fontSize,
+ useSystemFont,
+ squareAvatars,
+ highlightSensitiveMedia,
+ enableSeasonalScreenEffect,
+ showGapBetweenNotesInTimeline,
+ mediaListWithOneImageAppearance,
+ reactionsDisplaySize,
+ limitWidthOfReaction,
+ mediaListWithOneImageAppearance,
+ reactionsDisplaySize,
+ limitWidthOfReaction,
+ instanceTicker,
+], async () => {
+ await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
+});
+
+let smashCount = 0;
+let smashTimer: number | null = null;
+
+function testNotification(): void {
+ const notification: Misskey.entities.Notification = {
+ id: Math.random().toString(),
+ createdAt: new Date().toUTCString(),
+ isRead: false,
+ type: 'test',
+ };
+
+ globalEvents.emit('clientNotification', notification);
+
+ // セルフ通知破壊 実績関連
+ smashCount++;
+ if (smashCount >= 10) {
+ claimAchievement('smashTestNotificationButton');
+ smashCount = 0;
+ }
+ if (smashTimer) {
+ clearTimeout(smashTimer);
+ }
+ smashTimer = window.setTimeout(() => {
+ smashCount = 0;
+ }, 300);
+}
+
+const headerActions = computed(() => []);
+
+const headerTabs = computed(() => []);
+
+definePageMetadata(() => ({
+ title: i18n.ts.appearance,
+ icon: 'ti ti-device-desktop',
+}));
+</script>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue
index 9fca306f9f..79be2b9b1e 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.vue
@@ -4,44 +4,46 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div>
- <div v-if="!loading" class="_gaps">
- <MkInfo>{{ i18n.tsx._profile.avatarDecorationMax({ max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.tsx.remainingN({ n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo>
+<SearchMarker path="/settings/avatar-decoration" :label="i18n.ts.avatarDecorations" :keywords="['avatar', 'icon', 'decoration']" icon="ti ti-sparkles">
+ <div>
+ <div v-if="!loading" class="_gaps">
+ <MkInfo>{{ i18n.tsx._profile.avatarDecorationMax({ max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.tsx.remainingN({ n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo>
- <MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration/>
+ <MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration/>
- <div v-if="$i.avatarDecorations.length > 0" v-panel :class="$style.current" class="_gaps_s">
- <div>{{ i18n.ts.inUse }}</div>
+ <div v-if="$i.avatarDecorations.length > 0" v-panel :class="$style.current" class="_gaps_s">
+ <div>{{ i18n.ts.inUse }}</div>
+
+ <div :class="$style.decorations">
+ <XDecoration
+ v-for="(avatarDecoration, i) in $i.avatarDecorations"
+ :decoration="avatarDecorations.find(d => d.id === avatarDecoration.id)"
+ :angle="avatarDecoration.angle"
+ :flipH="avatarDecoration.flipH"
+ :offsetX="avatarDecoration.offsetX"
+ :offsetY="avatarDecoration.offsetY"
+ :active="true"
+ @click="openDecoration(avatarDecoration, i)"
+ />
+ </div>
+
+ <MkButton danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton>
+ </div>
<div :class="$style.decorations">
<XDecoration
- v-for="(avatarDecoration, i) in $i.avatarDecorations"
- :decoration="avatarDecorations.find(d => d.id === avatarDecoration.id)"
- :angle="avatarDecoration.angle"
- :flipH="avatarDecoration.flipH"
- :offsetX="avatarDecoration.offsetX"
- :offsetY="avatarDecoration.offsetY"
- :active="true"
- @click="openDecoration(avatarDecoration, i)"
+ v-for="avatarDecoration in avatarDecorations"
+ :key="avatarDecoration.id"
+ :decoration="avatarDecoration"
+ @click="openDecoration(avatarDecoration)"
/>
</div>
-
- <MkButton danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton>
</div>
-
- <div :class="$style.decorations">
- <XDecoration
- v-for="avatarDecoration in avatarDecorations"
- :key="avatarDecoration.id"
- :decoration="avatarDecoration"
- @click="openDecoration(avatarDecoration)"
- />
+ <div v-else>
+ <MkLoading/>
</div>
</div>
- <div v-else>
- <MkLoading/>
- </div>
-</div>
+</SearchMarker>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 0e66b93f1c..0138aac1c5 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -4,60 +4,81 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="_gaps_m">
- <FormSection v-if="!fetching" first>
- <template #label>{{ i18n.ts.usageAmount }}</template>
+<SearchMarker path="/settings/drive" :label="i18n.ts.drive" :keywords="['drive']" icon="ti ti-cloud">
+ <div class="_gaps_m">
+ <SearchMarker :keywords="['capacity', 'usage']">
+ <FormSection first>
+ <template #label><SearchLabel>{{ i18n.ts.usageAmount }}</SearchLabel></template>
- <div class="_gaps_m">
- <div>
- <div :class="$style.meter"><div :class="$style.meterValue" :style="meterStyle"></div></div>
- </div>
- <FormSplit>
- <MkKeyValue>
- <template #key>{{ i18n.ts.capacity }}</template>
- <template #value>{{ bytes(capacity, 1) }}</template>
- </MkKeyValue>
- <MkKeyValue>
- <template #key>{{ i18n.ts.inUse }}</template>
- <template #value>{{ bytes(usage, 1) }}</template>
- </MkKeyValue>
- </FormSplit>
- </div>
- </FormSection>
+ <div v-if="!fetching" class="_gaps_m">
+ <div>
+ <div :class="$style.meter"><div :class="$style.meterValue" :style="meterStyle"></div></div>
+ </div>
+ <FormSplit>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.capacity }}</template>
+ <template #value>{{ bytes(capacity, 1) }}</template>
+ </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.inUse }}</template>
+ <template #value>{{ bytes(usage, 1) }}</template>
+ </MkKeyValue>
+ </FormSplit>
+ </div>
+ </FormSection>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['statistics', 'usage']">
+ <FormSection>
+ <template #label><SearchLabel>{{ i18n.ts.statistics }}</SearchLabel></template>
+ <MkChart src="per-user-drive" :args="{ user: $i }" span="day" :limit="7 * 5" :bar="true" :stacked="true" :detailed="false" :aspectRatio="6"/>
+ </FormSection>
+ </SearchMarker>
+
+ <FormSection>
+ <div class="_gaps_m">
+ <SearchMarker :keywords="['default', 'upload', 'folder']">
+ <FormLink @click="chooseUploadFolder()">
+ <SearchLabel>{{ i18n.ts.uploadFolder }}</SearchLabel>
+ <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
+ <template #suffixIcon><i class="ti ti-folder"></i></template>
+ </FormLink>
+ </SearchMarker>
+
+ <FormLink to="/settings/drive/cleaner">
+ {{ i18n.ts.drivecleaner }}
+ </FormLink>
- <FormSection>
- <template #label>{{ i18n.ts.statistics }}</template>
- <MkChart src="per-user-drive" :args="{ user: $i }" span="day" :limit="7 * 5" :bar="true" :stacked="true" :detailed="false" :aspectRatio="6"/>
- </FormSection>
+ <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>
+ </SearchMarker>
- <FormSection>
- <div class="_gaps_m">
- <FormLink @click="chooseUploadFolder()">
- {{ i18n.ts.uploadFolder }}
- <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
- <template #suffixIcon><i class="ti ti-folder"></i></template>
- </FormLink>
- <FormLink to="/settings/drive/cleaner">
- {{ i18n.ts.drivecleaner }}
- </FormLink>
- <MkSwitch v-model="keepOriginalUploading">
- <template #label>{{ i18n.ts.keepOriginalUploading }}</template>
- <template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
- </MkSwitch>
- <MkSwitch v-model="keepOriginalFilename">
- <template #label>{{ i18n.ts.keepOriginalFilename }}</template>
- <template #caption>{{ i18n.ts.keepOriginalFilenameDescription }}</template>
- </MkSwitch>
- <MkSwitch v-model="alwaysMarkNsfw" @update:modelValue="saveProfile()">
- <template #label>{{ i18n.ts.alwaysMarkSensitive }}</template>
- </MkSwitch>
- <MkSwitch v-model="autoSensitive" @update:modelValue="saveProfile()">
- <template #label>{{ i18n.ts.enableAutoSensitive }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
- <template #caption>{{ i18n.ts.enableAutoSensitiveDescription }}</template>
- </MkSwitch>
- </div>
- </FormSection>
-</div>
+ <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>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file']">
+ <MkSwitch v-model="alwaysMarkNsfw" @update:modelValue="saveProfile()">
+ <template #label><SearchLabel>{{ i18n.ts.alwaysMarkSensitive }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['auto', 'nsfw', 'sensitive', 'media', 'file']">
+ <MkSwitch v-model="autoSensitive" @update:modelValue="saveProfile()">
+ <template #label><SearchLabel>{{ i18n.ts.enableAutoSensitive }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <template #caption><SearchKeyword>{{ i18n.ts.enableAutoSensitiveDescription }}</SearchKeyword></template>
+ </MkSwitch>
+ </SearchMarker>
+ </div>
+ </FormSection>
+ </div>
+</SearchMarker>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index d452f249b6..e7a8fc5634 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -4,47 +4,58 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div v-if="instance.enableEmail" class="_gaps_m">
- <FormSection first>
- <template #label>{{ i18n.ts.emailAddress }}</template>
- <MkInput v-model="emailAddress" type="email" manualSave>
- <template #prefix><i class="ti ti-mail"></i></template>
- <template v-if="$i.email && !$i.emailVerified" #caption>{{ i18n.ts.verificationEmailSent }}</template>
- <template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i> {{ i18n.ts.emailVerified }}</template>
- </MkInput>
- </FormSection>
+<SearchMarker path="/settings/email" :label="i18n.ts.email" :keywords="['email']" icon="ti ti-mail">
+ <div class="_gaps_m">
+ <MkInfo v-if="!instance.enableEmail">{{ i18n.ts.emailNotSupported }}</MkInfo>
- <FormSection>
- <MkSwitch :modelValue="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail">
- {{ i18n.ts.receiveAnnouncementFromInstance }}
- </MkSwitch>
- </FormSection>
+ <MkDisableSection :disabled="!instance.enableEmail">
+ <div class="_gaps_m">
+ <SearchMarker :keywords="['email', 'address']">
+ <FormSection first>
+ <template #label><SearchLabel>{{ i18n.ts.emailAddress }}</SearchLabel></template>
+ <MkInput v-model="emailAddress" type="email" manualSave>
+ <template #prefix><i class="ti ti-mail"></i></template>
+ <template v-if="$i.email && !$i.emailVerified" #caption>{{ i18n.ts.verificationEmailSent }}</template>
+ <template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i> {{ i18n.ts.emailVerified }}</template>
+ </MkInput>
+ </FormSection>
+ </SearchMarker>
- <FormSection>
- <template #label>{{ i18n.ts.emailNotification }}</template>
+ <FormSection>
+ <SearchMarker :keywords="['announcement', 'email']">
+ <MkSwitch :modelValue="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail">
+ <template #label><SearchLabel>{{ i18n.ts.receiveAnnouncementFromInstance }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+ </FormSection>
- <div class="_gaps_s">
- <MkSwitch v-model="emailNotification_mention">
- {{ i18n.ts._notification._types.mention }}
- </MkSwitch>
- <MkSwitch v-model="emailNotification_reply">
- {{ i18n.ts._notification._types.reply }}
- </MkSwitch>
- <MkSwitch v-model="emailNotification_quote">
- {{ i18n.ts._notification._types.quote }}
- </MkSwitch>
- <MkSwitch v-model="emailNotification_follow">
- {{ i18n.ts._notification._types.follow }}
- </MkSwitch>
- <MkSwitch v-model="emailNotification_receiveFollowRequest">
- {{ i18n.ts._notification._types.receiveFollowRequest }}
- </MkSwitch>
- </div>
- </FormSection>
-</div>
-<div v-if="!instance.enableEmail" class="_gaps_m">
- <MkInfo>{{ i18n.ts.emailNotSupported }}</MkInfo>
-</div>
+ <SearchMarker :keywords="['notification', 'email']">
+ <FormSection>
+ <template #label><SearchLabel>{{ i18n.ts.emailNotification }}</SearchLabel></template>
+
+ <div class="_gaps_s">
+ <MkSwitch v-model="emailNotification_mention">
+ {{ i18n.ts._notification._types.mention }}
+ </MkSwitch>
+ <MkSwitch v-model="emailNotification_reply">
+ {{ i18n.ts._notification._types.reply }}
+ </MkSwitch>
+ <MkSwitch v-model="emailNotification_quote">
+ {{ i18n.ts._notification._types.quote }}
+ </MkSwitch>
+ <MkSwitch v-model="emailNotification_follow">
+ {{ i18n.ts._notification._types.follow }}
+ </MkSwitch>
+ <MkSwitch v-model="emailNotification_receiveFollowRequest">
+ {{ i18n.ts._notification._types.receiveFollowRequest }}
+ </MkSwitch>
+ </div>
+ </FormSection>
+ </SearchMarker>
+ </div>
+ </MkDisableSection>
+ </div>
+</SearchMarker>
</template>
<script lang="ts" setup>
@@ -53,6 +64,7 @@ import FormSection from '@/components/form/section.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
+import MkDisableSection from '@/components/MkDisableSection.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { signinRequired } from '@/account.js';
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
deleted file mode 100644
index 4449d6169f..0000000000
--- a/packages/frontend/src/pages/settings/general.vue
+++ /dev/null
@@ -1,492 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and misskey-project
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<div class="_gaps_m">
- <MkSelect v-model="lang">
- <template #label>{{ i18n.ts.uiLanguage }}</template>
- <option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
- <template #caption>
- <I18n :src="i18n.ts.i18nInfo" tag="span">
- <template #link>
- <MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
- </template>
- </I18n>
- </template>
- </MkSelect>
-
- <MkRadios v-model="overridedDeviceKind">
- <template #label>{{ i18n.ts.overridedDeviceKind }}</template>
- <option :value="null">{{ i18n.ts.auto }}</option>
- <option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option>
- <option value="tablet"><i class="ti ti-device-tablet"/> {{ i18n.ts.tablet }}</option>
- <option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option>
- </MkRadios>
-
- <FormSection>
- <div class="_gaps_s">
- <MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
- <MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
- <MkFolder>
- <template #label>{{ i18n.ts.pinnedList }}</template>
- <!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
- <MkButton v-if="defaultStore.reactiveState.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>
- </div>
- </FormSection>
-
- <FormSection>
- <template #label>{{ i18n.ts.displayOfNote }}</template>
-
- <div class="_gaps_m">
- <div class="_gaps_s">
- <MkSwitch v-model="collapseRenotes">
- <template #label>{{ i18n.ts.collapseRenotes }}</template>
- <template #caption>{{ i18n.ts.collapseRenotesDescription }}</template>
- </MkSwitch>
- <MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch>
- <MkSwitch v-model="showClipButtonInNoteFooter">{{ i18n.ts.showClipButtonInNoteFooter }}</MkSwitch>
- <MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
- <MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
- <MkSwitch v-if="advancedMfm" v-model="enableQuickAddMfmFunction">{{ i18n.ts.enableQuickAddMfmFunction }}</MkSwitch>
- <MkSwitch v-model="showReactionsCount">{{ i18n.ts.showReactionsCount }}</MkSwitch>
- <MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch>
- <MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch>
- <MkRadios v-model="reactionsDisplaySize">
- <template #label>{{ i18n.ts.reactionsDisplaySize }}</template>
- <option value="small">{{ i18n.ts.small }}</option>
- <option value="medium">{{ i18n.ts.medium }}</option>
- <option value="large">{{ i18n.ts.large }}</option>
- </MkRadios>
- <MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch>
- </div>
-
- <MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker">
- <template #label>{{ i18n.ts.instanceTicker }}</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>
-
- <MkSelect v-model="nsfw">
- <template #label>{{ i18n.ts.displayOfSensitiveMedia }}</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>
-
- <MkRadios v-model="mediaListWithOneImageAppearance">
- <template #label>{{ i18n.ts.mediaListWithOneImageAppearance }}</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>
- </div>
- </FormSection>
-
- <FormSection>
- <template #label>{{ i18n.ts.notificationDisplay }}</template>
-
- <div class="_gaps_m">
- <MkSwitch v-model="useGroupedNotifications">{{ i18n.ts.useGroupedNotifications }}</MkSwitch>
-
- <MkRadios v-model="notificationPosition">
- <template #label>{{ i18n.ts.position }}</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>
-
- <MkRadios v-model="notificationStackAxis">
- <template #label>{{ i18n.ts.stackAxis }}</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>
-
- <MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton>
- </div>
- </FormSection>
-
- <FormSection>
- <template #label>{{ i18n.ts.appearance }}</template>
-
- <div class="_gaps_m">
- <div class="_gaps_s">
- <MkSwitch v-model="reduceAnimation">{{ i18n.ts.reduceUiAnimation }}</MkSwitch>
- <MkSwitch v-model="useBlurEffect">{{ i18n.ts.useBlurEffect }}</MkSwitch>
- <MkSwitch v-model="useBlurEffectForModal">{{ i18n.ts.useBlurEffectForModal }}</MkSwitch>
- <MkSwitch v-model="disableShowingAnimatedImages">{{ i18n.ts.disableShowingAnimatedImages }}</MkSwitch>
- <MkSwitch v-model="highlightSensitiveMedia">{{ i18n.ts.highlightSensitiveMedia }}</MkSwitch>
- <MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch>
- <MkSwitch v-model="showAvatarDecorations">{{ i18n.ts.showAvatarDecorations }}</MkSwitch>
- <MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
- <MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
- <MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch>
- <MkSwitch v-model="useNativeUIForVideoAudioPlayer">{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</MkSwitch>
- </div>
-
- <MkSelect v-model="menuStyle">
- <template #label>{{ i18n.ts.menuStyle }}</template>
- <option value="auto">{{ i18n.ts.auto }}</option>
- <option value="popup">{{ i18n.ts.popup }}</option>
- <option value="drawer">{{ i18n.ts.drawer }}</option>
- </MkSelect>
-
- <div>
- <MkRadios v-model="emojiStyle">
- <template #label>{{ i18n.ts.emojiStyle }}</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>
-
- <MkRadios v-model="fontSize">
- <template #label>{{ i18n.ts.fontSize }}</template>
- <option :value="null"><span style="font-size: 14px;">Aa</span></option>
- <option value="1"><span style="font-size: 15px;">Aa</span></option>
- <option value="2"><span style="font-size: 16px;">Aa</span></option>
- <option value="3"><span style="font-size: 17px;">Aa</span></option>
- </MkRadios>
- </div>
- </FormSection>
-
- <FormSection>
- <template #label>{{ i18n.ts.behavior }}</template>
-
- <div class="_gaps_m">
- <div class="_gaps_s">
- <MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
- <MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch>
- <MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
- <MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
- <MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
- <MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
- <MkSwitch v-model="alwaysConfirmFollow">{{ i18n.ts.alwaysConfirmFollow }}</MkSwitch>
- <MkSwitch v-model="confirmWhenRevealingSensitiveMedia">{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</MkSwitch>
- <MkSwitch v-model="confirmOnReact">{{ i18n.ts.confirmOnReact }}</MkSwitch>
- </div>
- <MkSelect v-model="serverDisconnectedBehavior">
- <template #label>{{ i18n.ts.whenServerDisconnected }}</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>
- <MkSelect v-model="contextMenu">
- <template #label>{{ i18n.ts._contextMenu.title }}</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>
- <MkRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing>
- <template #label>{{ i18n.ts.numberOfPageCache }}</template>
- <template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
- </MkRange>
-
- <MkFolder>
- <template #label>{{ i18n.ts.dataSaver }}</template>
-
- <div class="_gaps_m">
- <MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
-
- <div class="_buttons">
- <MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton>
- <MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton>
- </div>
- <div class="_gaps_m">
- <MkSwitch v-model="dataSaver.media">
- {{ i18n.ts._dataSaver._media.title }}
- <template #caption>{{ i18n.ts._dataSaver._media.description }}</template>
- </MkSwitch>
- <MkSwitch v-model="dataSaver.avatar">
- {{ i18n.ts._dataSaver._avatar.title }}
- <template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template>
- </MkSwitch>
- <MkSwitch v-model="dataSaver.urlPreview">
- {{ i18n.ts._dataSaver._urlPreview.title }}
- <template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template>
- </MkSwitch>
- <MkSwitch v-model="dataSaver.code">
- {{ i18n.ts._dataSaver._code.title }}
- <template #caption>{{ i18n.ts._dataSaver._code.description }}</template>
- </MkSwitch>
- </div>
- </div>
- </MkFolder>
- </div>
- </FormSection>
-
- <FormSection>
- <template #label>{{ i18n.ts.other }}</template>
-
- <div class="_gaps">
- <MkRadios v-model="hemisphere">
- <template #label>{{ i18n.ts.hemisphere }}</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>
- <MkFolder>
- <template #label>{{ i18n.ts.additionalEmojiDictionary }}</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>
- </template>
- </div>
- </MkFolder>
- <FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
- <FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
- </div>
- </FormSection>
-</div>
-</template>
-
-<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';
-import MkRadios from '@/components/MkRadios.vue';
-import MkRange from '@/components/MkRange.vue';
-import MkFolder from '@/components/MkFolder.vue';
-import MkButton from '@/components/MkButton.vue';
-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 * 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 { globalEvents } from '@/events.js';
-import { claimAchievement } from '@/scripts/achievements.js';
-
-const lang = ref(miLocalStorage.getItem('lang'));
-const fontSize = ref(miLocalStorage.getItem('fontSize'));
-const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
-const dataSaver = ref(defaultStore.state.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 reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
-const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction'));
-const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
-const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
-const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
-const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
-const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'));
-const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm'));
-const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
-const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount'));
-const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction'));
-const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
-const menuStyle = computed(defaultStore.makeGetterSetter('menuStyle'));
-const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
-const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
-const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
-const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
-const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
-const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
-const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
-const showFixedPostFormInChannel = computed(defaultStore.makeGetterSetter('showFixedPostFormInChannel'));
-const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache'));
-const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'));
-const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));
-const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu'));
-const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
-const showAvatarDecorations = computed(defaultStore.makeGetterSetter('showAvatarDecorations'));
-const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance'));
-const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
-const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
-const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
-const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
-const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
-const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect'));
-const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe'));
-const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer'));
-const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
-const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia'));
-const confirmOnReact = computed(defaultStore.makeGetterSetter('confirmOnReact'));
-const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu'));
-
-watch(lang, () => {
- miLocalStorage.setItem('lang', lang.value as string);
- miLocalStorage.removeItem('locale');
- miLocalStorage.removeItem('localeVersion');
-});
-
-watch(fontSize, () => {
- if (fontSize.value == null) {
- miLocalStorage.removeItem('fontSize');
- } else {
- miLocalStorage.setItem('fontSize', fontSize.value);
- }
-});
-
-watch(useSystemFont, () => {
- if (useSystemFont.value) {
- miLocalStorage.setItem('useSystemFont', 't');
- } else {
- miLocalStorage.removeItem('useSystemFont');
- }
-});
-
-watch([
- hemisphere,
- lang,
- fontSize,
- useSystemFont,
- enableInfiniteScroll,
- squareAvatars,
- showNoteActionsOnlyHover,
- showGapBetweenNotesInTimeline,
- instanceTicker,
- overridedDeviceKind,
- mediaListWithOneImageAppearance,
- reactionsDisplaySize,
- limitWidthOfReaction,
- highlightSensitiveMedia,
- keepScreenOn,
- disableStreamingTimeline,
- enableSeasonalScreenEffect,
- alwaysConfirmFollow,
- confirmWhenRevealingSensitiveMedia,
- contextMenu,
-], async () => {
- await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
-});
-
-const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const;
-
-function getEmojiIndexLangName(targetLang: typeof emojiIndexLangs[number]) {
- if (langs.find(x => x[0] === targetLang)) {
- return langs.find(x => x[0] === targetLang)![1];
- } else {
- // 絵文字辞書限定の言語定義
- switch (targetLang) {
- case 'ja-JP_hira': return 'ひらがな';
- default: return targetLang;
- }
- }
-}
-
-function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
- async function main() {
- const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
-
- function download() {
- switch (lang) {
- case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default);
- case 'ja-JP': return import('../../unicode-emoji-indexes/ja-JP.json').then(x => x.default);
- case 'ja-JP_hira': return import('../../unicode-emoji-indexes/ja-JP_hira.json').then(x => x.default);
- default: throw new Error('unrecognized lang: ' + lang);
- }
- }
-
- currentIndexes[lang] = await download();
- await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
- }
-
- os.promiseDialog(main());
-}
-
-function removeEmojiIndex(lang: string) {
- async function main() {
- const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
- delete currentIndexes[lang];
- await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
- }
-
- os.promiseDialog(main());
-}
-
-async function setPinnedList() {
- const lists = await misskeyApi('users/lists/list');
- const { canceled, result: list } = await os.select({
- title: i18n.ts.selectList,
- items: lists.map(x => ({
- value: x, text: x.name,
- })),
- });
- if (canceled) return;
-
- defaultStore.set('pinnedUserLists', [list]);
-}
-
-function removePinnedList() {
- defaultStore.set('pinnedUserLists', []);
-}
-
-let smashCount = 0;
-let smashTimer: number | null = null;
-
-function testNotification(): void {
- const notification: Misskey.entities.Notification = {
- id: Math.random().toString(),
- createdAt: new Date().toUTCString(),
- isRead: false,
- type: 'test',
- };
-
- globalEvents.emit('clientNotification', notification);
-
- // セルフ通知破壊 実績関連
- smashCount++;
- if (smashCount >= 10) {
- claimAchievement('smashTestNotificationButton');
- smashCount = 0;
- }
- if (smashTimer) {
- clearTimeout(smashTimer);
- }
- smashTimer = window.setTimeout(() => {
- smashCount = 0;
- }, 300);
-}
-
-function enableAllDataSaver() {
- const g = { ...defaultStore.state.dataSaver };
-
- Object.keys(g).forEach((key) => { g[key] = true; });
-
- dataSaver.value = g;
-}
-
-function disableAllDataSaver() {
- const g = { ...defaultStore.state.dataSaver };
-
- Object.keys(g).forEach((key) => { g[key] = false; });
-
- dataSaver.value = g;
-}
-
-watch(dataSaver, (to) => {
- defaultStore.set('dataSaver', to);
-}, {
- deep: true,
-});
-
-const headerActions = computed(() => []);
-
-const headerTabs = computed(() => []);
-
-definePageMetadata(() => ({
- title: i18n.ts.general,
- icon: 'ti ti-adjustments',
-}));
-</script>
diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue
index 5acbc50756..6b67a9a1a8 100644
--- a/packages/frontend/src/pages/settings/import-export.vue
+++ b/packages/frontend/src/pages/settings/import-export.vue
@@ -4,118 +4,143 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="_gaps_m">
- <FormSection first>
- <template #label><i class="ti ti-pencil"></i> {{ i18n.ts._exportOrImport.allNotes }}</template>
- <MkFolder>
- <template #label>{{ i18n.ts.export }}</template>
- <template #icon><i class="ti ti-download"></i></template>
- <MkButton primary :class="$style.button" inline @click="exportNotes()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
- </MkFolder>
- </FormSection>
- <FormSection>
- <template #label><i class="ti ti-star"></i> {{ i18n.ts._exportOrImport.favoritedNotes }}</template>
- <MkFolder>
- <template #label>{{ i18n.ts.export }}</template>
- <template #icon><i class="ti ti-download"></i></template>
- <MkButton primary :class="$style.button" inline @click="exportFavorites()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
- </MkFolder>
- </FormSection>
- <FormSection>
- <template #label><i class="ti ti-star"></i> {{ i18n.ts._exportOrImport.clips }}</template>
- <MkFolder>
- <template #label>{{ i18n.ts.export }}</template>
- <template #icon><i class="ti ti-download"></i></template>
- <MkButton primary :class="$style.button" inline @click="exportClips()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
- </MkFolder>
- </FormSection>
- <FormSection>
- <template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.followingList }}</template>
- <div class="_gaps_s">
- <MkFolder>
- <template #label>{{ i18n.ts.export }}</template>
- <template #icon><i class="ti ti-download"></i></template>
+<SearchMarker path="/settings/import-export" :label="i18n.ts.importAndExport" :keywords="['import', 'export', 'data']" icon="ti ti-package">
+ <div class="_gaps_m">
+ <SearchMarker :keywords="['notes']">
+ <FormSection first>
+ <template #label><i class="ti ti-pencil"></i> <SearchLabel>{{ i18n.ts._exportOrImport.allNotes }}</SearchLabel></template>
+ <MkFolder>
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="ti ti-download"></i></template>
+ <MkButton primary :class="$style.button" inline @click="exportNotes()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
+ </MkFolder>
+ </FormSection>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['favorite', 'notes']">
+ <FormSection>
+ <template #label><i class="ti ti-star"></i> <SearchLabel>{{ i18n.ts._exportOrImport.favoritedNotes }}</SearchLabel></template>
+ <MkFolder>
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="ti ti-download"></i></template>
+ <MkButton primary :class="$style.button" inline @click="exportFavorites()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
+ </MkFolder>
+ </FormSection>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['clip', 'notes']">
+ <FormSection>
+ <template #label><i class="ti ti-star"></i> <SearchLabel>{{ i18n.ts._exportOrImport.clips }}</SearchLabel></template>
+ <MkFolder>
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="ti ti-download"></i></template>
+ <MkButton primary :class="$style.button" inline @click="exportClips()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
+ </MkFolder>
+ </FormSection>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['following', 'users']">
+ <FormSection>
+ <template #label><i class="ti ti-users"></i> <SearchLabel>{{ i18n.ts._exportOrImport.followingList }}</SearchLabel></template>
+ <div class="_gaps_s">
+ <MkFolder>
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="ti ti-download"></i></template>
+ <div class="_gaps_s">
+ <MkSwitch v-model="excludeMutingUsers">
+ {{ i18n.ts._exportOrImport.excludeMutingUsers }}
+ </MkSwitch>
+ <MkSwitch v-model="excludeInactiveUsers">
+ {{ i18n.ts._exportOrImport.excludeInactiveUsers }}
+ </MkSwitch>
+ <MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
+ </div>
+ </MkFolder>
+ <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportFollowing">
+ <template #label>{{ i18n.ts.import }}</template>
+ <template #icon><i class="ti ti-upload"></i></template>
+ <MkSwitch v-model="withReplies">
+ {{ i18n.ts._exportOrImport.withReplies }}
+ </MkSwitch>
+ <MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
+ </MkFolder>
+ </div>
+ </FormSection>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['user', 'lists']">
+ <FormSection>
+ <template #label><i class="ti ti-users"></i> <SearchLabel>{{ i18n.ts._exportOrImport.userLists }}</SearchLabel></template>
+ <div class="_gaps_s">
+ <MkFolder>
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="ti ti-download"></i></template>
+ <MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
+ </MkFolder>
+ <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportUserLists">
+ <template #label>{{ i18n.ts.import }}</template>
+ <template #icon><i class="ti ti-upload"></i></template>
+ <MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
+ </MkFolder>
+ </div>
+ </FormSection>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['mute', 'users']">
+ <FormSection>
+ <template #label><i class="ti ti-user-off"></i> <SearchLabel>{{ i18n.ts._exportOrImport.muteList }}</SearchLabel></template>
+ <div class="_gaps_s">
+ <MkFolder>
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="ti ti-download"></i></template>
+ <MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
+ </MkFolder>
+ <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportMuting">
+ <template #label>{{ i18n.ts.import }}</template>
+ <template #icon><i class="ti ti-upload"></i></template>
+ <MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
+ </MkFolder>
+ </div>
+ </FormSection>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['block', 'users']">
+ <FormSection>
+ <template #label><i class="ti ti-user-off"></i> <SearchLabel>{{ i18n.ts._exportOrImport.blockingList }}</SearchLabel></template>
+ <div class="_gaps_s">
+ <MkFolder>
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="ti ti-download"></i></template>
+ <MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
+ </MkFolder>
+ <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportBlocking">
+ <template #label>{{ i18n.ts.import }}</template>
+ <template #icon><i class="ti ti-upload"></i></template>
+ <MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
+ </MkFolder>
+ </div>
+ </FormSection>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['antennas']">
+ <FormSection>
+ <template #label><i class="ti ti-antenna"></i> <SearchLabel>{{ i18n.ts.antennas }}</SearchLabel></template>
<div class="_gaps_s">
- <MkSwitch v-model="excludeMutingUsers">
- {{ i18n.ts._exportOrImport.excludeMutingUsers }}
- </MkSwitch>
- <MkSwitch v-model="excludeInactiveUsers">
- {{ i18n.ts._exportOrImport.excludeInactiveUsers }}
- </MkSwitch>
- <MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
+ <MkFolder>
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="ti ti-download"></i></template>
+ <MkButton primary :class="$style.button" inline @click="exportAntennas()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
+ </MkFolder>
+ <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportAntennas">
+ <template #label>{{ i18n.ts.import }}</template>
+ <template #icon><i class="ti ti-upload"></i></template>
+ <MkButton primary :class="$style.button" inline @click="importAntennas($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
+ </MkFolder>
</div>
- </MkFolder>
- <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportFollowing">
- <template #label>{{ i18n.ts.import }}</template>
- <template #icon><i class="ti ti-upload"></i></template>
- <MkSwitch v-model="withReplies">
- {{ i18n.ts._exportOrImport.withReplies }}
- </MkSwitch>
- <MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
- </MkFolder>
- </div>
- </FormSection>
- <FormSection>
- <template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.userLists }}</template>
- <div class="_gaps_s">
- <MkFolder>
- <template #label>{{ i18n.ts.export }}</template>
- <template #icon><i class="ti ti-download"></i></template>
- <MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
- </MkFolder>
- <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportUserLists">
- <template #label>{{ i18n.ts.import }}</template>
- <template #icon><i class="ti ti-upload"></i></template>
- <MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
- </MkFolder>
- </div>
- </FormSection>
- <FormSection>
- <template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.muteList }}</template>
- <div class="_gaps_s">
- <MkFolder>
- <template #label>{{ i18n.ts.export }}</template>
- <template #icon><i class="ti ti-download"></i></template>
- <MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
- </MkFolder>
- <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportMuting">
- <template #label>{{ i18n.ts.import }}</template>
- <template #icon><i class="ti ti-upload"></i></template>
- <MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
- </MkFolder>
- </div>
- </FormSection>
- <FormSection>
- <template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.blockingList }}</template>
- <div class="_gaps_s">
- <MkFolder>
- <template #label>{{ i18n.ts.export }}</template>
- <template #icon><i class="ti ti-download"></i></template>
- <MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
- </MkFolder>
- <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportBlocking">
- <template #label>{{ i18n.ts.import }}</template>
- <template #icon><i class="ti ti-upload"></i></template>
- <MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
- </MkFolder>
- </div>
- </FormSection>
- <FormSection>
- <template #label><i class="ti ti-antenna"></i> {{ i18n.ts.antennas }}</template>
- <div class="_gaps_s">
- <MkFolder>
- <template #label>{{ i18n.ts.export }}</template>
- <template #icon><i class="ti ti-download"></i></template>
- <MkButton primary :class="$style.button" inline @click="exportAntennas()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
- </MkFolder>
- <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportAntennas">
- <template #label>{{ i18n.ts.import }}</template>
- <template #icon><i class="ti ti-upload"></i></template>
- <MkButton primary :class="$style.button" inline @click="importAntennas($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
- </MkFolder>
- </div>
- </FormSection>
-</div>
+ </FormSection>
+ </SearchMarker>
+ </div>
+</SearchMarker>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index bc6d6d0261..458605d545 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -12,7 +12,7 @@ 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>
- <MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
+ <MkSuperMenu :def="menuDef" :grid="narrow" :searchIndex="SETTING_INDEX"></MkSuperMenu>
</div>
</div>
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
@@ -29,6 +29,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { computed, onActivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
+import type { PageMetadata } from '@/scripts/page-metadata.js';
+import type { SuperMenuDef } from '@/components/MkSuperMenu.vue';
import { i18n } from '@/i18n.js';
import MkInfo from '@/components/MkInfo.vue';
import MkSuperMenu from '@/components/MkSuperMenu.vue';
@@ -38,8 +40,9 @@ import { instance } from '@/instance.js';
import { definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import * as os from '@/os.js';
import { useRouter } from '@/router/supplier.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
-import type { SuperMenuDef } from '@/components/MkSuperMenu.vue';
+import { searchIndexes } from '@/scripts/autogen/settings-search-index.js';
+
+const SETTING_INDEX = searchIndexes; // TODO: lazy load
const indexInfo = {
title: i18n.ts.settings,
@@ -63,7 +66,6 @@ const ro = new ResizeObserver((entries, observer) => {
});
const menuDef = computed<SuperMenuDef[]>(() => [{
- title: i18n.ts.basicSettings,
items: [{
icon: 'ti ti-user',
text: i18n.ts.profile,
@@ -101,40 +103,38 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
active: currentPage.value?.route.name === 'security',
}],
}, {
- title: i18n.ts.clientSettings,
items: [{
icon: 'ti ti-adjustments',
- text: i18n.ts.general,
- to: '/settings/general',
- active: currentPage.value?.route.name === 'general',
+ text: i18n.ts.preferences,
+ to: '/settings/preferences',
+ active: currentPage.value?.route.name === 'preferences',
}, {
icon: 'ti ti-palette',
text: i18n.ts.theme,
to: '/settings/theme',
active: currentPage.value?.route.name === 'theme',
}, {
- icon: 'ti ti-menu-2',
- text: i18n.ts.navbar,
- to: '/settings/navbar',
- active: currentPage.value?.route.name === 'navbar',
- }, {
- icon: 'ti ti-equal-double',
- text: i18n.ts.statusbar,
- to: '/settings/statusbar',
- active: currentPage.value?.route.name === 'statusbar',
+ icon: 'ti ti-device-desktop',
+ text: i18n.ts.appearance,
+ to: '/settings/appearance',
+ active: currentPage.value?.route.name === 'appearance',
}, {
icon: 'ti ti-music',
text: i18n.ts.sounds,
to: '/settings/sounds',
active: currentPage.value?.route.name === 'sounds',
}, {
+ icon: 'ti ti-accessible',
+ text: i18n.ts.accessibility,
+ to: '/settings/accessibility',
+ active: currentPage.value?.route.name === 'accessibility',
+ }, {
icon: 'ti ti-plug',
text: i18n.ts.plugins,
to: '/settings/plugin',
active: currentPage.value?.route.name === 'plugin',
}],
}, {
- title: i18n.ts.otherSettings,
items: [{
icon: 'ti ti-badges',
text: i18n.ts.roles,
@@ -161,11 +161,6 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
to: '/settings/import-export',
active: currentPage.value?.route.name === 'import-export',
}, {
- icon: 'ti ti-plane',
- text: `${i18n.ts.accountMigration}`,
- to: '/settings/migration',
- active: currentPage.value?.route.name === 'migration',
- }, {
icon: 'ti ti-dots',
text: i18n.ts.other,
to: '/settings/other',
diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue
index ddc23945dd..1c00d64d73 100644
--- a/packages/frontend/src/pages/settings/migration.vue
+++ b/packages/frontend/src/pages/settings/migration.vue
@@ -68,7 +68,6 @@ import MkUserInfo from '@/components/MkUserInfo.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
import { signinRequired } from '@/account.js';
import { unisonReload } from '@/scripts/unison-reload.js';
@@ -120,11 +119,6 @@ async function save(): Promise<void> {
}
init();
-
-definePageMetadata(() => ({
- title: i18n.ts.accountMigration,
- icon: 'ti ti-plane',
-}));
</script>
<style lang="scss">
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 491676e0d0..4aac2a25bd 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -4,132 +4,171 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="_gaps_m">
- <MkFolder>
- <template #icon><i class="ti ti-message-off"></i></template>
- <template #label>{{ i18n.ts.wordMute }}</template>
+<SearchMarker path="/settings/mute-block" :label="i18n.ts.muteAndBlock" icon="ti ti-ban" :keywords="['mute', 'block']">
+ <div class="_gaps_m">
+ <SearchMarker
+ :label="i18n.ts.wordMute"
+ :keywords="['note', 'word', 'soft', 'mute', 'hide']"
+ >
+ <MkFolder>
+ <template #icon><i class="ti ti-message-off"></i></template>
+ <template #label>{{ i18n.ts.wordMute }}</template>
- <div class="_gaps_m">
- <MkInfo>{{ i18n.ts.wordMuteDescription }}</MkInfo>
- <MkSwitch v-model="showSoftWordMutedWord">{{ i18n.ts.showMutedWord }}</MkSwitch>
- <XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/>
- </div>
- </MkFolder>
+ <div class="_gaps_m">
+ <MkInfo>{{ i18n.ts.wordMuteDescription }}</MkInfo>
- <MkFolder>
- <template #icon><i class="ti ti-message-off"></i></template>
- <template #label>{{ i18n.ts.hardWordMute }}</template>
+ <SearchMarker
+ :label="i18n.ts.showMutedWord"
+ :keywords="['show']"
+ >
+ <MkSwitch v-model="showSoftWordMutedWord">{{ i18n.ts.showMutedWord }}</MkSwitch>
+ </SearchMarker>
- <div class="_gaps_m">
- <MkInfo>{{ i18n.ts.hardWordMuteDescription }}</MkInfo>
- <XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/>
- </div>
- </MkFolder>
+ <XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder v-if="instance.federation !== 'none'">
- <template #icon><i class="ti ti-planet-off"></i></template>
- <template #label>{{ i18n.ts.instanceMute }}</template>
+ <SearchMarker
+ :label="i18n.ts.hardWordMute"
+ :keywords="['note', 'word', 'hard', 'mute', 'hide']"
+ >
+ <MkFolder>
+ <template #icon><i class="ti ti-message-off"></i></template>
+ <template #label>{{ i18n.ts.hardWordMute }}</template>
- <XInstanceMute/>
- </MkFolder>
+ <div class="_gaps_m">
+ <MkInfo>{{ i18n.ts.hardWordMuteDescription }}</MkInfo>
+ <XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-repeat-off"></i></template>
- <template #label>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</template>
+ <SearchMarker
+ :label="i18n.ts.instanceMute"
+ :keywords="['note', 'server', 'instance', 'host', 'federation', 'mute', 'hide']"
+ >
+ <MkFolder v-if="instance.federation !== 'none'">
+ <template #icon><i class="ti ti-planet-off"></i></template>
+ <template #label>{{ i18n.ts.instanceMute }}</template>
- <MkPagination :pagination="renoteMutingPagination">
- <template #empty>
- <div class="_fullinfo">
- <img :src="infoImageUrl" class="_ghost"/>
- <div>{{ i18n.ts.noUsers }}</div>
- </div>
- </template>
+ <XInstanceMute/>
+ </MkFolder>
+ </SearchMarker>
- <template #default="{ items }">
- <div class="_gaps_s">
- <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedRenoteMuteItems.includes(item.id) }]">
- <div :class="$style.userItemMain">
- <MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)">
- <MkUserCardMini :user="item.mutee"/>
- </MkA>
- <button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
- <button class="_button" :class="$style.remove" @click="unrenoteMute(item.mutee, $event)"><i class="ti ti-x"></i></button>
- </div>
- <div v-if="expandedRenoteMuteItems.includes(item.id)" :class="$style.userItemSub">
- <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div>
+ <SearchMarker
+ :label="`${i18n.ts.mutedUsers} (${ i18n.ts.renote })`"
+ :keywords="['renote', 'mute', 'hide', 'user']"
+ >
+ <MkFolder>
+ <template #icon><i class="ti ti-repeat-off"></i></template>
+ <template #label>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</template>
+
+ <MkPagination :pagination="renoteMutingPagination">
+ <template #empty>
+ <div class="_fullinfo">
+ <img :src="infoImageUrl" class="_ghost"/>
+ <div>{{ i18n.ts.noUsers }}</div>
</div>
- </div>
- </div>
- </template>
- </MkPagination>
- </MkFolder>
+ </template>
- <MkFolder>
- <template #icon><i class="ti ti-eye-off"></i></template>
- <template #label>{{ i18n.ts.mutedUsers }}</template>
+ <template #default="{ items }">
+ <div class="_gaps_s">
+ <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedRenoteMuteItems.includes(item.id) }]">
+ <div :class="$style.userItemMain">
+ <MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)">
+ <MkUserCardMini :user="item.mutee"/>
+ </MkA>
+ <button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
+ <button class="_button" :class="$style.remove" @click="unrenoteMute(item.mutee, $event)"><i class="ti ti-x"></i></button>
+ </div>
+ <div v-if="expandedRenoteMuteItems.includes(item.id)" :class="$style.userItemSub">
+ <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div>
+ </div>
+ </div>
+ </div>
+ </template>
+ </MkPagination>
+ </MkFolder>
+ </SearchMarker>
- <MkPagination :pagination="mutingPagination">
- <template #empty>
- <div class="_fullinfo">
- <img :src="infoImageUrl" class="_ghost"/>
- <div>{{ i18n.ts.noUsers }}</div>
- </div>
- </template>
+ <SearchMarker
+ :label="i18n.ts.mutedUsers"
+ :keywords="['note', 'mute', 'hide', 'user']"
+ >
+ <MkFolder>
+ <template #icon><i class="ti ti-eye-off"></i></template>
+ <template #label>{{ i18n.ts.mutedUsers }}</template>
- <template #default="{ items }">
- <div class="_gaps_s">
- <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedMuteItems.includes(item.id) }]">
- <div :class="$style.userItemMain">
- <MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)">
- <MkUserCardMini :user="item.mutee"/>
- </MkA>
- <button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
- <button class="_button" :class="$style.remove" @click="unmute(item.mutee, $event)"><i class="ti ti-x"></i></button>
+ <MkPagination :pagination="mutingPagination">
+ <template #empty>
+ <div class="_fullinfo">
+ <img :src="infoImageUrl" class="_ghost"/>
+ <div>{{ i18n.ts.noUsers }}</div>
</div>
- <div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub">
- <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div>
- <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
- <div v-else>Period: {{ i18n.ts.indefinitely }}</div>
+ </template>
+
+ <template #default="{ items }">
+ <div class="_gaps_s">
+ <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedMuteItems.includes(item.id) }]">
+ <div :class="$style.userItemMain">
+ <MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)">
+ <MkUserCardMini :user="item.mutee"/>
+ </MkA>
+ <button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
+ <button class="_button" :class="$style.remove" @click="unmute(item.mutee, $event)"><i class="ti ti-x"></i></button>
+ </div>
+ <div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub">
+ <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div>
+ <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
+ <div v-else>Period: {{ i18n.ts.indefinitely }}</div>
+ </div>
+ </div>
</div>
- </div>
- </div>
- </template>
- </MkPagination>
- </MkFolder>
+ </template>
+ </MkPagination>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-ban"></i></template>
- <template #label>{{ i18n.ts.blockedUsers }}</template>
+ <SearchMarker
+ :label="i18n.ts.blockedUsers"
+ :keywords="['block', 'user']"
+ >
+ <MkFolder>
+ <template #icon><i class="ti ti-ban"></i></template>
+ <template #label>{{ i18n.ts.blockedUsers }}</template>
- <MkPagination :pagination="blockingPagination">
- <template #empty>
- <div class="_fullinfo">
- <img :src="infoImageUrl" class="_ghost"/>
- <div>{{ i18n.ts.noUsers }}</div>
- </div>
- </template>
-
- <template #default="{ items }">
- <div class="_gaps_s">
- <div v-for="item in items" :key="item.blockee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedBlockItems.includes(item.id) }]">
- <div :class="$style.userItemMain">
- <MkA :class="$style.userItemMainBody" :to="userPage(item.blockee)">
- <MkUserCardMini :user="item.blockee"/>
- </MkA>
- <button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
- <button class="_button" :class="$style.remove" @click="unblock(item.blockee, $event)"><i class="ti ti-x"></i></button>
+ <MkPagination :pagination="blockingPagination">
+ <template #empty>
+ <div class="_fullinfo">
+ <img :src="infoImageUrl" class="_ghost"/>
+ <div>{{ i18n.ts.noUsers }}</div>
</div>
- <div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub">
- <div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div>
- <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
- <div v-else>Period: {{ i18n.ts.indefinitely }}</div>
+ </template>
+
+ <template #default="{ items }">
+ <div class="_gaps_s">
+ <div v-for="item in items" :key="item.blockee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedBlockItems.includes(item.id) }]">
+ <div :class="$style.userItemMain">
+ <MkA :class="$style.userItemMainBody" :to="userPage(item.blockee)">
+ <MkUserCardMini :user="item.blockee"/>
+ </MkA>
+ <button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
+ <button class="_button" :class="$style.remove" @click="unblock(item.blockee, $event)"><i class="ti ti-x"></i></button>
+ </div>
+ <div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub">
+ <div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div>
+ <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
+ <div v-else>Period: {{ i18n.ts.indefinitely }}</div>
+ </div>
+ </div>
</div>
- </div>
- </div>
- </template>
- </MkPagination>
- </MkFolder>
-</div>
+ </template>
+ </MkPagination>
+ </MkFolder>
+ </SearchMarker>
+ </div>
+</SearchMarker>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 4a52e59d02..9742c548e7 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -4,91 +4,111 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="_gaps_m">
- <!--
- <MkSwitch v-model="$i.injectFeaturedNote" @update:model-value="onChangeInjectFeaturedNote">
- <template #label>{{ i18n.ts.showFeaturedNotesInTimeline }}</template>
- </MkSwitch>
- -->
+<SearchMarker path="/settings/other" :label="i18n.ts.other" :keywords="['other']" icon="ti ti-dots">
+ <div class="_gaps_m">
+ <!--
+ <MkSwitch v-model="$i.injectFeaturedNote" @update:model-value="onChangeInjectFeaturedNote">
+ <template #label>{{ i18n.ts.showFeaturedNotesInTimeline }}</template>
+ </MkSwitch>
+ -->
- <!--
- <MkSwitch v-model="reportError">{{ i18n.ts.sendErrorReports }}<template #caption>{{ i18n.ts.sendErrorReportsDescription }}</template></MkSwitch>
- -->
+ <!--
+ <MkSwitch v-model="reportError">{{ i18n.ts.sendErrorReports }}<template #caption>{{ i18n.ts.sendErrorReportsDescription }}</template></MkSwitch>
+ -->
- <FormSection first>
- <div class="_gaps_s">
- <MkFolder>
- <template #icon><i class="ti ti-info-circle"></i></template>
- <template #label>{{ i18n.ts.accountInfo }}</template>
+ <FormSection first>
+ <div class="_gaps_s">
+ <SearchMarker :keywords="['account', 'info']">
+ <MkFolder>
+ <template #icon><i class="ti ti-info-circle"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.accountInfo }}</SearchLabel></template>
- <div class="_gaps_m">
- <MkKeyValue>
- <template #key>ID</template>
- <template #value><span class="_monospace">{{ $i.id }}</span></template>
- </MkKeyValue>
+ <div class="_gaps_m">
+ <MkKeyValue>
+ <template #key>ID</template>
+ <template #value><span class="_monospace">{{ $i.id }}</span></template>
+ </MkKeyValue>
- <MkKeyValue>
- <template #key>{{ i18n.ts.registeredDate }}</template>
- <template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
- </MkKeyValue>
- </div>
- </MkFolder>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.registeredDate }}</template>
+ <template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
+ </MkKeyValue>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-alert-triangle"></i></template>
- <template #label>{{ i18n.ts.closeAccount }}</template>
+ <SearchMarker :keywords="['account', 'move', 'migration']">
+ <MkFolder>
+ <template #icon><i class="ti ti-plane"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.accountMigration }}</SearchLabel></template>
- <div class="_gaps_m">
- <FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
- <FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
- <MkButton v-if="!$i.isDeleted" danger @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</MkButton>
- <MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
- </div>
- </MkFolder>
+ <XMigration/>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-flask"></i></template>
- <template #label>{{ i18n.ts.experimentalFeatures }}</template>
+ <SearchMarker :keywords="['account', 'close', 'delete']">
+ <MkFolder>
+ <template #icon><i class="ti ti-alert-triangle"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.closeAccount }}</SearchLabel></template>
- <div class="_gaps_m">
- <MkSwitch v-model="enableCondensedLine">
- <template #label>Enable condensed line</template>
- </MkSwitch>
- <MkSwitch v-model="skipNoteRender">
- <template #label>Enable note render skipping</template>
- </MkSwitch>
- </div>
- </MkFolder>
+ <div class="_gaps_m">
+ <FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
+ <FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
+ <MkButton v-if="!$i.isDeleted" danger @click="deleteAccount"><SearchKeyword>{{ i18n.ts._accountDelete.requestAccountDelete }}</SearchKeyword></MkButton>
+ <MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-code"></i></template>
- <template #label>{{ i18n.ts.developer }}</template>
+ <SearchMarker :keywords="['experimental', 'feature', 'flags']">
+ <MkFolder>
+ <template #icon><i class="ti ti-flask"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.experimentalFeatures }}</SearchLabel></template>
- <div class="_gaps_m">
- <MkSwitch v-model="devMode">
- <template #label>{{ i18n.ts.devMode }}</template>
- </MkSwitch>
- </div>
- </MkFolder>
- </div>
- </FormSection>
+ <div class="_gaps_m">
+ <MkSwitch v-model="enableCondensedLine">
+ <template #label>Enable condensed line</template>
+ </MkSwitch>
+ <MkSwitch v-model="skipNoteRender">
+ <template #label>Enable note render skipping</template>
+ </MkSwitch>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <FormSection>
- <FormLink to="/registry"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink>
- </FormSection>
+ <SearchMarker :keywords="['developer', 'mode', 'debug']">
+ <MkFolder>
+ <template #icon><i class="ti ti-code"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.developer }}</SearchLabel></template>
- <FormSection>
- <div class="_gaps_s">
- <MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch>
- <MkButton danger @click="updateRepliesAll(true)"><i class="ti ti-messages"></i> {{ i18n.ts.showRepliesToOthersInTimelineAll }}</MkButton>
- <MkButton danger @click="updateRepliesAll(false)"><i class="ti ti-messages-off"></i> {{ i18n.ts.hideRepliesToOthersInTimelineAll }}</MkButton>
- </div>
- </FormSection>
-</div>
+ <div class="_gaps_m">
+ <MkSwitch v-model="devMode">
+ <template #label>{{ i18n.ts.devMode }}</template>
+ </MkSwitch>
+ </div>
+ </MkFolder>
+ </SearchMarker>
+ </div>
+ </FormSection>
+
+ <FormSection>
+ <FormLink to="/registry"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink>
+ </FormSection>
+
+ <FormSection>
+ <div class="_gaps_s">
+ <MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch>
+ <MkButton danger @click="updateRepliesAll(true)"><i class="ti ti-messages"></i> {{ i18n.ts.showRepliesToOthersInTimelineAll }}</MkButton>
+ <MkButton danger @click="updateRepliesAll(false)"><i class="ti ti-messages-off"></i> {{ i18n.ts.hideRepliesToOthersInTimelineAll }}</MkButton>
+ </div>
+ </FormSection>
+ </div>
+</SearchMarker>
</template>
<script lang="ts" setup>
import { computed, watch } from 'vue';
+import XMigration from './migration.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue';
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
new file mode 100644
index 0000000000..fe718bfa69
--- /dev/null
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -0,0 +1,434 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<SearchMarker path="/settings/preferences" :label="i18n.ts.preferences" :keywords="['general', 'preferences']" icon="ti ti-adjustments">
+ <div class="_gaps_m">
+ <SearchMarker :keywords="['language']">
+ <MkSelect v-model="lang">
+ <template #label><SearchLabel>{{ i18n.ts.uiLanguage }}</SearchLabel></template>
+ <option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
+ <template #caption>
+ <I18n :src="i18n.ts.i18nInfo" tag="span">
+ <template #link>
+ <MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
+ </template>
+ </I18n>
+ </template>
+ </MkSelect>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop']">
+ <MkRadios v-model="overridedDeviceKind">
+ <template #label><SearchLabel>{{ i18n.ts.overridedDeviceKind }}</SearchLabel></template>
+ <option :value="null">{{ i18n.ts.auto }}</option>
+ <option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option>
+ <option value="tablet"><i class="ti ti-device-tablet"/> {{ i18n.ts.tablet }}</option>
+ <option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option>
+ </MkRadios>
+ </SearchMarker>
+
+ <FormSection>
+ <div class="_gaps_s">
+ <SearchMarker :keywords="['post', 'form', 'timeline']">
+ <MkSwitch v-model="showFixedPostForm">
+ <template #label><SearchLabel>{{ i18n.ts.showFixedPostForm }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['post', 'form', 'timeline', 'channel']">
+ <MkSwitch v-model="showFixedPostFormInChannel">
+ <template #label><SearchLabel>{{ i18n.ts.showFixedPostFormInChannel }}</SearchLabel></template>
+ </MkSwitch>
+ </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-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>
+ </SearchMarker>
+ </div>
+ </FormSection>
+
+ <SearchMarker :keywords="['note']">
+ <FormSection>
+ <template #label><SearchLabel>{{ i18n.ts.note }}</SearchLabel></template>
+
+ <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>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['hover', 'show', 'footer', 'action']">
+ <MkSwitch v-model="showNoteActionsOnlyHover">
+ <template #label><SearchLabel>{{ i18n.ts.showNoteActionsOnlyHover }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['footer', 'action', 'clip', 'show']">
+ <MkSwitch v-model="showClipButtonInNoteFooter">
+ <template #label><SearchLabel>{{ i18n.ts.showClipButtonInNoteFooter }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['mfm', 'enable', 'show', 'advanced']">
+ <MkSwitch v-model="advancedMfm">
+ <template #label><SearchLabel>{{ i18n.ts.enableAdvancedMfm }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['reaction', 'count', 'show']">
+ <MkSwitch v-model="showReactionsCount">
+ <template #label><SearchLabel>{{ i18n.ts.showReactionsCount }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment']">
+ <MkSwitch v-model="loadRawImages">
+ <template #label><SearchLabel>{{ i18n.ts.loadRawImages }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+ </div>
+ </div>
+ </FormSection>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['notification']">
+ <FormSection>
+ <template #label><SearchLabel>{{ i18n.ts.notifications }}</SearchLabel></template>
+
+ <div class="_gaps_m">
+ <SearchMarker :keywords="['group']">
+ <MkSwitch v-model="useGroupedNotifications">
+ <template #label><SearchLabel>{{ i18n.ts.useGroupedNotifications }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+ </div>
+ </FormSection>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['behavior']">
+ <FormSection>
+ <template #label><SearchLabel>{{ i18n.ts.behavior }}</SearchLabel></template>
+
+ <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>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['reaction', 'picker', 'contextmenu', 'open']">
+ <MkSwitch v-model="useReactionPickerForContextMenu">
+ <template #label><SearchLabel>{{ i18n.ts.useReactionPickerForContextMenu }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['load', 'auto', 'more']">
+ <MkSwitch v-model="enableInfiniteScroll">
+ <template #label><SearchLabel>{{ i18n.ts.enableInfiniteScroll }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['disable', 'streaming', 'timeline']">
+ <MkSwitch v-model="disableStreamingTimeline">
+ <template #label><SearchLabel>{{ i18n.ts.disableStreamingTimeline }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['follow', 'confirm', 'always']">
+ <MkSwitch v-model="alwaysConfirmFollow">
+ <template #label><SearchLabel>{{ i18n.ts.alwaysConfirmFollow }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm']">
+ <MkSwitch v-model="confirmWhenRevealingSensitiveMedia">
+ <template #label><SearchLabel>{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['reaction', 'confirm']">
+ <MkSwitch v-model="confirmOnReact">
+ <template #label><SearchLabel>{{ i18n.ts.confirmOnReact }}</SearchLabel></template>
+ </MkSwitch>
+ </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>
+ </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>
+ </SearchMarker>
+
+ <SearchMarker :label="i18n.ts.dataSaver" :keywords="['datasaver']">
+ <MkFolder>
+ <template #label><SearchLabel>{{ i18n.ts.dataSaver }}</SearchLabel></template>
+
+ <div class="_gaps_m">
+ <MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
+
+ <div class="_buttons">
+ <MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton>
+ <MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton>
+ </div>
+ <div class="_gaps_m">
+ <MkSwitch v-model="dataSaver.media">
+ {{ i18n.ts._dataSaver._media.title }}
+ <template #caption>{{ i18n.ts._dataSaver._media.description }}</template>
+ </MkSwitch>
+ <MkSwitch v-model="dataSaver.avatar">
+ {{ i18n.ts._dataSaver._avatar.title }}
+ <template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template>
+ </MkSwitch>
+ <MkSwitch v-model="dataSaver.urlPreview">
+ {{ i18n.ts._dataSaver._urlPreview.title }}
+ <template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template>
+ </MkSwitch>
+ <MkSwitch v-model="dataSaver.code">
+ {{ i18n.ts._dataSaver._code.title }}
+ <template #caption>{{ i18n.ts._dataSaver._code.description }}</template>
+ </MkSwitch>
+ </div>
+ </div>
+ </MkFolder>
+ </SearchMarker>
+ </div>
+ </FormSection>
+ </SearchMarker>
+
+ <SearchMarker>
+ <FormSection>
+ <template #label><SearchLabel>{{ i18n.ts.other }}</SearchLabel></template>
+
+ <div class="_gaps">
+ <SearchMarker :keywords="['ad', 'show']">
+ <MkSwitch v-model="forceShowAds">
+ <template #label><SearchLabel>{{ i18n.ts.forceShowAds }}</SearchLabel></template>
+ </MkSwitch>
+ </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>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['emoji', 'dictionary', 'additional', 'extra']">
+ <MkFolder>
+ <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>
+ </template>
+ </div>
+ </MkFolder>
+ </SearchMarker>
+
+ <FormLink to="/settings/navbar">{{ i18n.ts.navbar }}</FormLink>
+ <FormLink to="/settings/statusbar">{{ i18n.ts.statusbar }}</FormLink>
+ </div>
+ </FormSection>
+ </SearchMarker>
+
+ <FormSection>
+ <div class="_gaps">
+ <FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
+ </div>
+ </FormSection>
+ </div>
+</SearchMarker>
+</template>
+
+<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';
+import MkRadios from '@/components/MkRadios.vue';
+import MkRange from '@/components/MkRange.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkButton from '@/components/MkButton.vue';
+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 * 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';
+
+const lang = ref(miLocalStorage.getItem('lang'));
+const dataSaver = ref(defaultStore.state.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'));
+
+watch(lang, () => {
+ miLocalStorage.setItem('lang', lang.value as string);
+ miLocalStorage.removeItem('locale');
+ miLocalStorage.removeItem('localeVersion');
+});
+
+watch([
+ hemisphere,
+ lang,
+ enableInfiniteScroll,
+ showNoteActionsOnlyHover,
+ overridedDeviceKind,
+ disableStreamingTimeline,
+ alwaysConfirmFollow,
+ confirmWhenRevealingSensitiveMedia,
+ contextMenu,
+], async () => {
+ await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
+});
+
+const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const;
+
+function getEmojiIndexLangName(targetLang: typeof emojiIndexLangs[number]) {
+ if (langs.find(x => x[0] === targetLang)) {
+ return langs.find(x => x[0] === targetLang)![1];
+ } else {
+ // 絵文字辞書限定の言語定義
+ switch (targetLang) {
+ case 'ja-JP_hira': return 'ひらがな';
+ default: return targetLang;
+ }
+ }
+}
+
+function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
+ async function main() {
+ const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
+
+ function download() {
+ switch (lang) {
+ case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default);
+ case 'ja-JP': return import('../../unicode-emoji-indexes/ja-JP.json').then(x => x.default);
+ case 'ja-JP_hira': return import('../../unicode-emoji-indexes/ja-JP_hira.json').then(x => x.default);
+ default: throw new Error('unrecognized lang: ' + lang);
+ }
+ }
+
+ currentIndexes[lang] = await download();
+ await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
+ }
+
+ os.promiseDialog(main());
+}
+
+function removeEmojiIndex(lang: string) {
+ async function main() {
+ const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
+ delete currentIndexes[lang];
+ await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
+ }
+
+ os.promiseDialog(main());
+}
+
+async function setPinnedList() {
+ const lists = await misskeyApi('users/lists/list');
+ const { canceled, result: list } = await os.select({
+ title: i18n.ts.selectList,
+ items: lists.map(x => ({
+ value: x, text: x.name,
+ })),
+ });
+ if (canceled) return;
+
+ defaultStore.set('pinnedUserLists', [list]);
+}
+
+function removePinnedList() {
+ defaultStore.set('pinnedUserLists', []);
+}
+
+function enableAllDataSaver() {
+ const g = { ...defaultStore.state.dataSaver };
+
+ Object.keys(g).forEach((key) => { g[key] = true; });
+
+ dataSaver.value = g;
+}
+
+function disableAllDataSaver() {
+ const g = { ...defaultStore.state.dataSaver };
+
+ Object.keys(g).forEach((key) => { g[key] = false; });
+
+ dataSaver.value = g;
+}
+
+watch(dataSaver, (to) => {
+ defaultStore.set('dataSaver', to);
+}, {
+ deep: true,
+});
+
+const headerActions = computed(() => []);
+
+const headerTabs = computed(() => []);
+
+definePageMetadata(() => ({
+ title: i18n.ts.general,
+ icon: 'ti ti-adjustments',
+}));
+</script>
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index 54a5aeb6c1..cd0d54a73b 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -4,158 +4,208 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="_gaps_m">
- <MkSwitch v-model="isLocked" @update:modelValue="save()">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
- <MkSwitch v-if="isLocked" v-model="autoAcceptFollowed" @update:modelValue="save()">{{ i18n.ts.autoAcceptFollowed }}</MkSwitch>
+<SearchMarker path="/settings/privacy" :label="i18n.ts.privacy" :keywords="['privacy']" icon="ti ti-lock-open">
+ <div class="_gaps_m">
+ <SearchMarker :keywords="['follow', 'lock']">
+ <MkSwitch v-model="isLocked" @update:modelValue="save()">
+ <template #label><SearchLabel>{{ i18n.ts.makeFollowManuallyApprove }}</SearchLabel></template>
+ <template #caption><SearchKeyword>{{ i18n.ts.lockedAccountInfo }}</SearchKeyword></template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSwitch v-model="publicReactions" @update:modelValue="save()">
- {{ i18n.ts.makeReactionsPublic }}
- <template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template>
- </MkSwitch>
+ <MkDisableSection :disabled="!isLocked">
+ <SearchMarker :keywords="['follow', 'auto', 'accept']">
+ <MkSwitch v-model="autoAcceptFollowed" @update:modelValue="save()">
+ <template #label><SearchLabel>{{ i18n.ts.autoAcceptFollowed }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+ </MkDisableSection>
- <MkSelect v-model="followingVisibility" @update:modelValue="save()">
- <template #label>{{ i18n.ts.followingVisibility }}</template>
- <option value="public">{{ i18n.ts._ffVisibility.public }}</option>
- <option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
- <option value="private">{{ i18n.ts._ffVisibility.private }}</option>
- </MkSelect>
+ <SearchMarker :keywords="['reaction', 'public']">
+ <MkSwitch v-model="publicReactions" @update:modelValue="save()">
+ <template #label><SearchLabel>{{ i18n.ts.makeReactionsPublic }}</SearchLabel></template>
+ <template #caption><SearchKeyword>{{ i18n.ts.makeReactionsPublicDescription }}</SearchKeyword></template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSelect v-model="followersVisibility" @update:modelValue="save()">
- <template #label>{{ i18n.ts.followersVisibility }}</template>
- <option value="public">{{ i18n.ts._ffVisibility.public }}</option>
- <option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
- <option value="private">{{ i18n.ts._ffVisibility.private }}</option>
- </MkSelect>
+ <SearchMarker :keywords="['following', 'visibility']">
+ <MkSelect v-model="followingVisibility" @update:modelValue="save()">
+ <template #label><SearchLabel>{{ i18n.ts.followingVisibility }}</SearchLabel></template>
+ <option value="public">{{ i18n.ts._ffVisibility.public }}</option>
+ <option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
+ <option value="private">{{ i18n.ts._ffVisibility.private }}</option>
+ </MkSelect>
+ </SearchMarker>
- <MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
- {{ i18n.ts.hideOnlineStatus }}
- <template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template>
- </MkSwitch>
- <MkSwitch v-model="noCrawle" @update:modelValue="save()">
- {{ i18n.ts.noCrawle }}
- <template #caption>{{ i18n.ts.noCrawleDescription }}</template>
- </MkSwitch>
- <MkSwitch v-model="preventAiLearning" @update:modelValue="save()">
- {{ i18n.ts.preventAiLearning }}
- <template #caption>{{ i18n.ts.preventAiLearningDescription }}</template>
- </MkSwitch>
- <MkSwitch v-model="isExplorable" @update:modelValue="save()">
- {{ i18n.ts.makeExplorable }}
- <template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
- </MkSwitch>
+ <SearchMarker :keywords="['follower', 'visibility']">
+ <MkSelect v-model="followersVisibility" @update:modelValue="save()">
+ <template #label><SearchLabel>{{ i18n.ts.followersVisibility }}</SearchLabel></template>
+ <option value="public">{{ i18n.ts._ffVisibility.public }}</option>
+ <option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
+ <option value="private">{{ i18n.ts._ffVisibility.private }}</option>
+ </MkSelect>
+ </SearchMarker>
- <FormSection>
- <template #label>{{ i18n.ts.lockdown }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <SearchMarker :keywords="['online', 'status']">
+ <MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
+ <template #label><SearchLabel>{{ i18n.ts.hideOnlineStatus }}</SearchLabel></template>
+ <template #caption><SearchKeyword>{{ i18n.ts.hideOnlineStatusDescription }}</SearchKeyword></template>
+ </MkSwitch>
+ </SearchMarker>
- <div class="_gaps_m">
- <MkSwitch :modelValue="requireSigninToViewContents" @update:modelValue="update_requireSigninToViewContents">
- {{ i18n.ts._accountSettings.requireSigninToViewContents }}
- <template #caption>
- <div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div>
- <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div>
- <div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div>
- </template>
+ <SearchMarker :keywords="['crawle', 'index', 'search']">
+ <MkSwitch v-model="noCrawle" @update:modelValue="save()">
+ <template #label><SearchLabel>{{ i18n.ts.noCrawle }}</SearchLabel></template>
+ <template #caption><SearchKeyword>{{ i18n.ts.noCrawleDescription }}</SearchKeyword></template>
</MkSwitch>
+ </SearchMarker>
- <FormSlot>
- <template #label>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBefore }}</template>
+ <SearchMarker :keywords="['crawle', 'ai']">
+ <MkSwitch v-model="preventAiLearning" @update:modelValue="save()">
+ <template #label><SearchLabel>{{ i18n.ts.preventAiLearning }}</SearchLabel></template>
+ <template #caption><SearchKeyword>{{ i18n.ts.preventAiLearningDescription }}</SearchKeyword></template>
+ </MkSwitch>
+ </SearchMarker>
- <div class="_gaps_s">
- <MkSelect :modelValue="makeNotesFollowersOnlyBefore_type" @update:modelValue="makeNotesFollowersOnlyBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
- <option :value="null">{{ i18n.ts.none }}</option>
- <option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
- <option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
- </MkSelect>
+ <SearchMarker :keywords="['explore']">
+ <MkSwitch v-model="isExplorable" @update:modelValue="save()">
+ <template #label><SearchLabel>{{ i18n.ts.makeExplorable }}</SearchLabel></template>
+ <template #caption><SearchKeyword>{{ i18n.ts.makeExplorableDescription }}</SearchKeyword></template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSelect v-if="makeNotesFollowersOnlyBefore_type === 'relative'" v-model="makeNotesFollowersOnlyBefore">
- <option :value="-3600">{{ i18n.ts.oneHour }}</option>
- <option :value="-86400">{{ i18n.ts.oneDay }}</option>
- <option :value="-259200">{{ i18n.ts.threeDays }}</option>
- <option :value="-604800">{{ i18n.ts.oneWeek }}</option>
- <option :value="-2592000">{{ i18n.ts.oneMonth }}</option>
- <option :value="-7776000">{{ i18n.ts.threeMonths }}</option>
- <option :value="-31104000">{{ i18n.ts.oneYear }}</option>
- </MkSelect>
+ <SearchMarker :keywords="['lockdown']">
+ <FormSection>
+ <template #label><SearchLabel>{{ i18n.ts.lockdown }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
- <MkInput
- v-if="makeNotesFollowersOnlyBefore_type === 'absolute'"
- :modelValue="formatDateTimeString(new Date(makeNotesFollowersOnlyBefore * 1000), 'yyyy-MM-dd')"
- type="date"
- :manualSave="true"
- @update:modelValue="makeNotesFollowersOnlyBefore = Math.floor(new Date($event).getTime() / 1000)"
- >
- </MkInput>
- </div>
+ <div class="_gaps_m">
+ <SearchMarker :keywords="['login', 'signin']">
+ <MkSwitch :modelValue="requireSigninToViewContents" @update:modelValue="update_requireSigninToViewContents">
+ <template #label><SearchLabel>{{ i18n.ts._accountSettings.requireSigninToViewContents }}</SearchLabel></template>
+ <template #caption>
+ <div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div>
+ <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div>
+ <div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div>
+ </template>
+ </MkSwitch>
+ </SearchMarker>
- <template #caption>
- <div>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</div>
- <div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
- </template>
- </FormSlot>
+ <SearchMarker :keywords="['follower']">
+ <FormSlot>
+ <template #label><SearchLabel>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBefore }}</SearchLabel></template>
- <FormSlot>
- <template #label>{{ i18n.ts._accountSettings.makeNotesHiddenBefore }}</template>
+ <div class="_gaps_s">
+ <MkSelect :modelValue="makeNotesFollowersOnlyBefore_type" @update:modelValue="makeNotesFollowersOnlyBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
+ <option :value="null">{{ i18n.ts.none }}</option>
+ <option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
+ <option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
+ </MkSelect>
- <div class="_gaps_s">
- <MkSelect :modelValue="makeNotesHiddenBefore_type" @update:modelValue="makeNotesHiddenBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
- <option :value="null">{{ i18n.ts.none }}</option>
- <option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
- <option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
- </MkSelect>
+ <MkSelect v-if="makeNotesFollowersOnlyBefore_type === 'relative'" v-model="makeNotesFollowersOnlyBefore">
+ <option :value="-3600">{{ i18n.ts.oneHour }}</option>
+ <option :value="-86400">{{ i18n.ts.oneDay }}</option>
+ <option :value="-259200">{{ i18n.ts.threeDays }}</option>
+ <option :value="-604800">{{ i18n.ts.oneWeek }}</option>
+ <option :value="-2592000">{{ i18n.ts.oneMonth }}</option>
+ <option :value="-7776000">{{ i18n.ts.threeMonths }}</option>
+ <option :value="-31104000">{{ i18n.ts.oneYear }}</option>
+ </MkSelect>
- <MkSelect v-if="makeNotesHiddenBefore_type === 'relative'" v-model="makeNotesHiddenBefore">
- <option :value="-3600">{{ i18n.ts.oneHour }}</option>
- <option :value="-86400">{{ i18n.ts.oneDay }}</option>
- <option :value="-259200">{{ i18n.ts.threeDays }}</option>
- <option :value="-604800">{{ i18n.ts.oneWeek }}</option>
- <option :value="-2592000">{{ i18n.ts.oneMonth }}</option>
- <option :value="-7776000">{{ i18n.ts.threeMonths }}</option>
- <option :value="-31104000">{{ i18n.ts.oneYear }}</option>
- </MkSelect>
+ <MkInput
+ v-if="makeNotesFollowersOnlyBefore_type === 'absolute'"
+ :modelValue="formatDateTimeString(new Date(makeNotesFollowersOnlyBefore * 1000), 'yyyy-MM-dd')"
+ type="date"
+ :manualSave="true"
+ @update:modelValue="makeNotesFollowersOnlyBefore = Math.floor(new Date($event).getTime() / 1000)"
+ >
+ </MkInput>
+ </div>
- <MkInput
- v-if="makeNotesHiddenBefore_type === 'absolute'"
- :modelValue="formatDateTimeString(new Date(makeNotesHiddenBefore * 1000), 'yyyy-MM-dd')"
- type="date"
- :manualSave="true"
- @update:modelValue="makeNotesHiddenBefore = Math.floor(new Date($event).getTime() / 1000)"
- >
- </MkInput>
- </div>
+ <template #caption>
+ <div><SearchKeyword>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</SearchKeyword></div>
+ <div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
+ </template>
+ </FormSlot>
+ </SearchMarker>
- <template #caption>
- <div>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</div>
- <div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
- </template>
- </FormSlot>
- </div>
- </FormSection>
+ <SearchMarker :keywords="['hidden']">
+ <FormSlot>
+ <template #label><SearchLabel>{{ i18n.ts._accountSettings.makeNotesHiddenBefore }}</SearchLabel></template>
- <FormSection>
- <div class="_gaps_m">
- <MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch>
- <MkFolder v-if="!rememberNoteVisibility">
- <template #label>{{ i18n.ts.defaultNoteVisibility }}</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_s">
+ <MkSelect :modelValue="makeNotesHiddenBefore_type" @update:modelValue="makeNotesHiddenBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
+ <option :value="null">{{ i18n.ts.none }}</option>
+ <option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
+ <option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
+ </MkSelect>
- <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>
+ <MkSelect v-if="makeNotesHiddenBefore_type === 'relative'" v-model="makeNotesHiddenBefore">
+ <option :value="-3600">{{ i18n.ts.oneHour }}</option>
+ <option :value="-86400">{{ i18n.ts.oneDay }}</option>
+ <option :value="-259200">{{ i18n.ts.threeDays }}</option>
+ <option :value="-604800">{{ i18n.ts.oneWeek }}</option>
+ <option :value="-2592000">{{ i18n.ts.oneMonth }}</option>
+ <option :value="-7776000">{{ i18n.ts.threeMonths }}</option>
+ <option :value="-31104000">{{ i18n.ts.oneYear }}</option>
+ </MkSelect>
+
+ <MkInput
+ v-if="makeNotesHiddenBefore_type === 'absolute'"
+ :modelValue="formatDateTimeString(new Date(makeNotesHiddenBefore * 1000), 'yyyy-MM-dd')"
+ type="date"
+ :manualSave="true"
+ @update:modelValue="makeNotesHiddenBefore = Math.floor(new Date($event).getTime() / 1000)"
+ >
+ </MkInput>
+ </div>
+
+ <template #caption>
+ <div><SearchKeyword>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</SearchKeyword></div>
+ <div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
+ </template>
+ </FormSlot>
+ </SearchMarker>
</div>
- </MkFolder>
- </div>
- </FormSection>
+ </FormSection>
+ </SearchMarker>
+
+ <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>
+ </SearchMarker>
- <MkSwitch v-model="keepCw" @update:modelValue="save()">{{ i18n.ts.keepCw }}</MkSwitch>
-</div>
+ <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>
+
+ <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>
+ </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>
<script lang="ts" setup>
@@ -174,6 +224,7 @@ import FormSlot from '@/components/form/slot.vue';
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';
const $i = signinRequired();
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 561894d2b7..51148a1f72 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -4,115 +4,152 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="_gaps_m">
- <div class="_panel">
- <div :class="$style.banner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
- <MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
- </div>
- <div :class="$style.avatarContainer">
- <MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration @click="changeAvatar"/>
- <div class="_buttonsCenter">
- <MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
- <MkButton primary rounded link to="/settings/avatar-decoration">{{ i18n.ts.decorate }} <i class="ti ti-sparkles"></i></MkButton>
+<SearchMarker path="/settings/profile" :label="i18n.ts.profile" :keywords="['profile']" icon="ti ti-user">
+ <div class="_gaps_m">
+ <div class="_panel">
+ <div :class="$style.banner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
+ <div :class="$style.bannerEdit">
+ <SearchMarker :keywords="['banner', 'change']">
+ <MkButton primary rounded @click="changeBanner"><SearchLabel>{{ i18n.ts._profile.changeBanner }}</SearchLabel></MkButton>
+ </SearchMarker>
+ </div>
+ </div>
+ <div :class="$style.avatarContainer">
+ <MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration @click="changeAvatar"/>
+ <div class="_buttonsCenter">
+ <SearchMarker :keywords="['avatar', 'icon', 'change']">
+ <MkButton primary rounded @click="changeAvatar"><SearchLabel>{{ i18n.ts._profile.changeAvatar }}</SearchLabel></MkButton>
+ </SearchMarker>
+ <MkButton primary rounded link to="/settings/avatar-decoration">{{ i18n.ts.decorate }} <i class="ti ti-sparkles"></i></MkButton>
+ </div>
</div>
</div>
- </div>
-
- <MkInput v-model="profile.name" :max="30" manualSave :mfmAutocomplete="['emoji']">
- <template #label>{{ i18n.ts._profile.name }}</template>
- </MkInput>
- <MkTextarea v-model="profile.description" :max="500" tall manualSave mfmAutocomplete :mfmPreview="true">
- <template #label>{{ i18n.ts._profile.description }}</template>
- <template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
- </MkTextarea>
+ <SearchMarker :keywords="['name']">
+ <MkInput v-model="profile.name" :max="30" manualSave :mfmAutocomplete="['emoji']">
+ <template #label><SearchLabel>{{ i18n.ts._profile.name }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="profile.location" manualSave>
- <template #label>{{ i18n.ts.location }}</template>
- <template #prefix><i class="ti ti-map-pin"></i></template>
- </MkInput>
+ <SearchMarker :keywords="['description', 'bio']">
+ <MkTextarea v-model="profile.description" :max="500" tall manualSave mfmAutocomplete :mfmPreview="true">
+ <template #label><SearchLabel>{{ i18n.ts._profile.description }}</SearchLabel></template>
+ <template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
+ </MkTextarea>
+ </SearchMarker>
- <MkInput v-model="profile.birthday" type="date" manualSave>
- <template #label>{{ i18n.ts.birthday }}</template>
- <template #prefix><i class="ti ti-cake"></i></template>
- </MkInput>
+ <SearchMarker :keywords="['location', 'locale']">
+ <MkInput v-model="profile.location" manualSave>
+ <template #label><SearchLabel>{{ i18n.ts.location }}</SearchLabel></template>
+ <template #prefix><i class="ti ti-map-pin"></i></template>
+ </MkInput>
+ </SearchMarker>
- <MkSelect v-model="profile.lang">
- <template #label>{{ i18n.ts.language }}</template>
- <option v-for="x in Object.keys(langmap)" :key="x" :value="x">{{ langmap[x].nativeName }}</option>
- </MkSelect>
-
- <FormSlot>
- <MkFolder>
- <template #icon><i class="ti ti-list"></i></template>
- <template #label>{{ i18n.ts._profile.metadataEdit }}</template>
- <template #footer>
- <div class="_buttons">
- <MkButton primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
- <MkButton :disabled="fields.length >= 16" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
- <MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" danger @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
- <MkButton v-else @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton>
- </div>
- </template>
+ <SearchMarker :keywords="['birthday', 'birthdate', 'age']">
+ <MkInput v-model="profile.birthday" type="date" manualSave>
+ <template #label><SearchLabel>{{ i18n.ts.birthday }}</SearchLabel></template>
+ <template #prefix><i class="ti ti-cake"></i></template>
+ </MkInput>
+ </SearchMarker>
- <div :class="$style.metadataRoot" class="_gaps_s">
- <MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
+ <SearchMarker :keywords="['language', 'locale']">
+ <MkSelect v-model="profile.lang">
+ <template #label><SearchLabel>{{ i18n.ts.language }}</SearchLabel></template>
+ <option v-for="x in Object.keys(langmap)" :key="x" :value="x">{{ langmap[x].nativeName }}</option>
+ </MkSelect>
+ </SearchMarker>
- <Sortable
- v-model="fields"
- class="_gaps_s"
- itemKey="id"
- :animation="150"
- :handle="'.' + $style.dragItemHandle"
- @start="e => e.item.classList.add('active')"
- @end="e => e.item.classList.remove('active')"
- >
- <template #item="{element, index}">
- <div v-panel :class="$style.fieldDragItem">
- <button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button>
- <button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button>
- <div :class="$style.dragItemForm">
- <FormSplit :minWidth="200">
- <MkInput v-model="element.name" small :placeholder="i18n.ts._profile.metadataLabel">
- </MkInput>
- <MkInput v-model="element.value" small :placeholder="i18n.ts._profile.metadataContent">
- </MkInput>
- </FormSplit>
- </div>
+ <SearchMarker :keywords="['metadata']">
+ <FormSlot>
+ <MkFolder>
+ <template #icon><i class="ti ti-list"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts._profile.metadataEdit }}</SearchLabel></template>
+ <template #footer>
+ <div class="_buttons">
+ <MkButton primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+ <MkButton :disabled="fields.length >= 16" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+ <MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" danger @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+ <MkButton v-else @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton>
</div>
</template>
- </Sortable>
- </div>
- </MkFolder>
- <template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
- </FormSlot>
- <MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false">
- <template #label>{{ i18n.ts._profile.followedMessage }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
- <template #caption>
- <div>{{ i18n.ts._profile.followedMessageDescription }}</div>
- <div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div>
- </template>
- </MkInput>
+ <div :class="$style.metadataRoot" class="_gaps_s">
+ <MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
- <MkSelect v-model="reactionAcceptance">
- <template #label>{{ i18n.ts.reactionAcceptance }}</template>
- <option :value="null">{{ i18n.ts.all }}</option>
- <option value="likeOnlyForRemote">{{ i18n.ts.likeOnlyForRemote }}</option>
- <option value="nonSensitiveOnly">{{ i18n.ts.nonSensitiveOnly }}</option>
- <option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option>
- <option value="likeOnly">{{ i18n.ts.likeOnly }}</option>
- </MkSelect>
+ <Sortable
+ v-model="fields"
+ class="_gaps_s"
+ itemKey="id"
+ :animation="150"
+ :handle="'.' + $style.dragItemHandle"
+ @start="e => e.item.classList.add('active')"
+ @end="e => e.item.classList.remove('active')"
+ >
+ <template #item="{element, index}">
+ <div v-panel :class="$style.fieldDragItem">
+ <button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button>
+ <button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button>
+ <div :class="$style.dragItemForm">
+ <FormSplit :minWidth="200">
+ <MkInput v-model="element.name" small :placeholder="i18n.ts._profile.metadataLabel">
+ </MkInput>
+ <MkInput v-model="element.value" small :placeholder="i18n.ts._profile.metadataContent">
+ </MkInput>
+ </FormSplit>
+ </div>
+ </div>
+ </template>
+ </Sortable>
+ </div>
+ </MkFolder>
+ <template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
+ </FormSlot>
+ </SearchMarker>
- <MkFolder>
- <template #label>{{ i18n.ts.advancedSettings }}</template>
+ <SearchMarker :keywords="['follow', 'message']">
+ <MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false">
+ <template #label><SearchLabel>{{ i18n.ts._profile.followedMessage }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <template #caption>
+ <div><SearchKeyword>{{ i18n.ts._profile.followedMessageDescription }}</SearchKeyword></div>
+ <div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div>
+ </template>
+ </MkInput>
+ </SearchMarker>
- <div class="_gaps_m">
- <MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
- <MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
- </div>
- </MkFolder>
-</div>
+ <SearchMarker :keywords="['reaction']">
+ <MkSelect v-model="reactionAcceptance">
+ <template #label><SearchLabel>{{ i18n.ts.reactionAcceptance }}</SearchLabel></template>
+ <option :value="null">{{ i18n.ts.all }}</option>
+ <option value="likeOnlyForRemote">{{ i18n.ts.likeOnlyForRemote }}</option>
+ <option value="nonSensitiveOnly">{{ i18n.ts.nonSensitiveOnly }}</option>
+ <option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option>
+ <option value="likeOnly">{{ i18n.ts.likeOnly }}</option>
+ </MkSelect>
+ </SearchMarker>
+
+ <SearchMarker>
+ <MkFolder>
+ <template #label><SearchLabel>{{ i18n.ts.advancedSettings }}</SearchLabel></template>
+
+ <div class="_gaps_m">
+ <SearchMarker :keywords="['cat']">
+ <MkSwitch v-model="profile.isCat">
+ <template #label><SearchLabel>{{ i18n.ts.flagAsCat }}</SearchLabel></template>
+ <template #caption>{{ i18n.ts.flagAsCatDescription }}</template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker :keywords="['bot']">
+ <MkSwitch v-model="profile.isBot">
+ <template #label><SearchLabel>{{ i18n.ts.flagAsBot }}</SearchLabel></template>
+ <template #caption>{{ i18n.ts.flagAsBotDescription }}</template>
+ </MkSwitch>
+ </SearchMarker>
+ </div>
+ </MkFolder>
+ </SearchMarker>
+ </div>
+</SearchMarker>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index 8f9d4f858b..f365146e0a 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -4,39 +4,48 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="_gaps_m">
- <FormSection first>
- <template #label>{{ i18n.ts.password }}</template>
- <MkButton primary @click="change()">{{ i18n.ts.changePassword }}</MkButton>
- </FormSection>
+<SearchMarker path="/settings/security" :label="i18n.ts.security" :keywords="['security']" icon="ti ti-lock" :inlining="['2fa']">
+ <div class="_gaps_m">
+ <SearchMarker :keywords="['password']">
+ <FormSection first>
+ <template #label><SearchLabel>{{ i18n.ts.password }}</SearchLabel></template>
- <X2fa/>
+ <SearchMarker>
+ <MkButton primary @click="change()">
+ <SearchLabel>{{ i18n.ts.changePassword }}</SearchLabel>
+ </MkButton>
+ </SearchMarker>
+ </FormSection>
+ </SearchMarker>
- <FormSection>
- <template #label>{{ i18n.ts.signinHistory }}</template>
- <MkPagination :pagination="pagination" disableAutoLoad>
- <template #default="{items}">
- <div>
- <div v-for="item in items" :key="item.id" v-panel class="timnmucd">
- <header>
- <i v-if="item.success" class="ti ti-check icon succ"></i>
- <i v-else class="ti ti-circle-x icon fail"></i>
- <code class="ip _monospace">{{ item.ip }}</code>
- <MkTime :time="item.createdAt" class="time"/>
- </header>
+ <X2fa/>
+
+ <FormSection>
+ <template #label>{{ i18n.ts.signinHistory }}</template>
+ <MkPagination :pagination="pagination" disableAutoLoad>
+ <template #default="{items}">
+ <div>
+ <div v-for="item in items" :key="item.id" v-panel class="timnmucd">
+ <header>
+ <i v-if="item.success" class="ti ti-check icon succ"></i>
+ <i v-else class="ti ti-circle-x icon fail"></i>
+ <code class="ip _monospace">{{ item.ip }}</code>
+ <MkTime :time="item.createdAt" class="time"/>
+ </header>
+ </div>
</div>
- </div>
- </template>
- </MkPagination>
- </FormSection>
+ </template>
+ </MkPagination>
+ </FormSection>
- <FormSection>
- <FormSlot>
- <MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> {{ i18n.ts.regenerateLoginToken }}</MkButton>
- <template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
- </FormSlot>
- </FormSection>
-</div>
+ <FormSection>
+ <FormSlot>
+ <MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> {{ i18n.ts.regenerateLoginToken }}</MkButton>
+ <template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
+ </FormSlot>
+ </FormSection>
+ </div>
+</SearchMarker>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index bf461f173b..1df2d89277 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -4,43 +4,53 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="_gaps_m">
- <MkSwitch v-model="notUseSound">
- <template #label>{{ i18n.ts.notUseSound }}</template>
- </MkSwitch>
- <MkSwitch v-model="useSoundOnlyWhenActive">
- <template #label>{{ i18n.ts.useSoundOnlyWhenActive }}</template>
- </MkSwitch>
- <MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
- <template #label>{{ i18n.ts.masterVolume }}</template>
- </MkRange>
+<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>
+ </SearchMarker>
- <FormSection>
- <template #label>{{ i18n.ts.sounds }}</template>
- <div class="_gaps_s">
- <MkFolder v-for="type in operationTypes" :key="type">
- <template #label>{{ i18n.ts._sfx[type] }}</template>
- <template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template>
- <Suspense>
- <template #default>
- <XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/>
- </template>
- <template #fallback>
- <MkLoading/>
- </template>
- </Suspense>
- </MkFolder>
- </div>
- </FormSection>
+ <SearchMarker :keywords="['active', 'mute']">
+ <MkSwitch v-model="useSoundOnlyWhenActive">
+ <template #label><SearchLabel>{{ i18n.ts.useSoundOnlyWhenActive }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
- <MkButton danger @click="reset()"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
-</div>
+ <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>
+ </SearchMarker>
+
+ <FormSection>
+ <template #label>{{ i18n.ts.sounds }}</template>
+ <div class="_gaps_s">
+ <MkFolder v-for="type in operationTypes" :key="type">
+ <template #label>{{ i18n.ts._sfx[type] }}</template>
+ <template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template>
+ <Suspense>
+ <template #default>
+ <XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/>
+ </template>
+ <template #fallback>
+ <MkLoading/>
+ </template>
+ </Suspense>
+ </MkFolder>
+ </div>
+ </FormSection>
+
+ <MkButton danger @click="reset()"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
+ </div>
+</SearchMarker>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
-import type { 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 MkRange from '@/components/MkRange.vue';
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index fcf5b3cd9b..b0e4ce13d5 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -4,56 +4,72 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="_gaps_m rsljpzjq">
- <div v-adaptive-border class="rfqxtzch _panel">
- <div class="toggle">
- <div class="toggleWrapper">
- <input id="dn" v-model="darkMode" type="checkbox" class="dn"/>
- <label for="dn" class="toggle">
- <span class="before">{{ i18n.ts.light }}</span>
- <span class="after">{{ i18n.ts.dark }}</span>
- <span class="toggle__handler">
- <span class="crater crater--1"></span>
- <span class="crater crater--2"></span>
- <span class="crater crater--3"></span>
- </span>
- <span class="star star--1"></span>
- <span class="star star--2"></span>
- <span class="star star--3"></span>
- <span class="star star--4"></span>
- <span class="star star--5"></span>
- <span class="star star--6"></span>
- </label>
+<SearchMarker path="/settings/theme" :label="i18n.ts.theme" :keywords="['theme']" icon="ti ti-palette">
+ <div class="_gaps_m rsljpzjq">
+ <div v-adaptive-border class="rfqxtzch _panel">
+ <div class="toggle">
+ <div class="toggleWrapper">
+ <input id="dn" v-model="darkMode" type="checkbox" class="dn"/>
+ <label for="dn" class="toggle">
+ <span class="before">{{ i18n.ts.light }}</span>
+ <span class="after">{{ i18n.ts.dark }}</span>
+ <span class="toggle__handler">
+ <span class="crater crater--1"></span>
+ <span class="crater crater--2"></span>
+ <span class="crater crater--3"></span>
+ </span>
+ <span class="star star--1"></span>
+ <span class="star star--2"></span>
+ <span class="star star--3"></span>
+ <span class="star star--4"></span>
+ <span class="star star--5"></span>
+ <span class="star star--6"></span>
+ </label>
+ </div>
+ </div>
+ <div class="sync">
+ <SearchMarker :keywords="['sync', 'device', 'dark', 'light', 'mode']">
+ <MkSwitch v-model="syncDeviceDarkMode">
+ <template #label><SearchLabel>{{ i18n.ts.syncDeviceDarkMode }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
</div>
</div>
- <div class="sync">
- <MkSwitch v-model="syncDeviceDarkMode">{{ i18n.ts.syncDeviceDarkMode }}</MkSwitch>
- </div>
- </div>
-
- <div class="selects">
- <MkSelect v-model="lightThemeId" large class="select" :items="lightThemeSelectorItems">
- <template #label>{{ i18n.ts.themeForLightMode }}</template>
- <template #prefix><i class="ti ti-sun"></i></template>
- </MkSelect>
- <MkSelect v-model="darkThemeId" large class="select" :items="darkThemeSelectorItems">
- <template #label>{{ i18n.ts.themeForDarkMode }}</template>
- <template #prefix><i class="ti ti-moon"></i></template>
- </MkSelect>
- </div>
- <FormSection>
- <div class="_formLinksGrid">
- <FormLink to="/settings/theme/manage"><template #icon><i class="ti ti-tool"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
- <FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="ti ti-world"></i></template>{{ i18n.ts._theme.explore }}</FormLink>
- <FormLink to="/settings/theme/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._theme.install }}</FormLink>
- <FormLink to="/theme-editor"><template #icon><i class="ti ti-paint"></i></template>{{ i18n.ts._theme.make }}</FormLink>
+ <div class="selects">
+ <div class="select">
+ <SearchMarker :keywords="['light', 'theme']">
+ <MkSelect v-model="lightThemeId" large :items="lightThemeSelectorItems">
+ <template #label><SearchLabel>{{ i18n.ts.themeForLightMode }}</SearchLabel></template>
+ <template #prefix><i class="ti ti-sun"></i></template>
+ </MkSelect>
+ </SearchMarker>
+ </div>
+ <div class="select">
+ <SearchMarker :keywords="['dark', 'theme']">
+ <MkSelect v-model="darkThemeId" large :items="darkThemeSelectorItems">
+ <template #label><SearchLabel>{{ i18n.ts.themeForDarkMode }}</SearchLabel></template>
+ <template #prefix><i class="ti ti-moon"></i></template>
+ </MkSelect>
+ </SearchMarker>
+ </div>
</div>
- </FormSection>
- <MkButton v-if="wallpaper == null" @click="setWallpaper">{{ i18n.ts.setWallpaper }}</MkButton>
- <MkButton v-else @click="wallpaper = null">{{ i18n.ts.removeWallpaper }}</MkButton>
-</div>
+ <FormSection>
+ <div class="_formLinksGrid">
+ <FormLink to="/settings/theme/manage"><template #icon><i class="ti ti-tool"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
+ <FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="ti ti-world"></i></template>{{ i18n.ts._theme.explore }}</FormLink>
+ <FormLink to="/settings/theme/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._theme.install }}</FormLink>
+ <FormLink to="/theme-editor"><template #icon><i class="ti ti-paint"></i></template>{{ i18n.ts._theme.make }}</FormLink>
+ </div>
+ </FormSection>
+
+ <SearchMarker :keywords="['wallpaper']">
+ <MkButton v-if="wallpaper == null" @click="setWallpaper"><SearchLabel>{{ i18n.ts.setWallpaper }}</SearchLabel></MkButton>
+ <MkButton v-else @click="wallpaper = null">{{ i18n.ts.removeWallpaper }}</MkButton>
+ </SearchMarker>
+ </div>
+</SearchMarker>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index d2a4484c45..c6ee128f5f 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -90,9 +90,9 @@ const routes: RouteDef[] = [{
name: 'security',
component: page(() => import('@/pages/settings/security.vue')),
}, {
- path: '/general',
- name: 'general',
- component: page(() => import('@/pages/settings/general.vue')),
+ path: '/preferences',
+ name: 'preferences',
+ component: page(() => import('@/pages/settings/preferences.vue')),
}, {
path: '/theme/install',
name: 'theme',
@@ -106,6 +106,10 @@ const routes: RouteDef[] = [{
name: 'theme',
component: page(() => import('@/pages/settings/theme.vue')),
}, {
+ path: '/appearance',
+ name: 'appearance',
+ component: page(() => import('@/pages/settings/appearance.vue')),
+ }, {
path: '/navbar',
name: 'navbar',
component: page(() => import('@/pages/settings/navbar.vue')),
@@ -118,6 +122,10 @@ const routes: RouteDef[] = [{
name: 'sounds',
component: page(() => import('@/pages/settings/sounds.vue')),
}, {
+ path: '/accessibility',
+ name: 'accessibility',
+ component: page(() => import('@/pages/settings/accessibility.vue')),
+ }, {
path: '/plugin/install',
name: 'plugin',
component: page(() => import('@/pages/settings/plugin.install.vue')),
@@ -162,12 +170,8 @@ const routes: RouteDef[] = [{
name: 'preferences-backups',
component: page(() => import('@/pages/settings/preferences-backups.vue')),
}, {
- path: '/migration',
- name: 'migration',
- component: page(() => import('@/pages/settings/migration.vue')),
- }, {
path: '/custom-css',
- name: 'general',
+ name: 'preferences',
component: page(() => import('@/pages/settings/custom-css.vue')),
}, {
path: '/accounts',
diff --git a/packages/frontend/src/scripts/autogen/settings-search-index.ts b/packages/frontend/src/scripts/autogen/settings-search-index.ts
new file mode 100644
index 0000000000..c62272b271
--- /dev/null
+++ b/packages/frontend/src/scripts/autogen/settings-search-index.ts
@@ -0,0 +1,815 @@
+
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// This file was automatically generated by create-search-index.
+// Do not edit this file.
+
+import { i18n } from '@/i18n.js';
+
+export type SearchIndexItem = {
+ id: string;
+ path?: string;
+ label: string;
+ keywords: string[];
+ icon?: string;
+ children?: SearchIndexItem[];
+};
+
+export const searchIndexes: SearchIndexItem[] = [
+ {
+ id: 'flXd1LC7r',
+ children: [
+ {
+ id: 'hB11H5oul',
+ label: i18n.ts.syncDeviceDarkMode,
+ keywords: ['sync', 'device', 'dark', 'light', 'mode'],
+ },
+ {
+ id: 'fDbLtIKeo',
+ label: i18n.ts.themeForLightMode,
+ keywords: ['light', 'theme'],
+ },
+ {
+ id: 'eLOwK5Ia2',
+ label: i18n.ts.themeForDarkMode,
+ keywords: ['dark', 'theme'],
+ },
+ {
+ id: 'ujvMfyzUr',
+ label: i18n.ts.setWallpaper,
+ keywords: ['wallpaper'],
+ },
+ ],
+ label: i18n.ts.theme,
+ keywords: ['theme'],
+ path: '/settings/theme',
+ icon: 'ti ti-palette',
+ },
+ {
+ id: '6fFIRXUww',
+ children: [
+ {
+ id: 'nO7NnzqiC',
+ label: i18n.ts.notUseSound,
+ keywords: ['mute'],
+ },
+ {
+ id: 'xy5OOBB4A',
+ label: i18n.ts.useSoundOnlyWhenActive,
+ keywords: ['active', 'mute'],
+ },
+ {
+ id: '9MxYVIf7k',
+ label: i18n.ts.masterVolume,
+ keywords: ['volume', 'master'],
+ },
+ ],
+ label: i18n.ts.sounds,
+ keywords: ['sounds'],
+ path: '/settings/sounds',
+ icon: 'ti ti-music',
+ },
+ {
+ id: '5BjnxMfYV',
+ children: [
+ {
+ id: '3UqdSCaFw',
+ children: [
+ {
+ id: '75QPEg57v',
+ label: i18n.ts.changePassword,
+ keywords: [],
+ },
+ ],
+ label: i18n.ts.password,
+ keywords: ['password'],
+ },
+ {
+ id: '2fa',
+ children: [
+ {
+ id: 'qCXM0HtJ7',
+ label: i18n.ts.totp,
+ keywords: ['totp', 'app', i18n.ts.totpDescription],
+ },
+ {
+ id: '3g1RePuD9',
+ label: i18n.ts.securityKeyAndPasskey,
+ keywords: ['security', 'key', 'passkey'],
+ },
+ {
+ id: 'pFRud5u8k',
+ label: i18n.ts.passwordLessLogin,
+ keywords: ['password', 'less', 'key', 'passkey', 'login', 'signin', i18n.ts.passwordLessLoginDescription],
+ },
+ ],
+ label: i18n.ts['2fa'],
+ keywords: ['2fa'],
+ },
+ ],
+ label: i18n.ts.security,
+ keywords: ['security'],
+ path: '/settings/security',
+ icon: 'ti ti-lock',
+ },
+ {
+ id: 'w4L6myH61',
+ children: [
+ {
+ id: 'ru8DrOn3J',
+ label: i18n.ts._profile.changeBanner,
+ keywords: ['banner', 'change'],
+ },
+ {
+ id: 'CCnD8Apnu',
+ label: i18n.ts._profile.changeAvatar,
+ keywords: ['avatar', 'icon', 'change'],
+ },
+ {
+ id: 'yFEVCJxFX',
+ label: i18n.ts._profile.name,
+ keywords: ['name'],
+ },
+ {
+ id: '2O1S5reaB',
+ label: i18n.ts._profile.description,
+ keywords: ['description', 'bio'],
+ },
+ {
+ id: 'pWi4OLS8g',
+ label: i18n.ts.location,
+ keywords: ['location', 'locale'],
+ },
+ {
+ id: 'oLO5X6Wtw',
+ label: i18n.ts.birthday,
+ keywords: ['birthday', 'birthdate', 'age'],
+ },
+ {
+ id: 'm2trKwPgq',
+ label: i18n.ts.language,
+ keywords: ['language', 'locale'],
+ },
+ {
+ id: 'kfDZxCDp9',
+ label: i18n.ts._profile.metadataEdit,
+ keywords: ['metadata'],
+ },
+ {
+ id: 'uPt3MFymp',
+ label: i18n.ts._profile.followedMessage,
+ keywords: ['follow', 'message', i18n.ts._profile.followedMessageDescription],
+ },
+ {
+ id: 'wuGg0tBjw',
+ label: i18n.ts.reactionAcceptance,
+ keywords: ['reaction'],
+ },
+ {
+ id: 'EezPpmMnf',
+ children: [
+ {
+ id: 'f2cRLh8ad',
+ label: i18n.ts.flagAsCat,
+ keywords: ['cat'],
+ },
+ {
+ id: 'eVoViiF3h',
+ label: i18n.ts.flagAsBot,
+ keywords: ['bot'],
+ },
+ ],
+ label: i18n.ts.advancedSettings,
+ keywords: [],
+ },
+ ],
+ label: i18n.ts.profile,
+ keywords: ['profile'],
+ path: '/settings/profile',
+ icon: 'ti ti-user',
+ },
+ {
+ id: '2rp9ka5Ht',
+ children: [
+ {
+ id: 'qBUSKPxLW',
+ label: i18n.ts.makeFollowManuallyApprove,
+ keywords: ['follow', 'lock', i18n.ts.lockedAccountInfo],
+ },
+ {
+ id: '3LZBlZCej',
+ label: i18n.ts.autoAcceptFollowed,
+ keywords: ['follow', 'auto', 'accept'],
+ },
+ {
+ id: '9gOp28wKG',
+ label: i18n.ts.makeReactionsPublic,
+ keywords: ['reaction', 'public', i18n.ts.makeReactionsPublicDescription],
+ },
+ {
+ id: 'CjAkqMhct',
+ label: i18n.ts.followingVisibility,
+ keywords: ['following', 'visibility'],
+ },
+ {
+ id: '4nEwI6LYt',
+ label: i18n.ts.followersVisibility,
+ keywords: ['follower', 'visibility'],
+ },
+ {
+ id: 'naMp37wTL',
+ label: i18n.ts.hideOnlineStatus,
+ keywords: ['online', 'status', i18n.ts.hideOnlineStatusDescription],
+ },
+ {
+ id: 'p0dCVR0UP',
+ label: i18n.ts.noCrawle,
+ keywords: ['crawle', 'index', 'search', i18n.ts.noCrawleDescription],
+ },
+ {
+ id: 'aceURmNPq',
+ label: i18n.ts.preventAiLearning,
+ keywords: ['crawle', 'ai', i18n.ts.preventAiLearningDescription],
+ },
+ {
+ id: 'ahABA0j7u',
+ label: i18n.ts.makeExplorable,
+ keywords: ['explore', i18n.ts.makeExplorableDescription],
+ },
+ {
+ id: 'cyeDbLN8N',
+ children: [
+ {
+ id: 'xEYlOghao',
+ label: i18n.ts._accountSettings.requireSigninToViewContents,
+ keywords: ['login', 'signin'],
+ },
+ {
+ id: 'sMmYFCS60',
+ label: i18n.ts._accountSettings.makeNotesFollowersOnlyBefore,
+ keywords: ['follower', i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription],
+ },
+ {
+ id: '2prkeWRSd',
+ label: i18n.ts._accountSettings.makeNotesHiddenBefore,
+ keywords: ['hidden', i18n.ts._accountSettings.makeNotesHiddenBeforeDescription],
+ },
+ ],
+ label: i18n.ts.lockdown,
+ keywords: ['lockdown'],
+ },
+ {
+ id: '37QLEyrtk',
+ label: i18n.ts.rememberNoteVisibility,
+ keywords: ['remember', 'keep', 'note', 'visibility'],
+ },
+ {
+ id: 'rhKwScbVS',
+ label: i18n.ts.defaultNoteVisibility,
+ keywords: ['default', 'note', 'visibility'],
+ },
+ {
+ id: '3EmXVyevo',
+ label: i18n.ts.keepCw,
+ keywords: ['remember', 'keep', 'note', 'cw'],
+ },
+ ],
+ label: i18n.ts.privacy,
+ keywords: ['privacy'],
+ path: '/settings/privacy',
+ icon: 'ti ti-lock-open',
+ },
+ {
+ id: '3yCAv0IsZ',
+ children: [
+ {
+ id: 'x1GWSQnPw',
+ label: i18n.ts.uiLanguage,
+ keywords: ['language'],
+ },
+ {
+ id: 'EOSa4rtt3',
+ label: i18n.ts.overridedDeviceKind,
+ keywords: ['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop'],
+ },
+ {
+ id: 'm9LhX8BG8',
+ label: i18n.ts.showFixedPostForm,
+ keywords: ['post', 'form', 'timeline'],
+ },
+ {
+ id: '9ra14w32V',
+ label: i18n.ts.showFixedPostFormInChannel,
+ keywords: ['post', 'form', 'timeline', 'channel'],
+ },
+ {
+ id: '84MdeDWL1',
+ label: i18n.ts.pinnedList,
+ keywords: ['pinned', 'list'],
+ },
+ {
+ id: 'fYdWhBbrN',
+ label: i18n.ts.enableQuickAddMfmFunction,
+ keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'],
+ },
+ {
+ id: '4huRldNp5',
+ children: [
+ {
+ id: 'puIqj1a8b',
+ label: i18n.ts.collapseRenotes,
+ keywords: ['renote', i18n.ts.collapseRenotesDescription],
+ },
+ {
+ id: 'wqpOC22Zm',
+ label: i18n.ts.showNoteActionsOnlyHover,
+ keywords: ['hover', 'show', 'footer', 'action'],
+ },
+ {
+ id: 'cjfAtxMzP',
+ label: i18n.ts.showClipButtonInNoteFooter,
+ keywords: ['footer', 'action', 'clip', 'show'],
+ },
+ {
+ id: 'khzxoCjtp',
+ label: i18n.ts.enableAdvancedMfm,
+ keywords: ['mfm', 'enable', 'show', 'advanced'],
+ },
+ {
+ id: 'uJkoVjTmF',
+ label: i18n.ts.showReactionsCount,
+ keywords: ['reaction', 'count', 'show'],
+ },
+ {
+ id: '9gTCaLkIf',
+ label: i18n.ts.loadRawImages,
+ keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'],
+ },
+ ],
+ label: i18n.ts.note,
+ keywords: ['note'],
+ },
+ {
+ id: '5G6O6qdis',
+ children: [
+ {
+ id: 'sYTvqUbhP',
+ label: i18n.ts.useGroupedNotifications,
+ keywords: ['group'],
+ },
+ ],
+ label: i18n.ts.notifications,
+ keywords: ['notification'],
+ },
+ {
+ id: 'c3xhLyXZ5',
+ children: [
+ {
+ id: 'FbhoeuRAD',
+ label: i18n.ts.openImageInNewTab,
+ keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'],
+ },
+ {
+ id: 'qixh85g2N',
+ label: i18n.ts.useReactionPickerForContextMenu,
+ keywords: ['reaction', 'picker', 'contextmenu', 'open'],
+ },
+ {
+ id: 'd2H4E5ys6',
+ label: i18n.ts.enableInfiniteScroll,
+ keywords: ['load', 'auto', 'more'],
+ },
+ {
+ id: 'jC7LtTnmc',
+ label: i18n.ts.disableStreamingTimeline,
+ keywords: ['disable', 'streaming', 'timeline'],
+ },
+ {
+ id: '8xazEqlgZ',
+ label: i18n.ts.alwaysConfirmFollow,
+ keywords: ['follow', 'confirm', 'always'],
+ },
+ {
+ id: 'wZqrDQZar',
+ label: i18n.ts.confirmWhenRevealingSensitiveMedia,
+ keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'],
+ },
+ {
+ id: '5QTUzrpT3',
+ label: i18n.ts.confirmOnReact,
+ keywords: ['reaction', 'confirm'],
+ },
+ {
+ id: 'nygexkaUk',
+ label: i18n.ts.whenServerDisconnected,
+ keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'],
+ },
+ {
+ id: 'whKYKvaQB',
+ label: i18n.ts.numberOfPageCache,
+ keywords: ['cache', 'page'],
+ },
+ {
+ id: 'lBbtAg0Hm',
+ label: i18n.ts.dataSaver,
+ keywords: ['datasaver'],
+ },
+ ],
+ label: i18n.ts.behavior,
+ keywords: ['behavior'],
+ },
+ {
+ id: 'y2v7CV9zs',
+ children: [
+ {
+ id: 'k1qTdyfzM',
+ label: i18n.ts.forceShowAds,
+ keywords: ['ad', 'show'],
+ },
+ {
+ id: 'e9As4Us48',
+ label: i18n.ts.hemisphere,
+ keywords: [],
+ },
+ {
+ id: 'zvM13vl26',
+ label: i18n.ts.additionalEmojiDictionary,
+ keywords: ['emoji', 'dictionary', 'additional', 'extra'],
+ },
+ ],
+ label: i18n.ts.other,
+ keywords: [],
+ },
+ ],
+ label: i18n.ts.preferences,
+ keywords: ['general', 'preferences'],
+ path: '/settings/preferences',
+ icon: 'ti ti-adjustments',
+ },
+ {
+ id: 'F1uK9ssiY',
+ children: [
+ {
+ id: 'msAcN6u3S',
+ label: i18n.ts.accountInfo,
+ keywords: ['account', 'info'],
+ },
+ {
+ id: 'ts8DgdnZV',
+ label: i18n.ts.accountMigration,
+ keywords: ['account', 'move', 'migration'],
+ },
+ {
+ id: '4BG7nBECm',
+ label: i18n.ts.closeAccount,
+ keywords: ['account', 'close', 'delete', i18n.ts._accountDelete.requestAccountDelete],
+ },
+ {
+ id: '2qI6ruPgi',
+ label: i18n.ts.experimentalFeatures,
+ keywords: ['experimental', 'feature', 'flags'],
+ },
+ {
+ id: 'cIeaax47o',
+ label: i18n.ts.developer,
+ keywords: ['developer', 'mode', 'debug'],
+ },
+ ],
+ label: i18n.ts.other,
+ keywords: ['other'],
+ path: '/settings/other',
+ icon: 'ti ti-dots',
+ },
+ {
+ id: '3icEvyv2D',
+ children: [
+ {
+ id: 'Tyt3gZTy',
+ children: [
+ {
+ id: '9b7ZURyAt',
+ label: i18n.ts.showMutedWord,
+ keywords: ['show'],
+ },
+ ],
+ label: i18n.ts.wordMute,
+ keywords: ['note', 'word', 'soft', 'mute', 'hide'],
+ },
+ {
+ id: 'kdMk41II0',
+ label: i18n.ts.hardWordMute,
+ keywords: ['note', 'word', 'hard', 'mute', 'hide'],
+ },
+ {
+ id: 'mjORQamAK',
+ label: i18n.ts.instanceMute,
+ keywords: ['note', 'server', 'instance', 'host', 'federation', 'mute', 'hide'],
+ },
+ {
+ id: '1ZT7S9FZd',
+ label: `${i18n.ts.mutedUsers} (${ i18n.ts.renote })`,
+ keywords: ['renote', 'mute', 'hide', 'user'],
+ },
+ {
+ id: 'ANrPit3kQ',
+ label: i18n.ts.mutedUsers,
+ keywords: ['note', 'mute', 'hide', 'user'],
+ },
+ {
+ id: 'bPAE4lfno',
+ label: i18n.ts.blockedUsers,
+ keywords: ['block', 'user'],
+ },
+ ],
+ label: i18n.ts.muteAndBlock,
+ keywords: ['mute', 'block'],
+ path: '/settings/mute-block',
+ icon: 'ti ti-ban',
+ },
+ {
+ id: 'qE2vLlMkF',
+ children: [
+ {
+ id: 'hPPEzjvZC',
+ label: i18n.ts._exportOrImport.allNotes,
+ keywords: ['notes'],
+ },
+ {
+ id: 'AFaeHsCUB',
+ label: i18n.ts._exportOrImport.favoritedNotes,
+ keywords: ['favorite', 'notes'],
+ },
+ {
+ id: 'xyCPmQiRo',
+ label: i18n.ts._exportOrImport.clips,
+ keywords: ['clip', 'notes'],
+ },
+ {
+ id: 'Ch7hWAGUy',
+ label: i18n.ts._exportOrImport.followingList,
+ keywords: ['following', 'users'],
+ },
+ {
+ id: 'AwPgFboEx',
+ label: i18n.ts._exportOrImport.userLists,
+ keywords: ['user', 'lists'],
+ },
+ {
+ id: 'nporiHshC',
+ label: i18n.ts._exportOrImport.muteList,
+ keywords: ['mute', 'users'],
+ },
+ {
+ id: 'BsCzR7vNw',
+ label: i18n.ts._exportOrImport.blockingList,
+ keywords: ['block', 'users'],
+ },
+ {
+ id: 'dvf4IgYrQ',
+ label: i18n.ts.antennas,
+ keywords: ['antennas'],
+ },
+ ],
+ label: i18n.ts.importAndExport,
+ keywords: ['import', 'export', 'data'],
+ path: '/settings/import-export',
+ icon: 'ti ti-package',
+ },
+ {
+ id: '3Tcxw4Fwl',
+ children: [
+ {
+ id: 'iIai9O65I',
+ label: i18n.ts.emailAddress,
+ keywords: ['email', 'address'],
+ },
+ {
+ id: 'i6cC6oi0m',
+ label: i18n.ts.receiveAnnouncementFromInstance,
+ keywords: ['announcement', 'email'],
+ },
+ {
+ id: 'C1YTinP11',
+ label: i18n.ts.emailNotification,
+ keywords: ['notification', 'email'],
+ },
+ ],
+ label: i18n.ts.email,
+ keywords: ['email'],
+ path: '/settings/email',
+ icon: 'ti ti-mail',
+ },
+ {
+ id: 'tnYoppRiv',
+ children: [
+ {
+ id: 'ncIq6TAR2',
+ label: i18n.ts.usageAmount,
+ keywords: ['capacity', 'usage'],
+ },
+ {
+ id: '2c4CQSvSr',
+ label: i18n.ts.statistics,
+ keywords: ['statistics', 'usage'],
+ },
+ {
+ id: 'pepHELHMt',
+ label: i18n.ts.uploadFolder,
+ keywords: ['default', 'upload', 'folder'],
+ },
+ {
+ id: 'xqOWrABxV',
+ label: i18n.ts.keepOriginalUploading,
+ keywords: ['keep', 'original', 'raw', 'upload', i18n.ts.keepOriginalUploadingDescription],
+ },
+ {
+ id: 'oqUiI5w0s',
+ label: i18n.ts.keepOriginalFilename,
+ keywords: ['keep', 'original', 'filename', i18n.ts.keepOriginalFilenameDescription],
+ },
+ {
+ id: 'Aszkikq9n',
+ label: i18n.ts.alwaysMarkSensitive,
+ keywords: ['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file'],
+ },
+ {
+ id: 'iGlVjsfVj',
+ label: i18n.ts.enableAutoSensitive,
+ keywords: ['auto', 'nsfw', 'sensitive', 'media', 'file', i18n.ts.enableAutoSensitiveDescription],
+ },
+ ],
+ label: i18n.ts.drive,
+ keywords: ['drive'],
+ path: '/settings/drive',
+ icon: 'ti ti-cloud',
+ },
+ {
+ id: 'gtaOSdIJB',
+ label: i18n.ts.avatarDecorations,
+ keywords: ['avatar', 'icon', 'decoration'],
+ path: '/settings/avatar-decoration',
+ icon: 'ti ti-sparkles',
+ },
+ {
+ id: 'AqPvMgn3A',
+ children: [
+ {
+ id: 'j5gTtuMWP',
+ label: i18n.ts.useBlurEffect,
+ keywords: ['blur'],
+ },
+ {
+ id: 'vbZvyLDC1',
+ label: i18n.ts.useBlurEffectForModal,
+ keywords: ['blur', 'modal'],
+ },
+ {
+ id: '6fLNMTwNt',
+ label: i18n.ts.highlightSensitiveMedia,
+ keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'],
+ },
+ {
+ id: 'hhvF8Z4pF',
+ label: i18n.ts.squareAvatars,
+ keywords: ['avatar', 'icon', 'square'],
+ },
+ {
+ id: 'DsS2CwjYE',
+ label: i18n.ts.showAvatarDecorations,
+ keywords: ['avatar', 'icon', 'decoration', 'show'],
+ },
+ {
+ id: 'pWZ0ypy2g',
+ label: i18n.ts.showGapBetweenNotesInTimeline,
+ keywords: ['note', 'timeline', 'gap'],
+ },
+ {
+ id: 'AfRMcC6IM',
+ label: i18n.ts.useSystemFont,
+ keywords: ['font', 'system', 'native'],
+ },
+ {
+ id: 'jD0qbxlzN',
+ label: i18n.ts.seasonalScreenEffect,
+ keywords: ['effect', 'show'],
+ },
+ {
+ id: 'EdYo3hOK',
+ label: i18n.ts.menuStyle,
+ keywords: ['menu', 'style', 'popup', 'drawer'],
+ },
+ {
+ id: '9mSlX0EkD',
+ label: i18n.ts.emojiStyle,
+ keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'],
+ },
+ {
+ id: '44UmMwmUe',
+ label: i18n.ts.fontSize,
+ keywords: ['font', 'size'],
+ },
+ {
+ id: 'vFB0pLzck',
+ children: [
+ {
+ id: 'pc7IpPEU4',
+ label: i18n.ts.reactionsDisplaySize,
+ keywords: ['reaction', 'size', 'scale', 'display'],
+ },
+ {
+ id: 'siOW5aSwp',
+ label: i18n.ts.limitWidthOfReaction,
+ keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'],
+ },
+ {
+ id: 'dDUvhk13F',
+ label: i18n.ts.mediaListWithOneImageAppearance,
+ keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'],
+ },
+ {
+ id: 'CLxNL1Rp0',
+ label: i18n.ts.instanceTicker,
+ keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'],
+ },
+ {
+ id: 'dP2KWDYzD',
+ label: i18n.ts.displayOfSensitiveMedia,
+ keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'],
+ },
+ ],
+ label: i18n.ts.displayOfNote,
+ keywords: ['note', 'display'],
+ },
+ {
+ id: 'dVOzi22IW',
+ children: [
+ {
+ id: 'aoF4ufUwn',
+ label: i18n.ts.position,
+ keywords: ['position'],
+ },
+ {
+ id: 'sKK2XSS69',
+ label: i18n.ts.stackAxis,
+ keywords: ['stack', 'axis', 'direction'],
+ },
+ ],
+ label: i18n.ts.notificationDisplay,
+ keywords: ['notification', 'display'],
+ },
+ ],
+ label: i18n.ts.appearance,
+ keywords: ['appearance'],
+ path: '/settings/appearance',
+ icon: 'ti ti-device-desktop',
+ },
+ {
+ id: 'f08Mi1Uwn',
+ children: [
+ {
+ id: '7ov7ceoij',
+ label: i18n.ts.reduceUiAnimation,
+ keywords: ['animation', 'motion', 'reduce'],
+ },
+ {
+ id: 'RhYwm8At',
+ label: i18n.ts.disableShowingAnimatedImages,
+ keywords: ['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif'],
+ },
+ {
+ id: '5mZxz2cru',
+ label: i18n.ts.enableAnimatedMfm,
+ keywords: ['mfm', 'enable', 'show', 'animated'],
+ },
+ {
+ id: 'bgjamYEis',
+ label: i18n.ts.enableHorizontalSwipe,
+ keywords: ['swipe', 'horizontal', 'tab'],
+ },
+ {
+ id: 'yPEpJigqY',
+ label: i18n.ts.keepScreenOn,
+ keywords: ['keep', 'screen', 'display', 'on'],
+ },
+ {
+ id: 'oxwiGKMu0',
+ label: i18n.ts.useNativeUIForVideoAudioPlayer,
+ keywords: ['native', 'system', 'video', 'audio', 'player', 'media'],
+ },
+ {
+ id: 'n90tffyiU',
+ label: i18n.ts._contextMenu.title,
+ keywords: ['contextmenu', 'system', 'native'],
+ },
+ ],
+ label: i18n.ts.accessibility,
+ keywords: ['accessibility'],
+ path: '/settings/accessibility',
+ icon: 'ti ti-accessible',
+ },
+] as const;
+
+export type SearchIndex = typeof searchIndexes;
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 9724905e02..fec8666dc1 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -56,21 +56,35 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</div>
</div>
- <button v-if="!forceIconOnly" class="_button" :class="$style.toggleButton" @click="toggleIconOnly">
- <!--
- <svg viewBox="0 0 16 48" :class="$style.toggleButtonShape">
- <g transform="matrix(0.333333,0,0,0.222222,0.000895785,13.3333)">
- <path d="M23.935,-24C37.223,-24 47.995,-7.842 47.995,12.09C47.995,34.077 47.995,62.07 47.995,84.034C47.995,93.573 45.469,102.721 40.972,109.466C36.475,116.211 30.377,120 24.018,120L23.997,120C10.743,120 -0.003,136.118 -0.003,156C-0.003,156 -0.003,156 -0.003,156L-0.003,-60L-0.003,-59.901C-0.003,-50.379 2.519,-41.248 7.007,-34.515C11.496,-27.782 17.584,-24 23.931,-24C23.932,-24 23.934,-24 23.935,-24Z" style="fill:var(--MI_THEME-navBg);"/>
- </g>
- </svg>
- -->
- <svg viewBox="0 0 16 64" :class="$style.toggleButtonShape">
- <g transform="matrix(0.333333,0,0,0.222222,0.000895785,21.3333)">
- <path d="M47.488,7.995C47.79,10.11 47.943,12.266 47.943,14.429C47.997,26.989 47.997,84 47.997,84C47.997,84 44.018,118.246 23.997,133.5C-0.374,152.07 -0.003,192 -0.003,192L-0.003,-96C-0.003,-96 0.151,-56.216 23.997,-37.5C40.861,-24.265 46.043,-1.243 47.488,7.995Z" style="fill:var(--MI_THEME-navBg);"/>
- </g>
- </svg>
- <i :class="'ti ' + `ti-chevron-${ iconOnly ? 'right' : 'left' }`" style="font-size: 12px; margin-left: -8px;"></i>
- </button>
+
+ <!--
+ <svg viewBox="0 0 16 48" :class="$style.subButtonShape">
+ <g transform="matrix(0.333333,0,0,0.222222,0.000895785,13.3333)">
+ <path d="M23.935,-24C37.223,-24 47.995,-7.842 47.995,12.09C47.995,34.077 47.995,62.07 47.995,84.034C47.995,93.573 45.469,102.721 40.972,109.466C36.475,116.211 30.377,120 24.018,120L23.997,120C10.743,120 -0.003,136.118 -0.003,156C-0.003,156 -0.003,156 -0.003,156L-0.003,-60L-0.003,-59.901C-0.003,-50.379 2.519,-41.248 7.007,-34.515C11.496,-27.782 17.584,-24 23.931,-24C23.932,-24 23.934,-24 23.935,-24Z" style="fill:var(--MI_THEME-navBg);"/>
+ </g>
+ </svg>
+ -->
+
+ <div :class="$style.subButtons">
+ <div :class="[$style.subButton, $style.menuEditButton]">
+ <svg viewBox="0 0 16 64" :class="$style.subButtonShape">
+ <g transform="matrix(0.333333,0,0,0.222222,0.000895785,21.3333)">
+ <path d="M47.488,7.995C47.79,10.11 47.943,12.266 47.943,14.429C47.997,26.989 47.997,84 47.997,84C47.997,84 44.018,118.246 23.997,133.5C-0.374,152.07 -0.003,192 -0.003,192L-0.003,-96C-0.003,-96 0.151,-56.216 23.997,-37.5C40.861,-24.265 46.043,-1.243 47.488,7.995Z" style="fill:var(--MI_THEME-navBg);"/>
+ </g>
+ </svg>
+ <button class="_button" :class="$style.subButtonClickable" @click="menuEdit"><i :class="$style.subButtonIcon" class="ti ti-settings-2"></i></button>
+ </div>
+ <div v-if="!forceIconOnly" :class="$style.subButtonGapFill"></div>
+ <div v-if="!forceIconOnly" :class="$style.subButtonGapFillDivider"></div>
+ <div v-if="!forceIconOnly" :class="[$style.subButton, $style.toggleButton]">
+ <svg viewBox="0 0 16 64" :class="$style.subButtonShape">
+ <g transform="matrix(0.333333,0,0,0.222222,0.000895785,21.3333)">
+ <path d="M47.488,7.995C47.79,10.11 47.943,12.266 47.943,14.429C47.997,26.989 47.997,84 47.997,84C47.997,84 44.018,118.246 23.997,133.5C-0.374,152.07 -0.003,192 -0.003,192L-0.003,-96C-0.003,-96 0.151,-56.216 23.997,-37.5C40.861,-24.265 46.043,-1.243 47.488,7.995Z" style="fill:var(--MI_THEME-navBg);"/>
+ </g>
+ </svg>
+ <button class="_button" :class="$style.subButtonClickable" @click="toggleIconOnly"><i v-if="iconOnly" class="ti ti-chevron-right" :class="$style.subButtonIcon"></i><i v-else class="ti ti-chevron-left" :class="$style.subButtonIcon"></i></button>
+ </div>
+ </div>
</div>
</template>
@@ -84,6 +98,9 @@ import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
+import { useRouter } from '@/router/supplier.js';
+
+const router = useRouter();
const forceIconOnly = ref(window.innerWidth <= 1279);
const iconOnly = computed(() => {
@@ -128,6 +145,10 @@ function more(ev: MouseEvent) {
closed: () => dispose(),
});
}
+
+function menuEdit() {
+ router.push('/settings/navbar');
+}
</script>
<style lang="scss" module>
@@ -136,6 +157,8 @@ function more(ev: MouseEvent) {
--nav-icon-only-width: 80px;
--nav-bg-transparent: color(from var(--MI_THEME-navBg) srgb r g b / 0.5);
+ --subButtonWidth: 20px;
+
flex: 0 0 var(--nav-width);
width: var(--nav-width);
box-sizing: border-box;
@@ -171,23 +194,80 @@ function more(ev: MouseEvent) {
direction: ltr;
}
-.toggleButton {
+.subButtons {
position: fixed;
- bottom: 20px;
left: var(--nav-width);
+ bottom: 80px;
z-index: 1001;
- width: 16px;
- height: 64px;
box-sizing: border-box;
}
-.toggleButtonShape {
+.subButton {
+ display: block;
+ position: relative;
+ z-index: 1002;
+ width: var(--subButtonWidth);
+ height: 50px;
+ box-sizing: border-box;
+ align-content: center;
+}
+
+.subButtonShape {
position: absolute;
z-index: -1;
top: 0;
+ bottom: 0;
left: 0;
- width: 16px;
+ margin: auto;
+ width: var(--subButtonWidth);
+ height: calc(var(--subButtonWidth) * 4);
+}
+
+.subButtonClickable {
+ position: absolute;
+ display: block;
+ max-width: unset;
+ width: 24px;
+ height: 42px;
+ top: 0;
+ bottom: 0;
+ left: -4px;
+ margin: auto;
+ font-size: 10px;
+
+ &:hover {
+ color: var(--MI_THEME-fgHighlighted);
+
+ .subButtonIcon {
+ opacity: 1;
+ }
+ }
+}
+
+.subButtonIcon {
+ margin-left: -4px;
+ opacity: 0.7;
+}
+
+.subButtonGapFill {
+ position: relative;
+ z-index: 1001;
+ width: var(--subButtonWidth);
height: 64px;
+ margin-top: -32px;
+ margin-bottom: -32px;
+ pointer-events: none;
+ background: var(--MI_THEME-navBg);
+}
+
+.subButtonGapFillDivider {
+ position: relative;
+ z-index: 1010;
+ margin-left: -2px;
+ width: 14px;
+ height: 1px;
+ background: var(--MI_THEME-divider);
+ pointer-events: none;
}
.root:not(.iconOnly) {
@@ -419,7 +499,7 @@ function more(ev: MouseEvent) {
font-size: 0.9em;
}
- .toggleButton {
+ .subButtons {
left: var(--nav-width);
}
}
@@ -623,7 +703,7 @@ function more(ev: MouseEvent) {
}
}
- .toggleButton {
+ .subButtons {
left: var(--nav-icon-only-width);
}
}
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 9fff5efe51..25f47a2d55 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -96,12 +96,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef } from 'vue';
-import type { Ref } from 'vue';
import { instanceName } from '@@/js/config.js';
import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js';
import { isLink } from '@@/js/is-link.js';
import XCommon from './_common_/common.vue';
+import type { Ref } from 'vue';
import type MkStickyContainer from '@/components/global/MkStickyContainer.vue';
+import type { PageMetadata } from '@/scripts/page-metadata.js';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
@@ -109,7 +110,6 @@ import { navbarItemDef } from '@/navbar.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
import { deviceKind } from '@/scripts/device-kind.js';
import { miLocalStorage } from '@/local-storage.js';
import { useScrollPositionManager } from '@/nirax.js';
@@ -331,6 +331,8 @@ $widgets-hide-threshold: 1090px;
overflow-y: scroll;
overscroll-behavior: contain;
background: var(--MI_THEME-bg);
+ scroll-padding-top: 60px; // TODO: ちゃんと計算する
+ scroll-padding-bottom: 60px; // TODO: ちゃんと計算する
}
.widgets {