enhance: アイコンデコレーションを複数設定できるように
This commit is contained in:
@@ -284,7 +284,7 @@ export async function openAccountMenu(opts: {
|
||||
text: i18n.ts.profile,
|
||||
to: `/@${ $i.username }`,
|
||||
avatar: $i,
|
||||
}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
|
||||
}, { type: 'divider' }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
|
||||
type: 'parent' as const,
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.addAccount,
|
||||
|
@@ -39,6 +39,7 @@ import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
folder: Misskey.entities.DriveFolder;
|
||||
@@ -250,7 +251,7 @@ function setAsUploadFolder() {
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
let menu;
|
||||
let menu: MenuItem[];
|
||||
menu = [{
|
||||
text: i18n.ts.openInWindow,
|
||||
icon: 'ti ti-app-window',
|
||||
@@ -260,18 +261,18 @@ function onContextmenu(ev: MouseEvent) {
|
||||
}, {
|
||||
}, 'closed');
|
||||
},
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
text: i18n.ts.rename,
|
||||
icon: 'ti ti-forms',
|
||||
action: rename,
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
text: i18n.ts.delete,
|
||||
icon: 'ti ti-trash',
|
||||
danger: true,
|
||||
action: deleteFolder,
|
||||
}];
|
||||
if (defaultStore.state.devMode) {
|
||||
menu = menu.concat([null, {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
icon: 'ti ti-id',
|
||||
text: i18n.ts.copyFolderId,
|
||||
action: () => {
|
||||
|
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
|
||||
import contains from '@/scripts/contains.js';
|
||||
import * as os from '@/os.js';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
@@ -57,7 +57,7 @@ function onContextmenu(ev) {
|
||||
action: () => {
|
||||
router.push(props.to, 'forcePage');
|
||||
},
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
|
@@ -23,16 +23,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
v-if="showDecoration && (decoration || user.avatarDecorations.length > 0)"
|
||||
:class="[$style.decoration]"
|
||||
:src="decoration?.url ?? user.avatarDecorations[0].url"
|
||||
:style="{
|
||||
rotate: getDecorationAngle(),
|
||||
scale: getDecorationScale(),
|
||||
}"
|
||||
alt=""
|
||||
>
|
||||
<template v-if="showDecoration">
|
||||
<img
|
||||
v-for="decoration in decorations ?? user.avatarDecorations"
|
||||
:class="[$style.decoration]"
|
||||
:src="decoration.url"
|
||||
:style="{
|
||||
rotate: getDecorationAngle(decoration),
|
||||
scale: getDecorationScale(decoration),
|
||||
}"
|
||||
alt=""
|
||||
>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -57,19 +59,14 @@ const props = withDefaults(defineProps<{
|
||||
link?: boolean;
|
||||
preview?: boolean;
|
||||
indicator?: boolean;
|
||||
decoration?: {
|
||||
url: string;
|
||||
angle?: number;
|
||||
flipH?: boolean;
|
||||
flipV?: boolean;
|
||||
};
|
||||
decorations?: Misskey.entities.UserDetailed['avatarDecorations'][number][];
|
||||
forceShowDecoration?: boolean;
|
||||
}>(), {
|
||||
target: null,
|
||||
link: false,
|
||||
preview: false,
|
||||
indicator: false,
|
||||
decoration: undefined,
|
||||
decorations: undefined,
|
||||
forceShowDecoration: false,
|
||||
});
|
||||
|
||||
@@ -92,27 +89,13 @@ function onClick(ev: MouseEvent): void {
|
||||
emit('click', ev);
|
||||
}
|
||||
|
||||
function getDecorationAngle() {
|
||||
let angle;
|
||||
if (props.decoration) {
|
||||
angle = props.decoration.angle ?? 0;
|
||||
} else if (props.user.avatarDecorations.length > 0) {
|
||||
angle = props.user.avatarDecorations[0].angle ?? 0;
|
||||
} else {
|
||||
angle = 0;
|
||||
}
|
||||
function getDecorationAngle(decoration: Misskey.entities.UserDetailed['avatarDecorations'][number]) {
|
||||
const angle = decoration.angle ?? 0;
|
||||
return angle === 0 ? undefined : `${angle * 360}deg`;
|
||||
}
|
||||
|
||||
function getDecorationScale() {
|
||||
let scaleX;
|
||||
if (props.decoration) {
|
||||
scaleX = props.decoration.flipH ? -1 : 1;
|
||||
} else if (props.user.avatarDecorations.length > 0) {
|
||||
scaleX = props.user.avatarDecorations[0].flipH ? -1 : 1;
|
||||
} else {
|
||||
scaleX = 1;
|
||||
}
|
||||
function getDecorationScale(decoration: Misskey.entities.UserDetailed['avatarDecorations'][number]) {
|
||||
const scaleX = decoration.flipH ? -1 : 1;
|
||||
return scaleX === 1 ? undefined : `${scaleX} 1`;
|
||||
}
|
||||
|
||||
|
@@ -81,6 +81,7 @@ export const ROLE_POLICIES = [
|
||||
'userListLimit',
|
||||
'userEachUserListsLimit',
|
||||
'rateLimitFactor',
|
||||
'avatarDecorationLimit',
|
||||
] as const;
|
||||
|
||||
// なんか動かない
|
||||
|
@@ -531,6 +531,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.avatarDecorationLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.avatarDecorationLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.avatarDecorationLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.avatarDecorationLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.avatarDecorationLimit.value" type="number" :min="0">
|
||||
<template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.avatarDecorationLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</FormSlot>
|
||||
</div>
|
||||
@@ -549,7 +569,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRange from '@/components/MkRange.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { ROLE_POLICIES } from '@/const';
|
||||
import { ROLE_POLICIES } from '@/const.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
|
||||
|
@@ -192,6 +192,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
|
||||
<template #suffix>{{ policies.avatarDecorationLimit }}</template>
|
||||
<MkInput v-model="policies.avatarDecorationLimit" type="number" :min="0">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
@@ -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" :decoration="{ url: decoration.url, angle, flipH }" forceShowDecoration/>
|
||||
<MkAvatar style="width: 64px; height: 64px; margin-bottom: 20px;" :user="$i" :decorations="[...$i.avatarDecorations, { url: decoration.url, angle, flipH }]" 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)}°`">
|
||||
@@ -54,6 +54,7 @@ const props = defineProps<{
|
||||
decoration: {
|
||||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
}
|
||||
}>();
|
||||
|
||||
@@ -77,18 +78,18 @@ async function attach() {
|
||||
flipH: flipH.value,
|
||||
};
|
||||
await os.apiWithDialog('i/update', {
|
||||
avatarDecorations: [decoration],
|
||||
avatarDecorations: [...$i.avatarDecorations, decoration],
|
||||
});
|
||||
$i.avatarDecorations = [decoration];
|
||||
$i.avatarDecorations = [...$i.avatarDecorations, decoration];
|
||||
|
||||
dialog.value.close();
|
||||
}
|
||||
|
||||
async function detach() {
|
||||
await os.apiWithDialog('i/update', {
|
||||
avatarDecorations: [],
|
||||
avatarDecorations: $i.avatarDecorations.filter(x => x.id !== props.decoration.id),
|
||||
});
|
||||
$i.avatarDecorations = [];
|
||||
$i.avatarDecorations = $i.avatarDecorations.filter(x => x.id !== props.decoration.id);
|
||||
|
||||
dialog.value.close();
|
||||
}
|
||||
|
@@ -87,16 +87,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #icon><i class="ti ti-sparkles"></i></template>
|
||||
<template #label>{{ i18n.ts.avatarDecorations }}</template>
|
||||
|
||||
<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" :decoration="{ 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 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>
|
||||
</MkFolder>
|
||||
@@ -273,6 +279,19 @@ function openDecoration(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(() => []);
|
||||
|
Reference in New Issue
Block a user