2023-12-14 03:29:27 +01:00
|
|
|
<!--
|
|
|
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
-->
|
|
|
|
|
|
|
|
<template>
|
2023-12-16 09:37:50 +01:00
|
|
|
<div>
|
|
|
|
<div v-if="!loading" class="_gaps">
|
2024-01-20 00:11:59 +01:00
|
|
|
<MkInfo>{{ i18n.tsx._profile.avatarDecorationMax({ max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.tsx.remainingN({ n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo>
|
2023-12-14 03:29:27 +01:00
|
|
|
|
2023-12-16 09:37:50 +01:00
|
|
|
<MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration/>
|
|
|
|
|
|
|
|
<div v-if="$i.avatarDecorations.length > 0" v-panel :class="$style.current" class="_gaps_s">
|
|
|
|
<div>{{ i18n.ts.inUse }}</div>
|
|
|
|
|
|
|
|
<div :class="$style.decorations">
|
|
|
|
<XDecoration
|
|
|
|
v-for="(avatarDecoration, i) in $i.avatarDecorations"
|
|
|
|
:decoration="avatarDecorations.find(d => d.id === avatarDecoration.id)"
|
|
|
|
:angle="avatarDecoration.angle"
|
|
|
|
:flipH="avatarDecoration.flipH"
|
|
|
|
:offsetX="avatarDecoration.offsetX"
|
|
|
|
:offsetY="avatarDecoration.offsetY"
|
|
|
|
:active="true"
|
|
|
|
@click="openDecoration(avatarDecoration, i)"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<MkButton danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton>
|
|
|
|
</div>
|
2024-01-10 23:03:14 +01:00
|
|
|
<MkInput v-model="q" :placeholder="i18n.ts.search"/>
|
|
|
|
<div v-if="searchResult.length > 0" :class="$style.decorations">
|
|
|
|
<span> {{ i18n.ts.searchResult }}</span><br>
|
|
|
|
<XDecoration
|
|
|
|
v-for="avatarDecoration in searchResult"
|
|
|
|
:key="avatarDecoration.name"
|
|
|
|
:decoration="avatarDecoration"
|
|
|
|
@click="openDecoration(avatarDecoration)"
|
|
|
|
/>
|
|
|
|
</div>
|
2024-01-02 17:25:54 +01:00
|
|
|
<div v-for="category in categories">
|
2024-01-02 18:13:25 +01:00
|
|
|
<MkFoldableSection :defaultOpen="false">
|
2024-01-02 17:25:54 +01:00
|
|
|
<template #header> {{ (category !== '') ? category : i18n.ts.other }}</template>
|
|
|
|
<div :class="$style.decorations">
|
|
|
|
<div v-for="avatarDecoration in avatarDecorations.filter(ad => ad.category === category)">
|
|
|
|
<XDecoration
|
|
|
|
:key="avatarDecoration.id"
|
|
|
|
:decoration="avatarDecoration"
|
|
|
|
@click="openDecoration(avatarDecoration)"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</MkFoldableSection>
|
2023-12-14 03:29:27 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
2023-12-16 09:37:50 +01:00
|
|
|
<div v-else>
|
|
|
|
<MkLoading/>
|
2023-12-14 03:29:27 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2024-01-10 23:03:14 +01:00
|
|
|
import { ref, defineAsyncComponent, computed, watch } from 'vue';
|
2023-12-14 03:29:27 +01:00
|
|
|
import * as Misskey from 'misskey-js';
|
2023-12-16 09:37:50 +01:00
|
|
|
import XDecoration from './avatar-decoration.decoration.vue';
|
2023-12-14 03:29:27 +01:00
|
|
|
import MkButton from '@/components/MkButton.vue';
|
|
|
|
import * as os from '@/os.js';
|
2024-01-04 10:32:46 +01:00
|
|
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
2023-12-14 03:29:27 +01:00
|
|
|
import { i18n } from '@/i18n.js';
|
2024-01-04 07:30:40 +01:00
|
|
|
import { signinRequired } from '@/account.js';
|
2023-12-14 03:29:27 +01:00
|
|
|
import MkInfo from '@/components/MkInfo.vue';
|
2023-12-16 09:37:50 +01:00
|
|
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
2024-01-02 17:25:54 +01:00
|
|
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
2024-01-10 23:03:14 +01:00
|
|
|
import MkInput from '@/components/MkInput.vue';
|
2023-12-14 03:29:27 +01:00
|
|
|
|
2024-01-04 07:30:40 +01:00
|
|
|
const $i = signinRequired();
|
2024-01-10 23:03:14 +01:00
|
|
|
const searchResult = ref([]);
|
2023-12-14 03:29:27 +01:00
|
|
|
const loading = ref(true);
|
2024-01-02 17:25:54 +01:00
|
|
|
const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse & { category:string }>([]);
|
2024-01-10 23:03:14 +01:00
|
|
|
const q = ref<string>('');
|
|
|
|
watch(() => q.value, () => {
|
|
|
|
const searchCustom = () => {
|
|
|
|
const max = 100;
|
|
|
|
const matches = new Set();
|
|
|
|
const decos = avatarDecorations.value;
|
|
|
|
const exactMatch = decos.find(avatarDecoration => avatarDecoration.name === q.value);
|
|
|
|
if (exactMatch) matches.add(exactMatch);
|
|
|
|
|
|
|
|
if (decos.includes(' ')) { // AND検索
|
|
|
|
const keywords = q.value.split(' ');
|
2023-12-14 03:29:27 +01:00
|
|
|
|
2024-01-10 23:03:14 +01:00
|
|
|
// 名前にキーワードが含まれている
|
|
|
|
for (const deco of decos) {
|
|
|
|
if (keywords.every(keyword => deco.name.includes(keyword))) {
|
|
|
|
matches.add(deco);
|
|
|
|
if (matches.size >= max) break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (matches.size >= max) return matches;
|
|
|
|
|
|
|
|
// 名前またはエイリアスにキーワードが含まれている
|
|
|
|
for (const deco of decos) {
|
|
|
|
if (keywords.every(keyword => deco.name.includes(keyword))) {
|
|
|
|
matches.add(deco);
|
|
|
|
if (matches.size >= max) break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for (const deco of decos) {
|
|
|
|
if (deco.name.startsWith(q.value)) {
|
|
|
|
matches.add(deco);
|
|
|
|
if (matches.size >= max) break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (matches.size >= max) return matches;
|
|
|
|
|
|
|
|
for (const deco of decos) {
|
|
|
|
if (deco.name.includes(q.value)) {
|
|
|
|
matches.add(deco);
|
|
|
|
if (matches.size >= max) break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (matches.size >= max) return matches;
|
|
|
|
}
|
|
|
|
|
|
|
|
return matches;
|
|
|
|
};
|
|
|
|
searchResult.value = Array.from(searchCustom());
|
|
|
|
});
|
2024-01-04 10:32:46 +01:00
|
|
|
misskeyApi('get-avatar-decorations').then(_avatarDecorations => {
|
2023-12-14 03:29:27 +01:00
|
|
|
avatarDecorations.value = _avatarDecorations;
|
|
|
|
loading.value = false;
|
|
|
|
});
|
2024-01-02 17:25:54 +01:00
|
|
|
const categories = computed(() => {
|
|
|
|
const allCategories = avatarDecorations.value.map(ad => ad.category);
|
|
|
|
const uniqueCategories = [...new Set(allCategories)];
|
|
|
|
return uniqueCategories.sort();
|
|
|
|
});
|
2023-12-14 03:29:27 +01:00
|
|
|
|
|
|
|
function openDecoration(avatarDecoration, index?: number) {
|
2023-12-16 09:37:50 +01:00
|
|
|
os.popup(defineAsyncComponent(() => import('./avatar-decoration.dialog.vue')), {
|
2023-12-14 03:29:27 +01:00
|
|
|
decoration: avatarDecoration,
|
|
|
|
usingIndex: index,
|
|
|
|
}, {
|
|
|
|
'attach': async (payload) => {
|
|
|
|
const decoration = {
|
|
|
|
id: avatarDecoration.id,
|
|
|
|
angle: payload.angle,
|
|
|
|
flipH: payload.flipH,
|
2023-12-14 12:58:08 +01:00
|
|
|
offsetX: payload.offsetX,
|
|
|
|
offsetY: payload.offsetY,
|
2023-12-14 03:29:27 +01:00
|
|
|
};
|
|
|
|
const update = [...$i.avatarDecorations, decoration];
|
|
|
|
await os.apiWithDialog('i/update', {
|
|
|
|
avatarDecorations: update,
|
|
|
|
});
|
|
|
|
$i.avatarDecorations = update;
|
|
|
|
},
|
|
|
|
'update': async (payload) => {
|
|
|
|
const decoration = {
|
|
|
|
id: avatarDecoration.id,
|
|
|
|
angle: payload.angle,
|
|
|
|
flipH: payload.flipH,
|
2023-12-14 12:58:08 +01:00
|
|
|
offsetX: payload.offsetX,
|
|
|
|
offsetY: payload.offsetY,
|
2023-12-14 03:29:27 +01:00
|
|
|
};
|
|
|
|
const update = [...$i.avatarDecorations];
|
|
|
|
update[index] = decoration;
|
|
|
|
await os.apiWithDialog('i/update', {
|
|
|
|
avatarDecorations: update,
|
|
|
|
});
|
|
|
|
$i.avatarDecorations = update;
|
|
|
|
},
|
|
|
|
'detach': async () => {
|
|
|
|
const update = [...$i.avatarDecorations];
|
|
|
|
update.splice(index, 1);
|
|
|
|
await os.apiWithDialog('i/update', {
|
|
|
|
avatarDecorations: update,
|
|
|
|
});
|
|
|
|
$i.avatarDecorations = update;
|
|
|
|
},
|
|
|
|
}, 'closed');
|
|
|
|
}
|
|
|
|
|
|
|
|
function detachAllDecorations() {
|
|
|
|
os.confirm({
|
|
|
|
type: 'warning',
|
|
|
|
text: i18n.ts.areYouSure,
|
|
|
|
}).then(async ({ canceled }) => {
|
|
|
|
if (canceled) return;
|
|
|
|
await os.apiWithDialog('i/update', {
|
|
|
|
avatarDecorations: [],
|
|
|
|
});
|
|
|
|
$i.avatarDecorations = [];
|
|
|
|
});
|
|
|
|
}
|
2023-12-16 09:37:50 +01:00
|
|
|
|
|
|
|
const headerActions = computed(() => []);
|
|
|
|
|
|
|
|
const headerTabs = computed(() => []);
|
|
|
|
|
|
|
|
definePageMetadata({
|
|
|
|
title: i18n.ts.avatarDecorations,
|
|
|
|
icon: 'ti ti-sparkles',
|
|
|
|
});
|
2023-12-14 03:29:27 +01:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" module>
|
2023-12-16 09:37:50 +01:00
|
|
|
.avatar {
|
|
|
|
display: inline-block;
|
|
|
|
width: 72px;
|
|
|
|
height: 72px;
|
|
|
|
margin: 16px auto;
|
|
|
|
}
|
|
|
|
|
2023-12-14 03:29:27 +01:00
|
|
|
.current {
|
|
|
|
padding: 16px;
|
|
|
|
border-radius: var(--radius);
|
|
|
|
}
|
|
|
|
|
|
|
|
.decorations {
|
|
|
|
display: grid;
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
|
|
grid-gap: 12px;
|
|
|
|
}
|
|
|
|
</style>
|