summaryrefslogtreecommitdiff
path: root/packages/client/src/pages/admin
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-06-20 17:38:49 +0900
committerGitHub <noreply@github.com>2022-06-20 17:38:49 +0900
commit699f24f3dcdb156838eb70602885c0b2cdd02cbc (patch)
tree45b28eeadbb7d9e7f3847bd04f75ed010153619a /packages/client/src/pages/admin
parentrefactor: チャットルームをComposition API化 (#8850) (diff)
downloadmisskey-699f24f3dcdb156838eb70602885c0b2cdd02cbc.tar.gz
misskey-699f24f3dcdb156838eb70602885c0b2cdd02cbc.tar.bz2
misskey-699f24f3dcdb156838eb70602885c0b2cdd02cbc.zip
refactor(client): Refine routing (#8846)
Diffstat (limited to 'packages/client/src/pages/admin')
-rw-r--r--packages/client/src/pages/admin/_header_.vue249
-rw-r--r--packages/client/src/pages/admin/abuses.vue80
-rw-r--r--packages/client/src/pages/admin/ads.vue103
-rw-r--r--packages/client/src/pages/admin/announcements.vue81
-rw-r--r--packages/client/src/pages/admin/bot-protection.vue1
-rw-r--r--packages/client/src/pages/admin/database.vue23
-rw-r--r--packages/client/src/pages/admin/email-settings.vue121
-rw-r--r--packages/client/src/pages/admin/emojis.vue184
-rw-r--r--packages/client/src/pages/admin/files.vue119
-rw-r--r--packages/client/src/pages/admin/index.vue50
-rw-r--r--packages/client/src/pages/admin/instance-block.vue38
-rw-r--r--packages/client/src/pages/admin/integrations.vue27
-rw-r--r--packages/client/src/pages/admin/object-storage.vue137
-rw-r--r--packages/client/src/pages/admin/other-settings.vue41
-rw-r--r--packages/client/src/pages/admin/overview.vue22
-rw-r--r--packages/client/src/pages/admin/proxy-account.vue23
-rw-r--r--packages/client/src/pages/admin/queue.vue57
-rw-r--r--packages/client/src/pages/admin/relays.vue63
-rw-r--r--packages/client/src/pages/admin/security.vue70
-rw-r--r--packages/client/src/pages/admin/settings.vue265
-rw-r--r--packages/client/src/pages/admin/users.vue172
21 files changed, 1136 insertions, 790 deletions
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
new file mode 100644
index 0000000000..9e11d065d9
--- /dev/null
+++ b/packages/client/src/pages/admin/_header_.vue
@@ -0,0 +1,249 @@
+<template>
+<div ref="el" class="fdidabkc" :style="{ background: bg }" @click="onClick">
+ <template v-if="metadata">
+ <div class="titleContainer" @click="showTabsPopup">
+ <i v-if="metadata.icon" class="icon" :class="metadata.icon"></i>
+
+ <div class="title">
+ <div class="title">{{ metadata.title }}</div>
+ </div>
+ </div>
+ <div class="tabs">
+ <button v-for="tab in tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick">
+ <i v-if="tab.icon" class="icon" :class="tab.icon"></i>
+ <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
+ </button>
+ </div>
+ </template>
+ <div class="buttons right">
+ <template v-if="actions">
+ <template v-for="action in actions">
+ <MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
+ <button v-else v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
+ </template>
+ </template>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted, onUnmounted, ref, inject } from 'vue';
+import tinycolor from 'tinycolor2';
+import { popupMenu } from '@/os';
+import { url } from '@/config';
+import { scrollToTop } from '@/scripts/scroll';
+import MkButton from '@/components/ui/button.vue';
+import { i18n } from '@/i18n';
+import { globalEvents } from '@/events';
+import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata';
+
+const props = defineProps<{
+ tabs?: {
+ title: string;
+ active: boolean;
+ icon?: string;
+ iconOnly?: boolean;
+ onClick: () => void;
+ }[];
+ actions?: {
+ text: string;
+ icon: string;
+ asFullButton?: boolean;
+ handler: (ev: MouseEvent) => void;
+ }[];
+ thin?: boolean;
+}>();
+
+const metadata = injectPageMetadata();
+
+const el = ref<HTMLElement>(null);
+const bg = ref(null);
+const height = ref(0);
+const hasTabs = computed(() => {
+ return props.tabs && props.tabs.length > 0;
+});
+
+const showTabsPopup = (ev: MouseEvent) => {
+ if (!hasTabs.value) return;
+ if (!narrow.value) return;
+ ev.preventDefault();
+ ev.stopPropagation();
+ const menu = props.tabs.map(tab => ({
+ text: tab.title,
+ icon: tab.icon,
+ action: tab.onClick,
+ }));
+ popupMenu(menu, ev.currentTarget ?? ev.target);
+};
+
+const preventDrag = (ev: TouchEvent) => {
+ ev.stopPropagation();
+};
+
+const onClick = () => {
+ scrollToTop(el.value, { behavior: 'smooth' });
+};
+
+const calcBg = () => {
+ const rawBg = metadata?.bg || 'var(--bg)';
+ const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
+ tinyBg.setAlpha(0.85);
+ bg.value = tinyBg.toRgbString();
+};
+
+onMounted(() => {
+ calcBg();
+ globalEvents.on('themeChanged', calcBg);
+});
+
+onUnmounted(() => {
+ globalEvents.off('themeChanged', calcBg);
+});
+</script>
+
+<style lang="scss" scoped>
+.fdidabkc {
+ --height: 60px;
+ display: flex;
+ position: sticky;
+ top: var(--stickyTop, 0);
+ z-index: 1000;
+ width: 100%;
+ -webkit-backdrop-filter: var(--blur, blur(15px));
+ backdrop-filter: var(--blur, blur(15px));
+
+ > .buttons {
+ --margin: 8px;
+ display: flex;
+ align-items: center;
+ height: var(--height);
+ margin: 0 var(--margin);
+
+ &.right {
+ margin-left: auto;
+ }
+
+ &:empty {
+ width: var(--height);
+ }
+
+ > .button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: calc(var(--height) - (var(--margin) * 2));
+ width: calc(var(--height) - (var(--margin) * 2));
+ box-sizing: border-box;
+ position: relative;
+ border-radius: 5px;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.05);
+ }
+
+ &.highlighted {
+ color: var(--accent);
+ }
+ }
+
+ > .fullButton {
+ & + .fullButton {
+ margin-left: 12px;
+ }
+ }
+ }
+
+ > .titleContainer {
+ display: flex;
+ align-items: center;
+ max-width: 400px;
+ overflow: auto;
+ white-space: nowrap;
+ text-align: left;
+ font-weight: bold;
+ flex-shrink: 0;
+ margin-left: 24px;
+
+ > .avatar {
+ $size: 32px;
+ display: inline-block;
+ width: $size;
+ height: $size;
+ vertical-align: bottom;
+ margin: 0 8px;
+ pointer-events: none;
+ }
+
+ > .icon {
+ margin-right: 8px;
+ }
+
+ > .title {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ line-height: 1.1;
+
+ > .subtitle {
+ opacity: 0.6;
+ font-size: 0.8em;
+ font-weight: normal;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &.activeTab {
+ text-align: center;
+
+ > .chevron {
+ display: inline-block;
+ margin-left: 6px;
+ }
+ }
+ }
+ }
+ }
+
+ > .tabs {
+ margin-left: 16px;
+ font-size: 0.8em;
+ overflow: auto;
+ white-space: nowrap;
+
+ > .tab {
+ display: inline-block;
+ position: relative;
+ padding: 0 10px;
+ height: 100%;
+ font-weight: normal;
+ opacity: 0.7;
+
+ &:hover {
+ opacity: 1;
+ }
+
+ &.active {
+ opacity: 1;
+
+ &:after {
+ content: "";
+ display: block;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ margin: 0 auto;
+ width: 100%;
+ height: 3px;
+ background: var(--accent);
+ }
+ }
+
+ > .icon + .title {
+ margin-left: 8px;
+ }
+ }
+ }
+}
+</style>
diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue
index e1d0361c0b..2b6dadf7c6 100644
--- a/packages/client/src/pages/admin/abuses.vue
+++ b/packages/client/src/pages/admin/abuses.vue
@@ -1,28 +1,31 @@
<template>
-<div class="lcixvhis">
- <div class="_section reports">
- <div class="_content">
- <div class="inputs" style="display: flex;">
- <MkSelect v-model="state" style="margin: 0; flex: 1;">
- <template #label>{{ $ts.state }}</template>
- <option value="all">{{ $ts.all }}</option>
- <option value="unresolved">{{ $ts.unresolved }}</option>
- <option value="resolved">{{ $ts.resolved }}</option>
- </MkSelect>
- <MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
- <template #label>{{ $ts.reporteeOrigin }}</template>
- <option value="combined">{{ $ts.all }}</option>
- <option value="local">{{ $ts.local }}</option>
- <option value="remote">{{ $ts.remote }}</option>
- </MkSelect>
- <MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
- <template #label>{{ $ts.reporterOrigin }}</template>
- <option value="combined">{{ $ts.all }}</option>
- <option value="local">{{ $ts.local }}</option>
- <option value="remote">{{ $ts.remote }}</option>
- </MkSelect>
- </div>
- <!-- TODO
+<MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="900">
+ <div class="lcixvhis">
+ <div class="_section reports">
+ <div class="_content">
+ <div class="inputs" style="display: flex;">
+ <MkSelect v-model="state" style="margin: 0; flex: 1;">
+ <template #label>{{ $ts.state }}</template>
+ <option value="all">{{ $ts.all }}</option>
+ <option value="unresolved">{{ $ts.unresolved }}</option>
+ <option value="resolved">{{ $ts.resolved }}</option>
+ </MkSelect>
+ <MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
+ <template #label>{{ $ts.reporteeOrigin }}</template>
+ <option value="combined">{{ $ts.all }}</option>
+ <option value="local">{{ $ts.local }}</option>
+ <option value="remote">{{ $ts.remote }}</option>
+ </MkSelect>
+ <MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
+ <template #label>{{ $ts.reporterOrigin }}</template>
+ <option value="combined">{{ $ts.all }}</option>
+ <option value="local">{{ $ts.local }}</option>
+ <option value="remote">{{ $ts.remote }}</option>
+ </MkSelect>
+ </div>
+ <!-- TODO
<div class="inputs" style="display: flex; padding-top: 1.2em;">
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false">
<span>{{ $ts.username }}</span>
@@ -33,24 +36,27 @@
</div>
-->
- <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
- <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
- </MkPagination>
+ <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
+ <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
+ </MkPagination>
+ </div>
+ </div>
</div>
- </div>
-</div>
+ </MkSpacer>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
+import XHeader from './_header_.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
import MkPagination from '@/components/ui/pagination.vue';
import XAbuseReport from '@/components/abuse-report.vue';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let reports = $ref<InstanceType<typeof MkPagination>>();
@@ -74,12 +80,14 @@ function resolved(reportId) {
reports.removeItem(item => item.id === reportId);
}
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.abuseReports,
- icon: 'fas fa-exclamation-circle',
- bg: 'var(--bg)',
- }
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.abuseReports,
+ icon: 'fas fa-exclamation-circle',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/ads.vue b/packages/client/src/pages/admin/ads.vue
index b18e08db96..05557469e7 100644
--- a/packages/client/src/pages/admin/ads.vue
+++ b/packages/client/src/pages/admin/ads.vue
@@ -1,21 +1,23 @@
<template>
-<MkSpacer :content-max="900">
- <div class="uqshojas">
- <div v-for="ad in ads" class="_panel _formRoot ad">
- <MkAd v-if="ad.url" :specify="ad"/>
- <MkInput v-model="ad.url" type="url" class="_formBlock">
- <template #label>URL</template>
- </MkInput>
- <MkInput v-model="ad.imageUrl" class="_formBlock">
- <template #label>{{ i18n.ts.imageUrl }}</template>
- </MkInput>
- <FormRadios v-model="ad.place" class="_formBlock">
- <template #label>Form</template>
- <option value="square">square</option>
- <option value="horizontal">horizontal</option>
- <option value="horizontal-big">horizontal-big</option>
- </FormRadios>
- <!--
+<MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="900">
+ <div class="uqshojas">
+ <div v-for="ad in ads" class="_panel _formRoot ad">
+ <MkAd v-if="ad.url" :specify="ad"/>
+ <MkInput v-model="ad.url" type="url" class="_formBlock">
+ <template #label>URL</template>
+ </MkInput>
+ <MkInput v-model="ad.imageUrl" class="_formBlock">
+ <template #label>{{ i18n.ts.imageUrl }}</template>
+ </MkInput>
+ <FormRadios v-model="ad.place" class="_formBlock">
+ <template #label>Form</template>
+ <option value="square">square</option>
+ <option value="horizontal">horizontal</option>
+ <option value="horizontal-big">horizontal-big</option>
+ </FormRadios>
+ <!--
<div style="margin: 32px 0;">
{{ i18n.ts.priority }}
<MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio>
@@ -23,36 +25,38 @@
<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
</div>
-->
- <FormSplit>
- <MkInput v-model="ad.ratio" type="number">
- <template #label>{{ i18n.ts.ratio }}</template>
- </MkInput>
- <MkInput v-model="ad.expiresAt" type="date">
- <template #label>{{ i18n.ts.expiration }}</template>
- </MkInput>
- </FormSplit>
- <MkTextarea v-model="ad.memo" class="_formBlock">
- <template #label>{{ i18n.ts.memo }}</template>
- </MkTextarea>
- <div class="buttons _formBlock">
- <MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
- <MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
+ <FormSplit>
+ <MkInput v-model="ad.ratio" type="number">
+ <template #label>{{ i18n.ts.ratio }}</template>
+ </MkInput>
+ <MkInput v-model="ad.expiresAt" type="date">
+ <template #label>{{ i18n.ts.expiration }}</template>
+ </MkInput>
+ </FormSplit>
+ <MkTextarea v-model="ad.memo" class="_formBlock">
+ <template #label>{{ i18n.ts.memo }}</template>
+ </MkTextarea>
+ <div class="buttons _formBlock">
+ <MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
+ <MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
+ </div>
</div>
</div>
- </div>
-</MkSpacer>
+ </MkSpacer>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
+import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let ads: any[] = $ref([]);
@@ -81,7 +85,7 @@ function remove(ad) {
if (canceled) return;
ads = ads.filter(x => x !== ad);
os.apiWithDialog('admin/ad/delete', {
- id: ad.id
+ id: ad.id,
});
});
}
@@ -90,28 +94,29 @@ function save(ad) {
if (ad.id == null) {
os.apiWithDialog('admin/ad/create', {
...ad,
- expiresAt: new Date(ad.expiresAt).getTime()
+ expiresAt: new Date(ad.expiresAt).getTime(),
});
} else {
os.apiWithDialog('admin/ad/update', {
...ad,
- expiresAt: new Date(ad.expiresAt).getTime()
+ expiresAt: new Date(ad.expiresAt).getTime(),
});
}
}
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.ads,
- icon: 'fas fa-audio-description',
- bg: 'var(--bg)',
- actions: [{
- asFullButton: true,
- icon: 'fas fa-plus',
- text: i18n.ts.add,
- handler: add,
- }],
- }
+const headerActions = $computed(() => [{
+ asFullButton: true,
+ icon: 'fas fa-plus',
+ text: i18n.ts.add,
+ handler: add,
+}]);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.ads,
+ icon: 'fas fa-audio-description',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/announcements.vue b/packages/client/src/pages/admin/announcements.vue
index 97774975de..025897d093 100644
--- a/packages/client/src/pages/admin/announcements.vue
+++ b/packages/client/src/pages/admin/announcements.vue
@@ -1,34 +1,40 @@
<template>
-<div class="ztgjmzrw">
- <section v-for="announcement in announcements" class="_card _gap announcements">
- <div class="_content announcement">
- <MkInput v-model="announcement.title">
- <template #label>{{ i18n.ts.title }}</template>
- </MkInput>
- <MkTextarea v-model="announcement.text">
- <template #label>{{ i18n.ts.text }}</template>
- </MkTextarea>
- <MkInput v-model="announcement.imageUrl">
- <template #label>{{ i18n.ts.imageUrl }}</template>
- </MkInput>
- <p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
- <div class="buttons">
- <MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
- <MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
- </div>
+<MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="900">
+ <div class="ztgjmzrw">
+ <section v-for="announcement in announcements" class="_card _gap announcements">
+ <div class="_content announcement">
+ <MkInput v-model="announcement.title">
+ <template #label>{{ i18n.ts.title }}</template>
+ </MkInput>
+ <MkTextarea v-model="announcement.text">
+ <template #label>{{ i18n.ts.text }}</template>
+ </MkTextarea>
+ <MkInput v-model="announcement.imageUrl">
+ <template #label>{{ i18n.ts.imageUrl }}</template>
+ </MkInput>
+ <p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
+ <div class="buttons">
+ <MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
+ <MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
+ </div>
+ </div>
+ </section>
</div>
- </section>
-</div>
+ </MkSpacer>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
+import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let announcements: any[] = $ref([]);
@@ -41,7 +47,7 @@ function add() {
id: null,
title: '',
text: '',
- imageUrl: null
+ imageUrl: null,
});
}
@@ -61,41 +67,42 @@ function save(announcement) {
os.api('admin/announcements/create', announcement).then(() => {
os.alert({
type: 'success',
- text: i18n.ts.saved
+ text: i18n.ts.saved,
});
}).catch(err => {
os.alert({
type: 'error',
- text: err
+ text: err,
});
});
} else {
os.api('admin/announcements/update', announcement).then(() => {
os.alert({
type: 'success',
- text: i18n.ts.saved
+ text: i18n.ts.saved,
});
}).catch(err => {
os.alert({
type: 'error',
- text: err
+ text: err,
});
});
}
}
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.announcements,
- icon: 'fas fa-broadcast-tower',
- bg: 'var(--bg)',
- actions: [{
- asFullButton: true,
- icon: 'fas fa-plus',
- text: i18n.ts.add,
- handler: add,
- }],
- }
+const headerActions = $computed(() => [{
+ asFullButton: true,
+ icon: 'fas fa-plus',
+ text: i18n.ts.add,
+ handler: add,
+}]);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.announcements,
+ icon: 'fas fa-broadcast-tower',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue
index 30fee5015a..d2e7919b4f 100644
--- a/packages/client/src/pages/admin/bot-protection.vue
+++ b/packages/client/src/pages/admin/bot-protection.vue
@@ -51,7 +51,6 @@ import FormButton from '@/components/ui/button.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue'));
diff --git a/packages/client/src/pages/admin/database.vue b/packages/client/src/pages/admin/database.vue
index d3519922b1..b9c5f9e393 100644
--- a/packages/client/src/pages/admin/database.vue
+++ b/packages/client/src/pages/admin/database.vue
@@ -1,12 +1,13 @@
-<template>
-<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
+<template><MkStickyContainer>
+ <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
<FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory">
<MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;">
<template #key>{{ table[0] }}</template>
<template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template>
</MkKeyValue>
</FormSuspense>
-</MkSpacer>
+</MkSpacer></MkStickyContainer>
</template>
<script lang="ts" setup>
@@ -14,18 +15,20 @@ import { } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import bytes from '@/filters/bytes';
import number from '@/filters/number';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.database,
- icon: 'fas fa-database',
- bg: 'var(--bg)',
- }
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.database,
+ icon: 'fas fa-database',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/email-settings.vue b/packages/client/src/pages/admin/email-settings.vue
index aa13043193..5487c5f333 100644
--- a/packages/client/src/pages/admin/email-settings.vue
+++ b/packages/client/src/pages/admin/email-settings.vue
@@ -1,49 +1,53 @@
<template>
-<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- <div class="_formRoot">
- <FormSwitch v-model="enableEmail" class="_formBlock">
- <template #label>{{ i18n.ts.enableEmail }}</template>
- <template #caption>{{ i18n.ts.emailConfigInfo }}</template>
- </FormSwitch>
+<MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
+ <FormSuspense :p="init">
+ <div class="_formRoot">
+ <FormSwitch v-model="enableEmail" class="_formBlock">
+ <template #label>{{ i18n.ts.enableEmail }}</template>
+ <template #caption>{{ i18n.ts.emailConfigInfo }}</template>
+ </FormSwitch>
- <template v-if="enableEmail">
- <FormInput v-model="email" type="email" class="_formBlock">
- <template #label>{{ i18n.ts.emailAddress }}</template>
- </FormInput>
+ <template v-if="enableEmail">
+ <FormInput v-model="email" type="email" class="_formBlock">
+ <template #label>{{ i18n.ts.emailAddress }}</template>
+ </FormInput>
- <FormSection>
- <template #label>{{ i18n.ts.smtpConfig }}</template>
- <FormSplit :min-width="280">
- <FormInput v-model="smtpHost" class="_formBlock">
- <template #label>{{ i18n.ts.smtpHost }}</template>
- </FormInput>
- <FormInput v-model="smtpPort" type="number" class="_formBlock">
- <template #label>{{ i18n.ts.smtpPort }}</template>
- </FormInput>
- </FormSplit>
- <FormSplit :min-width="280">
- <FormInput v-model="smtpUser" class="_formBlock">
- <template #label>{{ i18n.ts.smtpUser }}</template>
- </FormInput>
- <FormInput v-model="smtpPass" type="password" class="_formBlock">
- <template #label>{{ i18n.ts.smtpPass }}</template>
- </FormInput>
- </FormSplit>
- <FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
- <FormSwitch v-model="smtpSecure" class="_formBlock">
- <template #label>{{ i18n.ts.smtpSecure }}</template>
- <template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
- </FormSwitch>
- </FormSection>
- </template>
- </div>
- </FormSuspense>
-</MkSpacer>
+ <FormSection>
+ <template #label>{{ i18n.ts.smtpConfig }}</template>
+ <FormSplit :min-width="280">
+ <FormInput v-model="smtpHost" class="_formBlock">
+ <template #label>{{ i18n.ts.smtpHost }}</template>
+ </FormInput>
+ <FormInput v-model="smtpPort" type="number" class="_formBlock">
+ <template #label>{{ i18n.ts.smtpPort }}</template>
+ </FormInput>
+ </FormSplit>
+ <FormSplit :min-width="280">
+ <FormInput v-model="smtpUser" class="_formBlock">
+ <template #label>{{ i18n.ts.smtpUser }}</template>
+ </FormInput>
+ <FormInput v-model="smtpPass" type="password" class="_formBlock">
+ <template #label>{{ i18n.ts.smtpPass }}</template>
+ </FormInput>
+ </FormSplit>
+ <FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
+ <FormSwitch v-model="smtpSecure" class="_formBlock">
+ <template #label>{{ i18n.ts.smtpSecure }}</template>
+ <template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
+ </FormSwitch>
+ </FormSection>
+ </template>
+ </div>
+ </FormSuspense>
+ </MkSpacer>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
+import XHeader from './_header_.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormInfo from '@/components/ui/info.vue';
@@ -51,9 +55,9 @@ 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';
-import * as symbols from '@/symbols';
import { fetchInstance, instance } from '@/instance';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let enableEmail: boolean = $ref(false);
let email: any = $ref(null);
@@ -78,13 +82,13 @@ async function testEmail() {
const { canceled, result: destination } = await os.inputText({
title: i18n.ts.destination,
type: 'email',
- placeholder: instance.maintainerEmail
+ placeholder: instance.maintainerEmail,
});
if (canceled) return;
os.apiWithDialog('admin/send-email', {
to: destination,
subject: 'Test email',
- text: 'Yo'
+ text: 'Yo',
});
}
@@ -102,21 +106,22 @@ function save() {
});
}
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.emailServer,
- icon: 'fas fa-envelope',
- bg: 'var(--bg)',
- actions: [{
- asFullButton: true,
- text: i18n.ts.testEmail,
- handler: testEmail,
- }, {
- asFullButton: true,
- icon: 'fas fa-check',
- text: i18n.ts.save,
- handler: save,
- }],
- }
+const headerActions = $computed(() => [{
+ asFullButton: true,
+ text: i18n.ts.testEmail,
+ handler: testEmail,
+}, {
+ asFullButton: true,
+ icon: 'fas fa-check',
+ text: i18n.ts.save,
+ handler: save,
+}]);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.emailServer,
+ icon: 'fas fa-envelope',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue
index 8ca5b3d65c..9d6b56dbc5 100644
--- a/packages/client/src/pages/admin/emojis.vue
+++ b/packages/client/src/pages/admin/emojis.vue
@@ -1,69 +1,75 @@
<template>
-<MkSpacer :content-max="900">
- <div class="ogwlenmc">
- <div v-if="tab === 'local'" class="local">
- <MkInput v-model="query" :debounce="true" type="search">
- <template #prefix><i class="fas fa-search"></i></template>
- <template #label>{{ $ts.search }}</template>
- </MkInput>
- <MkSwitch v-model="selectMode" style="margin: 8px 0;">
- <template #label>Select mode</template>
- </MkSwitch>
- <div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
- <MkButton inline @click="selectAll">Select all</MkButton>
- <MkButton inline @click="setCategoryBulk">Set category</MkButton>
- <MkButton inline @click="addTagBulk">Add tag</MkButton>
- <MkButton inline @click="removeTagBulk">Remove tag</MkButton>
- <MkButton inline @click="setTagBulk">Set tag</MkButton>
- <MkButton inline danger @click="delBulk">Delete</MkButton>
- </div>
- <MkPagination ref="emojisPaginationComponent" :pagination="pagination">
- <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
- <template v-slot="{items}">
- <div class="ldhfsamy">
- <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
- <img :src="emoji.url" class="img" :alt="emoji.name"/>
- <div class="body">
- <div class="name _monospace">{{ emoji.name }}</div>
- <div class="info">{{ emoji.category }}</div>
- </div>
- </button>
+<div>
+ <MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="900">
+ <div class="ogwlenmc">
+ <div v-if="tab === 'local'" class="local">
+ <MkInput v-model="query" :debounce="true" type="search">
+ <template #prefix><i class="fas fa-search"></i></template>
+ <template #label>{{ $ts.search }}</template>
+ </MkInput>
+ <MkSwitch v-model="selectMode" style="margin: 8px 0;">
+ <template #label>Select mode</template>
+ </MkSwitch>
+ <div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+ <MkButton inline @click="selectAll">Select all</MkButton>
+ <MkButton inline @click="setCategoryBulk">Set category</MkButton>
+ <MkButton inline @click="addTagBulk">Add tag</MkButton>
+ <MkButton inline @click="removeTagBulk">Remove tag</MkButton>
+ <MkButton inline @click="setTagBulk">Set tag</MkButton>
+ <MkButton inline danger @click="delBulk">Delete</MkButton>
</div>
- </template>
- </MkPagination>
- </div>
+ <MkPagination ref="emojisPaginationComponent" :pagination="pagination">
+ <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
+ <template #default="{items}">
+ <div class="ldhfsamy">
+ <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
+ <img :src="emoji.url" class="img" :alt="emoji.name"/>
+ <div class="body">
+ <div class="name _monospace">{{ emoji.name }}</div>
+ <div class="info">{{ emoji.category }}</div>
+ </div>
+ </button>
+ </div>
+ </template>
+ </MkPagination>
+ </div>
- <div v-else-if="tab === 'remote'" class="remote">
- <FormSplit>
- <MkInput v-model="queryRemote" :debounce="true" type="search">
- <template #prefix><i class="fas fa-search"></i></template>
- <template #label>{{ $ts.search }}</template>
- </MkInput>
- <MkInput v-model="host" :debounce="true">
- <template #label>{{ $ts.host }}</template>
- </MkInput>
- </FormSplit>
- <MkPagination :pagination="remotePagination">
- <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
- <template v-slot="{items}">
- <div class="ldhfsamy">
- <div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
- <img :src="emoji.url" class="img" :alt="emoji.name"/>
- <div class="body">
- <div class="name _monospace">{{ emoji.name }}</div>
- <div class="info">{{ emoji.host }}</div>
+ <div v-else-if="tab === 'remote'" class="remote">
+ <FormSplit>
+ <MkInput v-model="queryRemote" :debounce="true" type="search">
+ <template #prefix><i class="fas fa-search"></i></template>
+ <template #label>{{ $ts.search }}</template>
+ </MkInput>
+ <MkInput v-model="host" :debounce="true">
+ <template #label>{{ $ts.host }}</template>
+ </MkInput>
+ </FormSplit>
+ <MkPagination :pagination="remotePagination">
+ <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
+ <template #default="{items}">
+ <div class="ldhfsamy">
+ <div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
+ <img :src="emoji.url" class="img" :alt="emoji.name"/>
+ <div class="body">
+ <div class="name _monospace">{{ emoji.name }}</div>
+ <div class="info">{{ emoji.host }}</div>
+ </div>
+ </div>
</div>
- </div>
- </div>
- </template>
- </MkPagination>
- </div>
- </div>
-</MkSpacer>
+ </template>
+ </MkPagination>
+ </div>
+ </div>
+ </MkSpacer>
+ </MkStickyContainer>
+</div>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue';
+import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkPagination from '@/components/ui/pagination.vue';
@@ -72,8 +78,8 @@ import MkSwitch from '@/components/form/switch.vue';
import FormSplit from '@/components/form/split.vue';
import { selectFile, selectFiles } from '@/scripts/select-file';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
@@ -131,13 +137,13 @@ const add = async (ev: MouseEvent) => {
const edit = (emoji) => {
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
- emoji: emoji
+ emoji: emoji,
}, {
done: result => {
if (result.updated) {
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
...oldEmoji,
- ...result.updated
+ ...result.updated,
}));
} else if (result.deleted) {
emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
@@ -159,7 +165,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
}, {
text: i18n.ts.import,
icon: 'fas fa-plus',
- action: () => { im(emoji); }
+ action: () => { im(emoji); },
}], ev.currentTarget ?? ev.target);
};
@@ -181,7 +187,7 @@ const menu = (ev: MouseEvent) => {
text: err.message,
});
});
- }
+ },
}, {
icon: 'fas fa-upload',
text: i18n.ts.import,
@@ -201,7 +207,7 @@ const menu = (ev: MouseEvent) => {
text: err.message,
});
});
- }
+ },
}], ev.currentTarget ?? ev.target);
};
@@ -265,31 +271,31 @@ const delBulk = async () => {
emojisPaginationComponent.value.reload();
};
-defineExpose({
- [symbols.PAGE_INFO]: computed(() => ({
- title: i18n.ts.customEmojis,
- icon: 'fas fa-laugh',
- bg: 'var(--bg)',
- actions: [{
- asFullButton: true,
- icon: 'fas fa-plus',
- text: i18n.ts.addEmoji,
- handler: add,
- }, {
- icon: 'fas fa-ellipsis-h',
- handler: menu,
- }],
- tabs: [{
- active: tab.value === 'local',
- title: i18n.ts.local,
- onClick: () => { tab.value = 'local'; },
- }, {
- active: tab.value === 'remote',
- title: i18n.ts.remote,
- onClick: () => { tab.value = 'remote'; },
- },]
- })),
-});
+const headerActions = $computed(() => [{
+ asFullButton: true,
+ icon: 'fas fa-plus',
+ text: i18n.ts.addEmoji,
+ handler: add,
+}, {
+ icon: 'fas fa-ellipsis-h',
+ handler: menu,
+}]);
+
+const headerTabs = $computed(() => [{
+ active: tab.value === 'local',
+ title: i18n.ts.local,
+ onClick: () => { tab.value = 'local'; },
+}, {
+ active: tab.value === 'remote',
+ title: i18n.ts.remote,
+ onClick: () => { tab.value = 'remote'; },
+}]);
+
+definePageMetadata(computed(() => ({
+ title: i18n.ts.customEmojis,
+ icon: 'fas fa-laugh',
+ bg: 'var(--bg)',
+})));
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue
index 9350911b60..18bf4f9a8c 100644
--- a/packages/client/src/pages/admin/files.vue
+++ b/packages/client/src/pages/admin/files.vue
@@ -1,50 +1,58 @@
<template>
-<div class="xrmjdkdw">
- <div>
- <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
- <MkSelect v-model="origin" style="margin: 0; flex: 1;">
- <template #label>{{ $ts.instance }}</template>
- <option value="combined">{{ $ts.all }}</option>
- <option value="local">{{ $ts.local }}</option>
- <option value="remote">{{ $ts.remote }}</option>
- </MkSelect>
- <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
- <template #label>{{ $ts.host }}</template>
- </MkInput>
- </div>
- <div class="inputs" style="display: flex; padding-top: 1.2em;">
- <MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
- <template #label>MIME type</template>
- </MkInput>
- </div>
- <MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
- <button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _panel _button" @click="show(file, $event)">
- <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
- <div v-if="viewMode === 'list'" class="body">
- <div>
- <small style="opacity: 0.7;">{{ file.name }}</small>
+<div>
+ <MkStickyContainer>
+ <template #header><XHeader :actions="headerActions"/></template>
+ <MkSpacer :content-max="900">
+ <div class="xrmjdkdw">
+ <div>
+ <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+ <MkSelect v-model="origin" style="margin: 0; flex: 1;">
+ <template #label>{{ $ts.instance }}</template>
+ <option value="combined">{{ $ts.all }}</option>
+ <option value="local">{{ $ts.local }}</option>
+ <option value="remote">{{ $ts.remote }}</option>
+ </MkSelect>
+ <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
+ <template #label>{{ $ts.host }}</template>
+ </MkInput>
</div>
- <div>
- <MkAcct v-if="file.user" :user="file.user"/>
- <div v-else>{{ $ts.system }}</div>
- </div>
- <div>
- <span style="margin-right: 1em;">{{ file.type }}</span>
- <span>{{ bytes(file.size) }}</span>
- </div>
- <div>
- <span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
+ <div class="inputs" style="display: flex; padding-top: 1.2em;">
+ <MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
+ <template #label>MIME type</template>
+ </MkInput>
</div>
+ <MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
+ <button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _button" @click="show(file, $event)">
+ <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
+ <div v-if="viewMode === 'list'" class="body">
+ <div>
+ <small style="opacity: 0.7;">{{ file.name }}</small>
+ </div>
+ <div>
+ <MkAcct v-if="file.user" :user="file.user"/>
+ <div v-else>{{ $ts.system }}</div>
+ </div>
+ <div>
+ <span style="margin-right: 1em;">{{ file.type }}</span>
+ <span>{{ bytes(file.size) }}</span>
+ </div>
+ <div>
+ <span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
+ </div>
+ </div>
+ </button>
+ </MkPagination>
</div>
- </button>
- </MkPagination>
- </div>
+ </div>
+ </MkSpacer>
+ </MkStickyContainer>
</div>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent } from 'vue';
import * as Acct from 'misskey-js/built/acct';
+import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
@@ -53,8 +61,8 @@ import MkContainer from '@/components/ui/container.vue';
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
import bytes from '@/filters/bytes';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let origin = $ref('local');
let type = $ref(null);
@@ -82,7 +90,7 @@ function clear() {
}
function show(file) {
- os.pageWindow(`/admin-file/${file.id}`);
+ os.pageWindow(`/admin/file/${file.id}`);
}
async function find() {
@@ -104,22 +112,23 @@ async function find() {
});
}
-defineExpose({
- [symbols.PAGE_INFO]: computed(() => ({
- title: i18n.ts.files,
- icon: 'fas fa-cloud',
- bg: 'var(--bg)',
- actions: [{
- text: i18n.ts.lookup,
- icon: 'fas fa-search',
- handler: find,
- }, {
- text: i18n.ts.clearCachedFiles,
- icon: 'fas fa-trash-alt',
- handler: clear,
- }],
- })),
-});
+const headerActions = $computed(() => [{
+ text: i18n.ts.lookup,
+ icon: 'fas fa-search',
+ handler: find,
+}, {
+ text: i18n.ts.clearCachedFiles,
+ icon: 'fas fa-trash-alt',
+ handler: clear,
+}]);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata(computed(() => ({
+ title: i18n.ts.files,
+ icon: 'fas fa-cloud',
+ bg: 'var(--bg)',
+})));
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue
index 9b7fa5678e..5db91101d7 100644
--- a/packages/client/src/pages/admin/index.vue
+++ b/packages/client/src/pages/admin/index.vue
@@ -1,8 +1,6 @@
<template>
<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
- <div v-if="!narrow || initialPage == null" class="nav">
- <MkHeader :info="header"></MkHeader>
-
+ <div v-if="!narrow || initialPage == null" class="nav">
<MkSpacer :content-max="700" :margin-min="16">
<div class="lxpfedzu">
<div class="banner">
@@ -17,29 +15,26 @@
</MkSpacer>
</div>
<div v-if="!(narrow && initialPage == null)" class="main">
- <MkStickyContainer>
- <template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
- <component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/>
- </MkStickyContainer>
+ <component :is="component" :key="initialPage" v-bind="pageProps"/>
</div>
</div>
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
+import { defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
import { i18n } from '@/i18n';
import MkSuperMenu from '@/components/ui/super-menu.vue';
import MkInfo from '@/components/ui/info.vue';
import { scroll } from '@/scripts/scroll';
import { instance } from '@/instance';
-import * as symbols from '@/symbols';
import * as os from '@/os';
import { lookupUser } from '@/scripts/lookup-user';
-import { MisskeyNavigator } from '@/scripts/navigate';
+import { useRouter } from '@/router';
+import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
const isEmpty = (x: string | null) => x == null || x === '';
-const nav = new MisskeyNavigator();
+const router = useRouter();
const indexInfo = {
title: i18n.ts.controlPanel,
@@ -224,7 +219,7 @@ watch(component, () => {
watch(() => props.initialPage, () => {
if (props.initialPage == null && !narrow) {
- nav.push('/admin/overview');
+ router.push('/admin/overview');
} else {
if (props.initialPage == null) {
INFO = indexInfo;
@@ -234,7 +229,7 @@ watch(() => props.initialPage, () => {
watch(narrow, () => {
if (props.initialPage == null && !narrow) {
- nav.push('/admin/overview');
+ router.push('/admin/overview');
}
});
@@ -243,7 +238,7 @@ onMounted(() => {
narrow = el.offsetWidth < NARROW_THRESHOLD;
if (props.initialPage == null && !narrow) {
- nav.push('/admin/overview');
+ router.push('/admin/overview');
}
});
@@ -251,19 +246,19 @@ onUnmounted(() => {
ro.disconnect();
});
-const pageChanged = (page) => {
- if (page == null) {
+provideMetadataReceiver((info) => {
+ if (info == null) {
childInfo = null;
} else {
- childInfo = page[symbols.PAGE_INFO];
+ childInfo = info;
}
-};
+});
const invite = () => {
os.api('admin/invite').then(x => {
os.alert({
type: 'info',
- text: x.code
+ text: x.code,
});
}).catch(err => {
os.alert({
@@ -279,33 +274,38 @@ const lookup = (ev) => {
icon: 'fas fa-user',
action: () => {
lookupUser();
- }
+ },
}, {
text: i18n.ts.note,
icon: 'fas fa-pencil-alt',
action: () => {
alert('TODO');
- }
+ },
}, {
text: i18n.ts.file,
icon: 'fas fa-cloud',
action: () => {
alert('TODO');
- }
+ },
}, {
text: i18n.ts.instance,
icon: 'fas fa-globe',
action: () => {
alert('TODO');
- }
+ },
}], ev.currentTarget ?? ev.target);
};
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata(INFO);
+
defineExpose({
- [symbols.PAGE_INFO]: INFO,
header: {
title: i18n.ts.controlPanel,
- }
+ },
});
</script>
diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue
index 3347846a80..1aec151abb 100644
--- a/packages/client/src/pages/admin/instance-block.vue
+++ b/packages/client/src/pages/admin/instance-block.vue
@@ -1,25 +1,29 @@
<template>
-<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- <FormTextarea v-model="blockedHosts" class="_formBlock">
- <span>{{ i18n.ts.blockedInstances }}</span>
- <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
- </FormTextarea>
+<MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
+ <FormSuspense :p="init">
+ <FormTextarea v-model="blockedHosts" class="_formBlock">
+ <span>{{ i18n.ts.blockedInstances }}</span>
+ <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
+ </FormTextarea>
- <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
- </FormSuspense>
-</MkSpacer>
+ <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
+ </FormSuspense>
+ </MkSpacer>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
+import XHeader from './_header_.vue';
import FormButton from '@/components/ui/button.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let blockedHosts: string = $ref('');
@@ -36,11 +40,13 @@ function save() {
});
}
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.instanceBlocking,
- icon: 'fas fa-ban',
- bg: 'var(--bg)',
- }
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.instanceBlocking,
+ icon: 'fas fa-ban',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/integrations.vue b/packages/client/src/pages/admin/integrations.vue
index d6061d0e51..d407d440b9 100644
--- a/packages/client/src/pages/admin/integrations.vue
+++ b/packages/client/src/pages/admin/integrations.vue
@@ -1,5 +1,6 @@
-<template>
-<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
+<template><MkStickyContainer>
+ <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<FormFolder class="_formBlock">
<template #icon><i class="fab fa-twitter"></i></template>
@@ -20,19 +21,19 @@
<XDiscord/>
</FormFolder>
</FormSuspense>
-</MkSpacer>
+</MkSpacer></MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
-import FormFolder from '@/components/form/folder.vue';
-import FormSuspense from '@/components/form/suspense.vue';
import XTwitter from './integrations.twitter.vue';
import XGithub from './integrations.github.vue';
import XDiscord from './integrations.discord.vue';
+import FormSuspense from '@/components/form/suspense.vue';
+import FormFolder from '@/components/form/folder.vue';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let enableTwitterIntegration: boolean = $ref(false);
let enableGithubIntegration: boolean = $ref(false);
@@ -45,11 +46,13 @@ async function init() {
enableDiscordIntegration = meta.enableDiscordIntegration;
}
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.integration,
- icon: 'fas fa-share-alt',
- bg: 'var(--bg)',
- }
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.integration,
+ icon: 'fas fa-share-alt',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue
index d109db9c38..bae5277f49 100644
--- a/packages/client/src/pages/admin/object-storage.vue
+++ b/packages/client/src/pages/admin/object-storage.vue
@@ -1,72 +1,76 @@
<template>
-<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- <div class="_formRoot">
- <FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
+<MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
+ <FormSuspense :p="init">
+ <div class="_formRoot">
+ <FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
- <template v-if="useObjectStorage">
- <FormInput v-model="objectStorageBaseUrl" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
- <template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
- </FormInput>
-
- <FormInput v-model="objectStorageBucket" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageBucket }}</template>
- <template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
- </FormInput>
-
- <FormInput v-model="objectStoragePrefix" class="_formBlock">
- <template #label>{{ i18n.ts.objectStoragePrefix }}</template>
- <template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
- </FormInput>
+ <template v-if="useObjectStorage">
+ <FormInput v-model="objectStorageBaseUrl" class="_formBlock">
+ <template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
+ <template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
+ </FormInput>
- <FormInput v-model="objectStorageEndpoint" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
- <template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
- </FormInput>
+ <FormInput v-model="objectStorageBucket" class="_formBlock">
+ <template #label>{{ i18n.ts.objectStorageBucket }}</template>
+ <template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
+ </FormInput>
- <FormInput v-model="objectStorageRegion" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageRegion }}</template>
- <template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
- </FormInput>
+ <FormInput v-model="objectStoragePrefix" class="_formBlock">
+ <template #label>{{ i18n.ts.objectStoragePrefix }}</template>
+ <template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
+ </FormInput>
- <FormSplit :min-width="280">
- <FormInput v-model="objectStorageAccessKey" class="_formBlock">
- <template #prefix><i class="fas fa-key"></i></template>
- <template #label>Access key</template>
+ <FormInput v-model="objectStorageEndpoint" class="_formBlock">
+ <template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
+ <template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
</FormInput>
- <FormInput v-model="objectStorageSecretKey" class="_formBlock">
- <template #prefix><i class="fas fa-key"></i></template>
- <template #label>Secret key</template>
+ <FormInput v-model="objectStorageRegion" class="_formBlock">
+ <template #label>{{ i18n.ts.objectStorageRegion }}</template>
+ <template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
</FormInput>
- </FormSplit>
- <FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
- <template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
- </FormSwitch>
+ <FormSplit :min-width="280">
+ <FormInput v-model="objectStorageAccessKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>Access key</template>
+ </FormInput>
- <FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
- <template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
- </FormSwitch>
+ <FormInput v-model="objectStorageSecretKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>Secret key</template>
+ </FormInput>
+ </FormSplit>
- <FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
- </FormSwitch>
+ <FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
+ <template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
+ <template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
+ </FormSwitch>
- <FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
- <template #label>s3ForcePathStyle</template>
- </FormSwitch>
- </template>
- </div>
- </FormSuspense>
-</MkSpacer>
+ <FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
+ <template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
+ <template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
+ </FormSwitch>
+
+ <FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
+ <template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
+ </FormSwitch>
+
+ <FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
+ <template #label>s3ForcePathStyle</template>
+ </FormSwitch>
+ </template>
+ </div>
+ </FormSuspense>
+ </MkSpacer>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
+import XHeader from './_header_.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormGroup from '@/components/form/group.vue';
@@ -74,9 +78,9 @@ 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';
-import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let useObjectStorage: boolean = $ref(false);
let objectStorageBaseUrl: string | null = $ref(null);
@@ -129,17 +133,18 @@ function save() {
});
}
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.objectStorage,
- icon: 'fas fa-cloud',
- bg: 'var(--bg)',
- actions: [{
- asFullButton: true,
- icon: 'fas fa-check',
- text: i18n.ts.save,
- handler: save,
- }],
- }
+const headerActions = $computed(() => [{
+ asFullButton: true,
+ icon: 'fas fa-check',
+ text: i18n.ts.save,
+ handler: save,
+}]);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.objectStorage,
+ icon: 'fas fa-cloud',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/other-settings.vue b/packages/client/src/pages/admin/other-settings.vue
index 552b05f347..59b3503c3c 100644
--- a/packages/client/src/pages/admin/other-settings.vue
+++ b/packages/client/src/pages/admin/other-settings.vue
@@ -1,18 +1,22 @@
<template>
-<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- none
- </FormSuspense>
-</MkSpacer>
+<MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
+ <FormSuspense :p="init">
+ none
+ </FormSuspense>
+ </MkSpacer>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
+import XHeader from './_header_.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
async function init() {
await os.api('admin/meta');
@@ -24,17 +28,18 @@ function save() {
});
}
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.other,
- icon: 'fas fa-cogs',
- bg: 'var(--bg)',
- actions: [{
- asFullButton: true,
- icon: 'fas fa-check',
- text: i18n.ts.save,
- handler: save,
- }],
- }
+const headerActions = $computed(() => [{
+ asFullButton: true,
+ icon: 'fas fa-check',
+ text: i18n.ts.save,
+ handler: save,
+}]);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.other,
+ icon: 'fas fa-cogs',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue
index cc69424c3b..82b3c33852 100644
--- a/packages/client/src/pages/admin/overview.vue
+++ b/packages/client/src/pages/admin/overview.vue
@@ -35,7 +35,7 @@
</MkContainer>
</div>
- <!--<XMetrics/>-->
+ <!--<XMetrics/>-->
<MkFolder style="margin: var(--margin)">
<template #header><i class="fas fa-info-circle"></i> {{ i18n.ts.info }}</template>
@@ -67,6 +67,7 @@
<script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
+import XMetrics from './metrics.vue';
import MkInstanceStats from '@/components/instance-stats.vue';
import MkNumberDiff from '@/components/number-diff.vue';
import MkContainer from '@/components/ui/container.vue';
@@ -74,11 +75,10 @@ import MkFolder from '@/components/ui/folder.vue';
import MkQueueChart from '@/components/queue-chart.vue';
import { version, url } from '@/config';
import number from '@/filters/number';
-import XMetrics from './metrics.vue';
import * as os from '@/os';
import { stream } from '@/stream';
-import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let stats: any = $ref(null);
let serverInfo: any = $ref(null);
@@ -106,7 +106,7 @@ onMounted(async () => {
nextTick(() => {
queueStatsConnection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
- length: 200
+ length: 200,
});
});
});
@@ -115,12 +115,14 @@ onBeforeUnmount(() => {
queueStatsConnection.dispose();
});
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.dashboard,
- icon: 'fas fa-tachometer-alt',
- bg: 'var(--bg)',
- }
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.dashboard,
+ icon: 'fas fa-tachometer-alt',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/proxy-account.vue b/packages/client/src/pages/admin/proxy-account.vue
index 727e20e7e5..0c5bb1bc9f 100644
--- a/packages/client/src/pages/admin/proxy-account.vue
+++ b/packages/client/src/pages/admin/proxy-account.vue
@@ -1,5 +1,6 @@
-<template>
-<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
+<template><MkStickyContainer>
+ <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<MkKeyValue class="_formBlock">
@@ -9,7 +10,7 @@
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton>
</FormSuspense>
-</MkSpacer>
+</MkSpacer></MkStickyContainer>
</template>
<script lang="ts" setup>
@@ -19,9 +20,9 @@ import FormButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let proxyAccount: any = $ref(null);
let proxyAccountId: any = $ref(null);
@@ -50,11 +51,13 @@ function save() {
});
}
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.proxyAccount,
- icon: 'fas fa-ghost',
- bg: 'var(--bg)',
- }
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.proxyAccount,
+ icon: 'fas fa-ghost',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/queue.vue b/packages/client/src/pages/admin/queue.vue
index 656b18199f..c2865525ab 100644
--- a/packages/client/src/pages/admin/queue.vue
+++ b/packages/client/src/pages/admin/queue.vue
@@ -1,24 +1,28 @@
<template>
-<MkSpacer :content-max="800">
- <XQueue :connection="connection" domain="inbox">
- <template #title>In</template>
- </XQueue>
- <XQueue :connection="connection" domain="deliver">
- <template #title>Out</template>
- </XQueue>
- <MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
-</MkSpacer>
+<MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="800">
+ <XQueue :connection="connection" domain="inbox">
+ <template #title>In</template>
+ </XQueue>
+ <XQueue :connection="connection" domain="deliver">
+ <template #title>Out</template>
+ </XQueue>
+ <MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
+ </MkSpacer>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue';
-import MkButton from '@/components/ui/button.vue';
import XQueue from './queue.chart.vue';
+import XHeader from './_header_.vue';
+import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import { stream } from '@/stream';
-import * as symbols from '@/symbols';
import * as config from '@/config';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
const connection = markRaw(stream.useChannel('queueStats'));
@@ -38,7 +42,7 @@ onMounted(() => {
nextTick(() => {
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
- length: 200
+ length: 200,
});
});
});
@@ -47,19 +51,20 @@ onBeforeUnmount(() => {
connection.dispose();
});
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.jobQueue,
- icon: 'fas fa-clipboard-list',
- bg: 'var(--bg)',
- actions: [{
- asFullButton: true,
- icon: 'fas fa-up-right-from-square',
- text: i18n.ts.dashboard,
- handler: () => {
- window.open(config.url + '/queue', '_blank');
- },
- }],
- }
+const headerActions = $computed(() => [{
+ asFullButton: true,
+ icon: 'fas fa-up-right-from-square',
+ text: i18n.ts.dashboard,
+ handler: () => {
+ window.open(config.url + '/queue', '_blank');
+ },
+}]);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.jobQueue,
+ icon: 'fas fa-clipboard-list',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/relays.vue b/packages/client/src/pages/admin/relays.vue
index 1a36bb4753..1ca4f2df09 100644
--- a/packages/client/src/pages/admin/relays.vue
+++ b/packages/client/src/pages/admin/relays.vue
@@ -1,24 +1,28 @@
<template>
-<MkSpacer :content-max="800">
- <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;">
- <div>{{ relay.inbox }}</div>
- <div class="status">
- <i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i>
- <i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i>
- <i v-else class="fas fa-clock icon requesting"></i>
- <span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
+<MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="800">
+ <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;">
+ <div>{{ relay.inbox }}</div>
+ <div class="status">
+ <i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i>
+ <i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i>
+ <i v-else class="fas fa-clock icon requesting"></i>
+ <span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
+ </div>
+ <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
- <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
- </div>
-</MkSpacer>
+ </MkSpacer>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
+import XHeader from './_header_.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let relays: any[] = $ref([]);
@@ -26,30 +30,30 @@ async function addRelay() {
const { canceled, result: inbox } = await os.inputText({
title: i18n.ts.addRelay,
type: 'url',
- placeholder: i18n.ts.inboxUrl
+ placeholder: i18n.ts.inboxUrl,
});
if (canceled) return;
os.api('admin/relays/add', {
- inbox
+ inbox,
}).then((relay: any) => {
refresh();
}).catch((err: any) => {
os.alert({
type: 'error',
- text: err.message || err
+ text: err.message || err,
});
});
}
function remove(inbox: string) {
os.api('admin/relays/remove', {
- inbox
+ inbox,
}).then(() => {
refresh();
}).catch((err: any) => {
os.alert({
type: 'error',
- text: err.message || err
+ text: err.message || err,
});
});
}
@@ -62,18 +66,19 @@ function refresh() {
refresh();
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.relays,
- icon: 'fas fa-globe',
- bg: 'var(--bg)',
- actions: [{
- asFullButton: true,
- icon: 'fas fa-plus',
- text: i18n.ts.addRelay,
- handler: addRelay,
- }],
- }
+const headerActions = $computed(() => [{
+ asFullButton: true,
+ icon: 'fas fa-plus',
+ text: i18n.ts.addRelay,
+ handler: addRelay,
+}]);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.relays,
+ icon: 'fas fa-globe',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue
index 6b8f70cca5..65b08565cd 100644
--- a/packages/client/src/pages/admin/security.vue
+++ b/packages/client/src/pages/admin/security.vue
@@ -1,36 +1,41 @@
<template>
-<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- <div class="_formRoot">
- <FormFolder class="_formBlock">
- <template #icon><i class="fas fa-shield-alt"></i></template>
- <template #label>{{ i18n.ts.botProtection }}</template>
- <template v-if="enableHcaptcha" #suffix>hCaptcha</template>
- <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
- <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
+<MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
+ <FormSuspense :p="init">
+ <div class="_formRoot">
+ <FormFolder class="_formBlock">
+ <template #icon><i class="fas fa-shield-alt"></i></template>
+ <template #label>{{ i18n.ts.botProtection }}</template>
+ <template v-if="enableHcaptcha" #suffix>hCaptcha</template>
+ <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
+ <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
- <XBotProtection/>
- </FormFolder>
+ <XBotProtection/>
+ </FormFolder>
- <FormFolder class="_formBlock">
- <template #label>Summaly Proxy</template>
+ <FormFolder class="_formBlock">
+ <template #label>Summaly Proxy</template>
- <div class="_formRoot">
- <FormInput v-model="summalyProxy" class="_formBlock">
- <template #prefix><i class="fas fa-link"></i></template>
- <template #label>Summaly Proxy URL</template>
- </FormInput>
+ <div class="_formRoot">
+ <FormInput v-model="summalyProxy" class="_formBlock">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <template #label>Summaly Proxy URL</template>
+ </FormInput>
- <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
- </div>
- </FormFolder>
- </div>
- </FormSuspense>
-</MkSpacer>
+ <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
+ </div>
+ </FormFolder>
+ </div>
+ </FormSuspense>
+ </MkSpacer>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
+import XBotProtection from './bot-protection.vue';
+import XHeader from './_header_.vue';
import FormFolder from '@/components/form/folder.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInfo from '@/components/ui/info.vue';
@@ -38,11 +43,10 @@ import FormSuspense from '@/components/form/suspense.vue';
import FormSection from '@/components/form/section.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
-import XBotProtection from './bot-protection.vue';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let summalyProxy: string = $ref('');
let enableHcaptcha: boolean = $ref(false);
@@ -63,11 +67,13 @@ function save() {
});
}
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.security,
- icon: 'fas fa-lock',
- bg: 'var(--bg)',
- }
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.security,
+ icon: 'fas fa-lock',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue
index 6dc30fe50b..a5767cc2c2 100644
--- a/packages/client/src/pages/admin/settings.vue
+++ b/packages/client/src/pages/admin/settings.vue
@@ -1,149 +1,155 @@
<template>
-<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- <div class="_formRoot">
- <FormInput v-model="name" class="_formBlock">
- <template #label>{{ i18n.ts.instanceName }}</template>
- </FormInput>
+<div>
+ <MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
+ <FormSuspense :p="init">
+ <div class="_formRoot">
+ <FormInput v-model="name" class="_formBlock">
+ <template #label>{{ i18n.ts.instanceName }}</template>
+ </FormInput>
- <FormTextarea v-model="description" class="_formBlock">
- <template #label>{{ i18n.ts.instanceDescription }}</template>
- </FormTextarea>
+ <FormTextarea v-model="description" class="_formBlock">
+ <template #label>{{ i18n.ts.instanceDescription }}</template>
+ </FormTextarea>
- <FormInput v-model="tosUrl" class="_formBlock">
- <template #prefix><i class="fas fa-link"></i></template>
- <template #label>{{ i18n.ts.tosUrl }}</template>
- </FormInput>
+ <FormInput v-model="tosUrl" class="_formBlock">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <template #label>{{ i18n.ts.tosUrl }}</template>
+ </FormInput>
- <FormSplit :min-width="300">
- <FormInput v-model="maintainerName" class="_formBlock">
- <template #label>{{ i18n.ts.maintainerName }}</template>
- </FormInput>
+ <FormSplit :min-width="300">
+ <FormInput v-model="maintainerName" class="_formBlock">
+ <template #label>{{ i18n.ts.maintainerName }}</template>
+ </FormInput>
- <FormInput v-model="maintainerEmail" type="email" class="_formBlock">
- <template #prefix><i class="fas fa-envelope"></i></template>
- <template #label>{{ i18n.ts.maintainerEmail }}</template>
- </FormInput>
- </FormSplit>
+ <FormInput v-model="maintainerEmail" type="email" class="_formBlock">
+ <template #prefix><i class="fas fa-envelope"></i></template>
+ <template #label>{{ i18n.ts.maintainerEmail }}</template>
+ </FormInput>
+ </FormSplit>
- <FormTextarea v-model="pinnedUsers" class="_formBlock">
- <template #label>{{ i18n.ts.pinnedUsers }}</template>
- <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
- </FormTextarea>
+ <FormTextarea v-model="pinnedUsers" class="_formBlock">
+ <template #label>{{ i18n.ts.pinnedUsers }}</template>
+ <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
+ </FormTextarea>
- <FormSection>
- <FormSwitch v-model="enableRegistration" class="_formBlock">
- <template #label>{{ i18n.ts.enableRegistration }}</template>
- </FormSwitch>
+ <FormSection>
+ <FormSwitch v-model="enableRegistration" class="_formBlock">
+ <template #label>{{ i18n.ts.enableRegistration }}</template>
+ </FormSwitch>
- <FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
- <template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
- </FormSwitch>
- </FormSection>
+ <FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
+ <template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
+ </FormSwitch>
+ </FormSection>
- <FormSection>
- <FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
- <FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
- <FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
- </FormSection>
+ <FormSection>
+ <FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
+ <FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
+ <FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
+ </FormSection>
- <FormSection>
- <template #label>{{ i18n.ts.theme }}</template>
+ <FormSection>
+ <template #label>{{ i18n.ts.theme }}</template>
- <FormInput v-model="iconUrl" class="_formBlock">
- <template #prefix><i class="fas fa-link"></i></template>
- <template #label>{{ i18n.ts.iconUrl }}</template>
- </FormInput>
+ <FormInput v-model="iconUrl" class="_formBlock">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <template #label>{{ i18n.ts.iconUrl }}</template>
+ </FormInput>
- <FormInput v-model="bannerUrl" class="_formBlock">
- <template #prefix><i class="fas fa-link"></i></template>
- <template #label>{{ i18n.ts.bannerUrl }}</template>
- </FormInput>
+ <FormInput v-model="bannerUrl" class="_formBlock">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <template #label>{{ i18n.ts.bannerUrl }}</template>
+ </FormInput>
- <FormInput v-model="backgroundImageUrl" class="_formBlock">
- <template #prefix><i class="fas fa-link"></i></template>
- <template #label>{{ i18n.ts.backgroundImageUrl }}</template>
- </FormInput>
+ <FormInput v-model="backgroundImageUrl" class="_formBlock">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <template #label>{{ i18n.ts.backgroundImageUrl }}</template>
+ </FormInput>
- <FormInput v-model="themeColor" class="_formBlock">
- <template #prefix><i class="fas fa-palette"></i></template>
- <template #label>{{ i18n.ts.themeColor }}</template>
- <template #caption>#RRGGBB</template>
- </FormInput>
+ <FormInput v-model="themeColor" class="_formBlock">
+ <template #prefix><i class="fas fa-palette"></i></template>
+ <template #label>{{ i18n.ts.themeColor }}</template>
+ <template #caption>#RRGGBB</template>
+ </FormInput>
- <FormTextarea v-model="defaultLightTheme" class="_formBlock">
- <template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
- <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
- </FormTextarea>
+ <FormTextarea v-model="defaultLightTheme" class="_formBlock">
+ <template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
+ <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
+ </FormTextarea>
- <FormTextarea v-model="defaultDarkTheme" class="_formBlock">
- <template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
- <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
- </FormTextarea>
- </FormSection>
+ <FormTextarea v-model="defaultDarkTheme" class="_formBlock">
+ <template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
+ <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
+ </FormTextarea>
+ </FormSection>
- <FormSection>
- <template #label>{{ i18n.ts.files }}</template>
+ <FormSection>
+ <template #label>{{ i18n.ts.files }}</template>
- <FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
- <template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
- <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
- </FormSwitch>
+ <FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
+ <template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
+ <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
+ </FormSwitch>
- <FormSplit :min-width="280">
- <FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
- <template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
- <template #suffix>MB</template>
- <template #caption>{{ i18n.ts.inMb }}</template>
- </FormInput>
+ <FormSplit :min-width="280">
+ <FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
+ <template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
+ <template #suffix>MB</template>
+ <template #caption>{{ i18n.ts.inMb }}</template>
+ </FormInput>
- <FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
- <template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
- <template #suffix>MB</template>
- <template #caption>{{ i18n.ts.inMb }}</template>
- </FormInput>
- </FormSplit>
- </FormSection>
+ <FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
+ <template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
+ <template #suffix>MB</template>
+ <template #caption>{{ i18n.ts.inMb }}</template>
+ </FormInput>
+ </FormSplit>
+ </FormSection>
- <FormSection>
- <template #label>ServiceWorker</template>
+ <FormSection>
+ <template #label>ServiceWorker</template>
- <FormSwitch v-model="enableServiceWorker" class="_formBlock">
- <template #label>{{ i18n.ts.enableServiceworker }}</template>
- <template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
- </FormSwitch>
+ <FormSwitch v-model="enableServiceWorker" class="_formBlock">
+ <template #label>{{ i18n.ts.enableServiceworker }}</template>
+ <template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
+ </FormSwitch>
- <template v-if="enableServiceWorker">
- <FormInput v-model="swPublicKey" class="_formBlock">
- <template #prefix><i class="fas fa-key"></i></template>
- <template #label>Public key</template>
- </FormInput>
+ <template v-if="enableServiceWorker">
+ <FormInput v-model="swPublicKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>Public key</template>
+ </FormInput>
- <FormInput v-model="swPrivateKey" class="_formBlock">
- <template #prefix><i class="fas fa-key"></i></template>
- <template #label>Private key</template>
- </FormInput>
- </template>
- </FormSection>
+ <FormInput v-model="swPrivateKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>Private key</template>
+ </FormInput>
+ </template>
+ </FormSection>
- <FormSection>
- <template #label>DeepL Translation</template>
+ <FormSection>
+ <template #label>DeepL Translation</template>
- <FormInput v-model="deeplAuthKey" class="_formBlock">
- <template #prefix><i class="fas fa-key"></i></template>
- <template #label>DeepL Auth Key</template>
- </FormInput>
- <FormSwitch v-model="deeplIsPro" class="_formBlock">
- <template #label>Pro account</template>
- </FormSwitch>
- </FormSection>
- </div>
- </FormSuspense>
-</MkSpacer>
+ <FormInput v-model="deeplAuthKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>DeepL Auth Key</template>
+ </FormInput>
+ <FormSwitch v-model="deeplIsPro" class="_formBlock">
+ <template #label>Pro account</template>
+ </FormSwitch>
+ </FormSection>
+ </div>
+ </FormSuspense>
+ </MkSpacer>
+ </MkStickyContainer>
+</div>
</template>
<script lang="ts" setup>
import { } from 'vue';
+import XHeader from './_header_.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormTextarea from '@/components/form/textarea.vue';
@@ -152,9 +158,9 @@ import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let name: string | null = $ref(null);
let description: string | null = $ref(null);
@@ -240,17 +246,18 @@ function save() {
});
}
-defineExpose({
- [symbols.PAGE_INFO]: {
- title: i18n.ts.general,
- icon: 'fas fa-cog',
- bg: 'var(--bg)',
- actions: [{
- asFullButton: true,
- icon: 'fas fa-check',
- text: i18n.ts.save,
- handler: save,
- }],
- }
+const headerActions = $computed(() => [{
+ asFullButton: true,
+ icon: 'fas fa-check',
+ text: i18n.ts.save,
+ handler: save,
+}]);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.general,
+ icon: 'fas fa-cog',
+ bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue
index f05aa5ff45..dccf952ba9 100644
--- a/packages/client/src/pages/admin/users.vue
+++ b/packages/client/src/pages/admin/users.vue
@@ -1,76 +1,84 @@
<template>
-<div class="lknzcolw">
- <div class="users">
- <div class="inputs">
- <MkSelect v-model="sort" style="flex: 1;">
- <template #label>{{ $ts.sort }}</template>
- <option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
- <option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
- <option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
- <option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
- </MkSelect>
- <MkSelect v-model="state" style="flex: 1;">
- <template #label>{{ $ts.state }}</template>
- <option value="all">{{ $ts.all }}</option>
- <option value="available">{{ $ts.normal }}</option>
- <option value="admin">{{ $ts.administrator }}</option>
- <option value="moderator">{{ $ts.moderator }}</option>
- <option value="silenced">{{ $ts.silence }}</option>
- <option value="suspended">{{ $ts.suspend }}</option>
- </MkSelect>
- <MkSelect v-model="origin" style="flex: 1;">
- <template #label>{{ $ts.instance }}</template>
- <option value="combined">{{ $ts.all }}</option>
- <option value="local">{{ $ts.local }}</option>
- <option value="remote">{{ $ts.remote }}</option>
- </MkSelect>
- </div>
- <div class="inputs">
- <MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
- <template #prefix>@</template>
- <template #label>{{ $ts.username }}</template>
- </MkInput>
- <MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
- <template #prefix>@</template>
- <template #label>{{ $ts.host }}</template>
- </MkInput>
- </div>
-
- <MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users">
- <button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)">
- <MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
- <div class="body">
- <header>
- <MkUserName class="name" :user="user"/>
- <span class="acct">@{{ acct(user) }}</span>
- <span v-if="user.isAdmin" class="staff"><i class="fas fa-bookmark"></i></span>
- <span v-if="user.isModerator" class="staff"><i class="far fa-bookmark"></i></span>
- <span v-if="user.isSilenced" class="punished"><i class="fas fa-microphone-slash"></i></span>
- <span v-if="user.isSuspended" class="punished"><i class="fas fa-snowflake"></i></span>
- </header>
- <div>
- <span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
+<div>
+ <MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="900">
+ <div class="lknzcolw">
+ <div class="users">
+ <div class="inputs">
+ <MkSelect v-model="sort" style="flex: 1;">
+ <template #label>{{ $ts.sort }}</template>
+ <option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
+ <option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
+ </MkSelect>
+ <MkSelect v-model="state" style="flex: 1;">
+ <template #label>{{ $ts.state }}</template>
+ <option value="all">{{ $ts.all }}</option>
+ <option value="available">{{ $ts.normal }}</option>
+ <option value="admin">{{ $ts.administrator }}</option>
+ <option value="moderator">{{ $ts.moderator }}</option>
+ <option value="silenced">{{ $ts.silence }}</option>
+ <option value="suspended">{{ $ts.suspend }}</option>
+ </MkSelect>
+ <MkSelect v-model="origin" style="flex: 1;">
+ <template #label>{{ $ts.instance }}</template>
+ <option value="combined">{{ $ts.all }}</option>
+ <option value="local">{{ $ts.local }}</option>
+ <option value="remote">{{ $ts.remote }}</option>
+ </MkSelect>
</div>
- <div>
- <span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
+ <div class="inputs">
+ <MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
+ <template #prefix>@</template>
+ <template #label>{{ $ts.username }}</template>
+ </MkInput>
+ <MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
+ <template #prefix>@</template>
+ <template #label>{{ $ts.host }}</template>
+ </MkInput>
</div>
+
+ <MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users">
+ <button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)">
+ <MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
+ <div class="body">
+ <header>
+ <MkUserName class="name" :user="user"/>
+ <span class="acct">@{{ acct(user) }}</span>
+ <span v-if="user.isAdmin" class="staff"><i class="fas fa-bookmark"></i></span>
+ <span v-if="user.isModerator" class="staff"><i class="far fa-bookmark"></i></span>
+ <span v-if="user.isSilenced" class="punished"><i class="fas fa-microphone-slash"></i></span>
+ <span v-if="user.isSuspended" class="punished"><i class="fas fa-snowflake"></i></span>
+ </header>
+ <div>
+ <span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
+ </div>
+ <div>
+ <span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
+ </div>
+ </div>
+ </button>
+ </MkPagination>
</div>
- </button>
- </MkPagination>
- </div>
+ </div>
+ </MkSpacer>
+ </MkStickyContainer>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
+import XHeader from './_header_.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
import MkPagination from '@/components/ui/pagination.vue';
import { acct } from '@/filters/user';
import * as os from '@/os';
-import * as symbols from '@/symbols';
import { lookupUser } from '@/scripts/lookup-user';
import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
let paginationComponent = $ref<InstanceType<typeof MkPagination>>();
@@ -89,7 +97,7 @@ const pagination = {
username: searchUsername,
hostname: searchHost,
})),
- offsetMode: true
+ offsetMode: true,
};
function searchUser() {
@@ -106,7 +114,7 @@ async function addUser() {
const { canceled: canceled2, result: password } = await os.inputText({
title: i18n.ts.password,
- type: 'password'
+ type: 'password',
});
if (canceled2) return;
@@ -122,34 +130,34 @@ function show(user) {
os.pageWindow(`/user-info/${user.id}`);
}
-defineExpose({
- [symbols.PAGE_INFO]: computed(() => ({
- title: i18n.ts.users,
- icon: 'fas fa-users',
- bg: 'var(--bg)',
- actions: [{
- icon: 'fas fa-search',
- text: i18n.ts.search,
- handler: searchUser
- }, {
- asFullButton: true,
- icon: 'fas fa-plus',
- text: i18n.ts.addUser,
- handler: addUser
- }, {
- asFullButton: true,
- icon: 'fas fa-search',
- text: i18n.ts.lookup,
- handler: lookupUser
- }],
- })),
-});
+const headerActions = $computed(() => [{
+ icon: 'fas fa-search',
+ text: i18n.ts.search,
+ handler: searchUser,
+}, {
+ asFullButton: true,
+ icon: 'fas fa-plus',
+ text: i18n.ts.addUser,
+ handler: addUser,
+}, {
+ asFullButton: true,
+ icon: 'fas fa-search',
+ text: i18n.ts.lookup,
+ handler: lookupUser,
+}]);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata(computed(() => ({
+ title: i18n.ts.users,
+ icon: 'fas fa-users',
+ bg: 'var(--bg)',
+})));
</script>
<style lang="scss" scoped>
.lknzcolw {
> .users {
- margin: var(--margin);
> .inputs {
display: flex;