rudeshark.net/packages/client/src/pages/admin/emojis.vue

550 lines
12 KiB
Vue
Raw Normal View History

<template>
2023-04-08 02:01:42 +02:00
<div>
<MkStickyContainer>
<template #header
><MkPageHeader
v-model:tab="tab"
:actions="headerActions"
:tabs="headerTabs"
:display-back-button="true"
/></template>
<MkSpacer :content-max="900">
<div class="ogwlenmc">
<div v-if="tab === 'local'" class="local">
<MkInput v-model="query" :debounce="true" type="search">
<template #prefix
><i
class="ph-magnifying-glass ph-bold ph-lg"
></i
></template>
2022-07-20 15:24:26 +02:00
<template #label>{{ i18n.ts.search }}</template>
</MkInput>
2023-04-08 02:01:42 +02:00
<MkSwitch v-model="selectMode" style="margin: 8px 0">
<template #label>Select mode</template>
</MkSwitch>
<div
v-if="selectMode"
style="
display: flex;
gap: var(--margin);
flex-wrap: wrap;
"
>
<MkButton inline @click="selectAll"
>Select all</MkButton
>
<MkButton inline @click="setCategoryBulk"
>Set category</MkButton
>
<MkButton inline @click="addTagBulk"
>Add tag</MkButton
>
<MkButton inline @click="removeTagBulk"
>Remove tag</MkButton
>
<MkButton inline @click="setTagBulk"
>Set tag</MkButton
>
<MkButton inline @click="setLicenseBulk"
>Set license</MkButton
>
<MkButton inline danger @click="delBulk"
>Delete</MkButton
>
</div>
<MkPagination
ref="emojisPaginationComponent"
:pagination="pagination"
>
<template #empty
><span>{{
i18n.ts.noCustomEmojis
}}</span></template
>
<template #default="{ items }">
<div class="ldhfsamy">
<button
v-for="emoji in items"
:key="emoji.id"
class="emoji _panel _button"
:class="{
selected: selectedEmojis.includes(
emoji.id
),
}"
@click="
selectMode
? toggleSelect(emoji)
: edit(emoji)
"
>
<img
:src="emoji.url"
class="img"
:alt="emoji.name"
/>
<div class="body">
<div class="name _monospace">
{{ emoji.name }}
</div>
<div class="info">
{{ emoji.category }}
</div>
</div>
</button>
</div>
</template>
</MkPagination>
</div>
<div v-else-if="tab === 'remote'" class="remote">
<FormSplit>
<MkInput
v-model="queryRemote"
:debounce="true"
type="search"
>
<template #prefix
><i
class="ph-magnifying-glass ph-bold ph-lg"
></i
></template>
<template #label>{{ i18n.ts.search }}</template>
</MkInput>
<MkInput v-model="host" :debounce="true">
<template #label>{{ i18n.ts.host }}</template>
</MkInput>
</FormSplit>
<MkPagination :pagination="remotePagination">
<template #empty
><span>{{
i18n.ts.noCustomEmojis
}}</span></template
>
<template #default="{ items }">
<div class="ldhfsamy">
<div
v-for="emoji in items"
:key="emoji.id"
class="emoji _panel _button"
@click="remoteMenu(emoji, $event)"
>
<img
:src="emoji.url"
class="img"
:alt="emoji.name"
/>
<div class="body">
<div class="name _monospace">
{{ emoji.name }}
</div>
<div class="info">
{{ emoji.host }}
</div>
</div>
</div>
</div>
2023-04-08 02:01:42 +02:00
</template>
</MkPagination>
</div>
</div>
2023-04-08 02:01:42 +02:00
</MkSpacer>
</MkStickyContainer>
</div>
</template>
2022-01-12 16:47:05 +01:00
<script lang="ts" setup>
2023-04-08 02:01:42 +02:00
import {
computed,
defineAsyncComponent,
defineComponent,
ref,
toRef,
} from "vue";
import MkButton from "@/components/MkButton.vue";
import MkInput from "@/components/form/input.vue";
import MkPagination from "@/components/MkPagination.vue";
import MkTab from "@/components/MkTab.vue";
import MkSwitch from "@/components/form/switch.vue";
import FormSplit from "@/components/form/split.vue";
import { selectFile, selectFiles } from "@/scripts/select-file";
import * as os from "@/os";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
Migrate to Vue3 (#6587) * Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * wip * wip * wip * Update init.ts * Update drive-window.vue * wip * wip * Use PascalCase for components * Use PascalCase for components * update dep * wip * wip * wip * Update init.ts * wip * Update paging.ts * Update test.vue * watch deep * wip * lint * wip * wip * wip * wip * wiop * wip * Update webpack.config.ts * alllow null poll * wip * wip * wip * wiop * UI redesign & refactor (#6714) * wip * wip * wip * wip * wip * Update drive.vue * Update word-mute.vue * wip * wip * wip * clean up * wip * Update default.vue * wip * Update notes.vue * Update mfm.ts * Update index.home.vue * Update post-form.vue * Update post-form-attaches.vue * wip * Update post-form.vue * Update sidebar.vue * wip * wip * Update index.vue * wip * Update default.vue * Update index.vue * Update index.vue * wip * Update post-form-attaches.vue * Update note.vue * wip * clean up * Update notes.vue * wip * wip * Update ja-JP.yml * wip * wip * Update index.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update default.vue * wip * Update _dark.json5 * wip * wip * wip * clean up * wip * wip * Update index.vue * Update test.vue * wip * wip * fix * wip * wip * wip * wip * clena yop * wip * wip * Update store.ts * Update messaging-room.vue * Update default.widgets.vue * fix * wip * wip * Update modal.vue * wip * Update os.ts * Update os.ts * Update deck.vue * Update init.ts * wip * Update ja-JP.yml * v-sizeは単にwindowのresizeを監視するだけで良いかもしれない * Update modal.vue * wip * Update tooltip.ts * wip * wip * wip * wip * wip * Update image-viewer.vue * wip * wip * Update style.scss * Update style.scss * Update visitor.vue * wip * Update init.ts * Update init.ts * wip * wip * Update visitor.vue * Update visitor.vue * Update visitor.vue * Update visitor.vue * wip * wip * Update modal.vue * Update header.vue * Update menu.vue * Update about.vue * Update about-misskey.vue * wip * wip * Update visitor.vue * Update tooltip.ts * wip * Update drive.vue * wip * Update style.scss * Update header.vue * wip * wip * Update users.user.vue * Update announcements.vue * wip * wip * wip * Update emojis.vue * wip * Update emojis.vue * Update style.scss * Update users.vue * wip * Update style.scss * wip * Update welcome.entrance.vue * Update radio.vue * Update size.ts * Update emoji-edit-dialog.vue * wip * Update emojis.vue * wip * Update emojis.vue * Update emojis.vue * Update emojis.vue * wip * wip * wip * wip * Update file-dialog.vue * wip * wip * Update token-generate-window.vue * Update notification-setting-window.vue * wip * wip * Update _error_.vue * Update ja-JP.yml * wip * wip * Update store.ts * Update emojis.vue * Update emojis.vue * Update emojis.vue * Update announcements.vue * Update store.ts * wip * Update page-editor.vue * wip * wip * Update modal.vue * wip * Update select-file.ts * Update timeline.vue * Update emojis.vue * Update os.ts * wip * Update user-select.vue * Update mfm.ts * Update get-file-info.ts * Update drive.vue * Update init.ts * Update mfm.ts * wip * wip * Update window.vue * Update note.vue * wip * wip * Update user-info.vue * wip * wip * wip * wip * wip * Update header.vue * Update header.vue * wip * Update explore.vue * wip * wip * wip * Update webpack.config.ts * wip * wip * wip * wip * wip * wip * Update autocomplete.ts * wip * wip * wip * Update toast.vue * wip * Update post-form-dialog.vue * wip * wip * wip * wip * wip * Update users.vue * wip * Update explore.vue * wip * wip * wip * Update package.json * wip * Update icon-dialog.vue * wip * wip * Update user-preview.ts * wip * wip * wip * wip * wip * Update instance.vue * Update user-name.vue * Update federation.vue * Update instance.vue * wip * wip * Update tag.vue * wip * wip * wip * wip * wip * Update instance.vue * wip * Update os.ts * Update os.ts * wip * wip * wip * Update router.ts * wip * Update init.ts * Update note.vue * Update messages.vue * wip * wip * wip * wip * wip * google * wip * wip * wip * wip * Update theme-editor.vue * wip * wip * Update room.vue * Update channel-editor.vue * wip * Update window.vue * Update window.vue * wip * Update window.vue * Update window.vue * wip * Update menu.vue * wip * wip * wip * wip * Update messaging-room.vue * wip * Update post-form.vue * Update default.widgets.vue * Update window.vue * wip
2020-10-17 13:12:00 +02:00
2022-01-12 16:47:05 +01:00
const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
2023-04-08 02:01:42 +02:00
const tab = ref("local");
2022-01-12 16:47:05 +01:00
const query = ref(null);
const queryRemote = ref(null);
const host = ref(null);
const selectMode = ref(false);
const selectedEmojis = ref<string[]>([]);
2022-01-12 16:47:05 +01:00
const pagination = {
2023-04-08 02:01:42 +02:00
endpoint: "admin/emoji/list" as const,
2022-01-12 16:47:05 +01:00
limit: 30,
params: computed(() => ({
2023-04-08 02:01:42 +02:00
query: query.value && query.value !== "" ? query.value : null,
2022-01-12 16:47:05 +01:00
})),
};
2022-01-12 16:47:05 +01:00
const remotePagination = {
2023-04-08 02:01:42 +02:00
endpoint: "admin/emoji/list-remote" as const,
2022-01-12 16:47:05 +01:00
limit: 30,
params: computed(() => ({
2023-04-08 02:01:42 +02:00
query:
queryRemote.value && queryRemote.value !== ""
? queryRemote.value
: null,
host: host.value && host.value !== "" ? host.value : null,
2022-01-12 16:47:05 +01:00
})),
};
2022-01-12 16:47:05 +01:00
const selectAll = () => {
if (selectedEmojis.value.length > 0) {
selectedEmojis.value = [];
} else {
2023-04-08 02:01:42 +02:00
selectedEmojis.value = emojisPaginationComponent.value.items.map(
(item) => item.id
);
2022-01-12 16:47:05 +01:00
}
};
2020-02-13 17:09:39 +01:00
2022-01-12 16:47:05 +01:00
const toggleSelect = (emoji) => {
if (selectedEmojis.value.includes(emoji.id)) {
2023-04-08 02:01:42 +02:00
selectedEmojis.value = selectedEmojis.value.filter(
(x) => x !== emoji.id
);
2022-01-12 16:47:05 +01:00
} else {
selectedEmojis.value.push(emoji.id);
}
};
const add = async (ev: MouseEvent) => {
2022-01-28 03:53:12 +01:00
const files = await selectFiles(ev.currentTarget ?? ev.target, null);
2023-04-08 02:01:42 +02:00
const promise = Promise.all(
files.map((file) =>
os.api("admin/emoji/add", {
fileId: file.id,
})
)
);
2022-01-12 16:47:05 +01:00
promise.then(() => {
emojisPaginationComponent.value.reload();
});
os.promiseDialog(promise);
};
const edit = (emoji) => {
2023-04-08 02:01:42 +02:00
os.popup(
defineAsyncComponent(() => import("./emoji-edit-dialog.vue")),
{
emoji: emoji,
},
{
done: (result) => {
if (result.updated) {
emojisPaginationComponent.value.updateItem(
result.updated.id,
(oldEmoji: any) => ({
...oldEmoji,
...result.updated,
})
);
} else if (result.deleted) {
emojisPaginationComponent.value.removeItem(
(item) => item.id === emoji.id
);
}
},
2021-12-10 13:41:37 +01:00
},
2023-04-08 02:01:42 +02:00
"closed"
);
2022-01-12 16:47:05 +01:00
};
2021-12-10 13:41:37 +01:00
2022-01-12 16:47:05 +01:00
const im = (emoji) => {
2023-04-08 02:01:42 +02:00
os.apiWithDialog("admin/emoji/copy", {
2022-01-12 16:47:05 +01:00
emojiId: emoji.id,
});
};
const remoteMenu = (emoji, ev: MouseEvent) => {
2023-04-08 02:01:42 +02:00
os.popupMenu(
[
{
type: "label",
text: ":" + emoji.name + ":",
},
{
text: i18n.ts.import,
icon: "ph-plus ph-bold ph-lg",
action: () => {
im(emoji);
},
},
],
ev.currentTarget ?? ev.target
);
2022-01-12 16:47:05 +01:00
};
const menu = (ev: MouseEvent) => {
2023-04-08 02:01:42 +02:00
os.popupMenu(
[
{
icon: "ph-download-simple ph-bold ph-lg",
text: i18n.ts.export,
action: async () => {
os.api("export-custom-emojis", {})
.then(() => {
os.alert({
type: "info",
text: i18n.ts.exportRequested,
});
})
.catch((err) => {
os.alert({
type: "error",
text: err.message,
});
});
},
},
{
icon: "ph-upload-simple ph-bold ph-lg",
text: i18n.ts.import,
action: async () => {
const file = await selectFile(
ev.currentTarget ?? ev.target
);
os.api("admin/emoji/import-zip", {
fileId: file.id,
})
.then(() => {
os.alert({
type: "info",
text: i18n.ts.importRequested,
});
})
.catch((err) => {
os.alert({
type: "error",
text: err.message,
});
});
},
},
],
ev.currentTarget ?? ev.target
);
2022-01-12 16:47:05 +01:00
};
const setCategoryBulk = async () => {
const { canceled, result } = await os.inputText({
2023-04-08 02:01:42 +02:00
title: "Category",
2022-01-12 16:47:05 +01:00
});
if (canceled) return;
2023-04-08 02:01:42 +02:00
await os.apiWithDialog("admin/emoji/set-category-bulk", {
2022-01-12 16:47:05 +01:00
ids: selectedEmojis.value,
category: result,
});
emojisPaginationComponent.value.reload();
};
const addTagBulk = async () => {
const { canceled, result } = await os.inputText({
2023-04-08 02:01:42 +02:00
title: "Tag",
2022-01-12 16:47:05 +01:00
});
if (canceled) return;
2023-04-08 02:01:42 +02:00
await os.apiWithDialog("admin/emoji/add-aliases-bulk", {
2022-01-12 16:47:05 +01:00
ids: selectedEmojis.value,
2023-04-08 02:01:42 +02:00
aliases: result.split(" "),
2022-01-12 16:47:05 +01:00
});
emojisPaginationComponent.value.reload();
};
const removeTagBulk = async () => {
const { canceled, result } = await os.inputText({
2023-04-08 02:01:42 +02:00
title: "Tag",
2022-01-12 16:47:05 +01:00
});
if (canceled) return;
2023-04-08 02:01:42 +02:00
await os.apiWithDialog("admin/emoji/remove-aliases-bulk", {
2022-01-12 16:47:05 +01:00
ids: selectedEmojis.value,
2023-04-08 02:01:42 +02:00
aliases: result.split(" "),
2022-01-12 16:47:05 +01:00
});
emojisPaginationComponent.value.reload();
};
const setTagBulk = async () => {
const { canceled, result } = await os.inputText({
2023-04-08 02:01:42 +02:00
title: "Tag",
2022-01-12 16:47:05 +01:00
});
if (canceled) return;
2023-04-08 02:01:42 +02:00
await os.apiWithDialog("admin/emoji/set-aliases-bulk", {
2022-01-12 16:47:05 +01:00
ids: selectedEmojis.value,
2023-04-08 02:01:42 +02:00
aliases: result.split(" "),
2022-01-12 16:47:05 +01:00
});
emojisPaginationComponent.value.reload();
};
const setLicenseBulk = async () => {
const { canceled, result } = await os.inputParagraph({
2023-04-08 02:01:42 +02:00
title: "License",
});
if (canceled) return;
2023-04-08 02:01:42 +02:00
await os.apiWithDialog("admin/emoji/set-license-bulk", {
ids: selectedEmojis.value,
license: result,
});
emojisPaginationComponent.value.reload();
};
2022-01-12 16:47:05 +01:00
const delBulk = async () => {
const { canceled } = await os.confirm({
2023-04-08 02:01:42 +02:00
type: "warning",
text: i18n.ts.deleteConfirm,
2022-01-12 16:47:05 +01:00
});
if (canceled) return;
2023-04-08 02:01:42 +02:00
await os.apiWithDialog("admin/emoji/delete-bulk", {
2022-01-12 16:47:05 +01:00
ids: selectedEmojis.value,
});
emojisPaginationComponent.value.reload();
};
2023-04-08 02:01:42 +02:00
const headerActions = $computed(() => [
{
asFullButton: true,
icon: "ph-plus ph-bold ph-lg",
text: i18n.ts.addEmoji,
handler: add,
},
{
icon: "ph-dots-three-outline ph-bold ph-lg",
handler: menu,
},
]);
2023-04-08 02:01:42 +02:00
const headerTabs = $computed(() => [
{
key: "local",
icon: "ph-hand-fist ph-bold ph-lg",
title: i18n.ts.local,
},
{
key: "remote",
icon: "ph-planet ph-bold ph-lg",
title: i18n.ts.remote,
},
]);
2023-04-08 02:01:42 +02:00
definePageMetadata(
computed(() => ({
title: i18n.ts.customEmojis,
icon: "ph-smiley ph-bold ph-lg",
}))
);
</script>
<style lang="scss" scoped>
.ogwlenmc {
> .local {
2021-12-10 13:41:37 +01:00
.empty {
margin: var(--margin);
2021-10-31 11:22:19 +01:00
}
2022-11-07 04:14:55 +01:00
.ldhfsamy {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
grid-gap: 12px;
2021-12-10 13:41:37 +01:00
margin: var(--margin) 0;
2023-04-08 02:01:42 +02:00
> .emoji {
display: flex;
align-items: center;
2022-01-12 16:47:05 +01:00
padding: 11px;
text-align: left;
2022-01-12 16:47:05 +01:00
border: solid 1px var(--panel);
&:hover {
2022-01-12 16:47:05 +01:00
border-color: var(--inputBorderHover);
}
&.selected {
border-color: var(--accent);
}
> .img {
width: 42px;
height: 42px;
}
> .body {
padding: 0 0 0 8px;
white-space: nowrap;
overflow: hidden;
> .name {
text-overflow: ellipsis;
overflow: hidden;
}
2020-02-13 17:09:39 +01:00
> .info {
opacity: 0.5;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
}
}
> .remote {
2021-12-10 13:41:37 +01:00
.empty {
margin: var(--margin);
}
.ldhfsamy {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
grid-gap: 12px;
2021-12-10 13:41:37 +01:00
margin: var(--margin) 0;
> .emoji {
display: flex;
align-items: center;
padding: 12px;
text-align: left;
&:hover {
color: var(--accent);
}
Migrate to Vue3 (#6587) * Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * wip * wip * wip * Update init.ts * Update drive-window.vue * wip * wip * Use PascalCase for components * Use PascalCase for components * update dep * wip * wip * wip * Update init.ts * wip * Update paging.ts * Update test.vue * watch deep * wip * lint * wip * wip * wip * wip * wiop * wip * Update webpack.config.ts * alllow null poll * wip * wip * wip * wiop * UI redesign & refactor (#6714) * wip * wip * wip * wip * wip * Update drive.vue * Update word-mute.vue * wip * wip * wip * clean up * wip * Update default.vue * wip * Update notes.vue * Update mfm.ts * Update index.home.vue * Update post-form.vue * Update post-form-attaches.vue * wip * Update post-form.vue * Update sidebar.vue * wip * wip * Update index.vue * wip * Update default.vue * Update index.vue * Update index.vue * wip * Update post-form-attaches.vue * Update note.vue * wip * clean up * Update notes.vue * wip * wip * Update ja-JP.yml * wip * wip * Update index.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update default.vue * wip * Update _dark.json5 * wip * wip * wip * clean up * wip * wip * Update index.vue * Update test.vue * wip * wip * fix * wip * wip * wip * wip * clena yop * wip * wip * Update store.ts * Update messaging-room.vue * Update default.widgets.vue * fix * wip * wip * Update modal.vue * wip * Update os.ts * Update os.ts * Update deck.vue * Update init.ts * wip * Update ja-JP.yml * v-sizeは単にwindowのresizeを監視するだけで良いかもしれない * Update modal.vue * wip * Update tooltip.ts * wip * wip * wip * wip * wip * Update image-viewer.vue * wip * wip * Update style.scss * Update style.scss * Update visitor.vue * wip * Update init.ts * Update init.ts * wip * wip * Update visitor.vue * Update visitor.vue * Update visitor.vue * Update visitor.vue * wip * wip * Update modal.vue * Update header.vue * Update menu.vue * Update about.vue * Update about-misskey.vue * wip * wip * Update visitor.vue * Update tooltip.ts * wip * Update drive.vue * wip * Update style.scss * Update header.vue * wip * wip * Update users.user.vue * Update announcements.vue * wip * wip * wip * Update emojis.vue * wip * Update emojis.vue * Update style.scss * Update users.vue * wip * Update style.scss * wip * Update welcome.entrance.vue * Update radio.vue * Update size.ts * Update emoji-edit-dialog.vue * wip * Update emojis.vue * wip * Update emojis.vue * Update emojis.vue * Update emojis.vue * wip * wip * wip * wip * Update file-dialog.vue * wip * wip * Update token-generate-window.vue * Update notification-setting-window.vue * wip * wip * Update _error_.vue * Update ja-JP.yml * wip * wip * Update store.ts * Update emojis.vue * Update emojis.vue * Update emojis.vue * Update announcements.vue * Update store.ts * wip * Update page-editor.vue * wip * wip * Update modal.vue * wip * Update select-file.ts * Update timeline.vue * Update emojis.vue * Update os.ts * wip * Update user-select.vue * Update mfm.ts * Update get-file-info.ts * Update drive.vue * Update init.ts * Update mfm.ts * wip * wip * Update window.vue * Update note.vue * wip * wip * Update user-info.vue * wip * wip * wip * wip * wip * Update header.vue * Update header.vue * wip * Update explore.vue * wip * wip * wip * Update webpack.config.ts * wip * wip * wip * wip * wip * wip * Update autocomplete.ts * wip * wip * wip * Update toast.vue * wip * Update post-form-dialog.vue * wip * wip * wip * wip * wip * Update users.vue * wip * Update explore.vue * wip * wip * wip * Update package.json * wip * Update icon-dialog.vue * wip * wip * Update user-preview.ts * wip * wip * wip * wip * wip * Update instance.vue * Update user-name.vue * Update federation.vue * Update instance.vue * wip * wip * Update tag.vue * wip * wip * wip * wip * wip * Update instance.vue * wip * Update os.ts * Update os.ts * wip * wip * wip * Update router.ts * wip * Update init.ts * Update note.vue * Update messages.vue * wip * wip * wip * wip * wip * google * wip * wip * wip * wip * Update theme-editor.vue * wip * wip * Update room.vue * Update channel-editor.vue * wip * Update window.vue * Update window.vue * wip * Update window.vue * Update window.vue * wip * Update menu.vue * wip * wip * wip * wip * Update messaging-room.vue * wip * Update post-form.vue * Update default.widgets.vue * Update window.vue * wip
2020-10-17 13:12:00 +02:00
> .img {
width: 32px;
height: 32px;
}
> .body {
padding: 0 0 0 8px;
white-space: nowrap;
overflow: hidden;
> .name {
text-overflow: ellipsis;
overflow: hidden;
}
> .info {
opacity: 0.5;
font-size: 90%;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
}
}
}
</style>