Merge branch 'develop' into mahjong

This commit is contained in:
syuilo
2024-07-02 10:10:01 +09:00
840 changed files with 44657 additions and 21435 deletions

View File

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

View File

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

View File

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

View File

@@ -48,10 +48,10 @@ onDeactivated(() => {
}
});
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.achievements,
icon: 'ti ti-medal',
});
}));
</script>
<style lang="scss" module>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -157,8 +157,8 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.announcements,
icon: 'ti ti-speakerphone',
});
}));
</script>

View File

@@ -142,7 +142,7 @@ function save() {
turnstileSiteKey: turnstileSiteKey.value,
turnstileSecretKey: turnstileSecretKey.value,
}).then(() => {
fetchInstance();
fetchInstance(true);
});
}
</script>

View File

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

View File

@@ -33,8 +33,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.database,
icon: 'ti ti-database',
});
}));
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,8 +68,8 @@ const headerTabs = computed(() => [{
title: 'Inbox',
}]);
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.jobQueue,
icon: 'ti ti-clock-play',
});
}));
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -87,8 +87,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
definePageMetadata(() => ({
title: 'API console',
icon: 'ti ti-terminal-2',
});
}));
</script>

View File

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

View File

@@ -94,8 +94,8 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.avatarDecorations,
icon: 'ti ti-sparkles',
});
}));
</script>

View File

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

View File

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

View File

@@ -152,8 +152,8 @@ const headerTabs = computed(() => [{
icon: 'ti ti-edit',
}]);
definePageMetadata(computed(() => ({
definePageMetadata(() => ({
title: i18n.ts.channel,
icon: 'ti ti-device-tv',
})));
}));
</script>

View File

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

View File

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

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

View File

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

View File

@@ -48,8 +48,8 @@ const headerTabs = computed(() => [{
icon: 'ti ti-pencil',
}]);
definePageMetadata(computed(() => ({
definePageMetadata(() => ({
title: i18n.ts._fileViewer.title,
icon: 'ti ti-file',
})));
}));
</script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,6 +29,9 @@ const paginationForPolls = {
endpoint: 'notes/polls/recommendation' as const,
limit: 10,
offsetMode: true,
params: {
excludeChannels: true,
},
};
const tab = ref('notes');

View File

@@ -60,8 +60,8 @@ const headerTabs = computed(() => [{
title: i18n.ts.roles,
}]);
definePageMetadata(computed(() => ({
definePageMetadata(() => ({
title: i18n.ts.explore,
icon: 'ti ti-hash',
})));
}));
</script>

View File

@@ -38,10 +38,10 @@ const pagination = {
limit: 10,
};
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.favorites,
icon: 'ti ti-star',
});
}));
</script>
<style lang="scss" module>

View File

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

View File

@@ -90,8 +90,8 @@ const headerTabs = computed(() => [{
icon: 'ti ti-heart',
}]);
definePageMetadata(computed(() => ({
definePageMetadata(() => ({
title: 'Play',
icon: 'ti ti-player-play',
})));
}));
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -93,10 +93,10 @@ async function update() {
update();
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.invite,
icon: 'ti ti-user-plus',
});
}));
</script>
<style lang="scss" module>

View File

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

View File

@@ -93,10 +93,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
definePageMetadata(() => ({
title: 'MiAuth',
icon: 'ti ti-apps',
});
}));
</script>
<style lang="scss" module>

View File

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

View File

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

View File

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

View File

@@ -55,10 +55,10 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.manageAntennas,
icon: 'ti ti-antenna',
});
}));
onActivated(() => {
antennasCache.fetch();

View File

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

View File

@@ -71,10 +71,10 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.manageLists,
icon: 'ti ti-list',
});
}));
onActivated(() => {
fetch();

View File

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

View File

@@ -31,8 +31,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.notFound,
icon: 'ti ti-alert-triangle',
});
}));
</script>

View File

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

View File

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

View File

@@ -51,10 +51,10 @@ function onLogin(res): void {
login(res.i);
}
definePageMetadata({
definePageMetadata(() => ({
title: 'OAuth',
icon: 'ti ti-apps',
});
}));
</script>
<style lang="scss" module>

View File

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

View File

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

View File

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

View File

@@ -88,8 +88,8 @@ const headerTabs = computed(() => [{
icon: 'ti ti-heart',
}]);
definePageMetadata(computed(() => ({
definePageMetadata(() => ({
title: i18n.ts.pages,
icon: 'ti ti-note',
})));
}));
</script>

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

View File

@@ -96,8 +96,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.registry,
icon: 'ti ti-adjustments',
});
}));
</script>

View File

@@ -123,8 +123,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.registry,
icon: 'ti ti-adjustments',
});
}));
</script>

View File

@@ -73,8 +73,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.registry,
icon: 'ti ti-adjustments',
});
}));
</script>

View File

@@ -53,8 +53,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
definePageMetadata(() => ({
title: i18n.ts.resetPassword,
icon: 'ti ti-lock',
});
}));
</script>

View File

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

View File

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