wip
This commit is contained in:
		
							parent
							
								
									012213fc64
								
							
						
					
					
						commit
						171632365d
					
				|  | @ -9584,6 +9584,10 @@ export interface Locale extends ILocale { | |||
|         "disableFederationDescription": string; | ||||
|     }; | ||||
|     "_postForm": { | ||||
|         /** | ||||
|          * アップロードされていないファイルがありますが、破棄してフォームを閉じますか? | ||||
|          */ | ||||
|         "quitInspiteOfThereAreUnuploadedFilesConfirm": string; | ||||
|         /** | ||||
|          * このノートに返信... | ||||
|          */ | ||||
|  |  | |||
|  | @ -2522,6 +2522,7 @@ _visibility: | |||
|   disableFederationDescription: "他サーバーへの配信を行いません" | ||||
| 
 | ||||
| _postForm: | ||||
|   quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされていないファイルがありますが、破棄してフォームを閉じますか?" | ||||
|   replyPlaceholder: "このノートに返信..." | ||||
|   quotePlaceholder: "このノートを引用..." | ||||
|   channelPlaceholder: "チャンネルに投稿..." | ||||
|  |  | |||
|  | @ -214,6 +214,11 @@ const uploader = useUploader({ | |||
| 	features: props.features, | ||||
| }); | ||||
| 
 | ||||
| uploader.events.on('itemUploaded', ctx => { | ||||
| 	files.value.push(ctx.item.uploaded!); | ||||
| 	uploader.removeItem(ctx.item); | ||||
| }); | ||||
| 
 | ||||
| const draftKey = computed((): string => { | ||||
| 	let key = props.channel ? `channel:${props.channel.id}` : ''; | ||||
| 
 | ||||
|  | @ -1151,8 +1156,23 @@ onMounted(() => { | |||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| async function canClose() { | ||||
| 	if (!uploader.allItemsUploaded.value) { | ||||
| 		const { canceled } = await os.confirm({ | ||||
| 			type: 'question', | ||||
| 			text: i18n.ts._postForm.quitInspiteOfThereAreUnuploadedFilesConfirm, | ||||
| 			okText: i18n.ts.yes, | ||||
| 			cancelText: i18n.ts.no, | ||||
| 		}); | ||||
| 		if (canceled) return false; | ||||
| 	} | ||||
| 
 | ||||
| 	return true; | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	clear, | ||||
| 	canClose, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| <MkModal | ||||
| 	ref="modal" | ||||
| 	:preferType="'dialog'" | ||||
| 	@click="modal?.close()" | ||||
| 	@click="_close()" | ||||
| 	@closed="onModalClosed()" | ||||
| 	@esc="modal?.close()" | ||||
| 	@esc="_close()" | ||||
| > | ||||
| 	<MkPostForm | ||||
| 		ref="form" | ||||
|  | @ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 		autofocus | ||||
| 		freezeAfterPosted | ||||
| 		@posted="onPosted" | ||||
| 		@cancel="modal?.close()" | ||||
| 		@esc="modal?.close()" | ||||
| 		@cancel="_close()" | ||||
| 		@esc="_close()" | ||||
| 	/> | ||||
| </MkModal> | ||||
| </template> | ||||
|  | @ -43,6 +43,7 @@ const emit = defineEmits<{ | |||
| }>(); | ||||
| 
 | ||||
| const modal = useTemplateRef('modal'); | ||||
| const form = useTemplateRef('form'); | ||||
| 
 | ||||
| function onPosted() { | ||||
| 	modal.value?.close({ | ||||
|  | @ -50,6 +51,12 @@ function onPosted() { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function _close() { | ||||
| 	const canClose = await form.value?.canClose(); | ||||
| 	if (!canClose) return; | ||||
| 	modal.value?.close(); | ||||
| } | ||||
| 
 | ||||
| function onModalClosed() { | ||||
| 	emit('closed'); | ||||
| } | ||||
|  |  | |||
|  | @ -93,7 +93,7 @@ onMounted(() => { | |||
| const items = uploader.items; | ||||
| 
 | ||||
| const firstUploadAttempted = ref(false); | ||||
| const canRetry = computed(() => firstUploadAttempted.value && !items.value.some(item => item.uploading || item.preprocessing) && items.value.some(item => item.uploaded == null)); | ||||
| const canRetry = computed(() => firstUploadAttempted.value && uploader.readyForUpload.value); | ||||
| const canDone = computed(() => items.value.some(item => item.uploaded != null)); | ||||
| const overallProgress = computed(() => { | ||||
| 	const max = items.value.length; | ||||
|  | @ -151,7 +151,7 @@ async function abortWithConfirm() { | |||
| } | ||||
| 
 | ||||
| async function done() { | ||||
| 	if (items.value.some(item => item.uploaded == null)) { | ||||
| 	if (!uploader.allItemsUploaded.value) { | ||||
| 		const { canceled } = await os.confirm({ | ||||
| 			type: 'question', | ||||
| 			text: i18n.ts._uploader.doneConfirm, | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 			<div :class="$style.itemActionWrapper"> | ||||
| 				<MkButton :iconOnly="true" rounded @click="emit('showMenu', item, $event)"><i class="ti ti-dots"></i></MkButton> | ||||
| 			</div> | ||||
| 			<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }"></div> | ||||
| 			<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }" @click="onThumbnailClick(item, $event)"></div> | ||||
| 			<div :class="$style.itemBody"> | ||||
| 				<div><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div> | ||||
| 				<div :class="$style.itemInfo"> | ||||
|  | @ -60,6 +60,10 @@ function onContextmenu(item: UploaderItem, ev: MouseEvent) { | |||
| 
 | ||||
| 	emit('showMenuViaContextmenu', item, ev); | ||||
| } | ||||
| 
 | ||||
| function onThumbnailClick(item: UploaderItem, ev: MouseEvent) { | ||||
| 	// TODO: preview when item is image | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" module> | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
| import * as Misskey from 'misskey-js'; | ||||
| import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; | ||||
| import isAnimated from 'is-file-animated'; | ||||
| import { EventEmitter } from 'eventemitter3'; | ||||
| import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef } from 'vue'; | ||||
| import type { MenuItem } from '@/types/menu.js'; | ||||
| import { genId } from '@/utility/id.js'; | ||||
|  | @ -97,6 +98,10 @@ export function useUploader(options: { | |||
| } = {}) { | ||||
| 	const $i = ensureSignin(); | ||||
| 
 | ||||
| 	const events = new EventEmitter<{ | ||||
| 		'itemUploaded': (ctx: { item: UploaderItem; }) => void; | ||||
| 	}>(); | ||||
| 
 | ||||
| 	const uploaderFeatures = computed<Required<UploaderFeatures>>(() => { | ||||
| 		return { | ||||
| 			effect: options.features?.effect ?? true, | ||||
|  | @ -145,22 +150,28 @@ export function useUploader(options: { | |||
| 	function getMenu(item: UploaderItem): MenuItem[] { | ||||
| 		const menu: MenuItem[] = []; | ||||
| 
 | ||||
| 		menu.push({ | ||||
| 			icon: 'ti ti-cursor-text', | ||||
| 			text: i18n.ts.rename, | ||||
| 			action: async () => { | ||||
| 				const { result, canceled } = await os.inputText({ | ||||
| 					type: 'text', | ||||
| 					title: i18n.ts.rename, | ||||
| 					placeholder: item.name, | ||||
| 					default: item.name, | ||||
| 				}); | ||||
| 				if (canceled) return; | ||||
| 				if (result.trim() === '') return; | ||||
| 		if ( | ||||
| 			!item.preprocessing && | ||||
| 			!item.uploading && | ||||
| 			!item.uploaded | ||||
| 		) { | ||||
| 			menu.push({ | ||||
| 				icon: 'ti ti-cursor-text', | ||||
| 				text: i18n.ts.rename, | ||||
| 				action: async () => { | ||||
| 					const { result, canceled } = await os.inputText({ | ||||
| 						type: 'text', | ||||
| 						title: i18n.ts.rename, | ||||
| 						placeholder: item.name, | ||||
| 						default: item.name, | ||||
| 					}); | ||||
| 					if (canceled) return; | ||||
| 					if (result.trim() === '') return; | ||||
| 
 | ||||
| 				item.name = result; | ||||
| 			}, | ||||
| 		}); | ||||
| 					item.name = result; | ||||
| 				}, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		if ( | ||||
| 			uploaderFeatures.value.crop && | ||||
|  | @ -332,6 +343,12 @@ export function useUploader(options: { | |||
| 		if (!item.preprocessing && !item.uploading && !item.uploaded) { | ||||
| 			menu.push({ | ||||
| 				type: 'divider', | ||||
| 			}, { | ||||
| 				icon: 'ti ti-upload', | ||||
| 				text: i18n.ts.upload, | ||||
| 				action: () => { | ||||
| 					uploadOne(item); | ||||
| 				}, | ||||
| 			}, { | ||||
| 				icon: 'ti ti-x', | ||||
| 				text: i18n.ts.remove, | ||||
|  | @ -357,6 +374,45 @@ export function useUploader(options: { | |||
| 		return menu; | ||||
| 	} | ||||
| 
 | ||||
| 	async function uploadOne(item: UploaderItem): Promise<void> { | ||||
| 		item.uploadFailed = false; | ||||
| 		item.uploading = true; | ||||
| 
 | ||||
| 		const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, { | ||||
| 			name: item.uploadName ?? item.name, | ||||
| 			folderId: options.folderId, | ||||
| 			onProgress: (progress) => { | ||||
| 				if (item.progress == null) { | ||||
| 					item.progress = { max: progress.total, value: progress.loaded }; | ||||
| 				} else { | ||||
| 					item.progress.value = progress.loaded; | ||||
| 					item.progress.max = progress.total; | ||||
| 				} | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
| 		item.abort = () => { | ||||
| 			item.abort = null; | ||||
| 			abort(); | ||||
| 			item.uploading = false; | ||||
| 			item.uploadFailed = true; | ||||
| 		}; | ||||
| 
 | ||||
| 		await filePromise.then((file) => { | ||||
| 			item.uploaded = file; | ||||
| 			item.abort = null; | ||||
| 			events.emit('itemUploaded', { item }); | ||||
| 		}).catch(err => { | ||||
| 			item.uploadFailed = true; | ||||
| 			item.progress = null; | ||||
| 			if (!(err instanceof UploadAbortedError)) { | ||||
| 				throw err; | ||||
| 			} | ||||
| 		}).finally(() => { | ||||
| 			item.uploading = false; | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
 | ||||
| 		items.value = items.value.map(item => ({ | ||||
| 			...item, | ||||
|  | @ -371,41 +427,7 @@ export function useUploader(options: { | |||
| 				continue; | ||||
| 			} | ||||
| 
 | ||||
| 			item.uploadFailed = false; | ||||
| 			item.uploading = true; | ||||
| 
 | ||||
| 			const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, { | ||||
| 				name: item.uploadName ?? item.name, | ||||
| 				folderId: options.folderId, | ||||
| 				onProgress: (progress) => { | ||||
| 					if (item.progress == null) { | ||||
| 						item.progress = { max: progress.total, value: progress.loaded }; | ||||
| 					} else { | ||||
| 						item.progress.value = progress.loaded; | ||||
| 						item.progress.max = progress.total; | ||||
| 					} | ||||
| 				}, | ||||
| 			}); | ||||
| 
 | ||||
| 			item.abort = () => { | ||||
| 				item.abort = null; | ||||
| 				abort(); | ||||
| 				item.uploading = false; | ||||
| 				item.uploadFailed = true; | ||||
| 			}; | ||||
| 
 | ||||
| 			await filePromise.then((file) => { | ||||
| 				item.uploaded = file; | ||||
| 				item.abort = null; | ||||
| 			}).catch(err => { | ||||
| 				item.uploadFailed = true; | ||||
| 				item.progress = null; | ||||
| 				if (!(err instanceof UploadAbortedError)) { | ||||
| 					throw err; | ||||
| 				} | ||||
| 			}).finally(() => { | ||||
| 				item.uploading = false; | ||||
| 			}); | ||||
| 			await uploadOne(item); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | @ -505,7 +527,9 @@ export function useUploader(options: { | |||
| 		upload, | ||||
| 		getMenu, | ||||
| 		uploading: computed(() => items.value.some(item => item.uploading)), | ||||
| 		readyForUpload: computed(() => items.value.length > 0 && !items.value.some(item => item.uploading || item.preprocessing)), | ||||
| 		readyForUpload: computed(() => items.value.length > 0 && items.value.some(item => item.uploaded == null) && !items.value.some(item => item.uploading || item.preprocessing)), | ||||
| 		allItemsUploaded: computed(() => items.value.every(item => item.uploaded != null)), | ||||
| 		events, | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue