feat(frontend): リモート絵文字のインポート時に詳細を確認できるように (#15344)

* feat(frontend): リモート絵文字のインポート時に詳細を確認できるように

* 追加対応

* MkInput -> MkKeyValue
This commit is contained in:
おさむのひと 2025-01-26 14:59:03 +09:00 committed by GitHub
parent e94c697aae
commit f4bca4708e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 202 additions and 2 deletions

View File

@ -23,6 +23,7 @@
(Based on https://github.com/Otaku-Social/maniakey/pull/14) (Based on https://github.com/Otaku-Social/maniakey/pull/14)
- Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に - Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に
- Enhance: クエリパラメータでuiを一時的に変更できるように #15240 - Enhance: クエリパラメータでuiを一時的に変更できるように #15240
- Enhance: リモート絵文字のインポート時に詳細を確認できるように #15336
- Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正 - Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正
- Fix: サーバー情報メニューに区切り線が不足していたのを修正 - Fix: サーバー情報メニューに区切り線が不足していたのを修正
- Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正 - Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正

4
locales/index.d.ts vendored
View File

@ -10635,6 +10635,10 @@ export interface Locale extends ILocale {
"logNothing": string; "logNothing": string;
}; };
"_remote": { "_remote": {
/**
*
*/
"selectionRowDetail": string;
/** /**
* *
*/ */

View File

@ -2837,6 +2837,7 @@ _customEmojisManager:
failureLogNothing: "失敗ログはありません。" failureLogNothing: "失敗ログはありません。"
logNothing: "ログはありません。" logNothing: "ログはありません。"
_remote: _remote:
selectionRowDetail: "選択行の詳細"
importSelectionRows: "選択行をインポート" importSelectionRows: "選択行をインポート"
importSelectionRangesRows: "選択範囲の行をインポート" importSelectionRangesRows: "選択範囲の行をインポート"
importEmojisButton: "チェックされた絵文字をインポート" importEmojisButton: "チェックされた絵文字をインポート"

View File

@ -0,0 +1,132 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkWindow
ref="windowEl"
:initialWidth="400"
:initialHeight="500"
:canResize="true"
@close="windowEl?.close()"
@closed="emit('closed')"
>
<template #header>:{{ name }}:</template>
<div style="display: flex; flex-direction: column; min-height: 100%;">
<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
<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" :alt="name"/>
</div>
<div style="background: #222;" :class="$style.imgContainer">
<img :src="imgUrl" :class="$style.img" :alt="name"/>
</div>
<div style="background: #ddd;" :class="$style.imgContainer">
<img :src="imgUrl" :class="$style.img" :alt="name"/>
</div>
<div style="background: #fff;" :class="$style.imgContainer">
<img :src="imgUrl" :class="$style.img" :alt="name"/>
</div>
</div>
<MkKeyValue>
<template #key>{{ i18n.ts.id }}</template>
<template #value>{{ name }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.host }}</template>
<template #value>{{ host }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.license }}</template>
<template #value>{{ license }}</template>
</MkKeyValue>
</div>
</MkSpacer>
<div :class="$style.footer">
<MkButton primary rounded style="margin: 0 auto;" @click="done">
<i class="ti ti-plus"></i> {{ i18n.ts.import }}
</MkButton>
</div>
</div>
</MkWindow>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkWindow from '@/components/MkWindow.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
const props = defineProps<{
emoji: {
id: string,
name: string,
host: string,
license: string | null,
url: string
},
}>();
const emit = defineEmits<{
//
(ev: 'done'): void,
(ev: 'closed'): void
}>();
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
const name = computed(() => props.emoji.name);
const host = computed(() => props.emoji.host);
const license = computed(() => props.emoji.license);
const imgUrl = computed(() => props.emoji.url);
async function done() {
await os.apiWithDialog('admin/emoji/copy', {
emojiId: props.emoji.id,
});
emit('done');
windowEl.value?.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;
}
.footer {
position: sticky;
z-index: 10000;
bottom: 0;
left: 0;
padding: 12px;
border-top: solid 0.5px var(--MI_THEME-divider);
background: var(--MI_THEME-acrylicBg);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
}
</style>

View File

@ -34,6 +34,16 @@ SPDX-License-Identifier: AGPL-3.0-only
> >
<template #label>host</template> <template #label>host</template>
</MkInput> </MkInput>
<MkInput
v-model="queryLicense"
type="search"
autocapitalize="off"
:class="[$style.col3, $style.row1]"
@enter="onSearchRequest"
>
<template #label>license</template>
</MkInput>
<MkInput <MkInput
v-model="queryUri" v-model="queryUri"
type="search" type="search"
@ -115,6 +125,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, useCssModule } from 'vue'; import { computed, onMounted, ref, useCssModule } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -135,7 +146,7 @@ import { deviceKind } from '@/scripts/device-kind.js';
import MkPagingButtons from '@/components/MkPagingButtons.vue'; import MkPagingButtons from '@/components/MkPagingButtons.vue';
import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue'; import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
import { SortOrder } from '@/components/MkSortOrderEditor.define.js'; import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
import { useLoading } from "@/components/hook/useLoading.js"; import { useLoading } from '@/components/hook/useLoading.js';
type GridItem = { type GridItem = {
checked: boolean; checked: boolean;
@ -178,12 +189,37 @@ function setupGrid(): GridSetting {
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' }, { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' },
{ bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' }, { bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'host', title: 'host', type: 'text', editable: false, width: 'auto' }, { bindTo: 'host', title: 'host', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'license', title: 'license', type: 'text', editable: false, width: 200 },
{ bindTo: 'uri', title: 'uri', type: 'text', editable: false, width: 'auto' }, { bindTo: 'uri', title: 'uri', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'publicUrl', title: 'publicUrl', type: 'text', editable: false, width: 'auto' }, { bindTo: 'publicUrl', title: 'publicUrl', type: 'text', editable: false, width: 'auto' },
], ],
cells: { cells: {
contextMenuFactory: (col, row, value, context) => { contextMenuFactory: (col, row, value, context) => {
return [ return [
{
type: 'button',
text: i18n.ts._customEmojisManager._remote.selectionRowDetail,
icon: 'ti ti-info-circle',
action: async () => {
const target = customEmojis.value[row.index];
const { dispose } = os.popup(MkRemoteEmojiEditDialog, {
emoji: {
id: target.id,
name: target.name,
host: target.host!,
license: target.license,
url: target.publicUrl,
},
}, {
done: () => {
dispose();
},
closed: () => {
dispose();
},
});
},
},
{ {
type: 'button', type: 'button',
text: i18n.ts._customEmojisManager._remote.importSelectionRangesRows, text: i18n.ts._customEmojisManager._remote.importSelectionRangesRows,
@ -207,6 +243,7 @@ const currentPage = ref<number>(0);
const queryName = ref<string | null>(null); const queryName = ref<string | null>(null);
const queryHost = ref<string | null>(null); const queryHost = ref<string | null>(null);
const queryLicense = ref<string | null>(null);
const queryUri = ref<string | null>(null); const queryUri = ref<string | null>(null);
const queryPublicUrl = ref<string | null>(null); const queryPublicUrl = ref<string | null>(null);
const previousQuery = ref<string | undefined>(undefined); const previousQuery = ref<string | undefined>(undefined);
@ -229,6 +266,7 @@ async function onSearchRequest() {
function onQueryResetButtonClicked() { function onQueryResetButtonClicked() {
queryName.value = null; queryName.value = null;
queryHost.value = null; queryHost.value = null;
queryLicense.value = null;
queryUri.value = null; queryUri.value = null;
queryPublicUrl.value = null; queryPublicUrl.value = null;
} }
@ -306,6 +344,7 @@ async function refreshCustomEmojis() {
const query: Misskey.entities.V2AdminEmojiListRequest['query'] = { const query: Misskey.entities.V2AdminEmojiListRequest['query'] = {
name: emptyStrToUndefined(queryName.value), name: emptyStrToUndefined(queryName.value),
host: emptyStrToUndefined(queryHost.value), host: emptyStrToUndefined(queryHost.value),
license: emptyStrToUndefined(queryLicense.value),
uri: emptyStrToUndefined(queryUri.value), uri: emptyStrToUndefined(queryUri.value),
publicUrl: emptyStrToUndefined(queryPublicUrl.value), publicUrl: emptyStrToUndefined(queryPublicUrl.value),
hostType: 'remote', hostType: 'remote',
@ -330,6 +369,7 @@ async function refreshCustomEmojis() {
id: it.id, id: it.id,
url: it.publicUrl, url: it.publicUrl,
name: it.name, name: it.name,
license: it.license,
host: it.host!, host: it.host!,
})); }));
} }
@ -356,6 +396,10 @@ onMounted(async () => {
grid-column: 2 / 3; grid-column: 2 / 3;
} }
.col3 {
grid-column: 3 / 4;
}
.root { .root {
padding: 16px; padding: 16px;
} }
@ -366,7 +410,7 @@ onMounted(async () => {
.searchArea { .searchArea {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
gap: 16px; gap: 16px;
} }

View File

@ -78,6 +78,7 @@ import { computed, defineAsyncComponent, ref, shallowRef } from 'vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';
import { selectFile } from '@/scripts/select-file.js'; import { selectFile } from '@/scripts/select-file.js';
@ -159,6 +160,19 @@ const edit = (emoji) => {
}); });
}; };
const detailRemoteEmoji = (emoji) => {
const { dispose } = os.popup(MkRemoteEmojiEditDialog, {
emoji: emoji,
}, {
done: () => {
dispose();
},
closed: () => {
dispose();
},
});
};
const importEmoji = (emoji) => { const importEmoji = (emoji) => {
os.apiWithDialog('admin/emoji/copy', { os.apiWithDialog('admin/emoji/copy', {
emojiId: emoji.id, emojiId: emoji.id,
@ -169,6 +183,10 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
os.popupMenu([{ os.popupMenu([{
type: 'label', type: 'label',
text: ':' + emoji.name + ':', text: ':' + emoji.name + ':',
}, {
text: i18n.ts.details,
icon: 'ti ti-info-circle',
action: () => { detailRemoteEmoji(emoji); },
}, { }, {
text: i18n.ts.import, text: i18n.ts.import,
icon: 'ti ti-plus', icon: 'ti ti-plus',