mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-29 07:49:53 +01:00
support remote import
This commit is contained in:
parent
84758b6eec
commit
dfe85d7722
13 changed files with 463 additions and 173 deletions
|
@ -24,39 +24,7 @@ export const meta = {
|
|||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
aliases: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
ref: 'EmojiDetailed',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -8,13 +8,12 @@ export type RegisterLogItem = {
|
|||
};
|
||||
|
||||
export type GridItem = {
|
||||
readonly id?: string;
|
||||
readonly fileId?: string;
|
||||
readonly url: string;
|
||||
|
||||
deleteCheck: boolean;
|
||||
|
||||
checked: boolean;
|
||||
id?: string;
|
||||
fileId?: string;
|
||||
url: string;
|
||||
name: string;
|
||||
host: string;
|
||||
category: string;
|
||||
aliases: string;
|
||||
license: string;
|
||||
|
@ -25,11 +24,12 @@ export type GridItem = {
|
|||
|
||||
export function fromEmojiDetailed(it: Misskey.entities.EmojiDetailed): GridItem {
|
||||
return {
|
||||
checked: false,
|
||||
id: it.id,
|
||||
fileId: undefined,
|
||||
url: it.url,
|
||||
deleteCheck: false,
|
||||
name: it.name,
|
||||
host: it.host ?? '',
|
||||
category: it.category ?? '',
|
||||
aliases: it.aliases.join(', '),
|
||||
license: it.license ?? '',
|
||||
|
@ -41,11 +41,12 @@ export function fromEmojiDetailed(it: Misskey.entities.EmojiDetailed): GridItem
|
|||
|
||||
export function fromDriveFile(it: Misskey.entities.DriveFile): GridItem {
|
||||
return {
|
||||
checked: false,
|
||||
id: undefined,
|
||||
fileId: it.id,
|
||||
url: it.url,
|
||||
deleteCheck: false,
|
||||
name: it.name.replace(/(\.[a-zA-Z0-9]+)+$/, ''),
|
||||
host: '',
|
||||
category: '',
|
||||
aliases: '',
|
||||
license: '',
|
||||
|
|
|
@ -1,27 +1,33 @@
|
|||
<template>
|
||||
<div class="_gaps">
|
||||
<div :class="$style.searchArea">
|
||||
<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off" style="flex: 1">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
<MkButton primary style="margin-left: auto;" @click="onSearchButtonClicked">
|
||||
{{ i18n.ts.search }}
|
||||
</MkButton>
|
||||
<div>
|
||||
<div v-if="gridItems.length === 0" style="text-align: center">
|
||||
登録された絵文字はありません。
|
||||
</div>
|
||||
|
||||
<div style="overflow-y: scroll; padding-top: 8px; padding-bottom: 8px;">
|
||||
<MkGrid :data="gridItems" :gridSetting="gridSetting" :columnSettings="columnSettings" @event="onGridEvent"/>
|
||||
</div>
|
||||
|
||||
<div class="_gaps">
|
||||
<div :class="$style.pages">
|
||||
<button @click="onLatestButtonClicked"><</button>
|
||||
<button @click="onOldestButtonClicked">></button>
|
||||
<div v-else class="_gaps">
|
||||
<div :class="$style.searchArea">
|
||||
<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off" style="flex: 1">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
<MkButton primary style="margin-left: auto;" @click="onSearchButtonClicked">
|
||||
{{ i18n.ts.search }}
|
||||
</MkButton>
|
||||
</div>
|
||||
|
||||
<div :class="$style.buttons">
|
||||
<MkButton primary :disabled="updateButtonDisabled" @click="onUpdateClicked">{{ i18n.ts.update }}</MkButton>
|
||||
<MkButton @click="onResetClicked">リセット</MkButton>
|
||||
<div style="overflow-y: scroll; padding-top: 8px; padding-bottom: 8px;">
|
||||
<MkGrid :data="gridItems" :gridSetting="gridSetting" :columnSettings="columnSettings" @event="onGridEvent"/>
|
||||
</div>
|
||||
|
||||
<div class="_gaps">
|
||||
<div :class="$style.pages">
|
||||
<button @click="onLatestButtonClicked"><</button>
|
||||
<button @click="onOldestButtonClicked">></button>
|
||||
</div>
|
||||
|
||||
<div :class="$style.buttons">
|
||||
<MkButton danger style="margin-right: auto" @click="onDeleteClicked">{{ i18n.ts.delete }}</MkButton>
|
||||
<MkButton primary :disabled="updateButtonDisabled" @click="onUpdateClicked">{{ i18n.ts.update }}</MkButton>
|
||||
<MkButton @click="onResetClicked">リセット</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -59,7 +65,7 @@ const gridSetting: GridSetting = {
|
|||
const required = validators.required();
|
||||
const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
|
||||
const columnSettings: ColumnSetting[] = [
|
||||
{ bindTo: 'deleteCheck', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 },
|
||||
{ bindTo: 'checked', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 },
|
||||
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] },
|
||||
{ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, validators: [required, regex] },
|
||||
{ bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 },
|
||||
|
@ -97,8 +103,23 @@ async function onUpdateClicked() {
|
|||
throw new Error('The number of items has been changed. Please refresh the page and try again.');
|
||||
}
|
||||
|
||||
const updatedItems = _items.filter((it, idx) => !it.deleteCheck && JSON.stringify(it) !== JSON.stringify(_originItems[idx]));
|
||||
const deleteItems = _items.filter((it) => it.deleteCheck);
|
||||
const confirm = await os.confirm({
|
||||
type: 'info',
|
||||
title: '確認',
|
||||
text: '絵文字の変更を保存します。よろしいですか?',
|
||||
});
|
||||
if (confirm.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedItems = _items.filter((it, idx) => !it.checked && JSON.stringify(it) !== JSON.stringify(_originItems[idx]));
|
||||
if (updatedItems.length === 0) {
|
||||
await os.alert({
|
||||
type: 'info',
|
||||
text: '変更された絵文字はありません。',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
async function action() {
|
||||
const emptyStrToNull = (value: string) => value === '' ? null : value;
|
||||
|
@ -116,7 +137,43 @@ async function onUpdateClicked() {
|
|||
roleIdsThatCanBeUsedThisEmojiAsReaction: emptyStrToEmptyArray(item.roleIdsThatCanBeUsedThisEmojiAsReaction),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await os.promiseDialog(
|
||||
action(),
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
emit('operation:search', query.value, undefined, undefined);
|
||||
}
|
||||
|
||||
async function onDeleteClicked() {
|
||||
const _items = gridItems.value;
|
||||
const _originItems = originGridItems.value;
|
||||
if (_items.length !== _originItems.length) {
|
||||
throw new Error('The number of items has been changed. Please refresh the page and try again.');
|
||||
}
|
||||
|
||||
const confirm = await os.confirm({
|
||||
type: 'info',
|
||||
title: '確認',
|
||||
text: 'チェックをつけられた絵文字を削除します。よろしいですか?',
|
||||
});
|
||||
if (confirm.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteItems = _items.filter((it) => it.checked);
|
||||
if (deleteItems.length === 0) {
|
||||
await os.alert({
|
||||
type: 'info',
|
||||
text: '削除対象の絵文字はありません。',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
async function action() {
|
||||
const deleteIds = deleteItems.map(it => it.id!);
|
||||
await misskeyApi('admin/emoji/delete-bulk', { ids: deleteIds });
|
||||
}
|
||||
|
@ -178,6 +235,16 @@ function onGridRowContextMenu(event: GridRowContextMenuEvent, currentState: Grid
|
|||
icon: 'ti ti-copy',
|
||||
action: () => optInGridUtils.copyToClipboard(gridItems, currentState),
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '選択行を削除対象とする',
|
||||
icon: 'ti ti-trash',
|
||||
action: () => {
|
||||
for (const row of currentState.rangedRows) {
|
||||
gridItems.value[row.index].checked = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -191,10 +258,20 @@ function onGridCellContextMenu(event: GridCellContextMenuEvent, currentState: Gr
|
|||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '選択行を削除',
|
||||
text: '選択範囲を削除',
|
||||
icon: 'ti ti-trash',
|
||||
action: () => optInGridUtils.deleteSelectionRange(gridItems, currentState),
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '選択行を削除対象とする',
|
||||
icon: 'ti ti-trash',
|
||||
action: () => {
|
||||
for (const rowIdx of [...new Set(currentState.rangedCells.map(it => it.row.index)).values()]) {
|
||||
gridItems.value[rowIdx].checked = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -240,8 +317,9 @@ function refreshGridItems() {
|
|||
}
|
||||
|
||||
.buttons {
|
||||
display: inline-flex;
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
<div>
|
||||
<div v-if="logs.length > 0" style="overflow-y: scroll;">
|
||||
<MkGrid
|
||||
:gridSetting="{ rowNumberVisible: false }"
|
||||
:gridSetting="{ rowNumberVisible: false, rowSelectable: false }"
|
||||
:data="logs"
|
||||
:columnSettings="columnSettings"
|
||||
@event="onGridEvent"
|
|
@ -99,7 +99,7 @@ import {
|
|||
import { ColumnSetting } from '@/components/grid/column.js';
|
||||
import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js';
|
||||
import { optInGridUtils } from '@/components/grid/optin-utils.js';
|
||||
import XRegisterLogs from '@/pages/admin/custom-emojis-grid.register.logs.vue';
|
||||
import XRegisterLogs from '@/pages/admin/custom-emojis-grid.local.register.logs.vue';
|
||||
|
||||
const MAXIMUM_EMOJI_COUNT = 100;
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<div class="_gaps" :class="$style.root">
|
||||
<MkTab v-model="modeTab" style="margin-bottom: var(--margin);">
|
||||
<option value="list">登録済み絵文字一覧</option>
|
||||
<option value="register">新規登録</option>
|
||||
</MkTab>
|
||||
|
||||
<div>
|
||||
<XListComponent
|
||||
v-if="modeTab === 'list'"
|
||||
:customEmojis="customEmojis"
|
||||
@operation:search="onOperationSearch"
|
||||
/>
|
||||
<XRegisterComponent
|
||||
v-else
|
||||
@operation:registered="onOperationRegistered"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import XListComponent from '@/pages/admin/custom-emojis-grid.local.list.vue';
|
||||
import XRegisterComponent from '@/pages/admin/custom-emojis-grid.local.register.vue';
|
||||
|
||||
type PageMode = 'list' | 'register';
|
||||
|
||||
const customEmojis = ref<Misskey.entities.EmojiDetailed[]>([]);
|
||||
const modeTab = ref<PageMode>('list');
|
||||
const query = ref<string>();
|
||||
|
||||
async function refreshCustomEmojis(query?: string, sinceId?: string, untilId?: string) {
|
||||
const emojis = await misskeyApi('admin/emoji/list', {
|
||||
limit: 100,
|
||||
query: query?.length ? query : undefined,
|
||||
sinceId,
|
||||
untilId,
|
||||
});
|
||||
|
||||
if (sinceId) {
|
||||
// 通常はID降順だが、sinceIdを設定すると昇順での並び替えとなるので、逆順にする必要がある
|
||||
emojis.reverse();
|
||||
}
|
||||
|
||||
customEmojis.value = emojis;
|
||||
}
|
||||
|
||||
async function onOperationSearch(q: string, sinceId?: string, untilId?: string) {
|
||||
query.value = q;
|
||||
await refreshCustomEmojis(q, sinceId, untilId);
|
||||
}
|
||||
|
||||
async function onOperationRegistered() {
|
||||
await refreshCustomEmojis(query.value);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshCustomEmojis();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.emoji-grid-row-edited {
|
||||
background-color: var(--ag-advanced-filter-column-pill-color);
|
||||
}
|
||||
|
||||
.emoji-grid-item-image {
|
||||
width: auto;
|
||||
height: 26px;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
padding: 16px;
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
245
packages/frontend/src/pages/admin/custom-emojis-grid.remote.vue
Normal file
245
packages/frontend/src/pages/admin/custom-emojis-grid.remote.vue
Normal file
|
@ -0,0 +1,245 @@
|
|||
<template>
|
||||
<div class="_gaps" :class="$style.root">
|
||||
<div :class="$style.searchArea">
|
||||
<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off" style="flex: 1">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
<template #label>絵文字名</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="host" :debounce="true" type="search" autocapitalize="off" style="flex: 1">
|
||||
<template #prefix><i class="ti ti-cloud-network"></i></template>
|
||||
<template #label>ホスト名</template>
|
||||
</MkInput>
|
||||
<MkButton primary style="margin-left: auto;" @click="onSearchButtonClicked">
|
||||
{{ i18n.ts.search }}
|
||||
</MkButton>
|
||||
</div>
|
||||
|
||||
<div v-if="gridItems.length > 0">
|
||||
<div style="overflow-y: scroll; padding-top: 8px; padding-bottom: 8px;">
|
||||
<MkGrid :data="gridItems" :columnSettings="columnSettings" @event="onGridEvent"/>
|
||||
</div>
|
||||
|
||||
<div class="_gaps">
|
||||
<div :class="$style.pages">
|
||||
<button @click="onLatestButtonClicked"><</button>
|
||||
<button @click="onOldestButtonClicked">></button>
|
||||
</div>
|
||||
|
||||
<div :class="$style.buttons">
|
||||
<MkButton primary @click="onImportClicked">チェックがついた絵文字をインポート</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkGrid from '@/components/grid/MkGrid.vue';
|
||||
import { ColumnSetting } from '@/components/grid/column.js';
|
||||
import { fromEmojiDetailed, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
|
||||
import {
|
||||
GridCellContextMenuEvent,
|
||||
GridCellValueChangeEvent,
|
||||
GridCurrentState,
|
||||
GridEvent,
|
||||
GridKeyDownEvent,
|
||||
GridRowContextMenuEvent,
|
||||
} from '@/components/grid/grid-event.js';
|
||||
import { optInGridUtils } from '@/components/grid/optin-utils.js';
|
||||
|
||||
const columnSettings: ColumnSetting[] = [
|
||||
{ bindTo: 'checked', icon: 'ti-download', type: 'boolean', editable: true, width: 34 },
|
||||
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' },
|
||||
{ bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' },
|
||||
{ bindTo: 'host', title: 'host', type: 'text', editable: false, width: 'auto' },
|
||||
];
|
||||
|
||||
const customEmojis = ref<Misskey.entities.EmojiDetailed[]>([]);
|
||||
const gridItems = ref<GridItem[]>([]);
|
||||
const query = ref<string>('');
|
||||
const host = ref<string>('');
|
||||
|
||||
const latest = computed(() => customEmojis.value.length > 0 ? customEmojis.value[0]?.id : undefined);
|
||||
const oldest = computed(() => customEmojis.value.length > 0 ? customEmojis.value[customEmojis.value.length - 1]?.id : undefined);
|
||||
|
||||
async function onSearchButtonClicked() {
|
||||
await refreshCustomEmojis(query.value, host.value);
|
||||
}
|
||||
|
||||
async function onLatestButtonClicked() {
|
||||
await refreshCustomEmojis(query.value, host.value, undefined, latest.value);
|
||||
}
|
||||
|
||||
async function onOldestButtonClicked() {
|
||||
await refreshCustomEmojis(query.value, host.value, oldest.value, undefined);
|
||||
}
|
||||
|
||||
async function onImportClicked() {
|
||||
const targets = gridItems.value.filter(it => it.checked);
|
||||
await importEmojis(targets);
|
||||
}
|
||||
|
||||
function onGridEvent(event: GridEvent, currentState: GridCurrentState) {
|
||||
switch (event.type) {
|
||||
case 'row-context-menu':
|
||||
onGridRowContextMenu(event, currentState);
|
||||
break;
|
||||
case 'cell-context-menu':
|
||||
onGridCellContextMenu(event, currentState);
|
||||
break;
|
||||
case 'cell-value-change':
|
||||
onGridCellValueChange(event, currentState);
|
||||
break;
|
||||
case 'keydown':
|
||||
onGridKeyDown(event, currentState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function onGridRowContextMenu(event: GridRowContextMenuEvent, currentState: GridCurrentState) {
|
||||
event.menuItems.push(
|
||||
{
|
||||
type: 'button',
|
||||
text: '選択行をインポート',
|
||||
icon: 'ti ti-download',
|
||||
action: async () => {
|
||||
const targets = currentState.rangedRows.map(it => gridItems.value[it.index]);
|
||||
console.log(targets);
|
||||
await importEmojis(targets);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function onGridCellContextMenu(event: GridCellContextMenuEvent, currentState: GridCurrentState) {
|
||||
event.menuItems.push(
|
||||
{
|
||||
type: 'button',
|
||||
text: '選択された絵文字をインポート',
|
||||
icon: 'ti ti-download',
|
||||
action: async () => {
|
||||
const targets = [...new Set(currentState.rangedCells.map(it => it.row)).values()].map(it => gridItems.value[it.index]);
|
||||
await importEmojis(targets);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function onGridCellValueChange(event: GridCellValueChangeEvent, currentState: GridCurrentState) {
|
||||
const { row, column, newValue } = event;
|
||||
if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
|
||||
gridItems.value[row.index][column.setting.bindTo] = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState) {
|
||||
optInGridUtils.defaultKeyDownHandler(gridItems, event, currentState);
|
||||
}
|
||||
|
||||
async function importEmojis(targets: GridItem[]) {
|
||||
async function action() {
|
||||
for (const target of targets) {
|
||||
await misskeyApi('admin/emoji/copy', {
|
||||
emojiId: target.id!,
|
||||
});
|
||||
}
|
||||
|
||||
await refreshCustomEmojis(query.value, host.value);
|
||||
}
|
||||
|
||||
const confirm = await os.confirm({
|
||||
type: 'info',
|
||||
title: '絵文字のインポート',
|
||||
text: `リモートから受信した${targets.length}個の絵文字のインポートを行います。絵文字のライセンスに十分な注意を払ってください。インポートを行いますか?`,
|
||||
});
|
||||
|
||||
if (!confirm.canceled) {
|
||||
await os.promiseDialog(
|
||||
action(),
|
||||
() => {
|
||||
},
|
||||
() => {
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCustomEmojis(query?: string, host?: string, sinceId?: string, untilId?: string) {
|
||||
const emojis = await misskeyApi('admin/emoji/list-remote', {
|
||||
limit: 100,
|
||||
query: query?.length ? query : undefined,
|
||||
host: host?.length ? host : undefined,
|
||||
sinceId,
|
||||
untilId,
|
||||
});
|
||||
|
||||
if (sinceId) {
|
||||
// 通常はID降順だが、sinceIdを設定すると昇順での並び替えとなるので、逆順にする必要がある
|
||||
emojis.reverse();
|
||||
}
|
||||
|
||||
customEmojis.value = emojis;
|
||||
console.log(customEmojis.value);
|
||||
gridItems.value = customEmojis.value.map(it => fromEmojiDetailed(it));
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshCustomEmojis();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.emoji-grid-row-edited {
|
||||
background-color: var(--ag-advanced-filter-column-pill-color);
|
||||
}
|
||||
|
||||
.emoji-grid-item-image {
|
||||
width: auto;
|
||||
height: 26px;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
padding: 16px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.searchArea {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
justify-content: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pages {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
background-color: var(--buttonBg);
|
||||
border-radius: 9999px;
|
||||
border: none;
|
||||
margin: 0 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: inline-flex;
|
||||
margin-left: auto;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
|
@ -4,74 +4,22 @@
|
|||
<template #header>
|
||||
<MkPageHeader v-model:tab="headerTab" :actions="headerActions" :tabs="headerTabs"/>
|
||||
</template>
|
||||
<div class="_gaps" :class="$style.root">
|
||||
<MkTab v-model="modeTab" style="margin-bottom: var(--margin);">
|
||||
<option value="list">登録済み絵文字一覧</option>
|
||||
<option value="register">新規登録</option>
|
||||
</MkTab>
|
||||
|
||||
<div>
|
||||
<XListComponent
|
||||
v-if="modeTab === 'list'"
|
||||
:customEmojis="customEmojis"
|
||||
@operation:search="onOperationSearch"
|
||||
/>
|
||||
<XRegisterComponent
|
||||
v-else
|
||||
@operation:registered="onOperationRegistered"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<XGridLocalComponent v-if="headerTab === 'local'"/>
|
||||
<XGridRemoteComponent v-else/>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { computed, ref } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import XListComponent from '@/pages/admin/custom-emojis-grid.list.vue';
|
||||
import XRegisterComponent from '@/pages/admin/custom-emojis-grid.register.vue';
|
||||
import XGridLocalComponent from '@/pages/admin/custom-emojis-grid.local.vue';
|
||||
import XGridRemoteComponent from '@/pages/admin/custom-emojis-grid.remote.vue';
|
||||
|
||||
type PageMode = 'list' | 'register';
|
||||
type PageMode = 'local' | 'remote';
|
||||
|
||||
const customEmojis = ref<Misskey.entities.EmojiDetailed[]>([]);
|
||||
const headerTab = ref('local');
|
||||
const modeTab = ref<PageMode>('list');
|
||||
const query = ref<string>();
|
||||
|
||||
async function refreshCustomEmojis(query?: string, sinceId?: string, untilId?: string) {
|
||||
const emojis = await misskeyApi('admin/emoji/list', {
|
||||
limit: 100,
|
||||
query,
|
||||
sinceId,
|
||||
untilId,
|
||||
});
|
||||
|
||||
if (sinceId) {
|
||||
// 通常はID降順だが、sinceIdを設定すると昇順での並び替えとなるので、逆順にする必要がある
|
||||
emojis.reverse();
|
||||
}
|
||||
|
||||
customEmojis.value = emojis;
|
||||
}
|
||||
|
||||
async function onOperationSearch(q: string, sinceId?: string, untilId?: string) {
|
||||
query.value = q;
|
||||
await refreshCustomEmojis(q, sinceId, untilId);
|
||||
}
|
||||
|
||||
async function onOperationRegistered() {
|
||||
await refreshCustomEmojis(query.value);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshCustomEmojis();
|
||||
});
|
||||
const headerTab = ref<PageMode>('local');
|
||||
|
||||
const headerTabs = computed(() => [{
|
||||
key: 'local',
|
||||
|
@ -114,30 +62,5 @@ definePageMetadata(computed(() => ({
|
|||
</style>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
padding: 16px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.controller {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.pages {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
|
||||
button {
|
||||
background-color: var(--buttonBg);
|
||||
border-radius: 9999px;
|
||||
border: none;
|
||||
margin: 0 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2024.2.0-beta.8
|
||||
* generatedAt: 2024-01-31T05:27:45.153Z
|
||||
* version: 2024.2.0-beta.7
|
||||
* generatedAt: 2024-02-04T07:16:03.625Z
|
||||
*/
|
||||
|
||||
import type { SwitchCaseResponseType } from '../api.js';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2024.2.0-beta.8
|
||||
* generatedAt: 2024-01-31T05:27:45.151Z
|
||||
* version: 2024.2.0-beta.7
|
||||
* generatedAt: 2024-02-04T07:16:03.623Z
|
||||
*/
|
||||
|
||||
import type {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2024.2.0-beta.8
|
||||
* generatedAt: 2024-01-31T05:27:45.149Z
|
||||
* version: 2024.2.0-beta.7
|
||||
* generatedAt: 2024-02-04T07:16:03.621Z
|
||||
*/
|
||||
|
||||
import { operations } from './types.js';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2024.2.0-beta.8
|
||||
* generatedAt: 2024-01-31T05:27:45.148Z
|
||||
* version: 2024.2.0-beta.7
|
||||
* generatedAt: 2024-02-04T07:16:03.620Z
|
||||
*/
|
||||
|
||||
import { components } from './types.js';
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
/* eslint @typescript-eslint/no-explicit-any: 0 */
|
||||
|
||||
/*
|
||||
* version: 2024.2.0-beta.8
|
||||
* generatedAt: 2024-01-31T05:27:45.068Z
|
||||
* version: 2024.2.0-beta.7
|
||||
* generatedAt: 2024-02-04T07:16:03.539Z
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -6472,16 +6472,7 @@ export type operations = {
|
|||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': ({
|
||||
/** Format: id */
|
||||
id: string;
|
||||
aliases: string[];
|
||||
name: string;
|
||||
category: string | null;
|
||||
/** @description The local host is represented with `null`. */
|
||||
host: string | null;
|
||||
url: string;
|
||||
})[];
|
||||
'application/json': components['schemas']['EmojiDetailed'][];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
|
|
Loading…
Reference in a new issue