revert useUploader→FileUploader

This commit is contained in:
tamaina 2025-08-01 21:22:07 +09:00
parent 7de7ecb566
commit 76de4ebea9
3 changed files with 82 additions and 74 deletions

View File

@ -146,7 +146,7 @@ import { getPluginHandlers } from '@/plugin.js';
import { DI } from '@/di.js'; import { DI } from '@/di.js';
import { globalEvents } from '@/events.js'; import { globalEvents } from '@/events.js';
import { checkDragDataType, getDragData } from '@/drag-and-drop.js'; import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
import { FileUploader } from '@/composables/use-uploader.js'; import { useUploader } from '@/composables/use-uploader.js';
const $i = ensureSignin(); const $i = ensureSignin();
@ -214,7 +214,7 @@ const targetChannel = shallowRef(props.channel);
const serverDraftId = ref<string | null>(null); const serverDraftId = ref<string | null>(null);
const postFormActions = getPluginHandlers('post_form_action'); const postFormActions = getPluginHandlers('post_form_action');
const uploader = new FileUploader({ const uploader = useUploader({
multiple: true, multiple: true,
}); });

View File

@ -58,7 +58,7 @@ import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { ensureSignin } from '@/i.js'; import { ensureSignin } from '@/i.js';
import { FileUploader } from '@/composables/use-uploader.js'; import { useUploader } from '@/composables/use-uploader.js';
import MkUploaderItems from '@/components/MkUploaderItems.vue'; import MkUploaderItems from '@/components/MkUploaderItems.vue';
const $i = ensureSignin(); const $i = ensureSignin();
@ -80,7 +80,7 @@ const emit = defineEmits<{
const dialog = useTemplateRef('dialog'); const dialog = useTemplateRef('dialog');
const uploader = new FileUploader({ const uploader = useUploader({
multiple: props.multiple, multiple: props.multiple,
folderId: props.folderId, folderId: props.folderId,
features: props.features, features: props.features,

View File

@ -99,43 +99,31 @@ function getCompressionSettings(level: 0 | 1 | 2 | 3) {
} }
} }
export class FileUploader { export function useUploader(options: {
private $i: Misskey.entities.MeDetailed; folderId?: string | null;
public events = new EventEmitter<{ multiple?: boolean;
features?: UploaderFeatures;
} = {}) {
const $i = ensureSignin();
const events = new EventEmitter<{
'itemUploaded': (ctx: { item: UploaderItem; }) => void; 'itemUploaded': (ctx: { item: UploaderItem; }) => void;
}>(); }>();
private uploaderFeatures = computed<Required<UploaderFeatures>>(() => {
const uploaderFeatures = computed<Required<UploaderFeatures>>(() => {
return { return {
imageEditing: this.options.features?.imageEditing ?? true, imageEditing: options.features?.imageEditing ?? true,
watermark: this.options.features?.watermark ?? true, watermark: options.features?.watermark ?? true,
}; };
}); });
public items = ref<UploaderItem[]>([]);
public uploading = computed(() => this.items.value.some(item => item.uploading));
public readyForUpload = computed(() => this.items.value.length > 0 && this.items.value.some(item => item.uploaded == null) && !this.items.value.some(item => item.uploading || item.preprocessing));
public allItemsUploaded = computed(() => this.items.value.every(item => item.uploaded != null));
constructor( const items = ref<UploaderItem[]>([]);
public options: {
folderId?: string | null;
multiple?: boolean;
features?: UploaderFeatures;
} = {}
) {
this.$i = ensureSignin();
onUnmounted(() => { function initializeFile(file: File) {
for (const item of this.items.value) {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
}
});
}
private initializeFile(file: File) {
const id = genId(); const id = genId();
const filename = file.name ?? 'untitled'; const filename = file.name ?? 'untitled';
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : ''; const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
this.items.value.push({ items.value.push({
id, id,
name: prefer.s.keepOriginalFilename ? filename : id + extension, name: prefer.s.keepOriginalFilename ? filename : id + extension,
progress: null, progress: null,
@ -146,27 +134,27 @@ export class FileUploader {
uploaded: null, uploaded: null,
uploadFailed: false, uploadFailed: false,
compressionLevel: prefer.s.defaultImageCompressionLevel, compressionLevel: prefer.s.defaultImageCompressionLevel,
watermarkPresetId: this.uploaderFeatures.value.watermark && this.$i.policies.watermarkAvailable ? prefer.s.defaultWatermarkPresetId : null, watermarkPresetId: uploaderFeatures.value.watermark && $i.policies.watermarkAvailable ? prefer.s.defaultWatermarkPresetId : null,
file: markRaw(file), file: markRaw(file),
}); });
const reactiveItem = this.items.value.at(-1)!; const reactiveItem = items.value.at(-1)!;
this.preprocess(reactiveItem).then(() => { preprocess(reactiveItem).then(() => {
triggerRef(this.items); triggerRef(items);
}); });
} }
public addFiles(newFiles: File[]) { function addFiles(newFiles: File[]) {
for (const file of newFiles) { for (const file of newFiles) {
this.initializeFile(file); initializeFile(file);
} }
} }
public removeItem(item: UploaderItem) { function removeItem(item: UploaderItem) {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
this.items.value.splice(this.items.value.indexOf(item), 1); items.value.splice(items.value.indexOf(item), 1);
} }
public getMenu(item: UploaderItem): MenuItem[] { function getMenu(item: UploaderItem): MenuItem[] {
const menu: MenuItem[] = []; const menu: MenuItem[] = [];
if ( if (
@ -218,7 +206,7 @@ export class FileUploader {
} }
if ( if (
this.uploaderFeatures.value.imageEditing && uploaderFeatures.value.imageEditing &&
IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) && IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing && !item.preprocessing &&
!item.uploading && !item.uploading &&
@ -234,14 +222,14 @@ export class FileUploader {
action: async () => { action: async () => {
const cropped = await os.cropImageFile(item.file, { aspectRatio: null }); const cropped = await os.cropImageFile(item.file, { aspectRatio: null });
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
this.items.value.splice(this.items.value.indexOf(item), 1, { items.value.splice(items.value.indexOf(item), 1, {
...item, ...item,
file: markRaw(cropped), file: markRaw(cropped),
thumbnail: window.URL.createObjectURL(cropped), thumbnail: window.URL.createObjectURL(cropped),
}); });
const reactiveItem = this.items.value.find(x => x.id === item.id)!; const reactiveItem = items.value.find(x => x.id === item.id)!;
this.preprocess(reactiveItem).then(() => { preprocess(reactiveItem).then(() => {
triggerRef(this.items); triggerRef(items);
}); });
}, },
}, /*{ }, /*{
@ -259,14 +247,14 @@ export class FileUploader {
}, { }, {
ok: (file) => { ok: (file) => {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail); if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
this.items.value.splice(this.items.value.indexOf(item), 1, { items.value.splice(items.value.indexOf(item), 1, {
...item, ...item,
file: markRaw(file), file: markRaw(file),
thumbnail: window.URL.createObjectURL(file), thumbnail: window.URL.createObjectURL(file),
}); });
const reactiveItem = this.items.value.find(x => x.id === item.id)!; const reactiveItem = items.value.find(x => x.id === item.id)!;
this.preprocess(reactiveItem).then(() => { preprocess(reactiveItem).then(() => {
triggerRef(this.items); triggerRef(items);
}); });
}, },
closed: () => dispose(), closed: () => dispose(),
@ -277,19 +265,19 @@ export class FileUploader {
} }
if ( if (
this.uploaderFeatures.value.watermark && uploaderFeatures.value.watermark &&
this.$i.policies.watermarkAvailable && $i.policies.watermarkAvailable &&
WATERMARK_SUPPORTED_TYPES.includes(item.file.type) && WATERMARK_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing && !item.preprocessing &&
!item.uploading && !item.uploading &&
!item.uploaded !item.uploaded
) { ) {
const changeWatermarkPreset = (presetId: string | null) => { function changeWatermarkPreset(presetId: string | null) {
item.watermarkPresetId = presetId; item.watermarkPresetId = presetId;
this.preprocess(item).then(() => { preprocess(item).then(() => {
triggerRef(this.items); triggerRef(items);
}); });
}; }
menu.push({ menu.push({
icon: 'ti ti-copyright', icon: 'ti ti-copyright',
@ -335,12 +323,12 @@ export class FileUploader {
!item.uploading && !item.uploading &&
!item.uploaded !item.uploaded
) { ) {
const changeCompressionLevel = (level: 0 | 1 | 2 | 3) => { function changeCompressionLevel(level: 0 | 1 | 2 | 3) {
item.compressionLevel = level; item.compressionLevel = level;
this.preprocess(item).then(() => { preprocess(item).then(() => {
triggerRef(this.items); triggerRef(items);
}); });
}; }
menu.push({ menu.push({
icon: 'ti ti-leaf', icon: 'ti ti-leaf',
@ -393,14 +381,14 @@ export class FileUploader {
icon: 'ti ti-upload', icon: 'ti ti-upload',
text: i18n.ts.upload, text: i18n.ts.upload,
action: () => { action: () => {
this.uploadOne(item); uploadOne(item);
}, },
}, { }, {
icon: 'ti ti-x', icon: 'ti ti-x',
text: i18n.ts.remove, text: i18n.ts.remove,
danger: true, danger: true,
action: () => { action: () => {
this.removeItem(item); removeItem(item);
}, },
}); });
} else if (item.uploading) { } else if (item.uploading) {
@ -421,13 +409,13 @@ export class FileUploader {
return menu; return menu;
} }
private async uploadOne(item: UploaderItem): Promise<void> { async function uploadOne(item: UploaderItem): Promise<void> {
item.uploadFailed = false; item.uploadFailed = false;
item.uploading = true; item.uploading = true;
const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, { const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, {
name: item.uploadName ?? item.name, name: item.uploadName ?? item.name,
folderId: this.options.folderId === undefined ? prefer.s.uploadFolder : this.options.folderId, folderId: options.folderId === undefined ? prefer.s.uploadFolder : options.folderId,
isSensitive: item.isSensitive ?? false, isSensitive: item.isSensitive ?? false,
caption: item.caption ?? null, caption: item.caption ?? null,
onProgress: (progress) => { onProgress: (progress) => {
@ -450,7 +438,7 @@ export class FileUploader {
await filePromise.then((file) => { await filePromise.then((file) => {
item.uploaded = file; item.uploaded = file;
item.abort = null; item.abort = null;
this.events.emit('itemUploaded', { item }); events.emit('itemUploaded', { item });
}).catch(err => { }).catch(err => {
item.uploadFailed = true; item.uploadFailed = true;
item.progress = null; item.progress = null;
@ -462,26 +450,26 @@ export class FileUploader {
}); });
} }
public async upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
this.items.value = this.items.value.map(item => ({ items.value = items.value.map(item => ({
...item, ...item,
aborted: false, aborted: false,
uploadFailed: false, uploadFailed: false,
uploading: false, uploading: false,
})); }));
for (const item of this.items.value.filter(item => item.uploaded == null)) { for (const item of items.value.filter(item => item.uploaded == null)) {
// アップロード処理途中で値が変わる場合途中で全キャンセルされたりなどもあるので、Array filterではなくここでチェック // アップロード処理途中で値が変わる場合途中で全キャンセルされたりなどもあるので、Array filterではなくここでチェック
if (item.aborted) { if (item.aborted) {
continue; continue;
} }
await this.uploadOne(item); await uploadOne(item);
} }
} }
public abortAll() { function abortAll() {
for (const item of this.items.value) { for (const item of items.value) {
if (item.uploaded != null) { if (item.uploaded != null) {
continue; continue;
} }
@ -494,12 +482,12 @@ export class FileUploader {
} }
} }
private async preprocess(item: UploaderItem): Promise<void> { async function preprocess(item: UploaderItem): Promise<void> {
item.preprocessing = true; item.preprocessing = true;
try { try {
if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) { if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) {
await this.preprocessForImage(item); await preprocessForImage(item);
} }
} catch (err) { } catch (err) {
console.error('Failed to preprocess image', err); console.error('Failed to preprocess image', err);
@ -510,12 +498,12 @@ export class FileUploader {
item.preprocessing = false; item.preprocessing = false;
} }
private async preprocessForImage(item: UploaderItem): Promise<void> { async function preprocessForImage(item: UploaderItem): Promise<void> {
const imageBitmap = await window.createImageBitmap(item.file); const imageBitmap = await window.createImageBitmap(item.file);
let preprocessedFile: Blob | File = item.file; let preprocessedFile: Blob | File = item.file;
const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(preprocessedFile.type) && this.$i.policies.watermarkAvailable; const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(preprocessedFile.type) && $i.policies.watermarkAvailable;
const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId); const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId);
if (needsWatermark && preset != null) { if (needsWatermark && preset != null) {
const canvas = window.document.createElement('canvas'); const canvas = window.document.createElement('canvas');
@ -575,4 +563,24 @@ export class FileUploader {
item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null; item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null;
item.preprocessedFile = markRaw(preprocessedFile); item.preprocessedFile = markRaw(preprocessedFile);
} }
onUnmounted(() => {
for (const item of items.value) {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
}
});
return {
items,
addFiles,
removeItem,
abortAll,
upload,
getMenu,
uploading: computed(() => items.value.some(item => item.uploading)),
readyForUpload: computed(() => items.value.length > 0 && items.value.some(item => item.uploaded == null) && !items.value.some(item => item.uploading || item.preprocessing)),
allItemsUploaded: computed(() => items.value.every(item => item.uploaded != null)),
events,
};
} }