1
0
forked from mirror/misskey
mi.moris.day/packages/frontend/src/components/MkEmojiEditDialog.vue
mattyatea 81ee7d9938
fix
2023-10-16 21:46:52 +09:00

333 lines
9.6 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="400"
:withOkButton="false "
@close="dialog.close()"
@closed="$emit('closed')"
>
<template v-if="emoji" #header>:{{ emoji.name }}:</template>
<template v-else-if="isRequest" #header>{{ i18n.ts.requestCustomEmojis }}</template>
<template v-else #header>New emoji</template>
<div>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps_m">
<div v-if="imgUrl != null" :class="$style.imgs">
<div style="background: #000;" :class="$style.imgContainer">
<img :src="imgUrl" :class="$style.img"/>
</div>
<div style="background: #222;" :class="$style.imgContainer">
<img :src="imgUrl" :class="$style.img"/>
</div>
<div style="background: #ddd;" :class="$style.imgContainer">
<img :src="imgUrl" :class="$style.img"/>
</div>
<div style="background: #fff;" :class="$style.imgContainer">
<img :src="imgUrl" :class="$style.img"/>
</div>
</div>
<MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton>
<MkInput v-model="name" pattern="[a-z0-9_]">
<template #label>{{ i18n.ts.name }}</template>
<template #caption>{{ i18n.ts.emojiNameValidation }}</template>
</MkInput>
<MkInput v-model="category" :datalist="customEmojiCategories">
<template #label>{{ i18n.ts.category }}</template>
</MkInput>
<MkInput v-model="aliases">
<template #label>{{ i18n.ts.tags }}</template>
<template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template>
</MkInput>
<MkInput v-model="license">
<template #label>{{ i18n.ts.license }}</template>
</MkInput>
<MkFolder v-if="!isRequest">
<template #label>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction }}</template>
<template #suffix>{{ rolesThatCanBeUsedThisEmojiAsReaction.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisEmojiAsReaction.length }}</template>
<div class="_gaps">
<MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<div v-for="role in rolesThatCanBeUsedThisEmojiAsReaction" :key="role.id" :class="$style.roleItem">
<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button>
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
</div>
<MkInfo>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription }}</MkInfo>
<MkInfo warn>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}</MkInfo>
</div>
</MkFolder>
<MkSwitch v-model="isSensitive">{{ i18n.ts.isSensitive }}</MkSwitch>
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
<MkSwitch v-if="!isRequest" v-model="draft" :disabled="isRequest">
{{ i18n.ts.draft }}
</MkSwitch>
</div>
</MkSpacer>
<div :class="$style.footer">
<div :class="$style.footerButtons">
<MkButton v-if="!isRequest" danger rounded style="margin: 0 auto;" @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
<MkButton v-if="validation" primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton>
<MkButton v-else rounded style="margin: 0 auto;"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton>
</div>
</div>
</div>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { computed, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { DriveFile } from 'misskey-js/built/entities.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { customEmojiCategories } from '@/custom-emojis.js';
import MkSwitch from '@/components/MkSwitch.vue';
import { selectFile, selectFiles } from '@/scripts/select-file.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
const props = defineProps<{
emoji?: any,
isRequest: boolean,
}>();
let dialog = $ref(null);
let name: string = $ref(props.emoji ? props.emoji.name : '');
let category: string = $ref(props.emoji ? props.emoji.category : '');
let aliases: string = $ref(props.emoji ? props.emoji.aliases.join(' ') : '');
let license: string = $ref(props.emoji ? (props.emoji.license ?? '') : '');
let isSensitive = $ref(props.emoji ? props.emoji.isSensitive : false);
let localOnly = $ref(props.emoji ? props.emoji.localOnly : false);
let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]);
let file = $ref<Misskey.entities.DriveFile>();
let chooseFile: DriveFile|null = $ref(null);
let draft = $ref(props.emoji ? props.emoji.draft : false);
let isRequest = $ref(props.isRequest);
let url;
watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => {
rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
}, { immediate: true });
const imgUrl = computed(() => file ? file.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
const validation = computed(() => {
return name.match(/^[a-zA-Z0-9_]+$/) && imgUrl.value != null;
});
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
(ev: 'closed'): void
}>();
function ok() {
if (isRequest) {
if (chooseFile !== null && name.match(/^[a-zA-Z0-9_]+$/)) {
add();
}
} else {
update();
}
}
async function add() {
const ret = await os.api('admin/emoji/add-draft', {
name: name,
category: category,
aliases: aliases.split(' '),
license: license === '' ? null : license,
fileId: chooseFile.id,
});
emit('done', {
updated: {
id: ret.id,
name,
category,
aliases: aliases.split(' '),
license: license === '' ? null : license,
draft: true,
},
});
dialog.close();
}
async function changeImage(ev) {
file = await selectFile(ev.currentTarget ?? ev.target, null);
const candidate = file.name.replace(/\.(.+)$/, '');
if (candidate.match(/^[a-z0-9_]+$/)) {
name = candidate;
}
}
async function addRole() {
const roles = await os.api('admin/roles/list');
const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id);
const { canceled, result: role } = await os.select({
items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
});
if (canceled) return;
rolesThatCanBeUsedThisEmojiAsReaction.push(role);
}
async function removeRole(role, ev) {
rolesThatCanBeUsedThisEmojiAsReaction = rolesThatCanBeUsedThisEmojiAsReaction.filter(x => x.id !== role.id);
}
async function update() {
await os.apiWithDialog('admin/emoji/update', {
id: props.emoji.id,
name,
category,
aliases: aliases.split(' '),
license: license === '' ? null : license,
fileId: chooseFile?.id,
draft: draft,
});
emit('done', {
updated: {
id: props.emoji.id,
name,
category,
aliases: aliases.split(' '),
license: license === '' ? null : license,
draft: draft,
},
});
dialog.close();
}
async function done() {
const params = {
name,
category: category === '' ? null : category,
aliases: aliases.replace(' ', ' ').split(' ').filter(x => x !== ''),
license: license === '' ? null : license,
draft: draft,
isSensitive,
localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id),
};
if (file) {
params.fileId = file.id;
}
console.log(props.emoji);
if (props.emoji) {
await os.apiWithDialog('admin/emoji/update', {
id: props.emoji.id,
...params,
});
emit('done', {
updated: {
id: props.emoji.id,
...params,
},
});
dialog.close();
} else {
const created = isRequest
? await os.apiWithDialog('admin/emoji/add-draft', params)
: await os.apiWithDialog('admin/emoji/add', params);
emit('done', {
created: created,
});
dialog.close();
}
}
function chooseFileFrom(ev) {
selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
chooseFile = files_[0];
url = chooseFile.url;
});
}
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: name }),
});
if (canceled) return;
os.api('admin/emoji/delete', {
id: props.emoji.id,
}).then(() => {
emit('done', {
deleted: true,
});
dialog.close();
});
}
</script>
<style lang="scss" module>
.imgs {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
.imgContainer {
padding: 8px;
border-radius: 6px;
}
.img {
display: block;
height: 64px;
width: 64px;
object-fit: contain;
}
.roleItem {
display: flex;
}
.role {
flex: 1;
}
.roleUnassign {
width: 32px;
height: 32px;
margin-left: 8px;
align-self: center;
}
.footer {
position: sticky;
bottom: 0;
left: 0;
padding: 12px;
border-top: solid 0.5px var(--divider);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
.footerButtons {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
</style>