vue · 10283 bytes Raw Blame History
1 <template>
2 <div class="file-upload">
3 <!-- Upload area -->
4 <div
5 ref="uploadArea"
6 class="upload-area"
7 :class="{
8 'drag-over': isDragOver,
9 'has-files': selectedFiles.length > 0
10 }"
11 @dragover.prevent="handleDragOver"
12 @dragleave.prevent="handleDragLeave"
13 @drop.prevent="handleDrop"
14 @click="triggerFileInput"
15 >
16 <input
17 ref="fileInput"
18 type="file"
19 multiple
20 class="hidden"
21 @change="handleFileSelect"
22 />
23
24 <div class="upload-content">
25 <CloudArrowUpIcon class="w-16 h-16 text-gray-400 mx-auto mb-4" />
26
27 <h3 class="text-lg font-medium text-gray-700 dark:text-gray-300 mb-2">
28 Drop files here or click to browse
29 </h3>
30
31 <p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
32 Upload files up to {{ formatFileSize(maxFileSize) }}
33 </p>
34
35 <div class="flex items-center justify-center space-x-4">
36 <button type="button" class="btn btn-primary">
37 <FolderOpenIcon class="w-4 h-4 mr-2" />
38 Choose Files
39 </button>
40
41 <span class="text-gray-400">or</span>
42
43 <label class="flex items-center">
44 <input
45 v-model="encryptFiles"
46 type="checkbox"
47 class="mr-2 rounded border-gray-300 focus:ring-primary-500"
48 />
49 <span class="text-sm text-gray-600 dark:text-gray-400">
50 Encrypt files
51 </span>
52 </label>
53 </div>
54 </div>
55
56 <!-- Selected files preview -->
57 <div v-if="selectedFiles.length > 0" class="selected-files mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
58 <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
59 Selected Files ({{ selectedFiles.length }})
60 </h4>
61
62 <div class="space-y-2 max-h-48 overflow-y-auto">
63 <div
64 v-for="(file, index) in selectedFiles"
65 :key="index"
66 class="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700 rounded-md"
67 >
68 <div class="flex items-center space-x-3 flex-1 min-w-0">
69 <DocumentIcon class="w-5 h-5 text-gray-400 flex-shrink-0" />
70 <div class="flex-1 min-w-0">
71 <p class="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
72 {{ file.name }}
73 </p>
74 <p class="text-xs text-gray-500 dark:text-gray-400">
75 {{ formatFileSize(file.size) }}
76 </p>
77 </div>
78 </div>
79 <button
80 @click.stop="removeFile(index)"
81 class="text-gray-400 hover:text-red-500 p-1"
82 >
83 <XMarkIcon class="w-4 h-4" />
84 </button>
85 </div>
86 </div>
87
88 <!-- Upload controls -->
89 <div class="flex items-center justify-between mt-4">
90 <div class="text-sm text-gray-500 dark:text-gray-400">
91 Total: {{ formatFileSize(totalSize) }}
92 </div>
93
94 <div class="flex items-center space-x-3">
95 <button
96 @click="clearFiles"
97 class="btn btn-sm btn-outline"
98 >
99 Clear
100 </button>
101 <button
102 @click="startUpload"
103 :disabled="uploading || selectedFiles.length === 0"
104 class="btn btn-sm btn-primary"
105 :class="{ 'opacity-50 cursor-not-allowed': uploading }"
106 >
107 <ArrowUpTrayIcon class="w-4 h-4 mr-2" />
108 {{ uploading ? 'Uploading...' : `Upload ${selectedFiles.length} file${selectedFiles.length !== 1 ? 's' : ''}` }}
109 </button>
110 </div>
111 </div>
112 </div>
113 </div>
114
115 <!-- Upload progress -->
116 <div v-if="activeUploads.length > 0" class="mt-6">
117 <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
118 Upload Progress
119 </h4>
120
121 <div class="space-y-3">
122 <UploadProgress
123 v-for="upload in activeUploads"
124 :key="upload.fileId"
125 :upload="upload"
126 @cancel="cancelUpload(upload.fileId)"
127 />
128 </div>
129 </div>
130
131 <!-- Upload history -->
132 <div v-if="completedUploads.length > 0" class="mt-6">
133 <div class="flex items-center justify-between mb-3">
134 <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300">
135 Recent Uploads
136 </h4>
137 <button
138 @click="clearHistory"
139 class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
140 >
141 Clear History
142 </button>
143 </div>
144
145 <div class="space-y-2">
146 <div
147 v-for="upload in completedUploads.slice(0, 5)"
148 :key="upload.fileId"
149 class="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700 rounded-md"
150 >
151 <div class="flex items-center space-x-3">
152 <CheckCircleIcon
153 v-if="upload.status === 'completed'"
154 class="w-5 h-5 text-green-500"
155 />
156 <ExclamationCircleIcon
157 v-else
158 class="w-5 h-5 text-red-500"
159 />
160 <span class="text-sm text-gray-700 dark:text-gray-300">
161 {{ upload.filename }}
162 </span>
163 </div>
164 <button
165 @click="removeFromHistory(upload.fileId)"
166 class="text-gray-400 hover:text-gray-600 p-1"
167 >
168 <XMarkIcon class="w-4 h-4" />
169 </button>
170 </div>
171 </div>
172 </div>
173 </div>
174 </template>
175
176 <script setup lang="ts">
177 import { ref, computed, nextTick } from 'vue'
178 import { useFilesStore } from '@/stores/files'
179 import { storeToRefs } from 'pinia'
180 import {
181 CloudArrowUpIcon,
182 FolderOpenIcon,
183 DocumentIcon,
184 ArrowUpTrayIcon,
185 XMarkIcon,
186 CheckCircleIcon,
187 ExclamationCircleIcon,
188 } from '@heroicons/vue/24/outline'
189 import UploadProgress from './UploadProgress.vue'
190
191 // Props
192 interface Props {
193 currentPath?: string
194 maxFileSize?: number
195 }
196
197 const props = withDefaults(defineProps<Props>(), {
198 currentPath: '/',
199 maxFileSize: 1024 * 1024 * 1024, // 1GB
200 })
201
202 // Emits
203 const emit = defineEmits<{
204 uploaded: [fileIds: string[]]
205 close: []
206 }>()
207
208 // Store
209 const filesStore = useFilesStore()
210 const { uploads } = storeToRefs(filesStore)
211
212 // Reactive state
213 const fileInput = ref<HTMLInputElement>()
214 const uploadArea = ref<HTMLDivElement>()
215 const selectedFiles = ref<File[]>([])
216 const isDragOver = ref(false)
217 const encryptFiles = ref(false)
218 const uploading = ref(false)
219
220 // Computed
221 const totalSize = computed(() => {
222 return selectedFiles.value.reduce((sum, file) => sum + file.size, 0)
223 })
224
225 const activeUploads = computed(() => {
226 return Array.from(uploads.value.values()).filter(upload => upload.status === 'uploading')
227 })
228
229 const completedUploads = computed(() => {
230 return Array.from(uploads.value.values()).filter(upload =>
231 upload.status === 'completed' || upload.status === 'error'
232 )
233 })
234
235 // File size formatting
236 function formatFileSize(bytes: number): string {
237 if (bytes === 0) return '0 B'
238 const k = 1024
239 const sizes = ['B', 'KB', 'MB', 'GB']
240 const i = Math.floor(Math.log(bytes) / Math.log(k))
241 return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
242 }
243
244 // Drag and drop handlers
245 function handleDragOver(event: DragEvent) {
246 event.preventDefault()
247 isDragOver.value = true
248 }
249
250 function handleDragLeave(event: DragEvent) {
251 event.preventDefault()
252 // Only set to false if we're leaving the upload area entirely
253 if (!uploadArea.value?.contains(event.relatedTarget as Node)) {
254 isDragOver.value = false
255 }
256 }
257
258 function handleDrop(event: DragEvent) {
259 event.preventDefault()
260 isDragOver.value = false
261
262 const files = Array.from(event.dataTransfer?.files || [])
263 addFiles(files)
264 }
265
266 // File input handlers
267 function triggerFileInput() {
268 fileInput.value?.click()
269 }
270
271 function handleFileSelect(event: Event) {
272 const target = event.target as HTMLInputElement
273 const files = Array.from(target.files || [])
274 addFiles(files)
275
276 // Reset input
277 target.value = ''
278 }
279
280 // File management
281 function addFiles(newFiles: File[]) {
282 // Filter out duplicates and validate
283 const validFiles = newFiles.filter(file => {
284 // Check size
285 if (file.size > props.maxFileSize) {
286 console.warn(`File ${file.name} is too large (${formatFileSize(file.size)})`)
287 return false
288 }
289
290 // Check for duplicates
291 const isDuplicate = selectedFiles.value.some(existing =>
292 existing.name === file.name && existing.size === file.size
293 )
294
295 return !isDuplicate
296 })
297
298 selectedFiles.value.push(...validFiles)
299 }
300
301 function removeFile(index: number) {
302 selectedFiles.value.splice(index, 1)
303 }
304
305 function clearFiles() {
306 selectedFiles.value = []
307 }
308
309 // Upload management
310 async function startUpload() {
311 if (selectedFiles.value.length === 0 || uploading.value) return
312
313 uploading.value = true
314
315 try {
316 await filesStore.uploadFiles(selectedFiles.value, props.currentPath, {
317 encrypted: encryptFiles.value
318 })
319
320 // Get uploaded file IDs (simplified - in real app you'd track these properly)
321 const fileIds = selectedFiles.value.map(() => crypto.randomUUID())
322
323 emit('uploaded', fileIds)
324 clearFiles()
325 } catch (error) {
326 console.error('Upload failed:', error)
327 } finally {
328 uploading.value = false
329 }
330 }
331
332 function cancelUpload(uploadId: string) {
333 // TODO: Implement upload cancellation
334 console.log('Cancel upload:', uploadId)
335 }
336
337 function removeFromHistory(uploadId: string) {
338 filesStore.removeUpload(uploadId)
339 }
340
341 function clearHistory() {
342 filesStore.clearCompletedUploads()
343 }
344 </script>
345
346 <style scoped>
347 .upload-area {
348 @apply border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center cursor-pointer transition-colors;
349 }
350
351 .upload-area:hover {
352 @apply border-gray-400 dark:border-gray-500 bg-gray-50 dark:bg-gray-800;
353 }
354
355 .upload-area.drag-over {
356 @apply border-primary-500 bg-primary-50 dark:bg-primary-900/20;
357 }
358
359 .upload-area.has-files {
360 @apply cursor-default;
361 }
362
363 .upload-content {
364 @apply pointer-events-none;
365 }
366
367 .selected-files {
368 @apply pointer-events-auto;
369 }
370 </style>