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'}
+
+
+
+
+
${file.uploaded_by || '-'}
+
+ ${this.roomManager.viewManager.formatDate(file.modified)}
+
+ Room: ${this.roomManager.roomName}
+ 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 = 'Root Folder ';
+
+ 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 += `${indent}${displayName} `;
+ });
+
+ 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 = '';
+ 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 = `
+
+
+
+
+
+ Name
+ Size
+ Modified
+ Actions
+
+
+
+ `;
+
+ files.forEach((file, index) => {
+ console.log('[ViewManager] Rendering list item:', file);
+ html += this.renderFileRow(file, index);
+ });
+
+ html += '
';
+ 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 @@
-
-
-
+
+
Drop files here to upload
@@ -93,109 +133,118 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ This item will be moved to trash. You can restore it from the trash page within 30 days.
+
+
+
-
-
- This item will be moved to trash. You can restore it from the trash page within 30 days.
-
-
-
-
-
-
-
-
-
-
-
-
-
A file with this name already exists. Do you want to overwrite ?
-
-
+
+
+
+
+
+
A file with this name already exists. Do you want to overwrite ?
+
+
+
-
@@ -217,1713 +266,29 @@
+{% endblock %}
-
-
+{% block extra_js %}
-{% endblock %}
\ No newline at end of file
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/rooms/roombefore.html b/templates/rooms/roombefore.html
new file mode 100644
index 0000000..e69de29