From ccf5bd337e98a470f30639ceedc16501f57aee68 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Wed, 21 May 2025 21:13:19 +0900
Subject: enhance(frontend): ファイルのアップロードを中止できるように (#16069)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* enhance(frontend): ファイルのアップロードを中止できるように

* Update Changelog

* fix: ダイアログを閉じたり、中断ボタンが押されたりしたときはその後のアップロードをすべて中止するように

* fix
---
 .../frontend/src/components/MkUploaderDialog.vue   | 93 ++++++++++++++++++----
 1 file changed, 76 insertions(+), 17 deletions(-)

(limited to 'packages/frontend/src/components/MkUploaderDialog.vue')

diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue
index 3a83247d4b..b171546854 100644
--- a/packages/frontend/src/components/MkUploaderDialog.vue
+++ b/packages/frontend/src/components/MkUploaderDialog.vue
@@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div :class="$style.root">
 		<div :class="[$style.overallProgress, canRetry ? $style.overallProgressError : null]" :style="{ '--op': `${overallProgress}%` }"></div>
 
-		<div :class="$style.main" class="_gaps_s">
-			<div :class="$style.items" class="_gaps_s">
+		<div class="_gaps_s _spacer">
+			<div class="_gaps_s">
 				<div
 					v-for="ctx in items"
 					:key="ctx.id"
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<template #footer>
 		<div class="_buttonsCenter">
-			<MkButton v-if="isUploading" rounded @click="cancel()"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
+			<MkButton v-if="isUploading" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
 			<MkButton v-else-if="!firstUploadAttempted" primary rounded @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
 
 			<MkButton v-if="canRetry" rounded @click="upload()"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
@@ -96,10 +96,9 @@ import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
 import MkButton from '@/components/MkButton.vue';
 import bytes from '@/filters/bytes.js';
-import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import { isWebpSupported } from '@/utility/isWebpSupported.js';
-import { uploadFile } from '@/utility/drive.js';
+import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
 import * as os from '@/os.js';
 import { ensureSignin } from '@/i.js';
 
@@ -138,7 +137,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const items = ref([] as {
+const items = ref<{
 	id: string;
 	name: string;
 	progress: { max: number; value: number } | null;
@@ -147,10 +146,12 @@ const items = ref([] as {
 	uploading: boolean;
 	uploaded: Misskey.entities.DriveFile | null;
 	uploadFailed: boolean;
+	aborted: boolean;
 	compressedSize?: number | null;
 	compressedImage?: Blob | null;
 	file: File;
-}[]);
+	abort?: (() => void) | null;
+}[]>([]);
 
 const dialog = useTemplateRef('dialog');
 
@@ -213,10 +214,23 @@ async function cancel() {
 	});
 	if (canceled) return;
 
+	abortAll();
 	emit('canceled');
 	dialog.value?.close();
 }
 
+async function abortWithConfirm() {
+	const { canceled } = await os.confirm({
+		type: 'question',
+		text: i18n.ts._uploader.abortConfirm,
+		okText: i18n.ts.yes,
+		cancelText: i18n.ts.no,
+	});
+	if (canceled) return;
+
+	abortAll();
+}
+
 async function done() {
 	if (items.value.some(item => item.uploaded == null)) {
 		const { canceled } = await os.confirm({
@@ -258,6 +272,17 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
 				items.value.splice(items.value.indexOf(item), 1);
 			},
 		});
+	} else if (item.uploading) {
+		menu.push({
+			icon: 'ti ti-cloud-pause',
+			text: i18n.ts.abort,
+			danger: true,
+			action: () => {
+				if (item.abort != null) {
+					item.abort();
+				}
+			}
+		});
 	}
 
 	os.popupMenu(menu, ev.currentTarget ?? ev.target);
@@ -266,7 +291,20 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
 async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
 	firstUploadAttempted.value = true;
 
+	items.value = items.value.map(item => ({
+		...item,
+		aborted: false,
+		uploadFailed: false,
+		waiting: false,
+		uploading: false,
+	}));
+
 	for (const item of items.value.filter(item => item.uploaded == null)) {
+		// アップロード処理途中で値が変わる場合（途中で全キャンセルされたりなど）もあるので、Array filterではなくここでチェック
+		if (item.aborted) {
+			continue;
+		}
+
 		item.waiting = true;
 		item.uploadFailed = false;
 
@@ -296,7 +334,7 @@ async function upload() { // エラーハンドリングなどを考慮してシ
 
 		item.uploading = true;
 
-		const driveFile = await uploadFile(item.compressedImage ?? item.file, {
+		const { filePromise, abort } = uploadFile(item.compressedImage ?? item.file, {
 			name: item.name,
 			folderId: props.folderId,
 			onProgress: (progress) => {
@@ -308,16 +346,43 @@ async function upload() { // エラーハンドリングなどを考慮してシ
 					item.progress.max = progress.total;
 				}
 			},
+		});
+
+		item.abort = () => {
+			item.abort = null;
+			abort();
+			item.uploading = false;
+			item.waiting = false;
+			item.uploadFailed = true;
+		};
+
+		await filePromise.then((file) => {
+			item.uploaded = file;
+			item.abort = null;
 		}).catch(err => {
 			item.uploadFailed = true;
 			item.progress = null;
-			throw err;
+			if (!(err instanceof UploadAbortedError)) {
+				throw err;
+			}
 		}).finally(() => {
 			item.uploading = false;
 			item.waiting = false;
 		});
+	}
+}
 
-		item.uploaded = driveFile;
+function abortAll() {
+	for (const item of items.value) {
+		if (item.uploaded != null) {
+			continue;
+		}
+
+		if (item.abort != null) {
+			item.abort();
+		}
+		item.aborted = true;
+		item.uploadFailed = true;
 	}
 }
 
@@ -340,6 +405,7 @@ function initializeFile(file: File) {
 		thumbnail: window.URL.createObjectURL(file),
 		waiting: false,
 		uploading: false,
+		aborted: false,
 		uploaded: null,
 		uploadFailed: false,
 		file: markRaw(file),
@@ -373,13 +439,6 @@ onMounted(() => {
 	}
 }
 
-.main {
-	padding: 12px;
-}
-
-.items {
-}
-
 .item {
 	position: relative;
 	border-radius: 10px;
-- 
cgit v1.2.3-freya

