summaryrefslogtreecommitdiff
path: root/packages/frontend/src/pages
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-10-21 18:38:07 +0900
committerGitHub <noreply@github.com>2023-10-21 18:38:07 +0900
commit2c0a139da60ddf33e82353acbb985230c71c78da (patch)
treeb12d2762b332b7c885c5b210954053869618b62a /packages/frontend/src/pages
parentUpdate CHANGELOG.md (diff)
downloadmisskey-2c0a139da60ddf33e82353acbb985230c71c78da.tar.gz
misskey-2c0a139da60ddf33e82353acbb985230c71c78da.tar.bz2
misskey-2c0a139da60ddf33e82353acbb985230c71c78da.zip
feat: Avatar decoration (#12096)
* wip * Update ja-JP.yml * Update profile.vue * .js * Update home.test.ts
Diffstat (limited to 'packages/frontend/src/pages')
-rw-r--r--packages/frontend/src/pages/admin/avatar-decorations.vue103
-rw-r--r--packages/frontend/src/pages/admin/index.vue5
-rw-r--r--packages/frontend/src/pages/admin/modlog.ModLog.vue12
-rw-r--r--packages/frontend/src/pages/settings/profile.vue55
4 files changed, 173 insertions, 2 deletions
diff --git a/packages/frontend/src/pages/admin/avatar-decorations.vue b/packages/frontend/src/pages/admin/avatar-decorations.vue
new file mode 100644
index 0000000000..b4007e6d20
--- /dev/null
+++ b/packages/frontend/src/pages/admin/avatar-decorations.vue
@@ -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>
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index a508c20cf3..b304edbf57 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -116,6 +116,11 @@ const menuDef = $computed(() => [{
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,
to: '/admin/federation',
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index 0af226f02e..bceefcf6c8 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -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>
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 5e4889f61c..c44a58d04a 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -84,6 +84,23 @@ SPDX-License-Identifier: AGPL-3.0-only
</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>
<div class="_gaps_m">
@@ -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>