Skip to content

Commit

Permalink
enhance(frontend): 同じ種類のデコレーションを複数付けられるように
Browse files Browse the repository at this point in the history
  • Loading branch information
syuilo authored and camilla-ett committed Jan 2, 2024
1 parent fefe665 commit 222be32
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 96 deletions.
6 changes: 3 additions & 3 deletions packages/frontend/src/components/global/MkAvatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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)}°`">
Expand All @@ -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>
Expand All @@ -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>
Expand Down
125 changes: 125 additions & 0 deletions packages/frontend/src/pages/settings/profile.avatar-decoration.vue
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit 222be32

Please sign in to comment.