Merge branch 'develop' into pizzax-indexeddb
This commit is contained in:
commit
764da890b6
1491 changed files with 65628 additions and 43153 deletions
7
packages/client/src/pages/_empty_.vue
Normal file
7
packages/client/src/pages/_empty_.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
</script>
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { version } from '@/config';
|
||||
import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ import { nextTick, onBeforeUnmount } from 'vue';
|
|||
import { version } from '@/config';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkLink from '@/components/link.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import { physics } from '@/scripts/physics';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
|
|
@ -204,7 +204,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.aboutMisskey,
|
||||
icon: null,
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import XEmoji from './emojis.emoji.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkFolder from '@/components/ui/folder.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import * as os from '@/os';
|
||||
import { emojiCategories, emojiTags } from '@/instance';
|
||||
import XEmoji from './emojis.emoji.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
|
@ -66,7 +66,7 @@ export default defineComponent({
|
|||
handler() {
|
||||
this.search();
|
||||
},
|
||||
deep: true
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -90,8 +90,8 @@ export default defineComponent({
|
|||
} else {
|
||||
this.selectedTags.add(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
106
packages/client/src/pages/about.federation.vue
Normal file
106
packages/client/src/pages/about.federation.vue
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div class="taeiyria">
|
||||
<div class="query">
|
||||
<MkInput v-model="host" :debounce="true" class="">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
<FormSplit style="margin-top: var(--margin);">
|
||||
<MkSelect v-model="state">
|
||||
<template #label>{{ i18n.ts.state }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="federating">{{ i18n.ts.federating }}</option>
|
||||
<option value="subscribing">{{ i18n.ts.subscribing }}</option>
|
||||
<option value="publishing">{{ i18n.ts.publishing }}</option>
|
||||
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
||||
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
||||
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="sort">
|
||||
<template #label>{{ i18n.ts.sort }}</template>
|
||||
<option value="+pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+notes">{{ i18n.ts.notes }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-notes">{{ i18n.ts.notes }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+users">{{ i18n.ts.users }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-users">{{ i18n.ts.users }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+following">{{ i18n.ts.following }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-following">{{ i18n.ts.following }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+followers">{{ i18n.ts.followers }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-followers">{{ i18n.ts.followers }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+caughtAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-caughtAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+lastCommunicatedAt">{{ i18n.ts.lastCommunication }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-lastCommunicatedAt">{{ i18n.ts.lastCommunication }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
</MkSelect>
|
||||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<div class="dqokceoi">
|
||||
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Last communicated: ${new Date(instance.lastCommunicatedAt).toLocaleString()}\nStatus: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`">
|
||||
<MkInstanceCardMini :instance="instance"/>
|
||||
</MkA>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
let host = $ref('');
|
||||
let state = $ref('federating');
|
||||
let sort = $ref('+pubSub');
|
||||
const pagination = {
|
||||
endpoint: 'federation/instances' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
params: computed(() => ({
|
||||
sort: sort,
|
||||
host: host !== '' ? host : null,
|
||||
...(
|
||||
state === 'federating' ? { federating: true } :
|
||||
state === 'subscribing' ? { subscribing: true } :
|
||||
state === 'publishing' ? { publishing: true } :
|
||||
state === 'suspended' ? { suspended: true } :
|
||||
state === 'blocked' ? { blocked: true } :
|
||||
state === 'notResponding' ? { notResponding: true } :
|
||||
{}),
|
||||
})),
|
||||
};
|
||||
|
||||
function getStatus(instance) {
|
||||
if (instance.isSuspended) return 'Suspended';
|
||||
if (instance.isBlocked) return 'Blocked';
|
||||
if (instance.isNotResponding) return 'Error';
|
||||
return 'Alive';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.taeiyria {
|
||||
> .query {
|
||||
background: var(--bg);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.dqokceoi {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
grid-gap: 12px;
|
||||
|
||||
> .instance:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20">
|
||||
<div class="_formRoot">
|
||||
<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
|
||||
<div class="content">
|
||||
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||
<img :src="$instance.iconUrl ?? $instance.faviconUrl ?? '/favicon.ico'" alt="" class="icon"/>
|
||||
<div class="name">
|
||||
<b>{{ $instance.name || host }}</b>
|
||||
<b>{{ $instance.name ?? host }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.description }}</template>
|
||||
<template #value>{{ $instance.description }}</template>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value><div v-html="$instance.description"></div></template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormSection>
|
||||
|
|
@ -22,33 +22,35 @@
|
|||
<template #key>Misskey</template>
|
||||
<template #value>{{ version }}</template>
|
||||
</MkKeyValue>
|
||||
<FormLink to="/about-misskey">{{ $ts.aboutMisskey }}</FormLink>
|
||||
<div class="_formBlock" v-html="i18n.t('poweredByMisskeyDescription', { name: $instance.name ?? host })">
|
||||
</div>
|
||||
<FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<FormSplit>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.administrator }}</template>
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>{{ $instance.maintainerName }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.contact }}</template>
|
||||
<template #key>{{ i18n.ts.contact }}</template>
|
||||
<template #value>{{ $instance.maintainerEmail }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink>
|
||||
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ i18n.ts.tos }}</FormLink>
|
||||
</FormSection>
|
||||
|
||||
<FormSuspense :p="initStats">
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.statistics }}</template>
|
||||
<template #label>{{ i18n.ts.statistics }}</template>
|
||||
<FormSplit>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.users }}</template>
|
||||
<template #key>{{ i18n.ts.users }}</template>
|
||||
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.notes }}</template>
|
||||
<template #key>{{ i18n.ts.notes }}</template>
|
||||
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
|
|
@ -67,7 +69,13 @@
|
|||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20">
|
||||
<MkSpacer v-else-if="tab === 'emojis'" :content-max="1000" :margin-min="20">
|
||||
<XEmojis/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'federation'" :content-max="1000" :margin-min="20">
|
||||
<XFederation/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'charts'" :content-max="1000" :margin-min="20">
|
||||
<MkInstanceStats :chart-limit="500" :detailed="true"/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
|
|
@ -75,20 +83,28 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { version, instanceName , host } from '@/config';
|
||||
import XEmojis from './about.emojis.vue';
|
||||
import XFederation from './about.federation.vue';
|
||||
import { version, instanceName, host } from '@/config';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import MkInstanceStats from '@/components/instance-stats.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkInstanceStats from '@/components/MkInstanceStats.vue';
|
||||
import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialTab?: string;
|
||||
}>(), {
|
||||
initialTab: 'overview',
|
||||
});
|
||||
|
||||
let stats = $ref(null);
|
||||
let tab = $ref('overview');
|
||||
let tab = $ref(props.initialTab);
|
||||
|
||||
const initStats = () => os.api('stats', {
|
||||
}).then((res) => {
|
||||
|
|
@ -98,20 +114,25 @@ const initStats = () => os.api('stats', {
|
|||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab === 'overview',
|
||||
key: 'overview',
|
||||
title: i18n.ts.overview,
|
||||
onClick: () => { tab = 'overview'; },
|
||||
}, {
|
||||
active: tab === 'charts',
|
||||
key: 'emojis',
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: 'fas fa-laugh',
|
||||
}, {
|
||||
key: 'federation',
|
||||
title: i18n.ts.federation,
|
||||
icon: 'fas fa-globe',
|
||||
}, {
|
||||
key: 'charts',
|
||||
title: i18n.ts.charts,
|
||||
icon: 'fas fa-chart-bar',
|
||||
onClick: () => { tab = 'charts'; },
|
||||
icon: 'fas fa-chart-simple',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.instanceInfo,
|
||||
icon: 'fas fa-info-circle',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,46 +1,84 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="500" :margin-min="16" :margin-max="32">
|
||||
<div v-if="file" class="cxqhhsmd _formRoot">
|
||||
<div class="_formBlock">
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="file" :content-max="600" :margin-min="16" :margin-max="32">
|
||||
<div v-if="tab === 'overview'" class="cxqhhsmd _formRoot">
|
||||
<a class="_formBlock thumbnail" :href="file.url" target="_blank">
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
<div class="info">
|
||||
<span style="margin-right: 1em;">{{ file.type }}</span>
|
||||
<span>{{ bytes(file.size) }}</span>
|
||||
<MkTime :time="file.createdAt" mode="detail" style="display: block;"/>
|
||||
</div>
|
||||
</a>
|
||||
<div class="_formBlock">
|
||||
<MkKeyValue :copy="file.type" oneline style="margin: 1em 0;">
|
||||
<template #key>MIME Type</template>
|
||||
<template #value><span class="_monospace">{{ file.type }}</span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>Size</template>
|
||||
<template #value><span class="_monospace">{{ bytes(file.size) }}</span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue :copy="file.id" oneline style="margin: 1em 0;">
|
||||
<template #key>ID</template>
|
||||
<template #value><span class="_monospace">{{ file.id }}</span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue :copy="file.md5" oneline style="margin: 1em 0;">
|
||||
<template #key>MD5</template>
|
||||
<template #value><span class="_monospace">{{ file.md5 }}</span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ i18n.ts.createdAt }}</template>
|
||||
<template #value><span class="_monospace"><MkTime :time="file.createdAt" mode="detail" style="display: block;"/></span></template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
<MkA v-if="file.user" class="user" :to="`/user-info/${file.user.id}`">
|
||||
<MkUserCardMini :user="file.user"/>
|
||||
</MkA>
|
||||
<div class="_formBlock">
|
||||
<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">NSFW</MkSwitch>
|
||||
</div>
|
||||
<FormLink class="_formBlock" :to="file.url" :external="true">Open</FormLink>
|
||||
<FormLink class="_formBlock" :to="`/user-info/${file.userId}`">{{ $ts.user }}</FormLink>
|
||||
|
||||
<div class="_formBlock">
|
||||
<MkButton full danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
|
||||
</div>
|
||||
<div v-if="info" class="_formBlock">
|
||||
<details class="_content rawdata">
|
||||
<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
|
||||
</details>
|
||||
<MkButton danger @click="del"><i class="fas fa-trash-alt"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab === 'ip' && info" class="_formRoot">
|
||||
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
|
||||
<MkKeyValue v-if="info.requestIp" class="_formBlock _monospace" :copy="info.requestIp" oneline>
|
||||
<template #key>IP</template>
|
||||
<template #value>{{ info.requestIp }}</template>
|
||||
</MkKeyValue>
|
||||
<FormSection v-if="info.requestHeaders">
|
||||
<template #label>Headers</template>
|
||||
<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_formBlock _monospace">
|
||||
<template #key>{{ k }}</template>
|
||||
<template #value>{{ v }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
</div>
|
||||
<div v-else-if="tab === 'raw'" class="_formRoot">
|
||||
<MkObjectView v-if="info" tall :value="info">
|
||||
</MkObjectView>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import MkObjectView from '@/components/MkObjectView.vue';
|
||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { acct } from '@/filters/user';
|
||||
import { iAmAdmin, iAmModerator } from '@/account';
|
||||
|
||||
let tab = $ref('overview');
|
||||
let file: any = $ref(null);
|
||||
let info: any = $ref(null);
|
||||
let isSensitive: boolean = $ref(false);
|
||||
|
|
@ -74,32 +112,48 @@ async function toggleIsSensitive(v) {
|
|||
isSensitive = v;
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
const headerActions = $computed(() => [{
|
||||
text: i18n.ts.openInNewTab,
|
||||
icon: 'fas fa-external-link-alt',
|
||||
handler: () => {
|
||||
window.open(file.url, '_blank');
|
||||
},
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'overview',
|
||||
title: i18n.ts.overview,
|
||||
icon: 'fas fa-info-circle',
|
||||
}, iAmModerator ? {
|
||||
key: 'ip',
|
||||
title: 'IP',
|
||||
icon: 'fas fa-bars-staggered',
|
||||
} : null, {
|
||||
key: 'raw',
|
||||
title: 'Raw data',
|
||||
icon: 'fas fa-code',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file,
|
||||
icon: 'fas fa-file',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cxqhhsmd {
|
||||
> ._section {
|
||||
> .thumbnail {
|
||||
display: block;
|
||||
|
||||
> .thumbnail {
|
||||
height: 150px;
|
||||
height: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> .info {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
> .rawdata {
|
||||
overflow: auto;
|
||||
> .user {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,17 +9,18 @@
|
|||
</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">
|
||||
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
|
||||
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
|
||||
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
|
||||
</button>
|
||||
<div ref="tabHighlightEl" class="highlight"></div>
|
||||
</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>
|
||||
<button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -27,24 +28,27 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, inject } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, inject, watch, nextTick } 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 MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { globalEvents } from '@/events';
|
||||
import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata';
|
||||
import { injectPageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
type Tab = {
|
||||
key?: string | null;
|
||||
title: string;
|
||||
icon?: string;
|
||||
iconOnly?: boolean;
|
||||
onClick?: (ev: MouseEvent) => void;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
tabs?: {
|
||||
title: string;
|
||||
active: boolean;
|
||||
icon?: string;
|
||||
iconOnly?: boolean;
|
||||
onClick: () => void;
|
||||
}[];
|
||||
tabs?: Tab[];
|
||||
tab?: string;
|
||||
actions?: {
|
||||
text: string;
|
||||
icon: string;
|
||||
|
|
@ -54,9 +58,15 @@ const props = defineProps<{
|
|||
thin?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:tab', key: string);
|
||||
}>();
|
||||
|
||||
const metadata = injectPageMetadata();
|
||||
|
||||
const el = ref<HTMLElement>(null);
|
||||
const tabRefs = {};
|
||||
const tabHighlightEl = $ref<HTMLElement | null>(null);
|
||||
const bg = ref(null);
|
||||
const height = ref(0);
|
||||
const hasTabs = computed(() => {
|
||||
|
|
@ -65,13 +75,15 @@ const hasTabs = computed(() => {
|
|||
|
||||
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,
|
||||
active: tab.key != null && tab.key === props.tab,
|
||||
action: (ev) => {
|
||||
onTabClick(tab, ev);
|
||||
},
|
||||
}));
|
||||
popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
|
@ -84,6 +96,24 @@ const onClick = () => {
|
|||
scrollToTop(el.value, { behavior: 'smooth' });
|
||||
};
|
||||
|
||||
function onTabMousedown(tab: Tab, ev: MouseEvent): void {
|
||||
// ユーザビリティの観点からmousedown時にはonClickは呼ばない
|
||||
if (tab.key) {
|
||||
emit('update:tab', tab.key);
|
||||
}
|
||||
}
|
||||
|
||||
function onTabClick(tab: Tab, ev: MouseEvent): void {
|
||||
if (tab.onClick) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
tab.onClick(ev);
|
||||
}
|
||||
if (tab.key) {
|
||||
emit('update:tab', tab.key);
|
||||
}
|
||||
}
|
||||
|
||||
const calcBg = () => {
|
||||
const rawBg = metadata?.bg || 'var(--bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
|
|
@ -94,6 +124,22 @@ const calcBg = () => {
|
|||
onMounted(() => {
|
||||
calcBg();
|
||||
globalEvents.on('themeChanged', calcBg);
|
||||
|
||||
watch(() => [props.tab, props.tabs], () => {
|
||||
nextTick(() => {
|
||||
const tabEl = tabRefs[props.tab];
|
||||
if (tabEl && tabHighlightEl) {
|
||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
||||
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
|
||||
const parentRect = tabEl.parentElement.getBoundingClientRect();
|
||||
const rect = tabEl.getBoundingClientRect();
|
||||
tabHighlightEl.style.width = rect.width + 'px';
|
||||
tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
|
||||
}
|
||||
});
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
@ -105,9 +151,6 @@ onUnmounted(() => {
|
|||
.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));
|
||||
|
|
@ -176,6 +219,8 @@ onUnmounted(() => {
|
|||
|
||||
> .icon {
|
||||
margin-right: 8px;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> .title {
|
||||
|
|
@ -206,6 +251,7 @@ onUnmounted(() => {
|
|||
}
|
||||
|
||||
> .tabs {
|
||||
position: relative;
|
||||
margin-left: 16px;
|
||||
font-size: 0.8em;
|
||||
overflow: auto;
|
||||
|
|
@ -225,25 +271,22 @@ onUnmounted(() => {
|
|||
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
|
||||
> .highlight {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
background: var(--accent);
|
||||
border-radius: 999px;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -7,31 +7,31 @@
|
|||
<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>
|
||||
<template #label>{{ i18n.ts.state }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="unresolved">{{ i18n.ts.unresolved }}</option>
|
||||
<option value="resolved">{{ i18n.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>
|
||||
<template #label>{{ i18n.ts.reporteeOrigin }}</template>
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.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>
|
||||
<template #label>{{ i18n.ts.reporterOrigin }}</template>
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.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>
|
||||
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" :spellcheck="false">
|
||||
<span>{{ i18n.ts.username }}</span>
|
||||
</MkInput>
|
||||
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" :disabled="pagination.params().origin === 'local'">
|
||||
<span>{{ $ts.host }}</span>
|
||||
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params().origin === 'local'">
|
||||
<span>{{ i18n.ts.host }}</span>
|
||||
</MkInput>
|
||||
</div>
|
||||
-->
|
||||
|
|
@ -52,8 +52,8 @@ 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 MkPagination from '@/components/MkPagination.vue';
|
||||
import XAbuseReport from '@/components/MkAbuseReport.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
|
@ -87,7 +87,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.abuseReports,
|
||||
icon: 'fas fa-exclamation-circle',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
|
|
@ -116,7 +116,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.ads,
|
||||
icon: 'fas fa-audio-description',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import * as os from '@/os';
|
||||
|
|
@ -102,7 +102,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'fas fa-broadcast-tower',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,41 +3,56 @@
|
|||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormRadios v-model="provider" class="_formBlock">
|
||||
<option :value="null">{{ $ts.none }} ({{ $ts.notRecommended }})</option>
|
||||
<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
|
||||
<option value="hcaptcha">hCaptcha</option>
|
||||
<option value="recaptcha">reCAPTCHA</option>
|
||||
<option value="turnstile">Turnstile</option>
|
||||
</FormRadios>
|
||||
|
||||
<template v-if="provider === 'hcaptcha'">
|
||||
<FormInput v-model="hcaptchaSiteKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>{{ $ts.hcaptchaSiteKey }}</template>
|
||||
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="hcaptchaSecretKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>{{ $ts.hcaptchaSecretKey }}</template>
|
||||
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
|
||||
</FormInput>
|
||||
<FormSlot class="_formBlock">
|
||||
<template #label>{{ $ts.preview }}</template>
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
<template v-else-if="provider === 'recaptcha'">
|
||||
<FormInput v-model="recaptchaSiteKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>{{ $ts.recaptchaSiteKey }}</template>
|
||||
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="recaptchaSecretKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>{{ $ts.recaptchaSecretKey }}</template>
|
||||
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
|
||||
</FormInput>
|
||||
<FormSlot v-if="recaptchaSiteKey" class="_formBlock">
|
||||
<template #label>{{ $ts.preview }}</template>
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
<template v-else-if="provider === 'turnstile'">
|
||||
<FormInput v-model="turnstileSiteKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="turnstileSecretKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
|
||||
</FormInput>
|
||||
<FormSlot class="_formBlock">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="turnstile" :sitekey="turnstileSiteKey || '1x00000000000000000000AA'"/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
|
||||
<FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
<FormButton primary @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</div>
|
||||
|
|
@ -47,43 +62,46 @@
|
|||
import { defineAsyncComponent } from 'vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import * as os from '@/os';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue'));
|
||||
const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
|
||||
|
||||
let provider = $ref(null);
|
||||
let hcaptchaSiteKey: string | null = $ref(null);
|
||||
let hcaptchaSecretKey: string | null = $ref(null);
|
||||
let recaptchaSiteKey: string | null = $ref(null);
|
||||
let recaptchaSecretKey: string | null = $ref(null);
|
||||
|
||||
const enableHcaptcha = $computed(() => provider === 'hcaptcha');
|
||||
const enableRecaptcha = $computed(() => provider === 'recaptcha');
|
||||
let turnstileSiteKey: string | null = $ref(null);
|
||||
let turnstileSecretKey: string | null = $ref(null);
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
enableHcaptcha = meta.enableHcaptcha;
|
||||
hcaptchaSiteKey = meta.hcaptchaSiteKey;
|
||||
hcaptchaSecretKey = meta.hcaptchaSecretKey;
|
||||
enableRecaptcha = meta.enableRecaptcha;
|
||||
recaptchaSiteKey = meta.recaptchaSiteKey;
|
||||
recaptchaSecretKey = meta.recaptchaSecretKey;
|
||||
turnstileSiteKey = meta.turnstileSiteKey;
|
||||
turnstileSecretKey = meta.turnstileSecretKey;
|
||||
|
||||
provider = enableHcaptcha ? 'hcaptcha' : enableRecaptcha ? 'recaptcha' : null;
|
||||
provider = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : meta.enableTurnstile ? 'turnstile' : null;
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
enableHcaptcha,
|
||||
enableHcaptcha: provider === 'hcaptcha',
|
||||
hcaptchaSiteKey,
|
||||
hcaptchaSecretKey,
|
||||
enableRecaptcha,
|
||||
enableRecaptcha: provider === 'recaptcha',
|
||||
recaptchaSiteKey,
|
||||
recaptchaSecretKey,
|
||||
enableTurnstile: provider === 'turnstile',
|
||||
turnstileSiteKey,
|
||||
turnstileSecretKey,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import * as os from '@/os';
|
||||
import bytes from '@/filters/bytes';
|
||||
import number from '@/filters/number';
|
||||
|
|
@ -29,6 +29,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.database,
|
||||
icon: 'fas fa-database',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ 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';
|
||||
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';
|
||||
|
|
@ -122,6 +122,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.emailServer,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<XModalWindow ref="dialog"
|
||||
<XModalWindow
|
||||
ref="dialog"
|
||||
:width="370"
|
||||
:with-ok-button="true"
|
||||
@close="$refs.dialog.close()"
|
||||
|
|
@ -12,16 +13,16 @@
|
|||
<div class="yigymqpb _section">
|
||||
<img :src="emoji.url" class="img"/>
|
||||
<MkInput v-model="name" class="_formBlock">
|
||||
<template #label>{{ $ts.name }}</template>
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="category" class="_formBlock" :datalist="categories">
|
||||
<template #label>{{ $ts.category }}</template>
|
||||
<template #label>{{ i18n.ts.category }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="aliases" class="_formBlock">
|
||||
<template #label>{{ $ts.tags }}</template>
|
||||
<template #caption>{{ $ts.setMultipleBySeparatingWithSpace }}</template>
|
||||
<template #label>{{ i18n.ts.tags }}</template>
|
||||
<template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template>
|
||||
</MkInput>
|
||||
<MkButton danger @click="del()"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
|
||||
<MkButton danger @click="del()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</XModalWindow>
|
||||
|
|
@ -29,8 +30,8 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import * as os from '@/os';
|
||||
import { unique } from '@/scripts/array';
|
||||
|
|
@ -70,7 +71,7 @@ async function update() {
|
|||
name,
|
||||
category,
|
||||
aliases: aliases.split(' '),
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
dialog.close();
|
||||
|
|
@ -84,10 +85,10 @@ async function del() {
|
|||
if (canceled) return;
|
||||
|
||||
os.api('admin/emoji/delete', {
|
||||
id: props.emoji.id
|
||||
id: props.emoji.id,
|
||||
}).then(() => {
|
||||
emit('done', {
|
||||
deleted: true
|
||||
deleted: true,
|
||||
});
|
||||
dialog.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><XHeader v-model:tab="tab" :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>
|
||||
<template #label>{{ i18n.ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="selectMode" style="margin: 8px 0;">
|
||||
<template #label>Select mode</template>
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
||||
</div>
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||
<template #empty><span>{{ i18n.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)">
|
||||
|
|
@ -40,14 +40,14 @@
|
|||
<FormSplit>
|
||||
<MkInput v-model="queryRemote" :debounce="true" type="search">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.search }}</template>
|
||||
<template #label>{{ i18n.ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="host" :debounce="true">
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkPagination :pagination="remotePagination">
|
||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||
<template #empty><span>{{ i18n.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)">
|
||||
|
|
@ -70,10 +70,10 @@
|
|||
<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 MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import { selectFile, selectFiles } from '@/scripts/select-file';
|
||||
|
|
@ -282,19 +282,16 @@ const headerActions = $computed(() => [{
|
|||
}]);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab.value === 'local',
|
||||
key: 'local',
|
||||
title: i18n.ts.local,
|
||||
onClick: () => { tab.value = 'local'; },
|
||||
}, {
|
||||
active: tab.value === 'remote',
|
||||
key: 'remote',
|
||||
title: i18n.ts.remote,
|
||||
onClick: () => { tab.value = 'remote'; },
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: 'fas fa-laugh',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,41 +7,24 @@
|
|||
<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>
|
||||
<template #label>{{ i18n.ts.instance }}</template>
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.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>
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<div class="inputs" style="display: flex; padding-top: 1.2em;">
|
||||
<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap; padding-top: 1.2em;">
|
||||
<MkInput v-model="userId" :debounce="true" type="search" style="margin: 0; flex: 1;">
|
||||
<template #label>User ID</template>
|
||||
</MkInput>
|
||||
<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>
|
||||
<MkFileListForAdmin :pagination="pagination" :view-mode="viewMode"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
|
@ -53,12 +36,10 @@
|
|||
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 MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
|
||||
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
|
@ -67,12 +48,14 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
|||
let origin = $ref('local');
|
||||
let type = $ref(null);
|
||||
let searchHost = $ref('');
|
||||
let userId = $ref('');
|
||||
let viewMode = $ref('grid');
|
||||
const pagination = {
|
||||
endpoint: 'admin/drive/files' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
type: (type && type !== '') ? type : null,
|
||||
userId: (userId && userId !== '') ? userId : null,
|
||||
origin: origin,
|
||||
hostname: (searchHost && searchHost !== '') ? searchHost : null,
|
||||
})),
|
||||
|
|
@ -127,61 +110,11 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.files,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.xrmjdkdw {
|
||||
margin: var(--margin);
|
||||
|
||||
.urempief {
|
||||
margin-top: var(--margin);
|
||||
|
||||
&.list {
|
||||
> .file {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
margin-left: 0.3em;
|
||||
padding: 8px;
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: var(--margin) 0;
|
||||
|
||||
> .file {
|
||||
aspect-ratio: 1;
|
||||
|
||||
> .thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
<template>
|
||||
<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
|
||||
<div v-if="!narrow || initialPage == null" class="nav">
|
||||
<div v-if="!narrow || currentPage?.route.name == null" class="nav">
|
||||
<MkSpacer :content-max="700" :margin-min="16">
|
||||
<div class="lxpfedzu">
|
||||
<div class="banner">
|
||||
<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||
</div>
|
||||
|
||||
<MkInfo v-if="thereIsUnresolvedAbuseReport" warn class="info">{{ $ts.thereIsUnresolvedAbuseReportWarning }} <MkA to="/admin/abuses" class="_link">{{ $ts.check }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ $ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noEmailServer" warn class="info">{{ $ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="thereIsUnresolvedAbuseReport" warn class="info">{{ i18n.ts.thereIsUnresolvedAbuseReportWarning }} <MkA to="/admin/abuses" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
|
||||
<MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu>
|
||||
<MkSuperMenu :def="menuDef" :grid="currentPage?.route.name == null"></MkSuperMenu>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div v-if="!(narrow && initialPage == null)" class="main">
|
||||
<component :is="component" :key="initialPage" v-bind="pageProps"/>
|
||||
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
|
||||
<RouterView/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -25,8 +25,8 @@
|
|||
<script lang="ts" setup>
|
||||
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 MkSuperMenu from '@/components/MkSuperMenu.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { scroll } from '@/scripts/scroll';
|
||||
import { instance } from '@/instance';
|
||||
import * as os from '@/os';
|
||||
|
|
@ -41,27 +41,22 @@ const router = useRouter();
|
|||
const indexInfo = {
|
||||
title: i18n.ts.controlPanel,
|
||||
icon: 'fas fa-cog',
|
||||
bg: 'var(--bg)',
|
||||
hideHeader: true,
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
initialPage?: string,
|
||||
}>();
|
||||
|
||||
provide('shouldOmitHeaderTitle', false);
|
||||
|
||||
let INFO = $ref(indexInfo);
|
||||
let childInfo = $ref(null);
|
||||
let page = $ref(props.initialPage);
|
||||
let narrow = $ref(false);
|
||||
let view = $ref(null);
|
||||
let el = $ref(null);
|
||||
let pageProps = $ref({});
|
||||
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
|
||||
let noBotProtection = !instance.enableHcaptcha && !instance.enableRecaptcha;
|
||||
let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile;
|
||||
let noEmailServer = !instance.enableEmail;
|
||||
let thereIsUnresolvedAbuseReport = $ref(false);
|
||||
let currentPage = $computed(() => router.currentRef.value.child);
|
||||
|
||||
os.api('admin/abuse-user-reports', {
|
||||
state: 'unresolved',
|
||||
|
|
@ -95,47 +90,47 @@ const menuDef = $computed(() => [{
|
|||
icon: 'fas fa-tachometer-alt',
|
||||
text: i18n.ts.dashboard,
|
||||
to: '/admin/overview',
|
||||
active: props.initialPage === 'overview',
|
||||
active: currentPage?.route.name === 'overview',
|
||||
}, {
|
||||
icon: 'fas fa-users',
|
||||
text: i18n.ts.users,
|
||||
to: '/admin/users',
|
||||
active: props.initialPage === 'users',
|
||||
active: currentPage?.route.name === 'users',
|
||||
}, {
|
||||
icon: 'fas fa-laugh',
|
||||
text: i18n.ts.customEmojis,
|
||||
to: '/admin/emojis',
|
||||
active: props.initialPage === 'emojis',
|
||||
active: currentPage?.route.name === 'emojis',
|
||||
}, {
|
||||
icon: 'fas fa-globe',
|
||||
text: i18n.ts.federation,
|
||||
to: '/admin/federation',
|
||||
active: props.initialPage === 'federation',
|
||||
to: '/about#federation',
|
||||
active: currentPage?.route.name === 'federation',
|
||||
}, {
|
||||
icon: 'fas fa-clipboard-list',
|
||||
text: i18n.ts.jobQueue,
|
||||
to: '/admin/queue',
|
||||
active: props.initialPage === 'queue',
|
||||
active: currentPage?.route.name === 'queue',
|
||||
}, {
|
||||
icon: 'fas fa-cloud',
|
||||
text: i18n.ts.files,
|
||||
to: '/admin/files',
|
||||
active: props.initialPage === 'files',
|
||||
active: currentPage?.route.name === 'files',
|
||||
}, {
|
||||
icon: 'fas fa-broadcast-tower',
|
||||
text: i18n.ts.announcements,
|
||||
to: '/admin/announcements',
|
||||
active: props.initialPage === 'announcements',
|
||||
active: currentPage?.route.name === 'announcements',
|
||||
}, {
|
||||
icon: 'fas fa-audio-description',
|
||||
text: i18n.ts.ads,
|
||||
to: '/admin/ads',
|
||||
active: props.initialPage === 'ads',
|
||||
active: currentPage?.route.name === 'ads',
|
||||
}, {
|
||||
icon: 'fas fa-exclamation-circle',
|
||||
text: i18n.ts.abuseReports,
|
||||
to: '/admin/abuses',
|
||||
active: props.initialPage === 'abuses',
|
||||
active: currentPage?.route.name === 'abuses',
|
||||
}],
|
||||
}, {
|
||||
title: i18n.ts.settings,
|
||||
|
|
@ -143,47 +138,47 @@ const menuDef = $computed(() => [{
|
|||
icon: 'fas fa-cog',
|
||||
text: i18n.ts.general,
|
||||
to: '/admin/settings',
|
||||
active: props.initialPage === 'settings',
|
||||
active: currentPage?.route.name === 'settings',
|
||||
}, {
|
||||
icon: 'fas fa-envelope',
|
||||
text: i18n.ts.emailServer,
|
||||
to: '/admin/email-settings',
|
||||
active: props.initialPage === 'email-settings',
|
||||
active: currentPage?.route.name === 'email-settings',
|
||||
}, {
|
||||
icon: 'fas fa-cloud',
|
||||
text: i18n.ts.objectStorage,
|
||||
to: '/admin/object-storage',
|
||||
active: props.initialPage === 'object-storage',
|
||||
active: currentPage?.route.name === 'object-storage',
|
||||
}, {
|
||||
icon: 'fas fa-lock',
|
||||
text: i18n.ts.security,
|
||||
to: '/admin/security',
|
||||
active: props.initialPage === 'security',
|
||||
active: currentPage?.route.name === 'security',
|
||||
}, {
|
||||
icon: 'fas fa-globe',
|
||||
text: i18n.ts.relays,
|
||||
to: '/admin/relays',
|
||||
active: props.initialPage === 'relays',
|
||||
active: currentPage?.route.name === 'relays',
|
||||
}, {
|
||||
icon: 'fas fa-share-alt',
|
||||
text: i18n.ts.integration,
|
||||
to: '/admin/integrations',
|
||||
active: props.initialPage === 'integrations',
|
||||
active: currentPage?.route.name === 'integrations',
|
||||
}, {
|
||||
icon: 'fas fa-ban',
|
||||
text: i18n.ts.instanceBlocking,
|
||||
to: '/admin/instance-block',
|
||||
active: props.initialPage === 'instance-block',
|
||||
active: currentPage?.route.name === 'instance-block',
|
||||
}, {
|
||||
icon: 'fas fa-ghost',
|
||||
text: i18n.ts.proxyAccount,
|
||||
to: '/admin/proxy-account',
|
||||
active: props.initialPage === 'proxy-account',
|
||||
active: currentPage?.route.name === 'proxy-account',
|
||||
}, {
|
||||
icon: 'fas fa-cogs',
|
||||
text: i18n.ts.other,
|
||||
to: '/admin/other-settings',
|
||||
active: props.initialPage === 'other-settings',
|
||||
active: currentPage?.route.name === 'other-settings',
|
||||
}],
|
||||
}, {
|
||||
title: i18n.ts.info,
|
||||
|
|
@ -191,55 +186,12 @@ const menuDef = $computed(() => [{
|
|||
icon: 'fas fa-database',
|
||||
text: i18n.ts.database,
|
||||
to: '/admin/database',
|
||||
active: props.initialPage === 'database',
|
||||
active: currentPage?.route.name === 'database',
|
||||
}],
|
||||
}]);
|
||||
|
||||
const component = $computed(() => {
|
||||
if (props.initialPage == null) return null;
|
||||
switch (props.initialPage) {
|
||||
case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
|
||||
case 'users': return defineAsyncComponent(() => import('./users.vue'));
|
||||
case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
|
||||
case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
|
||||
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
|
||||
case 'files': return defineAsyncComponent(() => import('./files.vue'));
|
||||
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
|
||||
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
|
||||
case 'database': return defineAsyncComponent(() => import('./database.vue'));
|
||||
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
|
||||
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
|
||||
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
|
||||
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
|
||||
case 'security': return defineAsyncComponent(() => import('./security.vue'));
|
||||
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
|
||||
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
|
||||
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
|
||||
case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
|
||||
case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
|
||||
}
|
||||
});
|
||||
|
||||
watch(component, () => {
|
||||
pageProps = {};
|
||||
|
||||
nextTick(() => {
|
||||
scroll(el, { top: 0 });
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => props.initialPage, () => {
|
||||
if (props.initialPage == null && !narrow) {
|
||||
router.push('/admin/overview');
|
||||
} else {
|
||||
if (props.initialPage == null) {
|
||||
INFO = indexInfo;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(narrow, () => {
|
||||
if (props.initialPage == null && !narrow) {
|
||||
if (currentPage?.route.name == null && !narrow) {
|
||||
router.push('/admin/overview');
|
||||
}
|
||||
});
|
||||
|
|
@ -248,7 +200,7 @@ onMounted(() => {
|
|||
ro.observe(el);
|
||||
|
||||
narrow = el.offsetWidth < NARROW_THRESHOLD;
|
||||
if (props.initialPage == null && !narrow) {
|
||||
if (currentPage?.route.name == null && !narrow) {
|
||||
router.push('/admin/overview');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
|
|
@ -47,6 +47,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.instanceBlocking,
|
||||
icon: 'fas fa-ban',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="enableDiscordIntegration" class="_formBlock">
|
||||
<template #label>{{ $ts.enable }}</template>
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableDiscordIntegration">
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
</FormInput>
|
||||
</template>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</template>
|
||||
|
|
@ -28,11 +28,12 @@
|
|||
import { } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
let uri: string = $ref('');
|
||||
let enableDiscordIntegration: boolean = $ref(false);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="enableGithubIntegration" class="_formBlock">
|
||||
<template #label>{{ $ts.enable }}</template>
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableGithubIntegration">
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
</FormInput>
|
||||
</template>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</template>
|
||||
|
|
@ -28,11 +28,12 @@
|
|||
import { } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
let uri: string = $ref('');
|
||||
let enableGithubIntegration: boolean = $ref(false);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="enableTwitterIntegration" class="_formBlock">
|
||||
<template #label>{{ $ts.enable }}</template>
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableTwitterIntegration">
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
</FormInput>
|
||||
</template>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</template>
|
||||
|
|
@ -28,11 +28,12 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
let uri: string = $ref('');
|
||||
let enableTwitterIntegration: boolean = $ref(false);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.integration,
|
||||
icon: 'fas fa-share-alt',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -67,11 +67,11 @@ import {
|
|||
Tooltip,
|
||||
SubTitle
|
||||
} from 'chart.js';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import MkFolder from '@/components/ui/folder.vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkwFederation from '../../widgets/federation.vue';
|
||||
import { version, url } from '@/config';
|
||||
import bytes from '@/filters/bytes';
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ 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';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
|
|
@ -145,6 +144,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.objectStorage,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.other,
|
||||
icon: 'fas fa-cogs',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
100
packages/client/src/pages/admin/overview.federation.vue
Normal file
100
packages/client/src/pages/admin/overview.federation.vue
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<template>
|
||||
<div class="wbrkwale">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="instances">
|
||||
<MkA v-for="(instance, i) in instances" :key="instance.id" :to="`/instance-info/${instance.host}`" class="instance">
|
||||
<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
|
||||
<div class="body">
|
||||
<div class="name">{{ instance.name ?? instance.host }}</div>
|
||||
<div class="host">{{ instance.host }}</div>
|
||||
</div>
|
||||
<MkMiniChart class="chart" :src="charts[i].requests.received"/>
|
||||
</MkA>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import MkMiniChart from '@/components/MkMiniChart.vue';
|
||||
import * as os from '@/os';
|
||||
import { useInterval } from '@/scripts/use-interval';
|
||||
|
||||
const instances = ref([]);
|
||||
const charts = ref([]);
|
||||
const fetching = ref(true);
|
||||
|
||||
const fetch = async () => {
|
||||
const fetchedInstances = await os.api('federation/instances', {
|
||||
sort: '+lastCommunicatedAt',
|
||||
limit: 5,
|
||||
});
|
||||
const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.apiGet('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
|
||||
instances.value = fetchedInstances;
|
||||
charts.value = fetchedCharts;
|
||||
fetching.value = false;
|
||||
};
|
||||
|
||||
useInterval(fetch, 1000 * 60, {
|
||||
immediate: true,
|
||||
afterMounted: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wbrkwale {
|
||||
> .instances {
|
||||
.chart-move {
|
||||
transition: transform 1s ease;
|
||||
}
|
||||
|
||||
> .instance {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
}
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
font-size: 0.9em;
|
||||
color: var(--fg);
|
||||
padding-right: 8px;
|
||||
|
||||
> .name {
|
||||
display: block;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
> .host {
|
||||
margin: 0;
|
||||
font-size: 75%;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
> .chart {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
108
packages/client/src/pages/admin/overview.pie.vue
Normal file
108
packages/client/src/pages/admin/overview.pie.vue
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<canvas ref="chartEl"></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import {
|
||||
Chart,
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
DoughnutController,
|
||||
} from 'chart.js';
|
||||
import number from '@/filters/number';
|
||||
import { defaultStore } from '@/store';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||
|
||||
Chart.register(
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
DoughnutController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
data: { name: string; value: number; color: string; onClick?: () => void }[];
|
||||
}>();
|
||||
|
||||
const chartEl = ref<HTMLCanvasElement>(null);
|
||||
|
||||
// フォントカラー
|
||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||
|
||||
let chartInstance: Chart;
|
||||
|
||||
onMounted(() => {
|
||||
chartInstance = new Chart(chartEl.value, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: props.data.map(x => x.name),
|
||||
datasets: [{
|
||||
backgroundColor: props.data.map(x => x.color),
|
||||
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
|
||||
borderWidth: 2,
|
||||
hoverOffset: 0,
|
||||
data: props.data.map(x => x.value),
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
layout: {
|
||||
padding: {
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
},
|
||||
},
|
||||
onClick: (ev) => {
|
||||
const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
|
||||
if (hit && props.data[hit.index].onClick) {
|
||||
props.data[hit.index].onClick();
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
external: externalTooltipHandler,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
211
packages/client/src/pages/admin/overview.queue-chart.vue
Normal file
211
packages/client/src/pages/admin/overview.queue-chart.vue
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
<template>
|
||||
<canvas ref="chartEl"></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
||||
import {
|
||||
Chart,
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
} from 'chart.js';
|
||||
import number from '@/filters/number';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||
|
||||
Chart.register(
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
domain: string;
|
||||
connection: any;
|
||||
}>();
|
||||
|
||||
const alpha = (hex, a) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
||||
const r = parseInt(result[1], 16);
|
||||
const g = parseInt(result[2], 16);
|
||||
const b = parseInt(result[3], 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
};
|
||||
|
||||
const chartEl = ref<HTMLCanvasElement>(null);
|
||||
|
||||
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
// フォントカラー
|
||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||
|
||||
let chartInstance: Chart;
|
||||
|
||||
const onStats = (stats) => {
|
||||
chartInstance.data.labels.push('');
|
||||
chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
|
||||
chartInstance.data.datasets[1].data.push(stats[props.domain].active);
|
||||
chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
|
||||
chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
|
||||
if (chartInstance.data.datasets[0].data.length > 100) {
|
||||
chartInstance.data.labels.shift();
|
||||
chartInstance.data.datasets[0].data.shift();
|
||||
chartInstance.data.datasets[1].data.shift();
|
||||
chartInstance.data.datasets[2].data.shift();
|
||||
chartInstance.data.datasets[3].data.shift();
|
||||
}
|
||||
chartInstance.update();
|
||||
};
|
||||
|
||||
const onStatsLog = (statsLog) => {
|
||||
for (const stats of [...statsLog].reverse()) {
|
||||
chartInstance.data.labels.push('');
|
||||
chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
|
||||
chartInstance.data.datasets[1].data.push(stats[props.domain].active);
|
||||
chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
|
||||
chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
|
||||
if (chartInstance.data.datasets[0].data.length > 100) {
|
||||
chartInstance.data.labels.shift();
|
||||
chartInstance.data.datasets[0].data.shift();
|
||||
chartInstance.data.datasets[1].data.shift();
|
||||
chartInstance.data.datasets[2].data.shift();
|
||||
chartInstance.data.datasets[3].data.shift();
|
||||
}
|
||||
}
|
||||
chartInstance.update();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
chartInstance = new Chart(chartEl.value, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Process',
|
||||
pointRadius: 0,
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
borderJoinStyle: 'round',
|
||||
borderColor: '#00E396',
|
||||
backgroundColor: alpha('#00E396', 0.1),
|
||||
data: [],
|
||||
}, {
|
||||
label: 'Active',
|
||||
pointRadius: 0,
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
borderJoinStyle: 'round',
|
||||
borderColor: '#00BCD4',
|
||||
backgroundColor: alpha('#00BCD4', 0.1),
|
||||
data: [],
|
||||
}, {
|
||||
label: 'Waiting',
|
||||
pointRadius: 0,
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
borderJoinStyle: 'round',
|
||||
borderColor: '#FFB300',
|
||||
backgroundColor: alpha('#FFB300', 0.1),
|
||||
data: [],
|
||||
}, {
|
||||
label: 'Delayed',
|
||||
pointRadius: 0,
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
borderJoinStyle: 'round',
|
||||
borderColor: '#E53935',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: [],
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 2.5,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: false,
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
maxTicksLimit: 10,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
display: false,
|
||||
min: 0,
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
external: externalTooltipHandler,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
props.connection.on('stats', onStats);
|
||||
props.connection.on('statsLog', onStatsLog);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
props.connection.off('stats', onStats);
|
||||
props.connection.off('statsLog', onStatsLog);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
76
packages/client/src/pages/admin/overview.user.vue
Normal file
76
packages/client/src/pages/admin/overview.user.vue
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<MkA :class="[$style.root]" :to="`/user-info/${user.id}`">
|
||||
<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
<span class="name"><MkUserName class="name" :user="user"/></span>
|
||||
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
|
||||
</div>
|
||||
<MkMiniChart v-if="chart" class="chart" :src="chart.inc"/>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkMiniChart from '@/components/MkMiniChart.vue';
|
||||
import * as os from '@/os';
|
||||
import { acct } from '@/filters/user';
|
||||
|
||||
const props = defineProps<{
|
||||
user: misskey.entities.User;
|
||||
}>();
|
||||
|
||||
let chart = $ref(null);
|
||||
|
||||
os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16, span: 'day' }).then(res => {
|
||||
chart = res;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
$bodyTitleHieght: 18px;
|
||||
$bodyInfoHieght: 16px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> :global(.avatar) {
|
||||
display: block;
|
||||
width: ($bodyTitleHieght + $bodyInfoHieght);
|
||||
height: ($bodyTitleHieght + $bodyInfoHieght);
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
> :global(.body) {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
font-size: 0.9em;
|
||||
color: var(--fg);
|
||||
padding-right: 8px;
|
||||
|
||||
> :global(.name) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: $bodyTitleHieght;
|
||||
}
|
||||
|
||||
> :global(.sub) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: 95%;
|
||||
opacity: 0.7;
|
||||
line-height: $bodyInfoHieght;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
> :global(.chart) {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,112 +1,458 @@
|
|||
<template>
|
||||
<div v-size="{ max: [740] }" class="edbbcaef">
|
||||
<div v-if="stats" class="cfcdecdf" style="margin: var(--margin)">
|
||||
<div class="number _panel">
|
||||
<div class="label">Users</div>
|
||||
<div class="value _monospace">
|
||||
{{ number(stats.originalUsersCount) }}
|
||||
<MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
||||
<MkSpacer :content-max="900">
|
||||
<div ref="rootEl" v-size="{ max: [740] }" class="edbbcaef">
|
||||
<div class="left">
|
||||
<div v-if="stats" class="container stats">
|
||||
<div class="title">Stats</div>
|
||||
<div class="body">
|
||||
<div class="number _panel">
|
||||
<div class="label">Users</div>
|
||||
<div class="value _monospace">
|
||||
{{ number(stats.originalUsersCount) }}
|
||||
<MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
||||
</div>
|
||||
</div>
|
||||
<div class="number _panel">
|
||||
<div class="label">Notes</div>
|
||||
<div class="value _monospace">
|
||||
{{ number(stats.originalNotesCount) }}
|
||||
<MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container queue">
|
||||
<div class="title">Job queue</div>
|
||||
<div class="body">
|
||||
<div class="chart deliver">
|
||||
<div class="title">Deliver</div>
|
||||
<XQueueChart :connection="queueStatsConnection" domain="deliver"/>
|
||||
</div>
|
||||
<div class="chart inbox">
|
||||
<div class="title">Inbox</div>
|
||||
<XQueueChart :connection="queueStatsConnection" domain="inbox"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container users">
|
||||
<div class="title">New users</div>
|
||||
<div v-if="newUsers" class="body">
|
||||
<XUser v-for="user in newUsers" :key="user.id" class="user" :user="user"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container files">
|
||||
<div class="title">Recent files</div>
|
||||
<div class="body">
|
||||
<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container env">
|
||||
<div class="title">Enviroment</div>
|
||||
<div class="body">
|
||||
<div class="number _panel">
|
||||
<div class="label">Misskey</div>
|
||||
<div class="value _monospace">{{ version }}</div>
|
||||
</div>
|
||||
<div v-if="serverInfo" class="number _panel">
|
||||
<div class="label">Node.js</div>
|
||||
<div class="value _monospace">{{ serverInfo.node }}</div>
|
||||
</div>
|
||||
<div v-if="serverInfo" class="number _panel">
|
||||
<div class="label">PostgreSQL</div>
|
||||
<div class="value _monospace">{{ serverInfo.psql }}</div>
|
||||
</div>
|
||||
<div v-if="serverInfo" class="number _panel">
|
||||
<div class="label">Redis</div>
|
||||
<div class="value _monospace">{{ serverInfo.redis }}</div>
|
||||
</div>
|
||||
<div class="number _panel">
|
||||
<div class="label">Vue</div>
|
||||
<div class="value _monospace">{{ vueVersion }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="number _panel">
|
||||
<div class="label">Notes</div>
|
||||
<div class="value _monospace">
|
||||
{{ number(stats.originalNotesCount) }}
|
||||
<MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
||||
<div class="right">
|
||||
<div class="container charts">
|
||||
<div class="title">Active users</div>
|
||||
<div class="body">
|
||||
<canvas ref="chartEl"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container federation">
|
||||
<div class="title">Active instances</div>
|
||||
<div class="body">
|
||||
<XFederation/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="stats" class="container federationStats">
|
||||
<div class="title">Federation</div>
|
||||
<div class="body">
|
||||
<div class="number _panel">
|
||||
<div class="label">Sub</div>
|
||||
<div class="value _monospace">
|
||||
{{ number(federationSubActive) }}
|
||||
<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
||||
</div>
|
||||
</div>
|
||||
<div class="number _panel">
|
||||
<div class="label">Pub</div>
|
||||
<div class="value _monospace">
|
||||
{{ number(federationPubActive) }}
|
||||
<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container tagCloud">
|
||||
<div class="body">
|
||||
<MkTagCloud v-if="activeInstances">
|
||||
<li v-for="instance in activeInstances">
|
||||
<a @click.prevent="onInstanceClick(instance)">
|
||||
<img style="width: 32px;" :src="instance.iconUrl">
|
||||
</a>
|
||||
</li>
|
||||
</MkTagCloud>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="topSubInstancesForPie && topPubInstancesForPie" class="container federationPies">
|
||||
<div class="body">
|
||||
<div class="chart deliver">
|
||||
<div class="title">Sub</div>
|
||||
<XPie :data="topSubInstancesForPie"/>
|
||||
<div class="subTitle">Top 10</div>
|
||||
</div>
|
||||
<div class="chart inbox">
|
||||
<div class="title">Pub</div>
|
||||
<XPie :data="topPubInstancesForPie"/>
|
||||
<div class="subTitle">Top 10</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkContainer :foldable="true" class="charts">
|
||||
<template #header><i class="fas fa-chart-bar"></i>{{ i18n.ts.charts }}</template>
|
||||
<div style="padding: 12px;">
|
||||
<MkInstanceStats :chart-limit="500" :detailed="true"/>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<div class="queue">
|
||||
<MkContainer :foldable="true" :thin="true" class="deliver">
|
||||
<template #header>Queue: deliver</template>
|
||||
<MkQueueChart :connection="queueStatsConnection" domain="deliver"/>
|
||||
</MkContainer>
|
||||
<MkContainer :foldable="true" :thin="true" class="inbox">
|
||||
<template #header>Queue: inbox</template>
|
||||
<MkQueueChart :connection="queueStatsConnection" domain="inbox"/>
|
||||
</MkContainer>
|
||||
</div>
|
||||
|
||||
<!--<XMetrics/>-->
|
||||
|
||||
<MkFolder style="margin: var(--margin)">
|
||||
<template #header><i class="fas fa-info-circle"></i> {{ i18n.ts.info }}</template>
|
||||
<div class="cfcdecdf">
|
||||
<div class="number _panel">
|
||||
<div class="label">Misskey</div>
|
||||
<div class="value _monospace">{{ version }}</div>
|
||||
</div>
|
||||
<div v-if="serverInfo" class="number _panel">
|
||||
<div class="label">Node.js</div>
|
||||
<div class="value _monospace">{{ serverInfo.node }}</div>
|
||||
</div>
|
||||
<div v-if="serverInfo" class="number _panel">
|
||||
<div class="label">PostgreSQL</div>
|
||||
<div class="value _monospace">{{ serverInfo.psql }}</div>
|
||||
</div>
|
||||
<div v-if="serverInfo" class="number _panel">
|
||||
<div class="label">Redis</div>
|
||||
<div class="value _monospace">{{ serverInfo.redis }}</div>
|
||||
</div>
|
||||
<div class="number _panel">
|
||||
<div class="label">Vue</div>
|
||||
<div class="value _monospace">{{ vueVersion }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import {
|
||||
Chart,
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
} from 'chart.js';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import MagicGrid from 'magic-grid';
|
||||
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';
|
||||
import MkFolder from '@/components/ui/folder.vue';
|
||||
import MkQueueChart from '@/components/queue-chart.vue';
|
||||
import XFederation from './overview.federation.vue';
|
||||
import XQueueChart from './overview.queue-chart.vue';
|
||||
import XUser from './overview.user.vue';
|
||||
import XPie from './overview.pie.vue';
|
||||
import MkNumberDiff from '@/components/MkNumberDiff.vue';
|
||||
import MkTagCloud from '@/components/MkTagCloud.vue';
|
||||
import { version, url } from '@/config';
|
||||
import number from '@/filters/number';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { defaultStore } from '@/store';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
|
||||
|
||||
Chart.register(
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
//gradient,
|
||||
);
|
||||
|
||||
const rootEl = $ref<HTMLElement>();
|
||||
const chartEl = $ref<HTMLCanvasElement>(null);
|
||||
let stats: any = $ref(null);
|
||||
let serverInfo: any = $ref(null);
|
||||
let topSubInstancesForPie: any = $ref(null);
|
||||
let topPubInstancesForPie: any = $ref(null);
|
||||
let usersComparedToThePrevDay: any = $ref(null);
|
||||
let notesComparedToThePrevDay: any = $ref(null);
|
||||
let federationPubActive = $ref<number | null>(null);
|
||||
let federationPubActiveDiff = $ref<number | null>(null);
|
||||
let federationSubActive = $ref<number | null>(null);
|
||||
let federationSubActiveDiff = $ref<number | null>(null);
|
||||
let newUsers = $ref(null);
|
||||
let activeInstances = $shallowRef(null);
|
||||
const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
|
||||
const now = new Date();
|
||||
let chartInstance: Chart = null;
|
||||
const chartLimit = 30;
|
||||
const filesPagination = {
|
||||
endpoint: 'admin/drive/files' as const,
|
||||
limit: 9,
|
||||
noPaging: true,
|
||||
};
|
||||
|
||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||
|
||||
async function renderChart() {
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
}
|
||||
|
||||
const getDate = (ago: number) => {
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth();
|
||||
const d = now.getDate();
|
||||
|
||||
return new Date(y, m, d - ago);
|
||||
};
|
||||
|
||||
const format = (arr) => {
|
||||
return arr.map((v, i) => ({
|
||||
x: getDate(i).getTime(),
|
||||
y: v,
|
||||
}));
|
||||
};
|
||||
|
||||
const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' });
|
||||
|
||||
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
// フォントカラー
|
||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
const color = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
|
||||
|
||||
chartInstance = new Chart(chartEl, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
//labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
|
||||
datasets: [{
|
||||
parsing: false,
|
||||
label: 'a',
|
||||
data: format(raw.readWrite).slice().reverse(),
|
||||
tension: 0.3,
|
||||
pointRadius: 0,
|
||||
borderWidth: 0,
|
||||
borderJoinStyle: 'round',
|
||||
borderRadius: 3,
|
||||
backgroundColor: color,
|
||||
/*gradient: props.bar ? undefined : {
|
||||
backgroundColor: {
|
||||
axis: 'y',
|
||||
colors: {
|
||||
0: alpha(x.color ? x.color : getColor(i), 0),
|
||||
[maxes[i]]: alpha(x.color ? x.color : getColor(i), 0.2),
|
||||
},
|
||||
},
|
||||
},*/
|
||||
barPercentage: 0.9,
|
||||
categoryPercentage: 0.9,
|
||||
clip: 8,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 2.5,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
display: false,
|
||||
stacked: true,
|
||||
offset: false,
|
||||
time: {
|
||||
stepSize: 1,
|
||||
unit: 'month',
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
adapters: {
|
||||
date: {
|
||||
locale: enUS,
|
||||
},
|
||||
},
|
||||
min: getDate(chartLimit).getTime(),
|
||||
},
|
||||
y: {
|
||||
display: false,
|
||||
position: 'left',
|
||||
stacked: true,
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
//mirror: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
hoverRadius: 5,
|
||||
hoverBorderWidth: 2,
|
||||
},
|
||||
},
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
external: externalTooltipHandler,
|
||||
},
|
||||
//gradient,
|
||||
},
|
||||
},
|
||||
plugins: [{
|
||||
id: 'vLine',
|
||||
beforeDraw(chart, args, options) {
|
||||
if (chart.tooltip?._active?.length) {
|
||||
const activePoint = chart.tooltip._active[0];
|
||||
const ctx = chart.ctx;
|
||||
const x = activePoint.element.x;
|
||||
const topY = chart.scales.y.top;
|
||||
const bottomY = chart.scales.y.bottom;
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, bottomY);
|
||||
ctx.lineTo(x, topY);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = vLineColor;
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
},
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
function onInstanceClick(i) {
|
||||
os.pageWindow(`/instance-info/${i.host}`);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
/*
|
||||
const magicGrid = new MagicGrid({
|
||||
container: rootEl,
|
||||
static: true,
|
||||
animate: true,
|
||||
});
|
||||
|
||||
magicGrid.listen();
|
||||
*/
|
||||
|
||||
renderChart();
|
||||
|
||||
onMounted(async () => {
|
||||
os.api('stats', {}).then(statsResponse => {
|
||||
stats = statsResponse;
|
||||
|
||||
os.api('charts/users', { limit: 2, span: 'day' }).then(chart => {
|
||||
os.apiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
|
||||
usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1];
|
||||
});
|
||||
|
||||
os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => {
|
||||
os.apiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
|
||||
notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1];
|
||||
});
|
||||
});
|
||||
|
||||
os.apiGet('charts/federation', { limit: 2, span: 'day' }).then(chart => {
|
||||
federationPubActive = chart.pubActive[0];
|
||||
federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1];
|
||||
federationSubActive = chart.subActive[0];
|
||||
federationSubActiveDiff = chart.subActive[0] - chart.subActive[1];
|
||||
});
|
||||
|
||||
os.apiGet('federation/stats', { limit: 10 }).then(res => {
|
||||
topSubInstancesForPie = res.topSubInstances.map(x => ({
|
||||
name: x.host,
|
||||
color: x.themeColor,
|
||||
value: x.followersCount,
|
||||
onClick: () => {
|
||||
os.pageWindow(`/instance-info/${x.host}`);
|
||||
},
|
||||
})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowersCount }]);
|
||||
topPubInstancesForPie = res.topPubInstances.map(x => ({
|
||||
name: x.host,
|
||||
color: x.themeColor,
|
||||
value: x.followingCount,
|
||||
onClick: () => {
|
||||
os.pageWindow(`/instance-info/${x.host}`);
|
||||
},
|
||||
})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowingCount }]);
|
||||
});
|
||||
|
||||
os.api('admin/server-info').then(serverInfoResponse => {
|
||||
serverInfo = serverInfoResponse;
|
||||
});
|
||||
|
||||
os.api('admin/show-users', {
|
||||
limit: 5,
|
||||
sort: '+createdAt',
|
||||
}).then(res => {
|
||||
newUsers = res;
|
||||
});
|
||||
|
||||
os.api('federation/instances', {
|
||||
sort: '+lastCommunicatedAt',
|
||||
limit: 25,
|
||||
}).then(res => {
|
||||
activeInstances = res;
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
queueStatsConnection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 200,
|
||||
length: 100,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -122,69 +468,170 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.dashboard,
|
||||
icon: 'fas fa-tachometer-alt',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edbbcaef {
|
||||
.cfcdecdf {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
grid-template-columns: repeat(auto-fill,minmax(150px,1fr));
|
||||
display: flex;
|
||||
|
||||
> .number {
|
||||
padding: 12px 16px;
|
||||
> .left, > .right {
|
||||
box-sizing: border-box;
|
||||
width: 50%;
|
||||
|
||||
> .label {
|
||||
opacity: 0.7;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
> .container {
|
||||
margin: 32px 0;
|
||||
|
||||
> .value {
|
||||
> .title {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
> .diff {
|
||||
font-size: 0.8em;
|
||||
&.stats, &.federationStats {
|
||||
> .body {
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
|
||||
> .number {
|
||||
padding: 14px 20px;
|
||||
|
||||
> .label {
|
||||
opacity: 0.7;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
> .value {
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
|
||||
> .diff {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.env {
|
||||
> .body {
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
|
||||
> .number {
|
||||
padding: 14px 20px;
|
||||
|
||||
> .label {
|
||||
opacity: 0.7;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
> .value {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.charts {
|
||||
> .body {
|
||||
padding: 32px;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
}
|
||||
|
||||
&.users {
|
||||
> .body {
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
|
||||
> .user {
|
||||
padding: 16px 20px;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.federation {
|
||||
> .body {
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
}
|
||||
}
|
||||
|
||||
&.queue {
|
||||
> .body {
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
|
||||
> .chart {
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
|
||||
> .title {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.federationPies {
|
||||
> .body {
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
|
||||
> .chart {
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
|
||||
> .title {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
> .subTitle {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
font-size: 85%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tagCloud {
|
||||
> .body {
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .charts {
|
||||
margin: var(--margin);
|
||||
> .left {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
> .queue {
|
||||
margin: var(--margin);
|
||||
display: flex;
|
||||
|
||||
> .deliver,
|
||||
> .inbox {
|
||||
flex: 1;
|
||||
width: 50%;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: var(--margin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_740px {
|
||||
> .queue {
|
||||
display: block;
|
||||
|
||||
> .deliver,
|
||||
> .inbox {
|
||||
width: 100%;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: var(--margin);
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .right {
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import { fetchInstance } from '@/instance';
|
||||
|
|
@ -58,6 +58,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.proxyAccount,
|
||||
icon: 'fas fa-ghost',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
181
packages/client/src/pages/admin/queue.chart.chart.vue
Normal file
181
packages/client/src/pages/admin/queue.chart.chart.vue
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
<template>
|
||||
<canvas ref="chartEl"></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted, onUnmounted, ref } from 'vue';
|
||||
import {
|
||||
Chart,
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
} from 'chart.js';
|
||||
import number from '@/filters/number';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||
|
||||
Chart.register(
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
}>();
|
||||
|
||||
const alpha = (hex, a) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
||||
const r = parseInt(result[1], 16);
|
||||
const g = parseInt(result[2], 16);
|
||||
const b = parseInt(result[3], 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
};
|
||||
|
||||
const chartEl = ref<HTMLCanvasElement>(null);
|
||||
|
||||
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
// フォントカラー
|
||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||
|
||||
let chartInstance: Chart;
|
||||
|
||||
function setData(values) {
|
||||
if (chartInstance == null) return;
|
||||
for (const value of values) {
|
||||
chartInstance.data.labels.push('');
|
||||
chartInstance.data.datasets[0].data.push(value);
|
||||
if (chartInstance.data.datasets[0].data.length > 200) {
|
||||
chartInstance.data.labels.shift();
|
||||
chartInstance.data.datasets[0].data.shift();
|
||||
}
|
||||
}
|
||||
chartInstance.update();
|
||||
}
|
||||
|
||||
function pushData(value) {
|
||||
if (chartInstance == null) return;
|
||||
chartInstance.data.labels.push('');
|
||||
chartInstance.data.datasets[0].data.push(value);
|
||||
if (chartInstance.data.datasets[0].data.length > 200) {
|
||||
chartInstance.data.labels.shift();
|
||||
chartInstance.data.datasets[0].data.shift();
|
||||
}
|
||||
chartInstance.update();
|
||||
}
|
||||
|
||||
const label =
|
||||
props.type === 'process' ? 'Process' :
|
||||
props.type === 'active' ? 'Active' :
|
||||
props.type === 'delayed' ? 'Delayed' :
|
||||
props.type === 'waiting' ? 'Waiting' :
|
||||
'?' as never;
|
||||
|
||||
const color =
|
||||
props.type === 'process' ? '#00E396' :
|
||||
props.type === 'active' ? '#00BCD4' :
|
||||
props.type === 'delayed' ? '#E53935' :
|
||||
props.type === 'waiting' ? '#FFB300' :
|
||||
'?' as never;
|
||||
|
||||
onMounted(() => {
|
||||
chartInstance = new Chart(chartEl.value, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: label,
|
||||
pointRadius: 0,
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
borderJoinStyle: 'round',
|
||||
borderColor: color,
|
||||
backgroundColor: alpha(color, 0.1),
|
||||
data: [],
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 2.5,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: true,
|
||||
color: gridColor,
|
||||
borderColor: 'rgb(0, 0, 0, 0)',
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
maxTicksLimit: 10,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
grid: {
|
||||
color: gridColor,
|
||||
borderColor: 'rgb(0, 0, 0, 0)',
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
external: externalTooltipHandler,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
setData,
|
||||
pushData,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -1,80 +1,149 @@
|
|||
<template>
|
||||
<div class="_debobigegoItem">
|
||||
<div class="_debobigegoLabel"><slot name="title"></slot></div>
|
||||
<div class="_debobigegoPanel pumxzjhg">
|
||||
<div class="_table status">
|
||||
<div class="_row">
|
||||
<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
|
||||
<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
|
||||
<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
|
||||
<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
|
||||
<div class="pumxzjhg">
|
||||
<div class="_table status">
|
||||
<div class="_row">
|
||||
<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
|
||||
<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
|
||||
<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
|
||||
<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="charts">
|
||||
<div class="chart">
|
||||
<div class="title">Process</div>
|
||||
<XChart ref="chartProcess" type="process"/>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<div class="title">Active</div>
|
||||
<XChart ref="chartActive" type="active"/>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<div class="title">Delayed</div>
|
||||
<XChart ref="chartDelayed" type="delayed"/>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<div class="title">Waiting</div>
|
||||
<XChart ref="chartWaiting" type="waiting"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jobs">
|
||||
<div v-if="jobs.length > 0">
|
||||
<div v-for="job in jobs" :key="job[0]">
|
||||
<span>{{ job[0] }}</span>
|
||||
<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<MkQueueChart :domain="domain" :connection="connection"/>
|
||||
</div>
|
||||
<div class="jobs">
|
||||
<div v-if="jobs.length > 0">
|
||||
<div v-for="job in jobs" :key="job[0]">
|
||||
<span>{{ job[0] }}</span>
|
||||
<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
|
||||
</div>
|
||||
<span v-else style="opacity: 0.5;">{{ i18n.ts.noJobs }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { markRaw, onMounted, onUnmounted, ref } from 'vue';
|
||||
import XChart from './queue.chart.chart.vue';
|
||||
import number from '@/filters/number';
|
||||
import MkQueueChart from '@/components/queue-chart.vue';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const connection = markRaw(stream.useChannel('queueStats'));
|
||||
|
||||
const activeSincePrevTick = ref(0);
|
||||
const active = ref(0);
|
||||
const waiting = ref(0);
|
||||
const delayed = ref(0);
|
||||
const waiting = ref(0);
|
||||
const jobs = ref([]);
|
||||
let chartProcess = $ref<InstanceType<typeof XChart>>();
|
||||
let chartActive = $ref<InstanceType<typeof XChart>>();
|
||||
let chartDelayed = $ref<InstanceType<typeof XChart>>();
|
||||
let chartWaiting = $ref<InstanceType<typeof XChart>>();
|
||||
|
||||
const props = defineProps<{
|
||||
domain: string,
|
||||
connection: any,
|
||||
domain: string;
|
||||
}>();
|
||||
|
||||
const onStats = (stats) => {
|
||||
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
|
||||
active.value = stats[props.domain].active;
|
||||
delayed.value = stats[props.domain].delayed;
|
||||
waiting.value = stats[props.domain].waiting;
|
||||
|
||||
chartProcess.pushData(stats[props.domain].activeSincePrevTick);
|
||||
chartActive.pushData(stats[props.domain].active);
|
||||
chartDelayed.pushData(stats[props.domain].delayed);
|
||||
chartWaiting.pushData(stats[props.domain].waiting);
|
||||
};
|
||||
|
||||
const onStatsLog = (statsLog) => {
|
||||
const dataProcess = [];
|
||||
const dataActive = [];
|
||||
const dataDelayed = [];
|
||||
const dataWaiting = [];
|
||||
|
||||
for (const stats of [...statsLog].reverse()) {
|
||||
dataProcess.push(stats[props.domain].activeSincePrevTick);
|
||||
dataActive.push(stats[props.domain].active);
|
||||
dataDelayed.push(stats[props.domain].delayed);
|
||||
dataWaiting.push(stats[props.domain].waiting);
|
||||
}
|
||||
|
||||
chartProcess.setData(dataProcess);
|
||||
chartActive.setData(dataActive);
|
||||
chartDelayed.setData(dataDelayed);
|
||||
chartWaiting.setData(dataWaiting);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
|
||||
jobs.value = result;
|
||||
});
|
||||
|
||||
const onStats = (stats) => {
|
||||
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
|
||||
active.value = stats[props.domain].active;
|
||||
waiting.value = stats[props.domain].waiting;
|
||||
delayed.value = stats[props.domain].delayed;
|
||||
};
|
||||
|
||||
props.connection.on('stats', onStats);
|
||||
|
||||
onUnmounted(() => {
|
||||
props.connection.off('stats', onStats);
|
||||
connection.on('stats', onStats);
|
||||
connection.on('statsLog', onStatsLog);
|
||||
connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 200,
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
connection.off('stats', onStats);
|
||||
connection.off('statsLog', onStatsLog);
|
||||
connection.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pumxzjhg {
|
||||
> .status {
|
||||
padding: 16px;
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
}
|
||||
|
||||
> .charts {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
|
||||
> .chart {
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
|
||||
> .title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .jobs {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><XHeader v-model:tab="tab" :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>
|
||||
<XQueue v-if="tab === 'deliver'" domain="deliver"/>
|
||||
<XQueue v-else-if="tab === 'inbox'" domain="inbox"/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
|
@ -17,14 +12,13 @@
|
|||
import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import XQueue from './queue.chart.vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import * as config from '@/config';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const connection = markRaw(stream.useChannel('queueStats'));
|
||||
let tab = $ref('deliver');
|
||||
|
||||
function clear() {
|
||||
os.confirm({
|
||||
|
|
@ -38,19 +32,6 @@ function clear() {
|
|||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 200,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
connection.dispose();
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-up-right-from-square',
|
||||
|
|
@ -60,11 +41,16 @@ const headerActions = $computed(() => [{
|
|||
},
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'deliver',
|
||||
title: 'Deliver',
|
||||
}, {
|
||||
key: 'inbox',
|
||||
title: 'Inbox',
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.jobQueue,
|
||||
icon: 'fas fa-clipboard-list',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
|
@ -78,7 +78,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.relays,
|
||||
icon: 'fas fa-globe',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,80 @@
|
|||
<template #label>{{ i18n.ts.botProtection }}</template>
|
||||
<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
|
||||
<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
|
||||
<template v-else-if="enableTurnstile" #suffix>Turnstile</template>
|
||||
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
|
||||
|
||||
<XBotProtection/>
|
||||
</FormFolder>
|
||||
|
||||
<FormFolder class="_formBlock">
|
||||
<template #icon><i class="fas fa-eye-slash"></i></template>
|
||||
<template #label>{{ i18n.ts.sensitiveMediaDetection }}</template>
|
||||
<template v-if="sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
|
||||
<template v-else-if="sensitiveMediaDetection === 'local'" #suffix>{{ i18n.ts.localOnly }}</template>
|
||||
<template v-else-if="sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
|
||||
<template v-else #suffix>{{ i18n.ts.none }}</template>
|
||||
|
||||
<div class="_formRoot">
|
||||
<span class="_formBlock">{{ i18n.ts._sensitiveMediaDetection.description }}</span>
|
||||
|
||||
<FormRadios v-model="sensitiveMediaDetection" class="_formBlock">
|
||||
<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>
|
||||
</FormRadios>
|
||||
|
||||
<FormRange v-model="sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :text-converter="(v) => `${v + 1}`" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</template>
|
||||
<template #caption>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</template>
|
||||
</FormRange>
|
||||
|
||||
<FormSwitch v-model="enableSensitiveMediaDetectionForVideos" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
|
||||
<template #caption>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="setSensitiveFlagAutomatically" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }} ({{ i18n.ts.notRecommended }})</template>
|
||||
<template #caption>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<!-- 現状 false positive が多すぎて実用に耐えない
|
||||
<FormSwitch v-model="disallowUploadWhenPredictedAsPorn" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._sensitiveMediaDetection.disallowUploadWhenPredictedAsPorn }}</template>
|
||||
</FormSwitch>
|
||||
-->
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</FormFolder>
|
||||
|
||||
<FormFolder class="_formBlock">
|
||||
<template #label>Active Email Validation</template>
|
||||
<template v-if="enableActiveEmailValidation" #suffix>Enabled</template>
|
||||
<template v-else #suffix>Disabled</template>
|
||||
|
||||
<div class="_formRoot">
|
||||
<span class="_formBlock">{{ i18n.ts.activeEmailValidationDescription }}</span>
|
||||
<FormSwitch v-model="enableActiveEmailValidation" class="_formBlock" @update:modelValue="save">
|
||||
<template #label>Enable</template>
|
||||
</FormSwitch>
|
||||
</div>
|
||||
</FormFolder>
|
||||
|
||||
<FormFolder class="_formBlock">
|
||||
<template #label>Log IP address</template>
|
||||
<template v-if="enableIpLogging" #suffix>Enabled</template>
|
||||
<template v-else #suffix>Disabled</template>
|
||||
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="enableIpLogging" class="_formBlock" @update:modelValue="save">
|
||||
<template #label>Enable</template>
|
||||
</FormSwitch>
|
||||
</div>
|
||||
</FormFolder>
|
||||
|
||||
<FormFolder class="_formBlock">
|
||||
<template #label>Summaly Proxy</template>
|
||||
|
||||
|
|
@ -37,12 +106,13 @@ import { } from 'vue';
|
|||
import XBotProtection from './bot-protection.vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import FormFolder from '@/components/form/folder.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormRange from '@/components/form/range.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
|
@ -51,17 +121,48 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
|||
let summalyProxy: string = $ref('');
|
||||
let enableHcaptcha: boolean = $ref(false);
|
||||
let enableRecaptcha: boolean = $ref(false);
|
||||
let enableTurnstile: boolean = $ref(false);
|
||||
let sensitiveMediaDetection: string = $ref('none');
|
||||
let sensitiveMediaDetectionSensitivity: number = $ref(0);
|
||||
let setSensitiveFlagAutomatically: boolean = $ref(false);
|
||||
let enableSensitiveMediaDetectionForVideos: boolean = $ref(false);
|
||||
let enableIpLogging: boolean = $ref(false);
|
||||
let enableActiveEmailValidation: boolean = $ref(false);
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
summalyProxy = meta.summalyProxy;
|
||||
enableHcaptcha = meta.enableHcaptcha;
|
||||
enableRecaptcha = meta.enableRecaptcha;
|
||||
enableTurnstile = meta.enableTurnstile;
|
||||
sensitiveMediaDetection = meta.sensitiveMediaDetection;
|
||||
sensitiveMediaDetectionSensitivity =
|
||||
meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 :
|
||||
meta.sensitiveMediaDetectionSensitivity === 'low' ? 1 :
|
||||
meta.sensitiveMediaDetectionSensitivity === 'medium' ? 2 :
|
||||
meta.sensitiveMediaDetectionSensitivity === 'high' ? 3 :
|
||||
meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 4 : 0;
|
||||
setSensitiveFlagAutomatically = meta.setSensitiveFlagAutomatically;
|
||||
enableSensitiveMediaDetectionForVideos = meta.enableSensitiveMediaDetectionForVideos;
|
||||
enableIpLogging = meta.enableIpLogging;
|
||||
enableActiveEmailValidation = meta.enableActiveEmailValidation;
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
summalyProxy,
|
||||
sensitiveMediaDetection,
|
||||
sensitiveMediaDetectionSensitivity:
|
||||
sensitiveMediaDetectionSensitivity === 0 ? 'veryLow' :
|
||||
sensitiveMediaDetectionSensitivity === 1 ? 'low' :
|
||||
sensitiveMediaDetectionSensitivity === 2 ? 'medium' :
|
||||
sensitiveMediaDetectionSensitivity === 3 ? 'high' :
|
||||
sensitiveMediaDetectionSensitivity === 4 ? 'veryHigh' :
|
||||
0,
|
||||
setSensitiveFlagAutomatically,
|
||||
enableSensitiveMediaDetectionForVideos,
|
||||
enableIpLogging,
|
||||
enableActiveEmailValidation,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
|
@ -74,6 +175,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.security,
|
||||
icon: 'fas fa-lock',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ 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';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
|
|
@ -258,6 +258,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.general,
|
||||
icon: 'fas fa-cog',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -7,59 +7,43 @@
|
|||
<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>
|
||||
<template #label>{{ i18n.ts.sort }}</template>
|
||||
<option value="-createdAt">{{ i18n.ts.registeredDate }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+createdAt">{{ i18n.ts.registeredDate }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="-updatedAt">{{ i18n.ts.lastUsed }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="+updatedAt">{{ i18n.ts.lastUsed }} ({{ i18n.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>
|
||||
<template #label>{{ i18n.ts.state }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="available">{{ i18n.ts.normal }}</option>
|
||||
<option value="admin">{{ i18n.ts.administrator }}</option>
|
||||
<option value="moderator">{{ i18n.ts.moderator }}</option>
|
||||
<option value="silenced">{{ i18n.ts.silence }}</option>
|
||||
<option value="suspended">{{ i18n.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>
|
||||
<template #label>{{ i18n.ts.instance }}</template>
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<div class="inputs">
|
||||
<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
|
||||
<MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" @update:modelValue="$refs.users.reload()">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ $ts.username }}</template>
|
||||
<template #label>{{ i18n.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()">
|
||||
<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>
|
||||
<template #label>{{ i18n.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>
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${new Date(user.updatedAt).toLocaleString()}`" class="user" :to="`/user-info/${user.id}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -73,12 +57,12 @@ 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 MkPagination from '@/components/MkPagination.vue';
|
||||
import * as os from '@/os';
|
||||
import { lookupUser } from '@/scripts/lookup-user';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
|
||||
let paginationComponent = $ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
|
|
@ -151,7 +135,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.users,
|
||||
icon: 'fas fa-users',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
|
@ -174,54 +157,12 @@ definePageMetadata(computed(() => ({
|
|||
|
||||
> .users {
|
||||
margin-top: var(--margin);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
grid-gap: 12px;
|
||||
|
||||
> .user {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
margin-left: 0.3em;
|
||||
padding: 0 8px;
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
> header {
|
||||
> .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> .acct {
|
||||
margin-left: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> .staff {
|
||||
margin-left: 0.5em;
|
||||
color: var(--badge);
|
||||
}
|
||||
|
||||
> .punished {
|
||||
margin-left: 0.5em;
|
||||
color: #4dabf7;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .user:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
|
@ -47,7 +47,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'fas fa-broadcast-tower',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,30 @@
|
|||
<template>
|
||||
<div ref="rootEl" v-hotkey.global="keymap" v-size="{ min: [800] }" class="tqmomfks">
|
||||
<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
|
||||
<div class="tl _block">
|
||||
<XTimeline
|
||||
ref="tlEl" :key="antennaId"
|
||||
class="tl"
|
||||
src="antenna"
|
||||
:antenna="antennaId"
|
||||
:sound="true"
|
||||
@queue="queueUpdated"
|
||||
/>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<div ref="rootEl" v-hotkey.global="keymap" v-size="{ min: [800] }" class="tqmomfks">
|
||||
<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
|
||||
<div class="tl _block">
|
||||
<XTimeline
|
||||
ref="tlEl" :key="antennaId"
|
||||
class="tl"
|
||||
src="antenna"
|
||||
:antenna="antennaId"
|
||||
:sound="true"
|
||||
@queue="queueUpdated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, watch } from 'vue';
|
||||
import XTimeline from '@/components/timeline.vue';
|
||||
import XTimeline from '@/components/MkTimeline.vue';
|
||||
import { scroll } from '@/scripts/scroll';
|
||||
import * as os from '@/os';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import i18n from '@/components/global/i18n';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -68,23 +71,21 @@ watch(() => props.antennaId, async () => {
|
|||
});
|
||||
}, { immediate: true });
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
const headerActions = $computed(() => antenna ? [{
|
||||
icon: 'fas fa-calendar-alt',
|
||||
text: i18n.ts.jumpToSpecifiedDate,
|
||||
handler: timetravel,
|
||||
}, {
|
||||
icon: 'fas fa-cog',
|
||||
text: i18n.ts.settings,
|
||||
handler: settings,
|
||||
}] : []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => antenna ? {
|
||||
title: antenna.name,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-calendar-alt',
|
||||
text: i18n.ts.jumpToSpecifiedDate,
|
||||
handler: timetravel,
|
||||
}, {
|
||||
icon: 'fas fa-cog',
|
||||
text: i18n.ts.settings,
|
||||
handler: settings,
|
||||
}],
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
import { ref } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import { Endpoints } from 'misskey-js';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import XForm from './auth.form.vue';
|
||||
import MkSignin from '@/components/signin.vue';
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import * as os from '@/os';
|
||||
import { login } from '@/account';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,22 +4,22 @@
|
|||
<MkSpacer :content-max="700">
|
||||
<div class="_formRoot">
|
||||
<MkInput v-model="name" class="_formBlock">
|
||||
<template #label>{{ $ts.name }}</template>
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkTextarea v-model="description" class="_formBlock">
|
||||
<template #label>{{ $ts.description }}</template>
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<div class="banner">
|
||||
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton>
|
||||
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
|
||||
<div v-else-if="bannerUrl">
|
||||
<img :src="bannerUrl" style="width: 100%;"/>
|
||||
<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton>
|
||||
<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ i18n.ts._channel.removeBanner }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_formBlock">
|
||||
<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton>
|
||||
<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? i18n.ts.save : i18n.ts.create }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, inject, watch } from 'vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
|
|
@ -111,11 +111,9 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => props.channelId ? {
|
||||
title: i18n.ts._channel.edit,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
} : {
|
||||
title: i18n.ts._channel.create,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
}));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
</div>
|
||||
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
|
||||
<div class="status">
|
||||
<div><i class="fas fa-users fa-fw"></i><I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
||||
<div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
||||
<div><i class="fas fa-users fa-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
||||
<div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
||||
</div>
|
||||
<div class="fade"></div>
|
||||
</div>
|
||||
|
|
@ -33,10 +33,10 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, watch } from 'vue';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import XPostForm from '@/components/post-form.vue';
|
||||
import XTimeline from '@/components/timeline.vue';
|
||||
import XChannelFollowButton from '@/components/channel-follow-button.vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import XPostForm from '@/components/MkPostForm.vue';
|
||||
import XTimeline from '@/components/MkTimeline.vue';
|
||||
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { useRouter } from '@/router';
|
||||
import { $i } from '@/account';
|
||||
|
|
@ -80,7 +80,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => channel ? {
|
||||
title: channel.name,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div v-if="tab === 'featured'" class="_content grwlizim featured">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredPagination">
|
||||
|
|
@ -24,9 +24,9 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, inject } from 'vue';
|
||||
import MkChannelPreview from '@/components/channel-preview.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkChannelPreview from '@/components/MkChannelPreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
|
|
@ -59,25 +59,21 @@ const headerActions = $computed(() => [{
|
|||
}]);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab === 'featured',
|
||||
key: 'featured',
|
||||
title: i18n.ts._channel.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
onClick: () => { tab = 'featured'; },
|
||||
}, {
|
||||
active: tab === 'following',
|
||||
key: 'following',
|
||||
title: i18n.ts._channel.following,
|
||||
icon: 'fas fa-heart',
|
||||
onClick: () => { tab = 'following'; },
|
||||
}, {
|
||||
active: tab === 'owned',
|
||||
key: 'owned',
|
||||
title: i18n.ts._channel.owned,
|
||||
icon: 'fas fa-edit',
|
||||
onClick: () => { tab = 'owned'; },
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.channel,
|
||||
icon: 'fas fa-satellite-dish',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, watch, provide } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import XNotes from '@/components/MkNotes.vue';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
|
|
@ -102,7 +102,6 @@ const headerActions = $computed(() => clip && isOwned ? [{
|
|||
definePageMetadata(computed(() => clip ? {
|
||||
title: clip.name,
|
||||
icon: 'fas fa-paperclip',
|
||||
bg: 'var(--bg)',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import XDrive from '@/components/drive.vue';
|
||||
import XDrive from '@/components/MkDrive.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
|
@ -20,7 +20,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => ({
|
||||
title: folder ? folder.name : i18n.ts.drive,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
hideHeader: true,
|
||||
})));
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<div :class="$style.root">
|
||||
<XCategory v-if="tab === 'category'"/>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import XCategory from './emojis.category.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const tab = ref('category');
|
||||
|
||||
function menu(ev) {
|
||||
os.popupMenu([{
|
||||
icon: 'fas fa-download',
|
||||
text: i18n.ts.export,
|
||||
action: async () => {
|
||||
os.api('export-custom-emojis', {
|
||||
})
|
||||
.then(() => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: i18n.ts.exportRequested,
|
||||
});
|
||||
}).catch((err) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err.message,
|
||||
});
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
handler: menu,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: 'fas fa-laugh',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
30
packages/client/src/pages/explore.featured.vue
Normal file
30
packages/client/src/pages/explore.featured.vue
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkTab v-model="tab" style="margin-bottom: var(--margin);">
|
||||
<option value="notes">{{ i18n.ts.notes }}</option>
|
||||
<option value="polls">{{ i18n.ts.poll }}</option>
|
||||
</MkTab>
|
||||
<XNotes v-if="tab === 'notes'" :pagination="paginationForNotes"/>
|
||||
<XNotes v-else-if="tab === 'polls'" :pagination="paginationForPolls"/>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/MkNotes.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const paginationForNotes = {
|
||||
endpoint: 'notes/featured' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
const paginationForPolls = {
|
||||
endpoint: 'notes/polls/recommendation' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
let tab = $ref('notes');
|
||||
</script>
|
||||
148
packages/client/src/pages/explore.users.vue
Normal file
148
packages/client/src/pages/explore.users.vue
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<template>
|
||||
<MkSpacer :content-max="1200">
|
||||
<MkTab v-model="origin" style="margin-bottom: var(--margin);">
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</MkTab>
|
||||
<div v-if="origin === 'local'">
|
||||
<template v-if="tag == null">
|
||||
<MkFolder class="_gap" persist-key="explore-pinned-users">
|
||||
<template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
|
||||
<XUserList :pagination="pinnedUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap" persist-key="explore-popular-users">
|
||||
<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||
<XUserList :pagination="popularUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap" persist-key="explore-recently-updated-users">
|
||||
<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
|
||||
<XUserList :pagination="recentlyUpdatedUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap" persist-key="explore-recently-registered-users">
|
||||
<template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template>
|
||||
<XUserList :pagination="recentlyRegisteredUsers"/>
|
||||
</MkFolder>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_gap">
|
||||
<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
|
||||
|
||||
<div class="vxjfqztj">
|
||||
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/explore/tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
|
||||
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/explore/tags/${tag.tag}`">{{ tag.tag }}</MkA>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="tag != null" :key="`${tag}`" class="_gap">
|
||||
<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
|
||||
<XUserList :pagination="tagUsers"/>
|
||||
</MkFolder>
|
||||
|
||||
<template v-if="tag == null">
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||
<XUserList :pagination="popularUsersF"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
|
||||
<XUserList :pagination="recentlyUpdatedUsersF"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template>
|
||||
<XUserList :pagination="recentlyRegisteredUsersF"/>
|
||||
</MkFolder>
|
||||
</template>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
import XUserList from '@/components/MkUserList.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import number from '@/filters/number';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
}>();
|
||||
|
||||
let origin = $ref('local');
|
||||
let tagsEl = $ref<InstanceType<typeof MkFolder>>();
|
||||
let tagsLocal = $ref([]);
|
||||
let tagsRemote = $ref([]);
|
||||
|
||||
watch(() => props.tag, () => {
|
||||
if (tagsEl) tagsEl.toggleContent(props.tag == null);
|
||||
});
|
||||
|
||||
const tagUsers = $computed(() => ({
|
||||
endpoint: 'hashtags/users' as const,
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: props.tag,
|
||||
origin: 'combined',
|
||||
sort: '+follower',
|
||||
},
|
||||
}));
|
||||
|
||||
const pinnedUsers = { endpoint: 'pinned-users' };
|
||||
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
} };
|
||||
const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
state: 'alive',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'remote',
|
||||
sort: '+follower',
|
||||
} };
|
||||
const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
|
||||
os.api('hashtags/list', {
|
||||
sort: '+attachedLocalUsers',
|
||||
attachedToLocalUserOnly: true,
|
||||
limit: 30,
|
||||
}).then(tags => {
|
||||
tagsLocal = tags;
|
||||
});
|
||||
os.api('hashtags/list', {
|
||||
sort: '+attachedRemoteUsers',
|
||||
attachedToRemoteUserOnly: true,
|
||||
limit: 30,
|
||||
}).then(tags => {
|
||||
tagsRemote = tags;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vxjfqztj {
|
||||
> * {
|
||||
margin-right: 16px;
|
||||
|
||||
&.local {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,91 +1,39 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="1200">
|
||||
<div class="lznhrdub">
|
||||
<div v-if="tab === 'local'">
|
||||
<div v-if="instance && stats && tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: instance.bannerUrl ? `url(${instance.bannerUrl})` : null }">
|
||||
<header><span>{{ $t('explore', { host: instance.name || 'Misskey' }) }}</span></header>
|
||||
<div><span>{{ $t('exploreUsersCount', { count: number(stats.originalUsersCount) }) }}</span></div>
|
||||
</div>
|
||||
|
||||
<template v-if="tag == null">
|
||||
<MkFolder class="_gap" persist-key="explore-pinned-users">
|
||||
<template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.pinnedUsers }}</template>
|
||||
<XUserList :pagination="pinnedUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap" persist-key="explore-popular-users">
|
||||
<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
|
||||
<XUserList :pagination="popularUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap" persist-key="explore-recently-updated-users">
|
||||
<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
|
||||
<XUserList :pagination="recentlyUpdatedUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap" persist-key="explore-recently-registered-users">
|
||||
<template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyRegisteredUsers }}</template>
|
||||
<XUserList :pagination="recentlyRegisteredUsers"/>
|
||||
</MkFolder>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="tab === 'remote'">
|
||||
<div v-if="tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: `url(/client-assets/fedi.jpg)` }">
|
||||
<header><span>{{ $ts.exploreFediverse }}</span></header>
|
||||
</div>
|
||||
|
||||
<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_gap">
|
||||
<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template>
|
||||
|
||||
<div class="vxjfqztj">
|
||||
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/explore/tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
|
||||
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/explore/tags/${tag.tag}`">{{ tag.tag }}</MkA>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="tag != null" :key="`${tag}`" class="_gap">
|
||||
<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
|
||||
<XUserList :pagination="tagUsers"/>
|
||||
</MkFolder>
|
||||
|
||||
<template v-if="tag == null">
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
|
||||
<XUserList :pagination="popularUsersF"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
|
||||
<XUserList :pagination="recentlyUpdatedUsersF"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyDiscoveredUsers }}</template>
|
||||
<XUserList :pagination="recentlyRegisteredUsersF"/>
|
||||
</MkFolder>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="tab === 'search'">
|
||||
<div class="_isolated">
|
||||
<MkInput v-model="searchQuery" :debounce="true" type="search">
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<div class="lznhrdub">
|
||||
<div v-if="tab === 'featured'">
|
||||
<XFeatured/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'users'">
|
||||
<XUsers/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'search'">
|
||||
<MkSpacer :content-max="1200">
|
||||
<div>
|
||||
<MkInput v-model="searchQuery" :debounce="true" type="search" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.searchUser }}</template>
|
||||
<template #label>{{ i18n.ts.searchUser }}</template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="searchOrigin">
|
||||
<option value="combined">{{ $ts.all }}</option>
|
||||
<option value="local">{{ $ts.local }}</option>
|
||||
<option value="remote">{{ $ts.remote }}</option>
|
||||
<MkRadios v-model="searchOrigin" class="_formBlock">
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</MkRadios>
|
||||
</div>
|
||||
|
||||
<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, watch } from 'vue';
|
||||
import XUserList from '@/components/user-list.vue';
|
||||
import MkFolder from '@/components/ui/folder.vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import XFeatured from './explore.featured.vue';
|
||||
import XUsers from './explore.users.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkRadios from '@/components/form/radios.vue';
|
||||
import number from '@/filters/number';
|
||||
|
|
@ -93,16 +41,14 @@ import * as os from '@/os';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
import XUserList from '@/components/MkUserList.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
}>();
|
||||
|
||||
let tab = $ref('local');
|
||||
let tab = $ref('featured');
|
||||
let tagsEl = $ref<InstanceType<typeof MkFolder>>();
|
||||
let tagsLocal = $ref([]);
|
||||
let tagsRemote = $ref([]);
|
||||
let stats = $ref(null);
|
||||
let searchQuery = $ref(null);
|
||||
let searchOrigin = $ref('combined');
|
||||
|
||||
|
|
@ -110,44 +56,6 @@ watch(() => props.tag, () => {
|
|||
if (tagsEl) tagsEl.toggleContent(props.tag == null);
|
||||
});
|
||||
|
||||
const tagUsers = $computed(() => ({
|
||||
endpoint: 'hashtags/users' as const,
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: props.tag,
|
||||
origin: 'combined',
|
||||
sort: '+follower',
|
||||
},
|
||||
}));
|
||||
|
||||
const pinnedUsers = { endpoint: 'pinned-users' };
|
||||
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
} };
|
||||
const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'local',
|
||||
state: 'alive',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'remote',
|
||||
sort: '+follower',
|
||||
} };
|
||||
const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
origin: 'combined',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
const searchPagination = {
|
||||
endpoint: 'users/search' as const,
|
||||
limit: 10,
|
||||
|
|
@ -157,86 +65,23 @@ const searchPagination = {
|
|||
} : null),
|
||||
};
|
||||
|
||||
os.api('hashtags/list', {
|
||||
sort: '+attachedLocalUsers',
|
||||
attachedToLocalUserOnly: true,
|
||||
limit: 30,
|
||||
}).then(tags => {
|
||||
tagsLocal = tags;
|
||||
});
|
||||
os.api('hashtags/list', {
|
||||
sort: '+attachedRemoteUsers',
|
||||
attachedToRemoteUserOnly: true,
|
||||
limit: 30,
|
||||
}).then(tags => {
|
||||
tagsRemote = tags;
|
||||
});
|
||||
os.api('stats').then(_stats => {
|
||||
stats = _stats;
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab === 'local',
|
||||
title: i18n.ts.local,
|
||||
onClick: () => { tab = 'local'; },
|
||||
key: 'featured',
|
||||
icon: 'fas fa-bolt',
|
||||
title: i18n.ts.featured,
|
||||
}, {
|
||||
active: tab === 'remote',
|
||||
title: i18n.ts.remote,
|
||||
onClick: () => { tab = 'remote'; },
|
||||
key: 'users',
|
||||
icon: 'fas fa-users',
|
||||
title: i18n.ts.users,
|
||||
}, {
|
||||
active: tab === 'search',
|
||||
key: 'search',
|
||||
title: i18n.ts.search,
|
||||
onClick: () => { tab = 'search'; },
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.explore,
|
||||
icon: 'fas fa-hashtag',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.localfedi7 {
|
||||
color: #fff;
|
||||
padding: 16px;
|
||||
height: 80px;
|
||||
background-position: 50%;
|
||||
background-size: cover;
|
||||
margin-bottom: var(--margin);
|
||||
|
||||
> * {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
padding: 6px 8px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
> header {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> div {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.vxjfqztj {
|
||||
> * {
|
||||
margin-right: 16px;
|
||||
|
||||
&.local {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.noNotes }}</div>
|
||||
<div>{{ i18n.ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -22,9 +22,9 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import XNote from '@/components/note.vue';
|
||||
import XList from '@/components/date-separated-list.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import XNote from '@/components/MkNote.vue';
|
||||
import XList from '@/components/MkDateSeparatedList.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
|
|
@ -38,7 +38,6 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
|||
definePageMetadata({
|
||||
title: i18n.ts.favorites,
|
||||
icon: 'fas fa-star',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XNotes ref="notes" :pagination="pagination"/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/featured' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="1000">
|
||||
<div class="taeiyria">
|
||||
<div class="query">
|
||||
<MkInput v-model="host" :debounce="true" class="">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
<FormSplit style="margin-top: var(--margin);">
|
||||
<MkSelect v-model="state">
|
||||
<template #label>{{ $ts.state }}</template>
|
||||
<option value="all">{{ $ts.all }}</option>
|
||||
<option value="federating">{{ $ts.federating }}</option>
|
||||
<option value="subscribing">{{ $ts.subscribing }}</option>
|
||||
<option value="publishing">{{ $ts.publishing }}</option>
|
||||
<option value="suspended">{{ $ts.suspended }}</option>
|
||||
<option value="blocked">{{ $ts.blocked }}</option>
|
||||
<option value="notResponding">{{ $ts.notResponding }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="sort">
|
||||
<template #label>{{ $ts.sort }}</template>
|
||||
<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
|
||||
</MkSelect>
|
||||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<div class="dqokceoi">
|
||||
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Last communicated: ${new Date(instance.lastCommunicatedAt).toLocaleString()}\nStatus: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`" :behavior="'window'">
|
||||
<MkInstanceInfo :instance="instance"/>
|
||||
</MkA>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkInstanceInfo from '@/components/instance-info.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let host = $ref('');
|
||||
let state = $ref('federating');
|
||||
let sort = $ref('+pubSub');
|
||||
const pagination = {
|
||||
endpoint: 'federation/instances' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
params: computed(() => ({
|
||||
sort: sort,
|
||||
host: host !== '' ? host : null,
|
||||
...(
|
||||
state === 'federating' ? { federating: true } :
|
||||
state === 'subscribing' ? { subscribing: true } :
|
||||
state === 'publishing' ? { publishing: true } :
|
||||
state === 'suspended' ? { suspended: true } :
|
||||
state === 'blocked' ? { blocked: true } :
|
||||
state === 'notResponding' ? { notResponding: true } :
|
||||
{}),
|
||||
})),
|
||||
};
|
||||
|
||||
function getStatus(instance) {
|
||||
if (instance.isSuspended) return 'Suspended';
|
||||
if (instance.isBlocked) return 'Blocked';
|
||||
if (instance.isNotResponding) return 'Error';
|
||||
return 'Alive';
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.federation,
|
||||
icon: 'fas fa-globe',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.taeiyria {
|
||||
> .query {
|
||||
background: var(--bg);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.dqokceoi {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
grid-gap: 12px;
|
||||
|
||||
> .instance:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,39 +1,42 @@
|
|||
<template>
|
||||
<div>
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.noFollowRequests }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{items}">
|
||||
<div class="mk-follow-requests">
|
||||
<div v-for="req in items" :key="req.id" class="user _panel">
|
||||
<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
|
||||
<p class="acct">@{{ acct(req.follower) }}</p>
|
||||
</div>
|
||||
<div v-if="req.follower.description" class="description" :title="req.follower.description">
|
||||
<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button>
|
||||
<button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noFollowRequests }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{items}">
|
||||
<div class="mk-follow-requests">
|
||||
<div v-for="req in items" :key="req.id" class="user _panel">
|
||||
<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
|
||||
<p class="acct">@{{ acct(req.follower) }}</p>
|
||||
</div>
|
||||
<div v-if="req.follower.description" class="description" :title="req.follower.description">
|
||||
<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button>
|
||||
<button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { userPage, acct } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
|
@ -65,7 +68,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.followRequests,
|
||||
icon: 'fas fa-user-clock',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,63 +3,60 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import * as os from '@/os';
|
||||
import { mainRouter } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
created() {
|
||||
const acct = new URL(location.href).searchParams.get('acct');
|
||||
if (acct == null) return;
|
||||
async function follow(user): Promise<void> {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('followConfirm', { name: user.name || user.username }),
|
||||
});
|
||||
|
||||
let promise;
|
||||
if (canceled) {
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
os.apiWithDialog('following/create', {
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (acct.startsWith('https://')) {
|
||||
promise = os.api('ap/show', {
|
||||
uri: acct,
|
||||
});
|
||||
promise.then(res => {
|
||||
if (res.type === 'User') {
|
||||
this.follow(res.object);
|
||||
} else if (res.type === 'Note') {
|
||||
mainRouter.push(`/notes/${res.object.id}`);
|
||||
} else {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Not a user',
|
||||
}).then(() => {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
const acct = new URL(location.href).searchParams.get('acct');
|
||||
if (acct == null) {
|
||||
throw new Error('acct required');
|
||||
}
|
||||
|
||||
let promise;
|
||||
|
||||
if (acct.startsWith('https://')) {
|
||||
promise = os.api('ap/show', {
|
||||
uri: acct,
|
||||
});
|
||||
promise.then(res => {
|
||||
if (res.type === 'User') {
|
||||
follow(res.object);
|
||||
} else if (res.type === 'Note') {
|
||||
mainRouter.push(`/notes/${res.object.id}`);
|
||||
} else {
|
||||
promise = os.api('users/show', Acct.parse(acct));
|
||||
promise.then(user => {
|
||||
this.follow(user);
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Not a user',
|
||||
}).then(() => {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
promise = os.api('users/show', Acct.parse(acct));
|
||||
promise.then(user => {
|
||||
follow(user);
|
||||
});
|
||||
}
|
||||
|
||||
os.promiseDialog(promise, null, null, this.$ts.fetchingAsApObject);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async follow(user) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: this.$t('followConfirm', { name: user.name || user.username }),
|
||||
});
|
||||
|
||||
if (canceled) {
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
os.apiWithDialog('following/create', {
|
||||
userId: user.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,39 +1,41 @@
|
|||
<template>
|
||||
<div>
|
||||
<FormSuspense :p="init">
|
||||
<FormInput v-model="title">
|
||||
<template #label>{{ $ts.title }}</template>
|
||||
</FormInput>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormInput v-model="title">
|
||||
<template #label>{{ i18n.ts.title }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormTextarea v-model="description" :max="500">
|
||||
<template #label>{{ $ts.description }}</template>
|
||||
</FormTextarea>
|
||||
<FormTextarea v-model="description" :max="500">
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormGroup>
|
||||
<div v-for="file in files" :key="file.id" class="_formGroup wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
|
||||
<div class="name">{{ file.name }}</div>
|
||||
<button v-tooltip="$ts.remove" class="remove _button" @click="remove(file)"><i class="fas fa-times"></i></button>
|
||||
<div class="">
|
||||
<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
|
||||
<div class="name">{{ file.name }}</div>
|
||||
<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<FormButton primary @click="selectFile"><i class="fas fa-plus"></i> {{ i18n.ts.attachFile }}</FormButton>
|
||||
</div>
|
||||
<FormButton primary @click="selectFile"><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton>
|
||||
</FormGroup>
|
||||
|
||||
<FormSwitch v-model="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
|
||||
<FormSwitch v-model="isSensitive">{{ i18n.ts.markAsSensitive }}</FormSwitch>
|
||||
|
||||
<FormButton v-if="postId" primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
<FormButton v-else primary @click="save"><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton>
|
||||
<FormButton v-if="postId" primary @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
<FormButton v-else primary @click="save"><i class="fas fa-save"></i> {{ i18n.ts.publish }}</FormButton>
|
||||
|
||||
<FormButton v-if="postId" danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton>
|
||||
</FormSuspense>
|
||||
</div>
|
||||
<FormButton v-if="postId" danger @click="del"><i class="fas fa-trash-alt"></i> {{ i18n.ts.delete }}</FormButton>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, watch } from 'vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import { selectFiles } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
|
|
@ -72,7 +74,7 @@ async function save() {
|
|||
fileIds: files.map(file => file.id),
|
||||
isSensitive: isSensitive,
|
||||
});
|
||||
mainRouter.push(`/gallery/${props.postId}`);
|
||||
router.push(`/gallery/${props.postId}`);
|
||||
} else {
|
||||
const created = await os.apiWithDialog('gallery/posts/create', {
|
||||
title: title,
|
||||
|
|
@ -93,7 +95,7 @@ async function del() {
|
|||
await os.apiWithDialog('gallery/posts/delete', {
|
||||
postId: props.postId,
|
||||
});
|
||||
mainRouter.push('/gallery');
|
||||
router.push('/gallery');
|
||||
}
|
||||
|
||||
watch(() => props.postId, () => {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="1400">
|
||||
<div class="_root">
|
||||
<MkTab v-if="$i" v-model="tab">
|
||||
<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option>
|
||||
<option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option>
|
||||
<option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option>
|
||||
</MkTab>
|
||||
|
||||
<div v-if="tab === 'explore'">
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template>
|
||||
<template #header><i class="fas fa-clock"></i>{{ i18n.ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
|
|
@ -19,7 +13,7 @@
|
|||
</MkPagination>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template>
|
||||
<template #header><i class="fas fa-fire-alt"></i>{{ i18n.ts.popularPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
|
|
@ -35,7 +29,7 @@
|
|||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'my'">
|
||||
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA>
|
||||
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ i18n.ts.postToGallery }}</MkA>
|
||||
<MkPagination v-slot="{items}" :pagination="myPostsPagination">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
|
|
@ -49,17 +43,20 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, watch } from 'vue';
|
||||
import XUserList from '@/components/user-list.vue';
|
||||
import MkFolder from '@/components/ui/folder.vue';
|
||||
import XUserList from '@/components/MkUserList.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
|
||||
import number from '@/filters/number';
|
||||
import * as os from '@/os';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
import { useRouter } from '@/router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
|
|
@ -100,14 +97,31 @@ watch(() => props.tag, () => {
|
|||
if (tagsRef) tagsRef.tags.toggleContent(props.tag == null);
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.create,
|
||||
handler: () => {
|
||||
router.push('/gallery/new');
|
||||
},
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'explore',
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'fas fa-icons',
|
||||
}, {
|
||||
key: 'liked',
|
||||
title: i18n.ts._gallery.liked,
|
||||
icon: 'fas fa-heart',
|
||||
}, {
|
||||
key: 'my',
|
||||
title: i18n.ts._gallery.my,
|
||||
icon: 'fas fa-edit',
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'fas fa-icons',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,63 +1,68 @@
|
|||
<template>
|
||||
<div class="_root">
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="post" class="rkxwuolj">
|
||||
<div class="files">
|
||||
<div v-for="file in post.files" :key="file.id" class="file">
|
||||
<img :src="file.url"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body _block">
|
||||
<div class="title">{{ post.title }}</div>
|
||||
<div class="description"><Mfm :text="post.description"/></div>
|
||||
<div class="info">
|
||||
<i class="fas fa-clock"></i> <MkTime :time="post.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="like">
|
||||
<MkButton v-if="post.isLiked" v-tooltip="$ts._gallery.unlike" class="button" primary @click="unlike()"><i class="fas fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="$ts._gallery.like" class="button" @click="like()"><i class="far fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="1000" :margin-min="16" :margin-max="32">
|
||||
<div class="_root">
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="post" class="rkxwuolj">
|
||||
<div class="files">
|
||||
<div v-for="file in post.files" :key="file.id" class="file">
|
||||
<img :src="file.url"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="other">
|
||||
<button v-if="$i && $i.id === post.user.id" v-tooltip="$ts.edit" v-click-anime class="_button" @click="edit"><i class="fas fa-pencil-alt fa-fw"></i></button>
|
||||
<button v-tooltip="$ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="fas fa-retweet fa-fw"></i></button>
|
||||
<button v-tooltip="$ts.share" v-click-anime class="_button" @click="share"><i class="fas fa-share-alt fa-fw"></i></button>
|
||||
<div class="body _block">
|
||||
<div class="title">{{ post.title }}</div>
|
||||
<div class="description"><Mfm :text="post.description"/></div>
|
||||
<div class="info">
|
||||
<i class="fas fa-clock"></i> <MkTime :time="post.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="like">
|
||||
<MkButton v-if="post.isLiked" v-tooltip="i18n.ts._gallery.unlike" class="button" primary @click="unlike()"><i class="fas fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts._gallery.like" class="button" @click="like()"><i class="far fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
|
||||
</div>
|
||||
<div class="other">
|
||||
<button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="fas fa-pencil-alt fa-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="fas fa-retweet fa-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="fas fa-share-alt fa-fw"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="post.user" class="avatar"/>
|
||||
<div class="name">
|
||||
<MkUserName :user="post.user" style="display: block;"/>
|
||||
<MkAcct :user="post.user"/>
|
||||
</div>
|
||||
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||
<template #header><i class="fas fa-clock"></i> {{ i18n.ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
|
||||
<div class="sdrarzaf">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkContainer>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="post.user" class="avatar"/>
|
||||
<div class="name">
|
||||
<MkUserName :user="post.user" style="display: block;"/>
|
||||
<MkAcct :user="post.user"/>
|
||||
</div>
|
||||
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
|
||||
<div class="sdrarzaf">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkContainer>
|
||||
<MkError v-else-if="error" @retry="fetch()"/>
|
||||
<MkLoading v-else/>
|
||||
</transition>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetch()"/>
|
||||
<MkLoading v-else/>
|
||||
</transition>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, inject, watch } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
|
||||
import MkFollowButton from '@/components/follow-button.vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
import { url } from '@/config';
|
||||
import { useRouter } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
|
|
@ -69,8 +74,8 @@ const props = defineProps<{
|
|||
postId: string;
|
||||
}>();
|
||||
|
||||
const post = $ref(null);
|
||||
const error = $ref(null);
|
||||
let post = $ref(null);
|
||||
let error = $ref(null);
|
||||
const otherPostsPagination = {
|
||||
endpoint: 'users/gallery/posts' as const,
|
||||
limit: 6,
|
||||
|
|
@ -133,23 +138,17 @@ function edit() {
|
|||
|
||||
watch(() => props.postId, fetchPost, { immediate: true });
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
text: i18n.ts.edit,
|
||||
handler: edit,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => post ? {
|
||||
title: post.title,
|
||||
avatar: post.user,
|
||||
path: `/gallery/${post.id}`,
|
||||
share: {
|
||||
title: post.title,
|
||||
text: post.description,
|
||||
},
|
||||
actions: [{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
text: i18n.ts.edit,
|
||||
handler: edit,
|
||||
}],
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,66 +1,67 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="instance" :content-max="600" :margin-min="16" :margin-max="32">
|
||||
<div v-if="tab === 'overview'" class="_formRoot">
|
||||
<div class="fnfelxur">
|
||||
<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/>
|
||||
<span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
|
||||
</div>
|
||||
<MkKeyValue :copy="host" oneline style="margin: 1em 0;">
|
||||
<template #key>Host</template>
|
||||
<template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>Name</template>
|
||||
<template #value>{{ instance.name || `(${$ts.unknown})` }}</template>
|
||||
<template #key>{{ i18n.ts.software }}</template>
|
||||
<template #value><span class="_monospace">{{ instance.softwareName || `(${i18n.ts.unknown})` }} / {{ instance.softwareVersion || `(${i18n.ts.unknown})` }}</span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>{{ instance.maintainerName || `(${i18n.ts.unknown})` }} ({{ instance.maintainerEmail || `(${i18n.ts.unknown})` }})</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ $ts.description }}</template>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value>{{ instance.description }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.software }}</template>
|
||||
<template #value><span class="_monospace">{{ instance.softwareName || `(${$ts.unknown})` }} / {{ instance.softwareVersion || `(${$ts.unknown})` }}</span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.administrator }}</template>
|
||||
<template #value>{{ instance.maintainerName || `(${$ts.unknown})` }} ({{ instance.maintainerEmail || `(${$ts.unknown})` }})</template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormSection v-if="iAmModerator">
|
||||
<template #label>Moderation</template>
|
||||
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch>
|
||||
<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch>
|
||||
<MkButton @click="refreshMetadata">Refresh metadata</MkButton>
|
||||
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</FormSwitch>
|
||||
<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</FormSwitch>
|
||||
<MkButton @click="refreshMetadata"><i class="fas fa-refresh"></i> Refresh metadata</MkButton>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.registeredAt }}</template>
|
||||
<template #key>{{ i18n.ts.registeredAt }}</template>
|
||||
<template #value><MkTime mode="detail" :time="instance.caughtAt"/></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.updatedAt }}</template>
|
||||
<template #key>{{ i18n.ts.updatedAt }}</template>
|
||||
<template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.latestRequestSentAt }}</template>
|
||||
<template #key>{{ i18n.ts.latestRequestSentAt }}</template>
|
||||
<template #value><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.latestStatus }}</template>
|
||||
<template #key>{{ i18n.ts.latestStatus }}</template>
|
||||
<template #value>{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.latestRequestReceivedAt }}</template>
|
||||
<template #key>{{ i18n.ts.latestRequestReceivedAt }}</template>
|
||||
<template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>Open Registrations</template>
|
||||
<template #value>{{ instance.openRegistrations ? $ts.yes : $ts.no }}</template>
|
||||
<template #key>Following (Pub)</template>
|
||||
<template #value>{{ number(instance.followingCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>Followers (Sub)</template>
|
||||
<template #value>{{ number(instance.followersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
|
||||
|
|
@ -73,21 +74,21 @@
|
|||
<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
|
||||
</FormSection>
|
||||
</div>
|
||||
<div v-if="tab === 'chart'" class="_formRoot">
|
||||
<div v-else-if="tab === 'chart'" class="_formRoot">
|
||||
<div class="cmhjzshl">
|
||||
<div class="selects">
|
||||
<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
|
||||
<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
|
||||
<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
|
||||
<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
||||
<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
|
||||
<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
||||
<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
|
||||
<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
||||
<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
||||
<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
||||
<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
|
||||
<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
||||
<option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option>
|
||||
<option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option>
|
||||
<option value="instance-users-total">{{ i18n.ts._instanceCharts.usersTotal }}</option>
|
||||
<option value="instance-notes">{{ i18n.ts._instanceCharts.notes }}</option>
|
||||
<option value="instance-notes-total">{{ i18n.ts._instanceCharts.notesTotal }}</option>
|
||||
<option value="instance-ff">{{ i18n.ts._instanceCharts.ff }}</option>
|
||||
<option value="instance-ff-total">{{ i18n.ts._instanceCharts.ffTotal }}</option>
|
||||
<option value="instance-drive-usage">{{ i18n.ts._instanceCharts.cacheSize }}</option>
|
||||
<option value="instance-drive-usage-total">{{ i18n.ts._instanceCharts.cacheSizeTotal }}</option>
|
||||
<option value="instance-drive-files">{{ i18n.ts._instanceCharts.files }}</option>
|
||||
<option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<div class="charts">
|
||||
|
|
@ -98,7 +99,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tab === 'raw'" class="_formRoot">
|
||||
<div v-else-if="tab === 'users'" class="_formRoot">
|
||||
<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${new Date(user.updatedAt).toLocaleString()}`" class="user" :to="`/user-info/${user.id}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'raw'" class="_formRoot">
|
||||
<MkObjectView tall :value="instance">
|
||||
</MkObjectView>
|
||||
</div>
|
||||
|
|
@ -109,13 +117,13 @@
|
|||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkChart from '@/components/chart.vue';
|
||||
import MkObjectView from '@/components/object-view.vue';
|
||||
import MkChart from '@/components/MkChart.vue';
|
||||
import MkObjectView from '@/components/MkObjectView.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import MkLink from '@/components/link.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import * as os from '@/os';
|
||||
|
|
@ -124,30 +132,37 @@ import bytes from '@/filters/bytes';
|
|||
import { iAmModerator } from '@/account';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
host: string;
|
||||
}>();
|
||||
|
||||
let tab = $ref('overview');
|
||||
let chartSrc = $ref('instance-requests');
|
||||
let meta = $ref<misskey.entities.DetailedInstanceMetadata | null>(null);
|
||||
let instance = $ref<misskey.entities.Instance | null>(null);
|
||||
let suspended = $ref(false);
|
||||
let isBlocked = $ref(false);
|
||||
let chartSrc = $ref('instance-requests');
|
||||
|
||||
const usersPagination = {
|
||||
endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
sort: '+updatedAt',
|
||||
state: 'all',
|
||||
hostname: props.host,
|
||||
},
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
async function fetch() {
|
||||
if (iAmModerator) {
|
||||
// suspended and blocked information is only displayed to moderators.
|
||||
// otherwise the API will error anyway
|
||||
|
||||
meta = await os.api('admin/meta', { detail: true });
|
||||
instance = await os.api('federation/show-instance', {
|
||||
host: props.host,
|
||||
});
|
||||
suspended = instance.isSuspended;
|
||||
isBlocked = meta.blockedHosts.includes(instance.host);
|
||||
}
|
||||
instance = await os.api('federation/show-instance', {
|
||||
host: props.host,
|
||||
});
|
||||
suspended = instance.isSuspended;
|
||||
isBlocked = instance.isBlocked;
|
||||
}
|
||||
|
||||
async function toggleBlock(ev) {
|
||||
|
|
@ -184,37 +199,44 @@ const headerActions = $computed(() => [{
|
|||
}]);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab === 'overview',
|
||||
key: 'overview',
|
||||
title: i18n.ts.overview,
|
||||
icon: 'fas fa-info-circle',
|
||||
onClick: () => { tab = 'overview'; },
|
||||
}, {
|
||||
active: tab === 'chart',
|
||||
key: 'chart',
|
||||
title: i18n.ts.charts,
|
||||
icon: 'fas fa-chart-simple',
|
||||
onClick: () => { tab = 'chart'; },
|
||||
}, {
|
||||
active: tab === 'raw',
|
||||
title: 'Raw data',
|
||||
key: 'users',
|
||||
title: i18n.ts.users,
|
||||
icon: 'fas fa-users',
|
||||
}, {
|
||||
key: 'raw',
|
||||
title: 'Raw',
|
||||
icon: 'fas fa-code',
|
||||
onClick: () => { tab = 'raw'; },
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
title: props.host,
|
||||
icon: 'fas fa-server',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fnfelxur {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .icon {
|
||||
display: block;
|
||||
margin: 0;
|
||||
margin: 0 16px 0 0;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
> .name {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.cmhjzshl {
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XNotes :pagination="pagination"/>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.mentions,
|
||||
icon: 'fas fa-at',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<template><MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<XNotes :pagination="pagination"/>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
visibility: 'specified',
|
||||
},
|
||||
};
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.directNotes,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, defineComponent, inject, markRaw, onMounted, onUnmounted } from 'vue';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { acct } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
|
|
@ -159,7 +159,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.messaging,
|
||||
icon: 'fas fa-comments',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,22 @@ function onDragover(ev: DragEvent) {
|
|||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||
if (isFile || isDriveFile) {
|
||||
ev.preventDefault();
|
||||
ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
|
||||
switch (ev.dataTransfer.effectAllowed) {
|
||||
case 'all':
|
||||
case 'uninitialized':
|
||||
case 'copy':
|
||||
case 'copyLink':
|
||||
case 'copyMove':
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
break;
|
||||
case 'linkMove':
|
||||
case 'move':
|
||||
ev.dataTransfer.dropEffect = 'move';
|
||||
break;
|
||||
default:
|
||||
ev.dataTransfer.dropEffect = 'none';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import { } from 'vue';
|
|||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
||||
import MkUrlPreview from '@/components/url-preview.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ import * as Misskey from 'misskey-js';
|
|||
import * as Acct from 'misskey-js/built/acct';
|
||||
import XMessage from './messaging-room.message.vue';
|
||||
import XForm from './messaging-room.form.vue';
|
||||
import XList from '@/components/date-separated-list.vue';
|
||||
import MkPagination, { Paging } from '@/components/ui/pagination.vue';
|
||||
import XList from '@/components/MkDateSeparatedList.vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import { isBottomVisible, onScrollBottom, scrollToBottom } from '@/scripts/scroll';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
|
|
@ -154,7 +154,22 @@ function onDragover(ev: DragEvent) {
|
|||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||
|
||||
if (isFile || isDriveFile) {
|
||||
ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
|
||||
switch (ev.dataTransfer.effectAllowed) {
|
||||
case 'all':
|
||||
case 'uninitialized':
|
||||
case 'copy':
|
||||
case 'copyLink':
|
||||
case 'copyMove':
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
break;
|
||||
case 'linkMove':
|
||||
case 'move':
|
||||
ev.dataTransfer.dropEffect = 'move';
|
||||
break;
|
||||
default:
|
||||
ev.dataTransfer.dropEffect = 'none';
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
ev.dataTransfer.dropEffect = 'none';
|
||||
}
|
||||
|
|
@ -292,6 +307,7 @@ definePageMetadata(computed(() => !fetching ? user ? {
|
|||
<style lang="scss" scoped>
|
||||
.mk-messaging-room {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
|
||||
> .body {
|
||||
.more {
|
||||
|
|
@ -335,10 +351,7 @@ definePageMetadata(computed(() => !fetching ? user ? {
|
|||
z-index: 2;
|
||||
bottom: 0;
|
||||
padding-top: 8px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
bottom: calc(env(safe-area-inset-bottom, 0px) + 92px);
|
||||
}
|
||||
bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
|
||||
|
||||
> .new-message {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -1,301 +1,313 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<div class="mwysmxbg">
|
||||
<div class="_isolated">{{ $ts._mfm.intro }}</div>
|
||||
<MkSpacer :content-max="800">
|
||||
<div class="mwysmxbg">
|
||||
<div>{{ i18n.ts._mfm.intro }}</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.mention }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.mentionDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_mention"/>
|
||||
<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.hashtag }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.hashtagDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_hashtag"/>
|
||||
<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.url }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.urlDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_url"/>
|
||||
<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.link }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.linkDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_link"/>
|
||||
<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.emoji }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.emojiDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_emoji"/>
|
||||
<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.bold }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.boldDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_bold"/>
|
||||
<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.small }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.smallDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_small"/>
|
||||
<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.quote }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.quoteDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_quote"/>
|
||||
<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.center }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.centerDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_center"/>
|
||||
<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.inlineCode }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.inlineCodeDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_inlineCode"/>
|
||||
<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.blockCode }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.blockCodeDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_blockCode"/>
|
||||
<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.inlineMath }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.inlineMathDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_inlineMath"/>
|
||||
<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- deprecated
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.mention }}</div>
|
||||
<div class="title">{{ i18n.ts._mfm.search }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.mentionDescription }}</p>
|
||||
<p>{{ i18n.ts._mfm.searchDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_mention"/>
|
||||
<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea>
|
||||
<Mfm :text="preview_search"/>
|
||||
<MkTextarea v-model="preview_search"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.hashtag }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.hashtagDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_hashtag"/>
|
||||
<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea>
|
||||
-->
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.flip }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.flipDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_flip"/>
|
||||
<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.font }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.fontDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_font"/>
|
||||
<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.x2 }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.x2Description }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_x2"/>
|
||||
<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.x3 }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.x3Description }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_x3"/>
|
||||
<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.x4 }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.x4Description }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_x4"/>
|
||||
<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.blur }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.blurDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_blur"/>
|
||||
<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.jelly }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.jellyDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_jelly"/>
|
||||
<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.tada }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.tadaDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_tada"/>
|
||||
<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.jump }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.jumpDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_jump"/>
|
||||
<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.bounce }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.bounceDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_bounce"/>
|
||||
<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.spin }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.spinDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_spin"/>
|
||||
<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.shake }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.shakeDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_shake"/>
|
||||
<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.twitch }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.twitchDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_twitch"/>
|
||||
<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.rainbow }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.rainbowDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_rainbow"/>
|
||||
<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.sparkle }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.sparkleDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_sparkle"/>
|
||||
<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.rotate }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.rotateDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_rotate"/>
|
||||
<MkTextarea v-model="preview_rotate"><span>MFM</span></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ i18n.ts._mfm.plain }}</div>
|
||||
<div class="content">
|
||||
<p>{{ i18n.ts._mfm.plainDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_plain"/>
|
||||
<MkTextarea v-model="preview_plain"><span>MFM</span></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.url }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.urlDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_url"/>
|
||||
<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.link }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.linkDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_link"/>
|
||||
<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.emoji }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.emojiDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_emoji"/>
|
||||
<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.bold }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.boldDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_bold"/>
|
||||
<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.small }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.smallDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_small"/>
|
||||
<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.quote }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.quoteDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_quote"/>
|
||||
<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.center }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.centerDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_center"/>
|
||||
<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.inlineCode }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.inlineCodeDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_inlineCode"/>
|
||||
<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.blockCode }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.blockCodeDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_blockCode"/>
|
||||
<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.inlineMath }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.inlineMathDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_inlineMath"/>
|
||||
<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- deprecated
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.search }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.searchDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_search"/>
|
||||
<MkTextarea v-model="preview_search"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.flip }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.flipDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_flip"/>
|
||||
<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.font }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.fontDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_font"/>
|
||||
<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.x2 }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.x2Description }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_x2"/>
|
||||
<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.x3 }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.x3Description }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_x3"/>
|
||||
<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.x4 }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.x4Description }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_x4"/>
|
||||
<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.blur }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.blurDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_blur"/>
|
||||
<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.jelly }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.jellyDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_jelly"/>
|
||||
<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.tada }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.tadaDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_tada"/>
|
||||
<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.jump }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.jumpDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_jump"/>
|
||||
<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.bounce }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.bounceDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_bounce"/>
|
||||
<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.spin }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.spinDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_spin"/>
|
||||
<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.shake }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.shakeDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_shake"/>
|
||||
<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.twitch }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.twitchDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_twitch"/>
|
||||
<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.rainbow }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.rainbowDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_rainbow"/>
|
||||
<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.sparkle }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.sparkleDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_sparkle"/>
|
||||
<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section _block">
|
||||
<div class="title">{{ $ts._mfm.rotate }}</div>
|
||||
<div class="content">
|
||||
<p>{{ $ts._mfm.rotateDescription }}</p>
|
||||
<div class="preview">
|
||||
<Mfm :text="preview_rotate"/>
|
||||
<MkTextarea v-model="preview_rotate"><span>MFM</span></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
|
|
@ -306,35 +318,36 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
|||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
|
||||
const preview_mention = '@example';
|
||||
const preview_hashtag = '#test';
|
||||
const preview_url = 'https://example.com';
|
||||
const preview_link = `[${i18n.ts._mfm.dummy}](https://example.com)`;
|
||||
const preview_emoji = instance.emojis.length ? `:${instance.emojis[0].name}:` : ':emojiname:';
|
||||
const preview_bold = `**${i18n.ts._mfm.dummy}**`;
|
||||
const preview_small = `<small>${i18n.ts._mfm.dummy}</small>`;
|
||||
const preview_center = `<center>${i18n.ts._mfm.dummy}</center>`;
|
||||
const preview_inlineCode = '`<: "Hello, world!"`';
|
||||
const preview_blockCode = '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```';
|
||||
const preview_inlineMath = '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)';
|
||||
const preview_quote = `> ${i18n.ts._mfm.dummy}`;
|
||||
const preview_search = `${i18n.ts._mfm.dummy} 検索`;
|
||||
const preview_jelly = '$[jelly 🍮] $[jelly.speed=5s 🍮]';
|
||||
const preview_tada = '$[tada 🍮] $[tada.speed=5s 🍮]';
|
||||
const preview_jump = '$[jump 🍮] $[jump.speed=5s 🍮]';
|
||||
const preview_bounce = '$[bounce 🍮] $[bounce.speed=5s 🍮]';
|
||||
const preview_shake = '$[shake 🍮] $[shake.speed=5s 🍮]';
|
||||
const preview_twitch = '$[twitch 🍮] $[twitch.speed=5s 🍮]';
|
||||
const preview_spin = '$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]';
|
||||
const preview_flip = `$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`;
|
||||
const preview_font = `$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]\n$[font.cursive ${i18n.ts._mfm.dummy}]\n$[font.fantasy ${i18n.ts._mfm.dummy}]`;
|
||||
const preview_x2 = '$[x2 🍮]';
|
||||
const preview_x3 = '$[x3 🍮]';
|
||||
const preview_x4 = '$[x4 🍮]';
|
||||
const preview_blur = `$[blur ${i18n.ts._mfm.dummy}]`;
|
||||
const preview_rainbow = '$[rainbow 🍮] $[rainbow.speed=5s 🍮]';
|
||||
const preview_sparkle = '$[sparkle 🍮]';
|
||||
const preview_rotate = '$[rotate 🍮]';
|
||||
let preview_mention = $ref('@example');
|
||||
let preview_hashtag = $ref('#test');
|
||||
let preview_url = $ref('https://example.com');
|
||||
let preview_link = $ref(`[${i18n.ts._mfm.dummy}](https://example.com)`);
|
||||
let preview_emoji = $ref(instance.emojis.length ? `:${instance.emojis[0].name}:` : ':emojiname:');
|
||||
let preview_bold = $ref(`**${i18n.ts._mfm.dummy}**`);
|
||||
let preview_small = $ref(`<small>${i18n.ts._mfm.dummy}</small>`);
|
||||
let preview_center = $ref(`<center>${i18n.ts._mfm.dummy}</center>`);
|
||||
let preview_inlineCode = $ref('`<: "Hello, world!"`');
|
||||
let preview_blockCode = $ref('```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```');
|
||||
let preview_inlineMath = $ref('\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)');
|
||||
let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
|
||||
let preview_search = $ref(`${i18n.ts._mfm.dummy} 検索`);
|
||||
let preview_jelly = $ref('$[jelly 🍮] $[jelly.speed=5s 🍮]');
|
||||
let preview_tada = $ref('$[tada 🍮] $[tada.speed=5s 🍮]');
|
||||
let preview_jump = $ref('$[jump 🍮] $[jump.speed=5s 🍮]');
|
||||
let preview_bounce = $ref('$[bounce 🍮] $[bounce.speed=5s 🍮]');
|
||||
let preview_shake = $ref('$[shake 🍮] $[shake.speed=5s 🍮]');
|
||||
let preview_twitch = $ref('$[twitch 🍮] $[twitch.speed=5s 🍮]');
|
||||
let preview_spin = $ref('$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]');
|
||||
let preview_flip = $ref(`$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`);
|
||||
let preview_font = $ref(`$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]\n$[font.cursive ${i18n.ts._mfm.dummy}]\n$[font.fantasy ${i18n.ts._mfm.dummy}]`);
|
||||
let preview_x2 = $ref('$[x2 🍮]');
|
||||
let preview_x3 = $ref('$[x3 🍮]');
|
||||
let preview_x4 = $ref('$[x4 🍮]');
|
||||
let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`);
|
||||
let preview_rainbow = $ref('$[rainbow 🍮] $[rainbow.speed=5s 🍮]');
|
||||
let preview_sparkle = $ref('$[sparkle 🍮]');
|
||||
let preview_rotate = $ref('$[rotate 🍮]');
|
||||
let preview_plain = $ref('<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>');
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts._mfm.cheatSheet,
|
||||
|
|
|
|||
|
|
@ -1,85 +1,88 @@
|
|||
<template>
|
||||
<div v-if="$i">
|
||||
<div v-if="state == 'waiting'" class="waiting _section">
|
||||
<div class="_content">
|
||||
<MkLoading/>
|
||||
<MkSpacer :content-max="800">
|
||||
<div v-if="$i">
|
||||
<div v-if="state == 'waiting'" class="waiting _section">
|
||||
<div class="_content">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="state == 'denied'" class="denied _section">
|
||||
<div class="_content">
|
||||
<p>{{ i18n.ts._auth.denied }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="state == 'accepted'" class="accepted _section">
|
||||
<div class="_content">
|
||||
<p v-if="callback">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
|
||||
<p v-else>{{ i18n.ts._auth.pleaseGoBack }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="_section">
|
||||
<div v-if="name" class="_title">{{ $t('_auth.shareAccess', { name: name }) }}</div>
|
||||
<div v-else class="_title">{{ i18n.ts._auth.shareAccessAsk }}</div>
|
||||
<div class="_content">
|
||||
<p>{{ i18n.ts._auth.permissionAsk }}</p>
|
||||
<ul>
|
||||
<li v-for="p in _permissions" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="state == 'denied'" class="denied _section">
|
||||
<div class="_content">
|
||||
<p>{{ $ts._auth.denied }}</p>
|
||||
</div>
|
||||
<div v-else class="signin">
|
||||
<MkSignin @login="onLogin"/>
|
||||
</div>
|
||||
<div v-else-if="state == 'accepted'" class="accepted _section">
|
||||
<div class="_content">
|
||||
<p v-if="callback">{{ $ts._auth.callback }}<MkEllipsis/></p>
|
||||
<p v-else>{{ $ts._auth.pleaseGoBack }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="_section">
|
||||
<div v-if="name" class="_title">{{ $t('_auth.shareAccess', { name: name }) }}</div>
|
||||
<div v-else class="_title">{{ $ts._auth.shareAccessAsk }}</div>
|
||||
<div class="_content">
|
||||
<p>{{ $ts._auth.permissionAsk }}</p>
|
||||
<ul>
|
||||
<li v-for="p in permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<MkButton inline @click="deny">{{ $ts.cancel }}</MkButton>
|
||||
<MkButton inline primary @click="accept">{{ $ts.accept }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="signin">
|
||||
<MkSignin @login="onLogin"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MkSignin from '@/components/signin.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { login } from '@/account';
|
||||
import { $i, login } from '@/account';
|
||||
import { appendQuery, query } from '@/scripts/url';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkSignin,
|
||||
MkButton,
|
||||
},
|
||||
props: ['session', 'callback', 'name', 'icon', 'permission'],
|
||||
data() {
|
||||
return {
|
||||
state: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async accept() {
|
||||
this.state = 'waiting';
|
||||
await os.api('miauth/gen-token', {
|
||||
session: this.session,
|
||||
name: this.name,
|
||||
iconUrl: this.icon,
|
||||
permission: this.permission,
|
||||
});
|
||||
const props = defineProps<{
|
||||
session: string;
|
||||
callback?: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
permission: string; // コンマ区切り
|
||||
}>();
|
||||
|
||||
this.state = 'accepted';
|
||||
if (this.callback) {
|
||||
location.href = appendQuery(this.callback, query({
|
||||
session: this.session,
|
||||
}));
|
||||
}
|
||||
},
|
||||
deny() {
|
||||
this.state = 'denied';
|
||||
},
|
||||
onLogin(res) {
|
||||
login(res.i);
|
||||
},
|
||||
},
|
||||
});
|
||||
const _permissions = props.permission.split(',');
|
||||
|
||||
let state = $ref<string | null>(null);
|
||||
|
||||
async function accept(): Promise<void> {
|
||||
state = 'waiting';
|
||||
await os.api('miauth/gen-token', {
|
||||
session: props.session,
|
||||
name: props.name,
|
||||
iconUrl: props.icon,
|
||||
permission: _permissions,
|
||||
});
|
||||
|
||||
state = 'accepted';
|
||||
if (props.callback) {
|
||||
location.href = appendQuery(props.callback, query({
|
||||
session: props.session,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function deny(): void {
|
||||
state = 'denied';
|
||||
}
|
||||
|
||||
function onLogin(res): void {
|
||||
login(res.i);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,51 +2,52 @@
|
|||
<div class="shaynizk">
|
||||
<div class="form">
|
||||
<MkInput v-model="name" class="_formBlock">
|
||||
<template #label>{{ $ts.name }}</template>
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="src" class="_formBlock">
|
||||
<template #label>{{ $ts.antennaSource }}</template>
|
||||
<option value="all">{{ $ts._antennaSources.all }}</option>
|
||||
<!--<option value="home">{{ $ts._antennaSources.homeTimeline }}</option>-->
|
||||
<option value="users">{{ $ts._antennaSources.users }}</option>
|
||||
<!--<option value="list">{{ $ts._antennaSources.userList }}</option>-->
|
||||
<!--<option value="group">{{ $ts._antennaSources.userGroup }}</option>-->
|
||||
<template #label>{{ i18n.ts.antennaSource }}</template>
|
||||
<option value="all">{{ i18n.ts._antennaSources.all }}</option>
|
||||
<!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
|
||||
<option value="users">{{ i18n.ts._antennaSources.users }}</option>
|
||||
<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
|
||||
<!--<option value="group">{{ i18n.ts._antennaSources.userGroup }}</option>-->
|
||||
</MkSelect>
|
||||
<MkSelect v-if="src === 'list'" v-model="userListId" class="_formBlock">
|
||||
<template #label>{{ $ts.userList }}</template>
|
||||
<template #label>{{ i18n.ts.userList }}</template>
|
||||
<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-else-if="src === 'group'" v-model="userGroupId" class="_formBlock">
|
||||
<template #label>{{ $ts.userGroup }}</template>
|
||||
<template #label>{{ i18n.ts.userGroup }}</template>
|
||||
<option v-for="group in userGroups" :key="group.id" :value="group.id">{{ group.name }}</option>
|
||||
</MkSelect>
|
||||
<MkTextarea v-else-if="src === 'users'" v-model="users" class="_formBlock">
|
||||
<template #label>{{ $ts.users }}</template>
|
||||
<template #caption>{{ $ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ $ts.addUser }}</button></template>
|
||||
<template #label>{{ i18n.ts.users }}</template>
|
||||
<template #caption>{{ i18n.ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ i18n.ts.addUser }}</button></template>
|
||||
</MkTextarea>
|
||||
<MkSwitch v-model="withReplies" class="_formBlock">{{ $ts.withReplies }}</MkSwitch>
|
||||
<MkSwitch v-model="withReplies" class="_formBlock">{{ i18n.ts.withReplies }}</MkSwitch>
|
||||
<MkTextarea v-model="keywords" class="_formBlock">
|
||||
<template #label>{{ $ts.antennaKeywords }}</template>
|
||||
<template #caption>{{ $ts.antennaKeywordsDescription }}</template>
|
||||
<template #label>{{ i18n.ts.antennaKeywords }}</template>
|
||||
<template #caption>{{ i18n.ts.antennaKeywordsDescription }}</template>
|
||||
</MkTextarea>
|
||||
<MkTextarea v-model="excludeKeywords" class="_formBlock">
|
||||
<template #label>{{ $ts.antennaExcludeKeywords }}</template>
|
||||
<template #caption>{{ $ts.antennaKeywordsDescription }}</template>
|
||||
<template #label>{{ i18n.ts.antennaExcludeKeywords }}</template>
|
||||
<template #caption>{{ i18n.ts.antennaKeywordsDescription }}</template>
|
||||
</MkTextarea>
|
||||
<MkSwitch v-model="caseSensitive" class="_formBlock">{{ $ts.caseSensitive }}</MkSwitch>
|
||||
<MkSwitch v-model="withFile" class="_formBlock">{{ $ts.withFileAntenna }}</MkSwitch>
|
||||
<MkSwitch v-model="notify" class="_formBlock">{{ $ts.notifyAntenna }}</MkSwitch>
|
||||
<MkSwitch v-model="caseSensitive" class="_formBlock">{{ i18n.ts.caseSensitive }}</MkSwitch>
|
||||
<MkSwitch v-model="withFile" class="_formBlock">{{ i18n.ts.withFileAntenna }}</MkSwitch>
|
||||
<MkSwitch v-model="notify" class="_formBlock">{{ i18n.ts.notifyAntenna }}</MkSwitch>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<MkButton inline primary @click="saveAntenna()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
<MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="fas fa-trash"></i> {{ $ts.delete }}</MkButton>
|
||||
<MkButton inline primary @click="saveAntenna()"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="fas fa-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
|
|
@ -34,7 +34,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
<template><MkStickyContainer>
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="qtcaoidl">
|
||||
<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="qtcaoidl">
|
||||
<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="list">
|
||||
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
|
||||
<b>{{ item.name }}</b>
|
||||
<div v-if="item.description" class="description">{{ item.description }}</div>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="list">
|
||||
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
|
||||
<b>{{ item.name }}</b>
|
||||
<div v-if="item.description" class="description">{{ item.description }}</div>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
|
@ -69,7 +71,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.clip,
|
||||
icon: 'fas fa-paperclip',
|
||||
bg: 'var(--bg)',
|
||||
action: {
|
||||
icon: 'fas fa-plus',
|
||||
handler: create,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
<template><MkStickyContainer>
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="qkcjvfiv">
|
||||
<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="qkcjvfiv">
|
||||
<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ i18n.ts.createList }}</MkButton>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="lists _content">
|
||||
<MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`">
|
||||
<div class="name">{{ list.name }}</div>
|
||||
<MkAvatars :user-ids="list.userIds"/>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="lists _content">
|
||||
<MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`">
|
||||
<div class="name">{{ list.name }}</div>
|
||||
<MkAvatars :user-ids="list.userIds"/>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkAvatars from '@/components/avatars.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkAvatars from '@/components/MkAvatars.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
|
@ -46,7 +48,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.manageLists,
|
||||
icon: 'fas fa-list-ul',
|
||||
bg: 'var(--bg)',
|
||||
action: {
|
||||
icon: 'fas fa-plus',
|
||||
handler: create,
|
||||
|
|
|
|||
|
|
@ -1,46 +1,49 @@
|
|||
<template><MkStickyContainer>
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="mk-list-page">
|
||||
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
||||
<div v-if="list" class="_section">
|
||||
<div class="_content">
|
||||
<MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton>
|
||||
<MkButton inline @click="renameList()">{{ $ts.rename }}</MkButton>
|
||||
<MkButton inline @click="deleteList()">{{ $ts.delete }}</MkButton>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="mk-list-page">
|
||||
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
||||
<div v-if="list" class="_section">
|
||||
<div class="_content">
|
||||
<MkButton inline @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
||||
<MkButton inline @click="renameList()">{{ i18n.ts.rename }}</MkButton>
|
||||
<MkButton inline @click="deleteList()">{{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</transition>
|
||||
|
||||
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
||||
<div v-if="list" class="_section members _gap">
|
||||
<div class="_title">{{ $ts.members }}</div>
|
||||
<div class="_content">
|
||||
<div class="users">
|
||||
<div v-for="user in users" :key="user.id" class="user _panel">
|
||||
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
<MkUserName :user="user" class="name"/>
|
||||
<MkAcct :user="user" class="acct"/>
|
||||
</div>
|
||||
<div class="action">
|
||||
<button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button>
|
||||
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
||||
<div v-if="list" class="_section members _gap">
|
||||
<div class="_title">{{ i18n.ts.members }}</div>
|
||||
<div class="_content">
|
||||
<div class="users">
|
||||
<div v-for="user in users" :key="user.id" class="user _panel">
|
||||
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
<MkUserName :user="user" class="name"/>
|
||||
<MkAcct :user="user" class="acct"/>
|
||||
</div>
|
||||
<div class="action">
|
||||
<button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
</transition>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, watch } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { mainRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
listId: string;
|
||||
|
|
@ -120,7 +123,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => list ? {
|
||||
title: list.name,
|
||||
icon: 'fas fa-list-ul',
|
||||
bg: 'var(--bg)',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="ipledcug">
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/not-found.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.notFoundDescription }}</div>
|
||||
<div>{{ i18n.ts.notFoundDescription }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -18,6 +18,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.notFound,
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,51 +1,53 @@
|
|||
<template><MkStickyContainer>
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div class="fcuexfpr">
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="note" class="note">
|
||||
<div v-if="showNext" class="_gap">
|
||||
<XNotes class="_content" :pagination="nextPagination" :no-gap="true"/>
|
||||
</div>
|
||||
|
||||
<div class="main _gap">
|
||||
<MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="fas fa-chevron-up"></i></MkButton>
|
||||
<div class="note _gap">
|
||||
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri" class="_isolated"/>
|
||||
<XNoteDetailed :key="note.id" v-model:note="note" class="_isolated note"/>
|
||||
<MkSpacer :content-max="800">
|
||||
<div class="fcuexfpr">
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="note" class="note">
|
||||
<div v-if="showNext" class="_gap">
|
||||
<XNotes class="_content" :pagination="nextPagination" :no-gap="true"/>
|
||||
</div>
|
||||
<div v-if="clips && clips.length > 0" class="_content clips _gap">
|
||||
<div class="title">{{ $ts.clip }}</div>
|
||||
<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
|
||||
<b>{{ item.name }}</b>
|
||||
<div v-if="item.description" class="description">{{ item.description }}</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
<MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="fas fa-chevron-down"></i></MkButton>
|
||||
</div>
|
||||
|
||||
<div v-if="showPrev" class="_gap">
|
||||
<XNotes class="_content" :pagination="prevPagination" :no-gap="true"/>
|
||||
<div class="main _gap">
|
||||
<MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="fas fa-chevron-up"></i></MkButton>
|
||||
<div class="note _gap">
|
||||
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
|
||||
<XNoteDetailed :key="note.id" v-model:note="note" class="note"/>
|
||||
</div>
|
||||
<div v-if="clips && clips.length > 0" class="_content clips _gap">
|
||||
<div class="title">{{ i18n.ts.clip }}</div>
|
||||
<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
|
||||
<b>{{ item.name }}</b>
|
||||
<div v-if="item.description" class="description">{{ item.description }}</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
<MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="fas fa-chevron-down"></i></MkButton>
|
||||
</div>
|
||||
|
||||
<div v-if="showPrev" class="_gap">
|
||||
<XNotes class="_content" :pagination="prevPagination" :no-gap="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetch()"/>
|
||||
<MkLoading v-else/>
|
||||
</transition>
|
||||
</div>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
<MkError v-else-if="error" @retry="fetch()"/>
|
||||
<MkLoading v-else/>
|
||||
</transition>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, watch } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import XNote from '@/components/note.vue';
|
||||
import XNoteDetailed from '@/components/note-detailed.vue';
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import MkRemoteCaution from '@/components/remote-caution.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import XNote from '@/components/MkNote.vue';
|
||||
import XNoteDetailed from '@/components/MkNoteDetailed.vue';
|
||||
import XNotes from '@/components/MkNotes.vue';
|
||||
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
|
|
@ -132,7 +134,6 @@ definePageMetadata(computed(() => note ? {
|
|||
title: i18n.t('noteOf', { user: note.user.name }),
|
||||
text: note.text,
|
||||
},
|
||||
bg: 'var(--bg)',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div class="clupoqwt">
|
||||
<XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/>
|
||||
<div v-if="tab === 'all' || tab === 'unread'">
|
||||
<XNotifications class="notifications" :include-types="includeTypes" :unread-only="unreadOnly"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'mentions'">
|
||||
<XNotes :pagination="mentionsPagination"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'directNotes'">
|
||||
<XNotes :pagination="directNotesPagination"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
|
|
@ -12,13 +18,28 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
import XNotifications from '@/components/notifications.vue';
|
||||
import XNotifications from '@/components/MkNotifications.vue';
|
||||
import XNotes from '@/components/MkNotes.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let tab = $ref('all');
|
||||
let includeTypes = $ref<string[] | null>(null);
|
||||
let unreadOnly = $computed(() => tab === 'unread');
|
||||
|
||||
const mentionsPagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
const directNotesPagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
visibility: 'specified',
|
||||
},
|
||||
};
|
||||
|
||||
function setFilter(ev) {
|
||||
const typeItems = notificationTypes.map(t => ({
|
||||
|
|
@ -38,37 +59,37 @@ function setFilter(ev) {
|
|||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
const headerActions = $computed(() => [tab === 'all' ? {
|
||||
text: i18n.ts.filter,
|
||||
icon: 'fas fa-filter',
|
||||
highlighted: includeTypes != null,
|
||||
handler: setFilter,
|
||||
}, {
|
||||
} : undefined, tab === 'all' ? {
|
||||
text: i18n.ts.markAllAsRead,
|
||||
icon: 'fas fa-check',
|
||||
handler: () => {
|
||||
os.apiWithDialog('notifications/mark-all-as-read');
|
||||
},
|
||||
}]);
|
||||
} : undefined].filter(x => x !== undefined));
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab === 'all',
|
||||
key: 'all',
|
||||
title: i18n.ts.all,
|
||||
onClick: () => { tab = 'all'; },
|
||||
}, {
|
||||
active: tab === 'unread',
|
||||
key: 'unread',
|
||||
title: i18n.ts.unread,
|
||||
onClick: () => { tab = 'unread'; },
|
||||
}, {
|
||||
key: 'mentions',
|
||||
title: i18n.ts.mentions,
|
||||
icon: 'fas fa-at',
|
||||
}, {
|
||||
key: 'directNotes',
|
||||
title: i18n.ts.directNotes,
|
||||
icon: 'fas fa-envelope',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.notifications,
|
||||
icon: 'fas fa-bell',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.clupoqwt {
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
/* eslint-disable vue/no-mutating-props */
|
||||
import { onMounted } from 'vue';
|
||||
import XContainer from '../page-editor.container.vue';
|
||||
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
|
||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import { watch } from 'vue';
|
|||
import XContainer from '../page-editor.container.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import XNote from '@/components/note.vue';
|
||||
import XNoteDetailed from '@/components/note-detailed.vue';
|
||||
import XNote from '@/components/MkNote.vue';
|
||||
import XNoteDetailed from '@/components/MkNoteDetailed.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const props = withDefaults(defineProps<{
|
|||
let values: string = $ref(props.value.values.join('\n'));
|
||||
|
||||
watch(values, () => {
|
||||
props.value.values = values.split('\n')
|
||||
props.value.values = values.split('\n');
|
||||
}, {
|
||||
deep: true
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="jqqmcavi">
|
||||
<MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ $ts._pages.viewPage }}</MkButton>
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, defineAsyncComponent, computed, provide, watch } from 'vue';
|
||||
import { defineAsyncComponent, computed, provide, watch } from 'vue';
|
||||
import 'prismjs';
|
||||
import { highlight, languages } from 'prismjs/components/prism-core';
|
||||
import 'prismjs/components/prism-clike';
|
||||
|
|
@ -93,8 +93,7 @@ import { v4 as uuid } from 'uuid';
|
|||
import XVariable from './page-editor.script-block.vue';
|
||||
import XBlocks from './page-editor.blocks.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
|
|
@ -168,15 +167,15 @@ function save() {
|
|||
const options = getSaveOptions();
|
||||
|
||||
const onError = err => {
|
||||
if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') {
|
||||
if (err.info.param == 'name') {
|
||||
if (err.id === '3d81ceae-475f-4600-b2a8-2bc116157532') {
|
||||
if (err.info.param === 'name') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts._pages.invalidNameTitle,
|
||||
text: i18n.ts._pages.invalidNameText,
|
||||
});
|
||||
}
|
||||
} else if (err.code == 'NAME_ALREADY_EXISTS') {
|
||||
} else if (err.code === 'NAME_ALREADY_EXISTS') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._pages.nameAlreadyExists,
|
||||
|
|
@ -310,7 +309,7 @@ function getPageBlockList() {
|
|||
function getScriptBlockList(type: string = null) {
|
||||
const list = [];
|
||||
|
||||
const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number');
|
||||
const blocks = blockDefs.filter(block => type == null || block.out == null || block.out === type || typeof block.out === 'number');
|
||||
|
||||
for (const block of blocks) {
|
||||
const category = list.find(x => x.category === block.category);
|
||||
|
|
@ -345,8 +344,8 @@ function getScriptBlockList(type: string = null) {
|
|||
return list;
|
||||
}
|
||||
|
||||
function setEyeCatchingImage(e) {
|
||||
selectFile(e.currentTarget ?? e.target, null).then(file => {
|
||||
function setEyeCatchingImage(img) {
|
||||
selectFile(img.currentTarget ?? img.target, null).then(file => {
|
||||
eyeCatchingImageId = file.id;
|
||||
});
|
||||
}
|
||||
|
|
@ -411,25 +410,21 @@ init();
|
|||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab === 'settings',
|
||||
key: 'settings',
|
||||
title: i18n.ts._pages.pageSetting,
|
||||
icon: 'fas fa-cog',
|
||||
onClick: () => { tab = 'settings'; },
|
||||
}, {
|
||||
active: tab === 'contents',
|
||||
key: 'contents',
|
||||
title: i18n.ts._pages.contents,
|
||||
icon: 'fas fa-sticky-note',
|
||||
onClick: () => { tab = 'contents'; },
|
||||
}, {
|
||||
active: tab === 'variables',
|
||||
key: 'variables',
|
||||
title: i18n.ts._pages.variables,
|
||||
icon: 'fas fa-magic',
|
||||
onClick: () => { tab = 'variables'; },
|
||||
}, {
|
||||
active: tab === 'script',
|
||||
key: 'script',
|
||||
title: i18n.ts.script,
|
||||
icon: 'fas fa-code',
|
||||
onClick: () => { tab = 'script'; },
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => {
|
||||
|
|
@ -443,8 +438,7 @@ definePageMetadata(computed(() => {
|
|||
return {
|
||||
title: title,
|
||||
icon: 'fas fa-pencil-alt',
|
||||
bg: 'var(--bg)',
|
||||
};
|
||||
};
|
||||
}));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@
|
|||
</div>
|
||||
<div class="actions">
|
||||
<div class="like">
|
||||
<MkButton v-if="page.isLiked" v-tooltip="$ts._pages.unlike" class="button" primary @click="unlike()"><i class="fas fa-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="$ts._pages.like" class="button" @click="like()"><i class="far fa-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
|
||||
<MkButton v-if="page.isLiked" v-tooltip="i18n.ts._pages.unlike" class="button" primary @click="unlike()"><i class="fas fa-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" @click="like()"><i class="far fa-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
|
||||
</div>
|
||||
<div class="other">
|
||||
<button v-tooltip="$ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="fas fa-retweet fa-fw"></i></button>
|
||||
<button v-tooltip="$ts.share" v-click-anime class="_button" @click="share"><i class="fas fa-share-alt fa-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="fas fa-retweet fa-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="fas fa-share-alt fa-fw"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user">
|
||||
|
|
@ -35,21 +35,21 @@
|
|||
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
</div>
|
||||
<div class="links">
|
||||
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA>
|
||||
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
|
||||
<template v-if="$i && $i.id === page.userId">
|
||||
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ $ts._pages.editThisPage }}</MkA>
|
||||
<button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ $ts.unpin }}</button>
|
||||
<button v-else class="link _textButton" @click="pin(true)">{{ $ts.pin }}</button>
|
||||
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ i18n.ts._pages.editThisPage }}</MkA>
|
||||
<button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ i18n.ts.unpin }}</button>
|
||||
<button v-else class="link _textButton" @click="pin(true)">{{ i18n.ts.pin }}</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
|
||||
<div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
|
||||
<div><i class="far fa-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
|
||||
<div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
|
||||
</div>
|
||||
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
|
||||
<template #header><i class="fas fa-clock"></i> {{ i18n.ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
|
||||
<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_gap"/>
|
||||
</MkPagination>
|
||||
|
|
@ -65,13 +65,13 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
import XPage from '@/components/page/page.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { url } from '@/config';
|
||||
import MkFollowButton from '@/components/follow-button.vue';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkPagePreview from '@/components/page-preview.vue';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkPagePreview from '@/components/MkPagePreview.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div v-if="tab === 'featured'" class="rknalgpo">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredPagesPagination">
|
||||
|
|
@ -26,9 +26,9 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import MkPagePreview from '@/components/page-preview.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkPagePreview from '@/components/MkPagePreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { useRouter } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
|
@ -61,26 +61,22 @@ const headerActions = $computed(() => [{
|
|||
}]);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
active: tab === 'featured',
|
||||
key: 'featured',
|
||||
title: i18n.ts._pages.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
onClick: () => { tab = 'featured'; },
|
||||
}, {
|
||||
active: tab === 'my',
|
||||
key: 'my',
|
||||
title: i18n.ts._pages.my,
|
||||
icon: 'fas fa-edit',
|
||||
onClick: () => { tab = 'my'; },
|
||||
}, {
|
||||
active: tab === 'liked',
|
||||
key: 'liked',
|
||||
title: i18n.ts._pages.liked,
|
||||
icon: 'fas fa-heart',
|
||||
onClick: () => { tab = 'liked'; },
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.pages,
|
||||
icon: 'fas fa-sticky-note',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkSample from '@/components/sample.vue';
|
||||
import MkSample from '@/components/MkSample.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
|
|
@ -17,7 +17,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.preview,
|
||||
icon: 'fas fa-eye',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
||||
|
|
|
|||
96
packages/client/src/pages/registry.keys.vue
Normal file
96
packages/client/src/pages/registry.keys.vue
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="600" :margin-min="16">
|
||||
<FormSplit>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts._registry.domain }}</template>
|
||||
<template #value>{{ i18n.ts.system }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts._registry.scope }}</template>
|
||||
<template #value>{{ scope.join('/') }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
|
||||
<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton>
|
||||
|
||||
<FormSection v-if="keys">
|
||||
<template #label>{{ i18n.ts.keys }}</template>
|
||||
<div class="_formLinks">
|
||||
<FormLink v-for="key in keys" :to="`/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
path: string;
|
||||
}>();
|
||||
|
||||
const scope = $computed(() => props.path.split('/'));
|
||||
|
||||
let keys = $ref(null);
|
||||
|
||||
function fetchKeys() {
|
||||
os.api('i/registry/keys-with-type', {
|
||||
scope: scope,
|
||||
}).then(res => {
|
||||
keys = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0]));
|
||||
});
|
||||
}
|
||||
|
||||
async function createKey() {
|
||||
const { canceled, result } = await os.form(i18n.ts._registry.createKey, {
|
||||
key: {
|
||||
type: 'string',
|
||||
label: i18n.ts._registry.key,
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
multiline: true,
|
||||
label: i18n.ts.value,
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
label: i18n.ts._registry.scope,
|
||||
default: scope.join('/'),
|
||||
},
|
||||
});
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: result.scope.split('/'),
|
||||
key: result.key,
|
||||
value: JSON5.parse(result.value),
|
||||
}).then(() => {
|
||||
fetchKeys();
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => props.path, fetchKeys, { immediate: true });
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.registry,
|
||||
icon: 'fas fa-cogs',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
123
packages/client/src/pages/registry.value.vue
Normal file
123
packages/client/src/pages/registry.value.vue
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="600" :margin-min="16">
|
||||
<FormInfo warn>{{ i18n.ts.editTheseSettingsMayBreakAccount }}</FormInfo>
|
||||
|
||||
<template v-if="value">
|
||||
<FormSplit>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts._registry.domain }}</template>
|
||||
<template #value>{{ i18n.ts.system }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts._registry.scope }}</template>
|
||||
<template #value>{{ scope.join('/') }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts._registry.key }}</template>
|
||||
<template #value>{{ key }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
|
||||
<FormTextarea v-model="valueForEditor" tall class="_formBlock _monospace">
|
||||
<template #label>{{ i18n.ts.value }} (JSON)</template>
|
||||
</FormTextarea>
|
||||
|
||||
<MkButton class="_formBlock" primary @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
|
||||
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts.updatedAt }}</template>
|
||||
<template #value><MkTime :time="value.updatedAt" mode="detail"/></template>
|
||||
</MkKeyValue>
|
||||
|
||||
<MkButton danger @click="del"><i class="fas fa-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</template>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
path: string;
|
||||
}>();
|
||||
|
||||
const scope = $computed(() => props.path.split('/').slice(0, -1));
|
||||
const key = $computed(() => props.path.split('/').at(-1));
|
||||
|
||||
let value = $ref(null);
|
||||
let valueForEditor = $ref(null);
|
||||
|
||||
function fetchValue() {
|
||||
os.api('i/registry/get-detail', {
|
||||
scope,
|
||||
key,
|
||||
}).then(res => {
|
||||
value = res;
|
||||
valueForEditor = JSON5.stringify(res.value, null, '\t');
|
||||
});
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
JSON5.parse(valueForEditor);
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.invalidValue,
|
||||
});
|
||||
return;
|
||||
}
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.saveConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope,
|
||||
key,
|
||||
value: JSON5.parse(valueForEditor),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function del() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/remove', {
|
||||
scope,
|
||||
key,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => props.path, fetchValue, { immediate: true });
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.registry,
|
||||
icon: 'fas fa-cogs',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
74
packages/client/src/pages/registry.vue
Normal file
74
packages/client/src/pages/registry.vue
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="600" :margin-min="16">
|
||||
<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton>
|
||||
|
||||
<FormSection v-if="scopes">
|
||||
<template #label>{{ i18n.ts.system }}</template>
|
||||
<div class="_formLinks">
|
||||
<FormLink v-for="scope in scopes" :to="`/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
let scopes = $ref(null);
|
||||
|
||||
function fetchScopes() {
|
||||
os.api('i/registry/scopes').then(res => {
|
||||
scopes = res.slice().sort((a, b) => a.join('/').localeCompare(b.join('/')));
|
||||
});
|
||||
}
|
||||
|
||||
async function createKey() {
|
||||
const { canceled, result } = await os.form(i18n.ts._registry.createKey, {
|
||||
key: {
|
||||
type: 'string',
|
||||
label: i18n.ts._registry.key,
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
multiline: true,
|
||||
label: i18n.ts.value,
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
label: i18n.ts._registry.scope,
|
||||
},
|
||||
});
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: result.scope.split('/'),
|
||||
key: result.key,
|
||||
value: JSON5.parse(result.value),
|
||||
}).then(() => {
|
||||
fetchScopes();
|
||||
});
|
||||
}
|
||||
|
||||
fetchScopes();
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.registry,
|
||||
icon: 'fas fa-cogs',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted } from 'vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { mainRouter } from '@/router';
|
||||
|
|
@ -39,7 +39,7 @@ async function save() {
|
|||
|
||||
onMounted(() => {
|
||||
if (props.token == null) {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {}, 'closed');
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {}, 'closed');
|
||||
mainRouter.push('/');
|
||||
}
|
||||
});
|
||||
|
|
@ -51,7 +51,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.resetPassword,
|
||||
icon: 'fas fa-lock',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ import 'prismjs/themes/prism-okaidia.css';
|
|||
import { PrismEditor } from 'vue-prism-editor';
|
||||
import 'vue-prism-editor/dist/prismeditor.min.css';
|
||||
import { AiScript, parse, utils } from '@syuilo/aiscript';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { createAiScriptEnv } from '@/scripts/aiscript/api';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import XNotes from '@/components/MkNotes.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
|
|
@ -34,6 +34,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata(computed(() => ({
|
||||
title: i18n.t('searchWith', { q: props.query }),
|
||||
icon: 'fas fa-search',
|
||||
bg: 'var(--bg)',
|
||||
})));
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
<li>{{ i18n.ts._2fa.step2 }}<br><img :src="twoFactorData.qr"><p>{{ $ts._2fa.step2Url }}<br>{{ twoFactorData.url }}</p></li>
|
||||
<li>
|
||||
{{ i18n.ts._2fa.step3 }}<br>
|
||||
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ i18n.ts.token }}</template></MkInput>
|
||||
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" :spellcheck="false"><template #label>{{ i18n.ts.token }}</template></MkInput>
|
||||
<MkButton primary @click="submit">{{ i18n.ts.done }}</MkButton>
|
||||
</li>
|
||||
</ol>
|
||||
|
|
@ -68,8 +68,8 @@
|
|||
import { ref } from 'vue';
|
||||
import { hostname } from '@/config';
|
||||
import { byteify, hexify, stringify } from '@/scripts/2fa';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import * as os from '@/os';
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
import bytes from '@/filters/bytes';
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
|
@ -75,7 +75,7 @@ function removeAccount(account) {
|
|||
}
|
||||
|
||||
function addExistingAccount() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
|
||||
done: res => {
|
||||
addAccounts(res.id, res.i);
|
||||
os.success();
|
||||
|
|
@ -84,7 +84,7 @@ function addExistingAccount() {
|
|||
}
|
||||
|
||||
function createAccount() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
|
||||
done: res => {
|
||||
addAccounts(res.id, res.i);
|
||||
switchAccountWithToken(res.i);
|
||||
|
|
@ -109,7 +109,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.accounts,
|
||||
icon: 'fas fa-users',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
|
@ -17,7 +17,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
|||
const isDesktop = ref(window.innerWidth >= 1100);
|
||||
|
||||
function generateToken() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/token-generate-window.vue')), {}, {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {}, {
|
||||
done: async result => {
|
||||
const { name, permissions } = result;
|
||||
const { token } = await os.api('miauth/gen-token', {
|
||||
|
|
@ -42,6 +42,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: 'API',
|
||||
icon: 'fas fa-key',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import FormPagination from '@/components/ui/pagination.vue';
|
||||
import FormPagination from '@/components/MkPagination.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
|
@ -67,7 +67,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.installedApps,
|
||||
icon: 'fas fa-plug',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import { i18n } from '@/i18n';
|
||||
|
|
@ -42,6 +42,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.customCss,
|
||||
icon: 'fas fa-code',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormGroup>
|
||||
<template #label>{{ i18n.ts.defaultNavigationBehaviour }}</template>
|
||||
<FormSwitch v-model="navWindow">{{ i18n.ts.openInWindow }}</FormSwitch>
|
||||
</FormGroup>
|
||||
<FormSwitch v-model="navWindow">{{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ i18n.ts._deck.alwaysShowMainColumn }}</FormSwitch>
|
||||
|
||||
|
|
@ -12,20 +9,6 @@
|
|||
<option value="left">{{ i18n.ts.left }}</option>
|
||||
<option value="center">{{ i18n.ts.center }}</option>
|
||||
</FormRadios>
|
||||
|
||||
<FormRadios v-model="columnHeaderHeight" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._deck.columnHeaderHeight }}</template>
|
||||
<option :value="42">{{ i18n.ts.narrow }}</option>
|
||||
<option :value="45">{{ i18n.ts.medium }}</option>
|
||||
<option :value="48">{{ i18n.ts.wide }}</option>
|
||||
</FormRadios>
|
||||
|
||||
<FormInput v-model="columnMargin" type="number" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._deck.columnMargin }}</template>
|
||||
<template #suffix>px</template>
|
||||
</FormInput>
|
||||
|
||||
<FormLink class="_formBlock" @click="setProfile">{{ i18n.ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -35,7 +18,6 @@ import FormSwitch from '@/components/form/switch.vue';
|
|||
import FormLink from '@/components/form/link.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import { deckStore } from '@/ui/deck/deck-store';
|
||||
import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
|
|
@ -45,30 +27,6 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
|||
const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
|
||||
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
|
||||
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
|
||||
const columnMargin = computed(deckStore.makeGetterSetter('columnMargin'));
|
||||
const columnHeaderHeight = computed(deckStore.makeGetterSetter('columnHeaderHeight'));
|
||||
const profile = computed(deckStore.makeGetterSetter('profile'));
|
||||
|
||||
watch(navWindow, async () => {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: i18n.ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
});
|
||||
|
||||
async function setProfile() {
|
||||
const { canceled, result: name } = await os.inputText({
|
||||
title: i18n.ts._deck.profile,
|
||||
allowEmpty: false,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
profile.value = name;
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
|
|
@ -77,6 +35,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.deck,
|
||||
icon: 'fas fa-columns',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { signout } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
|
@ -48,6 +48,5 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts._accountDelete.accountDelete,
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
bg: 'var(--bg)',
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue