Merge remote-tracking branch 'misskey-original/develop' into develop
# Conflicts: # package.json # packages/frontend/package.json # packages/frontend/src/components/MkMention.vue # packages/frontend/src/components/global/MkAvatar.vue # packages/frontend/src/pages/settings/general.vue
This commit is contained in:
commit
366ea80a06
68 changed files with 2748 additions and 655 deletions
103
packages/frontend/src/pages/admin/avatar-decorations.vue
Normal file
103
packages/frontend/src/pages/admin/avatar-decorations.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps">
|
||||
<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
|
||||
<template #label>{{ avatarDecoration.name }}</template>
|
||||
<template #caption>{{ avatarDecoration.description }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="avatarDecoration.name">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="avatarDecoration.description">
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="avatarDecoration.url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<div class="buttons _buttons">
|
||||
<MkButton class="button" inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton v-if="avatarDecoration.id != null" class="button" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
let avatarDecorations: any[] = $ref([]);
|
||||
|
||||
function add() {
|
||||
avatarDecorations.unshift({
|
||||
_id: Math.random().toString(36),
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
url: '',
|
||||
});
|
||||
}
|
||||
|
||||
function del(avatarDecoration) {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('deleteAreYouSure', { x: avatarDecoration.name }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
avatarDecorations = avatarDecorations.filter(x => x !== avatarDecoration);
|
||||
os.api('admin/avatar-decorations/delete', avatarDecoration);
|
||||
});
|
||||
}
|
||||
|
||||
async function save(avatarDecoration) {
|
||||
if (avatarDecoration.id == null) {
|
||||
await os.apiWithDialog('admin/avatar-decorations/create', avatarDecoration);
|
||||
load();
|
||||
} else {
|
||||
os.apiWithDialog('admin/avatar-decorations/update', avatarDecoration);
|
||||
}
|
||||
}
|
||||
|
||||
function load() {
|
||||
os.api('admin/avatar-decorations/list').then(_avatarDecorations => {
|
||||
avatarDecorations = _avatarDecorations;
|
||||
});
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.add,
|
||||
handler: add,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.avatarDecorations,
|
||||
icon: 'ti ti-sparkles',
|
||||
});
|
||||
</script>
|
||||
|
|
@ -129,6 +129,11 @@ const menuDef = $computed(() => [{
|
|||
text: i18n.ts.customEmojis,
|
||||
to: '/admin/emojis',
|
||||
active: currentPage?.route.name === 'emojis',
|
||||
}, {
|
||||
icon: 'ti ti-sparkles',
|
||||
text: i18n.ts.avatarDecorations,
|
||||
to: '/admin/avatar-decorations',
|
||||
active: currentPage?.route.name === 'avatarDecorations',
|
||||
}, {
|
||||
icon: 'ti ti-whirl',
|
||||
text: i18n.ts.federation,
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>
|
||||
<b
|
||||
:class="{
|
||||
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation'].includes(log.type),
|
||||
[$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'].includes(log.type)
|
||||
[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].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>
|
||||
|
|
@ -37,6 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="log.type === 'deleteUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'deleteNote'">: @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'deleteDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
||||
<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>
|
||||
</template>
|
||||
<template #icon>
|
||||
<MkAvatar :user="log.user" :class="$style.avatar"/>
|
||||
|
|
@ -102,6 +105,11 @@ 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 === 'updateAvatarDecoration'">
|
||||
<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>
|
||||
|
|
|
|||
354
packages/frontend/src/pages/install-extentions.vue
Normal file
354
packages/frontend/src/pages/install-extentions.vue
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="500">
|
||||
<MkLoading v-if="uiPhase === 'fetching'"/>
|
||||
<div v-else-if="uiPhase === 'confirm' && data" class="_gaps_m" :class="$style.extInstallerRoot">
|
||||
<div :class="$style.extInstallerIconWrapper">
|
||||
<i v-if="data.type === 'plugin'" class="ti ti-plug"></i>
|
||||
<i v-else-if="data.type === 'theme'" class="ti ti-palette"></i>
|
||||
<i v-else class="ti ti-download"></i>
|
||||
</div>
|
||||
<h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${data.type}`].title }}</h2>
|
||||
<div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
|
||||
<MkInfo v-if="data.type === 'plugin'" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._externalResourceInstaller[`_${data.type}`].metaTitle }}</template>
|
||||
<div class="_gaps_s">
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.name }}</template>
|
||||
<template #value>{{ data.meta?.name }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.author }}</template>
|
||||
<template #value>{{ data.meta?.author }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<MkKeyValue v-if="data.type === 'plugin'">
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value>{{ data.meta?.description }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="data.type === 'plugin'">
|
||||
<template #key>{{ i18n.ts.version }}</template>
|
||||
<template #value>{{ data.meta?.version }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="data.type === 'plugin'">
|
||||
<template #key>{{ i18n.ts.permission }}</template>
|
||||
<template #value>
|
||||
<ul :class="$style.extInstallerKVList">
|
||||
<li v-for="permission in data.meta?.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="data.type === 'theme' && data.meta?.base">
|
||||
<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
|
||||
<template #value>{{ i18n.ts[data.meta.base] }}</template>
|
||||
</MkKeyValue>
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
<template #label>{{ i18n.ts._plugin.viewSource }}</template>
|
||||
|
||||
<MkCode :code="data.raw ?? ''"/>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._externalResourceInstaller._vendorInfo.title }}</template>
|
||||
<div class="_gaps_s">
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template>
|
||||
<template #value><MkUrl :url="url ?? ''" :showUrlPreview="false"></MkUrl></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
|
||||
<template #value>
|
||||
<!--この画面が出ている時点でハッシュの検証には成功している-->
|
||||
<i class="ti ti-check" style="color: var(--accent)"></i>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</FormSection>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton primary @click="install()"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="uiPhase === 'error'" class="_gaps_m" :class="[$style.extInstallerRoot, $style.error]">
|
||||
<div :class="$style.extInstallerIconWrapper">
|
||||
<i class="ti ti-circle-x"></i>
|
||||
</div>
|
||||
<h2 :class="$style.extInstallerTitle">{{ errorKV?.title }}</h2>
|
||||
<div :class="$style.extInstallerNormDesc">{{ errorKV?.description }}</div>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton @click="goBack()">{{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton @click="goToMisskey()">{{ i18n.ts.goToMisskey }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import MkLoading from '@/components/global/MkLoading.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import MkCode from '@/components/MkCode.vue';
|
||||
import MkUrl from '@/components/global/MkUrl.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { AiScriptPluginMeta, parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js';
|
||||
import { parseThemeCode, installTheme } from '@/scripts/install-theme.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const uiPhase = ref<'fetching' | 'confirm' | 'error'>('fetching');
|
||||
const errorKV = ref<{
|
||||
title?: string;
|
||||
description?: string;
|
||||
}>({
|
||||
title: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const url = urlParams.get('url');
|
||||
const hash = urlParams.get('hash');
|
||||
|
||||
const data = ref<{
|
||||
type: 'plugin' | 'theme';
|
||||
raw: string;
|
||||
meta?: {
|
||||
// Plugin & Theme Common
|
||||
name: string;
|
||||
author: string;
|
||||
|
||||
// Plugin
|
||||
description?: string;
|
||||
version?: string;
|
||||
permissions?: string[];
|
||||
config?: Record<string, any>;
|
||||
|
||||
// Theme
|
||||
base?: 'light' | 'dark';
|
||||
};
|
||||
} | null>(null);
|
||||
|
||||
function goBack(): void {
|
||||
history.back();
|
||||
}
|
||||
|
||||
function goToMisskey(): void {
|
||||
location.href = '/';
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
if (!url || !hash) {
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._invalidParams.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._invalidParams.description,
|
||||
};
|
||||
uiPhase.value = 'error';
|
||||
return;
|
||||
}
|
||||
const res = await os.api('fetch-external-resources', {
|
||||
url,
|
||||
hash,
|
||||
}).catch((err) => {
|
||||
switch (err.id) {
|
||||
case 'bb774091-7a15-4a70-9dc5-6ac8cf125856':
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._failedToFetch.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._failedToFetch.parseErrorDescription,
|
||||
};
|
||||
uiPhase.value = 'error';
|
||||
break;
|
||||
case '693ba8ba-b486-40df-a174-72f8279b56a4':
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._hashUnmatched.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._hashUnmatched.description,
|
||||
};
|
||||
uiPhase.value = 'error';
|
||||
break;
|
||||
default:
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._failedToFetch.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._failedToFetch.fetchErrorDescription,
|
||||
};
|
||||
uiPhase.value = 'error';
|
||||
break;
|
||||
}
|
||||
throw new Error(err.code);
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._failedToFetch.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._failedToFetch.fetchErrorDescription,
|
||||
};
|
||||
uiPhase.value = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
switch (res.type) {
|
||||
case 'plugin':
|
||||
try {
|
||||
const meta = await parsePluginMeta(res.data);
|
||||
data.value = {
|
||||
type: 'plugin',
|
||||
meta,
|
||||
raw: res.data,
|
||||
};
|
||||
} catch (err) {
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._pluginParseFailed.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._pluginParseFailed.description,
|
||||
};
|
||||
console.error(err);
|
||||
uiPhase.value = 'error';
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'theme':
|
||||
try {
|
||||
const metaRaw = parseThemeCode(res.data);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { id, props, desc: description, ...meta } = metaRaw;
|
||||
data.value = {
|
||||
type: 'theme',
|
||||
meta: {
|
||||
description,
|
||||
...meta,
|
||||
},
|
||||
raw: res.data,
|
||||
};
|
||||
} catch (err) {
|
||||
switch (err.message.toLowerCase()) {
|
||||
case 'this theme is already installed':
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._themeParseFailed.title,
|
||||
description: i18n.ts._theme.alreadyInstalled,
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._themeParseFailed.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._themeParseFailed.description,
|
||||
};
|
||||
break;
|
||||
}
|
||||
console.error(err);
|
||||
uiPhase.value = 'error';
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._resourceTypeNotSupported.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._resourceTypeNotSupported.description,
|
||||
};
|
||||
uiPhase.value = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
uiPhase.value = 'confirm';
|
||||
}
|
||||
|
||||
async function install() {
|
||||
if (!data.value) return;
|
||||
|
||||
switch (data.value.type) {
|
||||
case 'plugin':
|
||||
if (!data.value.meta) return;
|
||||
try {
|
||||
await installPlugin(data.value.raw, data.value.meta as AiScriptPluginMeta);
|
||||
os.success();
|
||||
nextTick(() => {
|
||||
unisonReload('/');
|
||||
});
|
||||
} catch (err) {
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._pluginInstallFailed.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._pluginInstallFailed.description,
|
||||
};
|
||||
console.error(err);
|
||||
uiPhase.value = 'error';
|
||||
}
|
||||
break;
|
||||
case 'theme':
|
||||
if (!data.value.meta) return;
|
||||
await installTheme(data.value.raw);
|
||||
os.success();
|
||||
nextTick(() => {
|
||||
location.href = '/settings/theme';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetch();
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts._externalResourceInstaller.title,
|
||||
icon: 'ti ti-download',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.extInstallerRoot {
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.extInstallerIconWrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 24px;
|
||||
line-height: 48px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.error .extInstallerIconWrapper {
|
||||
background-color: rgba(255, 42, 42, .15);
|
||||
color: #ff2a2a;
|
||||
}
|
||||
|
||||
.extInstallerTitle {
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.extInstallerNormDesc {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.extInstallerKVList {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -29,11 +29,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
|
||||
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
|
||||
<MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch>
|
||||
<MkSwitch v-model="showMediaTimeline">{{ i18n.ts.showMediaTimeline}}<template #caption>{{ i18n.ts.showMediaTimelineInfo }} </template></MkSwitch>
|
||||
<MkSwitch v-model="FeaturedOrNote">{{ i18n.ts.FeaturedOrNote}}<template #caption>{{ i18n.ts.FeaturedOrNoteInfo }} </template></MkSwitch>
|
||||
<MkSwitch v-model="showGlobalTimeline">{{ i18n.ts.showGlobalTimeline }}</MkSwitch>
|
||||
<MkFolder>
|
||||
<MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch>
|
||||
<MkButton danger @click="updateRepliesAll(true)"><i class="ti ti-messages"></i> {{ i18n.ts.showRepliesToOthersInTimelineAll }}</MkButton>
|
||||
<MkButton danger @click="updateRepliesAll(false)"><i class="ti ti-messages-off"></i> {{ i18n.ts.hideRepliesToOthersInTimelineAll }}</MkButton>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.pinnedList }}</template>
|
||||
<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
|
||||
<MkButton v-if="defaultStore.reactiveState.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton>
|
||||
|
|
@ -402,6 +401,15 @@ async function setPinnedList() {
|
|||
defaultStore.set('pinnedUserLists', [list]);
|
||||
}
|
||||
|
||||
async function updateRepliesAll(withReplies: boolean) {
|
||||
const { canceled } = os.confirm({
|
||||
type: 'warning',
|
||||
text: withReplies ? i18n.ts.confirmShowRepliesAll : i18n.ts.confirmHideRepliesAll,
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.api('following/update-all', { withReplies });
|
||||
}
|
||||
|
||||
function removePinnedList() {
|
||||
defaultStore.set('pinnedUserLists', []);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,130 +18,35 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, nextTick, ref } from 'vue';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { nextTick, ref } from 'vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { ColdDeviceStorage } from '@/store.js';
|
||||
import { installPlugin } from '@/scripts/install-plugin.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const parser = new Parser();
|
||||
const code = ref(null);
|
||||
|
||||
function installPlugin({ id, meta, src, token }) {
|
||||
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
|
||||
...meta,
|
||||
id,
|
||||
active: true,
|
||||
configData: {},
|
||||
token: token,
|
||||
src: src,
|
||||
}));
|
||||
}
|
||||
|
||||
function isSupportedAiScriptVersion(version: string): boolean {
|
||||
try {
|
||||
return (compareVersions(version, '0.12.0') >= 0);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const code = ref<string | null>(null);
|
||||
|
||||
async function install() {
|
||||
if (code.value == null) return;
|
||||
if (!code.value) return;
|
||||
|
||||
const lv = utils.getLangVersion(code.value);
|
||||
if (lv == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'No language version annotation found :(',
|
||||
});
|
||||
return;
|
||||
} else if (!isSupportedAiScriptVersion(lv)) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: `aiscript version '${lv}' is not supported :(`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let ast;
|
||||
try {
|
||||
ast = parser.parse(code.value);
|
||||
await installPlugin(code.value);
|
||||
os.success();
|
||||
|
||||
nextTick(() => {
|
||||
unisonReload();
|
||||
});
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Syntax error :(',
|
||||
title: 'Install failed',
|
||||
text: err.toString() ?? null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = Interpreter.collectMetadata(ast);
|
||||
if (meta == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'No metadata found :(',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = meta.get(null);
|
||||
if (metadata == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'No metadata found :(',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, version, author, description, permissions, config } = metadata;
|
||||
if (name == null || version == null || author == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Required property not found :(',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
|
||||
title: i18n.ts.tokenRequested,
|
||||
information: i18n.ts.pluginTokenRequestedDescription,
|
||||
initialName: name,
|
||||
initialPermissions: permissions,
|
||||
}, {
|
||||
done: async result => {
|
||||
const { name, permissions } = result;
|
||||
const { token } = await os.api('miauth/gen-token', {
|
||||
session: null,
|
||||
name: name,
|
||||
permission: permissions,
|
||||
});
|
||||
res(token);
|
||||
},
|
||||
}, 'closed');
|
||||
});
|
||||
|
||||
installPlugin({
|
||||
id: uuid(),
|
||||
meta: {
|
||||
name, version, author, description, permissions, config,
|
||||
},
|
||||
token,
|
||||
src: code.value,
|
||||
});
|
||||
|
||||
os.success();
|
||||
|
||||
nextTick(() => {
|
||||
unisonReload();
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
|
|
|||
|
|
@ -83,6 +83,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
||||
</FormSlot>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-sparkles"></i></template>
|
||||
<template #label>{{ i18n.ts.avatarDecorations }}</template>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-gap: 12px;">
|
||||
<div
|
||||
v-for="avatarDecoration in avatarDecorations"
|
||||
:key="avatarDecoration.id"
|
||||
:class="[$style.avatarDecoration, { [$style.avatarDecorationActive]: $i.avatarDecorations.some(x => x.id === avatarDecoration.id) }]"
|
||||
@click="toggleDecoration(avatarDecoration)"
|
||||
>
|
||||
<div :class="$style.avatarDecorationName">{{ avatarDecoration.name }}</div>
|
||||
<MkAvatar style="width: 64px; height: 64px;" :user="$i" :decoration="avatarDecoration.url"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.advancedSettings }}</template>
|
||||
|
||||
|
|
@ -126,6 +143,7 @@ import MkInfo from '@/components/MkInfo.vue';
|
|||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
|
||||
let avatarDecorations: any[] = $ref([]);
|
||||
|
||||
const profile = reactive({
|
||||
name: $i.name,
|
||||
|
|
@ -146,6 +164,10 @@ watch(() => profile, () => {
|
|||
const fields = ref($i?.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []);
|
||||
const fieldEditMode = ref(false);
|
||||
|
||||
os.api('get-avatar-decorations').then(_avatarDecorations => {
|
||||
avatarDecorations = _avatarDecorations;
|
||||
});
|
||||
|
||||
function addField() {
|
||||
fields.value.push({
|
||||
id: Math.random().toString(),
|
||||
|
|
@ -244,6 +266,20 @@ function changeBanner(ev) {
|
|||
});
|
||||
}
|
||||
|
||||
function toggleDecoration(avatarDecoration) {
|
||||
if ($i.avatarDecorations.some(x => x.id === avatarDecoration.id)) {
|
||||
os.apiWithDialog('i/update', {
|
||||
avatarDecorations: [],
|
||||
});
|
||||
$i.avatarDecorations = [];
|
||||
} else {
|
||||
os.apiWithDialog('i/update', {
|
||||
avatarDecorations: [avatarDecoration.id],
|
||||
});
|
||||
$i.avatarDecorations.push(avatarDecoration);
|
||||
}
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
|
@ -338,4 +374,23 @@ definePageMetadata({
|
|||
.dragItemForm {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.avatarDecoration {
|
||||
cursor: pointer;
|
||||
padding: 16px 16px 24px 16px;
|
||||
border: solid 2px var(--divider);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatarDecorationActive {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.avatarDecorationName {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkTextarea>
|
||||
|
||||
<div class="_buttons">
|
||||
<MkButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
|
||||
<MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
|
||||
<MkButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -18,60 +18,41 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { applyTheme, validateTheme } from '@/scripts/theme.js';
|
||||
import { parseThemeCode, previewTheme, installTheme } from '@/scripts/install-theme.js';
|
||||
import * as os from '@/os.js';
|
||||
import { addTheme, getThemes } from '@/theme-store';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
let installThemeCode = $ref(null);
|
||||
|
||||
function parseThemeCode(code: string) {
|
||||
let theme;
|
||||
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._theme.invalid,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (!validateTheme(theme)) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._theme.invalid,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (getThemes().some(t => t.id === theme.id)) {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: i18n.ts._theme.alreadyInstalled,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
function preview(code: string): void {
|
||||
const theme = parseThemeCode(code);
|
||||
if (theme) applyTheme(theme, false);
|
||||
}
|
||||
|
||||
async function install(code: string): Promise<void> {
|
||||
const theme = parseThemeCode(code);
|
||||
if (!theme) return;
|
||||
await addTheme(theme);
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.t('_theme.installed', { name: theme.name }),
|
||||
});
|
||||
try {
|
||||
const theme = parseThemeCode(code);
|
||||
await installTheme(code);
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.t('_theme.installed', { name: theme.name }),
|
||||
});
|
||||
} catch (err) {
|
||||
switch (err.message.toLowerCase()) {
|
||||
case 'this theme is already installed':
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: i18n.ts._theme.alreadyInstalled,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._theme.invalid,
|
||||
});
|
||||
break;
|
||||
}
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue