-
+
{{ ctx.name }}
-
+
{{ bytes(ctx.file.size) }}
+ ({{ bytes(ctx.compressedSize) }})
@@ -34,6 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -55,8 +57,9 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.cancel }}
- {{ i18n.ts.upload }}
+ {{ i18n.ts.cancel }}
+ {{ i18n.ts.upload }}
+ {{ i18n.ts.retry }}
@@ -68,7 +71,8 @@ import * as Misskey from 'misskey-js';
import { v4 as uuid } from 'uuid';
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
import { apiUrl } from '@@/js/config.js';
-import { getCompressionConfig } from '@/utility/upload/compress-config.js';
+import isAnimated from 'is-file-animated';
+import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
@@ -78,9 +82,18 @@ 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/upload.js';
const $i = ensureSignin();
+const compressionSupportedTypes = [
+ 'image/jpeg',
+ 'image/png',
+ 'image/webp',
+ 'image/svg+xml',
+] as const;
+
const mimeTypeMap = {
'image/webp': 'webp',
'image/jpeg': 'jpg',
@@ -108,29 +121,34 @@ const items = ref([] as {
waiting: boolean;
uploading: boolean;
uploaded: Misskey.entities.DriveFile | null;
+ uploadFailed: boolean;
+ compressedSize?: number | null;
+ compressedImage?: Blob | null;
file: File;
}[]);
const dialog = useTemplateRef('dialog');
-const uploadStarted = ref(false);
-const compressionLevel = ref<0 | 1 | 2 | 3>(2);
+const firstUploadAttempted = ref(false);
+const isUploading = computed(() => items.value.some(item => item.uploading));
+const canRetry = computed(() => firstUploadAttempted.value && !isUploading.value && items.value.some(item => item.uploaded == null));
+const compressionLevel = ref<0 | 1 | 2 | 3>(2);
const compressionSettings = computed(() => {
if (compressionLevel.value === 1) {
return {
- maxWidth: 1024 + 512,
- maxHeight: 1024 + 512,
+ maxWidth: 2000,
+ maxHeight: 2000,
};
} else if (compressionLevel.value === 2) {
return {
- maxWidth: 1024 + 256,
- maxHeight: 1024 + 256,
+ maxWidth: 2000 * 0.75, // =1500
+ maxHeight: 2000 * 0.75, // =1500
};
} else if (compressionLevel.value === 3) {
return {
- maxWidth: 1024,
- maxHeight: 1024,
+ maxWidth: 2000 * 0.75 * 0.75, // =1125
+ maxHeight: 2000 * 0.75 * 0.75, // =1125
};
} else {
return null;
@@ -138,7 +156,12 @@ const compressionSettings = computed(() => {
});
watch(items, () => {
- if (uploadStarted.value && items.value.every(item => item.uploaded)) {
+ if (items.value.length === 0) {
+ dialog.value?.close();
+ return;
+ }
+
+ if (items.value.every(item => item.uploaded)) {
emit('done', items.value.map(item => item.uploaded!));
dialog.value?.close();
}
@@ -149,112 +172,60 @@ function cancel() {
dialog.value?.close();
}
-function upload() {
- uploadStarted.value = true;
+function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
- for (const item of items.value) {
- if ((item.file.size > instance.maxFileSize) || (item.file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) {
- alert({
- type: 'error',
- title: i18n.ts.failedToUpload,
- text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
- });
- continue;
- }
+}
+async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
+ firstUploadAttempted.value = true;
+
+ for (const item of items.value.filter(item => item.uploaded == null)) {
item.waiting = true;
- const reader = new FileReader();
- reader.onload = async (): Promise
=> {
- const config = compressionLevel.value !== 0 ? await getCompressionConfig(item.file, compressionSettings.value) : undefined;
- let resizedImage: Blob | undefined;
- if (config) {
- try {
- const resized = await readAndCompressImage(item.file, config);
- if (resized.size < item.file.size || item.file.type === 'image/webp') {
- // The compression may not always reduce the file size
- // (and WebP is not browser safe yet)
- resizedImage = resized;
- }
- if (_DEV_) {
- const saved = ((1 - resized.size / item.file.size) * 100).toFixed(2);
- console.log(`Image compression: before ${item.file.size} bytes, after ${resized.size} bytes, saved ${saved}%`);
- }
+ const shouldCompress = item.compressedImage == null && compressionLevel.value !== 0 && compressionSettings.value && compressionSupportedTypes.includes(item.file.type) && !(await isAnimated(item.file));
- item.name = item.file.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name;
- } catch (err) {
- console.error('Failed to resize image', err);
- }
- }
-
- const formData = new FormData();
- formData.append('i', $i.token);
- formData.append('force', 'true');
- formData.append('file', resizedImage ?? item.file);
- formData.append('name', item.name);
- if (props.folderId) formData.append('folderId', props.folderId);
-
- const xhr = new XMLHttpRequest();
- xhr.open('POST', apiUrl + '/drive/files/create', true);
- xhr.onload = ((ev: ProgressEvent) => {
- item.uploading = false;
-
- if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
- if (xhr.status === 413) {
- alert({
- type: 'error',
- title: i18n.ts.failedToUpload,
- text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
- });
- } else if (ev.target?.response) {
- const res = JSON.parse(ev.target.response);
- if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') {
- alert({
- type: 'error',
- title: i18n.ts.failedToUpload,
- text: i18n.ts.cannotUploadBecauseInappropriate,
- });
- } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') {
- alert({
- type: 'error',
- title: i18n.ts.failedToUpload,
- text: i18n.ts.cannotUploadBecauseNoFreeSpace,
- });
- } else {
- alert({
- type: 'error',
- title: i18n.ts.failedToUpload,
- text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`,
- });
- }
- } else {
- alert({
- type: 'error',
- title: 'Failed to upload',
- text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
- });
- }
-
- reject();
- return;
- }
-
- const driveFile = JSON.parse(ev.target.response);
- item.uploaded = driveFile;
- }) as (ev: ProgressEvent) => any;
-
- xhr.upload.onprogress = ev => {
- if (ev.lengthComputable) {
- item.waiting = false;
- item.progressMax = ev.total;
- item.progressValue = ev.loaded;
- }
+ if (shouldCompress) {
+ const config = {
+ mimeType: isWebpSupported() ? 'image/webp' : 'image/jpeg',
+ maxWidth: compressionSettings.value.maxWidth,
+ maxHeight: compressionSettings.value.maxHeight,
+ quality: isWebpSupported() ? 0.85 : 0.8,
};
- xhr.send(formData);
- item.uploading = true;
- };
- reader.readAsArrayBuffer(item.file);
+ try {
+ const result = await readAndCompressImage(item.file, config);
+ if (result.size < item.file.size || item.file.type === 'image/webp') {
+ // The compression may not always reduce the file size
+ // (and WebP is not browser safe yet)
+ item.compressedImage = markRaw(result);
+ item.compressedSize = result.size;
+ item.name = item.file.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name;
+ }
+ } catch (err) {
+ console.error('Failed to resize image', err);
+ }
+ }
+
+ item.uploading = true;
+
+ const driveFile = await uploadFile(item.compressedImage ?? item.file, {
+ name: item.name,
+ folderId: props.folderId,
+ onProgress: (progress) => {
+ item.waiting = false;
+ item.progressMax = progress.total;
+ item.progressValue = progress.loaded;
+ },
+ }).catch(err => {
+ item.uploadFailed = true;
+ item.progressMax = null;
+ item.progressValue = null;
+ throw err;
+ }).finally(() => {
+ item.uploading = false;
+ });
+
+ item.uploaded = driveFile;
}
}
@@ -272,6 +243,7 @@ onMounted(() => {
waiting: false,
uploading: false,
uploaded: null,
+ uploadFailed: false,
file: markRaw(file),
});
}
@@ -348,4 +320,12 @@ onMounted(() => {
flex: 1;
min-width: 0;
}
+
+.itemInfo {
+ opacity: 0.7;
+ margin-top: 4px;
+ font-size: 90%;
+ display: flex;
+ gap: 8px;
+}
diff --git a/packages/frontend/src/utility/upload/isWebpSupported.ts b/packages/frontend/src/utility/isWebpSupported.ts
similarity index 100%
rename from packages/frontend/src/utility/upload/isWebpSupported.ts
rename to packages/frontend/src/utility/isWebpSupported.ts
diff --git a/packages/frontend/src/utility/upload.ts b/packages/frontend/src/utility/upload.ts
new file mode 100644
index 0000000000..548f879226
--- /dev/null
+++ b/packages/frontend/src/utility/upload.ts
@@ -0,0 +1,97 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { apiUrl } from '@@/js/config.js';
+import { $i } from '@/i.js';
+import { instance } from '@/instance.js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+
+export function uploadFile(file: File, options: {
+ name?: string;
+ folderId?: string | null;
+ onProgress?: (ctx: { total: number; loaded: number; }) => void;
+} = {}): Promise {
+ return new Promise((resolve, reject) => {
+ if ($i == null) return reject();
+
+ if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.failedToUpload,
+ text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
+ });
+ return reject();
+ }
+
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', apiUrl + '/drive/files/create', true);
+ xhr.onload = ((ev: ProgressEvent) => {
+ if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
+ if (xhr.status === 413) {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.failedToUpload,
+ text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
+ });
+ } else if (ev.target?.response) {
+ const res = JSON.parse(ev.target.response);
+ if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.failedToUpload,
+ text: i18n.ts.cannotUploadBecauseInappropriate,
+ });
+ } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.failedToUpload,
+ text: i18n.ts.cannotUploadBecauseNoFreeSpace,
+ });
+ } else {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.failedToUpload,
+ text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`,
+ });
+ }
+ } else {
+ os.alert({
+ type: 'error',
+ title: 'Failed to upload',
+ text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
+ });
+ }
+
+ reject();
+ return;
+ }
+
+ const driveFile = JSON.parse(ev.target.response);
+ resolve(driveFile);
+ }) as (ev: ProgressEvent) => any;
+
+ if (options.onProgress) {
+ xhr.upload.onprogress = ev => {
+ if (ev.lengthComputable) {
+ options.onProgress({
+ total: ev.total,
+ loaded: ev.loaded,
+ });
+ }
+ };
+ }
+
+ const formData = new FormData();
+ formData.append('i', $i.token);
+ formData.append('force', 'true');
+ formData.append('file', file);
+ formData.append('name', options.name ?? file.name);
+ if (options.folderId) formData.append('folderId', options.folderId);
+
+ xhr.send(formData);
+ });
+}
diff --git a/packages/frontend/src/utility/upload/compress-config.ts b/packages/frontend/src/utility/upload/compress-config.ts
deleted file mode 100644
index 1868d262d2..0000000000
--- a/packages/frontend/src/utility/upload/compress-config.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import isAnimated from 'is-file-animated';
-import { isWebpSupported } from './isWebpSupported.js';
-import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer';
-
-const supportedTypes = [
- 'image/jpeg',
- 'image/png',
- 'image/webp',
- 'image/svg+xml',
-] as const;
-
-export async function getCompressionConfig(file: File, options: Partial<{ maxWidth: number; maxHeight: number; }> = {}): Promise {
- if (!supportedTypes.includes(file.type) || await isAnimated(file)) {
- return;
- }
-
- return {
- mimeType: isWebpSupported() ? 'image/webp' : 'image/jpeg',
- maxWidth: options.maxWidth ?? 2048,
- maxHeight: options.maxHeight ?? 2048,
- quality: isWebpSupported() ? 0.9 : 0.85,
- debug: true,
- };
-}