diff --git a/static/css/room.css b/static/css/room.css new file mode 100644 index 0000000..dd9a641 --- /dev/null +++ b/static/css/room.css @@ -0,0 +1,310 @@ +.file-name-ellipsis { + max-width: 140px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + vertical-align: bottom; +} + +@media (min-width: 992px) { + .file-name-ellipsis { + max-width: 180px; + } +} + +.file-action-btn { + min-width: 32px; + min-height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 5px; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s; +} + +.card:hover>.card-footer .file-action-btn { + opacity: 1; + pointer-events: auto; +} + +.card-footer.bg-white.border-0.d-flex.justify-content-center.gap-2 { + min-height: 40px; +} + +.card.file-card { + cursor: pointer; +} + +.card.file-card .select-item-checkbox, +.card.file-card .file-action-btn { + cursor: pointer; +} + +#upBtn.hidden { + display: none !important; +} + +#fileGrid.list-view { + display: block; + padding: 0; + width: 100%; +} + +#fileGrid.list-view table { + width: 100%; + border-collapse: collapse; + background: var(--white); + margin: 0; +} + +#fileGrid.list-view th, +#fileGrid.list-view td { + padding: 0.5rem 1rem; + border-bottom: 1px solid var(--border-light); + text-align: left; + font-size: 0.95rem; + vertical-align: middle; +} + +#fileGrid.list-view th { + background: var(--bg-color); + color: var(--text-muted); + font-weight: 500; + position: sticky; + top: 0; + z-index: 1; +} + +#fileGrid.list-view tr:hover td { + background-color: #edf4f5; + transition: background 0.15s; +} + +#fileGrid.list-view .file-icon { + width: 40px; + text-align: center; +} + +#fileGrid.list-view .file-actions { + min-width: 90px; + text-align: right; +} + +#fileGrid.list-view .file-action-btn { + opacity: 1; + pointer-events: auto; + min-width: 28px; + min-height: 28px; + font-size: 0.875rem; + margin-left: 0.25rem; +} + +.list-view-header { + display: grid; + grid-template-columns: 40px 2fr 1fr 1fr 1fr; + padding: 0.5rem 1rem; + background-color: var(--bg-color); + border-bottom: 1px solid var(--border-light); + color: var(--text-muted); + font-size: 0.875rem; + font-weight: 500; + margin-bottom: 0.5rem; +} + +.btn-group.btn-group-sm .btn { + background-color: var(--white); + border-color: var(--border-light); + color: var(--text-muted); + transition: background-color 0.15s, color 0.15s; +} + +.btn-group.btn-group-sm .btn.active, +.btn-group.btn-group-sm .btn:active { + background-color: var(--primary-bg-light) !important; + color: var(--primary-color) !important; + border-color: var(--primary-color) !important; + box-shadow: none; +} + +.btn-group.btn-group-sm .btn:focus { + box-shadow: 0 0 0 0.1rem var(--primary-opacity-20); +} + +.btn-group.btn-group-sm .btn:hover:not(.active) { + background-color: var(--bg-color); + color: var(--primary-color); +} + +#fileGrid.table-mode { + padding: 0; +} + +#fileGrid.table-mode table { + width: 100%; + border-collapse: collapse; + background: var(--white); +} + +#fileGrid.table-mode th, +#fileGrid.table-mode td { + padding: 0.5rem 1rem; + border-bottom: 1px solid var(--border-light); + text-align: left; + font-size: 0.95rem; + vertical-align: middle; +} + +#fileGrid.table-mode th { + background: var(--bg-color); + color: var(--text-muted); + font-weight: 500; +} + +#fileGrid.table-mode tr:hover td { + background-color: var(--primary-opacity-8); + transition: background 0.15s; +} + +#fileGrid.table-mode .file-icon { + width: 40px; + text-align: center; +} + +#fileGrid.table-mode .file-actions { + min-width: 90px; + text-align: right; +} + +#fileGrid.table-mode .file-action-btn { + opacity: 1; + pointer-events: auto; + min-width: 28px; + min-height: 28px; + font-size: 0.875rem; + margin-left: 0.25rem; +} + +#fileGrid.table-mode tr.selected { + background-color: var(--primary-bg-light) !important; +} + +/* Disable text selection for file grid and table rows/cards */ +#fileGrid, +#fileGrid * { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +#fileGrid .card.file-card { + cursor: pointer; +} + +#fileGrid.list-view tr { + cursor: pointer; +} + +#fileGrid.table-mode tr { + cursor: pointer; +} + +#fileGrid.table-mode tr.selected { + background-color: #e6f3f4 !important; +} + +.details-sidebar { + background: #f8fafc; + border-left: 1.5px solid #e6f3f4; + box-shadow: -2px 0 8px rgba(0, 0, 0, 0.08); + font-size: 0.97rem; + width: 320px; + height: 100%; + right: 0; + top: 0; + left: auto; + border-radius: 0; + position: fixed; + z-index: 1050; + padding: 1.3rem 1.2rem 1.2rem 1.2rem; + overflow-y: auto; +} + +.sidebar-ellipsis { + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; +} + +.sidebar-path-wrap { + max-width: 220px; + overflow-x: auto; + white-space: nowrap; + text-overflow: ellipsis; + display: block; + font-size: 0.91rem; +} + +.details-sidebar h5, +.details-sidebar .fw-bold { + font-size: 1.05rem; + color: #16767b; +} + +.details-sidebar strong { + color: #16767b; + font-weight: 500; +} + +.details-sidebar .rounded-circle { + border: 1.5px solid #e6f3f4; +} + +.details-sidebar hr { + margin: 0.7rem 0 0.5rem 0; + border-color: #e6f3f4; +} + +.sidebar-meta-grid { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 0.2rem 0.7rem; + font-size: 0.91rem; + color: #555; + margin-bottom: 0.2rem; +} + +.sidebar-meta-grid div:first-child { + color: #16767b; + font-weight: 500; +} + +@media (max-width: 900px) { + .details-sidebar { + width: 100vw !important; + right: 0; + left: 0; + border-radius: 0; + top: 0; + transform: none; + max-width: 100vw; + max-height: 100vh; + padding: 1.1rem 0.7rem 0.7rem 0.7rem; + } + + .sidebar-ellipsis, + .sidebar-path-wrap { + max-width: 90vw; + } +} + +.progress-bar { + background-color: #16767b !important; + color: #fff !important; + transition: background-color 0.2s; +} \ No newline at end of file diff --git a/static/js/file-grid.js b/static/js/file-grid.js index f76cbe5..5d1fcfb 100644 --- a/static/js/file-grid.js +++ b/static/js/file-grid.js @@ -102,6 +102,31 @@ function sortFiles(column) { renderFiles(currentFiles); } +function getFileIcon(filename) { + const extension = filename.split('.').pop().toLowerCase(); + + const iconMap = { + pdf: 'fa-file-pdf', + doc: 'fa-file-word', + docx: 'fa-file-word', + xls: 'fa-file-excel', + xlsx: 'fa-file-excel', + ppt: 'fa-file-powerpoint', + pptx: 'fa-file-powerpoint', + txt: 'fa-file-alt', + jpg: 'fa-file-image', + jpeg: 'fa-file-image', + png: 'fa-file-image', + gif: 'fa-file-image', + zip: 'fa-file-archive', + rar: 'fa-file-archive', + mp3: 'fa-file-audio', + mp4: 'fa-file-video' + }; + + return iconMap[extension] || 'fa-file'; +} + function renderFiles(files) { if (!files) return; currentFiles = files; @@ -127,7 +152,7 @@ function renderFiles(files) { files.forEach((file, idx) => { let icon = file.type === 'folder' ? `` - : ``; + : ``; let size = file.size !== '-' ? (file.size > 0 ? (file.size < 1024*1024 ? (file.size/1024).toFixed(1)+' KB' : (file.size/1024/1024).toFixed(2)+' MB') : '0 KB') : '-'; let actionsArr = []; let dblClickAction = ''; @@ -162,7 +187,7 @@ function renderFiles(files) { files.forEach((file, idx) => { let icon = file.type === 'folder' ? `` - : ``; + : ``; let size = file.size !== '-' ? (file.size > 0 ? (file.size < 1024*1024 ? (file.size/1024).toFixed(1)+' KB' : (file.size/1024/1024).toFixed(2)+' MB') : '0 KB') : '-'; let actionsArr = []; let dblClickAction = ''; diff --git a/static/js/rooms/fileManager.js b/static/js/rooms/fileManager.js new file mode 100644 index 0000000..95fc7ce --- /dev/null +++ b/static/js/rooms/fileManager.js @@ -0,0 +1,342 @@ +export class FileManager { + constructor(roomManager) { + console.log('[FileManager] Initializing...'); + this.roomManager = roomManager; + this.currentFiles = []; + this.selectedItems = new Set(); + this.lastSelectedIndex = -1; + this.batchDeleteItems = null; + this.fileToDelete = null; + this.fileToDeletePath = ''; + console.log('[FileManager] Initialized with roomManager:', roomManager); + } + + async fetchFiles() { + console.log('[FileManager] Fetching files...'); + try { + const url = `/api/rooms/${this.roomManager.roomId}/files${this.roomManager.currentPath ? `?path=${encodeURIComponent(this.roomManager.currentPath)}` : ''}`; + console.log('[FileManager] Fetching from URL:', url); + + const response = await fetch(url); + console.log('[FileManager] Response status:', response.status); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log('[FileManager] Received data:', data); + + this.currentFiles = data.sort((a, b) => a.name.localeCompare(b.name)); + console.log('[FileManager] Sorted files:', this.currentFiles); + + // Update the view + await this.roomManager.viewManager.renderFiles(this.currentFiles); + console.log('[FileManager] Files rendered in view'); + + return this.currentFiles; + } catch (error) { + console.error('[FileManager] Error fetching files:', error); + document.getElementById('fileError').textContent = 'Failed to load files. Please try again.'; + throw error; + } + } + + async deleteFile(fileId) { + console.log('[FileManager] Deleting file:', fileId); + try { + const response = await fetch(`/api/rooms/${this.roomManager.roomId}/files/${fileId}`, { + method: 'DELETE', + headers: { + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + } + }); + console.log('[FileManager] Delete response status:', response.status); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + console.log('[FileManager] Delete result:', result); + + if (result.success) { + this.currentFiles = this.currentFiles.filter(file => file.id !== fileId); + await this.roomManager.viewManager.renderFiles(this.currentFiles); + console.log('[FileManager] File deleted and view updated'); + return { success: true, message: 'File moved to trash' }; + } else { + throw new Error(result.message || 'Failed to delete file'); + } + } catch (error) { + console.error('[FileManager] Error deleting file:', error); + return { success: false, message: error.message }; + } + } + + async renameFile(fileId, newName) { + console.log('[FileManager] Renaming file:', { fileId, newName }); + try { + const response = await fetch(`/api/rooms/${this.roomManager.roomId}/files/${fileId}/rename`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ new_name: newName }) + }); + console.log('[FileManager] Rename response status:', response.status); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + console.log('[FileManager] Rename result:', result); + + if (result.success) { + const fileIndex = this.currentFiles.findIndex(file => file.id === fileId); + if (fileIndex !== -1) { + this.currentFiles[fileIndex].name = newName; + await this.roomManager.viewManager.renderFiles(this.currentFiles); + console.log('[FileManager] File renamed and view updated'); + } + return { success: true, message: 'File renamed successfully' }; + } else { + throw new Error(result.message || 'Failed to rename file'); + } + } catch (error) { + console.error('[FileManager] Error renaming file:', error); + return { success: false, message: error.message }; + } + } + + async moveFile(fileId, targetPath) { + console.log('[FileManager] Moving file:', { fileId, targetPath }); + try { + const response = await fetch(`/api/rooms/${this.roomManager.roomId}/files/${fileId}/move`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ target_path: targetPath }) + }); + console.log('[FileManager] Move response status:', response.status); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + console.log('[FileManager] Move result:', result); + + if (result.success) { + this.currentFiles = this.currentFiles.filter(file => file.id !== fileId); + await this.roomManager.viewManager.renderFiles(this.currentFiles); + console.log('[FileManager] File moved and view updated'); + return { success: true, message: 'File moved successfully' }; + } else { + throw new Error(result.message || 'Failed to move file'); + } + } catch (error) { + console.error('[FileManager] Error moving file:', error); + return { success: false, message: error.message }; + } + } + + async toggleStar(filename, path) { + console.log('[FileManager] Toggling star for:', filename, 'path:', path); + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + + // Find and update the button immediately for better UX + const starButton = event.target.closest('.file-action-btn'); + if (starButton) { + // Get the current state from the button's title + const isStarred = starButton.title === 'Unstar'; + + // Update button appearance using CSS variables + starButton.style.backgroundColor = isStarred ? 'var(--primary-opacity-8)' : 'var(--warning-opacity-15)'; + starButton.style.color = isStarred ? 'var(--primary-color)' : 'var(--warning-color)'; + starButton.title = isStarred ? 'Star' : 'Unstar'; + } + + try { + const response = await fetch(`/api/rooms/${this.roomManager.roomId}/star`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify({ + filename: filename, + path: path || '' + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log('[FileManager] Star toggle response:', data); + + if (data.success) { + // Update the file's starred status in currentFiles + const fileIndex = this.currentFiles.findIndex(f => f.name === filename && f.path === path); + if (fileIndex !== -1) { + this.currentFiles[fileIndex].starred = data.starred; + } + } else { + // Revert the button if the server request failed + if (starButton) { + const isStarred = starButton.title === 'Unstar'; + starButton.style.backgroundColor = isStarred ? 'var(--primary-opacity-8)' : 'var(--warning-opacity-15)'; + starButton.style.color = isStarred ? 'var(--primary-color)' : 'var(--warning-color)'; + starButton.title = isStarred ? 'Star' : 'Unstar'; + } + throw new Error(data.error || 'Failed to toggle star'); + } + } catch (error) { + // Revert the button if there was an error + if (starButton) { + const isStarred = starButton.title === 'Unstar'; + starButton.style.backgroundColor = isStarred ? 'var(--primary-opacity-8)' : 'var(--warning-opacity-15)'; + starButton.style.color = isStarred ? 'var(--primary-color)' : 'var(--warning-color)'; + starButton.title = isStarred ? 'Star' : 'Unstar'; + } + console.error('[FileManager] Error toggling star:', error); + throw error; + } + } + + downloadFile(fileId) { + console.log('[FileManager] Downloading file:', fileId); + const url = `/api/rooms/${this.roomManager.roomId}/files/${fileId}/download`; + console.log('[FileManager] Download URL:', url); + window.location.href = url; + } + + async downloadSelected() { + console.log('[FileManager] Downloading selected files...'); + const selectedItems = this.getSelectedItems(); + console.log('[FileManager] Selected items:', selectedItems); + + if (selectedItems.length === 0) { + console.log('[FileManager] No files selected for download'); + return; + } + + try { + const response = await fetch(`/api/rooms/${this.roomManager.roomId}/files/download`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ file_ids: selectedItems.map(item => item.id) }) + }); + console.log('[FileManager] Download response status:', response.status); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const blob = await response.blob(); + console.log('[FileManager] Received blob:', blob); + + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'files.zip'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + console.log('[FileManager] Download initiated'); + } catch (error) { + console.error('[FileManager] Error downloading files:', error); + throw error; + } + } + + async deleteFileConfirmed() { + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + if (this.batchDeleteItems && this.batchDeleteItems.length) { + // Batch delete + let completed = 0; + const deleteNext = async () => { + if (completed >= this.batchDeleteItems.length) { + await this.fetchFiles(); + this.batchDeleteItems = null; + this.fileToDelete = null; + this.fileToDeletePath = ''; + this.roomManager.modalManager.deleteModal.hide(); + document.getElementById('fileError').textContent = ''; + return; + } + const item = this.batchDeleteItems[completed]; + let url = `/api/rooms/${this.roomManager.roomId}/files/${encodeURIComponent(item.name)}`; + if (item.path) url += `?path=${encodeURIComponent(item.path)}`; + try { + const response = await fetch(url, { + method: 'DELETE', + headers: { 'X-CSRFToken': csrfToken } + }); + const result = await response.json(); + if (!result.success) { + throw new Error(result.error || 'Delete failed'); + } + } catch (error) { + console.error('[FileManager] Error deleting file:', error); + } + completed++; + await deleteNext(); + }; + await deleteNext(); + return; + } + + if (!this.fileToDelete) return; + let url = `/api/rooms/${this.roomManager.roomId}/files/${encodeURIComponent(this.fileToDelete)}`; + if (this.fileToDeletePath) url += `?path=${encodeURIComponent(this.fileToDeletePath)}`; + + try { + const response = await fetch(url, { + method: 'DELETE', + headers: { 'X-CSRFToken': csrfToken } + }); + const result = await response.json(); + if (result.success) { + await this.fetchFiles(); + this.fileToDelete = null; + this.fileToDeletePath = ''; + this.roomManager.modalManager.deleteModal.hide(); + document.getElementById('fileError').textContent = ''; + } else { + document.getElementById('fileError').textContent = result.error || 'Delete failed.'; + } + } catch (error) { + console.error('[FileManager] Error deleting file:', error); + document.getElementById('fileError').textContent = 'Delete failed.'; + } + } + + getSelectedItems() { + console.log('[FileManager] Getting selected items'); + return Array.from(this.selectedItems).map(index => this.currentFiles[index]); + } + + updateSelection(index, event) { + console.log('[FileManager] Updating selection:', { index, event }); + // Implementation of selection logic + } + + navigateToParent() { + if (!this.roomManager.currentPath) return; + const parts = this.roomManager.currentPath.split('/'); + parts.pop(); // Remove the last part + const parentPath = parts.join('/'); + this.roomManager.navigateTo(parentPath); + } +} \ No newline at end of file diff --git a/static/js/rooms/modalManager.js b/static/js/rooms/modalManager.js new file mode 100644 index 0000000..3902aad --- /dev/null +++ b/static/js/rooms/modalManager.js @@ -0,0 +1,238 @@ +export class ModalManager { + constructor(roomManager) { + this.roomManager = roomManager; + + // Initialize modals + this.deleteModal = new bootstrap.Modal(document.getElementById('deleteConfirmModal')); + this.newFolderModal = new bootstrap.Modal(document.getElementById('newFolderModal')); + this.renameModal = new bootstrap.Modal(document.getElementById('renameModal')); + this.detailsModal = new bootstrap.Modal(document.getElementById('detailsModal')); + this.overwriteModal = new bootstrap.Modal(document.getElementById('overwriteConfirmModal'), { + backdrop: 'static', + keyboard: false + }); + this.moveModal = new bootstrap.Modal(document.getElementById('moveModal')); + + this.initializeModals(); + } + + initializeModals() { + // Initialize delete modal + if (this.roomManager.canDelete) { + document.getElementById('confirmDeleteBtn').addEventListener('click', () => { + this.roomManager.fileManager.deleteFileConfirmed(); + }); + } + + // Initialize new folder modal + if (this.roomManager.canUpload) { + document.getElementById('newFolderBtn').addEventListener('click', () => { + document.getElementById('folderNameInput').value = ''; + document.getElementById('folderError').textContent = ''; + this.newFolderModal.show(); + setTimeout(() => { + document.getElementById('folderNameInput').focus(); + }, 100); + }); + + document.getElementById('createFolderBtn').addEventListener('click', () => { + this.createFolder(); + }); + } + + // Initialize rename modal + if (this.roomManager.canRename) { + document.getElementById('confirmRenameBtn').addEventListener('click', () => { + this.renameFile(); + }); + } + + // Initialize move modal + if (this.roomManager.canMove) { + document.getElementById('confirmMoveBtn').addEventListener('click', () => { + this.roomManager.fileManager.moveFileConfirmed(); + }); + } + } + + showDeleteModal(filename, path = '') { + const fileNameEl = document.getElementById('deleteFileName'); + const labelEl = document.getElementById('deleteConfirmLabel'); + + if (fileNameEl) fileNameEl.textContent = filename; + if (labelEl) labelEl.textContent = 'Move to Trash'; + + this.deleteModal.show(); + } + + showRenameModal(filename) { + document.getElementById('renameError').textContent = ''; + const ext = filename.includes('.') ? filename.substring(filename.lastIndexOf('.')) : ''; + const isFile = ext && filename.lastIndexOf('.') > 0; + const inputGroup = document.getElementById('renameInputGroup'); + + if (isFile) { + const base = filename.substring(0, filename.lastIndexOf('.')); + inputGroup.innerHTML = `
${ext}
`; + } else { + inputGroup.innerHTML = ``; + } + + this.renameModal.show(); + setTimeout(() => { + document.getElementById('renameInput').focus(); + }, 100); + } + + showDetailsModal(file) { + const icon = file.type === 'folder' + ? `` + : ``; + + const uploaderPic = file.uploader_profile_pic + ? `/uploads/profile_pics/${file.uploader_profile_pic}` + : '/static/default-avatar.png'; + + const detailsHtml = ` +
+
${icon}
+
+
${file.name}
+
${file.type === 'folder' ? 'Folder' : 'File'}
+
+
+
+ Profile Picture + ${file.uploaded_by || '-'} +
+
${this.roomManager.viewManager.formatDate(file.modified)}
+
+
Room:
+
Path: ${(file.path ? file.path + '/' : '') + file.name}
+
Size: ${this.roomManager.viewManager.formatFileSize(file.size)}
+
Uploaded at: ${file.uploaded_at ? new Date(file.uploaded_at).toLocaleString() : '-'}
+ `; + + document.getElementById('detailsModalBody').innerHTML = detailsHtml; + this.detailsModal.show(); + } + + showOverwriteModal(filename) { + return new Promise((resolve) => { + const fileNameEl = document.getElementById('overwriteFileName'); + if (fileNameEl) fileNameEl.textContent = filename; + + const skipBtn = document.getElementById('skipOverwriteBtn'); + const skipAllBtn = document.getElementById('skipAllOverwriteBtn'); + const overwriteBtn = document.getElementById('confirmOverwriteBtn'); + const overwriteAllBtn = document.getElementById('confirmAllOverwriteBtn'); + + const handleResult = (result) => { + this.overwriteModal.hide(); + resolve(result); + }; + + skipBtn.onclick = () => handleResult('skip'); + skipAllBtn.onclick = () => handleResult('skip_all'); + overwriteBtn.onclick = () => handleResult('overwrite'); + overwriteAllBtn.onclick = () => handleResult('overwrite_all'); + + this.overwriteModal.show(); + }); + } + + showMoveModal(filename, path) { + document.getElementById('moveError').textContent = ''; + + fetch(`/api/rooms/${this.roomManager.roomId}/folders`) + .then(r => r.json()) + .then(folders => { + const select = document.getElementById('moveTargetFolder'); + select.innerHTML = ''; + + folders.sort((a, b) => { + if (!a) return -1; + if (!b) return 1; + return a.localeCompare(b); + }); + + folders.forEach(folderPath => { + if (folderPath === path || (path && folderPath.startsWith(path + '/'))) { + return; + } + + const pathParts = folderPath.split('/'); + const displayName = pathParts[pathParts.length - 1]; + const indent = '    '.repeat(pathParts.length - 1); + + select.innerHTML += ``; + }); + + this.moveModal.show(); + }) + .catch(error => { + document.getElementById('moveError').textContent = 'Failed to load folders.'; + console.error('Error loading folders:', error); + }); + } + + async createFolder() { + const folderName = document.getElementById('folderNameInput').value.trim(); + if (!folderName) { + document.getElementById('folderError').textContent = 'Folder name is required.'; + return; + } + + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + try { + const response = await fetch(`/api/rooms/${this.roomManager.roomId}/folders`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify({ + name: folderName, + path: this.roomManager.currentPath + }) + }); + + const result = await response.json(); + if (result.success) { + this.newFolderModal.hide(); + await this.roomManager.fileManager.fetchFiles(); + } else { + if (result.error === 'A folder with this name exists in the trash') { + document.getElementById('folderError').textContent = `Cannot create folder "${folderName}" because this name is currently in the trash. Please restore or permanently delete the trashed item first.`; + } else if (result.error === 'A folder with this name already exists in this location') { + document.getElementById('folderError').textContent = `A folder named "${folderName}" already exists in this location. Please choose a different name.`; + } else { + document.getElementById('folderError').textContent = result.error || 'Failed to create folder.'; + } + } + } catch (error) { + document.getElementById('folderError').textContent = 'Failed to create folder.'; + } + } + + async renameFile() { + const newName = document.getElementById('renameInput').value.trim(); + if (!newName) { + document.getElementById('renameError').textContent = 'New name is required.'; + return; + } + + const result = await this.roomManager.fileManager.renameFile( + this.renameTarget, + newName, + this.roomManager.currentPath + ); + + if (result.success) { + this.renameModal.hide(); + await this.roomManager.fileManager.fetchFiles(); + } else { + document.getElementById('renameError').textContent = result.error || 'Rename failed.'; + } + } +} \ No newline at end of file diff --git a/static/js/rooms/room.js b/static/js/rooms/room.js new file mode 100644 index 0000000..27410e7 --- /dev/null +++ b/static/js/rooms/room.js @@ -0,0 +1,131 @@ +console.log('[RoomManager] Script loaded'); + +// Main room.js file - Coordinates all room functionality +import { FileManager } from './fileManager.js'; +import { ViewManager } from './viewManager.js'; +import { UploadManager } from './uploadManager.js'; +import { SearchManager } from './searchManager.js'; +import { ModalManager } from './modalManager.js'; + +console.log('[RoomManager] All modules imported successfully'); + +class RoomManager { + constructor(config) { + console.log('[RoomManager] Initializing with config:', config); + this.roomId = config.roomId; + this.canDelete = config.canDelete; + this.canShare = config.canShare; + this.canUpload = config.canUpload; + this.canDownload = config.canDownload; + this.canRename = config.canRename; + this.canMove = config.canMove; + + console.log('[RoomManager] Creating manager instances...'); + // Initialize managers + this.fileManager = new FileManager(this); + this.viewManager = new ViewManager(this); + this.uploadManager = new UploadManager(this); + this.searchManager = new SearchManager(this); + this.modalManager = new ModalManager(this); + + console.log('[RoomManager] All managers initialized'); + // Initialize the room + this.initialize(); + } + + async initialize() { + console.log('[RoomManager] Starting initialization...'); + // Get current path from URL + this.currentPath = this.getPathFromUrl(); + console.log('[RoomManager] Current path:', this.currentPath); + + try { + console.log('[RoomManager] Initializing view...'); + // Initialize view and fetch files + await this.viewManager.initializeView(); + console.log('[RoomManager] View initialized'); + + console.log('[RoomManager] Fetching files...'); + await this.fileManager.fetchFiles(); + console.log('[RoomManager] Files fetched'); + + console.log('[RoomManager] Initializing search...'); + // Initialize search + this.searchManager.initialize(); + console.log('[RoomManager] Search initialized'); + + console.log('[RoomManager] Setting up event listeners...'); + // Initialize event listeners + this.initializeEventListeners(); + console.log('[RoomManager] Event listeners initialized'); + + console.log('[RoomManager] Initialization complete'); + } catch (error) { + console.error('[RoomManager] Error during initialization:', error); + } + } + + getPathFromUrl() { + const urlParams = new URLSearchParams(window.location.search); + const path = urlParams.get('path') || ''; + console.log('[RoomManager] Getting path from URL:', path); + return path; + } + + initializeEventListeners() { + console.log('[RoomManager] Setting up event listeners'); + // Add any global event listeners here + } +} + +// Initialize the room manager when the script loads +function initializeRoom() { + console.log('[RoomManager] Starting room initialization...'); + + try { + // Wait for all meta tags to be available + const requiredMetaTags = [ + 'room-id', + 'can-delete', + 'can-share', + 'can-upload', + 'can-download', + 'can-rename', + 'can-move' + ]; + + console.log('[RoomManager] Checking required meta tags...'); + const missingTags = requiredMetaTags.filter(tag => !document.querySelector(`meta[name="${tag}"]`)); + + if (missingTags.length > 0) { + console.error('[RoomManager] Missing required meta tags:', missingTags); + return; + } + + console.log('[RoomManager] All meta tags found, creating config...'); + const config = { + roomId: document.querySelector('meta[name="room-id"]').getAttribute('content'), + canDelete: document.querySelector('meta[name="can-delete"]').getAttribute('content') === 'true', + canShare: document.querySelector('meta[name="can-share"]').getAttribute('content') === 'true', + canUpload: document.querySelector('meta[name="can-upload"]').getAttribute('content') === 'true', + canDownload: document.querySelector('meta[name="can-download"]').getAttribute('content') === 'true', + canRename: document.querySelector('meta[name="can-rename"]').getAttribute('content') === 'true', + canMove: document.querySelector('meta[name="can-move"]').getAttribute('content') === 'true' + }; + + console.log('[RoomManager] Config created:', config); + window.roomManager = new RoomManager(config); + } catch (error) { + console.error('[RoomManager] Error during initialization:', error); + } +} + +// Wait for DOM to be fully loaded +if (document.readyState === 'loading') { + console.log('[RoomManager] DOM still loading, waiting for DOMContentLoaded...'); + document.addEventListener('DOMContentLoaded', initializeRoom); +} else { + console.log('[RoomManager] DOM already loaded, initializing with delay...'); + // If DOM is already loaded, wait a bit to ensure all scripts are loaded + setTimeout(initializeRoom, 0); +} \ No newline at end of file diff --git a/static/js/rooms/searchManager.js b/static/js/rooms/searchManager.js new file mode 100644 index 0000000..2872f69 --- /dev/null +++ b/static/js/rooms/searchManager.js @@ -0,0 +1,61 @@ +export class SearchManager { + constructor(roomManager) { + this.roomManager = roomManager; + this.searchInput = document.getElementById('quickSearchInput'); + this.clearSearchBtn = document.getElementById('clearSearchBtn'); + } + + initialize() { + if (!this.searchInput) return; + + // Create debounced search function + const debouncedSearch = this.debounce((searchTerm) => { + if (searchTerm) { + this.performSearch(searchTerm); + this.clearSearchBtn.style.display = 'block'; + } else { + this.roomManager.fileManager.fetchFiles(); // Reset to show all files + this.clearSearchBtn.style.display = 'none'; + } + }, 300); + + // Add input event listener + this.searchInput.addEventListener('input', (e) => { + const searchTerm = e.target.value.trim(); + debouncedSearch(searchTerm); + }); + + // Add clear search button functionality + if (this.clearSearchBtn) { + this.clearSearchBtn.addEventListener('click', () => { + this.searchInput.value = ''; + this.roomManager.fileManager.fetchFiles(); // Reset to show all files + this.clearSearchBtn.style.display = 'none'; + }); + } + } + + performSearch(searchTerm) { + if (!this.roomManager.fileManager.currentFiles) return; + + const filteredFiles = this.roomManager.fileManager.currentFiles.filter(file => { + const searchLower = searchTerm.toLowerCase(); + return file.name.toLowerCase().includes(searchLower) || + (file.type && file.type.toLowerCase().includes(searchLower)); + }); + + this.roomManager.viewManager.renderFiles(filteredFiles); + } + + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } +} \ No newline at end of file diff --git a/static/js/rooms/uploadManager.js b/static/js/rooms/uploadManager.js new file mode 100644 index 0000000..1b42cbf --- /dev/null +++ b/static/js/rooms/uploadManager.js @@ -0,0 +1,262 @@ +export class UploadManager { + constructor(roomManager) { + this.roomManager = roomManager; + this.pendingUploads = []; + this.pendingUploadIdx = 0; + this.overwriteAll = false; + this.skipAll = false; + + // Initialize upload-related elements + this.uploadBtn = document.getElementById('uploadBtn'); + this.fileInput = document.getElementById('fileInput'); + this.uploadForm = document.getElementById('uploadForm'); + this.fileGrid = document.getElementById('fileGrid'); + this.dropZoneOverlay = document.getElementById('dropZoneOverlay'); + this.uploadProgressContainer = document.getElementById('uploadProgressContainer'); + this.uploadProgressBar = document.getElementById('uploadProgressBar'); + this.uploadProgressText = document.getElementById('uploadProgressText'); + + this.initializeUploadHandlers(); + } + + initializeUploadHandlers() { + if (!this.roomManager.canUpload) return; + + // Initialize drag and drop + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + this.fileGrid.addEventListener(eventName, this.preventDefaults.bind(this), false); + document.body.addEventListener(eventName, this.preventDefaults.bind(this), false); + }); + + ['dragenter', 'dragover'].forEach(eventName => { + this.fileGrid.addEventListener(eventName, this.highlight.bind(this), false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + this.fileGrid.addEventListener(eventName, this.unhighlight.bind(this), false); + }); + + // Handle dropped files + this.fileGrid.addEventListener('drop', this.handleDrop.bind(this), false); + + // Handle file input change + this.uploadBtn.addEventListener('click', () => this.fileInput.click()); + this.fileInput.addEventListener('change', this.handleFileSelect.bind(this)); + } + + preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + highlight() { + this.dropZoneOverlay.style.display = 'block'; + } + + unhighlight() { + this.dropZoneOverlay.style.display = 'none'; + } + + async handleDrop(e) { + const dt = e.dataTransfer; + const files = dt.files; + + if (files.length > 0) { + await this.startUpload(Array.from(files)); + } + } + + async handleFileSelect() { + if (!this.fileInput.files.length) return; + await this.startUpload(Array.from(this.fileInput.files)); + } + + async startUpload(files) { + this.uploadProgressContainer.style.display = 'block'; + this.uploadProgressBar.style.width = '0%'; + this.uploadProgressBar.classList.remove('bg-success'); + this.uploadProgressBar.classList.add('bg-info'); + this.uploadProgressText.textContent = ''; + + this.pendingUploads = files; + this.pendingUploadIdx = 0; + this.overwriteAll = false; + this.skipAll = false; + + await this.uploadFilesSequentially(); + } + + async uploadFilesSequentially() { + let completedFiles = 0; + let currentFileIndex = 0; + + const updateProgress = () => { + if (!this.pendingUploads || currentFileIndex >= this.pendingUploads.length) { + this.uploadProgressBar.style.width = '100%'; + this.uploadProgressBar.textContent = '100%'; + this.uploadProgressText.textContent = 'Upload complete!'; + return; + } + + const progress = Math.round((completedFiles / this.pendingUploads.length) * 100); + this.uploadProgressBar.style.width = progress + '%'; + this.uploadProgressBar.textContent = progress + '%'; + this.uploadProgressText.textContent = `Uploading ${this.pendingUploads[currentFileIndex].name} (${currentFileIndex + 1}/${this.pendingUploads.length})`; + }; + + const processNextFile = async () => { + if (currentFileIndex >= this.pendingUploads.length) { + // All files processed + this.uploadProgressBar.style.width = '100%'; + this.uploadProgressBar.textContent = '100%'; + this.uploadProgressBar.classList.remove('bg-info'); + this.uploadProgressBar.classList.add('bg-success'); + this.uploadProgressText.textContent = 'Upload complete!'; + + // Reset state + this.pendingUploads = null; + this.pendingUploadIdx = null; + this.overwriteAll = false; + this.skipAll = false; + + // Hide progress after delay + setTimeout(() => { + this.uploadProgressContainer.style.display = 'none'; + this.uploadProgressText.textContent = ''; + this.uploadProgressBar.style.backgroundColor = '#16767b'; + this.uploadProgressBar.style.color = '#fff'; + }, 3000); + + // Refresh file list + await this.roomManager.fileManager.fetchFiles(); + return; + } + + const file = this.pendingUploads[currentFileIndex]; + const formData = new FormData(this.uploadForm); + if (this.roomManager.currentPath) { + formData.append('path', this.roomManager.currentPath); + } + formData.set('file', file); + + try { + updateProgress(); + + let uploadFormData = formData; + if (this.overwriteAll) { + uploadFormData = new FormData(this.uploadForm); + if (this.roomManager.currentPath) { + uploadFormData.append('path', this.roomManager.currentPath); + } + uploadFormData.set('file', file); + uploadFormData.append('overwrite', 'true'); + } + + const response = await this.uploadFile(uploadFormData); + + if (response.success) { + completedFiles++; + currentFileIndex++; + updateProgress(); + await processNextFile(); + } else if (response.error === 'File type not allowed') { + this.handleFileTypeError(file); + currentFileIndex++; + updateProgress(); + await processNextFile(); + } else if (response.error === 'File exists') { + const result = await this.handleFileExists(file, uploadFormData); + if (result.continue) { + currentFileIndex++; + updateProgress(); + await processNextFile(); + } + } + } catch (error) { + console.error('Upload error:', error); + this.uploadProgressText.textContent = `Error uploading ${file.name}`; + this.uploadProgressBar.style.backgroundColor = '#ef4444'; + this.uploadProgressBar.style.color = '#fff'; + currentFileIndex++; + updateProgress(); + await processNextFile(); + } + }; + + await processNextFile(); + } + + async uploadFile(formData) { + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + const response = await fetch(`/api/rooms/${this.roomManager.roomId}/files/upload`, { + method: 'POST', + headers: { 'X-CSRFToken': csrfToken }, + body: formData + }); + + const result = await response.json(); + return { + success: response.ok, + error: result.error + }; + } + + handleFileTypeError(file) { + const allowedTypes = [ + 'Documents: PDF, DOCX, DOC, TXT, RTF, ODT, MD, CSV', + 'Spreadsheets: XLSX, XLS, ODS, XLSM', + 'Presentations: PPTX, PPT, ODP', + 'Images: JPG, JPEG, PNG, GIF, BMP, SVG, WEBP, TIFF', + 'Archives: ZIP, RAR, 7Z, TAR, GZ', + 'Code/Text: PY, JS, HTML, CSS, JSON, XML, SQL, SH, BAT', + 'Audio: MP3, WAV, OGG, M4A, FLAC', + 'Video: MP4, AVI, MOV, WMV, FLV, MKV, WEBM', + 'CAD/Design: DWG, DXF, AI, PSD, EPS, INDD', + 'Other: EML, MSG, VCF, ICS' + ].join('\n'); + + const uploadError = document.getElementById('uploadError'); + const uploadErrorContent = document.getElementById('uploadErrorContent'); + + const newError = `
File type not allowed: ${file.name}
`; + if (uploadErrorContent.innerHTML === '') { + uploadErrorContent.innerHTML = newError + `
Allowed file types:
${allowedTypes}
`; + } else { + uploadErrorContent.innerHTML = newError + uploadErrorContent.innerHTML; + } + + uploadError.style.display = 'block'; + this.uploadProgressBar.style.backgroundColor = '#ef4444'; + this.uploadProgressBar.style.color = '#fff'; + } + + async handleFileExists(file, formData) { + if (this.overwriteAll) { + formData.append('overwrite', 'true'); + const response = await this.uploadFile(formData); + return { continue: response.success }; + } + + if (this.skipAll) { + return { continue: true }; + } + + const result = await this.roomManager.modalManager.showOverwriteModal(file.name); + + if (result === 'overwrite' || result === 'overwrite_all') { + if (result === 'overwrite_all') { + this.overwriteAll = true; + } + formData.append('overwrite', 'true'); + const response = await this.uploadFile(formData); + return { continue: response.success }; + } else if (result === 'skip' || result === 'skip_all') { + if (result === 'skip_all') { + this.skipAll = true; + } + return { continue: true }; + } + + return { continue: false }; + } +} \ No newline at end of file diff --git a/static/js/rooms/viewManager.js b/static/js/rooms/viewManager.js new file mode 100644 index 0000000..b25f204 --- /dev/null +++ b/static/js/rooms/viewManager.js @@ -0,0 +1,357 @@ +export class ViewManager { + constructor(roomManager) { + console.log('[ViewManager] Initializing...'); + this.roomManager = roomManager; + this.currentView = 'grid'; + this.sortColumn = 'name'; + this.sortDirection = 'asc'; + console.log('[ViewManager] Initialized with roomManager:', roomManager); + } + + async initializeView() { + console.log('[ViewManager] Initializing view...'); + try { + const response = await fetch('/api/user/preferred_view'); + console.log('[ViewManager] Preferences response status:', response.status); + + if (response.ok) { + const data = await response.json(); + console.log('[ViewManager] User preferences:', data); + this.currentView = data.preferred_view || 'grid'; + } + + this.toggleView(this.currentView); + console.log('[ViewManager] View initialized with:', this.currentView); + } catch (error) { + console.error('[ViewManager] Error initializing view:', error); + this.currentView = 'grid'; + this.toggleView('grid'); + } + } + + async toggleView(view) { + console.log('[ViewManager] Toggling view to:', view); + this.currentView = view; + + // Update UI + document.getElementById('gridViewBtn').classList.toggle('active', view === 'grid'); + document.getElementById('listViewBtn').classList.toggle('active', view === 'list'); + document.getElementById('fileGrid').classList.toggle('list-view', view === 'list'); + + // Save preference + try { + const response = await fetch('/api/user/preferred_view', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ preferred_view: view }) + }); + console.log('[ViewManager] Save preferences response status:', response.status); + } catch (error) { + console.error('[ViewManager] Error saving view preference:', error); + } + + // Re-render files if we have any + if (this.roomManager.fileManager.currentFiles.length > 0) { + console.log('[ViewManager] Re-rendering files with new view'); + await this.renderFiles(this.roomManager.fileManager.currentFiles); + } + } + + async renderFiles(files) { + console.log('[ViewManager] Rendering files:', files); + const fileGrid = document.getElementById('fileGrid'); + + if (!files || files.length === 0) { + console.log('[ViewManager] No files to render'); + fileGrid.innerHTML = '
No files in this folder
'; + return; + } + + // Sort files + const sortedFiles = this.sortFiles(files); + console.log('[ViewManager] Sorted files:', sortedFiles); + + if (this.currentView === 'list') { + console.log('[ViewManager] Rendering list view'); + await this.renderListView(sortedFiles); + } else { + console.log('[ViewManager] Rendering grid view'); + await this.renderGridView(sortedFiles); + } + } + + renderBreadcrumb() { + console.log('[ViewManager] Rendering breadcrumb'); + const breadcrumb = document.getElementById('breadcrumb'); + const upBtn = document.getElementById('upBtn'); + const path = this.roomManager.currentPath; + + if (!path) { + console.log('[ViewManager] No path, hiding breadcrumb'); + breadcrumb.innerHTML = ''; + upBtn.style.display = 'none'; + return; + } + + console.log('[ViewManager] Building breadcrumb for path:', path); + const parts = path.split('/').filter(Boolean); + let html = ''; + let currentPath = ''; + + parts.forEach((part, index) => { + currentPath += '/' + part; + html += ` + + ${index > 0 ? '' : ''} + + ${part} + + + `; + }); + + breadcrumb.innerHTML = html; + upBtn.style.display = 'inline-block'; + console.log('[ViewManager] Breadcrumb rendered'); + } + + async renderListView(files) { + console.log('[ViewManager] Rendering list view'); + const fileGrid = document.getElementById('fileGrid'); + + // Create table structure + let html = ` + + + + + + + + + + + + + `; + + files.forEach((file, index) => { + console.log('[ViewManager] Rendering list item:', file); + html += this.renderFileRow(file, index); + }); + + html += '
NameSizeModifiedActions
'; + fileGrid.innerHTML = html; + console.log('[ViewManager] List view rendered'); + } + + async renderGridView(files) { + console.log('[ViewManager] Rendering grid view'); + const fileGrid = document.getElementById('fileGrid'); + let html = ''; + + files.forEach((file, index) => { + console.log('[ViewManager] Rendering grid item:', file); + html += this.renderFileCard(file, index); + }); + + fileGrid.innerHTML = html; + console.log('[ViewManager] Grid view rendered'); + } + + renderFileRow(file, index) { + console.log('[ViewManager] Rendering file row:', { file, index }); + const isFolder = file.type === 'folder'; + const icon = isFolder ? 'fa-folder' : this.getFileIcon(file.name); + const size = isFolder ? '-' : this.formatFileSize(file.size); + const modified = new Date(file.modified).toLocaleString(); + + return ` + + + + + + + + + ${file.name} + + ${size} + ${modified} + +
+ ${this.renderFileActions(file, index)} +
+ + + `; + } + + renderFileCard(file, index) { + console.log('[ViewManager] Rendering file card:', { file, index }); + const isFolder = file.type === 'folder'; + const icon = isFolder ? 'fa-folder' : this.getFileIcon(file.name); + const size = isFolder ? '-' : this.formatFileSize(file.size); + const modified = new Date(file.modified).toLocaleString(); + + return ` +
+
+
+
+ +
+
${file.name}
+
${modified}
+
${size}
+
+ +
+
+ `; + } + + renderFileActions(file, index) { + console.log('[ViewManager] Rendering file actions:', { file, index }); + const actions = []; + + if (file.type === 'folder') { + actions.push(` + + `); + } else { + if (this.roomManager.canDownload) { + actions.push(` + + `); + } + } + + if (this.roomManager.canRename) { + actions.push(` + + `); + } + + if (this.roomManager.canMove) { + actions.push(` + + `); + } + + actions.push(` + + `); + + if (this.roomManager.canDelete) { + actions.push(` + + `); + } + + return actions.join(''); + } + + sortFiles(files) { + console.log('[ViewManager] Sorting files:', { + column: this.sortColumn, + direction: this.sortDirection + }); + + return [...files].sort((a, b) => { + let comparison = 0; + + if (this.sortColumn === 'name') { + comparison = a.name.localeCompare(b.name); + } else if (this.sortColumn === 'size') { + comparison = (a.size || 0) - (b.size || 0); + } else if (this.sortColumn === 'modified') { + comparison = new Date(a.modified) - new Date(b.modified); + } + + return this.sortDirection === 'asc' ? comparison : -comparison; + }); + } + + getFileIcon(filename) { + const extension = filename.split('.').pop().toLowerCase(); + console.log('[ViewManager] Getting icon for file:', { filename, extension }); + + const iconMap = { + pdf: 'fa-file-pdf', + doc: 'fa-file-word', + docx: 'fa-file-word', + xls: 'fa-file-excel', + xlsx: 'fa-file-excel', + ppt: 'fa-file-powerpoint', + pptx: 'fa-file-powerpoint', + txt: 'fa-file-alt', + jpg: 'fa-file-image', + jpeg: 'fa-file-image', + png: 'fa-file-image', + gif: 'fa-file-image', + zip: 'fa-file-archive', + rar: 'fa-file-archive', + mp3: 'fa-file-audio', + mp4: 'fa-file-video' + }; + + return iconMap[extension] || 'fa-file'; + } + + formatFileSize(bytes) { + if (!bytes) return '0 B'; + + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${size.toFixed(1)} ${units[unitIndex]}`; + } + + updateMultiSelectUI() { + console.log('[ViewManager] Updating multi-select UI'); + const selectedItems = this.roomManager.fileManager.getSelectedItems(); + const hasSelection = selectedItems.length > 0; + + document.getElementById('downloadSelectedBtn').style.display = + hasSelection && this.roomManager.canDownload ? 'flex' : 'none'; + document.getElementById('deleteSelectedBtn').style.display = + hasSelection && this.roomManager.canDelete ? 'flex' : 'none'; + + console.log('[ViewManager] Multi-select UI updated:', { + hasSelection, + selectedCount: selectedItems.length + }); + } +} \ No newline at end of file diff --git a/templates/common/base.html b/templates/common/base.html index 6532404..4cb8dfd 100644 --- a/templates/common/base.html +++ b/templates/common/base.html @@ -3,6 +3,7 @@ + {% block head %}{% endblock %} {% block title %}{% if site_settings.company_name %}DocuPulse for {{ site_settings.company_name }}{% else %}DocuPulse{% endif %}{% endblock %} diff --git a/templates/rooms/room.html b/templates/rooms/room.html index c867cbb..23e4142 100644 --- a/templates/rooms/room.html +++ b/templates/rooms/room.html @@ -1,10 +1,24 @@ {% extends "common/base.html" %} {% from "components/header.html" import header %} -{% block title %}Room - DocuPulse{% endblock %} +{% block title %}{{ room.name }} - DocuPulse{% endblock %} + +{% block extra_css %} + +{% endblock %} {% block content %} + + + + + + + + + + {{ header( title=room.name, description=room.description or 'No description', @@ -20,68 +34,94 @@
- -
Files
- {% if current_user.is_admin %} - + {% if current_user.is_admin %} + Manage Members - {% endif %} - {% if current_user.is_admin or can_upload %} -
-
- {% endif %} - {% if current_user.is_admin or can_download %} - - {% endif %} - {% if current_user.is_admin or can_delete %} - - {% endif %} + {% endif %}