forked from mirrors/misskey
Merge branch 'develop' into mahjong
This commit is contained in:
@@ -67,10 +67,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.error,
|
||||
icon: 'ti ti-alert-triangle',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="misskey">Misskey</div>
|
||||
<div class="version">v{{ version }}</div>
|
||||
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }">
|
||||
<MkCustomEmoji v-if="emoji.emoji[0] === ':'" class="emoji" :name="emoji.emoji" :normal="true" :noStyle="true"/>
|
||||
<MkCustomEmoji v-if="emoji.emoji[0] === ':'" class="emoji" :name="emoji.emoji" :normal="true" :noStyle="true" :fallbackToImage="true"/>
|
||||
<MkEmoji v-else class="emoji" :emoji="emoji.emoji" :normal="true" :noStyle="true"/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="_gaps_s">
|
||||
<FormLink to="https://github.com/misskey-dev/misskey" external>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
{{ i18n.ts._aboutMisskey.source }}
|
||||
{{ i18n.ts._aboutMisskey.source }} ({{ i18n.ts._aboutMisskey.original }})
|
||||
<template #suffix>GitHub</template>
|
||||
</FormLink>
|
||||
<FormLink to="https://crowdin.com/project/misskey" external>
|
||||
@@ -46,6 +46,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection v-if="instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey'">
|
||||
<div class="_gaps_s">
|
||||
<MkInfo>
|
||||
{{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name }) }}
|
||||
</MkInfo>
|
||||
<FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
{{ i18n.ts._aboutMisskey.source }}
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.providesTarball" :to="`/tarball/misskey-${version}.tar.gz`" external>
|
||||
<template #icon><i class="ti ti-download"></i></template>
|
||||
{{ i18n.ts._aboutMisskey.source }}
|
||||
<template #suffix>Tarball</template>
|
||||
</FormLink>
|
||||
<MkInfo v-if="!instance.repositoryUrl && !instance.providesTarball" warn>
|
||||
{{ i18n.ts.sourceCodeIsNotYetProvided }}
|
||||
</MkInfo>
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._aboutMisskey.projectMembers }}</template>
|
||||
<div :class="$style.contributors">
|
||||
@@ -83,13 +102,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>Special thanks</template>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(130px, 1fr));grid-gap:24px;align-items:center;">
|
||||
<div>
|
||||
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img style="width: 100%;" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
|
||||
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
|
||||
</div>
|
||||
<div>
|
||||
<a style="display: inline-block;" class="xserver" title="XServer" href="https://www.xserver.ne.jp/" target="_blank"><img style="width: 100%;" src="https://misskey-hub.net/sponsors/xserver.png" alt="XServer"></a>
|
||||
<a style="display: inline-block;" class="xserver" title="XServer" href="https://www.xserver.ne.jp/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/xserver.png" alt="XServer"></a>
|
||||
</div>
|
||||
<div>
|
||||
<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img style="width: 100%;" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a>
|
||||
<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a>
|
||||
</div>
|
||||
<div>
|
||||
<a style="display: inline-block;" class="pepabo" title="GMO Pepabo" href="https://pepabo.com/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/gmo_pepabo.svg" alt="GMO Pepabo"></a>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
@@ -118,9 +140,10 @@ import { version } from '@/config.js';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { physics } from '@/scripts/physics.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
@@ -202,6 +225,24 @@ const patronsWithIcon = [{
|
||||
}, {
|
||||
name: '有栖かずみ',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/9240e8e0ba294a8884143e99ac7ed6a0.jpg',
|
||||
}, {
|
||||
name: 'イカロ(コアラ)',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/50b9bdc03735412c80807dbdf32cecb6.jpg',
|
||||
}, {
|
||||
name: 'ハチノス3号',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/030347a6f8ce4e82bc5184b5aad09a18.jpg',
|
||||
}, {
|
||||
name: 'Takeno',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/6fba81536aea48fe94a30909c502dfa1.jpg',
|
||||
}, {
|
||||
name: 'くびすじ',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/aa5789850b2149aeb5b89ebe2e9083db.jpg',
|
||||
}, {
|
||||
name: '古道京紗@ぷらいべったー',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/18346d0519704963a4beabe6abc170af.jpg',
|
||||
}, {
|
||||
name: '越貝鯛丸',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/86c7374de37849b882d8ebbc833dc968.jpg',
|
||||
}];
|
||||
|
||||
const patrons = [
|
||||
@@ -304,6 +345,8 @@ const patrons = [
|
||||
'てば',
|
||||
'たっくん',
|
||||
'SHO SEKIGUCHI',
|
||||
'塩キャベツ',
|
||||
'はとぽぷさん',
|
||||
];
|
||||
|
||||
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
@@ -363,10 +406,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.aboutMisskey,
|
||||
icon: null,
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -31,7 +31,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkKeyValue>
|
||||
<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
|
||||
</div>
|
||||
<FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink>
|
||||
<FormLink to="/about-misskey">
|
||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||
{{ i18n.ts.aboutMisskey }}
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
{{ i18n.ts.sourceCode }}
|
||||
</FormLink>
|
||||
<MkInfo v-else warn>
|
||||
{{ i18n.ts.sourceCodeIsNotYetProvided }}
|
||||
</MkInfo>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
@@ -47,17 +57,33 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #value>{{ instance.maintainerEmail }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink>
|
||||
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
|
||||
<template #icon><i class="ti ti-user-shield"></i></template>
|
||||
{{ i18n.ts.impressum }}
|
||||
</FormLink>
|
||||
<div class="_gaps_s">
|
||||
<MkFolder v-if="instance.serverRules.length > 0">
|
||||
<template #label>{{ i18n.ts.serverRules }}</template>
|
||||
<template #label>
|
||||
<i class="ti ti-checkup-list"></i>
|
||||
{{ i18n.ts.serverRules }}
|
||||
</template>
|
||||
|
||||
<ol class="_gaps_s" :class="$style.rules">
|
||||
<li v-for="item, index in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
|
||||
<li v-for="(item, index) in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
|
||||
</ol>
|
||||
</MkFolder>
|
||||
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink>
|
||||
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>{{ i18n.ts.privacyPolicy }}</FormLink>
|
||||
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
|
||||
<template #icon><i class="ti ti-license"></i></template>
|
||||
{{ i18n.ts.termsOfService }}
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
|
||||
<template #icon><i class="ti ti-shield-lock"></i></template>
|
||||
{{ i18n.ts.privacyPolicy }}
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
|
||||
<template #icon><i class="ti ti-message"></i></template>
|
||||
{{ i18n.ts.feedback }}
|
||||
</FormLink>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
@@ -86,7 +112,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
|
||||
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
|
||||
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
|
||||
<FormLink :to="`/tarball/misskey-${version}.tar.gz`" external>source code</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
@@ -116,6 +141,7 @@ import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkInstanceStats from '@/components/MkInstanceStats.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
@@ -164,10 +190,10 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-chart-line',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.instanceInfo,
|
||||
icon: 'ti ti-info-circle',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -48,10 +48,10 @@ onDeactivated(() => {
|
||||
}
|
||||
});
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.achievements,
|
||||
icon: 'ti ti-medal',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -140,10 +140,10 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-code',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: file.value ? i18n.ts.file + ': ' + file.value.name : i18n.ts.file,
|
||||
definePageMetadata(() => ({
|
||||
title: file.value ? `${i18n.ts.file}: ${file.value.name}` : i18n.ts.file,
|
||||
icon: 'ti ti-file',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -416,7 +416,7 @@ async function assignRole() {
|
||||
if (canceled) return;
|
||||
|
||||
const { canceled: canceled2, result: period } = await os.select({
|
||||
title: i18n.ts.period,
|
||||
title: i18n.ts.period + ': ' + roles.find(r => r.id === roleId)!.name,
|
||||
items: [{
|
||||
value: 'indefinitely', text: i18n.ts.indefinitely,
|
||||
}, {
|
||||
@@ -518,10 +518,10 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-code',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: user.value ? acct(user.value) : i18n.ts.userInfo,
|
||||
icon: 'ti ti-user-exclamation',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -9,6 +9,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSelect v-model="type" :class="$style.typeSelect">
|
||||
<option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option>
|
||||
<option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option>
|
||||
<option value="isSuspended">{{ i18n.ts._role._condition.isSuspended }}</option>
|
||||
<option value="isLocked">{{ i18n.ts._role._condition.isLocked }}</option>
|
||||
<option value="isBot">{{ i18n.ts._role._condition.isBot }}</option>
|
||||
<option value="isCat">{{ i18n.ts._role._condition.isCat }}</option>
|
||||
<option value="isExplorable">{{ i18n.ts._role._condition.isExplorable }}</option>
|
||||
<option value="roleAssignedTo">{{ i18n.ts._role._condition.roleAssignedTo }}</option>
|
||||
<option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option>
|
||||
<option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option>
|
||||
<option value="followersLessThanOrEq">{{ i18n.ts._role._condition.followersLessThanOrEq }}</option>
|
||||
@@ -51,6 +57,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
|
||||
</MkInput>
|
||||
|
||||
<MkSelect v-else-if="type === 'roleAssignedTo'" v-model="v.roleId">
|
||||
<option v-for="role in roles.filter(r => r.target === 'manual')" :key="role.id" :value="role.id">{{ role.name }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -62,6 +72,7 @@ import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { rolesCache } from '@/cache.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
@@ -77,6 +88,8 @@ const props = defineProps<{
|
||||
|
||||
const v = ref(deepClone(props.modelValue));
|
||||
|
||||
const roles = await rolesCache.fetch();
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
if (JSON.stringify(props.modelValue) === JSON.stringify(v.value)) return;
|
||||
v.value = deepClone(props.modelValue);
|
||||
@@ -92,6 +105,7 @@ const type = computed({
|
||||
if (t === 'and') v.value.values = [];
|
||||
if (t === 'or') v.value.values = [];
|
||||
if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' };
|
||||
if (t === 'roleAssignedTo') v.value.roleId = '';
|
||||
if (t === 'createdLessThan') v.value.sec = 86400;
|
||||
if (t === 'createdMoreThan') v.value.sec = 86400;
|
||||
if (t === 'followersLessThanOrEq') v.value.value = 10;
|
||||
|
||||
@@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div ref="el" class="fdidabkc" :style="{ background: bg }" @click="onClick">
|
||||
<template v-if="metadata">
|
||||
<template v-if="pageMetadata">
|
||||
<div class="titleContainer" @click="showTabsPopup">
|
||||
<i v-if="metadata.icon" class="icon" :class="metadata.icon"></i>
|
||||
<i v-if="pageMetadata.icon" class="icon" :class="pageMetadata.icon"></i>
|
||||
|
||||
<div class="title">
|
||||
<div class="title">{{ metadata.title }}</div>
|
||||
<div class="title">{{ pageMetadata.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
@@ -39,7 +39,7 @@ import { popupMenu } from '@/os.js';
|
||||
import { scrollToTop } from '@/scripts/scroll.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { injectPageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
type Tab = {
|
||||
key?: string | null;
|
||||
@@ -65,7 +65,7 @@ const emit = defineEmits<{
|
||||
(ev: 'update:tab', key: string);
|
||||
}>();
|
||||
|
||||
const metadata = injectPageMetadata();
|
||||
const pageMetadata = injectReactiveMetadata();
|
||||
|
||||
const el = shallowRef<HTMLElement>(null);
|
||||
const tabRefs = {};
|
||||
@@ -118,7 +118,7 @@ function onTabClick(tab: Tab, ev: MouseEvent): void {
|
||||
}
|
||||
|
||||
const calcBg = () => {
|
||||
const rawBg = metadata?.bg ?? 'var(--bg)';
|
||||
const rawBg = pageMetadata.value?.bg ?? 'var(--bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
tinyBg.setAlpha(0.85);
|
||||
bg.value = tinyBg.toRgbString();
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="490"
|
||||
:withOkButton="false"
|
||||
:okButtonDisabled="false"
|
||||
@close="onCancelClicked"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
{{ mode === 'create' ? i18n.ts._abuseReport._notificationRecipient.createRecipient : i18n.ts._abuseReport._notificationRecipient.modifyRecipient }}
|
||||
</template>
|
||||
<div v-if="loading === 0">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div :class="$style.root" class="_gaps_m">
|
||||
<MkInput v-model="title">
|
||||
<template #label>{{ i18n.ts.title }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="method">
|
||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template>
|
||||
<option value="email">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.mail }}</option>
|
||||
<option value="webhook">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.webhook }}</option>
|
||||
<template #caption>
|
||||
{{ methodCaption }}
|
||||
</template>
|
||||
</MkSelect>
|
||||
<div>
|
||||
<MkSelect v-if="method === 'email'" v-model="userId">
|
||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedUser }}</template>
|
||||
<option v-for="user in moderators" :key="user.id" :value="user.id">
|
||||
{{ user.name ? `${user.name}(${user.username})` : user.username }}
|
||||
</option>
|
||||
</MkSelect>
|
||||
<div v-else-if="method === 'webhook'" :class="$style.systemWebhook">
|
||||
<MkSelect v-model="systemWebhookId" style="flex: 1">
|
||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedWebhook }}</template>
|
||||
<option v-for="webhook in systemWebhooks" :key="webhook.id ?? undefined" :value="webhook.id">
|
||||
{{ webhook.name }}
|
||||
</option>
|
||||
</MkSelect>
|
||||
<MkButton rounded @click="onEditSystemWebhookClicked">
|
||||
<span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"/>
|
||||
<span v-else class="ti ti-settings" style="line-height: normal"/>
|
||||
</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkDivider/>
|
||||
|
||||
<MkSwitch v-model="isActive">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
||||
<div :class="$style.footer" class="_buttonsCenter">
|
||||
<MkButton primary :disabled="disableSubmitButton" @click="onSubmitClicked"><i class="ti ti-check"></i> {{ i18n.ts.ok }}</MkButton>
|
||||
<MkButton @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, toRefs } from 'vue';
|
||||
import { entities } from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { MkSystemWebhookResult, showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkDivider from '@/components/MkDivider.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
type NotificationRecipientMethod = 'email' | 'webhook';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'submitted'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'create' | 'edit';
|
||||
id?: string;
|
||||
}>();
|
||||
|
||||
const { mode, id } = toRefs(props);
|
||||
|
||||
const loading = ref<number>(0);
|
||||
|
||||
const title = ref<string>('');
|
||||
const method = ref<NotificationRecipientMethod>('email');
|
||||
const userId = ref<string | null>(null);
|
||||
const systemWebhookId = ref<string | null>(null);
|
||||
const isActive = ref<boolean>(true);
|
||||
|
||||
const moderators = ref<entities.User[]>([]);
|
||||
const systemWebhooks = ref<(entities.SystemWebhook | { id: null, name: string })[]>([]);
|
||||
|
||||
const methodCaption = computed(() => {
|
||||
switch (method.value) {
|
||||
case 'email': {
|
||||
return i18n.ts._abuseReport._notificationRecipient._recipientType._captions.mail;
|
||||
}
|
||||
case 'webhook': {
|
||||
return i18n.ts._abuseReport._notificationRecipient._recipientType._captions.webhook;
|
||||
}
|
||||
default: {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const disableSubmitButton = computed(() => {
|
||||
if (!title.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (method.value) {
|
||||
case 'email': {
|
||||
return userId.value === null;
|
||||
}
|
||||
case 'webhook': {
|
||||
return systemWebhookId.value === null;
|
||||
}
|
||||
default: {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function onSubmitClicked() {
|
||||
await loadingScope(async () => {
|
||||
const _userId = (method.value === 'email') ? userId.value : null;
|
||||
const _systemWebhookId = (method.value === 'webhook') ? systemWebhookId.value : null;
|
||||
const params = {
|
||||
isActive: isActive.value,
|
||||
name: title.value,
|
||||
method: method.value,
|
||||
userId: _userId ?? undefined,
|
||||
systemWebhookId: _systemWebhookId ?? undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
switch (mode.value) {
|
||||
case 'create': {
|
||||
await misskeyApi('admin/abuse-report/notification-recipient/create', params);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await misskeyApi('admin/abuse-report/notification-recipient/update', { id: id.value!, ...params });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit('submitted');
|
||||
// eslint-disable-next-line
|
||||
} catch (ex: any) {
|
||||
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
|
||||
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
|
||||
emit('closed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onCancelClicked() {
|
||||
emit('closed');
|
||||
}
|
||||
|
||||
async function onEditSystemWebhookClicked() {
|
||||
let result: MkSystemWebhookResult | null;
|
||||
if (systemWebhookId.value === null) {
|
||||
result = await showSystemWebhookEditorDialog({
|
||||
mode: 'create',
|
||||
});
|
||||
} else {
|
||||
result = await showSystemWebhookEditorDialog({
|
||||
mode: 'edit',
|
||||
id: systemWebhookId.value,
|
||||
});
|
||||
}
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchSystemWebhooks();
|
||||
systemWebhookId.value = result.id ?? null;
|
||||
}
|
||||
|
||||
async function fetchSystemWebhooks() {
|
||||
await loadingScope(async () => {
|
||||
systemWebhooks.value = [
|
||||
{ id: null, name: i18n.ts.createNew },
|
||||
...await misskeyApi('admin/system-webhook/list', { }),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchModerators() {
|
||||
await loadingScope(async () => {
|
||||
const users = Array.of<entities.User>();
|
||||
for (; ;) {
|
||||
const res = await misskeyApi('admin/show-users', {
|
||||
limit: 100,
|
||||
state: 'adminOrModerator',
|
||||
origin: 'local',
|
||||
offset: users.length,
|
||||
});
|
||||
|
||||
if (res.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
users.push(...res);
|
||||
}
|
||||
|
||||
moderators.value = users;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
|
||||
loading.value++;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
loading.value--;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadingScope(async () => {
|
||||
await fetchModerators();
|
||||
await fetchSystemWebhooks();
|
||||
|
||||
if (mode.value === 'edit') {
|
||||
if (!id.value) {
|
||||
throw new Error('id is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await misskeyApi('admin/abuse-report/notification-recipient/show', { id: id.value });
|
||||
|
||||
title.value = res.name;
|
||||
method.value = res.method;
|
||||
userId.value = res.userId ?? null;
|
||||
systemWebhookId.value = res.systemWebhookId ?? null;
|
||||
isActive.value = res.isActive;
|
||||
// eslint-disable-next-line
|
||||
} catch (ex: any) {
|
||||
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
|
||||
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
|
||||
emit('closed');
|
||||
}
|
||||
} else {
|
||||
userId.value = moderators.value[0]?.id ?? null;
|
||||
systemWebhookId.value = systemWebhooks.value[0]?.id ?? null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.systemWebhook {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
min-width: 2.5em;
|
||||
min-height: 2.5em;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root" class="_panel _gaps_s">
|
||||
<div :class="$style.rightDivider" style="width: 80px;"><span :class="`ti ${methodIcon}`"/> {{ methodName }}</div>
|
||||
<div :class="$style.rightDivider" style="flex: 0.5">{{ entity.name }}</div>
|
||||
<div :class="$style.rightDivider" style="flex: 1">
|
||||
<div v-if="method === 'email' && user">
|
||||
{{
|
||||
`${i18n.ts._abuseReport._notificationRecipient.notifiedUser}: ` + ((user.name) ? `${user.name}(${user.username})` : user.username)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="method === 'webhook' && systemWebhook">
|
||||
{{ `${i18n.ts._abuseReport._notificationRecipient.notifiedWebhook}: ` + systemWebhook.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.recipientButtons" style="margin-left: auto">
|
||||
<button :class="$style.recipientButton" @click="onEditButtonClicked()">
|
||||
<span class="ti ti-settings"/>
|
||||
</button>
|
||||
<button :class="$style.recipientButton" @click="onDeleteButtonClicked()">
|
||||
<span class="ti ti-trash"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { entities } from 'misskey-js';
|
||||
import { computed, toRefs } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'edit', id: entities.AbuseReportNotificationRecipient['id']): void;
|
||||
(ev: 'delete', id: entities.AbuseReportNotificationRecipient['id']): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
entity: entities.AbuseReportNotificationRecipient;
|
||||
}>();
|
||||
|
||||
const { entity } = toRefs(props);
|
||||
|
||||
const method = computed(() => entity.value.method);
|
||||
const user = computed(() => entity.value.user);
|
||||
const systemWebhook = computed(() => entity.value.systemWebhook);
|
||||
const methodIcon = computed(() => {
|
||||
switch (entity.value.method) {
|
||||
case 'email':
|
||||
return 'ti-mail';
|
||||
case 'webhook':
|
||||
return 'ti-webhook';
|
||||
default:
|
||||
return 'ti-help';
|
||||
}
|
||||
});
|
||||
const methodName = computed(() => {
|
||||
switch (entity.value.method) {
|
||||
case 'email':
|
||||
return i18n.ts._abuseReport._notificationRecipient._recipientType.mail;
|
||||
case 'webhook':
|
||||
return i18n.ts._abuseReport._notificationRecipient._recipientType.webhook;
|
||||
default:
|
||||
return '不明';
|
||||
}
|
||||
});
|
||||
|
||||
function onEditButtonClicked() {
|
||||
emit('edit', entity.value.id);
|
||||
}
|
||||
|
||||
function onDeleteButtonClicked() {
|
||||
emit('delete', entity.value.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.rightDivider {
|
||||
border-right: 0.5px solid var(--divider);
|
||||
}
|
||||
|
||||
.recipientButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: -4;
|
||||
}
|
||||
|
||||
.recipientButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
box-sizing: border-box;
|
||||
margin-top: -2px;
|
||||
margin-bottom: -2px;
|
||||
padding: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--buttonBg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<XHeader :actions="headerActions" :tabs="headerTabs"/>
|
||||
</template>
|
||||
|
||||
<MkSpacer :contentMax="900">
|
||||
<div :class="$style.root" class="_gaps_m">
|
||||
<div :class="$style.addButton">
|
||||
<MkButton primary @click="onAddButtonClicked">
|
||||
<span class="ti ti-plus"/> {{ i18n.ts._abuseReport._notificationRecipient.createRecipient }}
|
||||
</MkButton>
|
||||
</div>
|
||||
<div :class="$style.subMenus" class="_gaps_s">
|
||||
<MkSelect v-model="filterMethod" style="flex: 1">
|
||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template>
|
||||
<option :value="null">-</option>
|
||||
<option :value="'email'">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.mail }}</option>
|
||||
<option :value="'webhook'">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.webhook }}</option>
|
||||
</MkSelect>
|
||||
<MkInput v-model="filterText" type="search" style="flex: 1">
|
||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.keywords }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
|
||||
<MkDivider/>
|
||||
|
||||
<div :class="$style.recipients" class="_gaps_s">
|
||||
<XRecipient
|
||||
v-for="r in filteredRecipients"
|
||||
:key="r.id"
|
||||
:entity="r"
|
||||
@edit="onEditButtonClicked"
|
||||
@delete="onDeleteButtonClicked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { entities } from 'misskey-js';
|
||||
import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
|
||||
import XRecipient from './notification-recipient.item.vue';
|
||||
import XHeader from '@/pages/admin/_header_.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import MkDivider from '@/components/MkDivider.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const recipients = ref<entities.AbuseReportNotificationRecipient[]>([]);
|
||||
|
||||
const filterMethod = ref<string | null>(null);
|
||||
const filterText = ref<string>('');
|
||||
|
||||
const filteredRecipients = computed(() => {
|
||||
const method = filterMethod.value;
|
||||
const text = filterText.value.trim().length === 0 ? null : filterText.value;
|
||||
|
||||
return recipients.value.filter(it => {
|
||||
if (method ?? text) {
|
||||
if (text) {
|
||||
const keywords = [it.name, it.systemWebhook?.name, it.user?.name, it.user?.username];
|
||||
if (keywords.filter(k => k?.includes(text)).length !== 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (method) {
|
||||
return it.method.includes(method);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
const headerActions = computed(() => []);
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
async function onAddButtonClicked() {
|
||||
await showEditor('create');
|
||||
}
|
||||
|
||||
async function onEditButtonClicked(id: string) {
|
||||
await showEditor('edit', id);
|
||||
}
|
||||
|
||||
async function onDeleteButtonClicked(id: string) {
|
||||
const res = await os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts._abuseReport._notificationRecipient.deleteConfirm,
|
||||
});
|
||||
if (!res.canceled) {
|
||||
await misskeyApi('admin/abuse-report/notification-recipient/delete', { id: id });
|
||||
await fetchRecipients();
|
||||
}
|
||||
}
|
||||
|
||||
async function showEditor(mode: 'create' | 'edit', id?: string) {
|
||||
const { dispose, needLoad } = await new Promise<{ dispose: () => void, needLoad: boolean }>(async resolve => {
|
||||
const res = await os.popup(
|
||||
defineAsyncComponent(() => import('./notification-recipient.editor.vue')),
|
||||
{
|
||||
mode,
|
||||
id,
|
||||
},
|
||||
{
|
||||
submitted: async () => {
|
||||
resolve({ dispose: res.dispose, needLoad: true });
|
||||
},
|
||||
closed: () => {
|
||||
resolve({ dispose: res.dispose, needLoad: false });
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
dispose();
|
||||
|
||||
if (needLoad) {
|
||||
await fetchRecipients();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRecipients() {
|
||||
const result = await misskeyApi('admin/abuse-report/notification-recipient/list', {
|
||||
method: ['email', 'webhook'],
|
||||
});
|
||||
|
||||
recipients.value = result.sort((a, b) => (a.method + a.id).localeCompare(b.method + b.id));
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchRecipients();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.subMenus {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.recipients {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
</style>
|
||||
@@ -7,30 +7,33 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div>
|
||||
<div class="reports">
|
||||
<div class="">
|
||||
<div class="inputs" style="display: flex;">
|
||||
<MkSelect v-model="state" style="margin: 0; flex: 1;">
|
||||
<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>{{ 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>{{ 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="$style.root" class="_gaps">
|
||||
<div :class="$style.subMenus" class="_gaps">
|
||||
<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ "通知設定" }}</MkButton>
|
||||
</div>
|
||||
|
||||
<div :class="$style.inputs" class="_gaps">
|
||||
<MkSelect v-model="state" style="margin: 0; flex: 1;">
|
||||
<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>{{ 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>{{ 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>{{ i18n.ts.username }}</span>
|
||||
@@ -41,11 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
-->
|
||||
|
||||
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
|
||||
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
|
||||
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
@@ -60,6 +61,7 @@ import MkPagination from '@/components/MkPagination.vue';
|
||||
import XAbuseReport from '@/components/MkAbuseReport.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const reports = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
@@ -80,15 +82,38 @@ const pagination = {
|
||||
};
|
||||
|
||||
function resolved(reportId) {
|
||||
reports.value.removeItem(reportId);
|
||||
reports.value?.removeItem(reportId);
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.abuseReports,
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.subMenus {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -255,10 +255,10 @@ const headerActions = computed(() => [{
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.ads,
|
||||
icon: 'ti ti-ad',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -157,8 +157,8 @@ const headerActions = computed(() => [{
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -142,7 +142,7 @@ function save() {
|
||||
turnstileSiteKey: turnstileSiteKey.value,
|
||||
turnstileSecretKey: turnstileSecretKey.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -76,6 +76,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkInput v-model="repositoryUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.repositoryUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="feedbackUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.feedbackUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkTextarea v-model="manifestJsonOverride">
|
||||
<template #label>{{ i18n.ts._serverSettings.manifestJsonOverride }}</template>
|
||||
</MkTextarea>
|
||||
@@ -120,6 +130,8 @@ const defaultDarkTheme = ref<string | null>(null);
|
||||
const serverErrorImageUrl = ref<string | null>(null);
|
||||
const infoImageUrl = ref<string | null>(null);
|
||||
const notFoundImageUrl = ref<string | null>(null);
|
||||
const repositoryUrl = ref<string | null>(null);
|
||||
const feedbackUrl = ref<string | null>(null);
|
||||
const manifestJsonOverride = ref<string>('{}');
|
||||
|
||||
async function init() {
|
||||
@@ -135,6 +147,8 @@ async function init() {
|
||||
serverErrorImageUrl.value = meta.serverErrorImageUrl;
|
||||
infoImageUrl.value = meta.infoImageUrl;
|
||||
notFoundImageUrl.value = meta.notFoundImageUrl;
|
||||
repositoryUrl.value = meta.repositoryUrl;
|
||||
feedbackUrl.value = meta.feedbackUrl;
|
||||
manifestJsonOverride.value = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t');
|
||||
}
|
||||
|
||||
@@ -151,18 +165,20 @@ function save() {
|
||||
infoImageUrl: infoImageUrl.value === '' ? null : infoImageUrl.value,
|
||||
notFoundImageUrl: notFoundImageUrl.value === '' ? null : notFoundImageUrl.value,
|
||||
serverErrorImageUrl: serverErrorImageUrl.value === '' ? null : serverErrorImageUrl.value,
|
||||
repositoryUrl: repositoryUrl.value === '' ? null : repositoryUrl.value,
|
||||
feedbackUrl: feedbackUrl.value === '' ? null : feedbackUrl.value,
|
||||
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.branding,
|
||||
icon: 'ti ti-paint',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -33,8 +33,8 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.database,
|
||||
icon: 'ti ti-database',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -124,16 +124,16 @@ function save() {
|
||||
smtpUser: smtpUser.value,
|
||||
smtpPass: smtpPass.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.emailServer,
|
||||
icon: 'ti ti-mail',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -61,7 +61,7 @@ function save() {
|
||||
deeplAuthKey: deeplAuthKey.value,
|
||||
deeplIsPro: deeplIsPro.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.externalServices,
|
||||
icon: 'ti ti-link',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -58,6 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { computed, ref } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
@@ -90,8 +91,17 @@ const pagination = {
|
||||
})),
|
||||
};
|
||||
|
||||
function getStatus(instance) {
|
||||
if (instance.isSuspended) return 'Suspended';
|
||||
function getStatus(instance: Misskey.entities.FederationInstance) {
|
||||
switch (instance.suspensionState) {
|
||||
case 'manuallySuspended':
|
||||
return 'Manually Suspended';
|
||||
case 'goneSuspended':
|
||||
return 'Automatically Suspended (Gone)';
|
||||
case 'autoSuspendedForNotResponding':
|
||||
return 'Automatically Suspended (Not Responding)';
|
||||
case 'none':
|
||||
break;
|
||||
}
|
||||
if (instance.isBlocked) return 'Blocked';
|
||||
if (instance.isSilenced) return 'Silenced';
|
||||
if (instance.isNotResponding) return 'Error';
|
||||
@@ -102,10 +112,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.federation,
|
||||
icon: 'ti ti-whirl',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -42,7 +42,7 @@ import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { lookupFile } from '@/scripts/admin-lookup.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
@@ -73,33 +73,10 @@ function clear() {
|
||||
});
|
||||
}
|
||||
|
||||
function show(file) {
|
||||
os.pageWindow(`/admin/file/${file.id}`);
|
||||
}
|
||||
|
||||
async function find() {
|
||||
const { canceled, result: q } = await os.inputText({
|
||||
title: i18n.ts.fileIdOrUrl,
|
||||
minLength: 1,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
misskeyApi('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
|
||||
show(file);
|
||||
}).catch(err => {
|
||||
if (err.code === 'NO_SUCH_FILE') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.notFound,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
text: i18n.ts.lookup,
|
||||
icon: 'ti ti-search',
|
||||
handler: find,
|
||||
handler: lookupFile,
|
||||
}, {
|
||||
text: i18n.ts.clearCachedFiles,
|
||||
icon: 'ti ti-trash',
|
||||
@@ -108,8 +85,8 @@ const headerActions = computed(() => [{
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.files,
|
||||
icon: 'ti ti-cloud',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -12,10 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<img :src="instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div class="_gaps_s">
|
||||
<MkInfo v-if="thereIsUnresolvedAbuseReport" warn>{{ i18n.ts.thereIsUnresolvedAbuseReportWarning }} <MkA to="/admin/abuses" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noMaintainerInformation" warn>{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noInquiryUrl" warn>{{ i18n.ts.noInquiryUrlWarning }} <MkA to="/admin/moderation" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noBotProtection" warn>{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noEmailServer" warn>{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
</div>
|
||||
|
||||
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
|
||||
</div>
|
||||
@@ -28,15 +31,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ComputedRef, Ref, onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue';
|
||||
import { onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkSuperMenu from '@/components/MkSuperMenu.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import { lookup } from '@/scripts/lookup.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js';
|
||||
import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
import { lookupUser, lookupUserByEmail, lookupFile } from '@/scripts/admin-lookup.js';
|
||||
import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
||||
const isEmpty = (x: string | null) => x == null || x === '';
|
||||
@@ -52,7 +56,7 @@ const indexInfo = {
|
||||
provide('shouldOmitHeaderTitle', false);
|
||||
|
||||
const INFO = ref(indexInfo);
|
||||
const childInfo: Ref<ComputedRef<PageMetadata> | null> = ref(null);
|
||||
const childInfo = ref<null | PageMetadata>(null);
|
||||
const narrow = ref(false);
|
||||
const view = ref(null);
|
||||
const el = ref<HTMLDivElement | null>(null);
|
||||
@@ -60,6 +64,7 @@ const pageProps = ref({});
|
||||
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
|
||||
let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile;
|
||||
let noEmailServer = !instance.enableEmail;
|
||||
let noInquiryUrl = isEmpty(instance.inquiryUrl);
|
||||
const thereIsUnresolvedAbuseReport = ref(false);
|
||||
const currentPage = computed(() => router.currentRef.value.child);
|
||||
|
||||
@@ -82,7 +87,7 @@ const menuDef = computed(() => [{
|
||||
type: 'button',
|
||||
icon: 'ti ti-search',
|
||||
text: i18n.ts.lookup,
|
||||
action: lookup,
|
||||
action: adminLookup,
|
||||
}, ...(instance.disableRegistration ? [{
|
||||
type: 'button',
|
||||
icon: 'ti ti-user-plus',
|
||||
@@ -209,6 +214,11 @@ const menuDef = computed(() => [{
|
||||
text: i18n.ts.externalServices,
|
||||
to: '/admin/external-services',
|
||||
active: currentPage.value?.route.name === 'external-services',
|
||||
}, {
|
||||
icon: 'ti ti-webhook',
|
||||
text: 'Webhook',
|
||||
to: '/admin/system-webhook',
|
||||
active: currentPage.value?.route.name === 'system-webhook',
|
||||
}, {
|
||||
icon: 'ti ti-adjustments',
|
||||
text: i18n.ts.other,
|
||||
@@ -257,14 +267,16 @@ watch(router.currentRef, (to) => {
|
||||
}
|
||||
});
|
||||
|
||||
provideMetadataReceiver((info) => {
|
||||
provideMetadataReceiver((metadataGetter) => {
|
||||
const info = metadataGetter();
|
||||
if (info == null) {
|
||||
childInfo.value = null;
|
||||
} else {
|
||||
childInfo.value = info;
|
||||
INFO.value.needWideArea = info.value.needWideArea ?? undefined;
|
||||
INFO.value.needWideArea = info.needWideArea ?? undefined;
|
||||
}
|
||||
});
|
||||
provideReactiveMetadata(INFO);
|
||||
|
||||
function invite() {
|
||||
misskeyApi('admin/invite/create').then(x => {
|
||||
@@ -280,7 +292,7 @@ function invite() {
|
||||
});
|
||||
}
|
||||
|
||||
function lookup(ev: MouseEvent) {
|
||||
function adminLookup(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.user,
|
||||
icon: 'ti ti-user',
|
||||
@@ -293,23 +305,17 @@ function lookup(ev: MouseEvent) {
|
||||
action: () => {
|
||||
lookupUserByEmail();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.note,
|
||||
icon: 'ti ti-pencil',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.file,
|
||||
icon: 'ti ti-cloud',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
lookupFile();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.instance,
|
||||
icon: 'ti ti-planet',
|
||||
text: i18n.ts.lookup,
|
||||
icon: 'ti ti-world-search',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
lookup();
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
@@ -318,7 +324,7 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(INFO.value);
|
||||
definePageMetadata(() => INFO.value);
|
||||
|
||||
defineExpose({
|
||||
header: {
|
||||
@@ -351,10 +357,6 @@ defineExpose({
|
||||
|
||||
> .nav {
|
||||
.lxpfedzu {
|
||||
> .info {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
> .banner {
|
||||
margin: 16px;
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ function save() {
|
||||
silencedHosts: silencedHosts.value.split('\n') || [],
|
||||
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,8 +66,8 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-eye-off',
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.instanceBlocking,
|
||||
icon: 'ti ti-ban',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -113,10 +113,10 @@ function deleted(id: string) {
|
||||
const headerActions = computed(() => []);
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.invite,
|
||||
icon: 'ti ti-user-plus',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -30,6 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="inquiryUrl" type="url">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._serverSettings.inquiryUrl }}</template>
|
||||
<template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkTextarea v-model="preservedUsernames">
|
||||
<template #label>{{ i18n.ts.preservedUsernames }}</template>
|
||||
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
|
||||
@@ -86,6 +92,7 @@ const hiddenTags = ref<string>('');
|
||||
const preservedUsernames = ref<string>('');
|
||||
const tosUrl = ref<string | null>(null);
|
||||
const privacyPolicyUrl = ref<string | null>(null);
|
||||
const inquiryUrl = ref<string | null>(null);
|
||||
|
||||
async function init() {
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
@@ -97,6 +104,7 @@ async function init() {
|
||||
preservedUsernames.value = meta.preservedUsernames.join('\n');
|
||||
tosUrl.value = meta.tosUrl;
|
||||
privacyPolicyUrl.value = meta.privacyPolicyUrl;
|
||||
inquiryUrl.value = meta.inquiryUrl;
|
||||
}
|
||||
|
||||
function save() {
|
||||
@@ -105,21 +113,22 @@ function save() {
|
||||
emailRequiredForSignup: emailRequiredForSignup.value,
|
||||
tosUrl: tosUrl.value,
|
||||
privacyPolicyUrl: privacyPolicyUrl.value,
|
||||
inquiryUrl: inquiryUrl.value,
|
||||
sensitiveWords: sensitiveWords.value.split('\n'),
|
||||
prohibitedWords: prohibitedWords.value.split('\n'),
|
||||
hiddenTags: hiddenTags.value.split('\n'),
|
||||
preservedUsernames: preservedUsernames.value.split('\n'),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.moderation,
|
||||
icon: 'ti ti-shield',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -8,9 +8,35 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>
|
||||
<b
|
||||
:class="{
|
||||
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation', 'createAvatarDecoration'].includes(log.type),
|
||||
[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type),
|
||||
[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].includes(log.type)
|
||||
[$style.logGreen]: [
|
||||
'createRole',
|
||||
'addCustomEmoji',
|
||||
'createGlobalAnnouncement',
|
||||
'createUserAnnouncement',
|
||||
'createAd',
|
||||
'createInvitation',
|
||||
'createAvatarDecoration',
|
||||
'createSystemWebhook',
|
||||
'createAbuseReportNotificationRecipient',
|
||||
].includes(log.type),
|
||||
[$style.logYellow]: [
|
||||
'markSensitiveDriveFile',
|
||||
'resetPassword'
|
||||
].includes(log.type),
|
||||
[$style.logRed]: [
|
||||
'suspend',
|
||||
'deleteRole',
|
||||
'suspendRemoteInstance',
|
||||
'deleteGlobalAnnouncement',
|
||||
'deleteUserAnnouncement',
|
||||
'deleteCustomEmoji',
|
||||
'deleteNote',
|
||||
'deleteDriveFile',
|
||||
'deleteAd',
|
||||
'deleteAvatarDecoration',
|
||||
'deleteSystemWebhook',
|
||||
'deleteAbuseReportNotificationRecipient',
|
||||
].includes(log.type)
|
||||
}"
|
||||
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
||||
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
@@ -40,6 +66,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span v-else-if="log.type === 'createAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span>
|
||||
<span v-else-if="log.type === 'updateAvatarDecoration'">: {{ log.info.before.name }}</span>
|
||||
<span v-else-if="log.type === 'deleteAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span>
|
||||
<span v-else-if="log.type === 'createSystemWebhook'">: {{ log.info.webhook.name }}</span>
|
||||
<span v-else-if="log.type === 'updateSystemWebhook'">: {{ log.info.before.name }}</span>
|
||||
<span v-else-if="log.type === 'deleteSystemWebhook'">: {{ log.info.webhook.name }}</span>
|
||||
<span v-else-if="log.type === 'createAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
|
||||
<span v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">: {{ log.info.before.name }}</span>
|
||||
<span v-else-if="log.type === 'deleteAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
|
||||
</template>
|
||||
<template #icon>
|
||||
<MkAvatar :user="log.user" :class="$style.avatar"/>
|
||||
@@ -110,6 +142,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'updateRemoteInstanceNote'">
|
||||
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
|
||||
<div :class="$style.diff">
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'updateSystemWebhook'">
|
||||
<div :class="$style.diff">
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">
|
||||
<div :class="$style.diff">
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<details>
|
||||
<summary>raw</summary>
|
||||
|
||||
@@ -54,14 +54,12 @@ const pagination = {
|
||||
})),
|
||||
};
|
||||
|
||||
console.log(Misskey);
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.moderationLogs,
|
||||
icon: 'ti ti-list-search',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -143,16 +143,16 @@ function save() {
|
||||
objectStorageSetPublicRead: objectStorageSetPublicRead.value,
|
||||
objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.objectStorage,
|
||||
icon: 'ti ti-cloud',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -73,7 +73,7 @@ function save() {
|
||||
enableChartsForRemoteUser: enableChartsForRemoteUser.value,
|
||||
enableChartsForFederatedInstances: enableChartsForFederatedInstances.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,8 +86,8 @@ const headerActions = computed(() => [{
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.other,
|
||||
icon: 'ti ti-adjustments',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -184,10 +184,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.dashboard,
|
||||
icon: 'ti ti-dashboard',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -56,7 +56,7 @@ function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
proxyAccountId: proxyAccountId.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.proxyAccount,
|
||||
icon: 'ti ti-ghost',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -68,8 +68,8 @@ const headerTabs = computed(() => [{
|
||||
title: 'Inbox',
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.jobQueue,
|
||||
icon: 'ti ti-clock-play',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -84,10 +84,10 @@ const headerActions = computed(() => [{
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.relays,
|
||||
icon: 'ti ti-planet',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -87,11 +87,8 @@ async function save() {
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => role.value ? {
|
||||
title: i18n.ts._role.edit + ': ' + role.value.name,
|
||||
icon: 'ti ti-badge',
|
||||
} : {
|
||||
title: i18n.ts._role.new,
|
||||
definePageMetadata(() => ({
|
||||
title: role.value ? `${i18n.ts._role.edit}: ${role.value.name}` : i18n.ts._role.new,
|
||||
icon: 'ti ti-badge',
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -160,6 +160,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.mentionLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.mentionLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.mentionLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.mentionLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.mentionLimit.value" :disabled="role.policies.mentionLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.mentionLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #suffix>
|
||||
|
||||
@@ -119,7 +119,7 @@ async function assign() {
|
||||
const user = await os.selectUser({ includeSelf: true });
|
||||
|
||||
const { canceled: canceled2, result: period } = await os.select({
|
||||
title: i18n.ts.period,
|
||||
title: i18n.ts.period + ': ' + role.name,
|
||||
items: [{
|
||||
value: 'indefinitely', text: i18n.ts.indefinitely,
|
||||
}, {
|
||||
@@ -170,10 +170,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.role + ': ' + role.name,
|
||||
definePageMetadata(() => ({
|
||||
title: `${i18n.ts.role}: ${role.name}`,
|
||||
icon: 'ti ti-badge',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -48,6 +48,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||
<template #suffix>{{ policies.mentionLimit }}</template>
|
||||
<MkInput v-model="policies.mentionLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
|
||||
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
|
||||
<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
@@ -236,7 +243,7 @@ import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { instance, fetchInstance } from '@/instance.js';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { ROLE_POLICIES } from '@/const.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
@@ -260,6 +267,7 @@ async function updateBaseRole() {
|
||||
await os.apiWithDialog('admin/roles/update-default-policies', {
|
||||
policies,
|
||||
});
|
||||
fetchInstance(true);
|
||||
}
|
||||
|
||||
function create() {
|
||||
@@ -270,10 +278,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.roles,
|
||||
icon: 'ti ti-badges',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -118,19 +118,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>Summaly Proxy</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="summalyProxy">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>Summaly Proxy URL</template>
|
||||
</MkInput>
|
||||
|
||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
@@ -155,7 +142,6 @@ import { fetchInstance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const summalyProxy = ref<string>('');
|
||||
const enableHcaptcha = ref<boolean>(false);
|
||||
const enableMcaptcha = ref<boolean>(false);
|
||||
const enableRecaptcha = ref<boolean>(false);
|
||||
@@ -175,7 +161,6 @@ const bannedEmailDomains = ref<string>('');
|
||||
|
||||
async function init() {
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
summalyProxy.value = meta.summalyProxy;
|
||||
enableHcaptcha.value = meta.enableHcaptcha;
|
||||
enableMcaptcha.value = meta.enableMcaptcha;
|
||||
enableRecaptcha.value = meta.enableRecaptcha;
|
||||
@@ -196,12 +181,11 @@ async function init() {
|
||||
enableTruemailApi.value = meta.enableTruemailApi;
|
||||
truemailInstance.value = meta.truemailInstance;
|
||||
truemailAuthKey.value = meta.truemailAuthKey;
|
||||
bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || "";
|
||||
bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || '';
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
summalyProxy: summalyProxy.value,
|
||||
sensitiveMediaDetection: sensitiveMediaDetection.value,
|
||||
sensitiveMediaDetectionSensitivity:
|
||||
sensitiveMediaDetectionSensitivity.value === 0 ? 'veryLow' :
|
||||
@@ -221,7 +205,7 @@ function save() {
|
||||
truemailAuthKey: truemailAuthKey.value,
|
||||
bannedEmailDomains: bannedEmailDomains.value.split('\n'),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,8 +213,8 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.security,
|
||||
icon: 'ti ti-lock',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -58,7 +58,7 @@ const save = async () => {
|
||||
await os.apiWithDialog('admin/update-meta', {
|
||||
serverRules: serverRules.value,
|
||||
});
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
};
|
||||
|
||||
const remove = (index: number): void => {
|
||||
@@ -67,10 +67,10 @@ const remove = (index: number): void => {
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.serverRules,
|
||||
icon: 'ti ti-checkbox',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -34,6 +34,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
|
||||
<MkInput v-model="repositoryUrl" type="url">
|
||||
<template #label>{{ i18n.ts.repositoryUrl }}</template>
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #caption>{{ i18n.ts.repositoryUrlDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInfo v-if="!instance.providesTarball && !repositoryUrl" warn>
|
||||
{{ i18n.ts.repositoryUrlOrTarballRequired }}
|
||||
</MkInfo>
|
||||
|
||||
<MkInput v-model="impressumUrl" type="url">
|
||||
<template #label>{{ i18n.ts.impressumUrl }}</template>
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
@@ -133,6 +143,53 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._urlPreviewSetting.title }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkSwitch v-model="urlPreviewEnabled">
|
||||
<template #label>{{ i18n.ts._urlPreviewSetting.enable }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="urlPreviewRequireContentLength">
|
||||
<template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}</template>
|
||||
<template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkInput v-model="urlPreviewMaximumContentLength" type="number">
|
||||
<template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}</template>
|
||||
<template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="urlPreviewTimeout" type="number">
|
||||
<template #label>{{ i18n.ts._urlPreviewSetting.timeout }}</template>
|
||||
<template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="urlPreviewUserAgent" type="text">
|
||||
<template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}</template>
|
||||
<template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<div>
|
||||
<MkInput v-model="urlPreviewSummaryProxyUrl" type="text">
|
||||
<template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}</template>
|
||||
<template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<div :class="$style.subCaption">
|
||||
{{ i18n.ts._urlPreviewSetting.summaryProxyDescription2 }}
|
||||
<ul style="padding-left: 20px; margin: 4px 0">
|
||||
<li>{{ i18n.ts._urlPreviewSetting.timeout }} / key:timeout</li>
|
||||
<li>{{ i18n.ts._urlPreviewSetting.maximumContentLength }} / key:contentLengthLimit</li>
|
||||
<li>{{ i18n.ts._urlPreviewSetting.requireContentLength }} / key:contentLengthRequired</li>
|
||||
<li>{{ i18n.ts._urlPreviewSetting.userAgent }} / key:userAgent</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
@@ -159,16 +216,19 @@ import FormSplit from '@/components/form/split.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { fetchInstance } from '@/instance.js';
|
||||
import { fetchInstance, instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
|
||||
const name = ref<string | null>(null);
|
||||
const shortName = ref<string | null>(null);
|
||||
const description = ref<string | null>(null);
|
||||
const maintainerName = ref<string | null>(null);
|
||||
const maintainerEmail = ref<string | null>(null);
|
||||
const repositoryUrl = ref<string | null>(null);
|
||||
const impressumUrl = ref<string | null>(null);
|
||||
const pinnedUsers = ref<string>('');
|
||||
const cacheRemoteFiles = ref<boolean>(false);
|
||||
@@ -183,6 +243,12 @@ const perRemoteUserUserTimelineCacheMax = ref<number>(0);
|
||||
const perUserHomeTimelineCacheMax = ref<number>(0);
|
||||
const perUserListTimelineCacheMax = ref<number>(0);
|
||||
const notesPerOneAd = ref<number>(0);
|
||||
const urlPreviewEnabled = ref<boolean>(true);
|
||||
const urlPreviewTimeout = ref<number>(10000);
|
||||
const urlPreviewMaximumContentLength = ref<number>(1024 * 1024 * 10);
|
||||
const urlPreviewRequireContentLength = ref<boolean>(true);
|
||||
const urlPreviewUserAgent = ref<string | null>(null);
|
||||
const urlPreviewSummaryProxyUrl = ref<string | null>(null);
|
||||
|
||||
async function init(): Promise<void> {
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
@@ -191,6 +257,7 @@ async function init(): Promise<void> {
|
||||
description.value = meta.description;
|
||||
maintainerName.value = meta.maintainerName;
|
||||
maintainerEmail.value = meta.maintainerEmail;
|
||||
repositoryUrl.value = meta.repositoryUrl;
|
||||
impressumUrl.value = meta.impressumUrl;
|
||||
pinnedUsers.value = meta.pinnedUsers.join('\n');
|
||||
cacheRemoteFiles.value = meta.cacheRemoteFiles;
|
||||
@@ -205,15 +272,22 @@ async function init(): Promise<void> {
|
||||
perUserHomeTimelineCacheMax.value = meta.perUserHomeTimelineCacheMax;
|
||||
perUserListTimelineCacheMax.value = meta.perUserListTimelineCacheMax;
|
||||
notesPerOneAd.value = meta.notesPerOneAd;
|
||||
urlPreviewEnabled.value = meta.urlPreviewEnabled;
|
||||
urlPreviewTimeout.value = meta.urlPreviewTimeout;
|
||||
urlPreviewMaximumContentLength.value = meta.urlPreviewMaximumContentLength;
|
||||
urlPreviewRequireContentLength.value = meta.urlPreviewRequireContentLength;
|
||||
urlPreviewUserAgent.value = meta.urlPreviewUserAgent;
|
||||
urlPreviewSummaryProxyUrl.value = meta.urlPreviewSummaryProxyUrl;
|
||||
}
|
||||
|
||||
async function save(): void {
|
||||
async function save() {
|
||||
await os.apiWithDialog('admin/update-meta', {
|
||||
name: name.value,
|
||||
shortName: shortName.value === '' ? null : shortName.value,
|
||||
description: description.value,
|
||||
maintainerName: maintainerName.value,
|
||||
maintainerEmail: maintainerEmail.value,
|
||||
repositoryUrl: repositoryUrl.value,
|
||||
impressumUrl: impressumUrl.value,
|
||||
pinnedUsers: pinnedUsers.value.split('\n'),
|
||||
cacheRemoteFiles: cacheRemoteFiles.value,
|
||||
@@ -228,17 +302,23 @@ async function save(): void {
|
||||
perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value,
|
||||
perUserListTimelineCacheMax: perUserListTimelineCacheMax.value,
|
||||
notesPerOneAd: notesPerOneAd.value,
|
||||
urlPreviewEnabled: urlPreviewEnabled.value,
|
||||
urlPreviewTimeout: urlPreviewTimeout.value,
|
||||
urlPreviewMaximumContentLength: urlPreviewMaximumContentLength.value,
|
||||
urlPreviewRequireContentLength: urlPreviewRequireContentLength.value,
|
||||
urlPreviewUserAgent: urlPreviewUserAgent.value,
|
||||
urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value,
|
||||
});
|
||||
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
}
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.general,
|
||||
icon: 'ti ti-settings',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@@ -246,4 +326,9 @@ definePageMetadata({
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
|
||||
.subCaption {
|
||||
font-size: 0.85em;
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
</style>
|
||||
|
||||
117
packages/frontend/src/pages/admin/system-webhook.item.vue
Normal file
117
packages/frontend/src/pages/admin/system-webhook.item.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.main">
|
||||
<span :class="$style.icon">
|
||||
<i v-if="!entity.isActive" class="ti ti-player-pause"/>
|
||||
<i v-else-if="entity.latestStatus === null" class="ti ti-circle"/>
|
||||
<i
|
||||
v-else-if="[200, 201, 204].includes(entity.latestStatus)"
|
||||
class="ti ti-check"
|
||||
:style="{ color: 'var(--success)' }"
|
||||
/>
|
||||
<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"/>
|
||||
</span>
|
||||
<span :class="$style.text">{{ entity.name || entity.url }}</span>
|
||||
<span :class="$style.suffix">
|
||||
<MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/>
|
||||
<button :class="$style.suffixButton" @click="onEditClick">
|
||||
<i class="ti ti-settings"></i>
|
||||
</button>
|
||||
<button :class="$style.suffixButton" @click="onDeleteClick">
|
||||
<i class="ti ti-trash"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { entities } from 'misskey-js';
|
||||
import { toRefs } from 'vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'edit', value: entities.SystemWebhook): void;
|
||||
(ev: 'delete', value: entities.SystemWebhook): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
entity: entities.SystemWebhook;
|
||||
}>();
|
||||
|
||||
const { entity } = toRefs(props);
|
||||
|
||||
function onEditClick() {
|
||||
emit('edit', entity.value);
|
||||
}
|
||||
|
||||
function onDeleteClick() {
|
||||
emit('delete', entity.value);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 14px;
|
||||
background: var(--buttonBg);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9em;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: var(--buttonHoverBg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
background: var(--buttonHoverBg);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 0.75em;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
|
||||
.text {
|
||||
flex-shrink: 1;
|
||||
white-space: normal;
|
||||
padding-right: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.suffix {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gaps: 4px;
|
||||
margin-left: auto;
|
||||
margin-right: -8px;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.suffixButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
margin-top: -8px;
|
||||
margin-bottom: -8px;
|
||||
padding: 8px;
|
||||
|
||||
&:hover {
|
||||
background: var(--buttonBg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
96
packages/frontend/src/pages/admin/system-webhook.vue
Normal file
96
packages/frontend/src/pages/admin/system-webhook.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<XHeader :actions="headerActions" :tabs="headerTabs"/>
|
||||
</template>
|
||||
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps_m">
|
||||
<MkButton :class="$style.linkButton" full @click="onCreateWebhookClicked">
|
||||
{{ i18n.ts._webhookSettings.createWebhook }}
|
||||
</MkButton>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps">
|
||||
<XItem v-for="item in webhooks" :key="item.id" :entity="item" @edit="onEditButtonClicked" @delete="onDeleteButtonClicked"/>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { entities } from 'misskey-js';
|
||||
import XItem from './system-webhook.item.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import XHeader from '@/pages/admin/_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const webhooks = ref<entities.SystemWebhook[]>([]);
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
async function onCreateWebhookClicked() {
|
||||
await showSystemWebhookEditorDialog({
|
||||
mode: 'create',
|
||||
});
|
||||
|
||||
await fetchWebhooks();
|
||||
}
|
||||
|
||||
async function onEditButtonClicked(webhook: entities.SystemWebhook) {
|
||||
await showSystemWebhookEditorDialog({
|
||||
mode: 'edit',
|
||||
id: webhook.id,
|
||||
});
|
||||
|
||||
await fetchWebhooks();
|
||||
}
|
||||
|
||||
async function onDeleteButtonClicked(webhook: entities.SystemWebhook) {
|
||||
const result = await os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts._webhookSettings.deleteConfirm,
|
||||
});
|
||||
if (!result.canceled) {
|
||||
await misskeyApi('admin/system-webhook/delete', {
|
||||
id: webhook.id,
|
||||
});
|
||||
await fetchWebhooks();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWebhooks() {
|
||||
const result = await misskeyApi('admin/system-webhook/list', {});
|
||||
webhooks.value = result.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchWebhooks();
|
||||
});
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: 'SystemWebhook',
|
||||
icon: 'ti ti-webhook',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.linkButton {
|
||||
text-align: left;
|
||||
padding: 10px 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -63,7 +63,7 @@ import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { lookupUser } from '@/scripts/lookup-user.js';
|
||||
import { lookupUser } from '@/scripts/admin-lookup.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
@@ -137,10 +137,10 @@ const headerActions = computed(() => [{
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.users,
|
||||
icon: 'ti ti-users',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -20,9 +20,9 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.ads,
|
||||
icon: 'ti ti-ad',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
|
||||
151
packages/frontend/src/pages/announcement.vue
Normal file
151
packages/frontend/src/pages/announcement.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<Transition
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
|
||||
mode="out-in"
|
||||
>
|
||||
<div v-if="announcement" :key="announcement.id" class="_panel" :class="$style.announcement">
|
||||
<div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div>
|
||||
<div :class="$style.header">
|
||||
<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
|
||||
<span style="margin-right: 0.5em;">
|
||||
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
|
||||
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
|
||||
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
|
||||
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<Mfm :text="announcement.title"/>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<Mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
|
||||
<div style="margin-top: 8px; opacity: 0.7; font-size: 85%;">
|
||||
{{ i18n.ts.createdAt }}: <MkTime :time="announcement.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<div v-if="announcement.updatedAt" style="opacity: 0.7; font-size: 85%;">
|
||||
{{ i18n.ts.updatedAt }}: <MkTime :time="announcement.updatedAt" mode="detail"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer">
|
||||
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetch()"/>
|
||||
<MkLoading v-else/>
|
||||
</Transition>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i, updateAccount } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
announcementId: string;
|
||||
}>();
|
||||
|
||||
const announcement = ref<Misskey.entities.Announcement | null>(null);
|
||||
const error = ref<any>(null);
|
||||
const path = computed(() => props.announcementId);
|
||||
|
||||
function fetch() {
|
||||
announcement.value = null;
|
||||
misskeyApi('announcements/show', {
|
||||
announcementId: props.announcementId,
|
||||
}).then(async _announcement => {
|
||||
announcement.value = _announcement;
|
||||
}).catch(err => {
|
||||
error.value = err;
|
||||
});
|
||||
}
|
||||
|
||||
async function read(target: Misskey.entities.Announcement): Promise<void> {
|
||||
if (target.needConfirmationToRead) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts._announcement.readConfirmTitle,
|
||||
text: i18n.tsx._announcement.readConfirmText({ title: target.title }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
}
|
||||
|
||||
target.isRead = true;
|
||||
await misskeyApi('i/read-announcement', { announcementId: target.id });
|
||||
if ($i) {
|
||||
updateAccount({
|
||||
unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => path.value, fetch, { immediate: true });
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.fadeEnterActive,
|
||||
.fadeLeaveActive {
|
||||
transition: opacity 0.125s ease;
|
||||
}
|
||||
.fadeEnterFrom,
|
||||
.fadeLeaveTo {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.announcement {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.forYou {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 24px;
|
||||
font-size: 90%;
|
||||
white-space: pre;
|
||||
color: #d28a3f;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 16px;
|
||||
font-weight: bold;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.content {
|
||||
> img {
|
||||
display: block;
|
||||
max-height: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -21,14 +21,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
|
||||
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<span>{{ announcement.title }}</span>
|
||||
<MkA :to="`/announcements/${announcement.id}`"><span>{{ announcement.title }}</span></MkA>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<Mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
|
||||
<div style="opacity: 0.7; font-size: 85%;">
|
||||
<MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<MkA :to="`/announcements/${announcement.id}`">
|
||||
<div style="margin-top: 8px; opacity: 0.7; font-size: 85%;">
|
||||
{{ i18n.ts.createdAt }}: <MkTime :time="announcement.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<div v-if="announcement.updatedAt" style="opacity: 0.7; font-size: 85%;">
|
||||
{{ i18n.ts.updatedAt }}: <MkTime :time="announcement.updatedAt" mode="detail"/>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
<div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
|
||||
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||
@@ -73,24 +78,24 @@ const paginationEl = ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const tab = ref('current');
|
||||
|
||||
async function read(announcement) {
|
||||
if (announcement.needConfirmationToRead) {
|
||||
async function read(target) {
|
||||
if (target.needConfirmationToRead) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts._announcement.readConfirmTitle,
|
||||
text: i18n.tsx._announcement.readConfirmText({ title: announcement.title }),
|
||||
text: i18n.tsx._announcement.readConfirmText({ title: target.title }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
}
|
||||
|
||||
if (!paginationEl.value) return;
|
||||
paginationEl.value.updateItem(announcement.id, a => {
|
||||
paginationEl.value.updateItem(target.id, a => {
|
||||
a.isRead = true;
|
||||
return a;
|
||||
});
|
||||
misskeyApi('i/read-announcement', { announcementId: announcement.id });
|
||||
misskeyApi('i/read-announcement', { announcementId: target.id });
|
||||
updateAccount({
|
||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== announcement.id),
|
||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,10 +111,10 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-point',
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -91,10 +91,10 @@ const headerActions = computed(() => antenna.value ? [{
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => antenna.value ? {
|
||||
title: antenna.value.name,
|
||||
definePageMetadata(() => ({
|
||||
title: antenna.value ? antenna.value.name : i18n.ts.antennas,
|
||||
icon: 'ti ti-antenna',
|
||||
} : null));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -87,8 +87,8 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: 'API console',
|
||||
icon: 'ti ti-terminal-2',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -97,10 +97,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts._auth.shareAccessTitle,
|
||||
icon: 'ti ti-apps',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -94,8 +94,8 @@ const headerActions = computed(() => [{
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.avatarDecorations,
|
||||
icon: 'ti ti-sparkles',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -202,11 +202,8 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => props.channelId ? {
|
||||
title: i18n.ts._channel.edit,
|
||||
icon: 'ti ti-device-tv',
|
||||
} : {
|
||||
title: i18n.ts._channel.create,
|
||||
definePageMetadata(() => ({
|
||||
title: props.channelId ? i18n.ts._channel.edit : i18n.ts._channel.create,
|
||||
icon: 'ti ti-device-tv',
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -83,6 +83,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import { url } from '@/config.js';
|
||||
import { favoritedChannelsCache } from '@/cache.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
@@ -153,6 +154,7 @@ function favorite() {
|
||||
channelId: channel.value.id,
|
||||
}).then(() => {
|
||||
favorited.value = true;
|
||||
favoritedChannelsCache.delete();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -168,6 +170,7 @@ async function unfavorite() {
|
||||
channelId: channel.value.id,
|
||||
}).then(() => {
|
||||
favorited.value = false;
|
||||
favoritedChannelsCache.delete();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -258,10 +261,10 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-search',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => channel.value ? {
|
||||
title: channel.value.name,
|
||||
definePageMetadata(() => ({
|
||||
title: channel.value ? channel.value.name : i18n.ts.channel,
|
||||
icon: 'ti ti-device-tv',
|
||||
} : null));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -152,8 +152,8 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-edit',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.channel,
|
||||
icon: 'ti ti-device-tv',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -16,10 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import MkClickerGame from '@/components/MkClickerGame.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: '🍪👈',
|
||||
icon: 'ti ti-cookie',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -9,11 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSpacer :contentMax="800">
|
||||
<div v-if="clip" class="_gaps">
|
||||
<div class="_panel">
|
||||
<div v-if="clip.description" :class="$style.description">
|
||||
<Mfm :text="clip.description" :isNote="false"/>
|
||||
<div class="_gaps_s" :class="$style.description">
|
||||
<div v-if="clip.description">
|
||||
<Mfm :text="clip.description" :isNote="false"/>
|
||||
</div>
|
||||
<div v-else>({{ i18n.ts.noDescription }})</div>
|
||||
<div>
|
||||
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
|
||||
<div :class="$style.user">
|
||||
<MkAvatar :user="clip.user" :class="$style.avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||
</div>
|
||||
@@ -157,10 +162,10 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
||||
},
|
||||
}] : null);
|
||||
|
||||
definePageMetadata(computed(() => clip.value ? {
|
||||
title: clip.value.name,
|
||||
definePageMetadata(() => ({
|
||||
title: clip.value ? clip.value.name : i18n.ts.clip,
|
||||
icon: 'ti ti-paperclip',
|
||||
} : null));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
40
packages/frontend/src/pages/contact.vue
Normal file
40
packages/frontend/src/pages/contact.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :contentMax="600" :marginMin="20">
|
||||
<div class="_gaps">
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.inquiry }}</template>
|
||||
<template #value>
|
||||
<MkLink :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.email }}</template>
|
||||
<template #value>
|
||||
<div>{{ instance.maintainerEmail }}</div>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.inquiry,
|
||||
icon: 'ti ti-help-circle',
|
||||
}));
|
||||
</script>
|
||||
@@ -305,10 +305,10 @@ const headerTabs = computed(() => [{
|
||||
title: i18n.ts.remote,
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: 'ti ti-icons',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -48,8 +48,8 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-pencil',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-file',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -22,9 +22,9 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: folder.value ? folder.value.name : i18n.ts.drive,
|
||||
icon: 'ti ti-cloud',
|
||||
hideHeader: true,
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -7,9 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSpacer :contentMax="800">
|
||||
<div :class="$style.root">
|
||||
<div v-if="!gameLoaded" :class="$style.loadingScreen">
|
||||
<div>
|
||||
Loading...
|
||||
</div>
|
||||
<div>{{ i18n.ts.loading }}<MkEllipsis/></div>
|
||||
</div>
|
||||
<!-- ↓に対してTransitionコンポーネントを使うと何故かkeyを指定していてもキャッシュが効かず様々なコンポーネントが都度再評価されてパフォーマンスが低下する -->
|
||||
<div v-show="gameLoaded" class="_gaps_s">
|
||||
@@ -32,18 +30,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</Transition>
|
||||
|
||||
<div :class="$style.header">
|
||||
<div :class="[$style.frame, $style.headerTitle]">
|
||||
<div :class="$style.frameInner">
|
||||
<b>BUBBLE GAME</b>
|
||||
<div>- {{ gameMode }} -</div>
|
||||
<div class="_woodenFrame" :class="[$style.headerTitle]">
|
||||
<div class="_woodenFrameInner">
|
||||
<b>{{ i18n.ts.bubbleGame }}</b>
|
||||
<div>- {{ gameMode.toUpperCase() }} -</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="[$style.frame, $style.frameH]">
|
||||
<div :class="$style.frameInner">
|
||||
<MkButton inline small @click="hold">HOLD</MkButton>
|
||||
<div class="_woodenFrame _woodenFrameH">
|
||||
<div class="_woodenFrameInner">
|
||||
<MkButton inline small @click="hold">{{ i18n.ts._bubbleGame.hold }}</MkButton>
|
||||
<img v-if="holdingStock" :src="getTextureImageUrl(holdingStock.mono)" style="width: 32px; margin-left: 8px; vertical-align: bottom;"/>
|
||||
</div>
|
||||
<div :class="[$style.frameInner, $style.stock]" style="text-align: center;">
|
||||
<div class="_woodenFrameInner" :class="$style.stock" style="text-align: center;">
|
||||
<TransitionGroup
|
||||
:enterActiveClass="$style.transition_stock_enterActive"
|
||||
:leaveActiveClass="$style.transition_stock_leaveActive"
|
||||
@@ -90,58 +88,74 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="isGameOver && !replaying" :class="$style.gameOverLabel">
|
||||
<div class="_gaps_s">
|
||||
<img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/>
|
||||
<div>SCORE: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</div>
|
||||
<div>MAX CHAIN: <MkNumber :value="maxCombo"/></div>
|
||||
<div v-if="gameMode === 'yen'">TOTAL EARNINGS: <b><MkNumber :value="yenTotal ?? score"/>円</b></div>
|
||||
<div v-if="gameMode === 'sweets'"><b>おにぎり<MkNumber :value="score / 130"/>個分</b></div>
|
||||
<div>{{ i18n.ts._bubbleGame._score.score }}: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</div>
|
||||
<div>{{ i18n.ts._bubbleGame._score.maxChain }}: <MkNumber :value="maxCombo"/></div>
|
||||
<div v-if="gameMode === 'yen'">
|
||||
{{ i18n.ts._bubbleGame._score.scoreYen }}:
|
||||
<I18n :src="i18n.ts._bubbleGame._score.yen" tag="b">
|
||||
<template #yen><MkNumber :value="yenTotal ?? score"/></template>
|
||||
</I18n>
|
||||
</div>
|
||||
<I18n v-if="gameMode === 'sweets'" :src="i18n.ts._bubbleGame._score.scoreSweets" tag="div">
|
||||
<template #onigiriQtyWithUnit>
|
||||
<I18n :src="i18n.ts._bubbleGame._score.estimatedQty" tag="b">
|
||||
<template #qty><MkNumber :value="score / 130"/></template>
|
||||
</I18n>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ti ti-player-play"></i> {{ i18n.ts.replaying }}</span></div>
|
||||
</div>
|
||||
|
||||
<div v-if="replaying" :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div v-if="replaying" class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div style="background: #0004;">
|
||||
<div style="height: 10px; background: var(--accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton @click="endReplay"><i class="ti ti-player-stop"></i> END</MkButton>
|
||||
<MkButton @click="endReplay"><i class="ti ti-player-stop"></i> {{ i18n.ts.endReplay }}</MkButton>
|
||||
<MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ti ti-player-track-next"></i> x4</MkButton>
|
||||
<MkButton :primary="replayPlaybackRate === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ti ti-player-track-next"></i> x16</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isGameOver" :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div v-if="isGameOver" class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton primary rounded @click="backToTitle">{{ i18n.ts.backToTitle }}</MkButton>
|
||||
<MkButton primary rounded @click="replay">{{ i18n.ts.showReplay }}</MkButton>
|
||||
<MkButton primary rounded @click="share">{{ i18n.ts.share }}</MkButton>
|
||||
<MkButton rounded @click="exportLog">Copy replay data</MkButton>
|
||||
<MkButton rounded @click="exportLog">{{ i18n.ts.copyReplayData }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex;">
|
||||
<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
|
||||
<div :class="$style.frameInner">
|
||||
<div>SCORE: <b><MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</b></div>
|
||||
<div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/>{{ getScoreUnit(gameMode) }}</b><b v-else>-</b></div>
|
||||
<div v-if="gameMode === 'yen'">TOTAL EARNINGS: <b v-if="yenTotal"><MkNumber :value="yenTotal"/>円</b><b v-else>-</b></div>
|
||||
<div class="_woodenFrame" style="flex: 1; margin-right: 10px;">
|
||||
<div class="_woodenFrameInner">
|
||||
<div>{{ i18n.ts._bubbleGame._score.score }}: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</div>
|
||||
<div>{{ i18n.ts._bubbleGame._score.highScore }}: <b v-if="highScore"><MkNumber :value="highScore"/>{{ getScoreUnit(gameMode) }}</b><b v-else>-</b></div>
|
||||
<div v-if="gameMode === 'yen'">
|
||||
{{ i18n.ts._bubbleGame._score.scoreYen }}:
|
||||
<I18n :src="i18n.ts._bubbleGame._score.yen" tag="b">
|
||||
<template #yen><MkNumber :value="yenTotal ?? score"/></template>
|
||||
</I18n>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="[$style.frame]" style="margin-left: auto;">
|
||||
<div :class="$style.frameInner" style="text-align: center;">
|
||||
<div class="_woodenFrame" style="margin-left: auto;">
|
||||
<div class="_woodenFrameInner" style="text-align: center;">
|
||||
<div @click="showConfig = !showConfig"><i class="ti ti-settings"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showConfig" :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div v-if="showConfig" class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps">
|
||||
<MkRange v-model="bgmVolume" :min="0" :max="1" :step="0.01" :textConverter="(v) => `${Math.floor(v * 100)}%`" :continuousUpdate="true" @dragEnded="(v) => updateSettings('bgmVolume', v)">
|
||||
<template #label>BGM {{ i18n.ts.volume }}</template>
|
||||
@@ -153,8 +167,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div>FUSION RECIPE</div>
|
||||
<div>
|
||||
<div v-for="(mono, i) in game.monoDefinitions.sort((a, b) => a.level - b.level)" :key="mono.id" style="display: inline-block;">
|
||||
@@ -165,10 +179,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<MkButton v-if="!isGameOver && !replaying" full danger @click="surrender">Surrender</MkButton>
|
||||
<MkButton v-else full @click="restart">Retry</MkButton>
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<MkButton v-if="!isGameOver && !replaying" full danger @click="surrender">{{ i18n.ts.surrender }}</MkButton>
|
||||
<MkButton v-else full @click="restart">{{ i18n.ts.gameRetry }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1204,10 +1218,10 @@ onDeactivated(() => {
|
||||
bgmNodes?.soundSource.stop();
|
||||
});
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.bubbleGame,
|
||||
icon: 'ti ti-apple',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@@ -1313,38 +1327,6 @@ definePageMetadata({
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.frame {
|
||||
padding: 7px;
|
||||
background: #8C4F26;
|
||||
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.frameH {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.frameInner {
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
background: #F1E8DC;
|
||||
box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
|
||||
border-radius: 6px;
|
||||
color: #693410;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.frameDivider {
|
||||
height: 0;
|
||||
border: none;
|
||||
border-top: 1px solid #693410;
|
||||
border-bottom: 1px solid #ce8a5c;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
@@ -15,13 +15,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSpacer v-if="!gameStarted" :contentMax="800">
|
||||
<div :class="$style.root">
|
||||
<div class="_gaps">
|
||||
<div :class="$style.frame" style="text-align: center;">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame" style="text-align: center;">
|
||||
<div class="_woodenFrameInner">
|
||||
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame" style="text-align: center;">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame" style="text-align: center;">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps" style="padding: 16px;">
|
||||
<MkSelect v-model="gameMode">
|
||||
<option value="normal">NORMAL</option>
|
||||
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps" style="padding: 16px;">
|
||||
<div style="font-size: 90%;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div>
|
||||
<MkSwitch v-model="mute">
|
||||
@@ -42,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps_s" style="padding: 16px;">
|
||||
<div><b>{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</div>
|
||||
<div><b>{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode.toUpperCase() }})</div>
|
||||
<div v-if="ranking" class="_gaps_s">
|
||||
<div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord">
|
||||
<MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
|
||||
@@ -57,8 +57,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner" style="padding: 16px;">
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner" style="padding: 16px;">
|
||||
<div style="font-weight: bold;">{{ i18n.ts._bubbleGame.howToPlay }}</div>
|
||||
<ol>
|
||||
<li>{{ i18n.ts._bubbleGame._howToPlay.section1 }}</li>
|
||||
@@ -67,8 +67,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps_s" style="padding: 16px;">
|
||||
<div><b>Credit</b></div>
|
||||
<div>
|
||||
@@ -121,10 +121,10 @@ function onGameEnd() {
|
||||
gameStarted.value = false;
|
||||
}
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.bubbleGame,
|
||||
icon: 'ti ti-device-gamepad',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@@ -149,38 +149,6 @@ definePageMetadata({
|
||||
}
|
||||
}
|
||||
|
||||
.frame {
|
||||
padding: 7px;
|
||||
background: #8C4F26;
|
||||
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.frameH {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.frameInner {
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
background: #F1E8DC;
|
||||
box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
|
||||
border-radius: 6px;
|
||||
color: #693410;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.frameDivider {
|
||||
height: 0;
|
||||
border: none;
|
||||
border-top: 1px solid #693410;
|
||||
border-bottom: 1px solid #ce8a5c;
|
||||
}
|
||||
|
||||
.rankingRecord {
|
||||
display: flex;
|
||||
line-height: 24px;
|
||||
|
||||
@@ -135,7 +135,7 @@ async function addRole() {
|
||||
const { canceled, result: role } = await os.select({
|
||||
items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
|
||||
});
|
||||
if (canceled) return;
|
||||
if (canceled || role == null) return;
|
||||
|
||||
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ const paginationForPolls = {
|
||||
endpoint: 'notes/polls/recommendation' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
params: {
|
||||
excludeChannels: true,
|
||||
},
|
||||
};
|
||||
|
||||
const tab = ref('notes');
|
||||
|
||||
@@ -60,8 +60,8 @@ const headerTabs = computed(() => [{
|
||||
title: i18n.ts.roles,
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.explore,
|
||||
icon: 'ti ti-hash',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -38,10 +38,10 @@ const pagination = {
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.favorites,
|
||||
icon: 'ti ti-star',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkCodeEditor v-model="script" lang="is">
|
||||
<template #label>{{ i18n.ts._play.script }}</template>
|
||||
</MkCodeEditor>
|
||||
<MkSelect v-model="visibility">
|
||||
<template #label>{{ i18n.ts.visibility }}</template>
|
||||
<template #caption>{{ i18n.ts._play.visibilityDescription }}</template>
|
||||
<option :key="'public'" :value="'public'">{{ i18n.ts.public }}</option>
|
||||
<option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option>
|
||||
</MkSelect>
|
||||
<div class="_buttons">
|
||||
<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton>
|
||||
<MkButton v-if="flash" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
<MkSelect v-model="visibility">
|
||||
<template #label>{{ i18n.ts.visibility }}</template>
|
||||
<option :key="'public'" :value="'public'">{{ i18n.ts.public }}</option>
|
||||
<option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
@@ -47,7 +48,7 @@ import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
||||
const PRESET_DEFAULT = `/// @ 0.16.0
|
||||
const PRESET_DEFAULT = `/// @ 0.18.0
|
||||
|
||||
var name = ""
|
||||
|
||||
@@ -59,13 +60,13 @@ Ui:render([
|
||||
Ui:C:button({
|
||||
text: "Hello"
|
||||
onClick: @() {
|
||||
Mk:dialog(null \`Hello, {name}!\`)
|
||||
Mk:dialog(null, \`Hello, {name}!\`)
|
||||
}
|
||||
})
|
||||
])
|
||||
`;
|
||||
|
||||
const PRESET_OMIKUJI = `/// @ 0.16.0
|
||||
const PRESET_OMIKUJI = `/// @ 0.18.0
|
||||
// ユーザーごとに日替わりのおみくじのプリセット
|
||||
|
||||
// 選択肢
|
||||
@@ -80,11 +81,11 @@ let choices = [
|
||||
"大凶"
|
||||
]
|
||||
|
||||
// シードが「ユーザーID+今日の日付」である乱数生成器を用意
|
||||
let random = Math:gen_rng(\`{USER_ID}{Date:year()}{Date:month()}{Date:day()}\`)
|
||||
// シードが「PlayID+ユーザーID+今日の日付」である乱数生成器を用意
|
||||
let random = Math:gen_rng(\`{THIS_ID}{USER_ID}{Date:year()}{Date:month()}{Date:day()}\`)
|
||||
|
||||
// ランダムに選択肢を選ぶ
|
||||
let chosen = choices[random(0 (choices.len - 1))]
|
||||
let chosen = choices[random(0, (choices.len - 1))]
|
||||
|
||||
// 結果のテキスト
|
||||
let result = \`今日のあなたの運勢は **{chosen}** です。\`
|
||||
@@ -108,7 +109,7 @@ Ui:render([
|
||||
])
|
||||
`;
|
||||
|
||||
const PRESET_SHUFFLE = `/// @ 0.16.0
|
||||
const PRESET_SHUFFLE = `/// @ 0.18.0
|
||||
// 巻き戻し可能な文字シャッフルのプリセット
|
||||
|
||||
let string = "ペペロンチーノ"
|
||||
@@ -122,13 +123,13 @@ var cursor = 0
|
||||
|
||||
@do() {
|
||||
if (cursor != 0) {
|
||||
results = results.slice(0 (cursor + 1))
|
||||
results = results.slice(0, (cursor + 1))
|
||||
cursor = 0
|
||||
}
|
||||
|
||||
let chars = []
|
||||
for (let i, length) {
|
||||
let r = Math:rnd(0 (length - 1))
|
||||
let r = Math:rnd(0, (length - 1))
|
||||
chars.push(string.pick(r))
|
||||
}
|
||||
let result = chars.join("")
|
||||
@@ -162,11 +163,11 @@ var cursor = 0
|
||||
text: "←"
|
||||
disabled: !(results.len > 1 && (results.len - cursor) > 1)
|
||||
onClick: back
|
||||
} {
|
||||
}, {
|
||||
text: "→"
|
||||
disabled: !(results.len > 1 && cursor > 0)
|
||||
onClick: forward
|
||||
} {
|
||||
}, {
|
||||
text: "引き直す"
|
||||
onClick: do
|
||||
}]
|
||||
@@ -187,27 +188,27 @@ var cursor = 0
|
||||
do()
|
||||
`;
|
||||
|
||||
const PRESET_QUIZ = `/// @ 0.16.0
|
||||
const PRESET_QUIZ = `/// @ 0.18.0
|
||||
let title = '地理クイズ'
|
||||
|
||||
let qas = [{
|
||||
q: 'オーストラリアの首都は?'
|
||||
choices: ['シドニー' 'キャンベラ' 'メルボルン']
|
||||
choices: ['シドニー', 'キャンベラ', 'メルボルン']
|
||||
a: 'キャンベラ'
|
||||
aDescription: '最大の都市はシドニーですが首都はキャンベラです。'
|
||||
} {
|
||||
}, {
|
||||
q: '国土面積2番目の国は?'
|
||||
choices: ['カナダ' 'アメリカ' '中国']
|
||||
choices: ['カナダ', 'アメリカ', '中国']
|
||||
a: 'カナダ'
|
||||
aDescription: '大きい順にロシア、カナダ、アメリカ、中国です。'
|
||||
} {
|
||||
}, {
|
||||
q: '二重内陸国ではないのは?'
|
||||
choices: ['リヒテンシュタイン' 'ウズベキスタン' 'レソト']
|
||||
choices: ['リヒテンシュタイン', 'ウズベキスタン', 'レソト']
|
||||
a: 'レソト'
|
||||
aDescription: 'レソトは(一重)内陸国です。'
|
||||
} {
|
||||
}, {
|
||||
q: '閘門がない運河は?'
|
||||
choices: ['キール運河' 'スエズ運河' 'パナマ運河']
|
||||
choices: ['キール運河', 'スエズ運河', 'パナマ運河']
|
||||
a: 'スエズ運河'
|
||||
aDescription: 'スエズ運河は高低差がないので閘門はありません。'
|
||||
}]
|
||||
@@ -243,9 +244,9 @@ each (let qa, qas) {
|
||||
})
|
||||
Ui:C:container({
|
||||
children: []
|
||||
} \`{qa.id}:a\`)
|
||||
}, \`{qa.id}:a\`)
|
||||
]
|
||||
} qa.id))
|
||||
}, qa.id))
|
||||
}
|
||||
|
||||
@finish() {
|
||||
@@ -295,12 +296,12 @@ qaEls.push(Ui:C:container({
|
||||
onClick: finish
|
||||
})
|
||||
]
|
||||
} 'footer'))
|
||||
}, 'footer'))
|
||||
|
||||
Ui:render(qaEls)
|
||||
`;
|
||||
|
||||
const PRESET_TIMELINE = `/// @ 0.16.0
|
||||
const PRESET_TIMELINE = `/// @ 0.18.0
|
||||
// APIリクエストを行いローカルタイムラインを表示するプリセット
|
||||
|
||||
@fetch() {
|
||||
@@ -314,7 +315,7 @@ const PRESET_TIMELINE = `/// @ 0.16.0
|
||||
])
|
||||
|
||||
// タイムライン取得
|
||||
let notes = Mk:api("notes/local-timeline" {})
|
||||
let notes = Mk:api("notes/local-timeline", {})
|
||||
|
||||
// それぞれのノートごとにUI要素作成
|
||||
let noteEls = []
|
||||
@@ -367,7 +368,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const flash = ref<Misskey.entities.Flash | null>(null);
|
||||
const visibility = ref<Misskey.entities.FlashUpdateRequest['visibility']>('public');
|
||||
const visibility = ref<'private' | 'public'>('public');
|
||||
|
||||
if (props.id) {
|
||||
flash.value = await misskeyApi('flash/show', {
|
||||
@@ -420,6 +421,7 @@ async function save() {
|
||||
summary: summary.value,
|
||||
permissions: permissions.value,
|
||||
script: script.value,
|
||||
visibility: visibility.value,
|
||||
});
|
||||
router.push('/play/' + created.id + '/edit');
|
||||
}
|
||||
@@ -452,9 +454,7 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => flash.value ? {
|
||||
title: i18n.ts._play.edit + ': ' + flash.value.title,
|
||||
} : {
|
||||
title: i18n.ts._play.new,
|
||||
definePageMetadata(() => ({
|
||||
title: flash.value ? `${i18n.ts._play.edit}: ${flash.value.title}` : i18n.ts._play.new,
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -90,8 +90,8 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-heart',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: 'Play',
|
||||
icon: 'ti ti-player-play',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -15,11 +15,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkAsUi v-if="root" :component="root" :components="components"/>
|
||||
</div>
|
||||
<div class="actions _panel">
|
||||
<MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
|
||||
<MkButton v-tooltip="i18n.ts.shareWithNote" class="button" rounded @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></MkButton>
|
||||
<MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ti ti-link ti-fw"></i></MkButton>
|
||||
<MkButton v-if="isSupportShare()" v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton>
|
||||
<div class="items">
|
||||
<MkButton v-tooltip="i18n.ts.reload" class="button" rounded @click="reset"><i class="ti ti-reload"></i></MkButton>
|
||||
</div>
|
||||
<div class="items">
|
||||
<MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ti ti-heart"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
|
||||
<MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ti ti-link ti-fw"></i></MkButton>
|
||||
<MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.ready">
|
||||
@@ -49,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--accent);">{{ i18n.ts._play.editThisPage }}</MkA>
|
||||
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetchPage()"/>
|
||||
<MkError v-else-if="error" @retry="fetchFlash()"/>
|
||||
<MkLoading v-else/>
|
||||
</Transition>
|
||||
</MkSpacer>
|
||||
@@ -94,12 +98,33 @@ function fetchFlash() {
|
||||
});
|
||||
}
|
||||
|
||||
function share(ev: MouseEvent) {
|
||||
if (!flash.value) return;
|
||||
|
||||
os.popupMenu([
|
||||
{
|
||||
text: i18n.ts.shareWithNote,
|
||||
icon: 'ti ti-pencil',
|
||||
action: shareWithNote,
|
||||
},
|
||||
...(isSupportShare() ? [{
|
||||
text: i18n.ts.share,
|
||||
icon: 'ti ti-share',
|
||||
action: shareWithNavigator,
|
||||
}] : []),
|
||||
], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
if (!flash.value) return;
|
||||
|
||||
copyToClipboard(`${url}/play/${flash.value.id}`);
|
||||
os.success();
|
||||
}
|
||||
|
||||
function share() {
|
||||
function shareWithNavigator() {
|
||||
if (!flash.value) return;
|
||||
|
||||
navigator.share({
|
||||
title: flash.value.title,
|
||||
text: flash.value.summary,
|
||||
@@ -108,21 +133,28 @@ function share() {
|
||||
}
|
||||
|
||||
function shareWithNote() {
|
||||
if (!flash.value) return;
|
||||
|
||||
os.post({
|
||||
initialText: `${flash.value.title} ${url}/play/${flash.value.id}`,
|
||||
initialText: `${flash.value.title}\n${url}/play/${flash.value.id}`,
|
||||
instant: true,
|
||||
});
|
||||
}
|
||||
|
||||
function like() {
|
||||
if (!flash.value) return;
|
||||
|
||||
os.apiWithDialog('flash/like', {
|
||||
flashId: flash.value.id,
|
||||
}).then(() => {
|
||||
flash.value.isLiked = true;
|
||||
flash.value.likedCount++;
|
||||
flash.value!.isLiked = true;
|
||||
flash.value!.likedCount++;
|
||||
});
|
||||
}
|
||||
|
||||
async function unlike() {
|
||||
if (!flash.value) return;
|
||||
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unlikeConfirm,
|
||||
@@ -131,8 +163,8 @@ async function unlike() {
|
||||
os.apiWithDialog('flash/unlike', {
|
||||
flashId: flash.value.id,
|
||||
}).then(() => {
|
||||
flash.value.isLiked = false;
|
||||
flash.value.likedCount--;
|
||||
flash.value!.isLiked = false;
|
||||
flash.value!.likedCount--;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,6 +184,7 @@ function start() {
|
||||
|
||||
async function run() {
|
||||
if (aiscript.value) aiscript.value.abort();
|
||||
if (!flash.value) return;
|
||||
|
||||
aiscript.value = new Interpreter({
|
||||
...createAiScriptEnv({
|
||||
@@ -193,27 +226,34 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
onDeactivated(() => {
|
||||
function reset() {
|
||||
if (aiscript.value) aiscript.value.abort();
|
||||
started.value = false;
|
||||
}
|
||||
|
||||
onDeactivated(() => {
|
||||
reset();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (aiscript.value) aiscript.value.abort();
|
||||
reset();
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => flash.value ? {
|
||||
title: flash.value.title,
|
||||
avatar: flash.value.user,
|
||||
path: `/play/${flash.value.id}`,
|
||||
share: {
|
||||
title: flash.value.title,
|
||||
text: flash.value.summary,
|
||||
},
|
||||
} : null));
|
||||
definePageMetadata(() => ({
|
||||
title: flash.value ? flash.value.title : 'Play',
|
||||
...flash.value ? {
|
||||
avatar: flash.value.user,
|
||||
path: `/play/${flash.value.id}`,
|
||||
share: {
|
||||
title: flash.value.title,
|
||||
text: flash.value.summary,
|
||||
},
|
||||
} : {},
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@@ -263,11 +303,19 @@ definePageMetadata(computed(() => flash.value ? {
|
||||
}
|
||||
|
||||
> .actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
|
||||
> .items {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--divider);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.followRequests,
|
||||
icon: 'ti ti-user-plus',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -122,11 +122,8 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => props.postId ? {
|
||||
title: i18n.ts.edit,
|
||||
icon: 'ti ti-pencil',
|
||||
} : {
|
||||
title: i18n.ts.postToGallery,
|
||||
definePageMetadata(() => ({
|
||||
title: props.postId ? i18n.ts.edit : i18n.ts.postToGallery,
|
||||
icon: 'ti ti-pencil',
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -119,10 +119,10 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-edit',
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'ti ti-icons',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -163,10 +163,12 @@ const headerActions = computed(() => [{
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => post.value ? {
|
||||
title: post.value.title,
|
||||
avatar: post.value.user,
|
||||
} : null));
|
||||
definePageMetadata(() => ({
|
||||
title: post.value ? post.value.title : i18n.ts.gallery,
|
||||
...post.value ? {
|
||||
avatar: post.value.user,
|
||||
} : {},
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -32,8 +32,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: 'Misskey Games',
|
||||
icon: 'ti ti-device-gamepad',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -312,10 +312,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts._externalResourceInstaller.title,
|
||||
icon: 'ti ti-download',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -35,10 +35,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<FormSection v-if="iAmModerator">
|
||||
<template #label>Moderation</template>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
|
||||
<MkKeyValue>
|
||||
<template #key>
|
||||
{{ i18n.ts._delivery.status }}
|
||||
</template>
|
||||
<template #value>
|
||||
{{ i18n.ts._delivery._type[suspensionState] }}
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkButton v-if="suspensionState === 'none'" :disabled="!instance" danger @click="stopDelivery">{{ i18n.ts._delivery.stop }}</MkButton>
|
||||
<MkButton v-if="suspensionState !== 'none'" :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton>
|
||||
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
||||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
||||
<MkTextarea v-model="moderationNote" manualSave>
|
||||
<template #label>{{ i18n.ts.moderationNote }}</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
@@ -119,7 +131,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkChart from '@/components/MkChart.vue';
|
||||
import MkObjectView from '@/components/MkObjectView.vue';
|
||||
@@ -141,6 +153,7 @@ import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
|
||||
import { dateString } from '@/filters/date.js';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
host: string;
|
||||
@@ -151,10 +164,11 @@ const tab = ref('overview');
|
||||
const chartSrc = ref('instance-requests');
|
||||
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
|
||||
const instance = ref<Misskey.entities.FederationInstance | null>(null);
|
||||
const suspended = ref(false);
|
||||
const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none');
|
||||
const isBlocked = ref(false);
|
||||
const isSilenced = ref(false);
|
||||
const faviconUrl = ref<string | null>(null);
|
||||
const moderationNote = ref('');
|
||||
|
||||
const usersPagination = {
|
||||
endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
|
||||
@@ -167,6 +181,10 @@ const usersPagination = {
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
watch(moderationNote, async () => {
|
||||
await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value });
|
||||
});
|
||||
|
||||
async function fetch(): Promise<void> {
|
||||
if (iAmAdmin) {
|
||||
meta.value = await misskeyApi('admin/meta');
|
||||
@@ -174,10 +192,11 @@ async function fetch(): Promise<void> {
|
||||
instance.value = await misskeyApi('federation/show-instance', {
|
||||
host: props.host,
|
||||
});
|
||||
suspended.value = instance.value?.isSuspended ?? false;
|
||||
suspensionState.value = instance.value?.suspensionState ?? 'none';
|
||||
isBlocked.value = instance.value?.isBlocked ?? false;
|
||||
isSilenced.value = instance.value?.isSilenced ?? false;
|
||||
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
|
||||
moderationNote.value = instance.value?.moderationNote;
|
||||
}
|
||||
|
||||
async function toggleBlock(): Promise<void> {
|
||||
@@ -199,11 +218,21 @@ async function toggleSilenced(): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleSuspend(): Promise<void> {
|
||||
async function stopDelivery(): Promise<void> {
|
||||
if (!instance.value) throw new Error('No instance?');
|
||||
suspensionState.value = 'manuallySuspended';
|
||||
await misskeyApi('admin/federation/update-instance', {
|
||||
host: instance.value.host,
|
||||
isSuspended: suspended.value,
|
||||
isSuspended: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function resumeDelivery(): Promise<void> {
|
||||
if (!instance.value) throw new Error('No instance?');
|
||||
suspensionState.value = 'none';
|
||||
await misskeyApi('admin/federation/update-instance', {
|
||||
host: instance.value.host,
|
||||
isSuspended: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -245,10 +274,10 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-code',
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: props.host,
|
||||
icon: 'ti ti-server',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -93,10 +93,10 @@ async function update() {
|
||||
|
||||
update();
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.invite,
|
||||
icon: 'ti ti-user-plus',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -101,10 +101,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => list.value ? {
|
||||
title: list.value.name,
|
||||
definePageMetadata(() => ({
|
||||
title: list.value ? list.value.name : i18n.ts.lists,
|
||||
icon: 'ti ti-list',
|
||||
} : null));
|
||||
}));
|
||||
</script>
|
||||
<style lang="scss" module>
|
||||
.main {
|
||||
|
||||
@@ -93,10 +93,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: 'MiAuth',
|
||||
icon: 'ti ti-apps',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -26,6 +26,7 @@ const draft = ref({
|
||||
users: [],
|
||||
keywords: [],
|
||||
excludeKeywords: [],
|
||||
excludeBots: false,
|
||||
withReplies: false,
|
||||
caseSensitive: false,
|
||||
localOnly: false,
|
||||
@@ -38,8 +39,8 @@ function onAntennaCreated() {
|
||||
router.push('/my/antennas');
|
||||
}
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'ti ti-antenna',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -36,8 +36,8 @@ misskeyApi('antennas/show', { antennaId: props.antennaId }).then((antennaRespons
|
||||
antenna.value = antennaResponse;
|
||||
});
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'ti ti-antenna',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<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="excludeBots">{{ i18n.ts.antennaExcludeBots }}</MkSwitch>
|
||||
<MkSwitch v-model="withReplies">{{ i18n.ts.withReplies }}</MkSwitch>
|
||||
<MkTextarea v-model="keywords">
|
||||
<template #label>{{ i18n.ts.antennaKeywords }}</template>
|
||||
@@ -38,11 +39,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
|
||||
<MkSwitch v-model="caseSensitive">{{ i18n.ts.caseSensitive }}</MkSwitch>
|
||||
<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
|
||||
<MkSwitch v-model="notify">{{ i18n.ts.notifyAntenna }}</MkSwitch>
|
||||
</div>
|
||||
<div :class="$style.actions">
|
||||
<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
<div class="_buttons">
|
||||
<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
@@ -78,9 +80,9 @@ const keywords = ref<string>(props.antenna.keywords.map(x => x.join(' ')).join('
|
||||
const excludeKeywords = ref<string>(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
|
||||
const caseSensitive = ref<boolean>(props.antenna.caseSensitive);
|
||||
const localOnly = ref<boolean>(props.antenna.localOnly);
|
||||
const excludeBots = ref<boolean>(props.antenna.excludeBots);
|
||||
const withReplies = ref<boolean>(props.antenna.withReplies);
|
||||
const withFile = ref<boolean>(props.antenna.withFile);
|
||||
const notify = ref<boolean>(props.antenna.notify);
|
||||
const userLists = ref<Misskey.entities.UserList[] | null>(null);
|
||||
|
||||
watch(() => src.value, async () => {
|
||||
@@ -94,9 +96,9 @@ async function saveAntenna() {
|
||||
name: name.value,
|
||||
src: src.value,
|
||||
userListId: userListId.value,
|
||||
excludeBots: excludeBots.value,
|
||||
withReplies: withReplies.value,
|
||||
withFile: withFile.value,
|
||||
notify: notify.value,
|
||||
caseSensitive: caseSensitive.value,
|
||||
localOnly: localOnly.value,
|
||||
users: users.value.trim().split('\n').map(x => x.trim()),
|
||||
|
||||
@@ -55,10 +55,10 @@ const headerActions = computed(() => [{
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'ti ti-antenna',
|
||||
});
|
||||
}));
|
||||
|
||||
onActivated(() => {
|
||||
antennasCache.fetch();
|
||||
|
||||
@@ -11,16 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="tab === 'my'" key="my" class="_gaps">
|
||||
<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps">
|
||||
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`">
|
||||
<MkClipPreview :clip="item"/>
|
||||
</MkA>
|
||||
<MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps">
|
||||
<MkClipPreview v-for="item in items" :key="item.id" :clip="item"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'favorites'" key="favorites" class="_gaps">
|
||||
<MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`">
|
||||
<MkClipPreview :clip="item"/>
|
||||
</MkA>
|
||||
<MkClipPreview v-for="item in favorites" :key="item.id" :clip="item"/>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
@@ -104,14 +100,10 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-heart',
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.clip,
|
||||
icon: 'ti ti-paperclip',
|
||||
action: {
|
||||
icon: 'ti ti-plus',
|
||||
handler: create,
|
||||
},
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -71,10 +71,10 @@ const headerActions = computed(() => [{
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.manageLists,
|
||||
icon: 'ti ti-list',
|
||||
});
|
||||
}));
|
||||
|
||||
onActivated(() => {
|
||||
fetch();
|
||||
|
||||
@@ -186,10 +186,10 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => list.value ? {
|
||||
title: list.value.name,
|
||||
definePageMetadata(() => ({
|
||||
title: list.value ? list.value.name : i18n.ts.lists,
|
||||
icon: 'ti ti-list',
|
||||
} : null));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -31,8 +31,8 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.notFound,
|
||||
icon: 'ti ti-alert-triangle',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -21,14 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<div class="_margin _gaps_s">
|
||||
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
|
||||
<MkNoteDetailed :key="note.id" v-model:note="note" :class="$style.note"/>
|
||||
<MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note"/>
|
||||
</div>
|
||||
<div v-if="clips && clips.length > 0" class="_margin">
|
||||
<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
|
||||
<div class="_gaps">
|
||||
<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`">
|
||||
<MkClipPreview :clip="item"/>
|
||||
</MkA>
|
||||
<MkClipPreview v-for="item in clips" :key="item.id" :clip="item"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!showPrev" class="_buttons" :class="$style.loadPrev">
|
||||
@@ -66,6 +64,7 @@ import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
noteId: string;
|
||||
initialTab?: string;
|
||||
}>();
|
||||
|
||||
const note = ref<null | Misskey.entities.Note>();
|
||||
@@ -141,16 +140,18 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => note.value ? {
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.note,
|
||||
subtitle: dateString(note.value.createdAt),
|
||||
avatar: note.value.user,
|
||||
path: `/notes/${note.value.id}`,
|
||||
share: {
|
||||
title: i18n.tsx.noteOf({ user: note.value.user.name }),
|
||||
text: note.value.text,
|
||||
},
|
||||
} : null));
|
||||
...note.value ? {
|
||||
subtitle: dateString(note.value.createdAt),
|
||||
avatar: note.value.user,
|
||||
path: `/notes/${note.value.id}`,
|
||||
share: {
|
||||
title: i18n.tsx.noteOf({ user: note.value.user.name }),
|
||||
text: note.value.text,
|
||||
},
|
||||
} : {},
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -52,7 +52,7 @@ const directNotesPagination = {
|
||||
function setFilter(ev) {
|
||||
const typeItems = notificationTypes.map(t => ({
|
||||
text: i18n.ts._notification._types[t],
|
||||
active: includeTypes.value && includeTypes.value.includes(t),
|
||||
active: (includeTypes.value && includeTypes.value.includes(t)) ?? false,
|
||||
action: () => {
|
||||
includeTypes.value = [t];
|
||||
},
|
||||
@@ -63,7 +63,7 @@ function setFilter(ev) {
|
||||
action: () => {
|
||||
includeTypes.value = null;
|
||||
},
|
||||
}, { type: 'divider' }, ...typeItems] : typeItems;
|
||||
}, { type: 'divider' as const }, ...typeItems] : typeItems;
|
||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
@@ -94,10 +94,10 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-mail',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.notifications,
|
||||
icon: 'ti ti-bell',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
|
||||
@@ -51,10 +51,10 @@ function onLogin(res): void {
|
||||
login(res.i);
|
||||
}
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: 'OAuth',
|
||||
icon: 'ti ti-apps',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
|
||||
|
||||
<section style="padding: 0 16px 0 16px;">
|
||||
<section style="padding: 16px;" class="_gaps_s">
|
||||
<MkInput v-model="id">
|
||||
<template #label>{{ i18n.ts._pages.blocks._note.id }}</template>
|
||||
<template #caption>{{ i18n.ts._pages.blocks._note.idDescription }}</template>
|
||||
|
||||
@@ -284,17 +284,11 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-note',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => {
|
||||
let title = i18n.ts._pages.newPage;
|
||||
if (props.initPageId) {
|
||||
title = i18n.ts._pages.editPage;
|
||||
} else if (props.initPageName && props.initUser) {
|
||||
title = i18n.ts._pages.readPage;
|
||||
}
|
||||
return {
|
||||
title: title,
|
||||
icon: 'ti ti-pencil',
|
||||
};
|
||||
definePageMetadata(() => ({
|
||||
title: props.initPageId ? i18n.ts._pages.editPage
|
||||
: props.initPageName && props.initUser ? i18n.ts._pages.readPage
|
||||
: i18n.ts._pages.newPage,
|
||||
icon: 'ti ti-pencil',
|
||||
}));
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,48 +6,80 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="700">
|
||||
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="page" :key="page.id" class="xcukqgmh">
|
||||
<div class="main">
|
||||
<!--
|
||||
<div class="header">
|
||||
<h1>{{ page.title }}</h1>
|
||||
</div>
|
||||
-->
|
||||
<div class="banner">
|
||||
<MkMediaImage
|
||||
v-if="page.eyeCatchingImageId"
|
||||
:image="page.eyeCatchingImage"
|
||||
:cover="true"
|
||||
:disableImageLink="true"
|
||||
class="thumbnail"
|
||||
/>
|
||||
<MkSpacer :contentMax="800">
|
||||
<Transition
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
|
||||
mode="out-in"
|
||||
>
|
||||
<div v-if="page" :key="page.id" class="_gaps">
|
||||
<div :class="$style.pageMain">
|
||||
<div :class="$style.pageBanner">
|
||||
<div :class="$style.pageBannerBgRoot">
|
||||
<MkImgWithBlurhash
|
||||
v-if="page.eyeCatchingImageId"
|
||||
:class="$style.pageBannerBg"
|
||||
:hash="page.eyeCatchingImage?.blurhash"
|
||||
:cover="true"
|
||||
:forceBlurhash="true"
|
||||
/>
|
||||
<img
|
||||
v-else-if="instance.backgroundImageUrl || instance.bannerUrl"
|
||||
:class="[$style.pageBannerBg, $style.pageBannerBgFallback1]"
|
||||
:src="getStaticImageUrl(instance.backgroundImageUrl ?? instance.bannerUrl!)"
|
||||
/>
|
||||
<div v-else :class="[$style.pageBannerBg, $style.pageBannerBgFallback2]"></div>
|
||||
</div>
|
||||
<div v-if="page.eyeCatchingImageId" :class="$style.pageBannerImage">
|
||||
<MkMediaImage
|
||||
:image="page.eyeCatchingImage!"
|
||||
:cover="true"
|
||||
:disableImageLink="true"
|
||||
:class="$style.thumbnail"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.pageBannerTitle" class="_gaps_s">
|
||||
<h1>{{ page.title || page.name }}</h1>
|
||||
<div :class="$style.pageBannerTitleSub">
|
||||
<div v-if="page.user" :class="$style.pageBannerTitleUser">
|
||||
<MkAvatar :user="page.user" :class="$style.avatar" indicator link preview/> <MkA :to="`/@${username}`"><MkUserName :user="page.user" :nowrap="false"/></MkA>
|
||||
</div>
|
||||
<div :class="$style.pageBannerTitleSubActions">
|
||||
<MkA v-if="page.userId === $i?.id" v-tooltip="i18n.ts._pages.editThisPage" :to="`/pages/edit/${page.id}`" class="_button" :class="$style.generalActionButton"><i class="ti ti-pencil ti-fw"></i></MkA>
|
||||
<button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div :class="$style.pageContent">
|
||||
<XPage :page="page"/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="like">
|
||||
<div :class="$style.pageActions">
|
||||
<div>
|
||||
<MkButton v-if="page.isLiked" v-tooltip="i18n.ts._pages.unlike" class="button" asLike primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
|
||||
</div>
|
||||
<div class="other">
|
||||
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
|
||||
<button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ti ti-share ti-fw"></i></button>
|
||||
<div :class="$style.other">
|
||||
<button v-tooltip="i18n.ts.copyLink" class="_button" :class="$style.generalActionButton" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="page.user" class="avatar" link preview/>
|
||||
<div class="name">
|
||||
<MkUserName :user="page.user" style="display: block;"/>
|
||||
<MkAcct :user="page.user"/>
|
||||
</div>
|
||||
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
<div :class="$style.pageUser">
|
||||
<MkAvatar :user="page.user" :class="$style.avatar" link preview/>
|
||||
<MkA :to="`/@${username}`">
|
||||
<MkUserName :user="page.user" :class="$style.name"/>
|
||||
<MkAcct :user="page.user" :class="$style.acct"/>
|
||||
</MkA>
|
||||
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user!" :inline="true" :transparent="false" :full="true" :class="$style.follow"/>
|
||||
</div>
|
||||
<div class="links">
|
||||
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
|
||||
<div :class="$style.pageDate">
|
||||
<div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
|
||||
<div v-if="page.createdAt != page.updatedAt"><i class="ti ti-clock-edit"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
|
||||
</div>
|
||||
<div :class="$style.pageLinks">
|
||||
<MkA v-if="!$i || $i.id !== page.userId" :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">{{ i18n.ts._pages.editThisPage }}</MkA>
|
||||
<button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ i18n.ts.unpin }}</button>
|
||||
@@ -55,10 +87,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
|
||||
<div v-if="page.createdAt != page.updatedAt"><i class="ti ti-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 #icon><i class="ti ti-clock"></i></template>
|
||||
@@ -84,6 +112,7 @@ import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { url } from '@/config.js';
|
||||
import MkMediaImage from '@/components/MkMediaImage.vue';
|
||||
import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
@@ -94,6 +123,8 @@ import { pageViewInterruptors, defaultStore } from '@/store.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -133,35 +164,63 @@ function fetchPage() {
|
||||
});
|
||||
}
|
||||
|
||||
function share() {
|
||||
navigator.share({
|
||||
title: page.value.title ?? page.value.name,
|
||||
text: page.value.summary,
|
||||
url: `${url}/@${page.value.user.username}/pages/${page.value.name}`,
|
||||
});
|
||||
function share(ev: MouseEvent) {
|
||||
if (!page.value) return;
|
||||
|
||||
os.popupMenu([
|
||||
{
|
||||
text: i18n.ts.shareWithNote,
|
||||
icon: 'ti ti-pencil',
|
||||
action: shareWithNote,
|
||||
},
|
||||
...(isSupportShare() ? [{
|
||||
text: i18n.ts.share,
|
||||
icon: 'ti ti-share',
|
||||
action: shareWithNavigator,
|
||||
}] : []),
|
||||
], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
if (!page.value) return;
|
||||
|
||||
copyToClipboard(`${url}/@${page.value.user.username}/pages/${page.value.name}`);
|
||||
os.success();
|
||||
}
|
||||
|
||||
function shareWithNote() {
|
||||
if (!page.value) return;
|
||||
|
||||
os.post({
|
||||
initialText: `${page.value.title || page.value.name} ${url}/@${page.value.user.username}/pages/${page.value.name}`,
|
||||
initialText: `${page.value.title || page.value.name}\n${url}/@${page.value.user.username}/pages/${page.value.name}`,
|
||||
instant: true,
|
||||
});
|
||||
}
|
||||
|
||||
function shareWithNavigator() {
|
||||
if (!page.value) return;
|
||||
|
||||
navigator.share({
|
||||
title: page.value.title ?? page.value.name,
|
||||
text: page.value.summary ?? undefined,
|
||||
url: `${url}/@${page.value.user.username}/pages/${page.value.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
function like() {
|
||||
if (!page.value) return;
|
||||
|
||||
os.apiWithDialog('pages/like', {
|
||||
pageId: page.value.id,
|
||||
}).then(() => {
|
||||
page.value.isLiked = true;
|
||||
page.value.likedCount++;
|
||||
page.value!.isLiked = true;
|
||||
page.value!.likedCount++;
|
||||
});
|
||||
}
|
||||
|
||||
async function unlike() {
|
||||
if (!page.value) return;
|
||||
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unlikeConfirm,
|
||||
@@ -170,12 +229,14 @@ async function unlike() {
|
||||
os.apiWithDialog('pages/unlike', {
|
||||
pageId: page.value.id,
|
||||
}).then(() => {
|
||||
page.value.isLiked = false;
|
||||
page.value.likedCount--;
|
||||
page.value!.isLiked = false;
|
||||
page.value!.likedCount--;
|
||||
});
|
||||
}
|
||||
|
||||
function pin(pin) {
|
||||
if (!page.value) return;
|
||||
|
||||
os.apiWithDialog('i/update', {
|
||||
pinnedPageId: pin ? page.value.id : null,
|
||||
});
|
||||
@@ -187,120 +248,213 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => page.value ? {
|
||||
title: page.value.title || page.value.name,
|
||||
avatar: page.value.user,
|
||||
path: `/@${page.value.user.username}/pages/${page.value.name}`,
|
||||
share: {
|
||||
title: page.value.title || page.value.name,
|
||||
text: page.value.summary,
|
||||
},
|
||||
} : null));
|
||||
definePageMetadata(() => ({
|
||||
title: page.value ? page.value.title || page.value.name : i18n.ts.pages,
|
||||
...page.value ? {
|
||||
avatar: page.value.user,
|
||||
path: `/@${page.value.user.username}/pages/${page.value.name}`,
|
||||
share: {
|
||||
title: page.value.title || page.value.name,
|
||||
text: page.value.summary,
|
||||
},
|
||||
} : {},
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
<style lang="scss" module>
|
||||
.fadeEnterActive,
|
||||
.fadeLeaveActive {
|
||||
transition: opacity 0.125s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
.fadeEnterFrom,
|
||||
.fadeLeaveTo {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.xcukqgmh {
|
||||
> .main {
|
||||
padding: 32px;
|
||||
.generalActionButton {
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
text-align: center;
|
||||
border-radius: 99rem;
|
||||
|
||||
> .header {
|
||||
padding: 16px;
|
||||
|
||||
> h1 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> .banner {
|
||||
> .thumbnail {
|
||||
// TODO: 良い感じのアスペクト比で表示
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 3/1;
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
> .content {
|
||||
margin-top: 16px;
|
||||
padding: 16px 0 0 0;
|
||||
}
|
||||
|
||||
> .actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
padding: 16px 0 0 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
|
||||
> .other {
|
||||
margin-left: auto;
|
||||
|
||||
> button {
|
||||
padding: 8px;
|
||||
margin: 0 8px;
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .user {
|
||||
margin-top: 16px;
|
||||
padding: 16px 0 0 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
> .name {
|
||||
margin: 0 0 0 12px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
> .koudoku {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .links {
|
||||
margin-top: 16px;
|
||||
padding: 24px 0 0 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
|
||||
> .link {
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
}
|
||||
& :global(.ti) {
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
> .footer {
|
||||
margin: var(--margin) 0 var(--margin) 0;
|
||||
font-size: 85%;
|
||||
opacity: 0.75;
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style module>
|
||||
.pageMain {
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem;
|
||||
background: var(--panel);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pageBanner {
|
||||
width: calc(100% + 4rem);
|
||||
margin: -2rem -2rem 1.5rem;
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
> .pageBannerBgRoot {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.pageBannerBg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: .2;
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
.pageBannerBgFallback1 {
|
||||
filter: blur(20px);
|
||||
}
|
||||
|
||||
.pageBannerBgFallback2 {
|
||||
background-color: var(--accentedBg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background: linear-gradient(0deg, var(--panel), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
> .pageBannerImage {
|
||||
position: relative;
|
||||
padding-top: 56.25%;
|
||||
|
||||
> .thumbnail {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> .pageBannerTitle {
|
||||
position: relative;
|
||||
padding: 1.5rem 2rem;
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--fg);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pageBannerTitleSub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pageBannerTitleUser {
|
||||
--height: 32px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar {
|
||||
height: var(--height);
|
||||
width: var(--height);
|
||||
}
|
||||
|
||||
line-height: var(--height);
|
||||
}
|
||||
|
||||
.pageBannerTitleSubActions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--marginHalf);
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pageContent {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.pageActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
border-top: 1px solid var(--divider);
|
||||
padding-top: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
> .other {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: var(--marginHalf);
|
||||
}
|
||||
}
|
||||
|
||||
.pageUser {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
border-top: 1px solid var(--divider);
|
||||
padding-top: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.avatar,
|
||||
.name,
|
||||
.acct {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 110%;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.acct {
|
||||
font-size: 90%;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.follow {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.pageDate {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.pageLinks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--marginHalf);
|
||||
}
|
||||
|
||||
.relatedPagesRoot {
|
||||
padding: var(--margin);
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ const headerTabs = computed(() => [{
|
||||
icon: 'ti ti-heart',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.pages,
|
||||
icon: 'ti ti-note',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
26
packages/frontend/src/pages/preview.vue
Normal file
26
packages/frontend/src/pages/preview.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MkSample/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkSample from '@/components/MkPreview.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.preview,
|
||||
icon: 'ti ti-eye',
|
||||
})));
|
||||
</script>
|
||||
@@ -96,8 +96,8 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.registry,
|
||||
icon: 'ti ti-adjustments',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -123,8 +123,8 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.registry,
|
||||
icon: 'ti ti-adjustments',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -73,8 +73,8 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.registry,
|
||||
icon: 'ti ti-adjustments',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -53,8 +53,8 @@ const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.resetPassword,
|
||||
icon: 'ti ti-lock',
|
||||
});
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.board">
|
||||
<div class="_woodenFrame">
|
||||
<div :class="$style.boardInner">
|
||||
<div v-if="showBoardLabels" :class="$style.labelsX">
|
||||
<span v-for="i in game.map[0].length" :key="i" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
|
||||
@@ -124,8 +124,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.options }}</template>
|
||||
<div class="_gaps_s" style="text-align: left;">
|
||||
<MkSwitch v-model="showBoardLabels">Show labels</MkSwitch>
|
||||
<MkSwitch v-model="useAvatarAsStone">useAvatarAsStone</MkSwitch>
|
||||
<MkSwitch v-model="showBoardLabels">{{ i18n.ts._reversi.showBoardLabels }}</MkSwitch>
|
||||
<MkSwitch v-model="useAvatarAsStone">{{ i18n.ts._reversi.useAvatarAsStone }}</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
@@ -151,6 +151,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import { url } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
@@ -168,7 +169,7 @@ const props = defineProps<{
|
||||
const showBoardLabels = ref<boolean>(false);
|
||||
const useAvatarAsStone = ref<boolean>(true);
|
||||
const autoplaying = ref<boolean>(false);
|
||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const game = ref<Misskey.entities.ReversiGameDetailed & { logs: Reversi.Serializer.SerializedLog[] }>(deepClone(props.game));
|
||||
const logPos = ref<number>(game.value.logs.length);
|
||||
const engine = shallowRef<Reversi.Game>(Reversi.Serializer.restoreGame({
|
||||
@@ -248,7 +249,7 @@ if (game.value.isStarted && !game.value.isEnded) {
|
||||
crc32: crc32.toString(),
|
||||
}).then((res) => {
|
||||
if (res.desynced) {
|
||||
console.log('resynced');
|
||||
if (_DEV_) console.log('resynced');
|
||||
restoreGame(res.game!);
|
||||
}
|
||||
});
|
||||
@@ -442,7 +443,7 @@ function autoplay() {
|
||||
|
||||
function share() {
|
||||
os.post({
|
||||
initialText: `#MisskeyReversi ${location.href}`,
|
||||
initialText: `#MisskeyReversi\n${url}/reversi/g/${game.value.id}`,
|
||||
instant: true,
|
||||
});
|
||||
}
|
||||
@@ -500,17 +501,6 @@ $gap: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.board {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
|
||||
padding: 7px;
|
||||
background: #8C4F26;
|
||||
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.boardInner {
|
||||
padding: 32px;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useStream } from '@/stream.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import * as os from '@/os.js';
|
||||
import { url } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
|
||||
@@ -44,7 +45,7 @@ function start(_game: Misskey.entities.ReversiGameDetailed) {
|
||||
|
||||
if (shareWhenStart.value) {
|
||||
misskeyApi('notes/create', {
|
||||
text: i18n.ts._reversi.iStartedAGame + '\n' + location.href,
|
||||
text: `${i18n.ts._reversi.iStartedAGame}\n${url}/reversi/g/${props.gameId}`,
|
||||
visibility: 'home',
|
||||
});
|
||||
}
|
||||
@@ -113,8 +114,8 @@ onUnmounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePageMetadata(() => ({
|
||||
title: 'Reversi',
|
||||
icon: 'ti ti-device-gamepad',
|
||||
})));
|
||||
}));
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user