diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-03-06 23:15:19 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-03-06 23:15:19 +0900 |
| commit | 0214a0001fee2355a6d48da8ae5790c24650be33 (patch) | |
| tree | 64b8c3cd23dcfcdea9efc8b12ba583b949823149 /packages/frontend/src | |
| parent | [skip ci] Update CHANGELOG.md (prepend template) (diff) | |
| download | misskey-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')
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 { |