Compare commits
	
		
			1 Commits
		
	
	
		
			2025.2.1-b
			...
			frontend-d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9cd7ea77ff | 
@@ -3,6 +3,8 @@
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import 'reflect-metadata';
 | 
			
		||||
 | 
			
		||||
// https://vitejs.dev/config/build-options.html#build-modulepreload
 | 
			
		||||
import 'vite/modulepreload-polyfill';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								packages/frontend/src/services/AccountService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/frontend/src/services/AccountService.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { inject, injectable, container } from 'tsyringe';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { defineAsyncComponent, reactive, ref } from 'vue';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage.js';
 | 
			
		||||
 | 
			
		||||
type Account = Misskey.entities.MeDetailed & { token: string };
 | 
			
		||||
 | 
			
		||||
const accountData = miLocalStorage.getItem('account');
 | 
			
		||||
 | 
			
		||||
const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class AccountService {
 | 
			
		||||
	constructor(
 | 
			
		||||
	) {}
 | 
			
		||||
 | 
			
		||||
	public readonly i = $i;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										170
									
								
								packages/frontend/src/services/UploaderService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								packages/frontend/src/services/UploaderService.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,170 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { inject, injectable, container } from 'tsyringe';
 | 
			
		||||
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 { getCompressionConfig } from './upload/compress-config.js';
 | 
			
		||||
import { AccountService } from './AccountService.js';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { apiUrl } from '@/config.js';
 | 
			
		||||
import { alert } from '@/os.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
 | 
			
		||||
type Uploading = {
 | 
			
		||||
	id: string;
 | 
			
		||||
	name: string;
 | 
			
		||||
	progressMax: number | undefined;
 | 
			
		||||
	progressValue: number | undefined;
 | 
			
		||||
	img: string;
 | 
			
		||||
};
 | 
			
		||||
export const uploads = ref<Uploading[]>([]);
 | 
			
		||||
 | 
			
		||||
const mimeTypeMap = {
 | 
			
		||||
	'image/webp': 'webp',
 | 
			
		||||
	'image/jpeg': 'jpg',
 | 
			
		||||
	'image/png': 'png',
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class Uploader {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@inject('AccountService') private accountService: AccountService,
 | 
			
		||||
		@inject('ServerMetadataService') private serverMetadataService: ServerMetadataService,
 | 
			
		||||
	) {}
 | 
			
		||||
 | 
			
		||||
	public uploadFile(
 | 
			
		||||
		file: File,
 | 
			
		||||
		folder?: any,
 | 
			
		||||
		name?: string,
 | 
			
		||||
		keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
 | 
			
		||||
	): Promise<Misskey.entities.DriveFile> {
 | 
			
		||||
		if (this.accountService.i == null) throw new Error('Not logged in');
 | 
			
		||||
 | 
			
		||||
		if (folder && typeof folder === 'object') folder = folder.id;
 | 
			
		||||
 | 
			
		||||
		return fetchServerMetadata().then((serverMetadata) => new Promise((resolve, reject) => {
 | 
			
		||||
			if (file.size > serverMetadata.maxFileSize) {
 | 
			
		||||
				alert({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					title: i18n.ts.failedToUpload,
 | 
			
		||||
					text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
 | 
			
		||||
				});
 | 
			
		||||
				return reject();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const id = uuid();
 | 
			
		||||
 | 
			
		||||
			const reader = new FileReader();
 | 
			
		||||
			reader.onload = async (): Promise<void> => {
 | 
			
		||||
				const filename = name ?? file.name ?? 'untitled';
 | 
			
		||||
				const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
 | 
			
		||||
 | 
			
		||||
				const ctx = reactive<Uploading>({
 | 
			
		||||
					id,
 | 
			
		||||
					name: defaultStore.state.keepOriginalFilename ? filename : id + extension,
 | 
			
		||||
					progressMax: undefined,
 | 
			
		||||
					progressValue: undefined,
 | 
			
		||||
					img: window.URL.createObjectURL(file),
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				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', this.accountService.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<XMLHttpRequest>) => {
 | 
			
		||||
					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({
 | 
			
		||||
								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);
 | 
			
		||||
 | 
			
		||||
					resolve(driveFile);
 | 
			
		||||
 | 
			
		||||
					uploads.value = uploads.value.filter(x => x.id !== id);
 | 
			
		||||
				}) as (ev: ProgressEvent<EventTarget>) => any;
 | 
			
		||||
 | 
			
		||||
				xhr.upload.onprogress = ev => {
 | 
			
		||||
					if (ev.lengthComputable) {
 | 
			
		||||
						ctx.progressMax = ev.total;
 | 
			
		||||
						ctx.progressValue = ev.loaded;
 | 
			
		||||
					}
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
				xhr.send(formData);
 | 
			
		||||
			};
 | 
			
		||||
			reader.readAsArrayBuffer(file);
 | 
			
		||||
		}));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user