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 |  * SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
|  | import 'reflect-metadata'; | ||||||
|  |  | ||||||
| // https://vitejs.dev/config/build-options.html#build-modulepreload | // https://vitejs.dev/config/build-options.html#build-modulepreload | ||||||
| import 'vite/modulepreload-polyfill'; | 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