iceshrimp/packages/client/src/pages/admin/emojis.vue
2023-11-10 02:04:31 +01:00

550 lines
12 KiB
Vue

<template>
<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>
<template #label>{{ i18n.ts.search }}</template>
</MkInput>
<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>
</template>
</MkPagination>
</div>
</div>
</MkSpacer>
</MkStickyContainer>
</div>
</template>
<script lang="ts" setup>
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";
const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
const tab = ref("local");
const query = ref(null);
const queryRemote = ref(null);
const host = ref(null);
const selectMode = ref(false);
const selectedEmojis = ref<string[]>([]);
const pagination = {
endpoint: "admin/emoji/list" as const,
limit: 30,
params: computed(() => ({
query: query.value && query.value !== "" ? query.value : null,
})),
};
const remotePagination = {
endpoint: "admin/emoji/list-remote" as const,
limit: 30,
params: computed(() => ({
query:
queryRemote.value && queryRemote.value !== ""
? queryRemote.value
: null,
host: host.value && host.value !== "" ? host.value : null,
})),
};
const selectAll = () => {
if (selectedEmojis.value.length > 0) {
selectedEmojis.value = [];
} else {
selectedEmojis.value = emojisPaginationComponent.value.items.map(
(item) => item.id,
);
}
};
const toggleSelect = (emoji) => {
if (selectedEmojis.value.includes(emoji.id)) {
selectedEmojis.value = selectedEmojis.value.filter(
(x) => x !== emoji.id,
);
} else {
selectedEmojis.value.push(emoji.id);
}
};
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);
};
const edit = (emoji) => {
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,
);
}
},
},
"closed",
);
};
const im = (emoji) => {
os.apiWithDialog("admin/emoji/copy", {
emojiId: emoji.id,
});
};
const remoteMenu = (emoji, ev: MouseEvent) => {
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,
);
};
const menu = (ev: MouseEvent) => {
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,
);
};
const setCategoryBulk = async () => {
const { canceled, result } = await os.inputText({
title: "Category",
});
if (canceled) return;
await os.apiWithDialog("admin/emoji/set-category-bulk", {
ids: selectedEmojis.value,
category: result,
});
emojisPaginationComponent.value.reload();
};
const addTagBulk = async () => {
const { canceled, result } = await os.inputText({
title: "Tag",
});
if (canceled) return;
await os.apiWithDialog("admin/emoji/add-aliases-bulk", {
ids: selectedEmojis.value,
aliases: result.split(" "),
});
emojisPaginationComponent.value.reload();
};
const removeTagBulk = async () => {
const { canceled, result } = await os.inputText({
title: "Tag",
});
if (canceled) return;
await os.apiWithDialog("admin/emoji/remove-aliases-bulk", {
ids: selectedEmojis.value,
aliases: result.split(" "),
});
emojisPaginationComponent.value.reload();
};
const setTagBulk = async () => {
const { canceled, result } = await os.inputText({
title: "Tag",
});
if (canceled) return;
await os.apiWithDialog("admin/emoji/set-aliases-bulk", {
ids: selectedEmojis.value,
aliases: result.split(" "),
});
emojisPaginationComponent.value.reload();
};
const setLicenseBulk = async () => {
const { canceled, result } = await os.inputParagraph({
title: "License",
});
if (canceled) return;
await os.apiWithDialog("admin/emoji/set-license-bulk", {
ids: selectedEmojis.value,
license: result,
});
emojisPaginationComponent.value.reload();
};
const delBulk = async () => {
const { canceled } = await os.confirm({
type: "warning",
text: i18n.ts.deleteConfirm,
});
if (canceled) return;
await os.apiWithDialog("admin/emoji/delete-bulk", {
ids: selectedEmojis.value,
});
emojisPaginationComponent.value.reload();
};
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,
},
]);
const headerTabs = $computed(() => [
{
key: "local",
icon: "ph-users ph-bold ph-lg",
title: i18n.ts.local,
},
{
key: "remote",
icon: "ph-planet ph-bold ph-lg",
title: i18n.ts.remote,
},
]);
definePageMetadata(
computed(() => ({
title: i18n.ts.customEmojis,
icon: "ph-smiley ph-bold ph-lg",
})),
);
</script>
<style lang="scss" scoped>
.ogwlenmc {
> .local {
.empty {
margin: var(--margin);
}
.ldhfsamy {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
grid-gap: 12px;
margin: var(--margin) 0;
> .emoji {
display: flex;
align-items: center;
padding: 11px;
text-align: left;
border: solid 1px var(--panel);
&:hover {
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;
}
> .info {
opacity: 0.5;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
}
}
> .remote {
.empty {
margin: var(--margin);
}
.ldhfsamy {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
grid-gap: 12px;
margin: var(--margin) 0;
> .emoji {
display: flex;
align-items: center;
padding: 12px;
text-align: left;
&:hover {
color: var(--accent);
}
> .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>