1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-17 09:05:35 +02:00

enhance: アバターデコレーションへのカテゴリの導入 (#17034)

* feat(backend): AvatarDecorationにcategoryを追加し、関連APIのプロパティ・戻り値にも反映

* feat(frontend): アバターデコレーションのカテゴリ設定機能

* chore(frontend): 管理画面とユーザー側の画面で、アバターデコレーションのグループ化のコードをある程度統一

* CHANGELOGを更新

* fix: group-avatar-decorations.tsを使用するよう修正

* chore: コーディング規約への準拠

* 型エラーを解消
This commit is contained in:
るちーか
2026-04-15 20:29:17 +09:00
committed by GitHub
parent c95aef7535
commit 360e805638
12 changed files with 116 additions and 20 deletions

View File

@@ -32,6 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="url">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<MkInput v-model="category" :datalist="props.categories || []">
<template #label>{{ i18n.ts.category }}</template>
</MkInput>
<MkTextarea v-model="description">
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
@@ -79,6 +82,7 @@ const $i = ensureSignin();
const props = defineProps<{
avatarDecoration?: Misskey.entities.AdminAvatarDecorationsListResponse[number],
categories?: string[],
}>();
const emit = defineEmits<{
@@ -89,6 +93,7 @@ const emit = defineEmits<{
const windowEl = useTemplateRef('windowEl');
const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : '');
const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : '');
const category = ref<string>(props.avatarDecoration?.category ? props.avatarDecoration.category : '');
const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : '');
const roleIdsThatCanBeUsedThisDecoration = ref(props.avatarDecoration ? props.avatarDecoration.roleIdsThatCanBeUsedThisDecoration : []);
const rolesThatCanBeUsedThisDecoration = ref<Misskey.entities.Role[]>([]);
@@ -118,6 +123,7 @@ async function done() {
url: url.value,
name: name.value,
description: description.value,
category: category.value,
roleIdsThatCanBeUsedThisDecoration: rolesThatCanBeUsedThisDecoration.value.map(x => x.id),
};

View File

@@ -7,18 +7,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 900px;">
<div class="_gaps">
<div :class="$style.decorations">
<div
v-for="avatarDecoration in avatarDecorations"
:key="avatarDecoration.id"
v-panel
:class="$style.decoration"
@click="edit(avatarDecoration)"
>
<div :class="$style.decorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: avatarDecoration.url }]" forceShowDecoration/>
<MkFoldableSection v-for="category in Object.keys(groupedDecorations)" :key="category" :expanded="true">
<template #header>{{ category || i18n.ts.other }}</template>
<div :class="$style.decorations">
<div
v-for="avatarDecoration in groupedDecorations[category]"
:key="avatarDecoration.id"
v-panel
:class="$style.decoration"
@click="edit(avatarDecoration)"
>
<div :class="$style.decorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: avatarDecoration.url }]" forceShowDecoration/>
</div>
</div>
</div>
</MkFoldableSection>
</div>
</div>
</PageWithHeader>
@@ -32,10 +35,13 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { groupAvatarDecorations } from '@/utility/group-avatar-decorations.js';
const $i = ensureSignin();
const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
const groupedDecorations = computed(() => groupAvatarDecorations(avatarDecorations.value));
function load() {
misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => {
@@ -47,6 +53,7 @@ load();
async function add(ev: PointerEvent) {
const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration-edit-dialog.vue').then(x => x.default), {
categories: Object.keys(groupedDecorations.value),
}, {
done: result => {
if (result.created) {
@@ -60,6 +67,7 @@ async function add(ev: PointerEvent) {
async function edit(avatarDecoration: Misskey.entities.AdminAvatarDecorationsListResponse[number]) {
const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration-edit-dialog.vue').then(x => x.default), {
avatarDecoration: avatarDecoration,
categories: Object.keys(groupedDecorations.value),
}, {
done: result => {
if (result.updated) {

View File

@@ -29,15 +29,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton>
</div>
<div :class="$style.decorations">
<XDecoration
v-for="avatarDecoration in avatarDecorations"
:key="avatarDecoration.id"
:decoration="avatarDecoration"
@click="openDecoration(avatarDecoration)"
/>
</div>
<MkFoldableSection v-for="category in Object.keys(groupedDecorations)" :key="category" :expanded="true">
<template #header>{{ category || i18n.ts.other }}</template>
<div :class="$style.decorations">
<XDecoration
v-for="avatarDecoration in groupedDecorations[category]"
:key="avatarDecoration.id"
:decoration="avatarDecoration"
@click="openDecoration(avatarDecoration)"
/>
</div>
</MkFoldableSection>
</div>
<div v-else>
<MkLoading/>
@@ -52,17 +54,20 @@ import * as Misskey from 'misskey-js';
import XDecoration from './avatar-decoration.decoration.vue';
import XDialog from './avatar-decoration.dialog.vue';
import MkButton from '@/components/MkButton.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { ensureSignin } from '@/i.js';
import MkInfo from '@/components/MkInfo.vue';
import { definePage } from '@/page.js';
import { groupAvatarDecorations } from '@/utility/group-avatar-decorations.js';
const $i = ensureSignin();
const loading = ref(true);
const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse>([]);
const groupedDecorations = computed(() => groupAvatarDecorations(avatarDecorations.value));
misskeyApi('get-avatar-decorations').then(_avatarDecorations => {
avatarDecorations.value = _avatarDecorations;

View File

@@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
type AvatarDecorationBase = { category?: string | null | undefined };
/**
* アバターデコレーションをカテゴリごとにグループ化します。
* @param decorations アバターデコレーションの配列
* @returns カテゴリごとにグループ化されたアバターデコレーションオブジェクト
*/
export function groupAvatarDecorations<T extends AvatarDecorationBase>(decorations: T[]) {
const grouped: Record<string, T[]> = {};
for (const decoration of decorations) {
const category = decoration.category ?? '';
if (!(category in grouped)) {
grouped[category] = [];
}
grouped[category].push(decoration);
}
return grouped;
}