1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-27 14:14:36 +02:00

enhance(frontend): UploaderItemsの詳細メニューからもプレビューを起動できるように (#17811)

* enhance(frontend): UploaderItemsの詳細メニューからもプレビューを起動できるように

* clean up
This commit is contained in:
かっこかり
2026-07-27 18:34:31 +09:00
committed by GitHub
parent cb58ff32a4
commit 703aa2360b
2 changed files with 64 additions and 45 deletions

View File

@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, onUnmounted, watch } from 'vue';
import { computed } from 'vue';
import { isLink } from '@@/js/is-link.js';
import type { UploaderItem } from '@/composables/use-uploader.js';
import { getUploadName } from '@/composables/use-uploader.js';
@@ -57,6 +57,7 @@ import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import bytes from '@/filters/bytes.js';
import * as os from '@/os.js';
import type { Content } from '@/components/MkLightbox.item.vue';
const props = defineProps<{
items: UploaderItem[];
@@ -72,32 +73,6 @@ const emit = defineEmits<{
(ev: 'showMenuViaContextmenu', item: UploaderItem, event: PointerEvent): void;
}>();
//#region objectUrlMap
const objectUrlMap = new Map<UploaderItem, string>();
watch(() => props.items, () => {
for (const item of props.items) {
if (!objectUrlMap.has(item)) {
objectUrlMap.set(item, URL.createObjectURL(item.file));
}
}
for (const [item, url] of objectUrlMap.entries()) {
if (!props.items.includes(item)) {
URL.revokeObjectURL(url);
objectUrlMap.delete(item);
}
}
}, { immediate: true, deep: true });
onUnmounted(() => {
for (const url of objectUrlMap.values()) {
URL.revokeObjectURL(url);
}
objectUrlMap.clear();
});
//#endregion
function getUploadNameParts(item: UploaderItem): {
baseName: string;
extension: string | null;
@@ -127,13 +102,15 @@ function onContextmenu(item: UploaderItem, ev: PointerEvent) {
async function onThumbnailClick(item: UploaderItem, ev: PointerEvent) {
if (item.file.type.startsWith('image') || item.file.type.startsWith('video')) {
const contents = props.items.filter(item => item.file.type.startsWith('image') || item.file.type.startsWith('video')).map(item => ({
id: item.id,
type: (item.file.type.startsWith('video') ? 'video' as const : 'image' as const),
url: objectUrlMap.get(item)!,
thumbnailUrl: item.thumbnail,
filename: getUploadName(item),
}));
const contents = props.items
.filter(item => item.file.type.startsWith('image') || item.file.type.startsWith('video'))
.map<Content>(item => ({
id: item.id,
type: (item.file.type.startsWith('video') ? 'video' : 'image'),
url: item.objectUrl,
thumbnailUrl: item.thumbnail,
filename: getUploadName(item),
}));
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkLightbox.vue').then(x => x.default), {
defaultIndex: contents.findIndex(content => content.id === item.id),

View File

@@ -16,6 +16,7 @@ import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { isWebpSupported } from '@/utility/isWebpSupported.js';
import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
import type { Content } from '@/components/MkLightbox.item.vue';
import * as os from '@/os.js';
import { ensureSignin } from '@/i.js';
@@ -74,6 +75,7 @@ export type UploaderItem = {
compressedSize?: number | null;
preprocessedFile?: Blob | null;
file: File;
objectUrl: string;
watermarkPreset: WatermarkPreset | null;
watermarkLayers: WatermarkLayers | null;
imageFrameParams: ImageFrameParams | null;
@@ -133,12 +135,13 @@ export function useUploader(options: {
const filename = file.name ?? 'untitled';
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
const watermarkPreset = uploaderFeatures.value.watermark && $i.policies.watermarkAvailable ? (prefer.s.watermarkPresets.find(p => p.id === prefer.s.defaultWatermarkPresetId) ?? null) : null;
const objectUrl = window.URL.createObjectURL(file);
items.value.push({
id,
name: prefer.s.keepOriginalFilename ? filename : id + extension,
suffix: '',
progress: null,
thumbnail: THUMBNAIL_SUPPORTED_TYPES.includes(file.type) ? window.URL.createObjectURL(file) : null,
thumbnail: THUMBNAIL_SUPPORTED_TYPES.includes(file.type) ? objectUrl : null,
preprocessing: false,
preprocessProgress: null,
uploading: false,
@@ -150,6 +153,7 @@ export function useUploader(options: {
watermarkLayers: watermarkPreset?.layers ?? null,
imageFrameParams: null,
file: markRaw(file),
objectUrl,
});
const reactiveItem = items.value.at(-1)!;
preprocess(reactiveItem).then(() => {
@@ -163,8 +167,24 @@ export function useUploader(options: {
}
}
function removeItem(item: UploaderItem) {
function revokeItemObjectUrls(item: UploaderItem) {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
URL.revokeObjectURL(item.objectUrl);
}
function createItemObjectUrl(item: UploaderItem, file: Blob | File): string {
revokeItemObjectUrls(item);
return window.URL.createObjectURL(file);
}
function updateItemObjectUrls(item: UploaderItem, file: Blob | File) {
const newObjectUrl = createItemObjectUrl(item, file);
item.objectUrl = newObjectUrl;
item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(file.type) ? newObjectUrl : null;
}
function removeItem(item: UploaderItem) {
revokeItemObjectUrls(item);
items.value.splice(items.value.indexOf(item), 1);
}
@@ -214,6 +234,28 @@ export function useUploader(options: {
closed: () => dispose(),
});
},
}, {
text: i18n.ts.preview,
icon: 'ti ti-photo-search',
action: async () => {
const contents = items.value
.filter(item => item.file.type.startsWith('image/') || item.file.type.startsWith('video/'))
.map<Content>(item => ({
id: item.id,
type: item.file.type.startsWith('video/') ? 'video' : 'image',
url: item.objectUrl,
thumbnail: item.thumbnail,
filename: getUploadName(item),
caption: item.caption ?? null,
}));
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkLightbox.vue').then(x => x.default), {
defaultIndex: contents.findIndex(x => x.id === item.id),
contents,
}, {
closed: () => dispose(),
});
},
}, {
type: 'divider',
});
@@ -235,11 +277,12 @@ export function useUploader(options: {
text: i18n.ts.cropImage,
action: async () => {
const cropped = await os.cropImageFile(item.file, { aspectRatio: null });
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
const newObjectUrl = createItemObjectUrl(item, cropped);
items.value.splice(items.value.indexOf(item), 1, {
...item,
file: markRaw(cropped),
thumbnail: window.URL.createObjectURL(cropped),
thumbnail: THUMBNAIL_SUPPORTED_TYPES.includes(cropped.type) ? newObjectUrl : null,
objectUrl: newObjectUrl,
});
const reactiveItem = items.value.find(x => x.id === item.id)!;
preprocess(reactiveItem).then(() => {
@@ -260,11 +303,12 @@ export function useUploader(options: {
image: item.file,
}, {
ok: (file) => {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
const newObjectUrl = createItemObjectUrl(item, file);
items.value.splice(items.value.indexOf(item), 1, {
...item,
file: markRaw(file),
thumbnail: window.URL.createObjectURL(file),
thumbnail: THUMBNAIL_SUPPORTED_TYPES.includes(file.type) ? newObjectUrl : null,
objectUrl: newObjectUrl,
});
const reactiveItem = items.value.find(x => x.id === item.id)!;
preprocess(reactiveItem).then(() => {
@@ -694,8 +738,7 @@ export function useUploader(options: {
imageBitmap.close();
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null;
updateItemObjectUrls(item, preprocessedFile);
item.preprocessedFile = markRaw(preprocessedFile);
}
@@ -754,14 +797,13 @@ export function useUploader(options: {
item.suffix = '';
}
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null;
updateItemObjectUrls(item, preprocessedFile);
item.preprocessedFile = markRaw(preprocessedFile);
}
function reset() {
for (const item of items.value) {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
revokeItemObjectUrls(item);
}
abortAll();