diff --git a/CHANGELOG.md b/CHANGELOG.md index 57d8a528f..7339dbd3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ (Based on https://github.com/Otaku-Social/maniakey/pull/14) - Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に - Enhance: クエリパラメータでuiを一時的に変更できるように #15240 +- Enhance: リモート絵文字のインポート時に詳細を確認できるように #15336 - Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正 - Fix: サーバー情報メニューに区切り線が不足していたのを修正 - Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index b98fd5d42..83159337a 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10635,6 +10635,10 @@ export interface Locale extends ILocale { "logNothing": string; }; "_remote": { + /** + * 選択行の詳細 + */ + "selectionRowDetail": string; /** * 選択行をインポート */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 638f2a69c..42c25a58a 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2837,6 +2837,7 @@ _customEmojisManager: failureLogNothing: "失敗ログはありません。" logNothing: "ログはありません。" _remote: + selectionRowDetail: "選択行の詳細" importSelectionRows: "選択行をインポート" importSelectionRangesRows: "選択範囲の行をインポート" importEmojisButton: "チェックされた絵文字をインポート" diff --git a/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue b/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue new file mode 100644 index 000000000..873b276b3 --- /dev/null +++ b/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue @@ -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> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue index 9a9d2990b..14a3b71e5 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue @@ -34,6 +34,16 @@ SPDX-License-Identifier: AGPL-3.0-only > <template #label>host</template> </MkInput> + <MkInput + v-model="queryLicense" + type="search" + autocapitalize="off" + :class="[$style.col3, $style.row1]" + @enter="onSearchRequest" + > + <template #label>license</template> + </MkInput> + <MkInput v-model="queryUri" type="search" @@ -115,6 +125,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> import { computed, onMounted, ref, useCssModule } from 'vue'; import * as Misskey from 'misskey-js'; +import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; @@ -135,7 +146,7 @@ import { deviceKind } from '@/scripts/device-kind.js'; import MkPagingButtons from '@/components/MkPagingButtons.vue'; import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue'; import { SortOrder } from '@/components/MkSortOrderEditor.define.js'; -import { useLoading } from "@/components/hook/useLoading.js"; +import { useLoading } from '@/components/hook/useLoading.js'; type GridItem = { checked: boolean; @@ -178,12 +189,37 @@ function setupGrid(): GridSetting { { 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' }, + { bindTo: 'license', title: 'license', type: 'text', editable: false, width: 200 }, { bindTo: 'uri', title: 'uri', type: 'text', editable: false, width: 'auto' }, { bindTo: 'publicUrl', title: 'publicUrl', type: 'text', editable: false, width: 'auto' }, ], cells: { contextMenuFactory: (col, row, value, context) => { 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', text: i18n.ts._customEmojisManager._remote.importSelectionRangesRows, @@ -207,6 +243,7 @@ const currentPage = ref<number>(0); const queryName = ref<string | null>(null); const queryHost = ref<string | null>(null); +const queryLicense = ref<string | null>(null); const queryUri = ref<string | null>(null); const queryPublicUrl = ref<string | null>(null); const previousQuery = ref<string | undefined>(undefined); @@ -229,6 +266,7 @@ async function onSearchRequest() { function onQueryResetButtonClicked() { queryName.value = null; queryHost.value = null; + queryLicense.value = null; queryUri.value = null; queryPublicUrl.value = null; } @@ -306,6 +344,7 @@ async function refreshCustomEmojis() { const query: Misskey.entities.V2AdminEmojiListRequest['query'] = { name: emptyStrToUndefined(queryName.value), host: emptyStrToUndefined(queryHost.value), + license: emptyStrToUndefined(queryLicense.value), uri: emptyStrToUndefined(queryUri.value), publicUrl: emptyStrToUndefined(queryPublicUrl.value), hostType: 'remote', @@ -330,6 +369,7 @@ async function refreshCustomEmojis() { id: it.id, url: it.publicUrl, name: it.name, + license: it.license, host: it.host!, })); } @@ -356,6 +396,10 @@ onMounted(async () => { grid-column: 2 / 3; } +.col3 { + grid-column: 3 / 4; +} + .root { padding: 16px; } @@ -366,7 +410,7 @@ onMounted(async () => { .searchArea { display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr; gap: 16px; } diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 789a63eb9..82c6d8df4 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -78,6 +78,7 @@ import { computed, defineAsyncComponent, ref, shallowRef } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkPagination from '@/components/MkPagination.vue'; +import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import FormSplit from '@/components/form/split.vue'; 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) => { os.apiWithDialog('admin/emoji/copy', { emojiId: emoji.id, @@ -169,6 +183,10 @@ const remoteMenu = (emoji, ev: MouseEvent) => { os.popupMenu([{ type: 'label', text: ':' + emoji.name + ':', + }, { + text: i18n.ts.details, + icon: 'ti ti-info-circle', + action: () => { detailRemoteEmoji(emoji); }, }, { text: i18n.ts.import, icon: 'ti ti-plus',