diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-05-18 18:45:49 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2023-05-18 18:45:49 +0900 |
| commit | 7ce569424a67c786a4f44993ce74f06989eba9df (patch) | |
| tree | 895e142f514452310382ce8c315feebcce27b1e9 /packages/frontend | |
| parent | perf(backend): pre-compile regexp (diff) | |
| download | misskey-7ce569424a67c786a4f44993ce74f06989eba9df.tar.gz misskey-7ce569424a67c786a4f44993ce74f06989eba9df.tar.bz2 misskey-7ce569424a67c786a4f44993ce74f06989eba9df.zip | |
feat: カスタム絵文字ごとにそれをリアクションとして使えるロールを設定できるように
Diffstat (limited to 'packages/frontend')
| -rw-r--r-- | packages/frontend/src/components/MkRolePreview.vue | 13 | ||||
| -rw-r--r-- | packages/frontend/src/pages/custom-emojis-manager.vue | 19 | ||||
| -rw-r--r-- | packages/frontend/src/pages/emoji-edit-dialog.vue | 214 |
3 files changed, 186 insertions, 60 deletions
diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue index 2f5866f340..9fbe1ec993 100644 --- a/packages/frontend/src/components/MkRolePreview.vue +++ b/packages/frontend/src/components/MkRolePreview.vue @@ -12,8 +12,10 @@ </template> </span> <span :class="$style.name">{{ role.name }}</span> - <span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span> - <span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span> + <template v-if="detailed"> + <span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span> + <span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span> + </template> </div> <div :class="$style.description">{{ role.description }}</div> </MkA> @@ -23,10 +25,13 @@ import { } from 'vue'; import { i18n } from '@/i18n'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ role: any; forModeration: boolean; -}>(); + detailed: boolean; +}>(), { + detailed: true, +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 3f13f0787d..3da6a0d9cb 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -2,7 +2,7 @@ <div> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="900"> + <MkSpacer :contentMax="900"> <div class="ogwlenmc"> <div v-if="tab === 'local'" class="local"> <MkInput v-model="query" :debounce="true" type="search"> @@ -123,15 +123,14 @@ const toggleSelect = (emoji) => { }; const add = async (ev: MouseEvent) => { - const files = await selectFiles(ev.currentTarget ?? ev.target, null); - - const promise = Promise.all(files.map(file => os.api('admin/emoji/add', { - fileId: file.id, - }))); - promise.then(() => { - emojisPaginationComponent.value.reload(); - }); - os.promiseDialog(promise); + os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { + }, { + done: result => { + if (result.created) { + emojisPaginationComponent.value.prepend(result.created); + } + }, + }, 'closed'); }; const edit = (emoji) => { diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 3c829d6a8e..24b72b6f7f 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -1,84 +1,168 @@ <template> <MkModalWindow ref="dialog" - :width="370" - :with-ok-button="true" - @close="$refs.dialog.close()" + :width="400" + @close="dialog.close()" @closed="$emit('closed')" - @ok="ok()" > - <template #header>:{{ emoji.name }}:</template> + <template v-if="emoji" #header>:{{ emoji.name }}:</template> + <template v-else #header>New emoji</template> - <MkSpacer :margin-min="20" :margin-max="28"> - <div class="_gaps_m"> - <img :src="`/emoji/${emoji.name}.webp`" :class="$style.img"/> - <MkInput v-model="name"> - <template #label>{{ i18n.ts.name }}</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> - <MkButton danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + <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"> + <template #label>{{ i18n.ts.name }}</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> + <template #label>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction }}</template> + <div class="_gaps"> + <MkInfo>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription }}</MkInfo> + + <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"/> + <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> + </div> + </MkFolder> + <MkSwitch v-model="isSensitive">isSensitive</MkSwitch> + <MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch> + <MkButton danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </MkSpacer> + <div :class="$style.footer"> + <MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton> </div> - </MkSpacer> + </div> </MkModalWindow> </template> <script lang="ts" setup> -import { } from 'vue'; +import { computed, watch } from 'vue'; 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'; import { i18n } from '@/i18n'; import { customEmojiCategories } from '@/custom-emojis'; +import MkSwitch from '@/components/MkSwitch.vue'; +import { selectFile, selectFiles } from '@/scripts/select-file'; +import MkRolePreview from '@/components/MkRolePreview.vue'; const props = defineProps<{ - emoji: any, + emoji?: any, }>(); let dialog = $ref(null); -let name: string = $ref(props.emoji.name); -let category: string = $ref(props.emoji.category); -let aliases: string = $ref(props.emoji.aliases.join(' ')); -let license: string = $ref(props.emoji.license ?? ''); +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(); + +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 emit = defineEmits<{ - (ev: 'done', v: { deleted?: boolean, updated?: any }): void, + (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void, (ev: 'closed'): void }>(); -function ok() { - update(); +async function changeImage(ev) { + file = await selectFile(ev.currentTarget ?? ev.target, null); } -async function update() { - await os.apiWithDialog('admin/emoji/update', { - id: props.emoji.id, +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 => !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 done() { + const params = { name, category, aliases: aliases.split(' '), license: license === '' ? null : license, - }); + isSensitive, + localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id), + }; + + if (file) { + params.fileId = file.id; + } - emit('done', { - updated: { + if (props.emoji) { + await os.apiWithDialog('admin/emoji/update', { id: props.emoji.id, - name, - category, - aliases: aliases.split(' '), - license: license === '' ? null : license, - }, - }); + ...params, + }); + + emit('done', { + updated: { + id: props.emoji.id, + ...params, + }, + }); + + dialog.close(); + } else { + const created = await os.apiWithDialog('admin/emoji/add', params); + + emit('done', { + created: created, + }); - dialog.close(); + dialog.close(); + } } async function del() { @@ -100,9 +184,47 @@ async function del() { </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; - margin: 0 auto; + 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)); } </style> |