Merge branch 'develop' into pizzax-indexeddb

This commit is contained in:
tamaina 2022-11-17 23:35:55 +09:00
commit 764da890b6
1491 changed files with 65628 additions and 43153 deletions

View file

@ -0,0 +1,7 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import { } from 'vue';
</script>

View file

@ -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';

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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;
}
}
}

View file

@ -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 {
// mousedownonClick
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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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();
});

View file

@ -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>

View file

@ -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>

View file

@ -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();
});

View file

@ -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>

View file

@ -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>

View file

@ -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');
}
});

View file

@ -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>

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -53,6 +53,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.integration,
icon: 'fas fa-share-alt',
bg: 'var(--bg)',
});
</script>

View file

@ -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';

View file

@ -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>

View file

@ -40,6 +40,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.other,
icon: 'fas fa-cogs',
bg: 'var(--bg)',
});
</script>

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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;
}
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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';

View file

@ -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({

View file

@ -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';

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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, () => {

View file

@ -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>

View file

@ -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>

View file

@ -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 {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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;
}
}
}

View file

@ -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';

View file

@ -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%;

View file

@ -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,

View file

@ -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>

View file

@ -38,7 +38,6 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.manageAntennas,
icon: 'fas fa-satellite',
bg: 'var(--bg)',
});
</script>

View file

@ -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';

View file

@ -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>

View file

@ -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,

View file

@ -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,

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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<{

View file

@ -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<{

View file

@ -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
});

View file

@ -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>

View file

@ -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';

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View 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>

View file

@ -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>

View file

@ -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';

View file

@ -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>

View file

@ -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';

View file

@ -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';

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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