Compare commits
	
		
			1 Commits
		
	
	
		
			2024.11.0-
			...
			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