956 lines
27 KiB
Vue
956 lines
27 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
|
import { useDisplay } from 'vuetify'
|
|
import { useModuleStore } from '@KTXC'
|
|
import { useFileManager, useFileSelection, useFileUpload, useFileEditor } from '@/composables'
|
|
import type { ViewMode, SortField, SortOrder, BreadcrumbItem } from '@/types'
|
|
import { CollectionObject } from '@DocumentsManager/models/collection'
|
|
import { EntityObject } from '@DocumentsManager/models/entity'
|
|
import { ServiceObject } from '@DocumentsManager/models/service'
|
|
import { useServicesStore } from '@DocumentsManager/stores/servicesStore'
|
|
|
|
// Components
|
|
import {
|
|
FilesToolbar,
|
|
FilesSidebar,
|
|
FilesBreadcrumbs,
|
|
FilesEmptyState,
|
|
FilesDragOverlay,
|
|
FilesInfoPanel,
|
|
FilesGridView,
|
|
FilesListView,
|
|
FilesDetailsView,
|
|
FileViewerDialog,
|
|
FileEditorDialog,
|
|
NewFolderDialog,
|
|
RenameDialog,
|
|
DeleteConfirmDialog,
|
|
UploadDialog,
|
|
} from '@/components'
|
|
|
|
// Check if file manager is available
|
|
const display = useDisplay()
|
|
const isMobile = computed(() => display.mdAndDown.value)
|
|
|
|
const moduleStore = useModuleStore()
|
|
const isFileManagerAvailable = computed(() => {
|
|
return moduleStore.has('documents_manager') || moduleStore.has('DocumentsManager')
|
|
})
|
|
|
|
const servicesStore = useServicesStore()
|
|
|
|
// Active provider/service (will be selectable later)
|
|
const activeProviderId = ref('default')
|
|
const activeServiceId = ref('personal')
|
|
|
|
// File manager composable
|
|
const fileManager = useFileManager({
|
|
providerId: activeProviderId,
|
|
serviceId: activeServiceId,
|
|
})
|
|
|
|
// Selection composable
|
|
const selection = useFileSelection({ multiple: true })
|
|
|
|
// Upload composable
|
|
const upload = useFileUpload({
|
|
providerId: activeProviderId,
|
|
serviceId: activeServiceId,
|
|
})
|
|
|
|
// Keep upload collection in sync with current location
|
|
watch(() => fileManager.currentLocation.value, (newLocation) => {
|
|
upload.setCollection(newLocation)
|
|
})
|
|
|
|
watch(() => fileManager.error.value, (message) => {
|
|
if (message) {
|
|
notify(message, 'error')
|
|
}
|
|
})
|
|
|
|
// View state
|
|
const viewMode = ref<ViewMode>('grid')
|
|
const sortField = ref<SortField>('label')
|
|
const sortOrder = ref<SortOrder>('asc')
|
|
const sidebarVisible = ref(true)
|
|
const searchQuery = ref('')
|
|
|
|
// Dialogs
|
|
const showNewFolderDialog = ref(false)
|
|
const showRenameDialog = ref(false)
|
|
const showDeleteDialog = ref(false)
|
|
const showUploadDialog = ref(false)
|
|
const nodeToRename = ref<CollectionObject | EntityObject | null>(null)
|
|
const nodesToDelete = ref<(CollectionObject | EntityObject)[]>([])
|
|
|
|
// Drag and drop state
|
|
const isDragOver = ref(false)
|
|
|
|
// Notifications
|
|
const snackbarVisible = ref(false)
|
|
const snackbarMessage = ref('')
|
|
const snackbarColor = ref<'success' | 'info' | 'warning' | 'error'>('success')
|
|
|
|
// Upload preparation state
|
|
const isPreparingUploads = ref(false)
|
|
const uploadPreparationMessage = ref('Preparing uploads...')
|
|
const uploadPreparationProcessedCount = ref(0)
|
|
const uploadPreparationTotalCount = ref(0)
|
|
|
|
// Viewer state
|
|
const viewerEntity = ref<EntityObject | null>(null)
|
|
const showViewer = ref(false)
|
|
|
|
// Editor state
|
|
const fileEditorComposable = useFileEditor()
|
|
const editorEntity = ref<EntityObject | null>(null)
|
|
const showEditor = ref(false)
|
|
|
|
function notify(message: string, color: 'success' | 'info' | 'warning' | 'error' = 'success') {
|
|
snackbarMessage.value = message
|
|
snackbarColor.value = color
|
|
snackbarVisible.value = true
|
|
}
|
|
|
|
function getErrorMessage(error: unknown, fallback: string) {
|
|
return error instanceof Error ? error.message : fallback
|
|
}
|
|
|
|
function beginUploadPreparation(message: string, totalCount: number = 0) {
|
|
isPreparingUploads.value = true
|
|
uploadPreparationMessage.value = message
|
|
uploadPreparationProcessedCount.value = 0
|
|
uploadPreparationTotalCount.value = totalCount
|
|
}
|
|
|
|
function updateUploadPreparation(processedCount: number, totalCount: number) {
|
|
uploadPreparationProcessedCount.value = processedCount
|
|
uploadPreparationTotalCount.value = totalCount
|
|
}
|
|
|
|
function finishUploadPreparation() {
|
|
isPreparingUploads.value = false
|
|
uploadPreparationProcessedCount.value = 0
|
|
uploadPreparationTotalCount.value = 0
|
|
}
|
|
|
|
async function queueFolderUploads(
|
|
filesOrList: FileList | { file: File; relativePath: string }[],
|
|
message: string,
|
|
successMessage: string
|
|
) {
|
|
const totalCount = filesOrList.length
|
|
|
|
showUploadDialog.value = true
|
|
beginUploadPreparation(message, totalCount)
|
|
await nextTick()
|
|
|
|
try {
|
|
await upload.addFilesWithPathsBatched(filesOrList, {
|
|
batchSize: 250,
|
|
onProgress: updateUploadPreparation,
|
|
})
|
|
|
|
notify(successMessage, 'info')
|
|
} catch (error) {
|
|
console.error('Failed to prepare uploads:', error)
|
|
notify(getErrorMessage(error, 'Failed to prepare uploads'), 'error')
|
|
} finally {
|
|
finishUploadPreparation()
|
|
}
|
|
}
|
|
|
|
function handleOpenItem(item: CollectionObject | EntityObject) {
|
|
if (item instanceof EntityObject) {
|
|
viewerEntity.value = item
|
|
showViewer.value = true
|
|
} else {
|
|
// Folders: navigate into them when opened via double-click / action
|
|
selection.clear()
|
|
handleFolderOpen(item as CollectionObject)
|
|
}
|
|
}
|
|
|
|
function handleViewerNavigate(entity: EntityObject) {
|
|
viewerEntity.value = entity
|
|
}
|
|
|
|
function handleEditItem(item: CollectionObject | EntityObject) {
|
|
if (item instanceof EntityObject && fileEditorComposable.canEdit(item)) {
|
|
editorEntity.value = item
|
|
showEditor.value = true
|
|
}
|
|
}
|
|
|
|
// Hidden file inputs
|
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
|
const folderInputRef = ref<HTMLInputElement | null>(null)
|
|
|
|
function nodeId(node: CollectionObject | EntityObject): string {
|
|
return String(node.identifier ?? '')
|
|
}
|
|
|
|
function nodeLabel(node: CollectionObject | EntityObject): string {
|
|
if (node instanceof EntityObject) {
|
|
return node.properties.label || nodeId(node)
|
|
}
|
|
return node.properties.label || nodeId(node)
|
|
}
|
|
|
|
function renameCurrentName(node: CollectionObject | EntityObject | null): string {
|
|
if (!node) return ''
|
|
if (node instanceof EntityObject) {
|
|
return node.properties.label || nodeId(node)
|
|
}
|
|
return node.properties.label || nodeId(node)
|
|
}
|
|
|
|
// Computed
|
|
const breadcrumbs = computed<BreadcrumbItem[]>(() => {
|
|
const items: BreadcrumbItem[] = [
|
|
{ id: fileManager.ROOT_ID, label: 'Home', isRoot: true }
|
|
]
|
|
|
|
for (const node of fileManager.breadcrumbs.value) {
|
|
items.push({
|
|
id: nodeId(node),
|
|
label: nodeLabel(node),
|
|
isRoot: false,
|
|
})
|
|
}
|
|
|
|
return items
|
|
})
|
|
|
|
const sortedItems = computed(() => {
|
|
const collections = [...fileManager.currentCollections.value]
|
|
const entities = [...fileManager.currentEntities.value]
|
|
|
|
// Sort collections
|
|
collections.sort((a, b) => {
|
|
const aVal = sortField.value === 'label'
|
|
? (a.properties.label || nodeId(a))
|
|
: sortField.value === 'modifiedOn'
|
|
? (a.modified?.getTime() ?? 0)
|
|
: sortField.value === 'createdOn'
|
|
? (a.created?.getTime() ?? 0)
|
|
: ''
|
|
const bVal = sortField.value === 'label'
|
|
? (b.properties.label || nodeId(b))
|
|
: sortField.value === 'modifiedOn'
|
|
? (b.modified?.getTime() ?? 0)
|
|
: sortField.value === 'createdOn'
|
|
? (b.created?.getTime() ?? 0)
|
|
: ''
|
|
const cmp = String(aVal).localeCompare(String(bVal))
|
|
return sortOrder.value === 'asc' ? cmp : -cmp
|
|
})
|
|
|
|
// Sort entities
|
|
entities.sort((a, b) => {
|
|
const aVal = sortField.value === 'label'
|
|
? (a.properties.label || nodeId(a))
|
|
: sortField.value === 'mime'
|
|
? (a.properties.mime || '')
|
|
: sortField.value === 'size'
|
|
? a.properties.size
|
|
: sortField.value === 'modifiedOn'
|
|
? (a.modified?.getTime() ?? 0)
|
|
: sortField.value === 'createdOn'
|
|
? (a.created?.getTime() ?? 0)
|
|
: ''
|
|
const bVal = sortField.value === 'label'
|
|
? (b.properties.label || nodeId(b))
|
|
: sortField.value === 'mime'
|
|
? (b.properties.mime || '')
|
|
: sortField.value === 'size'
|
|
? b.properties.size
|
|
: sortField.value === 'modifiedOn'
|
|
? (b.modified?.getTime() ?? 0)
|
|
: sortField.value === 'createdOn'
|
|
? (b.created?.getTime() ?? 0)
|
|
: ''
|
|
const cmp = String(aVal).localeCompare(String(bVal))
|
|
return sortOrder.value === 'asc' ? cmp : -cmp
|
|
})
|
|
|
|
// Filter by search
|
|
const filterFn = (item: CollectionObject | EntityObject) => {
|
|
if (!searchQuery.value) return true
|
|
const label = item instanceof EntityObject
|
|
? (item.properties.label || nodeId(item))
|
|
: (item.properties.label || nodeId(item))
|
|
return label.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
}
|
|
|
|
return {
|
|
collections: collections.filter(filterFn),
|
|
entities: entities.filter(filterFn),
|
|
}
|
|
})
|
|
|
|
const hasItems = computed(() =>
|
|
sortedItems.value.collections.length > 0 || sortedItems.value.entities.length > 0
|
|
)
|
|
|
|
const hasSelection = computed(() => selection.hasSelection.value)
|
|
|
|
const selectedIds = computed(() => selection.selectedIds.value)
|
|
|
|
const availableServices = computed<ServiceObject[]>(() => {
|
|
const services = servicesStore.services
|
|
const enabledServices = services.filter(service => service.enabled)
|
|
const source = enabledServices.length > 0 ? enabledServices : services
|
|
|
|
return [...source].sort((left, right) => {
|
|
const leftLabel = (left.label || String(left.identifier || '')).toLowerCase()
|
|
const rightLabel = (right.label || String(right.identifier || '')).toLowerCase()
|
|
return leftLabel.localeCompare(rightLabel)
|
|
})
|
|
})
|
|
|
|
function syncActiveService(services: ServiceObject[]) {
|
|
if (services.length === 0) {
|
|
return
|
|
}
|
|
|
|
const currentService = services.find(service =>
|
|
service.provider === activeProviderId.value &&
|
|
String(service.identifier ?? '') === activeServiceId.value
|
|
)
|
|
|
|
if (currentService) {
|
|
return
|
|
}
|
|
|
|
const [nextService] = services
|
|
activeProviderId.value = nextService.provider
|
|
activeServiceId.value = String(nextService.identifier ?? '')
|
|
}
|
|
|
|
watch(availableServices, (services) => {
|
|
syncActiveService(services)
|
|
}, { immediate: true })
|
|
|
|
watch([activeProviderId, activeServiceId], async ([providerId, serviceId], [previousProviderId, previousServiceId]) => {
|
|
if (!providerId || !serviceId) {
|
|
return
|
|
}
|
|
|
|
if (providerId === previousProviderId && serviceId === previousServiceId) {
|
|
return
|
|
}
|
|
|
|
selection.clear()
|
|
upload.setCollection(fileManager.rootId.value)
|
|
await fileManager.navigateToRoot()
|
|
}, { flush: 'post' })
|
|
|
|
async function handleServiceSelect(service: ServiceObject) {
|
|
const providerId = service.provider
|
|
const serviceId = String(service.identifier ?? '')
|
|
|
|
if (providerId === activeProviderId.value && serviceId === activeServiceId.value) {
|
|
return
|
|
}
|
|
|
|
activeProviderId.value = providerId
|
|
activeServiceId.value = serviceId
|
|
if (isMobile.value) {
|
|
sidebarVisible.value = false
|
|
}
|
|
notify(`Switched to ${service.label || serviceId}`, 'info')
|
|
}
|
|
|
|
// Navigation methods
|
|
async function handleBreadcrumbNavigate(item: BreadcrumbItem) {
|
|
selection.clear()
|
|
await fileManager.navigateTo(item.isRoot ? null : item.id)
|
|
}
|
|
|
|
async function handleFolderOpen(folder: CollectionObject) {
|
|
selection.clear()
|
|
await fileManager.navigateTo(nodeId(folder))
|
|
}
|
|
|
|
// Item interaction methods
|
|
function handleItemClick(item: CollectionObject | EntityObject, event: MouseEvent | KeyboardEvent) {
|
|
const hasCtrl = event.ctrlKey || event.metaKey
|
|
const hasShift = event.shiftKey
|
|
|
|
if (hasCtrl) {
|
|
// Ctrl+click: toggle selection
|
|
selection.toggle(item)
|
|
} else if (hasShift && selection.hasSelection.value) {
|
|
// Shift+click: extend selection
|
|
selection.select(item)
|
|
} else {
|
|
// Single click behavior depends on item type
|
|
if (item instanceof CollectionObject) {
|
|
// Folders: navigate into them
|
|
selection.clear()
|
|
handleFolderOpen(item as CollectionObject)
|
|
} else {
|
|
// Files: show info panel
|
|
selection.clear()
|
|
selection.select(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show details panel for an item
|
|
function handleShowDetails(item: CollectionObject | EntityObject) {
|
|
selection.clear()
|
|
selection.select(item)
|
|
}
|
|
|
|
// Folder operations
|
|
async function handleCreateFolder(name: string) {
|
|
try {
|
|
await fileManager.createFolder(name)
|
|
showNewFolderDialog.value = false
|
|
notify(`Folder "${name}" created`, 'success')
|
|
} catch (error) {
|
|
console.error('Failed to create folder:', error)
|
|
notify(getErrorMessage(error, 'Failed to create folder'), 'error')
|
|
}
|
|
}
|
|
|
|
// Rename operations - for specific item
|
|
function handleRenameItem(item: CollectionObject | EntityObject) {
|
|
nodeToRename.value = item
|
|
showRenameDialog.value = true
|
|
}
|
|
|
|
async function handleRename(newName: string) {
|
|
if (!nodeToRename.value) return
|
|
const currentName = renameCurrentName(nodeToRename.value)
|
|
|
|
try {
|
|
await fileManager.renameNode(nodeId(nodeToRename.value), newName)
|
|
showRenameDialog.value = false
|
|
nodeToRename.value = null
|
|
notify(`Renamed "${currentName}" to "${newName}"`, 'success')
|
|
} catch (error) {
|
|
console.error('Failed to rename:', error)
|
|
notify(getErrorMessage(error, 'Failed to rename item'), 'error')
|
|
}
|
|
}
|
|
|
|
// Delete operations - for specific item
|
|
function handleDeleteItem(item: CollectionObject | EntityObject) {
|
|
nodesToDelete.value = [item]
|
|
showDeleteDialog.value = true
|
|
}
|
|
|
|
async function handleDelete() {
|
|
const deletedCount = nodesToDelete.value.length
|
|
|
|
try {
|
|
for (const node of nodesToDelete.value) {
|
|
await fileManager.deleteNode(nodeId(node))
|
|
}
|
|
selection.clear()
|
|
showDeleteDialog.value = false
|
|
nodesToDelete.value = []
|
|
notify(
|
|
deletedCount === 1 ? 'Item deleted' : `${deletedCount} items deleted`,
|
|
'success'
|
|
)
|
|
} catch (error) {
|
|
console.error('Failed to delete:', error)
|
|
notify(getErrorMessage(error, 'Failed to delete selected items'), 'error')
|
|
}
|
|
}
|
|
|
|
// Download operation
|
|
function handleDownloadItem(item: CollectionObject | EntityObject) {
|
|
if (item instanceof EntityObject) {
|
|
// Download single file
|
|
fileManager.downloadEntity(nodeId(item), String(item.collection ?? fileManager.ROOT_ID))
|
|
notify(`Download started for "${nodeLabel(item)}"`, 'info')
|
|
} else if (item instanceof CollectionObject) {
|
|
// Download folder as ZIP
|
|
fileManager.downloadCollection(nodeId(item))
|
|
notify(`Archive download started for "${nodeLabel(item)}"`, 'info')
|
|
}
|
|
}
|
|
|
|
// File picker methods
|
|
function openFilePicker() {
|
|
fileInputRef.value?.click()
|
|
}
|
|
|
|
function openFolderPicker() {
|
|
folderInputRef.value?.click()
|
|
}
|
|
|
|
function handleFileSelect(event: Event) {
|
|
const input = event.target as HTMLInputElement
|
|
if (input.files && input.files.length > 0) {
|
|
upload.addFiles(input.files)
|
|
showUploadDialog.value = true
|
|
notify(
|
|
input.files.length === 1 ? '1 file added to uploads' : `${input.files.length} files added to uploads`,
|
|
'info'
|
|
)
|
|
input.value = ''
|
|
}
|
|
}
|
|
|
|
async function handleFolderSelect(event: Event) {
|
|
const input = event.target as HTMLInputElement
|
|
if (input.files && input.files.length > 0) {
|
|
await queueFolderUploads(
|
|
input.files,
|
|
'Preparing folder upload...',
|
|
input.files.length === 1
|
|
? '1 file added from folder upload'
|
|
: `${input.files.length} files added from folder upload`
|
|
)
|
|
input.value = ''
|
|
}
|
|
}
|
|
|
|
// Drag and drop methods
|
|
function handleDragOver(event: DragEvent) {
|
|
event.preventDefault()
|
|
isDragOver.value = true
|
|
}
|
|
|
|
function handleDragLeave(event: DragEvent) {
|
|
event.preventDefault()
|
|
isDragOver.value = false
|
|
}
|
|
|
|
async function handleDrop(event: DragEvent) {
|
|
event.preventDefault()
|
|
isDragOver.value = false
|
|
|
|
if (!event.dataTransfer) return
|
|
|
|
const items = event.dataTransfer.items
|
|
if (items && items.length > 0) {
|
|
const entries: FileSystemEntry[] = []
|
|
for (let i = 0; i < items.length; i++) {
|
|
const item = items[i]
|
|
if (!item) continue
|
|
|
|
const entry = item.webkitGetAsEntry?.()
|
|
if (entry) {
|
|
entries.push(entry)
|
|
}
|
|
}
|
|
|
|
if (entries.length > 0) {
|
|
showUploadDialog.value = true
|
|
beginUploadPreparation('Scanning dropped folder...')
|
|
await nextTick()
|
|
|
|
try {
|
|
const filesWithPaths = await readEntriesRecursively(entries)
|
|
if (filesWithPaths.length > 0) {
|
|
await queueFolderUploads(
|
|
filesWithPaths,
|
|
'Preparing dropped folder upload...',
|
|
filesWithPaths.length === 1 ? '1 file added to uploads' : `${filesWithPaths.length} files added to uploads`
|
|
)
|
|
} else {
|
|
notify('No files found in dropped folder', 'warning')
|
|
finishUploadPreparation()
|
|
}
|
|
} catch (error) {
|
|
finishUploadPreparation()
|
|
console.error('Failed to read dropped folder:', error)
|
|
notify(getErrorMessage(error, 'Failed to read dropped folder'), 'error')
|
|
}
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
|
|
upload.addFiles(event.dataTransfer.files)
|
|
showUploadDialog.value = true
|
|
notify(
|
|
event.dataTransfer.files.length === 1 ? '1 file added to uploads' : `${event.dataTransfer.files.length} files added to uploads`,
|
|
'info'
|
|
)
|
|
}
|
|
}
|
|
|
|
async function readEntriesRecursively(
|
|
entries: FileSystemEntry[],
|
|
basePath: string = ''
|
|
): Promise<{ file: File; relativePath: string }[]> {
|
|
const results: { file: File; relativePath: string }[] = []
|
|
|
|
for (const entry of entries) {
|
|
if (entry.isFile) {
|
|
const fileEntry = entry as FileSystemFileEntry
|
|
const file = await new Promise<File>((resolve, reject) => {
|
|
fileEntry.file(resolve, reject)
|
|
})
|
|
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name
|
|
results.push({ file, relativePath })
|
|
} else if (entry.isDirectory) {
|
|
const dirEntry = entry as FileSystemDirectoryEntry
|
|
const dirReader = dirEntry.createReader()
|
|
const subEntries = await new Promise<FileSystemEntry[]>((resolve, reject) => {
|
|
const allEntries: FileSystemEntry[] = []
|
|
const readBatch = () => {
|
|
dirReader.readEntries((batch) => {
|
|
if (batch.length === 0) {
|
|
resolve(allEntries)
|
|
} else {
|
|
allEntries.push(...batch)
|
|
readBatch()
|
|
}
|
|
}, reject)
|
|
}
|
|
readBatch()
|
|
})
|
|
const subPath = basePath ? `${basePath}/${entry.name}` : entry.name
|
|
const subResults = await readEntriesRecursively(subEntries, subPath)
|
|
results.push(...subResults)
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// Upload methods
|
|
async function handleUploadAll() {
|
|
try {
|
|
await upload.uploadAll()
|
|
await fileManager.refresh()
|
|
notify('Uploads completed', 'success')
|
|
} catch (error) {
|
|
console.error('Failed to upload files:', error)
|
|
notify(getErrorMessage(error, 'Failed to upload files'), 'error')
|
|
}
|
|
}
|
|
|
|
function handleUploadDialogClose() {
|
|
showUploadDialog.value = false
|
|
finishUploadPreparation()
|
|
upload.clearAll()
|
|
}
|
|
|
|
// Refresh
|
|
async function handleRefresh() {
|
|
try {
|
|
await fileManager.refresh()
|
|
} catch (error) {
|
|
console.error('Failed to refresh files:', error)
|
|
notify(getErrorMessage(error, 'Failed to refresh files'), 'error')
|
|
}
|
|
}
|
|
|
|
// Initialize
|
|
onMounted(async () => {
|
|
try {
|
|
await fileManager.initialize()
|
|
await servicesStore.list()
|
|
syncActiveService(availableServices.value)
|
|
await fileManager.refresh()
|
|
console.log('[Files] - Initialized with:', {
|
|
provider: activeProviderId.value,
|
|
service: activeServiceId.value,
|
|
})
|
|
} catch (error) {
|
|
console.error('[Files] - Failed to initialize:', error)
|
|
notify(getErrorMessage(error, 'Failed to initialize file manager'), 'error')
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="!isFileManagerAvailable" class="files-unavailable">
|
|
<v-alert
|
|
type="warning"
|
|
variant="outlined"
|
|
class="files-unavailable-alert"
|
|
>
|
|
<v-icon size="64" color="warning" class="mb-6">mdi-folder-off-outline</v-icon>
|
|
<h2 class="text-h5 font-weight-bold mb-4">File Manager Not Available</h2>
|
|
<p>
|
|
The File Manager module is not installed or enabled.
|
|
This page requires the <strong>documents_manager</strong> module to function properly.
|
|
</p>
|
|
<p class="mb-0 mt-2">
|
|
Please contact your system administrator to install and enable the
|
|
<code>documents_manager</code> module.
|
|
</p>
|
|
</v-alert>
|
|
</div>
|
|
|
|
<div v-else class="files-container">
|
|
<!-- Top Toolbar -->
|
|
<FilesToolbar
|
|
v-model:search-query="searchQuery"
|
|
v-model:view-mode="viewMode"
|
|
:is-loading="fileManager.isLoading.value"
|
|
@toggle-sidebar="sidebarVisible = !sidebarVisible"
|
|
@refresh="handleRefresh"
|
|
@open-file-picker="openFilePicker"
|
|
@open-folder-picker="openFolderPicker"
|
|
@new-folder="showNewFolderDialog = true"
|
|
/>
|
|
|
|
<!-- Main Content -->
|
|
<div
|
|
class="files-content"
|
|
@dragover="handleDragOver"
|
|
@dragleave="handleDragLeave"
|
|
@drop="handleDrop"
|
|
>
|
|
<!-- Drag overlay -->
|
|
<FilesDragOverlay :visible="isDragOver" />
|
|
|
|
<!-- Sidebar -->
|
|
<FilesSidebar
|
|
v-model="sidebarVisible"
|
|
:active-provider-id="activeProviderId"
|
|
:active-service-id="activeServiceId"
|
|
:services="availableServices"
|
|
@navigate-home="fileManager.navigateToRoot()"
|
|
@select-service="handleServiceSelect"
|
|
/>
|
|
|
|
<!-- Main Area -->
|
|
<div class="files-main">
|
|
<div class="files-workspace">
|
|
<div class="files-browser-panel">
|
|
<!-- Breadcrumbs -->
|
|
<FilesBreadcrumbs
|
|
:items="breadcrumbs"
|
|
@navigate="handleBreadcrumbNavigate"
|
|
/>
|
|
|
|
<!-- Loading -->
|
|
<v-progress-linear
|
|
v-if="fileManager.isLoading.value"
|
|
indeterminate
|
|
color="primary"
|
|
class="mx-4"
|
|
/>
|
|
|
|
<div class="files-view-panel">
|
|
<!-- Empty state -->
|
|
<FilesEmptyState v-if="!hasItems && !fileManager.isLoading.value" />
|
|
|
|
<!-- Grid View -->
|
|
<FilesGridView
|
|
v-else-if="viewMode === 'grid'"
|
|
:collections="sortedItems.collections"
|
|
:entities="sortedItems.entities"
|
|
:selected-ids="selectedIds"
|
|
@item-click="handleItemClick"
|
|
@open="handleOpenItem"
|
|
@edit="handleEditItem"
|
|
@rename="handleRenameItem"
|
|
@delete="handleDeleteItem"
|
|
@download="handleDownloadItem"
|
|
@show-details="handleShowDetails"
|
|
/>
|
|
|
|
<!-- List View -->
|
|
<FilesListView
|
|
v-else-if="viewMode === 'list'"
|
|
:collections="sortedItems.collections"
|
|
:entities="sortedItems.entities"
|
|
:selected-ids="selectedIds"
|
|
@item-click="handleItemClick"
|
|
@open="handleOpenItem"
|
|
@edit="handleEditItem"
|
|
@rename="handleRenameItem"
|
|
@delete="handleDeleteItem"
|
|
@download="handleDownloadItem"
|
|
@show-details="handleShowDetails"
|
|
/>
|
|
|
|
<!-- Details View -->
|
|
<FilesDetailsView
|
|
v-else-if="viewMode === 'details'"
|
|
:collections="sortedItems.collections"
|
|
:entities="sortedItems.entities"
|
|
:selected-ids="selectedIds"
|
|
@item-click="handleItemClick"
|
|
@open="handleOpenItem"
|
|
@edit="handleEditItem"
|
|
@rename="handleRenameItem"
|
|
@delete="handleDeleteItem"
|
|
@download="handleDownloadItem"
|
|
@show-details="handleShowDetails"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<FilesInfoPanel
|
|
v-if="hasSelection && !isMobile"
|
|
embedded
|
|
:selected-items="selection.selectedNodeArray.value"
|
|
@close="selection.clear()"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<FilesInfoPanel
|
|
v-if="isMobile"
|
|
:selected-items="selection.selectedNodeArray.value"
|
|
@close="selection.clear()"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Hidden file inputs -->
|
|
<input
|
|
ref="fileInputRef"
|
|
type="file"
|
|
multiple
|
|
style="display: none;"
|
|
@change="handleFileSelect"
|
|
/>
|
|
<input
|
|
ref="folderInputRef"
|
|
type="file"
|
|
webkitdirectory
|
|
style="display: none;"
|
|
@change="handleFolderSelect"
|
|
/>
|
|
|
|
<!-- Dialogs -->
|
|
<NewFolderDialog
|
|
v-model="showNewFolderDialog"
|
|
@create="handleCreateFolder"
|
|
/>
|
|
|
|
<RenameDialog
|
|
v-model="showRenameDialog"
|
|
:current-name="renameCurrentName(nodeToRename)"
|
|
@rename="handleRename"
|
|
/>
|
|
|
|
<DeleteConfirmDialog
|
|
v-model="showDeleteDialog"
|
|
:item-count="nodesToDelete.length"
|
|
@confirm="handleDelete"
|
|
/>
|
|
|
|
<UploadDialog
|
|
v-model="showUploadDialog"
|
|
:uploads="upload.uploads.value"
|
|
:total-progress="upload.totalProgress.value"
|
|
:is-uploading="upload.isUploading.value"
|
|
:is-preparing="isPreparingUploads"
|
|
:preparing-message="uploadPreparationMessage"
|
|
:preparing-processed-count="uploadPreparationProcessedCount"
|
|
:preparing-total-count="uploadPreparationTotalCount"
|
|
:pending-count="upload.pendingUploads.value.length"
|
|
:completed-count="upload.completedUploads.value.length"
|
|
@upload-all="handleUploadAll"
|
|
@remove-upload="upload.removeUpload"
|
|
@retry-upload="upload.retryUpload"
|
|
@add-files="openFilePicker"
|
|
@close="handleUploadDialogClose"
|
|
/>
|
|
|
|
<!-- File Viewer -->
|
|
<FileViewerDialog
|
|
v-model="showViewer"
|
|
:entity="viewerEntity"
|
|
:all-entities="sortedItems.entities"
|
|
:get-url="fileManager.getEntityUrl"
|
|
:download-entity="fileManager.downloadEntity"
|
|
@navigate="handleViewerNavigate"
|
|
/>
|
|
|
|
<!-- File Editor -->
|
|
<FileEditorDialog
|
|
v-model="showEditor"
|
|
:entity="editorEntity"
|
|
:read-file="fileManager.readFile"
|
|
:write-file="fileManager.writeFile"
|
|
/>
|
|
|
|
<v-snackbar
|
|
v-model="snackbarVisible"
|
|
:color="snackbarColor"
|
|
:timeout="3000"
|
|
location="bottom right"
|
|
>
|
|
{{ snackbarMessage }}
|
|
<template #actions>
|
|
<v-btn variant="text" @click="snackbarVisible = false">Close</v-btn>
|
|
</template>
|
|
</v-snackbar>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.files-unavailable {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100vh;
|
|
padding: 48px;
|
|
text-align: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.files-unavailable-alert {
|
|
width: 100%;
|
|
text-align: left;
|
|
}
|
|
|
|
.files-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
isolation: isolate;
|
|
}
|
|
|
|
.files-content {
|
|
display: flex;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.files-main {
|
|
flex: 1;
|
|
display: flex;
|
|
overflow: hidden;
|
|
min-width: 0;
|
|
}
|
|
|
|
.files-workspace {
|
|
flex: 1;
|
|
display: flex;
|
|
overflow: hidden;
|
|
min-width: 0;
|
|
}
|
|
|
|
.files-browser-panel {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
min-width: 0;
|
|
}
|
|
|
|
.files-view-panel {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.files-workspace {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
</style>
|