diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-12-14 11:29:27 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2023-12-14 11:29:27 +0900 |
| commit | 839b7483ac85dc358cf116594f0003ca42e9b021 (patch) | |
| tree | f7cf92fe7b2c0e335dab0b06f43b084cb42219a2 /packages/frontend/src | |
| parent | fix(backend): モデレーションログがモデレーターは閲覧でき... (diff) | |
| download | misskey-839b7483ac85dc358cf116594f0003ca42e9b021.tar.gz misskey-839b7483ac85dc358cf116594f0003ca42e9b021.tar.bz2 misskey-839b7483ac85dc358cf116594f0003ca42e9b021.zip | |
enhance(frontend): 同じ種類のデコレーションを複数付けられるように
Diffstat (limited to 'packages/frontend/src')
| -rw-r--r-- | packages/frontend/src/components/global/MkAvatar.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue | 67 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue (renamed from packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue) | 63 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/profile.avatar-decoration.vue | 125 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/profile.vue | 75 |
5 files changed, 240 insertions, 96 deletions
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 6aa9a42037..9d13c03290 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -59,7 +59,7 @@ const props = withDefaults(defineProps<{ link?: boolean; preview?: boolean; indicator?: boolean; - decorations?: Misskey.entities.UserDetailed['avatarDecorations'][number][]; + decorations?: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>[]; forceShowDecoration?: boolean; }>(), { target: null, @@ -89,12 +89,12 @@ function onClick(ev: MouseEvent): void { emit('click', ev); } -function getDecorationAngle(decoration: Misskey.entities.UserDetailed['avatarDecorations'][number]) { +function getDecorationAngle(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { const angle = decoration.angle ?? 0; return angle === 0 ? undefined : `${angle * 360}deg`; } -function getDecorationScale(decoration: Misskey.entities.UserDetailed['avatarDecorations'][number]) { +function getDecorationScale(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { const scaleX = decoration.flipH ? -1 : 1; return scaleX === 1 ? undefined : `${scaleX} 1`; } diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue new file mode 100644 index 0000000000..c113868238 --- /dev/null +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue @@ -0,0 +1,67 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + :class="[$style.root, { [$style.active]: active }]" + @click="emit('click')" +> + <div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div> + <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH }]" forceShowDecoration/> + <i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i> +</div> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import { $i } from '@/account.js'; + +const props = defineProps<{ + active?: boolean; + decoration: { + id: string; + url: string; + name: string; + roleIdsThatCanBeUsedThisDecoration: string[]; + }; + angle?: number; + flipH?: boolean; +}>(); + +const emit = defineEmits<{ + (ev: 'click'): void; +}>(); +</script> + +<style lang="scss" module> +.root { + cursor: pointer; + padding: 16px 16px 28px 16px; + border: solid 2px var(--divider); + border-radius: 8px; + text-align: center; + font-size: 90%; + overflow: clip; + contain: content; +} + +.active { + background-color: var(--accentedBg); + border-color: var(--accent); +} + +.name { + position: relative; + z-index: 10; + font-weight: bold; + margin-bottom: 20px; +} + +.lock { + position: absolute; + bottom: 12px; + right: 12px; +} +</style> diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue index b7ea4c1521..a27b46aa3e 100644 --- a/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :marginMin="20" :marginMax="28"> <div style="text-align: center;"> <div :class="$style.name">{{ decoration.name }}</div> - <MkAvatar style="width: 64px; height: 64px; margin-bottom: 20px;" :user="$i" :decorations="[...$i.avatarDecorations, { url: decoration.url, angle, flipH }]" forceShowDecoration/> + <MkAvatar style="width: 64px; height: 64px; margin-bottom: 20px;" :user="$i" :decorations="decorationsForPreview" forceShowDecoration/> </div> <div class="_gaps_s"> <MkRange v-model="angle" continuousUpdate :min="-0.5" :max="0.5" :step="0.025" :textConverter="(v) => `${Math.floor(v * 360)}°`"> @@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSpacer> <div :class="$style.footer" class="_buttonsCenter"> - <MkButton v-if="using" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton> - <MkButton v-if="using" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton> + <MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton> + <MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton> <MkButton v-else primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton> </div> </div> @@ -51,48 +51,69 @@ import MkRange from '@/components/MkRange.vue'; import { $i } from '@/account.js'; const props = defineProps<{ + usingIndex: number | null; decoration: { id: string; url: string; name: string; - } + }; }>(); const emit = defineEmits<{ (ev: 'closed'): void; + (ev: 'attach', payload: { + angle: number; + flipH: boolean; + }): void; + (ev: 'update', payload: { + angle: number; + flipH: boolean; + }): void; + (ev: 'detach'): void; }>(); const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); -const using = computed(() => $i.avatarDecorations.some(x => x.id === props.decoration.id)); -const angle = ref(using.value ? $i.avatarDecorations.find(x => x.id === props.decoration.id).angle ?? 0 : 0); -const flipH = ref(using.value ? $i.avatarDecorations.find(x => x.id === props.decoration.id).flipH ?? false : false); +const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0); +const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false); + +const decorationsForPreview = computed(() => { + const decoration = { + id: props.decoration.id, + url: props.decoration.url, + angle: angle.value, + flipH: flipH.value, + }; + const decorations = [...$i.avatarDecorations]; + if (props.usingIndex != null) { + decorations[props.usingIndex] = decoration; + } else { + decorations.push(decoration); + } + return decorations; +}); function cancel() { dialog.value.close(); } -async function attach() { - const decoration = { - id: props.decoration.id, +async function update() { + emit('update', { angle: angle.value, flipH: flipH.value, - }; - const update = [...$i.avatarDecorations, decoration]; - await os.apiWithDialog('i/update', { - avatarDecorations: update, }); - $i.avatarDecorations = update; - dialog.value.close(); } -async function detach() { - const update = $i.avatarDecorations.filter(x => x.id !== props.decoration.id); - await os.apiWithDialog('i/update', { - avatarDecorations: update, +async function attach() { + emit('attach', { + angle: angle.value, + flipH: flipH.value, }); - $i.avatarDecorations = update; + dialog.value.close(); +} +async function detach() { + emit('detach'); dialog.value.close(); } </script> diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration.vue new file mode 100644 index 0000000000..90c2b75a4d --- /dev/null +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration.vue @@ -0,0 +1,125 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div v-if="!loading" class="_gaps"> + <MkInfo>{{ i18n.t('_profile.avatarDecorationMax', { max: $i?.policies.avatarDecorationLimit }) }} ({{ i18n.t('remainingN', { n: $i?.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo> + + <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" + :active="true" + @click="openDecoration(avatarDecoration, i)" + /> + </div> + + <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> +</div> +<div v-else> + <MkLoading/> +</div> +</template> + +<script lang="ts" setup> +import { ref, defineAsyncComponent } from 'vue'; +import * as Misskey from 'misskey-js'; +import XDecoration from './profile.avatar-decoration.decoration.vue'; +import MkButton from '@/components/MkButton.vue'; +import * as os from '@/os.js'; +import { i18n } from '@/i18n.js'; +import { $i } from '@/account.js'; +import MkInfo from '@/components/MkInfo.vue'; + +const loading = ref(true); +const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse>([]); + +os.api('get-avatar-decorations').then(_avatarDecorations => { + avatarDecorations.value = _avatarDecorations; + loading.value = false; +}); + +function openDecoration(avatarDecoration, index?: number) { + os.popup(defineAsyncComponent(() => import('./profile.avatar-decoration.dialog.vue')), { + decoration: avatarDecoration, + usingIndex: index, + }, { + 'attach': async (payload) => { + const decoration = { + id: avatarDecoration.id, + angle: payload.angle, + flipH: payload.flipH, + }; + 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, + }; + 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 = []; + }); +} +</script> + +<style lang="scss" module> +.current { + padding: 16px; + border-radius: var(--radius); +} + +.decorations { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + grid-gap: 12px; +} +</style> diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index a5d3835b93..5f0d1aee51 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -87,24 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-sparkles"></i></template> <template #label>{{ i18n.ts.avatarDecorations }}</template> - <div class="_gaps"> - <MkInfo>{{ i18n.t('_profile.avatarDecorationMax', { max: $i?.policies.avatarDecorationLimit }) }} ({{ i18n.t('remainingN', { n: $i?.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo> - - <MkButton v-if="$i.avatarDecorations.length > 0" danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton> - - <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="openDecoration(avatarDecoration)" - > - <div :class="$style.avatarDecorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div> - <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: avatarDecoration.url }]" forceShowDecoration/> - <i v-if="avatarDecoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => avatarDecoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.avatarDecorationLock" class="ti ti-lock"></i> - </div> - </div> - </div> + <XAvatarDecoration/> </MkFolder> <MkFolder> @@ -128,7 +111,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, reactive, ref, watch, defineAsyncComponent, onMounted, onUnmounted } from 'vue'; +import { computed, reactive, ref, watch, defineAsyncComponent } from 'vue'; +import XAvatarDecoration from './profile.avatar-decoration.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -150,7 +134,6 @@ import MkInfo from '@/components/MkInfo.vue'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance')); -const avatarDecorations = ref<any[]>([]); const profile = reactive({ name: $i.name, @@ -171,10 +154,6 @@ 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.value = _avatarDecorations; -}); - function addField() { fields.value.push({ id: Math.random().toString(), @@ -273,25 +252,6 @@ function changeBanner(ev) { }); } -function openDecoration(avatarDecoration) { - os.popup(defineAsyncComponent(() => import('./profile.avatar-decoration-dialog.vue')), { - decoration: avatarDecoration, - }, {}, '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 = []; - }); -} - const headerActions = computed(() => []); const headerTabs = computed(() => []); @@ -386,33 +346,4 @@ definePageMetadata({ .dragItemForm { flex-grow: 1; } - -.avatarDecoration { - cursor: pointer; - padding: 16px 16px 28px 16px; - border: solid 2px var(--divider); - border-radius: 8px; - text-align: center; - font-size: 90%; - overflow: clip; - contain: content; -} - -.avatarDecorationActive { - background-color: var(--accentedBg); - border-color: var(--accent); -} - -.avatarDecorationName { - position: relative; - z-index: 10; - font-weight: bold; - margin-bottom: 20px; -} - -.avatarDecorationLock { - position: absolute; - bottom: 12px; - right: 12px; -} </style> |