vue · 11938 bytes Raw Blame History
1 <template>
2 <div class="file-browser">
3 <!-- Create folder modal -->
4 <CreateFolderModal
5 v-if="showCreateFolder"
6 :current-path="currentPath"
7 @close="showCreateFolder = false"
8 @created="handleFolderCreated"
9 />
10
11 <!-- Toolbar -->
12 <div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-4">
13 <div class="flex items-center justify-between">
14 <!-- Breadcrumb -->
15 <nav class="flex items-center space-x-2 text-sm">
16 <button
17 @click="navigateToPath('/')"
18 class="text-primary-600 hover:text-primary-700 font-medium"
19 >
20 Home
21 </button>
22 <template v-for="(segment, index) in pathSegments" :key="index">
23 <ChevronRightIcon class="w-4 h-4 text-gray-400" />
24 <button
25 @click="navigateToPath(getPathUpTo(index))"
26 class="text-primary-600 hover:text-primary-700"
27 :class="{ 'text-gray-700 dark:text-gray-300': index === pathSegments.length - 1 }"
28 >
29 {{ segment }}
30 </button>
31 </template>
32 </nav>
33
34 <!-- Actions -->
35 <div class="flex items-center space-x-2">
36 <!-- View toggle -->
37 <div class="flex bg-gray-100 dark:bg-gray-700 rounded-md">
38 <button
39 @click="filesStore.setViewMode('grid')"
40 class="p-2 rounded-l-md"
41 :class="viewMode === 'grid' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-200 dark:hover:bg-gray-600'"
42 >
43 <Squares2X2Icon class="w-4 h-4" />
44 </button>
45 <button
46 @click="filesStore.setViewMode('list')"
47 class="p-2 rounded-r-md"
48 :class="viewMode === 'list' ? 'bg-white dark:bg-gray-600 shadow-sm' : 'hover:bg-gray-200 dark:hover:bg-gray-600'"
49 >
50 <ListBulletIcon class="w-4 h-4" />
51 </button>
52 </div>
53
54 <!-- Sort dropdown -->
55 <Menu as="div" class="relative">
56 <MenuButton class="btn btn-outline">
57 <BarsArrowUpIcon class="w-4 h-4 mr-2" />
58 Sort
59 </MenuButton>
60 <MenuItems class="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-10">
61 <MenuItem v-slot="{ active }">
62 <button
63 @click="filesStore.setSortBy('name')"
64 class="w-full text-left px-4 py-2 text-sm"
65 :class="active ? 'bg-gray-100 dark:bg-gray-700' : ''"
66 >
67 Name {{ sortBy === 'name' ? (sortOrder === 'asc' ? '↑' : '↓') : '' }}
68 </button>
69 </MenuItem>
70 <MenuItem v-slot="{ active }">
71 <button
72 @click="filesStore.setSortBy('size')"
73 class="w-full text-left px-4 py-2 text-sm"
74 :class="active ? 'bg-gray-100 dark:bg-gray-700' : ''"
75 >
76 Size {{ sortBy === 'size' ? (sortOrder === 'asc' ? '↑' : '↓') : '' }}
77 </button>
78 </MenuItem>
79 <MenuItem v-slot="{ active }">
80 <button
81 @click="filesStore.setSortBy('date')"
82 class="w-full text-left px-4 py-2 text-sm"
83 :class="active ? 'bg-gray-100 dark:bg-gray-700' : ''"
84 >
85 Date {{ sortBy === 'date' ? (sortOrder === 'asc' ? '↑' : '↓') : '' }}
86 </button>
87 </MenuItem>
88 </MenuItems>
89 </Menu>
90
91 <!-- Actions -->
92 <div class="flex items-center space-x-2">
93 <!-- Create folder button -->
94 <button
95 @click="showCreateFolder = true"
96 class="btn btn-outline"
97 >
98 <FolderPlusIcon class="w-4 h-4 mr-2" />
99 New Folder
100 </button>
101
102 <!-- Upload button -->
103 <button
104 @click="$emit('upload')"
105 class="btn btn-primary"
106 >
107 <ArrowUpTrayIcon class="w-4 h-4 mr-2" />
108 Upload
109 </button>
110 </div>
111 </div>
112 </div>
113
114 <!-- Selection actions -->
115 <div v-if="hasSelection" class="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-md">
116 <div class="flex items-center justify-between">
117 <span class="text-sm text-blue-700 dark:text-blue-300">
118 {{ selectedFiles.size }} file{{ selectedFiles.size !== 1 ? 's' : '' }} selected
119 </span>
120 <div class="flex items-center space-x-2">
121 <button
122 @click="downloadSelected"
123 class="btn btn-sm btn-outline"
124 >
125 <ArrowDownTrayIcon class="w-4 h-4 mr-1" />
126 Download
127 </button>
128 <button
129 @click="deleteSelected"
130 class="btn btn-sm btn-danger"
131 >
132 <TrashIcon class="w-4 h-4 mr-1" />
133 Delete
134 </button>
135 <button
136 @click="filesStore.clearSelection()"
137 class="btn btn-sm btn-outline"
138 >
139 Cancel
140 </button>
141 </div>
142 </div>
143 </div>
144 </div>
145
146 <!-- Loading state -->
147 <div v-if="loading" class="flex items-center justify-center py-12">
148 <div class="flex items-center space-x-3">
149 <div class="spinner w-6 h-6"></div>
150 <span class="text-gray-600 dark:text-gray-400">Loading files...</span>
151 </div>
152 </div>
153
154 <!-- Error state -->
155 <div v-else-if="error" class="p-6 text-center">
156 <ExclamationTriangleIcon class="w-12 h-12 text-red-500 mx-auto mb-3" />
157 <p class="text-red-600 dark:text-red-400 mb-4">{{ error }}</p>
158 <button @click="loadDirectory(currentPath)" class="btn btn-primary">
159 Try Again
160 </button>
161 </div>
162
163 <!-- Empty state -->
164 <div v-else-if="!sortedFiles.length" class="p-12 text-center">
165 <FolderIcon class="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
166 <h3 class="text-lg font-medium text-gray-700 dark:text-gray-300 mb-2">
167 No files found
168 </h3>
169 <p class="text-gray-500 dark:text-gray-400 mb-6">
170 This directory is empty. Upload some files to get started.
171 </p>
172 <button @click="$emit('upload')" class="btn btn-primary">
173 <ArrowUpTrayIcon class="w-4 h-4 mr-2" />
174 Upload Files
175 </button>
176 </div>
177
178 <!-- File grid view -->
179 <div v-else-if="viewMode === 'grid'" class="p-6 file-grid">
180 <FileCard
181 v-for="file in sortedFiles"
182 :key="file.id"
183 :file="file"
184 :selected="selectedFiles.has(file.id)"
185 @click="handleFileClick(file)"
186 @select="toggleSelection(file.id)"
187 @download="downloadFile(file)"
188 @delete="deleteFile(file.id)"
189 />
190 </div>
191
192 <!-- File list view -->
193 <div v-else class="file-list">
194 <FileRow
195 v-for="file in sortedFiles"
196 :key="file.id"
197 :file="file"
198 :selected="selectedFiles.has(file.id)"
199 @click="handleFileClick(file)"
200 @select="toggleSelection(file.id)"
201 @download="downloadFile(file)"
202 @delete="deleteFile(file.id)"
203 />
204 </div>
205 </div>
206 </template>
207
208 <script setup lang="ts">
209 import { computed, onMounted, watch } from 'vue'
210 import { useRoute, useRouter } from 'vue-router'
211 import { useFilesStore } from '@/stores/files'
212 import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
213 import {
214 ChevronRightIcon,
215 Squares2X2Icon,
216 ListBulletIcon,
217 BarsArrowUpIcon,
218 ArrowUpTrayIcon,
219 ArrowDownTrayIcon,
220 TrashIcon,
221 ExclamationTriangleIcon,
222 FolderIcon,
223 FolderPlusIcon,
224 } from '@heroicons/vue/24/outline'
225 import FileCard from './FileCard.vue'
226 import FileRow from './FileRow.vue'
227 import CreateFolderModal from './CreateFolderModal.vue'
228 import type { FileItem } from '@shared/types'
229
230 // Emits
231 defineEmits<{
232 upload: []
233 }>()
234
235 // Stores
236 const filesStore = useFilesStore()
237 const route = useRoute()
238 const router = useRouter()
239
240 // State
241 const showCreateFolder = ref(false)
242
243 // Computed
244 const { currentListing, currentPath, loading, error, selectedFiles, viewMode, sortBy, sortOrder } = filesStore
245 const { sortedFiles, hasSelection } = storeToRefs(filesStore)
246
247 const pathSegments = computed(() => {
248 return currentPath.value.split('/').filter(Boolean)
249 })
250
251 // Methods
252 function navigateToPath(path: string) {
253 router.push({ name: 'files-path', params: { path: path === '/' ? [] : path.split('/').filter(Boolean) } })
254 }
255
256 function getPathUpTo(index: number): string {
257 return '/' + pathSegments.value.slice(0, index + 1).join('/')
258 }
259
260 function handleFileClick(file: FileItem) {
261 if (file.type === 'directory') {
262 const newPath = currentPath.value === '/' ? `/${file.name}` : `${currentPath.value}/${file.name}`
263 navigateToPath(newPath)
264 } else {
265 // Preview or download file
266 downloadFile(file)
267 }
268 }
269
270 function toggleSelection(fileId: string) {
271 filesStore.toggleFileSelection(fileId)
272 }
273
274 async function downloadFile(file: FileItem) {
275 try {
276 await filesStore.downloadFile(file)
277 } catch (error) {
278 console.error('Download failed:', error)
279 }
280 }
281
282 async function deleteFile(fileId: string) {
283 if (confirm('Are you sure you want to delete this file?')) {
284 try {
285 await filesStore.deleteFile(fileId)
286 } catch (error) {
287 console.error('Delete failed:', error)
288 }
289 }
290 }
291
292 async function downloadSelected() {
293 if (!hasSelection.value) return
294
295 try {
296 const fileIds = Array.from(selectedFiles.value)
297
298 // Call bulk download API
299 const response = await fetch('/api/files/bulk/download', {
300 method: 'POST',
301 headers: {
302 'Content-Type': 'application/json',
303 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
304 },
305 body: JSON.stringify({
306 fileIds,
307 format: 'zip',
308 archiveName: `zephyrfs-files-${new Date().toISOString().split('T')[0]}.zip`,
309 }),
310 })
311
312 if (!response.ok) {
313 throw new Error('Bulk download failed')
314 }
315
316 // Create download link
317 const blob = await response.blob()
318 const url = window.URL.createObjectURL(blob)
319 const link = document.createElement('a')
320 link.href = url
321 link.download = `zephyrfs-files-${new Date().toISOString().split('T')[0]}.zip`
322 document.body.appendChild(link)
323 link.click()
324 document.body.removeChild(link)
325 window.URL.revokeObjectURL(url)
326
327 // Clear selection
328 filesStore.clearSelection()
329
330 if (window.$notify) {
331 window.$notify.success(`Downloaded ${fileIds.length} files as ZIP archive`)
332 }
333 } catch (error) {
334 console.error('Bulk download failed:', error)
335 if (window.$notify) {
336 window.$notify.error('Failed to download selected files')
337 }
338 }
339 }
340
341 async function deleteSelected() {
342 if (confirm(`Are you sure you want to delete ${selectedFiles.value.size} file(s)?`)) {
343 try {
344 await filesStore.deleteSelectedFiles()
345 } catch (error) {
346 console.error('Bulk delete failed:', error)
347 }
348 }
349 }
350
351 async function loadDirectory(path: string) {
352 await filesStore.loadDirectory(path)
353 }
354
355 function handleFolderCreated(folderPath: string) {
356 showCreateFolder.value = false
357 // Refresh current directory to show new folder
358 loadDirectory(currentPath.value)
359 }
360
361 // Watch route changes
362 watch(
363 () => route.params.path,
364 (newPath) => {
365 const path = Array.isArray(newPath) ? '/' + newPath.join('/') : newPath || '/'
366 if (path !== currentPath.value) {
367 loadDirectory(path)
368 }
369 },
370 { immediate: true }
371 )
372
373 // Load initial directory
374 onMounted(() => {
375 const path = Array.isArray(route.params.path) ? '/' + route.params.path.join('/') : route.params.path || '/'
376 loadDirectory(path)
377 })
378 </script>