-
+
@@ -15,7 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
+
+
+
@@ -48,10 +51,6 @@ const emit = defineEmits<{
}>();
const modal = useTemplateRef('modal');
-const rootEl = useTemplateRef('rootEl');
-const headerEl = useTemplateRef('headerEl');
-const bodyWidth = ref(0);
-const bodyHeight = ref(0);
function close() {
modal.value?.close();
@@ -61,23 +60,6 @@ function onBgClick() {
emit('click');
}
-const ro = new ResizeObserver((entries, observer) => {
- if (rootEl.value == null || headerEl.value == null) return;
- bodyWidth.value = rootEl.value.offsetWidth;
- bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
-});
-
-onMounted(() => {
- if (rootEl.value == null || headerEl.value == null) return;
- bodyWidth.value = rootEl.value.offsetWidth;
- bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
- ro.observe(rootEl.value);
-});
-
-onUnmounted(() => {
- ro.disconnect();
-});
-
defineExpose({
close,
});
@@ -143,7 +125,14 @@ defineExpose({
.body {
flex: 1;
overflow: auto;
- background: var(--MI_THEME-panel);
+ background: var(--MI_THEME-bg);
container-type: size;
}
+
+.footer {
+ padding: 8px 16px;
+ overflow: auto;
+ background: var(--MI_THEME-bg);
+ border-top: 1px solid var(--MI_THEME-divider);
+}
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 5d19f32ea5..4fa33e1f63 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -127,7 +127,6 @@ import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { ensureSignin, notesCount, incNotesCount } from '@/i.js';
import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js';
-import { uploadFile } from '@/utility/upload.js';
import { deepClone } from '@/utility/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage.js';
diff --git a/packages/frontend/src/components/MkUploadDialog.vue b/packages/frontend/src/components/MkUploadDialog.vue
new file mode 100644
index 0000000000..e942a8300a
--- /dev/null
+++ b/packages/frontend/src/components/MkUploadDialog.vue
@@ -0,0 +1,375 @@
+
+
+
+
+
+ {{ i18n.tsx.uploadNFiles({ n: files.length }) }}
+
+
+
+
+
+
+
+
+
+
+
+
{{ ctx.name }}
+
+ {{ bytes(ctx.file.size) }}
+ ({{ i18n.tsx._uploader.compressedToX({ x: bytes(ctx.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - ctx.compressedSize / ctx.file.size) * 100) }) }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.compress }}
+
+
+
+
+
+ {{ i18n.ts.cancel }}
+ {{ i18n.ts.upload }}
+ {{ i18n.ts.retry }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/global/MkSystemIcon.vue b/packages/frontend/src/components/global/MkSystemIcon.vue
index 3454cdc9f2..d2ef0fb2d8 100644
--- a/packages/frontend/src/components/global/MkSystemIcon.vue
+++ b/packages/frontend/src/components/global/MkSystemIcon.vue
@@ -28,13 +28,17 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -62,6 +66,10 @@ const props = defineProps<{
&.error {
color: var(--MI_THEME-error);
}
+
+ &.waiting {
+ color: var(--MI_THEME-accent);
+ }
}
.line {
@@ -87,6 +95,13 @@ const props = defineProps<{
transform: rotate(-90deg);
}
+.animCircleWaiting {
+ stroke-dasharray: var(--l);
+ stroke-dashoffset: calc(var(--l) / 1.5);
+ animation: waiting 0.75s linear infinite;
+ transform-origin: center;
+}
+
.animFade {
opacity: 0;
animation: fade-in var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards;
@@ -104,6 +119,15 @@ const props = defineProps<{
}
}
+@keyframes waiting {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
@keyframes fade-in {
0% {
opacity: 0;
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue
index e8e944df32..0f4912ece1 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue
@@ -95,7 +95,6 @@ import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { validators } from '@/components/grid/cell-validators.js';
import { chooseFileFromDrive, chooseFileFromPc } from '@/utility/select-file.js';
-import { uploadFile } from '@/utility/upload.js';
import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js';
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue
index 5e84ade08d..cf303301c5 100644
--- a/packages/frontend/src/pages/chat/room.form.vue
+++ b/packages/frontend/src/pages/chat/room.form.vue
@@ -41,7 +41,6 @@ import { formatTimeString } from '@/utility/format-time-string.js';
import { selectFile } from '@/utility/select-file.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
-import { uploadFile } from '@/utility/upload.js';
import { miLocalStorage } from '@/local-storage.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/pages/debug.vue b/packages/frontend/src/pages/debug.vue
index 4a28d513f5..5cd68c2c3a 100644
--- a/packages/frontend/src/pages/debug.vue
+++ b/packages/frontend/src/pages/debug.vue
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index fcf9fb234d..da20d23cfd 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -65,8 +65,6 @@ SPDX-License-Identifier: AGPL-3.0-only
v-on="popup.events"
/>
-
-
import('./stream-indicator.vue'));
-const XUpload = defineAsyncComponent(() => import('./upload.vue'));
const XWidgets = defineAsyncComponent(() => import('./widgets.vue'));
const drawerMenuShowing = defineModel('drawerMenuShowing');
diff --git a/packages/frontend/src/ui/_common_/upload.vue b/packages/frontend/src/ui/_common_/upload.vue
deleted file mode 100644
index 3e5653e46d..0000000000
--- a/packages/frontend/src/ui/_common_/upload.vue
+++ /dev/null
@@ -1,134 +0,0 @@
-
-
-
-
-
- -
-
-
-
{{ ctx.name }}
-
- {{ i18n.ts.waiting }}
- {{ String(Math.floor(ctx.progressValue / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}KB / {{ String(Math.floor(ctx.progressMax / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}KB
- {{ Math.floor((ctx.progressValue / ctx.progressMax) * 100) }}
-
-
-
-
-
-
-
-
-
-
-
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/select-file.ts b/packages/frontend/src/utility/select-file.ts
index 731ef58302..fe8a87f111 100644
--- a/packages/frontend/src/utility/select-file.ts
+++ b/packages/frontend/src/utility/select-file.ts
@@ -3,25 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { ref } from 'vue';
+import { defineAsyncComponent, markRaw, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
-import { uploadFile } from '@/utility/upload.js';
import { prefer } from '@/preferences.js';
export function chooseFileFromPc(
multiple: boolean,
options?: {
uploadFolder?: string | null;
- keepOriginal?: boolean;
nameConverter?: (file: File) => string | undefined;
},
): Promise {
const uploadFolder = options?.uploadFolder ?? prefer.s.uploadFolder;
- const keepOriginal = options?.keepOriginal ?? false;
const nameConverter = options?.nameConverter ?? (() => undefined);
return new Promise((res, rej) => {
@@ -30,15 +27,15 @@ export function chooseFileFromPc(
input.multiple = multiple;
input.onchange = () => {
if (!input.files) return res([]);
- const promises = Array.from(
- input.files,
- file => uploadFile(file, uploadFolder, nameConverter(file), keepOriginal),
- );
- Promise.all(promises).then(driveFiles => {
- res(driveFiles);
- }).catch(err => {
- // アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない
+ const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUploadDialog.vue')), {
+ files: markRaw(Array.from(input.files)),
+ folderId: uploadFolder,
+ }, {
+ done: driveFiles => {
+ res(driveFiles);
+ },
+ closed: () => dispose(),
});
// 一応廃棄
@@ -100,10 +97,6 @@ function select(src: HTMLElement | EventTarget | null, label: string | null, mul
text: label,
type: 'label',
} : undefined, {
- text: i18n.ts.upload + ' (' + i18n.ts.compress + ')',
- icon: 'ti ti-upload',
- action: () => chooseFileFromPc(multiple, { keepOriginal: false }).then(files => res(files)),
- }, {
text: i18n.ts.upload,
icon: 'ti ti-upload',
action: () => chooseFileFromPc(multiple, { keepOriginal: true }).then(files => res(files)),
diff --git a/packages/frontend/src/utility/upload.ts b/packages/frontend/src/utility/upload.ts
index 03240749e9..22735db844 100644
--- a/packages/frontend/src/utility/upload.ts
+++ b/packages/frontend/src/utility/upload.ts
@@ -3,160 +3,95 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { reactive, ref } from 'vue';
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 './upload/compress-config.js';
import { $i } from '@/i.js';
-import { alert } from '@/os.js';
-import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
-import { prefer } from '@/preferences.js';
-
-type Uploading = {
- id: string;
- name: string;
- progressMax: number | undefined;
- progressValue: number | undefined;
- img: string;
-};
-export const uploads = ref([]);
-
-const mimeTypeMap = {
- 'image/webp': 'webp',
- 'image/jpeg': 'jpg',
- 'image/png': 'png',
-} as const;
-
-export function uploadFile(
- file: File,
- folder?: string | Misskey.entities.DriveFolder | null,
- name?: string,
- keepOriginal = false,
-): Promise {
- if ($i == null) throw new Error('Not logged in');
-
- const _folder = typeof folder === 'string' ? folder : folder?.id;
-
- if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) {
- alert({
- type: 'error',
- title: i18n.ts.failedToUpload,
- text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
- });
- return Promise.reject();
- }
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+export function uploadFile(file: File | Blob, options: {
+ name?: string;
+ folderId?: string | null;
+ onProgress?: (ctx: { total: number; loaded: number; }) => void;
+} = {}): Promise {
return new Promise((resolve, reject) => {
- const id = uuid();
+ if ($i == null) return reject();
- const reader = new FileReader();
- reader.onload = async (): Promise => {
- const filename = name ?? file.name ?? 'untitled';
- const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
-
- const ctx = reactive({
- id,
- name: prefer.s.keepOriginalFilename ? filename : id + extension,
- progressMax: undefined,
- progressValue: undefined,
- img: window.URL.createObjectURL(file),
+ 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();
+ }
- uploads.value.push(ctx);
-
- const config = !keepOriginal ? await getCompressionConfig(file) : undefined;
- let resizedImage: Blob | undefined;
- if (config) {
- try {
- const resized = await readAndCompressImage(file, config);
- if (resized.size < file.size || 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 / file.size) * 100).toFixed(2);
- console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`);
- }
-
- ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.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 ?? file);
- formData.append('name', ctx.name);
- if (_folder) formData.append('folderId', _folder);
-
- 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) {
- // TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
- uploads.value = uploads.value.filter(x => x.id !== id);
-
- if (xhr.status === 413) {
- alert({
+ 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.cannotUploadBecauseExceedsFileSizeLimit,
+ text: i18n.ts.cannotUploadBecauseInappropriate,
});
- } 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({
+ } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') {
+ os.alert({
type: 'error',
- title: 'Failed to upload',
- text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
+ 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}`,
});
}
-
- reject();
- return;
+ } else {
+ os.alert({
+ type: 'error',
+ title: 'Failed to upload',
+ text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
+ });
}
- const driveFile = JSON.parse(ev.target.response);
+ reject();
+ return;
+ }
- resolve(driveFile);
-
- uploads.value = uploads.value.filter(x => x.id !== id);
- }) as (ev: ProgressEvent) => any;
+ const driveFile = JSON.parse(ev.target.response);
+ resolve(driveFile);
+ }) as (ev: ProgressEvent) => any;
+ if (options.onProgress) {
xhr.upload.onprogress = ev => {
if (ev.lengthComputable) {
- ctx.progressMax = ev.total;
- ctx.progressValue = ev.loaded;
+ options.onProgress({
+ total: ev.total,
+ loaded: ev.loaded,
+ });
}
};
+ }
- xhr.send(formData);
- };
- reader.readAsArrayBuffer(file);
+ const formData = new FormData();
+ formData.append('i', $i.token);
+ formData.append('force', 'true');
+ formData.append('file', file);
+ formData.append('name', options.name ?? file.name ?? 'untitled');
+ 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 3046b7f518..0000000000
--- a/packages/frontend/src/utility/upload/compress-config.ts
+++ /dev/null
@@ -1,36 +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 compressTypeMap = {
- 'image/jpeg': { quality: 0.90, mimeType: 'image/webp' },
- 'image/png': { quality: 1, mimeType: 'image/webp' },
- 'image/webp': { quality: 0.90, mimeType: 'image/webp' },
- 'image/svg+xml': { quality: 1, mimeType: 'image/webp' },
-} as const;
-
-const compressTypeMapFallback = {
- 'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' },
- 'image/png': { quality: 1, mimeType: 'image/png' },
- 'image/webp': { quality: 0.85, mimeType: 'image/jpeg' },
- 'image/svg+xml': { quality: 1, mimeType: 'image/png' },
-} as const;
-
-export async function getCompressionConfig(file: File): Promise {
- const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type];
- if (!imgConfig || await isAnimated(file)) {
- return;
- }
-
- return {
- maxWidth: 2048,
- maxHeight: 2048,
- debug: true,
- ...imgConfig,
- };
-}