summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-12-14 11:29:27 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-12-14 11:29:27 +0900
commit839b7483ac85dc358cf116594f0003ca42e9b021 (patch)
treef7cf92fe7b2c0e335dab0b06f43b084cb42219a2 /packages/frontend/src
parentfix(backend): モデレーションログがモデレーターは閲覧でき... (diff)
downloadmisskey-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.vue6
-rw-r--r--packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue67
-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.vue125
-rw-r--r--packages/frontend/src/pages/settings/profile.vue75
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>