summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/frontend/lib/vite-plugin-create-search-index.ts36
-rw-r--r--packages/frontend/src/components/MkSuperMenu.vue32
-rw-r--r--packages/frontend/src/components/global/SearchText.vue14
-rw-r--r--packages/frontend/src/components/index.ts6
-rw-r--r--packages/frontend/src/pages/admin/bot-protection.vue280
-rw-r--r--packages/frontend/src/pages/admin/branding.vue211
-rw-r--r--packages/frontend/src/pages/admin/email-settings.vue115
-rw-r--r--packages/frontend/src/pages/admin/external-services.vue78
-rw-r--r--packages/frontend/src/pages/admin/index.vue11
-rw-r--r--packages/frontend/src/pages/admin/moderation.vue267
-rw-r--r--packages/frontend/src/pages/admin/object-storage.vue161
-rw-r--r--packages/frontend/src/pages/admin/performance.vue252
-rw-r--r--packages/frontend/src/pages/admin/relays.vue22
-rw-r--r--packages/frontend/src/pages/admin/security.vue222
-rw-r--r--packages/frontend/src/pages/admin/server-rules.vue23
-rw-r--r--packages/frontend/src/pages/admin/settings.vue563
-rw-r--r--packages/frontend/src/pages/admin/system-webhook.vue24
-rw-r--r--packages/frontend/src/pages/settings/2fa.vue4
-rw-r--r--packages/frontend/src/pages/settings/account-data.vue2
-rw-r--r--packages/frontend/src/pages/settings/connect.vue2
-rw-r--r--packages/frontend/src/pages/settings/drive.vue6
-rw-r--r--packages/frontend/src/pages/settings/index.vue6
-rw-r--r--packages/frontend/src/pages/settings/mute-block.vue2
-rw-r--r--packages/frontend/src/pages/settings/notifications.vue2
-rw-r--r--packages/frontend/src/pages/settings/other.vue2
-rw-r--r--packages/frontend/src/pages/settings/plugin.vue2
-rw-r--r--packages/frontend/src/pages/settings/preferences.vue22
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue18
-rw-r--r--packages/frontend/src/pages/settings/profile.vue2
-rw-r--r--packages/frontend/src/pages/settings/security.vue50
-rw-r--r--packages/frontend/src/pages/settings/sounds.vue2
-rw-r--r--packages/frontend/src/router.definition.ts4
-rw-r--r--packages/frontend/src/utility/inapp-search.ts37
-rw-r--r--packages/frontend/src/utility/settings-search-index.ts36
-rw-r--r--packages/frontend/src/utility/virtual.d.ts29
-rw-r--r--packages/frontend/vite.config.ts5
36 files changed, 1396 insertions, 1154 deletions
diff --git a/packages/frontend/lib/vite-plugin-create-search-index.ts b/packages/frontend/lib/vite-plugin-create-search-index.ts
index 97f4e589a3..4e20828909 100644
--- a/packages/frontend/lib/vite-plugin-create-search-index.ts
+++ b/packages/frontend/lib/vite-plugin-create-search-index.ts
@@ -39,6 +39,7 @@ export interface SearchIndexItem {
path?: string;
label: string;
keywords: string[];
+ texts: string[];
icon?: string;
inlining?: string[];
}
@@ -227,14 +228,14 @@ function extractElementText2Inner(node: TemplateChildNode, processingNodeName: s
// region extractUsageInfoFromTemplateAst
/**
- * SearchLabel/SearchKeyword/SearchIconを探して抽出する関数
+ * SearchLabel/SearchText/SearchIconを探して抽出する関数
*/
-function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: string | null, keywords: string[], icon: string | null } {
+function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: string | null; texts: string[]; icon: string | null; } {
let label: string | null | undefined = undefined;
let icon: string | null | undefined = undefined;
- const keywords: string[] = [];
+ const texts: string[] = [];
- logger.info(`Extracting labels and keywords from ${nodes.length} nodes`);
+ logger.info(`Extracting labels and texts from ${nodes.length} nodes`);
walkVueElements(nodes, null, (node) => {
switch (node.tag) {
@@ -248,10 +249,10 @@ function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: stri
label = extractElementText(node, id);
return;
- case 'SearchKeyword':
+ case 'SearchText':
const content = extractElementText(node, id);
if (content) {
- keywords.push(content);
+ texts.push(content);
}
return;
case 'SearchIcon':
@@ -278,8 +279,8 @@ function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: stri
});
// デバッグ情報
- logger.info(`Extraction completed: label=${label}, keywords=[${keywords.join(', ')}, icon=${icon}]`);
- return { label: label ?? null, keywords, icon: icon ?? null };
+ logger.info(`Extraction completed: label=${label}, text=[${texts.join(', ')}, icon=${icon}]`);
+ return { label: label ?? null, texts, icon: icon ?? null };
}
function getStringProp(attr: AttributeNode | DirectiveNode | null, id: string): string | null {
@@ -351,33 +352,36 @@ function extractUsageInfoFromTemplateAst(
parentId: parentId ?? undefined,
label: '', // デフォルト値
keywords: [],
+ texts: [],
};
// バインドプロパティを取得
- const path = getStringProp(findAttribute(node.props, 'path'), id)
- const icon = getStringProp(findAttribute(node.props, 'icon'), id)
- const label = getStringProp(findAttribute(node.props, 'label'), id)
- const inlining = getStringArrayProp(findAttribute(node.props, 'inlining'), id)
- const keywords = getStringArrayProp(findAttribute(node.props, 'keywords'), id)
+ const path = getStringProp(findAttribute(node.props, 'path'), id);
+ const icon = getStringProp(findAttribute(node.props, 'icon'), id);
+ const label = getStringProp(findAttribute(node.props, 'label'), id);
+ const inlining = getStringArrayProp(findAttribute(node.props, 'inlining'), id);
+ const keywords = getStringArrayProp(findAttribute(node.props, 'keywords'), id);
+ const texts = getStringArrayProp(findAttribute(node.props, 'texts'), id);
if (path) markerInfo.path = path;
if (icon) markerInfo.icon = icon;
if (label) markerInfo.label = label;
if (inlining) markerInfo.inlining = inlining;
if (keywords) markerInfo.keywords = keywords;
+ if (texts) markerInfo.texts = texts;
- //pathがない場合はファイルパスを設定
+ // pathがない場合はファイルパスを設定
if (markerInfo.path == null && parentId == null) {
markerInfo.path = id.match(/.*(\/(admin|settings)\/[^\/]+)\.vue$/)?.[1];
}
- // SearchLabelとSearchKeywordを抽出 (AST全体を探索)
+ // SearchLabelとSearchTextを抽出 (AST全体を探索)
{
const extracted = extractSugarTags(node.children, id);
if (extracted.label && markerInfo.label) logger.warn(`Duplicate label found for ${markerId} at ${id}:${node.loc.start.line}`);
if (extracted.icon && markerInfo.icon) logger.warn(`Duplicate icon found for ${markerId} at ${id}:${node.loc.start.line}`);
markerInfo.label = extracted.label ?? markerInfo.label ?? '';
- markerInfo.keywords = [...extracted.keywords, ...markerInfo.keywords];
+ markerInfo.texts = [...extracted.texts, ...markerInfo.texts];
markerInfo.icon = extracted.icon ?? markerInfo.icon ?? undefined;
}
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 5c89a6530d..dbc673333c 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -52,9 +52,9 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ item.label }}
</template>
<template v-else>
- <span style="opacity: 0.7; font-size: 90%;">{{ item.parentLabels.join(' > ') }}</span>
+ <span style="opacity: 0.7; font-size: 90%; word-break: break-word;">{{ item.parentLabels.join(' > ') }}</span>
<br>
- <span>{{ item.label }}</span>
+ <span style="word-break: break-word;">{{ item.label }}</span>
</template>
</span>
</MkA>
@@ -95,7 +95,7 @@ export type SuperMenuDef = {
<script lang="ts" setup>
import { useTemplateRef, ref, watch, nextTick, computed } from 'vue';
import { getScrollContainer } from '@@/js/scroll.js';
-import type { SearchIndexItem } from '@/utility/settings-search-index.js';
+import type { SearchIndexItem } from '@/utility/inapp-search.js';
import MkInput from '@/components/MkInput.vue';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
@@ -165,12 +165,28 @@ watch(rawSearchQuery, (value) => {
});
};
- for (const item of searchIndexItemById.values()) {
- if (
- compareStringIncludes(item.label, value) ||
- item.keywords.some((x) => compareStringIncludes(x, value))
- ) {
+ // label, keywords, texts の順に優先して表示
+
+ let items = Array.from(searchIndexItemById.values());
+
+ for (const item of items) {
+ if (compareStringIncludes(item.label, value)) {
+ addSearchResult(item);
+ items = items.filter(i => i.id !== item.id);
+ }
+ }
+
+ for (const item of items) {
+ if (item.keywords.some((x) => compareStringIncludes(x, value))) {
+ addSearchResult(item);
+ items = items.filter(i => i.id !== item.id);
+ }
+ }
+
+ for (const item of items) {
+ if (item.texts.some((x) => compareStringIncludes(x, value))) {
addSearchResult(item);
+ items = items.filter(i => i.id !== item.id);
}
}
}
diff --git a/packages/frontend/src/components/global/SearchText.vue b/packages/frontend/src/components/global/SearchText.vue
new file mode 100644
index 0000000000..27a284faf0
--- /dev/null
+++ b/packages/frontend/src/components/global/SearchText.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/index.ts b/packages/frontend/src/components/index.ts
index 19766e8575..6b1b80695f 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -31,7 +31,7 @@ import PageWithHeader from './global/PageWithHeader.vue';
import PageWithAnimBg from './global/PageWithAnimBg.vue';
import SearchMarker from './global/SearchMarker.vue';
import SearchLabel from './global/SearchLabel.vue';
-import SearchKeyword from './global/SearchKeyword.vue';
+import SearchText from './global/SearchText.vue';
import SearchIcon from './global/SearchIcon.vue';
import type { App } from 'vue';
@@ -71,7 +71,7 @@ export const components = {
PageWithAnimBg: PageWithAnimBg,
SearchMarker: SearchMarker,
SearchLabel: SearchLabel,
- SearchKeyword: SearchKeyword,
+ SearchText: SearchText,
SearchIcon: SearchIcon,
};
@@ -105,7 +105,7 @@ declare module '@vue/runtime-core' {
PageWithAnimBg: typeof PageWithAnimBg;
SearchMarker: typeof SearchMarker;
SearchLabel: typeof SearchLabel;
- SearchKeyword: typeof SearchKeyword;
+ SearchText: typeof SearchText;
SearchIcon: typeof SearchIcon;
}
}
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 6c580f87f1..bd919146f8 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -4,158 +4,161 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkFolder>
- <template #icon><i class="ti ti-shield"></i></template>
- <template #label>{{ i18n.ts.botProtection }}</template>
- <template v-if="botProtectionForm.savedState.provider === 'hcaptcha'" #suffix>hCaptcha</template>
- <template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template>
- <template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template>
- <template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template>
- <template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template>
- <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
- <template #footer>
- <MkFormFooter :canSaving="canSaving" :form="botProtectionForm"/>
- </template>
+<SearchMarker markerId="botProtection" :keywords="['bot', 'protection', 'captcha', 'hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile']">
+ <MkFolder>
+ <template #icon><SearchIcon><i class="ti ti-shield"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.botProtection }}</SearchLabel></template>
+ <template v-if="botProtectionForm.savedState.provider === 'hcaptcha'" #suffix>hCaptcha</template>
+ <template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template>
+ <template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template>
+ <template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template>
+ <template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template>
+ <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
+ <template #footer>
+ <MkFormFooter :canSaving="canSaving" :form="botProtectionForm"/>
+ </template>
- <div class="_gaps_m">
- <MkRadios v-model="botProtectionForm.state.provider">
- <option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
- <option value="hcaptcha">hCaptcha</option>
- <option value="mcaptcha">mCaptcha</option>
- <option value="recaptcha">reCAPTCHA</option>
- <option value="turnstile">Turnstile</option>
- <option value="testcaptcha">testCaptcha</option>
- </MkRadios>
+ <div class="_gaps_m">
+ <MkRadios v-model="botProtectionForm.state.provider">
+ <option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
+ <option value="hcaptcha">hCaptcha</option>
+ <option value="mcaptcha">mCaptcha</option>
+ <option value="recaptcha">reCAPTCHA</option>
+ <option value="turnstile">Turnstile</option>
+ <option value="testcaptcha">testCaptcha</option>
+ </MkRadios>
- <template v-if="botProtectionForm.state.provider === 'hcaptcha'">
- <MkInput v-model="botProtectionForm.state.hcaptchaSiteKey" debounce>
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
- </MkInput>
- <MkInput v-model="botProtectionForm.state.hcaptchaSecretKey" debounce>
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
- </MkInput>
- <FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey">
- <template #label>{{ i18n.ts._captcha.verify }}</template>
- <MkCaptcha
- v-model="captchaResult"
- provider="hcaptcha"
- :sitekey="botProtectionForm.state.hcaptchaSiteKey"
- :secretKey="botProtectionForm.state.hcaptchaSecretKey"
- />
- </FormSlot>
- <MkInfo>
- <div :class="$style.captchaInfoMsg">
- <div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
- <div>
- <span>ref: </span><a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a>
+ <template v-if="botProtectionForm.state.provider === 'hcaptcha'">
+ <MkInput v-model="botProtectionForm.state.hcaptchaSiteKey" debounce>
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
+ </MkInput>
+ <MkInput v-model="botProtectionForm.state.hcaptchaSecretKey" debounce>
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
+ </MkInput>
+ <FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey">
+ <template #label>{{ i18n.ts._captcha.verify }}</template>
+ <MkCaptcha
+ v-model="captchaResult"
+ provider="hcaptcha"
+ :sitekey="botProtectionForm.state.hcaptchaSiteKey"
+ :secretKey="botProtectionForm.state.hcaptchaSecretKey"
+ />
+ </FormSlot>
+ <MkInfo>
+ <div :class="$style.captchaInfoMsg">
+ <div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
+ <div>
+ <span>ref: </span><a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a>
+ </div>
</div>
- </div>
- </MkInfo>
- </template>
+ </MkInfo>
+ </template>
- <template v-else-if="botProtectionForm.state.provider === 'mcaptcha'">
- <MkInput v-model="botProtectionForm.state.mcaptchaSiteKey" debounce>
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
- </MkInput>
- <MkInput v-model="botProtectionForm.state.mcaptchaSecretKey" debounce>
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
- </MkInput>
- <MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl" debounce>
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
- </MkInput>
- <FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
- <template #label>{{ i18n.ts._captcha.verify }}</template>
- <MkCaptcha
- v-model="captchaResult"
- provider="mcaptcha"
- :sitekey="botProtectionForm.state.mcaptchaSiteKey"
- :secretKey="botProtectionForm.state.mcaptchaSecretKey"
- :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"
- />
- </FormSlot>
- </template>
+ <template v-else-if="botProtectionForm.state.provider === 'mcaptcha'">
+ <MkInput v-model="botProtectionForm.state.mcaptchaSiteKey" debounce>
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
+ </MkInput>
+ <MkInput v-model="botProtectionForm.state.mcaptchaSecretKey" debounce>
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
+ </MkInput>
+ <MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl" debounce>
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
+ </MkInput>
+ <FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
+ <template #label>{{ i18n.ts._captcha.verify }}</template>
+ <MkCaptcha
+ v-model="captchaResult"
+ provider="mcaptcha"
+ :sitekey="botProtectionForm.state.mcaptchaSiteKey"
+ :secretKey="botProtectionForm.state.mcaptchaSecretKey"
+ :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"
+ />
+ </FormSlot>
+ </template>
- <template v-else-if="botProtectionForm.state.provider === 'recaptcha'">
- <MkInput v-model="botProtectionForm.state.recaptchaSiteKey" debounce>
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
- </MkInput>
- <MkInput v-model="botProtectionForm.state.recaptchaSecretKey" debounce>
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
- </MkInput>
- <FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
- <template #label>{{ i18n.ts._captcha.verify }}</template>
- <MkCaptcha
- v-model="captchaResult"
- provider="recaptcha"
- :sitekey="botProtectionForm.state.recaptchaSiteKey"
- :secretKey="botProtectionForm.state.recaptchaSecretKey"
- />
- </FormSlot>
- <MkInfo>
- <div :class="$style.captchaInfoMsg">
- <div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
- <div>
- <span>ref: </span>
- <a
- href="https://developers.google.com/recaptcha/docs/faq?hl=ja#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do"
- target="_blank"
- >reCAPTCHA FAQ</a>
+ <template v-else-if="botProtectionForm.state.provider === 'recaptcha'">
+ <MkInput v-model="botProtectionForm.state.recaptchaSiteKey" debounce>
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
+ </MkInput>
+ <MkInput v-model="botProtectionForm.state.recaptchaSecretKey" debounce>
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
+ </MkInput>
+ <FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
+ <template #label>{{ i18n.ts._captcha.verify }}</template>
+ <MkCaptcha
+ v-model="captchaResult"
+ provider="recaptcha"
+ :sitekey="botProtectionForm.state.recaptchaSiteKey"
+ :secretKey="botProtectionForm.state.recaptchaSecretKey"
+ />
+ </FormSlot>
+ <MkInfo>
+ <div :class="$style.captchaInfoMsg">
+ <div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
+ <div>
+ <span>ref: </span>
+ <a
+ href="https://developers.google.com/recaptcha/docs/faq?hl=ja#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do"
+ target="_blank"
+ >reCAPTCHA FAQ</a>
+ </div>
</div>
- </div>
- </MkInfo>
- </template>
+ </MkInfo>
+ </template>
- <template v-else-if="botProtectionForm.state.provider === 'turnstile'">
- <MkInput v-model="botProtectionForm.state.turnstileSiteKey" debounce>
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.turnstileSiteKey }}</template>
- </MkInput>
- <MkInput v-model="botProtectionForm.state.turnstileSecretKey" debounce>
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.turnstileSecretKey }}</template>
- </MkInput>
- <FormSlot v-if="botProtectionForm.state.turnstileSiteKey">
- <template #label>{{ i18n.ts._captcha.verify }}</template>
- <MkCaptcha
- v-model="captchaResult"
- provider="turnstile"
- :sitekey="botProtectionForm.state.turnstileSiteKey"
- :secretKey="botProtectionForm.state.turnstileSecretKey"
- />
- </FormSlot>
- <MkInfo>
- <div :class="$style.captchaInfoMsg">
- <div>
- {{ i18n.ts._captcha.testSiteKeyMessage }}
- </div>
- <div>
- <span>ref: </span><a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a>
+ <template v-else-if="botProtectionForm.state.provider === 'turnstile'">
+ <MkInput v-model="botProtectionForm.state.turnstileSiteKey" debounce>
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>{{ i18n.ts.turnstileSiteKey }}</template>
+ </MkInput>
+ <MkInput v-model="botProtectionForm.state.turnstileSecretKey" debounce>
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>{{ i18n.ts.turnstileSecretKey }}</template>
+ </MkInput>
+ <FormSlot v-if="botProtectionForm.state.turnstileSiteKey">
+ <template #label>{{ i18n.ts._captcha.verify }}</template>
+ <MkCaptcha
+ v-model="captchaResult"
+ provider="turnstile"
+ :sitekey="botProtectionForm.state.turnstileSiteKey"
+ :secretKey="botProtectionForm.state.turnstileSecretKey"
+ />
+ </FormSlot>
+ <MkInfo>
+ <div :class="$style.captchaInfoMsg">
+ <div>
+ {{ i18n.ts._captcha.testSiteKeyMessage }}
+ </div>
+ <div>
+ <span>ref: </span><a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a>
+ </div>
</div>
- </div>
- </MkInfo>
- </template>
+ </MkInfo>
+ </template>
- <template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
- <MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
- <FormSlot>
- <template #label>{{ i18n.ts._captcha.verify }}</template>
- <MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/>
- </FormSlot>
- </template>
- </div>
-</MkFolder>
+ <template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
+ <MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
+ <FormSlot>
+ <template #label>{{ i18n.ts._captcha.verify }}</template>
+ <MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/>
+ </FormSlot>
+ </template>
+ </div>
+ </MkFolder>
+</SearchMarker>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
+import type { ApiWithDialogCustomErrors } from '@/os.js';
import MkRadios from '@/components/MkRadios.vue';
import MkInput from '@/components/MkInput.vue';
import FormSlot from '@/components/form/slot.vue';
@@ -167,7 +170,6 @@ import { useForm } from '@/composables/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue';
-import type { ApiWithDialogCustomErrors } from '@/os.js';
const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue
index 19258216f6..dc6495e06d 100644
--- a/packages/frontend/src/pages/admin/branding.vue
+++ b/packages/frontend/src/pages/admin/branding.vue
@@ -6,89 +6,117 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
- <FormSuspense :p="init">
+ <SearchMarker path="/admin/branding" :label="i18n.ts.branding" :keywords="['branding']" icon="ti ti-paint">
<div class="_gaps_m">
- <MkInput v-model="iconUrl" type="url">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
- </MkInput>
+ <SearchMarker :keywords="['icon', 'image']">
+ <MkInput v-model="iconUrl" type="url">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.iconUrl }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="app192IconUrl" type="url">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
- <template #caption>
- <div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
- <div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
- <div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
- <div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '192x192px' }) }}</strong></div>
- </template>
- </MkInput>
+ <SearchMarker :keywords="['icon', 'image']">
+ <MkInput v-model="app192IconUrl" type="url">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</SearchLabel></template>
+ <template #caption>
+ <div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
+ <div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
+ <div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
+ <div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '192x192px' }) }}</strong></div>
+ </template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="app512IconUrl" type="url">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
- <template #caption>
- <div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
- <div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
- <div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
- <div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '512x512px' }) }}</strong></div>
- </template>
- </MkInput>
+ <SearchMarker :keywords="['icon', 'image']">
+ <MkInput v-model="app512IconUrl" type="url">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</SearchLabel></template>
+ <template #caption>
+ <div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
+ <div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
+ <div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
+ <div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '512x512px' }) }}</strong></div>
+ </template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="bannerUrl" type="url">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts.bannerUrl }}</template>
- </MkInput>
+ <SearchMarker :keywords="['banner', 'image']">
+ <MkInput v-model="bannerUrl" type="url">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.bannerUrl }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="backgroundImageUrl" type="url">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts.backgroundImageUrl }}</template>
- </MkInput>
+ <SearchMarker :keywords="['background', 'image']">
+ <MkInput v-model="backgroundImageUrl" type="url">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.backgroundImageUrl }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="notFoundImageUrl" type="url">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts.notFoundDescription }}</template>
- </MkInput>
+ <SearchMarker :keywords="['image']">
+ <MkInput v-model="notFoundImageUrl" type="url">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.notFoundDescription }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="infoImageUrl" type="url">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts.nothing }}</template>
- </MkInput>
+ <SearchMarker :keywords="['image']">
+ <MkInput v-model="infoImageUrl" type="url">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.nothing }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="serverErrorImageUrl" type="url">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts.somethingHappened }}</template>
- </MkInput>
+ <SearchMarker :keywords="['image']">
+ <MkInput v-model="serverErrorImageUrl" type="url">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.somethingHappened }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <MkColorInput v-model="themeColor">
- <template #label>{{ i18n.ts.themeColor }}</template>
- </MkColorInput>
+ <SearchMarker :keywords="['theme', 'color']">
+ <MkColorInput v-model="themeColor">
+ <template #label><SearchLabel>{{ i18n.ts.themeColor }}</SearchLabel></template>
+ </MkColorInput>
+ </SearchMarker>
- <MkTextarea v-model="defaultLightTheme">
- <template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
- <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
- </MkTextarea>
+ <SearchMarker :keywords="['theme', 'default', 'light']">
+ <MkTextarea v-model="defaultLightTheme">
+ <template #label><SearchLabel>{{ i18n.ts.instanceDefaultLightTheme }}</SearchLabel></template>
+ <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
+ </MkTextarea>
+ </SearchMarker>
- <MkTextarea v-model="defaultDarkTheme">
- <template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
- <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
- </MkTextarea>
+ <SearchMarker :keywords="['theme', 'default', 'dark']">
+ <MkTextarea v-model="defaultDarkTheme">
+ <template #label><SearchLabel>{{ i18n.ts.instanceDefaultDarkTheme }}</SearchLabel></template>
+ <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
+ </MkTextarea>
+ </SearchMarker>
- <MkInput v-model="repositoryUrl" type="url">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts.repositoryUrl }}</template>
- </MkInput>
+ <SearchMarker>
+ <MkInput v-model="repositoryUrl" type="url">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.repositoryUrl }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="feedbackUrl" type="url">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts.feedbackUrl }}</template>
- </MkInput>
+ <SearchMarker>
+ <MkInput v-model="feedbackUrl" type="url">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts.feedbackUrl }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <MkTextarea v-model="manifestJsonOverride">
- <template #label>{{ i18n.ts._serverSettings.manifestJsonOverride }}</template>
- </MkTextarea>
+ <SearchMarker>
+ <MkTextarea v-model="manifestJsonOverride">
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.manifestJsonOverride }}</SearchLabel></template>
+ </MkTextarea>
+ </SearchMarker>
</div>
- </FormSuspense>
+ </SearchMarker>
</div>
<template #footer>
<div :class="$style.footer">
@@ -106,7 +134,6 @@ import JSON5 from 'json5';
import { host } from '@@/js/config.js';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
-import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { instance, fetchInstance } from '@/instance.js';
@@ -115,38 +142,22 @@ import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
import MkColorInput from '@/components/MkColorInput.vue';
-const iconUrl = ref<string | null>(null);
-const app192IconUrl = ref<string | null>(null);
-const app512IconUrl = ref<string | null>(null);
-const bannerUrl = ref<string | null>(null);
-const backgroundImageUrl = ref<string | null>(null);
-const themeColor = ref<string | null>(null);
-const defaultLightTheme = ref<string | null>(null);
-const defaultDarkTheme = ref<string | null>(null);
-const serverErrorImageUrl = ref<string | null>(null);
-const infoImageUrl = ref<string | null>(null);
-const notFoundImageUrl = ref<string | null>(null);
-const repositoryUrl = ref<string | null>(null);
-const feedbackUrl = ref<string | null>(null);
-const manifestJsonOverride = ref<string>('{}');
+const meta = await misskeyApi('admin/meta');
-async function init() {
- const meta = await misskeyApi('admin/meta');
- iconUrl.value = meta.iconUrl;
- app192IconUrl.value = meta.app192IconUrl;
- app512IconUrl.value = meta.app512IconUrl;
- bannerUrl.value = meta.bannerUrl;
- backgroundImageUrl.value = meta.backgroundImageUrl;
- themeColor.value = meta.themeColor;
- defaultLightTheme.value = meta.defaultLightTheme;
- defaultDarkTheme.value = meta.defaultDarkTheme;
- serverErrorImageUrl.value = meta.serverErrorImageUrl;
- infoImageUrl.value = meta.infoImageUrl;
- notFoundImageUrl.value = meta.notFoundImageUrl;
- repositoryUrl.value = meta.repositoryUrl;
- feedbackUrl.value = meta.feedbackUrl;
- manifestJsonOverride.value = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t');
-}
+const iconUrl = ref(meta.iconUrl);
+const app192IconUrl = ref(meta.app192IconUrl);
+const app512IconUrl = ref(meta.app512IconUrl);
+const bannerUrl = ref(meta.bannerUrl);
+const backgroundImageUrl = ref(meta.backgroundImageUrl);
+const themeColor = ref(meta.themeColor);
+const defaultLightTheme = ref(meta.defaultLightTheme);
+const defaultDarkTheme = ref(meta.defaultDarkTheme);
+const serverErrorImageUrl = ref(meta.serverErrorImageUrl);
+const infoImageUrl = ref(meta.infoImageUrl);
+const notFoundImageUrl = ref(meta.notFoundImageUrl);
+const repositoryUrl = ref(meta.repositoryUrl);
+const feedbackUrl = ref(meta.feedbackUrl);
+const manifestJsonOverride = ref(meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t'));
function save() {
os.apiWithDialog('admin/update-meta', {
diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue
index 17f2f8b593..8eb403f94c 100644
--- a/packages/frontend/src/pages/admin/email-settings.vue
+++ b/packages/frontend/src/pages/admin/email-settings.vue
@@ -6,48 +6,67 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
- <FormSuspense :p="init">
+ <SearchMarker path="/admin/email-settings" :label="i18n.ts.emailServer" :keywords="['email']" icon="ti ti-mail">
<div class="_gaps_m">
- <MkSwitch v-model="enableEmail">
- <template #label>{{ i18n.ts.enableEmail }} ({{ i18n.ts.recommended }})</template>
- <template #caption>{{ i18n.ts.emailConfigInfo }}</template>
- </MkSwitch>
+ <SearchMarker>
+ <MkSwitch v-model="enableEmail">
+ <template #label><SearchLabel>{{ i18n.ts.enableEmail }}</SearchLabel> ({{ i18n.ts.recommended }})</template>
+ <template #caption><SearchText>{{ i18n.ts.emailConfigInfo }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
<template v-if="enableEmail">
- <MkInput v-model="email" type="email">
- <template #label>{{ i18n.ts.emailAddress }}</template>
- </MkInput>
+ <SearchMarker>
+ <MkInput v-model="email" type="email">
+ <template #label><SearchLabel>{{ i18n.ts.emailAddress }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <FormSection>
- <template #label>{{ i18n.ts.smtpConfig }}</template>
+ <SearchMarker>
+ <FormSection>
+ <template #label><SearchLabel>{{ i18n.ts.smtpConfig }}</SearchLabel></template>
- <div class="_gaps_m">
- <FormSplit :minWidth="280">
- <MkInput v-model="smtpHost">
- <template #label>{{ i18n.ts.smtpHost }}</template>
- </MkInput>
- <MkInput v-model="smtpPort" type="number">
- <template #label>{{ i18n.ts.smtpPort }}</template>
- </MkInput>
- </FormSplit>
- <FormSplit :minWidth="280">
- <MkInput v-model="smtpUser">
- <template #label>{{ i18n.ts.smtpUser }}</template>
- </MkInput>
- <MkInput v-model="smtpPass" type="password">
- <template #label>{{ i18n.ts.smtpPass }}</template>
- </MkInput>
- </FormSplit>
- <FormInfo>{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
- <MkSwitch v-model="smtpSecure">
- <template #label>{{ i18n.ts.smtpSecure }}</template>
- <template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
- </MkSwitch>
- </div>
- </FormSection>
+ <div class="_gaps_m">
+ <FormSplit :minWidth="280">
+ <SearchMarker>
+ <MkInput v-model="smtpHost">
+ <template #label><SearchLabel>{{ i18n.ts.smtpHost }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
+ <SearchMarker>
+ <MkInput v-model="smtpPort" type="number">
+ <template #label><SearchLabel>{{ i18n.ts.smtpPort }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
+ </FormSplit>
+
+ <FormSplit :minWidth="280">
+ <SearchMarker>
+ <MkInput v-model="smtpUser">
+ <template #label><SearchLabel>{{ i18n.ts.smtpUser }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
+ <SearchMarker>
+ <MkInput v-model="smtpPass" type="password">
+ <template #label><SearchLabel>{{ i18n.ts.smtpPass }}</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
+ </FormSplit>
+
+ <FormInfo>{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
+
+ <SearchMarker>
+ <MkSwitch v-model="smtpSecure">
+ <template #label><SearchLabel>{{ i18n.ts.smtpSecure }}</SearchLabel></template>
+ <template #caption><SearchText>{{ i18n.ts.smtpSecureInfo }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
+ </div>
+ </FormSection>
+ </SearchMarker>
</template>
</div>
- </FormSuspense>
+ </SearchMarker>
</div>
<template #footer>
<div :class="$style.footer">
@@ -67,7 +86,6 @@ import { ref, computed } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import FormInfo from '@/components/MkInfo.vue';
-import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
@@ -77,24 +95,15 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
-const enableEmail = ref<boolean>(false);
-const email = ref<string | null>(null);
-const smtpSecure = ref<boolean>(false);
-const smtpHost = ref<string>('');
-const smtpPort = ref<number>(0);
-const smtpUser = ref<string>('');
-const smtpPass = ref<string>('');
+const meta = await misskeyApi('admin/meta');
-async function init() {
- const meta = await misskeyApi('admin/meta');
- enableEmail.value = meta.enableEmail;
- email.value = meta.email;
- smtpSecure.value = meta.smtpSecure;
- smtpHost.value = meta.smtpHost;
- smtpPort.value = meta.smtpPort;
- smtpUser.value = meta.smtpUser;
- smtpPass.value = meta.smtpPass;
-}
+const enableEmail = ref(meta.enableEmail);
+const email = ref(meta.email);
+const smtpSecure = ref(meta.smtpSecure);
+const smtpHost = ref(meta.smtpHost);
+const smtpPort = ref(meta.smtpPort);
+const smtpUser = ref(meta.smtpUser);
+const smtpPass = ref(meta.smtpPass);
async function testEmail() {
const { canceled, result: destination } = await os.inputText({
diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue
index 845fb12c5d..3a4eb8c3c7 100644
--- a/packages/frontend/src/pages/admin/external-services.vue
+++ b/packages/frontend/src/pages/admin/external-services.vue
@@ -6,36 +6,49 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
- <FormSuspense :p="init">
+ <SearchMarker path="/admin/external-services" :label="i18n.ts.externalServices" :keywords="['external', 'services', 'thirdparty']" icon="ti ti-link">
<div class="_gaps_m">
- <MkFolder>
- <template #label>Google Analytics<span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <SearchMarker v-slot="slotProps">
+ <MkFolder :defaultOpen="slotProps.isParentOfTarget">
+ <template #label><SearchLabel>Google Analytics</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
- <div class="_gaps_m">
- <MkInput v-model="googleAnalyticsMeasurementId">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Measurement ID</template>
- </MkInput>
- <MkButton primary @click="save_googleAnalytics">Save</MkButton>
- </div>
- </MkFolder>
+ <div class="_gaps_m">
+ <SearchMarker>
+ <MkInput v-model="googleAnalyticsMeasurementId">
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label><SearchLabel>Measurement ID</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <MkFolder>
- <template #label>DeepL Translation</template>
+ <MkButton primary @click="save_googleAnalytics">Save</MkButton>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <div class="_gaps_m">
- <MkInput v-model="deeplAuthKey">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>DeepL Auth Key</template>
- </MkInput>
- <MkSwitch v-model="deeplIsPro">
- <template #label>Pro account</template>
- </MkSwitch>
- <MkButton primary @click="save_deepl">Save</MkButton>
- </div>
- </MkFolder>
+ <SearchMarker v-slot="slotProps">
+ <MkFolder :defaultOpen="slotProps.isParentOfTarget">
+ <template #label><SearchLabel>DeepL Translation</SearchLabel></template>
+
+ <div class="_gaps_m">
+ <SearchMarker>
+ <MkInput v-model="deeplAuthKey">
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label><SearchLabel>Auth Key</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
+
+ <SearchMarker>
+ <MkSwitch v-model="deeplIsPro">
+ <template #label><SearchLabel>Pro account</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <MkButton primary @click="save_deepl">Save</MkButton>
+ </div>
+ </MkFolder>
+ </SearchMarker>
</div>
- </FormSuspense>
+ </SearchMarker>
</div>
</PageWithHeader>
</template>
@@ -45,7 +58,6 @@ import { ref, computed } from 'vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
-import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { fetchInstance } from '@/instance.js';
@@ -53,17 +65,11 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkFolder from '@/components/MkFolder.vue';
-const deeplAuthKey = ref<string>('');
-const deeplIsPro = ref<boolean>(false);
+const meta = await misskeyApi('admin/meta');
-const googleAnalyticsMeasurementId = ref<string>('');
-
-async function init() {
- const meta = await misskeyApi('admin/meta');
- deeplAuthKey.value = meta.deeplAuthKey ?? '';
- deeplIsPro.value = meta.deeplIsPro;
- googleAnalyticsMeasurementId.value = meta.googleAnalyticsMeasurementId ?? '';
-}
+const deeplAuthKey = ref(meta.deeplAuthKey ?? '');
+const deeplIsPro = ref(meta.deeplIsPro);
+const googleAnalyticsMeasurementId = ref(meta.googleAnalyticsMeasurementId ?? '');
function save_deepl() {
os.apiWithDialog('admin/update-meta', {
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index a87028f008..94994dc94c 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="noEmailServer" warn>{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
</div>
- <MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
+ <MkSuperMenu :def="menuDef" :searchIndex="searchIndex" :grid="narrow"></MkSuperMenu>
</div>
</div>
</div>
@@ -44,6 +44,9 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { lookupUser, lookupUserByEmail, lookupFile } from '@/utility/admin-lookup.js';
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { useRouter } from '@/router.js';
+import { genSearchIndexes } from '@/utility/inapp-search.js';
+
+const searchIndex = await import('search-index:admin').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
const isEmpty = (x: string | null) => x == null || x === '';
@@ -324,12 +327,6 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePage(() => INFO.value);
-
-defineExpose({
- header: {
- title: i18n.ts.controlPanel,
- },
-});
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index 819f229c10..83438bb1d9 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -6,140 +6,162 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
- <FormSuspense :p="init">
+ <SearchMarker path="/admin/moderation" :label="i18n.ts.moderation" :keywords="['moderation']" icon="ti ti-shield" :inlining="['serverRules']">
<div class="_gaps_m">
- <MkSwitch :modelValue="enableRegistration" @update:modelValue="onChange_enableRegistration">
- <template #label>{{ i18n.ts._serverSettings.openRegistration }}</template>
- <template #caption>
- <div>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</div>
- <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._serverSettings.openRegistrationWarning }}</div>
- </template>
- </MkSwitch>
+ <SearchMarker :keywords="['open', 'registration']">
+ <MkSwitch :modelValue="enableRegistration" @update:modelValue="onChange_enableRegistration">
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.openRegistration }}</SearchLabel></template>
+ <template #caption>
+ <div><SearchText>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</SearchText></div>
+ <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> <SearchText>{{ i18n.ts._serverSettings.openRegistrationWarning }}</SearchText></div>
+ </template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
- <template #label>{{ i18n.ts.emailRequiredForSignup }} ({{ i18n.ts.recommended }})</template>
- </MkSwitch>
+ <SearchMarker :keywords="['email', 'required', 'signup']">
+ <MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
+ <template #label><SearchLabel>{{ i18n.ts.emailRequiredForSignup }}</SearchLabel> ({{ i18n.ts.recommended }})</template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSelect v-model="ugcVisibilityForVisitor" @update:modelValue="onChange_ugcVisibilityForVisitor">
- <template #label>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor }}</template>
- <option value="all">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.all }}</option>
- <option value="local">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.localOnly }} ({{ i18n.ts.recommended }})</option>
- <option value="none">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.none }}</option>
- <template #caption>
- <div>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description }}</div>
- <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description2 }}</div>
- </template>
- </MkSelect>
+ <SearchMarker :keywords="['ugc', 'content', 'visibility', 'visitor', 'guest']">
+ <MkSelect v-model="ugcVisibilityForVisitor" @update:modelValue="onChange_ugcVisibilityForVisitor">
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor }}</SearchLabel></template>
+ <option value="all">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.all }}</option>
+ <option value="local">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.localOnly }} ({{ i18n.ts.recommended }})</option>
+ <option value="none">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.none }}</option>
+ <template #caption>
+ <div><SearchText>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description }}</SearchText></div>
+ <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> <SearchText>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description2 }}</SearchText></div>
+ </template>
+ </MkSelect>
+ </SearchMarker>
- <FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
+ <XServerRules/>
- <MkFolder>
- <template #icon><i class="ti ti-lock-star"></i></template>
- <template #label>{{ i18n.ts.preservedUsernames }}</template>
+ <SearchMarker :keywords="['preserved', 'usernames']">
+ <MkFolder>
+ <template #icon><SearchIcon><i class="ti ti-lock-star"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.preservedUsernames }}</SearchLabel></template>
- <div class="_gaps">
- <MkTextarea v-model="preservedUsernames">
- <template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
- </MkTextarea>
- <MkButton primary @click="save_preservedUsernames">{{ i18n.ts.save }}</MkButton>
- </div>
- </MkFolder>
+ <div class="_gaps">
+ <MkTextarea v-model="preservedUsernames">
+ <template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
+ </MkTextarea>
+ <MkButton primary @click="save_preservedUsernames">{{ i18n.ts.save }}</MkButton>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-message-exclamation"></i></template>
- <template #label>{{ i18n.ts.sensitiveWords }}</template>
+ <SearchMarker :keywords="['sensitive', 'words']">
+ <MkFolder>
+ <template #icon><SearchIcon><i class="ti ti-message-exclamation"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.sensitiveWords }}</SearchLabel></template>
- <div class="_gaps">
- <MkTextarea v-model="sensitiveWords">
- <template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
- </MkTextarea>
- <MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton>
- </div>
- </MkFolder>
+ <div class="_gaps">
+ <MkTextarea v-model="sensitiveWords">
+ <template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
+ </MkTextarea>
+ <MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-message-x"></i></template>
- <template #label>{{ i18n.ts.prohibitedWords }}</template>
+ <SearchMarker :keywords="['prohibited', 'words']">
+ <MkFolder>
+ <template #icon><SearchIcon><i class="ti ti-message-x"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.prohibitedWords }}</SearchLabel></template>
- <div class="_gaps">
- <MkTextarea v-model="prohibitedWords">
- <template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
- </MkTextarea>
- <MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton>
- </div>
- </MkFolder>
+ <div class="_gaps">
+ <MkTextarea v-model="prohibitedWords">
+ <template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
+ </MkTextarea>
+ <MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-user-x"></i></template>
- <template #label>{{ i18n.ts.prohibitedWordsForNameOfUser }}</template>
+ <SearchMarker :keywords="['prohibited', 'name', 'user']">
+ <MkFolder>
+ <template #icon><SearchIcon><i class="ti ti-user-x"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.prohibitedWordsForNameOfUser }}</SearchLabel></template>
- <div class="_gaps">
- <MkTextarea v-model="prohibitedWordsForNameOfUser">
- <template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
- </MkTextarea>
- <MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton>
- </div>
- </MkFolder>
+ <div class="_gaps">
+ <MkTextarea v-model="prohibitedWordsForNameOfUser">
+ <template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
+ </MkTextarea>
+ <MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-eye-off"></i></template>
- <template #label>{{ i18n.ts.hiddenTags }}</template>
+ <SearchMarker :keywords="['hidden', 'tags', 'hashtags']">
+ <MkFolder>
+ <template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.hiddenTags }}</SearchLabel></template>
- <div class="_gaps">
- <MkTextarea v-model="hiddenTags">
- <template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
- </MkTextarea>
- <MkButton primary @click="save_hiddenTags">{{ i18n.ts.save }}</MkButton>
- </div>
- </MkFolder>
+ <div class="_gaps">
+ <MkTextarea v-model="hiddenTags">
+ <template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
+ </MkTextarea>
+ <MkButton primary @click="save_hiddenTags">{{ i18n.ts.save }}</MkButton>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-eye-off"></i></template>
- <template #label>{{ i18n.ts.silencedInstances }}</template>
+ <SearchMarker :keywords="['silenced', 'servers', 'hosts']">
+ <MkFolder>
+ <template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.silencedInstances }}</SearchLabel></template>
- <div class="_gaps">
- <MkTextarea v-model="silencedHosts">
- <template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
- </MkTextarea>
- <MkButton primary @click="save_silencedHosts">{{ i18n.ts.save }}</MkButton>
- </div>
- </MkFolder>
+ <div class="_gaps">
+ <MkTextarea v-model="silencedHosts">
+ <template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
+ </MkTextarea>
+ <MkButton primary @click="save_silencedHosts">{{ i18n.ts.save }}</MkButton>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-eye-off"></i></template>
- <template #label>{{ i18n.ts.mediaSilencedInstances }}</template>
+ <SearchMarker :keywords="['media', 'silenced', 'servers', 'hosts']">
+ <MkFolder>
+ <template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.mediaSilencedInstances }}</SearchLabel></template>
- <div class="_gaps">
- <MkTextarea v-model="mediaSilencedHosts">
- <template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template>
- </MkTextarea>
- <MkButton primary @click="save_mediaSilencedHosts">{{ i18n.ts.save }}</MkButton>
- </div>
- </MkFolder>
+ <div class="_gaps">
+ <MkTextarea v-model="mediaSilencedHosts">
+ <template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template>
+ </MkTextarea>
+ <MkButton primary @click="save_mediaSilencedHosts">{{ i18n.ts.save }}</MkButton>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-ban"></i></template>
- <template #label>{{ i18n.ts.blockedInstances }}</template>
+ <SearchMarker :keywords="['blocked', 'servers', 'hosts']">
+ <MkFolder>
+ <template #icon><SearchIcon><i class="ti ti-ban"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.blockedInstances }}</SearchLabel></template>
- <div class="_gaps">
- <MkTextarea v-model="blockedHosts">
- <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
- </MkTextarea>
- <MkButton primary @click="save_blockedHosts">{{ i18n.ts.save }}</MkButton>
- </div>
- </MkFolder>
+ <div class="_gaps">
+ <MkTextarea v-model="blockedHosts">
+ <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
+ </MkTextarea>
+ <MkButton primary @click="save_blockedHosts">{{ i18n.ts.save }}</MkButton>
+ </div>
+ </MkFolder>
+ </SearchMarker>
</div>
- </FormSuspense>
+ </SearchMarker>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
+import XServerRules from './server-rules.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
-import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { fetchInstance } from '@/instance.js';
@@ -150,32 +172,19 @@ import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
-const enableRegistration = ref<boolean>(false);
-const emailRequiredForSignup = ref<boolean>(false);
-const ugcVisibilityForVisitor = ref<string>('all');
-const sensitiveWords = ref<string>('');
-const prohibitedWords = ref<string>('');
-const prohibitedWordsForNameOfUser = ref<string>('');
-const hiddenTags = ref<string>('');
-const preservedUsernames = ref<string>('');
-const blockedHosts = ref<string>('');
-const silencedHosts = ref<string>('');
-const mediaSilencedHosts = ref<string>('');
+const meta = await misskeyApi('admin/meta');
-async function init() {
- const meta = await misskeyApi('admin/meta');
- enableRegistration.value = !meta.disableRegistration;
- emailRequiredForSignup.value = meta.emailRequiredForSignup;
- ugcVisibilityForVisitor.value = meta.ugcVisibilityForVisitor;
- sensitiveWords.value = meta.sensitiveWords.join('\n');
- prohibitedWords.value = meta.prohibitedWords.join('\n');
- prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
- hiddenTags.value = meta.hiddenTags.join('\n');
- preservedUsernames.value = meta.preservedUsernames.join('\n');
- blockedHosts.value = meta.blockedHosts.join('\n');
- silencedHosts.value = meta.silencedHosts?.join('\n') ?? '';
- mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
-}
+const enableRegistration = ref(!meta.disableRegistration);
+const emailRequiredForSignup = ref(meta.emailRequiredForSignup);
+const ugcVisibilityForVisitor = ref(meta.ugcVisibilityForVisitor);
+const sensitiveWords = ref(meta.sensitiveWords.join('\n'));
+const prohibitedWords = ref(meta.prohibitedWords.join('\n'));
+const prohibitedWordsForNameOfUser = ref(meta.prohibitedWordsForNameOfUser.join('\n'));
+const hiddenTags = ref(meta.hiddenTags.join('\n'));
+const preservedUsernames = ref(meta.preservedUsernames.join('\n'));
+const blockedHosts = ref(meta.blockedHosts.join('\n'));
+const silencedHosts = ref(meta.silencedHosts?.join('\n') ?? '');
+const mediaSilencedHosts = ref(meta.mediaSilencedHosts.join('\n'));
async function onChange_enableRegistration(value: boolean) {
if (value) {
diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue
index 7a46ae41c6..d42c23a51e 100644
--- a/packages/frontend/src/pages/admin/object-storage.vue
+++ b/packages/frontend/src/pages/admin/object-storage.vue
@@ -6,70 +6,94 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
- <FormSuspense :p="init">
+ <SearchMarker path="/admin/object-storage" :label="i18n.ts.objectStorage" :keywords="['objectStorage']" icon="ti ti-cloud">
<div class="_gaps_m">
- <MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
+ <SearchMarker>
+ <MkSwitch v-model="useObjectStorage"><SearchLabel>{{ i18n.ts.useObjectStorage }}</SearchLabel></MkSwitch>
+ </SearchMarker>
<template v-if="useObjectStorage">
- <MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'" type="url">
- <template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
- <template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
- </MkInput>
+ <SearchMarker>
+ <MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'" type="url">
+ <template #label><SearchLabel>{{ i18n.ts.objectStorageBaseUrl }}</SearchLabel></template>
+ <template #caption><SearchText>{{ i18n.ts.objectStorageBaseUrlDesc }}</SearchText></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="objectStorageBucket">
- <template #label>{{ i18n.ts.objectStorageBucket }}</template>
- <template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
- </MkInput>
+ <SearchMarker>
+ <MkInput v-model="objectStorageBucket">
+ <template #label><SearchLabel>{{ i18n.ts.objectStorageBucket }}</SearchLabel></template>
+ <template #caption><SearchText>{{ i18n.ts.objectStorageBucketDesc }}</SearchText></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="objectStoragePrefix">
- <template #label>{{ i18n.ts.objectStoragePrefix }}</template>
- <template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
- </MkInput>
+ <SearchMarker>
+ <MkInput v-model="objectStoragePrefix">
+ <template #label><SearchLabel>{{ i18n.ts.objectStoragePrefix }}</SearchLabel></template>
+ <template #caption><SearchText>{{ i18n.ts.objectStoragePrefixDesc }}</SearchText></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="objectStorageEndpoint" :placeholder="'example.com'">
- <template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
- <template #prefix>https://</template>
- <template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
- </MkInput>
+ <SearchMarker>
+ <MkInput v-model="objectStorageEndpoint" :placeholder="'example.com'">
+ <template #label><SearchLabel>{{ i18n.ts.objectStorageEndpoint }}</SearchLabel></template>
+ <template #prefix>https://</template>
+ <template #caption><SearchText>{{ i18n.ts.objectStorageEndpointDesc }}</SearchText></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="objectStorageRegion">
- <template #label>{{ i18n.ts.objectStorageRegion }}</template>
- <template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
- </MkInput>
+ <SearchMarker>
+ <MkInput v-model="objectStorageRegion">
+ <template #label><SearchLabel>{{ i18n.ts.objectStorageRegion }}</SearchLabel></template>
+ <template #caption><SearchText>{{ i18n.ts.objectStorageRegionDesc }}</SearchText></template>
+ </MkInput>
+ </SearchMarker>
<FormSplit :minWidth="280">
- <MkInput v-model="objectStorageAccessKey">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Access key</template>
- </MkInput>
+ <SearchMarker>
+ <MkInput v-model="objectStorageAccessKey">
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label><SearchLabel>Access key</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="objectStorageSecretKey" type="password">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Secret key</template>
- </MkInput>
+ <SearchMarker>
+ <MkInput v-model="objectStorageSecretKey" type="password">
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label><SearchLabel>Secret key</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
</FormSplit>
- <MkSwitch v-model="objectStorageUseSSL">
- <template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
- <template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
- </MkSwitch>
+ <SearchMarker>
+ <MkSwitch v-model="objectStorageUseSSL">
+ <template #label><SearchLabel>{{ i18n.ts.objectStorageUseSSL }}</SearchLabel></template>
+ <template #caption><SearchText>{{ i18n.ts.objectStorageUseSSLDesc }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSwitch v-model="objectStorageUseProxy">
- <template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
- <template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
- </MkSwitch>
+ <SearchMarker>
+ <MkSwitch v-model="objectStorageUseProxy">
+ <template #label><SearchLabel>{{ i18n.ts.objectStorageUseProxy }}</SearchLabel></template>
+ <template #caption><SearchText>{{ i18n.ts.objectStorageUseProxyDesc }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSwitch v-model="objectStorageSetPublicRead">
- <template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
- </MkSwitch>
+ <SearchMarker>
+ <MkSwitch v-model="objectStorageSetPublicRead">
+ <template #label><SearchLabel>{{ i18n.ts.objectStorageSetPublicRead }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSwitch v-model="objectStorageS3ForcePathStyle">
- <template #label>s3ForcePathStyle</template>
- <template #caption>{{ i18n.ts.s3ForcePathStyleDesc }}</template>
- </MkSwitch>
+ <SearchMarker>
+ <MkSwitch v-model="objectStorageS3ForcePathStyle">
+ <template #label><SearchLabel>s3ForcePathStyle</SearchLabel></template>
+ <template #caption><SearchText>{{ i18n.ts.s3ForcePathStyleDesc }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
</template>
</div>
- </FormSuspense>
+ </SearchMarker>
</div>
<template #footer>
<div :class="$style.footer">
@@ -94,36 +118,21 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
-const useObjectStorage = ref<boolean>(false);
-const objectStorageBaseUrl = ref<string | null>(null);
-const objectStorageBucket = ref<string | null>(null);
-const objectStoragePrefix = ref<string | null>(null);
-const objectStorageEndpoint = ref<string | null>(null);
-const objectStorageRegion = ref<string | null>(null);
-const objectStoragePort = ref<number | null>(null);
-const objectStorageAccessKey = ref<string | null>(null);
-const objectStorageSecretKey = ref<string | null>(null);
-const objectStorageUseSSL = ref<boolean>(false);
-const objectStorageUseProxy = ref<boolean>(false);
-const objectStorageSetPublicRead = ref<boolean>(false);
-const objectStorageS3ForcePathStyle = ref<boolean>(true);
+const meta = await misskeyApi('admin/meta');
-async function init() {
- const meta = await misskeyApi('admin/meta');
- useObjectStorage.value = meta.useObjectStorage;
- objectStorageBaseUrl.value = meta.objectStorageBaseUrl;
- objectStorageBucket.value = meta.objectStorageBucket;
- objectStoragePrefix.value = meta.objectStoragePrefix;
- objectStorageEndpoint.value = meta.objectStorageEndpoint;
- objectStorageRegion.value = meta.objectStorageRegion;
- objectStoragePort.value = meta.objectStoragePort;
- objectStorageAccessKey.value = meta.objectStorageAccessKey;
- objectStorageSecretKey.value = meta.objectStorageSecretKey;
- objectStorageUseSSL.value = meta.objectStorageUseSSL;
- objectStorageUseProxy.value = meta.objectStorageUseProxy;
- objectStorageSetPublicRead.value = meta.objectStorageSetPublicRead;
- objectStorageS3ForcePathStyle.value = meta.objectStorageS3ForcePathStyle;
-}
+const useObjectStorage = ref(meta.useObjectStorage);
+const objectStorageBaseUrl = ref(meta.objectStorageBaseUrl);
+const objectStorageBucket = ref(meta.objectStorageBucket);
+const objectStoragePrefix = ref(meta.objectStoragePrefix);
+const objectStorageEndpoint = ref(meta.objectStorageEndpoint);
+const objectStorageRegion = ref(meta.objectStorageRegion);
+const objectStoragePort = ref(meta.objectStoragePort);
+const objectStorageAccessKey = ref(meta.objectStorageAccessKey);
+const objectStorageSecretKey = ref(meta.objectStorageSecretKey);
+const objectStorageUseSSL = ref(meta.objectStorageUseSSL);
+const objectStorageUseProxy = ref(meta.objectStorageUseProxy);
+const objectStorageSetPublicRead = ref(meta.objectStorageSetPublicRead);
+const objectStorageS3ForcePathStyle = ref(meta.objectStorageS3ForcePathStyle);
function save() {
os.apiWithDialog('admin/update-meta', {
diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue
index ff3a5b9d7f..e3021778e7 100644
--- a/packages/frontend/src/pages/admin/performance.vue
+++ b/packages/frontend/src/pages/admin/performance.vue
@@ -6,131 +6,163 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
- <div class="_gaps">
- <div class="_panel" style="padding: 16px;">
- <MkSwitch v-model="enableServerMachineStats" @change="onChange_enableServerMachineStats">
- <template #label>{{ i18n.ts.enableServerMachineStats }}</template>
- <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
- </MkSwitch>
- </div>
-
- <div class="_panel" style="padding: 16px;">
- <MkSwitch v-model="enableIdenticonGeneration" @change="onChange_enableIdenticonGeneration">
- <template #label>{{ i18n.ts.enableIdenticonGeneration }}</template>
- <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
- </MkSwitch>
- </div>
+ <SearchMarker path="/admin/performance" :label="i18n.ts.performance" :keywords="['performance']" icon="ti ti-bolt">
+ <div class="_gaps">
+ <SearchMarker>
+ <div class="_panel" style="padding: 16px;">
+ <MkSwitch v-model="enableServerMachineStats" @change="onChange_enableServerMachineStats">
+ <template #label><SearchLabel>{{ i18n.ts.enableServerMachineStats }}</SearchLabel></template>
+ <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
+ </MkSwitch>
+ </div>
+ </SearchMarker>
- <div class="_panel" style="padding: 16px;">
- <MkSwitch v-model="enableChartsForRemoteUser" @change="onChange_enableChartsForRemoteUser">
- <template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template>
- <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
- </MkSwitch>
- </div>
+ <SearchMarker>
+ <div class="_panel" style="padding: 16px;">
+ <MkSwitch v-model="enableIdenticonGeneration" @change="onChange_enableIdenticonGeneration">
+ <template #label><SearchLabel>{{ i18n.ts.enableIdenticonGeneration }}</SearchLabel></template>
+ <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
+ </MkSwitch>
+ </div>
+ </SearchMarker>
- <div class="_panel" style="padding: 16px;">
- <MkSwitch v-model="enableStatsForFederatedInstances" @change="onChange_enableStatsForFederatedInstances">
- <template #label>{{ i18n.ts.enableStatsForFederatedInstances }}</template>
- <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
- </MkSwitch>
- </div>
+ <SearchMarker>
+ <div class="_panel" style="padding: 16px;">
+ <MkSwitch v-model="enableChartsForRemoteUser" @change="onChange_enableChartsForRemoteUser">
+ <template #label><SearchLabel>{{ i18n.ts.enableChartsForRemoteUser }}</SearchLabel></template>
+ <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
+ </MkSwitch>
+ </div>
+ </SearchMarker>
- <div class="_panel" style="padding: 16px;">
- <MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances">
- <template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template>
- <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
- </MkSwitch>
- </div>
+ <SearchMarker>
+ <div class="_panel" style="padding: 16px;">
+ <MkSwitch v-model="enableStatsForFederatedInstances" @change="onChange_enableStatsForFederatedInstances">
+ <template #label><SearchLabel>{{ i18n.ts.enableStatsForFederatedInstances }}</SearchLabel></template>
+ <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
+ </MkSwitch>
+ </div>
+ </SearchMarker>
- <MkFolder :defaultOpen="true">
- <template #icon><i class="ti ti-bolt"></i></template>
- <template #label>Misskey® Fan-out Timeline Technology™ (FTT)</template>
- <template v-if="fttForm.savedState.enableFanoutTimeline" #suffix>Enabled</template>
- <template v-else #suffix>Disabled</template>
- <template v-if="fttForm.modified.value" #footer>
- <MkFormFooter :form="fttForm"/>
- </template>
+ <SearchMarker>
+ <div class="_panel" style="padding: 16px;">
+ <MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances">
+ <template #label><SearchLabel>{{ i18n.ts.enableChartsForFederatedInstances }}</SearchLabel></template>
+ <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
+ </MkSwitch>
+ </div>
+ </SearchMarker>
- <div class="_gaps">
- <MkSwitch v-model="fttForm.state.enableFanoutTimeline">
- <template #label>{{ i18n.ts.enable }}<span v-if="fttForm.modifiedStates.enableFanoutTimeline" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>
- <div>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</div>
- <div><MkLink target="_blank" url="https://misskey-hub.net/docs/for-admin/features/ftt/">{{ i18n.ts.details }}</MkLink></div>
+ <SearchMarker>
+ <MkFolder :defaultOpen="true">
+ <template #icon><SearchIcon><i class="ti ti-bolt"></i></SearchIcon></template>
+ <template #label><SearchLabel>Misskey® Fan-out Timeline Technology™ (FTT)</SearchLabel></template>
+ <template v-if="fttForm.savedState.enableFanoutTimeline" #suffix>Enabled</template>
+ <template v-else #suffix>Disabled</template>
+ <template v-if="fttForm.modified.value" #footer>
+ <MkFormFooter :form="fttForm"/>
</template>
- </MkSwitch>
- <template v-if="fttForm.state.enableFanoutTimeline">
- <MkSwitch v-model="fttForm.state.enableFanoutTimelineDbFallback">
- <template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}<span v-if="fttForm.modifiedStates.enableFanoutTimelineDbFallback" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template>
- </MkSwitch>
+ <div class="_gaps">
+ <SearchMarker>
+ <MkSwitch v-model="fttForm.state.enableFanoutTimeline">
+ <template #label><SearchLabel>{{ i18n.ts.enable }}</SearchLabel><span v-if="fttForm.modifiedStates.enableFanoutTimeline" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption>
+ <div><SearchText>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</SearchText></div>
+ <div><MkLink target="_blank" url="https://misskey-hub.net/docs/for-admin/features/ftt/">{{ i18n.ts.details }}</MkLink></div>
+ </template>
+ </MkSwitch>
+ </SearchMarker>
- <MkInput v-model="fttForm.state.perLocalUserUserTimelineCacheMax" type="number">
- <template #label>perLocalUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perLocalUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
- </MkInput>
+ <template v-if="fttForm.state.enableFanoutTimeline">
+ <SearchMarker :keywords="['db', 'database', 'fallback']">
+ <MkSwitch v-model="fttForm.state.enableFanoutTimelineDbFallback">
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}</SearchLabel><span v-if="fttForm.modifiedStates.enableFanoutTimelineDbFallback" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
- <MkInput v-model="fttForm.state.perRemoteUserUserTimelineCacheMax" type="number">
- <template #label>perRemoteUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perRemoteUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
- </MkInput>
+ <SearchMarker>
+ <MkInput v-model="fttForm.state.perLocalUserUserTimelineCacheMax" type="number">
+ <template #label><SearchLabel>perLocalUserUserTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perLocalUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="fttForm.state.perUserHomeTimelineCacheMax" type="number">
- <template #label>perUserHomeTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserHomeTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
- </MkInput>
+ <SearchMarker>
+ <MkInput v-model="fttForm.state.perRemoteUserUserTimelineCacheMax" type="number">
+ <template #label><SearchLabel>perRemoteUserUserTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perRemoteUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="fttForm.state.perUserListTimelineCacheMax" type="number">
- <template #label>perUserListTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserListTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
- </MkInput>
- </template>
- </div>
- </MkFolder>
+ <SearchMarker>
+ <MkInput v-model="fttForm.state.perUserHomeTimelineCacheMax" type="number">
+ <template #label><SearchLabel>perUserHomeTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perUserHomeTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
+ </MkInput>
+ </SearchMarker>
- <MkFolder :defaultOpen="true">
- <template #icon><i class="ti ti-bolt"></i></template>
- <template #label>Misskey® Reactions Boost Technology™ (RBT)<span class="_beta">{{ i18n.ts.beta }}</span></template>
- <template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template>
- <template v-else #suffix>Disabled</template>
- <template v-if="rbtForm.modified.value" #footer>
- <MkFormFooter :form="rbtForm"/>
- </template>
+ <SearchMarker>
+ <MkInput v-model="fttForm.state.perUserListTimelineCacheMax" type="number">
+ <template #label><SearchLabel>perUserListTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perUserListTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
+ </MkInput>
+ </SearchMarker>
+ </template>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <div class="_gaps_m">
- <MkSwitch v-model="rbtForm.state.enableReactionsBuffering">
- <template #label>{{ i18n.ts.enable }}<span v-if="rbtForm.modifiedStates.enableReactionsBuffering" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._serverSettings.reactionsBufferingDescription }}</template>
- </MkSwitch>
- </div>
- </MkFolder>
+ <SearchMarker>
+ <MkFolder :defaultOpen="true">
+ <template #icon><SearchIcon><i class="ti ti-bolt"></i></SearchIcon></template>
+ <template #label><SearchLabel>Misskey® Reactions Boost Technology™ (RBT)</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template>
+ <template v-else #suffix>Disabled</template>
+ <template v-if="rbtForm.modified.value" #footer>
+ <MkFormFooter :form="rbtForm"/>
+ </template>
+
+ <div class="_gaps_m">
+ <SearchMarker>
+ <MkSwitch v-model="rbtForm.state.enableReactionsBuffering">
+ <template #label><SearchLabel>{{ i18n.ts.enable }}</SearchLabel><span v-if="rbtForm.modifiedStates.enableReactionsBuffering" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts._serverSettings.reactionsBufferingDescription }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder :defaultOpen="true">
- <template #icon><i class="ti ti-recycle"></i></template>
- <template #label>Remote Notes Cleaning (仮)</template>
- <template v-if="remoteNotesCleaningForm.savedState.enableRemoteNotesCleaning" #suffix>Enabled</template>
- <template v-else #suffix>Disabled</template>
- <template v-if="remoteNotesCleaningForm.modified.value" #footer>
- <MkFormFooter :form="remoteNotesCleaningForm"/>
- </template>
+ <SearchMarker>
+ <MkFolder :defaultOpen="true">
+ <template #icon><SearchIcon><i class="ti ti-recycle"></i></SearchIcon></template>
+ <template #label><SearchLabel>Remote Notes Cleaning (仮)</SearchLabel></template>
+ <template v-if="remoteNotesCleaningForm.savedState.enableRemoteNotesCleaning" #suffix>Enabled</template>
+ <template v-else #suffix>Disabled</template>
+ <template v-if="remoteNotesCleaningForm.modified.value" #footer>
+ <MkFormFooter :form="remoteNotesCleaningForm"/>
+ </template>
- <div class="_gaps_m">
- <MkSwitch v-model="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
- <template #label>{{ i18n.ts.enable }}<span v-if="remoteNotesCleaningForm.modifiedStates.enableRemoteNotesCleaning" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._serverSettings.remoteNotesCleaning_description }}</template>
- </MkSwitch>
+ <div class="_gaps_m">
+ <MkSwitch v-model="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
+ <template #label><SearchLabel>{{ i18n.ts.enable }}</SearchLabel><span v-if="remoteNotesCleaningForm.modifiedStates.enableRemoteNotesCleaning" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts._serverSettings.remoteNotesCleaning_description }}</SearchText></template>
+ </MkSwitch>
- <template v-if="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
- <MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningExpiryDaysForEachNotes" type="number">
- <template #label>{{ i18n.ts._serverSettings.remoteNotesCleaningExpiryDaysForEachNotes }} ({{ i18n.ts.inDays }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningExpiryDaysForEachNotes" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #suffix>{{ i18n.ts._time.day }}</template>
- </MkInput>
+ <template v-if="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
+ <MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningExpiryDaysForEachNotes" type="number">
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.remoteNotesCleaningExpiryDaysForEachNotes }}</SearchLabel> ({{ i18n.ts.inDays }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningExpiryDaysForEachNotes" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #suffix>{{ i18n.ts._time.day }}</template>
+ </MkInput>
- <MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningMaxProcessingDurationInMinutes" type="number">
- <template #label>{{ i18n.ts._serverSettings.remoteNotesCleaningMaxProcessingDuration }} ({{ i18n.ts.inMinutes }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningMaxProcessingDurationInMinutes" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #suffix>{{ i18n.ts._time.minute }}</template>
- </MkInput>
- </template>
- </div>
- </MkFolder>
- </div>
+ <MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningMaxProcessingDurationInMinutes" type="number">
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.remoteNotesCleaningMaxProcessingDuration }}</SearchLabel> ({{ i18n.ts.inMinutes }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningMaxProcessingDurationInMinutes" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #suffix>{{ i18n.ts._time.minute }}</template>
+ </MkInput>
+ </template>
+ </div>
+ </MkFolder>
+ </SearchMarker>
+ </div>
+ </SearchMarker>
</div>
</PageWithHeader>
</template>
@@ -243,7 +275,7 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePage(() => ({
- title: i18n.ts.other,
- icon: 'ti ti-adjustments',
+ title: i18n.ts.performance,
+ icon: 'ti ti-bolt',
}));
</script>
diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue
index aabf64342e..fca9776c0a 100644
--- a/packages/frontend/src/pages/admin/relays.vue
+++ b/packages/frontend/src/pages/admin/relays.vue
@@ -6,18 +6,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 800px;">
- <div class="_gaps">
- <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;">
- <div>{{ relay.inbox }}</div>
- <div style="margin: 8px 0;">
- <i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--MI_THEME-success);"></i>
- <i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--MI_THEME-error);"></i>
- <i v-else class="ti ti-clock" :class="$style.icon"></i>
- <span>{{ i18n.ts._relayStatus[relay.status] }}</span>
+ <SearchMarker path="/admin/relays" :label="i18n.ts.relays" :keywords="['relays']" icon="ti ti-planet">
+ <div class="_gaps">
+ <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;">
+ <div>{{ relay.inbox }}</div>
+ <div style="margin: 8px 0;">
+ <i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--MI_THEME-success);"></i>
+ <i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--MI_THEME-error);"></i>
+ <i v-else class="ti ti-clock" :class="$style.icon"></i>
+ <span>{{ i18n.ts._relayStatus[relay.status] }}</span>
+ </div>
+ <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
</div>
- <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
</div>
- </div>
+ </SearchMarker>
</div>
</PageWithHeader>
</template>
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index 9e907a4469..27e35c7e69 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -6,115 +6,153 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
- <div class="_gaps_m">
- <XBotProtection/>
+ <SearchMarker path="/admin/security" :label="i18n.ts.security" :keywords="['security']" icon="ti ti-lock" :inlining="['botProtection']">
+ <div class="_gaps_m">
+ <XBotProtection/>
- <MkFolder>
- <template #icon><i class="ti ti-eye-off"></i></template>
- <template #label>{{ i18n.ts.sensitiveMediaDetection }}</template>
- <template v-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
- <template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'local'" #suffix>{{ i18n.ts.localOnly }}</template>
- <template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
- <template v-else #suffix>{{ i18n.ts.none }}</template>
- <template v-if="sensitiveMediaDetectionForm.modified.value" #footer>
- <MkFormFooter :form="sensitiveMediaDetectionForm"/>
- </template>
+ <SearchMarker v-slot="slotProps" :keywords="['sensitive', 'media', 'detection']">
+ <MkFolder :defaultOpen="slotProps.isParentOfTarget">
+ <template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.sensitiveMediaDetection }}</SearchLabel></template>
+ <template v-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
+ <template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'local'" #suffix>{{ i18n.ts.localOnly }}</template>
+ <template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
+ <template v-else #suffix>{{ i18n.ts.none }}</template>
+ <template v-if="sensitiveMediaDetectionForm.modified.value" #footer>
+ <MkFormFooter :form="sensitiveMediaDetectionForm"/>
+ </template>
- <div class="_gaps_m">
- <span>{{ i18n.ts._sensitiveMediaDetection.description }}</span>
+ <div class="_gaps_m">
+ <div><SearchText>{{ i18n.ts._sensitiveMediaDetection.description }}</SearchText></div>
- <MkRadios v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection">
- <option value="none">{{ i18n.ts.none }}</option>
- <option value="all">{{ i18n.ts.all }}</option>
- <option value="local">{{ i18n.ts.localOnly }}</option>
- <option value="remote">{{ i18n.ts.remoteOnly }}</option>
- </MkRadios>
+ <MkRadios v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection">
+ <option value="none">{{ i18n.ts.none }}</option>
+ <option value="all">{{ i18n.ts.all }}</option>
+ <option value="local">{{ i18n.ts.localOnly }}</option>
+ <option value="remote">{{ i18n.ts.remoteOnly }}</option>
+ </MkRadios>
- <MkRange v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :textConverter="(v) => `${v + 1}`">
- <template #label>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</template>
- <template #caption>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</template>
- </MkRange>
+ <SearchMarker :keywords="['sensitivity']">
+ <MkRange v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :textConverter="(v) => `${v + 1}`">
+ <template #label><SearchLabel>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</SearchLabel></template>
+ <template #caption><SearchText>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</SearchText></template>
+ </MkRange>
+ </SearchMarker>
- <MkSwitch v-model="sensitiveMediaDetectionForm.state.enableSensitiveMediaDetectionForVideos">
- <template #label>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
- <template #caption>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</template>
- </MkSwitch>
+ <SearchMarker :keywords="['video', 'analyze']">
+ <MkSwitch v-model="sensitiveMediaDetectionForm.state.enableSensitiveMediaDetectionForVideos">
+ <template #label><SearchLabel>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSwitch v-model="sensitiveMediaDetectionForm.state.setSensitiveFlagAutomatically">
- <template #label>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }} ({{ i18n.ts.notRecommended }})</template>
- <template #caption>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</template>
- </MkSwitch>
+ <SearchMarker :keywords="['flag', 'automatically']">
+ <MkSwitch v-model="sensitiveMediaDetectionForm.state.setSensitiveFlagAutomatically">
+ <template #label><SearchLabel>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }}</SearchLabel> ({{ i18n.ts.notRecommended }})</template>
+ <template #caption><SearchText>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
- <!-- 現状 false positive が多すぎて実用に耐えない
+ <!-- 現状 false positive が多すぎて実用に耐えない
<MkSwitch v-model="disallowUploadWhenPredictedAsPorn">
<template #label>{{ i18n.ts._sensitiveMediaDetection.disallowUploadWhenPredictedAsPorn }}</template>
</MkSwitch>
-->
- </div>
- </MkFolder>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #label>Active Email Validation</template>
- <template v-if="emailValidationForm.savedState.enableActiveEmailValidation" #suffix>Enabled</template>
- <template v-else #suffix>Disabled</template>
- <template v-if="emailValidationForm.modified.value" #footer>
- <MkFormFooter :form="emailValidationForm"/>
- </template>
+ <SearchMarker v-slot="slotProps" :keywords="['email', 'validation']">
+ <MkFolder :defaultOpen="slotProps.isParentOfTarget">
+ <template #label><SearchLabel>Active Email Validation</SearchLabel></template>
+ <template v-if="emailValidationForm.savedState.enableActiveEmailValidation" #suffix>Enabled</template>
+ <template v-else #suffix>Disabled</template>
+ <template v-if="emailValidationForm.modified.value" #footer>
+ <MkFormFooter :form="emailValidationForm"/>
+ </template>
- <div class="_gaps_m">
- <span>{{ i18n.ts.activeEmailValidationDescription }}</span>
- <MkSwitch v-model="emailValidationForm.state.enableActiveEmailValidation">
- <template #label>Enable</template>
- </MkSwitch>
- <MkSwitch v-model="emailValidationForm.state.enableVerifymailApi">
- <template #label>Use Verifymail.io API</template>
- </MkSwitch>
- <MkInput v-model="emailValidationForm.state.verifymailAuthKey">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Verifymail.io API Auth Key</template>
- </MkInput>
- <MkSwitch v-model="emailValidationForm.state.enableTruemailApi">
- <template #label>Use TrueMail API</template>
- </MkSwitch>
- <MkInput v-model="emailValidationForm.state.truemailInstance">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>TrueMail API Instance</template>
- </MkInput>
- <MkInput v-model="emailValidationForm.state.truemailAuthKey">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>TrueMail API Auth Key</template>
- </MkInput>
- </div>
- </MkFolder>
+ <div class="_gaps_m">
+ <div><SearchText>{{ i18n.ts.activeEmailValidationDescription }}</SearchText></div>
- <MkFolder>
- <template #label>Banned Email Domains</template>
- <template v-if="bannedEmailDomainsForm.modified.value" #footer>
- <MkFormFooter :form="bannedEmailDomainsForm"/>
- </template>
+ <SearchMarker>
+ <MkSwitch v-model="emailValidationForm.state.enableActiveEmailValidation">
+ <template #label><SearchLabel>Enable</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
- <div class="_gaps_m">
- <MkTextarea v-model="bannedEmailDomainsForm.state.bannedEmailDomains">
- <template #label>Banned Email Domains List</template>
- </MkTextarea>
- </div>
- </MkFolder>
+ <SearchMarker>
+ <MkSwitch v-model="emailValidationForm.state.enableVerifymailApi">
+ <template #label><SearchLabel>Use Verifymail.io API</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
- <MkFolder>
- <template #label>Log IP address</template>
- <template v-if="ipLoggingForm.savedState.enableIpLogging" #suffix>Enabled</template>
- <template v-else #suffix>Disabled</template>
- <template v-if="ipLoggingForm.modified.value" #footer>
- <MkFormFooter :form="ipLoggingForm"/>
- </template>
+ <SearchMarker>
+ <MkInput v-model="emailValidationForm.state.verifymailAuthKey">
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label><SearchLabel>Verifymail.io API Auth Key</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
- <div class="_gaps_m">
- <MkSwitch v-model="ipLoggingForm.state.enableIpLogging">
- <template #label>Enable</template>
- </MkSwitch>
- </div>
- </MkFolder>
- </div>
+ <SearchMarker>
+ <MkSwitch v-model="emailValidationForm.state.enableTruemailApi">
+ <template #label><SearchLabel>Use TrueMail API</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
+ <SearchMarker>
+ <MkInput v-model="emailValidationForm.state.truemailInstance">
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label><SearchLabel>TrueMail API Instance</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
+
+ <SearchMarker>
+ <MkInput v-model="emailValidationForm.state.truemailAuthKey">
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label><SearchLabel>TrueMail API Auth Key</SearchLabel></template>
+ </MkInput>
+ </SearchMarker>
+ </div>
+ </MkFolder>
+ </SearchMarker>
+
+ <SearchMarker v-slot="slotProps" :keywords="['banned', 'email', 'domains', 'blacklist']">
+ <MkFolder :defaultOpen="slotProps.isParentOfTarget">
+ <template #label><SearchLabel>Banned Email Domains</SearchLabel></template>
+ <template v-if="bannedEmailDomainsForm.modified.value" #footer>
+ <MkFormFooter :form="bannedEmailDomainsForm"/>
+ </template>
+
+ <div class="_gaps_m">
+ <SearchMarker>
+ <MkTextarea v-model="bannedEmailDomainsForm.state.bannedEmailDomains">
+ <template #label><SearchLabel>Banned Email Domains List</SearchLabel></template>
+ </MkTextarea>
+ </SearchMarker>
+ </div>
+ </MkFolder>
+ </SearchMarker>
+
+ <SearchMarker v-slot="slotProps" :keywords="['log', 'ipAddress']">
+ <MkFolder :defaultOpen="slotProps.isParentOfTarget">
+ <template #label><SearchLabel>Log IP address</SearchLabel></template>
+ <template v-if="ipLoggingForm.savedState.enableIpLogging" #suffix>Enabled</template>
+ <template v-else #suffix>Disabled</template>
+ <template v-if="ipLoggingForm.modified.value" #footer>
+ <MkFormFooter :form="ipLoggingForm"/>
+ </template>
+
+ <div class="_gaps_m">
+ <SearchMarker>
+ <MkSwitch v-model="ipLoggingForm.state.enableIpLogging">
+ <template #label><SearchLabel>Enable</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+ </div>
+ </MkFolder>
+ </SearchMarker>
+ </div>
+ </SearchMarker>
</div>
</PageWithHeader>
</template>
diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue
index 276a7590c4..d26f02b41c 100644
--- a/packages/frontend/src/pages/admin/server-rules.vue
+++ b/packages/frontend/src/pages/admin/server-rules.vue
@@ -4,10 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<PageWithHeader :tabs="headerTabs">
- <div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
+<SearchMarker markerId="serverRules" :keywords="['rules']">
+ <MkFolder>
+ <template #icon><SearchIcon><i class="ti ti-checkbox"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.serverRules }}</SearchLabel></template>
+
<div class="_gaps_m">
- <div>{{ i18n.ts._serverRules.description }}</div>
+ <div><SearchText>{{ i18n.ts._serverRules.description }}</SearchText></div>
+
<Sortable
v-model="serverRules"
class="_gaps_m"
@@ -33,8 +37,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</div>
- </div>
-</PageWithHeader>
+ </MkFolder>
+</SearchMarker>
</template>
<script lang="ts" setup>
@@ -42,9 +46,9 @@ import { defineAsyncComponent, ref, computed } from 'vue';
import * as os from '@/os.js';
import { fetchInstance, instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
-import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
+import MkFolder from '@/components/MkFolder.vue';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -60,13 +64,6 @@ const save = async () => {
const remove = (index: number): void => {
serverRules.value.splice(index, 1);
};
-
-const headerTabs = computed(() => []);
-
-definePage(() => ({
- title: i18n.ts.serverRules,
- icon: 'ti ti-checkbox',
-}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index d079b4cb0c..541ee7c0cd 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -6,292 +6,369 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
- <div class="_gaps_m">
- <MkFolder :defaultOpen="true">
- <template #icon><i class="ti ti-info-circle"></i></template>
- <template #label>{{ i18n.ts.info }}</template>
- <template v-if="infoForm.modified.value" #footer>
- <MkFormFooter :form="infoForm"/>
- </template>
+ <SearchMarker path="/admin/settings" :label="i18n.ts.general" :keywords="['general', 'settings']" icon="ti ti-settings">
+ <div class="_gaps_m">
+ <SearchMarker v-slot="slotProps" :keywords="['information', 'meta']">
+ <MkFolder :defaultOpen="true">
+ <template #icon><SearchIcon><i class="ti ti-info-circle"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.info }}</SearchLabel></template>
+ <template v-if="infoForm.modified.value" #footer>
+ <MkFormFooter :form="infoForm"/>
+ </template>
- <div class="_gaps">
- <MkInput v-model="infoForm.state.name">
- <template #label>{{ i18n.ts.instanceName }}<span v-if="infoForm.modifiedStates.name" class="_modified">{{ i18n.ts.modified }}</span></template>
- </MkInput>
+ <div class="_gaps">
+ <SearchMarker :keywords="['name']">
+ <MkInput v-model="infoForm.state.name">
+ <template #label><SearchLabel>{{ i18n.ts.instanceName }}</SearchLabel><span v-if="infoForm.modifiedStates.name" class="_modified">{{ i18n.ts.modified }}</span></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="infoForm.state.shortName">
- <template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})<span v-if="infoForm.modifiedStates.shortName" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template>
- </MkInput>
+ <SearchMarker :keywords="['shortName']">
+ <MkInput v-model="infoForm.state.shortName">
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.shortName }}</SearchLabel> ({{ i18n.ts.optional }})<span v-if="infoForm.modifiedStates.shortName" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts._serverSettings.shortNameDescription }}</SearchText></template>
+ </MkInput>
+ </SearchMarker>
- <MkTextarea v-model="infoForm.state.description">
- <template #label>{{ i18n.ts.instanceDescription }}<span v-if="infoForm.modifiedStates.description" class="_modified">{{ i18n.ts.modified }}</span></template>
- </MkTextarea>
+ <SearchMarker :keywords="['description']">
+ <MkTextarea v-model="infoForm.state.description">
+ <template #label><SearchLabel>{{ i18n.ts.instanceDescription }}</SearchLabel><span v-if="infoForm.modifiedStates.description" class="_modified">{{ i18n.ts.modified }}</span></template>
+ </MkTextarea>
+ </SearchMarker>
- <FormSplit :minWidth="300">
- <MkInput v-model="infoForm.state.maintainerName">
- <template #label>{{ i18n.ts.maintainerName }}<span v-if="infoForm.modifiedStates.maintainerName" class="_modified">{{ i18n.ts.modified }}</span></template>
- </MkInput>
+ <FormSplit :minWidth="300">
+ <SearchMarker :keywords="['maintainer', 'name']">
+ <MkInput v-model="infoForm.state.maintainerName">
+ <template #label><SearchLabel>{{ i18n.ts.maintainerName }}</SearchLabel><span v-if="infoForm.modifiedStates.maintainerName" class="_modified">{{ i18n.ts.modified }}</span></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="infoForm.state.maintainerEmail" type="email">
- <template #label>{{ i18n.ts.maintainerEmail }}<span v-if="infoForm.modifiedStates.maintainerEmail" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #prefix><i class="ti ti-mail"></i></template>
- </MkInput>
- </FormSplit>
+ <SearchMarker :keywords="['maintainer', 'email', 'contact']">
+ <MkInput v-model="infoForm.state.maintainerEmail" type="email">
+ <template #label><SearchLabel>{{ i18n.ts.maintainerEmail }}</SearchLabel><span v-if="infoForm.modifiedStates.maintainerEmail" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #prefix><i class="ti ti-mail"></i></template>
+ </MkInput>
+ </SearchMarker>
+ </FormSplit>
- <MkInput v-model="infoForm.state.tosUrl" type="url">
- <template #label>{{ i18n.ts.tosUrl }}<span v-if="infoForm.modifiedStates.tosUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #prefix><i class="ti ti-link"></i></template>
- </MkInput>
+ <SearchMarker :keywords="['tos', 'termsOfService']">
+ <MkInput v-model="infoForm.state.tosUrl" type="url">
+ <template #label><SearchLabel>{{ i18n.ts.tosUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.tosUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #prefix><i class="ti ti-link"></i></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="infoForm.state.privacyPolicyUrl" type="url">
- <template #label>{{ i18n.ts.privacyPolicyUrl }}<span v-if="infoForm.modifiedStates.privacyPolicyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #prefix><i class="ti ti-link"></i></template>
- </MkInput>
+ <SearchMarker :keywords="['privacyPolicy']">
+ <MkInput v-model="infoForm.state.privacyPolicyUrl" type="url">
+ <template #label><SearchLabel>{{ i18n.ts.privacyPolicyUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.privacyPolicyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #prefix><i class="ti ti-link"></i></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="infoForm.state.inquiryUrl" type="url">
- <template #label>{{ i18n.ts._serverSettings.inquiryUrl }}<span v-if="infoForm.modifiedStates.inquiryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template>
- <template #prefix><i class="ti ti-link"></i></template>
- </MkInput>
+ <SearchMarker :keywords="['inquiry', 'contact']">
+ <MkInput v-model="infoForm.state.inquiryUrl" type="url">
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.inquiryUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.inquiryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</SearchText></template>
+ <template #prefix><i class="ti ti-link"></i></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="infoForm.state.repositoryUrl" type="url">
- <template #label>{{ i18n.ts.repositoryUrl }}<span v-if="infoForm.modifiedStates.repositoryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts.repositoryUrlDescription }}</template>
- <template #prefix><i class="ti ti-link"></i></template>
- </MkInput>
+ <SearchMarker :keywords="['repository', 'url']">
+ <MkInput v-model="infoForm.state.repositoryUrl" type="url">
+ <template #label><SearchLabel>{{ i18n.ts.repositoryUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.repositoryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts.repositoryUrlDescription }}</SearchText></template>
+ <template #prefix><i class="ti ti-link"></i></template>
+ </MkInput>
+ </SearchMarker>
- <MkInfo v-if="!instance.providesTarball && !infoForm.state.repositoryUrl" warn>
- {{ i18n.ts.repositoryUrlOrTarballRequired }}
- </MkInfo>
+ <MkInfo v-if="!instance.providesTarball && !infoForm.state.repositoryUrl" warn>
+ {{ i18n.ts.repositoryUrlOrTarballRequired }}
+ </MkInfo>
- <MkInput v-model="infoForm.state.impressumUrl" type="url">
- <template #label>{{ i18n.ts.impressumUrl }}<span v-if="infoForm.modifiedStates.impressumUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts.impressumDescription }}</template>
- <template #prefix><i class="ti ti-link"></i></template>
- </MkInput>
- </div>
- </MkFolder>
+ <SearchMarker :keywords="['impressum', 'legalNotice']">
+ <MkInput v-model="infoForm.state.impressumUrl" type="url">
+ <template #label><SearchLabel>{{ i18n.ts.impressumUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.impressumUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts.impressumDescription }}</SearchText></template>
+ <template #prefix><i class="ti ti-link"></i></template>
+ </MkInput>
+ </SearchMarker>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-user-star"></i></template>
- <template #label>{{ i18n.ts.pinnedUsers }}</template>
- <template v-if="pinnedUsersForm.modified.value" #footer>
- <MkFormFooter :form="pinnedUsersForm"/>
- </template>
+ <SearchMarker v-slot="slotProps" :keywords="['pinned', 'users']">
+ <MkFolder :defaultOpen="slotProps.isParentOfTarget">
+ <template #icon><SearchIcon><i class="ti ti-user-star"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.pinnedUsers }}</SearchLabel></template>
+ <template v-if="pinnedUsersForm.modified.value" #footer>
+ <MkFormFooter :form="pinnedUsersForm"/>
+ </template>
- <MkTextarea v-model="pinnedUsersForm.state.pinnedUsers">
- <template #label>{{ i18n.ts.pinnedUsers }}<span v-if="pinnedUsersForm.modifiedStates.pinnedUsers" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
- </MkTextarea>
- </MkFolder>
+ <MkTextarea v-model="pinnedUsersForm.state.pinnedUsers">
+ <template #label>{{ i18n.ts.pinnedUsers }}<span v-if="pinnedUsersForm.modifiedStates.pinnedUsers" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts.pinnedUsersDescription }}</SearchText></template>
+ </MkTextarea>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-world-cog"></i></template>
- <template #label>ServiceWorker</template>
- <template v-if="serviceWorkerForm.modified.value" #footer>
- <MkFormFooter :form="serviceWorkerForm"/>
- </template>
+ <SearchMarker v-slot="slotProps" :keywords="['serviceWorker']">
+ <MkFolder :defaultOpen="slotProps.isParentOfTarget">
+ <template #icon><SearchIcon><i class="ti ti-world-cog"></i></SearchIcon></template>
+ <template #label><SearchLabel>ServiceWorker</SearchLabel></template>
+ <template v-if="serviceWorkerForm.modified.value" #footer>
+ <MkFormFooter :form="serviceWorkerForm"/>
+ </template>
- <div class="_gaps">
- <MkSwitch v-model="serviceWorkerForm.state.enableServiceWorker">
- <template #label>{{ i18n.ts.enableServiceworker }}<span v-if="serviceWorkerForm.modifiedStates.enableServiceWorker" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
- </MkSwitch>
+ <div class="_gaps">
+ <SearchMarker>
+ <MkSwitch v-model="serviceWorkerForm.state.enableServiceWorker">
+ <template #label><SearchLabel>{{ i18n.ts.enableServiceworker }}</SearchLabel><span v-if="serviceWorkerForm.modifiedStates.enableServiceWorker" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts.serviceworkerInfo }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
- <template v-if="serviceWorkerForm.state.enableServiceWorker">
- <MkInput v-model="serviceWorkerForm.state.swPublicKey">
- <template #label>Public key<span v-if="serviceWorkerForm.modifiedStates.swPublicKey" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #prefix><i class="ti ti-key"></i></template>
- </MkInput>
+ <template v-if="serviceWorkerForm.state.enableServiceWorker">
+ <SearchMarker>
+ <MkInput v-model="serviceWorkerForm.state.swPublicKey">
+ <template #label><SearchLabel>Public key</SearchLabel><span v-if="serviceWorkerForm.modifiedStates.swPublicKey" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #prefix><i class="ti ti-key"></i></template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="serviceWorkerForm.state.swPrivateKey">
- <template #label>Private key<span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #prefix><i class="ti ti-key"></i></template>
- </MkInput>
- </template>
- </div>
- </MkFolder>
+ <SearchMarker>
+ <MkInput v-model="serviceWorkerForm.state.swPrivateKey">
+ <template #label><SearchLabel>Private key</SearchLabel><span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #prefix><i class="ti ti-key"></i></template>
+ </MkInput>
+ </SearchMarker>
+ </template>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-ad"></i></template>
- <template #label>{{ i18n.ts._ad.adsSettings }}</template>
- <template v-if="adForm.modified.value" #footer>
- <MkFormFooter :form="adForm"/>
- </template>
+ <SearchMarker v-slot="slotProps" :keywords="['ads']">
+ <MkFolder :defaultOpen="slotProps.isParentOfTarget">
+ <template #icon><SearchIcon><i class="ti ti-ad"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts._ad.adsSettings }}</SearchLabel></template>
+ <template v-if="adForm.modified.value" #footer>
+ <MkFormFooter :form="adForm"/>
+ </template>
- <div class="_gaps">
- <div class="_gaps_s">
- <MkInput v-model="adForm.state.notesPerOneAd" :min="0" type="number">
- <template #label>{{ i18n.ts._ad.notesPerOneAd }}<span v-if="adForm.modifiedStates.notesPerOneAd" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
- </MkInput>
- <MkInfo v-if="adForm.state.notesPerOneAd > 0 && adForm.state.notesPerOneAd < 20" :warn="true">
- {{ i18n.ts._ad.adsTooClose }}
- </MkInfo>
- </div>
- </div>
- </MkFolder>
+ <div class="_gaps">
+ <div class="_gaps_s">
+ <SearchMarker>
+ <MkInput v-model="adForm.state.notesPerOneAd" :min="0" type="number">
+ <template #label><SearchLabel>{{ i18n.ts._ad.notesPerOneAd }}</SearchLabel><span v-if="adForm.modifiedStates.notesPerOneAd" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
+ </MkInput>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-world-search"></i></template>
- <template #label>{{ i18n.ts._urlPreviewSetting.title }}</template>
- <template v-if="urlPreviewForm.modified.value" #footer>
- <MkFormFooter :form="urlPreviewForm"/>
- </template>
+ <MkInfo v-if="adForm.state.notesPerOneAd > 0 && adForm.state.notesPerOneAd < 20" :warn="true">
+ {{ i18n.ts._ad.adsTooClose }}
+ </MkInfo>
+ </div>
+ </div>
+ </MkFolder>
+ </SearchMarker>
+
+ <SearchMarker v-slot="slotProps" :keywords="['url', 'preview']">
+ <MkFolder :defaultOpen="slotProps.isParentOfTarget">
+ <template #icon><SearchIcon><i class="ti ti-world-search"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.title }}</SearchLabel></template>
+ <template v-if="urlPreviewForm.modified.value" #footer>
+ <MkFormFooter :form="urlPreviewForm"/>
+ </template>
- <div class="_gaps">
- <MkSwitch v-model="urlPreviewForm.state.urlPreviewEnabled">
- <template #label>{{ i18n.ts._urlPreviewSetting.enable }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewEnabled" class="_modified">{{ i18n.ts.modified }}</span></template>
- </MkSwitch>
+ <div class="_gaps">
+ <SearchMarker>
+ <MkSwitch v-model="urlPreviewForm.state.urlPreviewEnabled">
+ <template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.enable }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewEnabled" class="_modified">{{ i18n.ts.modified }}</span></template>
+ </MkSwitch>
+ </SearchMarker>
- <template v-if="urlPreviewForm.state.urlPreviewEnabled">
- <MkSwitch v-model="urlPreviewForm.state.urlPreviewAllowRedirect">
- <template #label>{{ i18n.ts._urlPreviewSetting.allowRedirect }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewAllowRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._urlPreviewSetting.allowRedirectDescription }}</template>
- </MkSwitch>
+ <template v-if="urlPreviewForm.state.urlPreviewEnabled">
+ <SearchMarker :keywords="['allow', 'redirect']">
+ <MkSwitch v-model="urlPreviewForm.state.urlPreviewAllowRedirect">
+ <template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.allowRedirect }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewAllowRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption>{{ i18n.ts._urlPreviewSetting.allowRedirectDescription }}</template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSwitch v-model="urlPreviewForm.state.urlPreviewRequireContentLength">
- <template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
- </MkSwitch>
+ <SearchMarker :keywords="['contentLength']">
+ <MkSwitch v-model="urlPreviewForm.state.urlPreviewRequireContentLength">
+ <template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.requireContentLength }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
+ </MkSwitch>
+ </SearchMarker>
- <MkInput v-model="urlPreviewForm.state.urlPreviewMaximumContentLength" type="number">
- <template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewMaximumContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template>
- </MkInput>
+ <SearchMarker :keywords="['contentLength']">
+ <MkInput v-model="urlPreviewForm.state.urlPreviewMaximumContentLength" type="number">
+ <template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewMaximumContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="urlPreviewForm.state.urlPreviewTimeout" type="number">
- <template #label>{{ i18n.ts._urlPreviewSetting.timeout }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewTimeout" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template>
- </MkInput>
+ <SearchMarker :keywords="['timeout']">
+ <MkInput v-model="urlPreviewForm.state.urlPreviewTimeout" type="number">
+ <template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.timeout }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewTimeout" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template>
+ </MkInput>
+ </SearchMarker>
- <MkInput v-model="urlPreviewForm.state.urlPreviewUserAgent" type="text">
- <template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewUserAgent" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template>
- </MkInput>
+ <SearchMarker :keywords="['userAgent']">
+ <MkInput v-model="urlPreviewForm.state.urlPreviewUserAgent" type="text">
+ <template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.userAgent }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewUserAgent" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template>
+ </MkInput>
+ </SearchMarker>
- <div>
- <MkInput v-model="urlPreviewForm.state.urlPreviewSummaryProxyUrl" type="text">
- <template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewSummaryProxyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template>
- </MkInput>
+ <div>
+ <SearchMarker :keywords="['proxy']">
+ <MkInput v-model="urlPreviewForm.state.urlPreviewSummaryProxyUrl" type="text">
+ <template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.summaryProxy }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewSummaryProxyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template>
+ </MkInput>
+ </SearchMarker>
- <div :class="$style.subCaption">
- {{ i18n.ts._urlPreviewSetting.summaryProxyDescription2 }}
- <ul style="padding-left: 20px; margin: 4px 0">
- <li>{{ i18n.ts._urlPreviewSetting.timeout }} / key:timeout</li>
- <li>{{ i18n.ts._urlPreviewSetting.maximumContentLength }} / key:contentLengthLimit</li>
- <li>{{ i18n.ts._urlPreviewSetting.requireContentLength }} / key:contentLengthRequired</li>
- <li>{{ i18n.ts._urlPreviewSetting.userAgent }} / key:userAgent</li>
- </ul>
- </div>
+ <div :class="$style.subCaption">
+ {{ i18n.ts._urlPreviewSetting.summaryProxyDescription2 }}
+ <ul style="padding-left: 20px; margin: 4px 0">
+ <li>{{ i18n.ts._urlPreviewSetting.timeout }} / key:timeout</li>
+ <li>{{ i18n.ts._urlPreviewSetting.maximumContentLength }} / key:contentLengthLimit</li>
+ <li>{{ i18n.ts._urlPreviewSetting.requireContentLength }} / key:contentLengthRequired</li>
+ <li>{{ i18n.ts._urlPreviewSetting.userAgent }} / key:userAgent</li>
+ </ul>
+ </div>
+ </div>
+ </template>
</div>
- </template>
- </div>
- </MkFolder>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-planet"></i></template>
- <template #label>{{ i18n.ts.federation }}</template>
- <template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template>
- <template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template>
- <template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template>
- <template v-if="federationForm.modified.value" #footer>
- <MkFormFooter :form="federationForm"/>
- </template>
+ <SearchMarker v-slot="slotProps" :keywords="['federation']">
+ <MkFolder :defaultOpen="slotProps.isParentOfTarget">
+ <template #icon><SearchIcon><i class="ti ti-planet"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.federation }}</SearchLabel></template>
+ <template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template>
+ <template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template>
+ <template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template>
+ <template v-if="federationForm.modified.value" #footer>
+ <MkFormFooter :form="federationForm"/>
+ </template>
- <div class="_gaps">
- <MkRadios v-model="federationForm.state.federation">
- <template #label>{{ i18n.ts.behavior }}<span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
- <option value="all">{{ i18n.ts.all }}</option>
- <option value="specified">{{ i18n.ts.specifyHost }}</option>
- <option value="none">{{ i18n.ts.none }}</option>
- </MkRadios>
+ <div class="_gaps">
+ <SearchMarker>
+ <MkRadios v-model="federationForm.state.federation">
+ <template #label><SearchLabel>{{ i18n.ts.behavior }}</SearchLabel><span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <option value="all">{{ i18n.ts.all }}</option>
+ <option value="specified">{{ i18n.ts.specifyHost }}</option>
+ <option value="none">{{ i18n.ts.none }}</option>
+ </MkRadios>
+ </SearchMarker>
- <MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts">
- <template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
- </MkTextarea>
+ <SearchMarker :keywords="['hosts']">
+ <MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts">
+ <template #label><SearchLabel>{{ i18n.ts.federationAllowedHosts }}</SearchLabel><span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
+ </MkTextarea>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-list"></i></template>
- <template #label><SearchLabel>{{ i18n.ts._serverSettings.deliverSuspendedSoftware }}</SearchLabel></template>
- <template #footer>
- <div class="_buttons">
- <MkButton @click="federationForm.state.deliverSuspendedSoftware.push({software: '', versionRange: ''})"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
- </div>
- </template>
+ <SearchMarker :keywords="['suspended', 'software']">
+ <MkFolder>
+ <template #icon><i class="ti ti-list"></i></template>
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.deliverSuspendedSoftware }}</SearchLabel></template>
+ <template #footer>
+ <div class="_buttons">
+ <MkButton @click="federationForm.state.deliverSuspendedSoftware.push({software: '', versionRange: ''})"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+ </div>
+ </template>
- <div :class="$style.metadataRoot" class="_gaps_s">
- <MkInfo>{{ i18n.ts._serverSettings.deliverSuspendedSoftwareDescription }}</MkInfo>
- <div v-for="(element, index) in federationForm.state.deliverSuspendedSoftware" :key="index" v-panel :class="$style.fieldDragItem">
- <button class="_button" :class="$style.dragItemRemove" @click="federationForm.state.deliverSuspendedSoftware.splice(index, 1)"><i class="ti ti-x"></i></button>
- <div :class="$style.dragItemForm">
- <FormSplit :minWidth="200">
- <MkInput v-model="element.software" small :placeholder="i18n.ts.softwareName">
- </MkInput>
- <MkInput v-model="element.versionRange" small :placeholder="i18n.ts.version">
- </MkInput>
- </FormSplit>
- </div>
- </div>
- </div>
- </MkFolder>
+ <div :class="$style.metadataRoot" class="_gaps_s">
+ <MkInfo>{{ i18n.ts._serverSettings.deliverSuspendedSoftwareDescription }}</MkInfo>
+ <div v-for="(element, index) in federationForm.state.deliverSuspendedSoftware" :key="index" v-panel :class="$style.fieldDragItem">
+ <button class="_button" :class="$style.dragItemRemove" @click="federationForm.state.deliverSuspendedSoftware.splice(index, 1)"><i class="ti ti-x"></i></button>
+ <div :class="$style.dragItemForm">
+ <FormSplit :minWidth="200">
+ <MkInput v-model="element.software" small :placeholder="i18n.ts.softwareName">
+ </MkInput>
+ <MkInput v-model="element.versionRange" small :placeholder="i18n.ts.version">
+ </MkInput>
+ </FormSplit>
+ </div>
+ </div>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkSwitch v-model="federationForm.state.signToActivityPubGet">
- <template #label>{{ i18n.ts._serverSettings.signToActivityPubGet }}<span v-if="federationForm.modifiedStates.signToActivityPubGet" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._serverSettings.signToActivityPubGet_description }}</template>
- </MkSwitch>
+ <SearchMarker :keywords="['sign', 'get']">
+ <MkSwitch v-model="federationForm.state.signToActivityPubGet">
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.signToActivityPubGet }}</SearchLabel><span v-if="federationForm.modifiedStates.signToActivityPubGet" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts._serverSettings.signToActivityPubGet_description }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSwitch v-model="federationForm.state.proxyRemoteFiles">
- <template #label>{{ i18n.ts._serverSettings.proxyRemoteFiles }}<span v-if="federationForm.modifiedStates.proxyRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts._serverSettings.proxyRemoteFiles_description }}</template>
- </MkSwitch>
+ <SearchMarker :keywords="['proxy', 'remote', 'files']">
+ <MkSwitch v-model="federationForm.state.proxyRemoteFiles">
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.proxyRemoteFiles }}</SearchLabel><span v-if="federationForm.modifiedStates.proxyRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts._serverSettings.proxyRemoteFiles_description }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSwitch v-model="federationForm.state.allowExternalApRedirect">
- <template #label>{{ i18n.ts._serverSettings.allowExternalApRedirect }}<span v-if="federationForm.modifiedStates.allowExternalApRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>
- <div>{{ i18n.ts._serverSettings.allowExternalApRedirect_description }}</div>
- <div>{{ i18n.ts.needToRestartServerToApply }}</div>
- </template>
- </MkSwitch>
+ <SearchMarker :keywords="['allow', 'external', 'redirect']">
+ <MkSwitch v-model="federationForm.state.allowExternalApRedirect">
+ <template #label><SearchLabel>{{ i18n.ts._serverSettings.allowExternalApRedirect }}</SearchLabel><span v-if="federationForm.modifiedStates.allowExternalApRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption>
+ <div><SearchText>{{ i18n.ts._serverSettings.allowExternalApRedirect_description }}</SearchText></div>
+ <div>{{ i18n.ts.needToRestartServerToApply }}</div>
+ </template>
+ </MkSwitch>
+ </SearchMarker>
- <MkSwitch v-model="federationForm.state.cacheRemoteFiles">
- <template #label>{{ i18n.ts.cacheRemoteFiles }}<span v-if="federationForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
- </MkSwitch>
+ <SearchMarker :keywords="['cache', 'remote', 'files']">
+ <MkSwitch v-model="federationForm.state.cacheRemoteFiles">
+ <template #label><SearchLabel>{{ i18n.ts.cacheRemoteFiles }}</SearchLabel><span v-if="federationForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts.cacheRemoteFilesDescription }}</SearchText>{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
+ </MkSwitch>
+ </SearchMarker>
- <template v-if="federationForm.state.cacheRemoteFiles">
- <MkSwitch v-model="federationForm.state.cacheRemoteSensitiveFiles">
- <template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}<span v-if="federationForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
- <template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
- </MkSwitch>
- </template>
- </div>
- </MkFolder>
+ <template v-if="federationForm.state.cacheRemoteFiles">
+ <SearchMarker :keywords="['cache', 'remote', 'sensitive', 'files']">
+ <MkSwitch v-model="federationForm.state.cacheRemoteSensitiveFiles">
+ <template #label><SearchLabel>{{ i18n.ts.cacheRemoteSensitiveFiles }}</SearchLabel><span v-if="federationForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption><SearchText>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</SearchText></template>
+ </MkSwitch>
+ </SearchMarker>
+ </template>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkFolder>
- <template #icon><i class="ti ti-ghost"></i></template>
- <template #label>{{ i18n.ts.proxyAccount }}</template>
- <template v-if="proxyAccountForm.modified.value" #footer>
- <MkFormFooter :form="proxyAccountForm"/>
- </template>
+ <SearchMarker v-slot="slotProps" :keywords="['proxy', 'account']">
+ <MkFolder :defaultOpen="slotProps.isParentOfTarget">
+ <template #icon><SearchIcon><i class="ti ti-ghost"></i></SearchIcon></template>
+ <template #label><SearchLabel>{{ i18n.ts.proxyAccount }}</SearchLabel></template>
+ <template v-if="proxyAccountForm.modified.value" #footer>
+ <MkFormFooter :form="proxyAccountForm"/>
+ </template>
- <div class="_gaps">
- <MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
+ <div class="_gaps">
+ <MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
- <MkTextarea v-model="proxyAccountForm.state.description" :max="500" tall mfmAutocomplete :mfmPreview="true">
- <template #label>{{ i18n.ts._profile.description }}</template>
- <template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
- </MkTextarea>
- </div>
- </MkFolder>
+ <SearchMarker :keywords="['description']">
+ <MkTextarea v-model="proxyAccountForm.state.description" :max="500" tall mfmAutocomplete :mfmPreview="true">
+ <template #label><SearchLabel>{{ i18n.ts._profile.description }}</SearchLabel></template>
+ <template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
+ </MkTextarea>
+ </SearchMarker>
+ </div>
+ </MkFolder>
+ </SearchMarker>
- <MkButton primary @click="openSetupWizard">
- Open setup wizard
- </MkButton>
- </div>
+ <MkButton primary @click="openSetupWizard">
+ Open setup wizard
+ </MkButton>
+ </div>
+ </SearchMarker>
</div>
</PageWithHeader>
</template>
diff --git a/packages/frontend/src/pages/admin/system-webhook.vue b/packages/frontend/src/pages/admin/system-webhook.vue
index d5402f608c..0fd255d5f6 100644
--- a/packages/frontend/src/pages/admin/system-webhook.vue
+++ b/packages/frontend/src/pages/admin/system-webhook.vue
@@ -6,17 +6,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 900px;">
- <div class="_gaps_m">
- <MkButton primary @click="onCreateWebhookClicked">
- <i class="ti ti-plus"></i> {{ i18n.ts._webhookSettings.createWebhook }}
- </MkButton>
+ <SearchMarker path="/admin/system-webhook" label="SystemWebhook" :keywords="['webhook']" icon="ti ti-webhook">
+ <div class="_gaps_m">
+ <SearchMarker>
+ <MkButton primary @click="onCreateWebhookClicked">
+ <i class="ti ti-plus"></i> <SearchLabel>{{ i18n.ts._webhookSettings.createWebhook }}</SearchLabel>
+ </MkButton>
+ </SearchMarker>
- <FormSection>
- <div class="_gaps">
- <XItem v-for="item in webhooks" :key="item.id" :entity="item" @edit="onEditButtonClicked" @delete="onDeleteButtonClicked"/>
- </div>
- </FormSection>
- </div>
+ <FormSection>
+ <div class="_gaps">
+ <XItem v-for="item in webhooks" :key="item.id" :entity="item" @edit="onEditButtonClicked" @delete="onDeleteButtonClicked"/>
+ </div>
+ </FormSection>
+ </div>
+ </SearchMarker>
</div>
</PageWithHeader>
</template>
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index 2f639cd090..1f98fab618 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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 #caption><SearchText>{{ i18n.ts.totpDescription }}</SearchText></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">
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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>
+ <template #caption><SearchText>{{ i18n.ts.passwordLessLoginDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</div>
diff --git a/packages/frontend/src/pages/settings/account-data.vue b/packages/frontend/src/pages/settings/account-data.vue
index 5a00d7a9d7..c75667b06b 100644
--- a/packages/frontend/src/pages/settings/account-data.vue
+++ b/packages/frontend/src/pages/settings/account-data.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/account-data" :label="i18n.ts._settings.accountData" :keywords="['import', 'export', 'data', 'archive']" icon="ti ti-package">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/package_3d.png" color="#ff9100">
- <SearchKeyword>{{ i18n.ts._settings.accountDataBanner }}</SearchKeyword>
+ <SearchText>{{ i18n.ts._settings.accountDataBanner }}</SearchText>
</MkFeatureBanner>
<div class="_gaps_s">
diff --git a/packages/frontend/src/pages/settings/connect.vue b/packages/frontend/src/pages/settings/connect.vue
index 1e701096c5..28579b915f 100644
--- a/packages/frontend/src/pages/settings/connect.vue
+++ b/packages/frontend/src/pages/settings/connect.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/connect" :label="i18n.ts._settings.serviceConnection" :keywords="['app', 'service', 'connect', 'webhook', 'api', 'token']" icon="ti ti-link">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/link_3d.png" color="#ff0088">
- <SearchKeyword>{{ i18n.ts._settings.serviceConnectionBanner }}</SearchKeyword>
+ <SearchText>{{ i18n.ts._settings.serviceConnectionBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['api', 'app', 'token', 'accessToken']">
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 1b99f6dea5..cfa4df18fc 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/drive" :label="i18n.ts.drive" :keywords="['drive']" icon="ti ti-cloud">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/cloud_3d.png" color="#0059ff">
- <SearchKeyword>{{ i18n.ts._settings.driveBanner }}</SearchKeyword>
+ <SearchText>{{ i18n.ts._settings.driveBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['capacity', 'usage']">
@@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="keepOriginalFilename">
<MkSwitch v-model="keepOriginalFilename">
<template #label><SearchLabel>{{ i18n.ts.keepOriginalFilename }}</SearchLabel></template>
- <template #caption><SearchKeyword>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchKeyword></template>
+ <template #caption><SearchText>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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>
+ <template #caption><SearchText>{{ i18n.ts.enableAutoSensitiveDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</div>
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 61e3ca8b6c..eda9dfde7b 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>{{ i18n.ts._preferencesBackup.autoPreferencesBackupIsNotEnabledForThisDevice }}</div>
<div><button class="_textButton" @click="enableAutoBackup">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipAutoBackup">{{ i18n.ts.skip }}</button></div>
</MkInfo>
- <MkSuperMenu :def="menuDef" :grid="narrow" :searchIndex="SETTING_INDEX"></MkSuperMenu>
+ <MkSuperMenu :def="menuDef" :grid="narrow" :searchIndex="searchIndex"></MkSuperMenu>
</div>
</div>
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
@@ -42,12 +42,12 @@ import { instance } from '@/instance.js';
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import * as os from '@/os.js';
import { useRouter } from '@/router.js';
-import { searchIndexes } from '@/utility/settings-search-index.js';
import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utility.js';
import { store } from '@/store.js';
import { signout } from '@/signout.js';
+import { genSearchIndexes } from '@/utility/inapp-search.js';
-const SETTING_INDEX = searchIndexes; // TODO: lazy load
+const searchIndex = await import('search-index:settings').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
const indexInfo = {
title: i18n.ts.settings,
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 57aa30226b..3b05f0aa80 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/mute-block" :label="i18n.ts.muteAndBlock" icon="ti ti-ban" :keywords="['mute', 'block']">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/prohibited_3d.png" color="#ff2600">
- <SearchKeyword>{{ i18n.ts._settings.muteAndBlockBanner }}</SearchKeyword>
+ <SearchText>{{ i18n.ts._settings.muteAndBlockBanner }}</SearchText>
</MkFeatureBanner>
<div class="_gaps_s">
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index 4e8d88ab74..3ddfb81c33 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/notifications" :label="i18n.ts.notifications" :keywords="['notifications']" icon="ti ti-bell">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/bell_3d.png" color="#ffff00">
- <SearchKeyword>{{ i18n.ts._settings.notificationsBanner }}</SearchKeyword>
+ <SearchText>{{ i18n.ts._settings.notificationsBanner }}</SearchText>
</MkFeatureBanner>
<FormSection first>
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index e0fb9b86bb..c896ee8232 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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-if="!$i.isDeleted" danger @click="deleteAccount"><SearchText>{{ i18n.ts._accountDelete.requestAccountDelete }}</SearchText></MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
</div>
</MkFolder>
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index bff307ab7d..7c6ce90e7e 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/plugin" :label="i18n.ts.plugins" :keywords="['plugin', 'addon', 'extension']" icon="ti ti-plug">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/electric_plug_3d.png" color="#ffbb00">
- <SearchKeyword>{{ i18n.ts._settings.pluginBanner }}</SearchKeyword>
+ <SearchText>{{ i18n.ts._settings.pluginBanner }}</SearchText>
</MkFeatureBanner>
<MkInfo v-if="isSafeMode" warn>{{ i18n.ts.pluginsAreDisabledBecauseSafeMode }}</MkInfo>
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index 04f9b0512b..7ee5f151fa 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/preferences" :label="i18n.ts.preferences" :keywords="['general', 'preferences']" icon="ti ti-adjustments">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/gear_3d.png" color="#00ff9d">
- <SearchKeyword>{{ i18n.ts._settings.preferencesBanner }}</SearchKeyword>
+ <SearchText>{{ i18n.ts._settings.preferencesBanner }}</SearchText>
</MkFeatureBanner>
<div class="_gaps_s">
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['realtimemode']">
<MkSwitch v-model="realtimeMode">
<template #label><i class="ti ti-bolt"></i> <SearchLabel>{{ i18n.ts.realtimeMode }}</SearchLabel></template>
- <template #caption><SearchKeyword>{{ i18n.ts._settings.realtimeMode_description }}</SearchKeyword></template>
+ <template #caption><SearchText>{{ i18n.ts._settings.realtimeMode_description }}</SearchText></template>
</MkSwitch>
</SearchMarker>
@@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="pollingInterval">
<MkRange v-model="pollingInterval" :min="1" :max="3" :step="1" easing :showTicks="true" :textConverter="(v) => v === 1 ? i18n.ts.low : v === 2 ? i18n.ts.middle : v === 3 ? i18n.ts.high : ''">
<template #label><SearchLabel>{{ i18n.ts._settings.contentsUpdateFrequency }}</SearchLabel></template>
- <template #caption><SearchKeyword>{{ i18n.ts._settings.contentsUpdateFrequency_description }}</SearchKeyword><br><SearchKeyword>{{ i18n.ts._settings.contentsUpdateFrequency_description2 }}</SearchKeyword></template>
+ <template #caption><SearchText>{{ i18n.ts._settings.contentsUpdateFrequency_description }}</SearchText><br><SearchText>{{ i18n.ts._settings.contentsUpdateFrequency_description2 }}</SearchText></template>
<template #prefix><i class="ti ti-player-play"></i></template>
<template #suffix><i class="ti ti-player-track-next"></i></template>
</MkRange>
@@ -165,7 +165,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="collapseRenotes">
<MkSwitch v-model="collapseRenotes">
<template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template>
- <template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template>
+ <template #caption><SearchText>{{ i18n.ts.collapseRenotesDescription }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -449,7 +449,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/mens_room_3d.png" color="#0011ff">
- <SearchKeyword>{{ i18n.ts._settings.accessibilityBanner }}</SearchKeyword>
+ <SearchText>{{ i18n.ts._settings.accessibilityBanner }}</SearchText>
</MkFeatureBanner>
<div class="_gaps_s">
@@ -489,7 +489,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="enablePullToRefresh">
<MkSwitch v-model="enablePullToRefresh">
<template #label><SearchLabel>{{ i18n.ts._settings.enablePullToRefresh }}</SearchLabel></template>
- <template #caption><SearchKeyword>{{ i18n.ts._settings.enablePullToRefresh_description }}</SearchKeyword></template>
+ <template #caption><SearchText>{{ i18n.ts._settings.enablePullToRefresh_description }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -571,7 +571,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="animation">
<MkSwitch :modelValue="!reduceAnimation" @update:modelValue="v => reduceAnimation = !v">
<template #label><SearchLabel>{{ i18n.ts._settings.uiAnimations }}</SearchLabel></template>
- <template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
+ <template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -580,7 +580,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="useBlurEffect">
<MkSwitch v-model="useBlurEffect">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
- <template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
+ <template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -589,7 +589,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="useBlurEffectForModal">
<MkSwitch v-model="useBlurEffectForModal">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
- <template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
+ <template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -598,7 +598,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="enableHighQualityImagePlaceholders">
<MkSwitch v-model="enableHighQualityImagePlaceholders">
<template #label><SearchLabel>{{ i18n.ts._settings.enableHighQualityImagePlaceholders }}</SearchLabel></template>
- <template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
+ <template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -607,7 +607,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="useStickyIcons">
<MkSwitch v-model="useStickyIcons">
<template #label><SearchLabel>{{ i18n.ts._settings.useStickyIcons }}</SearchLabel></template>
- <template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
+ <template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index 4e6425667e..3977359c54 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -7,13 +7,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/privacy" :label="i18n.ts.privacy" :keywords="['privacy']" icon="ti ti-lock-open">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/unlocked_3d.png" color="#aeff00">
- <SearchKeyword>{{ i18n.ts._settings.privacyBanner }}</SearchKeyword>
+ <SearchText>{{ i18n.ts._settings.privacyBanner }}</SearchText>
</MkFeatureBanner>
<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>
+ <template #caption><SearchText>{{ i18n.ts.lockedAccountInfo }}</SearchText></template>
</MkSwitch>
</SearchMarker>
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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>
+ <template #caption><SearchText>{{ i18n.ts.makeReactionsPublicDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
@@ -53,28 +53,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<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>
+ <template #caption><SearchText>{{ i18n.ts.hideOnlineStatusDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<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>
+ <template #caption><SearchText>{{ i18n.ts.noCrawleDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<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>
+ <template #caption><SearchText>{{ i18n.ts.preventAiLearningDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<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>
+ <template #caption><SearchText>{{ i18n.ts.makeExplorableDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
@@ -146,7 +146,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<template #caption>
- <div><SearchKeyword>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</SearchKeyword></div>
+ <div><SearchText>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</SearchText></div>
</template>
</FormSlot>
</SearchMarker>
@@ -183,7 +183,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<template #caption>
- <div><SearchKeyword>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</SearchKeyword></div>
+ <div><SearchText>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</SearchText></div>
</template>
</FormSlot>
</SearchMarker>
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index ce7f31cd23..e2679623ef 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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><SearchText>{{ i18n.ts._profile.followedMessageDescription }}</SearchText></div>
<div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div>
</template>
</MkInput>
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index 2562993be3..c954b9dd5a 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/security" :label="i18n.ts.security" :keywords="['security']" icon="ti ti-lock" :inlining="['2fa']">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/locked_with_key_3d.png" color="#ffbf00">
- <SearchKeyword>{{ i18n.ts._settings.securityBanner }}</SearchKeyword>
+ <SearchText>{{ i18n.ts._settings.securityBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['password']">
@@ -24,30 +24,34 @@ SPDX-License-Identifier: AGPL-3.0-only
<X2fa/>
- <FormSection>
- <template #label>{{ i18n.ts.signinHistory }}</template>
- <MkPagination :paginator="paginator" withControl>
- <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>
+ <SearchMarker :keywords="['signin', 'login', 'history', 'log']">
+ <FormSection>
+ <template #label><SearchLabel>{{ i18n.ts.signinHistory }}</SearchLabel></template>
+ <MkPagination :paginator="paginator" withControl>
+ <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>
+ </SearchMarker>
- <FormSection>
- <FormSlot>
- <MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> {{ i18n.ts.regenerateLoginToken }}</MkButton>
- <template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
- </FormSlot>
- </FormSection>
+ <SearchMarker :keywords="['regenerate', 'refresh', 'reset', 'token']">
+ <FormSection>
+ <FormSlot>
+ <MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> <SearchLabel>{{ i18n.ts.regenerateLoginToken }}</SearchLabel></MkButton>
+ <template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
+ </FormSlot>
+ </FormSection>
+ </SearchMarker>
</div>
</SearchMarker>
</template>
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index 590db19bca..ea5b347525 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/sounds" :label="i18n.ts.sounds" :keywords="['sounds']" icon="ti ti-music">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/speaker_high_volume_3d.png" color="#ff006f">
- <SearchKeyword>{{ i18n.ts._settings.soundsBanner }}</SearchKeyword>
+ <SearchText>{{ i18n.ts._settings.soundsBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['mute']">
diff --git a/packages/frontend/src/router.definition.ts b/packages/frontend/src/router.definition.ts
index 7edc5ed9b7..57d9a860d6 100644
--- a/packages/frontend/src/router.definition.ts
+++ b/packages/frontend/src/router.definition.ts
@@ -492,10 +492,6 @@ export const ROUTE_DEF = [{
name: 'performance',
component: page(() => import('@/pages/admin/performance.vue')),
}, {
- path: '/server-rules',
- name: 'server-rules',
- component: page(() => import('@/pages/admin/server-rules.vue')),
- }, {
path: '/invites',
name: 'invites',
component: page(() => import('@/pages/admin/invites.vue')),
diff --git a/packages/frontend/src/utility/inapp-search.ts b/packages/frontend/src/utility/inapp-search.ts
new file mode 100644
index 0000000000..cbc3d87ff8
--- /dev/null
+++ b/packages/frontend/src/utility/inapp-search.ts
@@ -0,0 +1,37 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { GeneratedSearchIndexItem } from 'search-index';
+
+export type SearchIndexItem = {
+ id: string;
+ parentId?: string;
+ path?: string;
+ label: string;
+ keywords: string[];
+ texts: string[];
+ icon?: string;
+};
+
+export function genSearchIndexes(generated: GeneratedSearchIndexItem[]): SearchIndexItem[] {
+ const rootMods = new Map(generated.map(item => [item.id, item]));
+
+ // link inlining here
+ for (const item of generated) {
+ if (item.inlining) {
+ for (const id of item.inlining) {
+ const inline = rootMods.get(id);
+ if (inline) {
+ inline.parentId = item.id;
+ inline.path = item.path;
+ } else {
+ console.log('[Settings Search Index] Failed to inline', id);
+ }
+ }
+ }
+ }
+
+ return generated;
+}
diff --git a/packages/frontend/src/utility/settings-search-index.ts b/packages/frontend/src/utility/settings-search-index.ts
deleted file mode 100644
index 8506e4fe2f..0000000000
--- a/packages/frontend/src/utility/settings-search-index.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { searchIndexes as generated } from 'search-index:settings';
-import type { GeneratedSearchIndexItem } from 'search-index:settings';
-
-export type SearchIndexItem = {
- id: string;
- parentId?: string;
- path?: string;
- label: string;
- keywords: string[];
- icon?: string;
-};
-
-const rootMods = new Map(generated.map(item => [item.id, item]));
-
-// link inlining here
-for (const item of generated) {
- if (item.inlining) {
- for (const id of item.inlining) {
- const inline = rootMods.get(id);
- if (inline) {
- inline.parentId = item.id;
- inline.path = item.path;
- } else {
- console.log('[Settings Search Index] Failed to inline', id);
- }
- }
- }
-}
-
-export const searchIndexes: SearchIndexItem[] = generated;
-
diff --git a/packages/frontend/src/utility/virtual.d.ts b/packages/frontend/src/utility/virtual.d.ts
index 63dc4372b7..00f01992aa 100644
--- a/packages/frontend/src/utility/virtual.d.ts
+++ b/packages/frontend/src/utility/virtual.d.ts
@@ -3,16 +3,25 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+type XGeneratedSearchIndexItem = {
+ id: string;
+ parentId?: string;
+ path?: string;
+ label: string;
+ keywords: string[];
+ texts: string[];
+ icon?: string;
+ inlining?: string[];
+};
+
+declare module 'search-index' {
+ export type GeneratedSearchIndexItem = XGeneratedSearchIndexItem;
+}
+
declare module 'search-index:settings' {
- export type GeneratedSearchIndexItem = {
- id: string;
- parentId?: string;
- path?: string;
- label: string;
- keywords: string[];
- icon?: string;
- inlining?: string[];
- };
+ export const searchIndexes: XGeneratedSearchIndexItem[];
+}
- export const searchIndexes: GeneratedSearchIndexItem[];
+declare module 'search-index:admin' {
+ export const searchIndexes: XGeneratedSearchIndexItem[];
}
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 77b5b48a18..7b6bf436b1 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -28,6 +28,11 @@ export const searchIndexes = [{
mainVirtualModule: 'search-index:settings',
modulesToHmrOnUpdate: ['src/pages/settings/index.vue'],
verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
+}, {
+ targetFilePaths: ['src/pages/admin/*.vue'],
+ mainVirtualModule: 'search-index:admin',
+ modulesToHmrOnUpdate: ['src/pages/admin/index.vue'],
+ verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
}] satisfies SearchIndexOptions[];
/**