diff --git a/routes/__pycache__/__init__.cpython-313.pyc b/routes/__pycache__/__init__.cpython-313.pyc
index 9a0d19c..4bf5c4d 100644
Binary files a/routes/__pycache__/__init__.cpython-313.pyc and b/routes/__pycache__/__init__.cpython-313.pyc differ
diff --git a/routes/__pycache__/room_files.cpython-313.pyc b/routes/__pycache__/room_files.cpython-313.pyc
index 17cd6ae..793cbc8 100644
Binary files a/routes/__pycache__/room_files.cpython-313.pyc and b/routes/__pycache__/room_files.cpython-313.pyc differ
diff --git a/static/css/file-grid.css b/static/css/file-grid.css
new file mode 100644
index 0000000..f31d36d
--- /dev/null
+++ b/static/css/file-grid.css
@@ -0,0 +1,105 @@
+.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 .file-action-btn {
+ cursor: pointer;
+}
+
+#fileGrid.table-mode {
+ padding: 0;
+}
+
+#fileGrid.table-mode table {
+ width: 100%;
+ border-collapse: collapse;
+ background: #fff;
+}
+
+#fileGrid.table-mode th, #fileGrid.table-mode td {
+ padding: 0.5rem 1rem;
+ border-bottom: 1px solid #e9ecef;
+ text-align: left;
+ font-size: 0.95rem;
+ vertical-align: middle;
+}
+
+#fileGrid.table-mode th {
+ background: #f8f9fa;
+ color: #6c757d;
+ font-weight: 500;
+}
+
+#fileGrid.table-mode tr:hover td {
+ background-color: rgba(22, 118, 123, 0.08);
+ 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;
+}
+
+/* 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.table-mode tr {
+ cursor: pointer;
+}
\ No newline at end of file
diff --git a/static/js/file-grid.js b/static/js/file-grid.js
new file mode 100644
index 0000000..b94116c
--- /dev/null
+++ b/static/js/file-grid.js
@@ -0,0 +1,510 @@
+let currentView = 'grid';
+let lastSelectedIndex = -1;
+let sortColumn = 'name'; // Set default sort column to name
+let sortDirection = 1; // 1 for ascending, -1 for descending
+let batchDeleteItems = null;
+let currentFiles = [];
+let fileToDelete = null;
+window.isAdmin = document.body.dataset.isAdmin === 'true';
+
+// Check if we're on the trash page
+const isTrashPage = window.location.pathname.includes('/trash');
+
+// Initialize the view and fetch files
+async function initializeView() {
+ try {
+ const response = await fetch('/api/user/preferred_view');
+ const data = await response.json();
+ currentView = data.preferred_view || 'grid';
+ } catch (error) {
+ console.error('Error fetching preferred view:', error);
+ currentView = 'grid';
+ }
+
+ // First fetch files
+ await fetchFiles();
+
+ // Then toggle view after files are loaded
+ toggleView(currentView);
+
+ // Sort files by name by default
+ sortFiles('name');
+}
+
+function toggleView(view) {
+ currentView = view;
+ const grid = document.getElementById('fileGrid');
+ const gridBtn = document.getElementById('gridViewBtn');
+ const listBtn = document.getElementById('listViewBtn');
+ if (view === 'grid') {
+ grid.classList.remove('table-mode');
+ if (gridBtn) gridBtn.classList.add('active');
+ if (listBtn) listBtn.classList.remove('active');
+ } else {
+ grid.classList.add('table-mode');
+ if (gridBtn) gridBtn.classList.remove('active');
+ if (listBtn) listBtn.classList.add('active');
+ }
+ renderFiles(currentFiles);
+
+ // Save the new preference
+ const csrfToken = getCsrfToken();
+ if (!csrfToken) {
+ console.error('CSRF token not available for saving view preference');
+ return;
+ }
+
+ fetch('/api/user/preferred_view', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': csrfToken
+ },
+ body: JSON.stringify({ preferred_view: view })
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ return response.json();
+ })
+ .then(data => {
+ console.log('Preferred view saved:', data);
+ })
+ .catch(error => {
+ console.error('Error saving preferred view:', error);
+ // Continue with the view change even if saving fails
+ });
+}
+
+function sortFiles(column) {
+ if (sortColumn === column) {
+ sortDirection *= -1; // Toggle direction
+ } else {
+ sortColumn = column;
+ sortDirection = 1;
+ }
+ currentFiles.sort((a, b) => {
+ let valA = a[column];
+ let valB = b[column];
+ // For size, convert to number
+ if (column === 'size') {
+ valA = typeof valA === 'number' ? valA : 0;
+ valB = typeof valB === 'number' ? valB : 0;
+ }
+ // For name/type, compare as strings
+ if (typeof valA === 'string' && typeof valB === 'string') {
+ return valA.localeCompare(valB) * sortDirection;
+ }
+ // For date (modified), compare as numbers
+ return (valA - valB) * sortDirection;
+ });
+ renderFiles(currentFiles);
+}
+
+function renderFiles(files) {
+ if (!files) return;
+ currentFiles = files;
+ const grid = document.getElementById('fileGrid');
+ grid.innerHTML = '';
+
+ if (!files.length) {
+ grid.innerHTML = '
';
+ return;
+ }
+
+ if (currentView === 'list') {
+ let table = `
+ |
+ Room |
+ Name ${(sortColumn==='name') ? (sortDirection===1?'▲':'▼') : ''} |
+ Date Modified ${(sortColumn==='modified') ? (sortDirection===1?'▲':'▼') : ''} |
+ Type ${(sortColumn==='type') ? (sortDirection===1?'▲':'▼') : ''} |
+ Size ${(sortColumn==='size') ? (sortDirection===1?'▲':'▼') : ''} |
+ ${isTrashPage ? `Auto Delete ${(sortColumn==='auto_delete') ? (sortDirection===1?'▲':'▼') : ''} | ` : ''}
+ |
+
`;
+ 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 = '';
+ if (file.type === 'folder') {
+ dblClickAction = `ondblclick=\"window.location.href='/room/${file.room_id}?path=${encodeURIComponent(file.path ? file.path + '/' + file.name : file.name)}'\"`;
+ } else {
+ dblClickAction = `ondblclick=\"window.location.href='/api/rooms/${file.room_id}/files/${encodeURIComponent(file.name)}?path=${encodeURIComponent(file.path)}'\"`;
+ }
+
+ if (isTrashPage) {
+ actionsArr.push(``);
+ actionsArr.push(``);
+ } else {
+ actionsArr.push(``);
+ }
+ actionsArr.push(``);
+ const actions = actionsArr.join('');
+ table += `
+ | ${icon} |
+ |
+ ${file.name} |
+ ${formatDate(file.modified)} |
+ ${file.type} |
+ ${size} |
+ ${isTrashPage ? `${file.auto_delete ? formatDate(file.auto_delete) : 'Never'} | ` : ''}
+ ${actions} |
+
`;
+ });
+ table += '
';
+ grid.innerHTML = table;
+ } else {
+ 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 = '';
+ if (file.type === 'folder') {
+ dblClickAction = `ondblclick=\"window.location.href='/room/${file.room_id}?path=${encodeURIComponent(file.path ? file.path + '/' + file.name : file.name)}'\"`;
+ } else {
+ dblClickAction = `ondblclick=\"window.location.href='/api/rooms/${file.room_id}/files/${encodeURIComponent(file.name)}?path=${encodeURIComponent(file.path)}'\"`;
+ }
+
+ if (isTrashPage) {
+ actionsArr.push(``);
+ actionsArr.push(``);
+ } else {
+ actionsArr.push(``);
+ }
+ actionsArr.push(``);
+ const actions = actionsArr.join('');
+ grid.innerHTML += `
+
+
+
+
${icon}
+
${file.name}
+
${formatDate(file.modified)}
+
${size}
+ ${isTrashPage ? `
Auto Delete: ${file.auto_delete ? formatDate(file.auto_delete) : 'Never'}
` : ''}
+
+
+
+
+
`;
+ });
+ }
+}
+
+async function fetchFiles() {
+ try {
+ const endpoint = isTrashPage ? '/api/rooms/trash' : '/api/rooms/starred';
+ const response = await fetch(endpoint);
+ const files = await response.json();
+ if (files) {
+ window.currentFiles = files;
+ // Sort files by name by default
+ window.currentFiles.sort((a, b) => {
+ if (a.name < b.name) return -1;
+ if (a.name > b.name) return 1;
+ return 0;
+ });
+ renderFiles(files);
+ }
+ } catch (error) {
+ console.error('Error loading files:', error);
+ document.getElementById('fileGrid').innerHTML = 'Failed to load files. Please try refreshing the page.
';
+ }
+}
+
+function getCsrfToken() {
+ // First try to get it from the meta tag
+ const metaTag = document.querySelector('meta[name="csrf-token"]');
+ if (metaTag) {
+ const token = metaTag.getAttribute('content');
+ if (token && token.trim() !== '') {
+ return token;
+ }
+ }
+
+ // If not found in meta tag, try to get it from any form
+ const forms = document.querySelectorAll('form');
+ for (const form of forms) {
+ const csrfInput = form.querySelector('input[name="csrf_token"]');
+ if (csrfInput && csrfInput.value && csrfInput.value.trim() !== '') {
+ return csrfInput.value;
+ }
+ }
+
+ // If still not found, try to get it from any hidden input
+ const hiddenInputs = document.querySelectorAll('input[name="csrf_token"]');
+ for (const input of hiddenInputs) {
+ if (input.value && input.value.trim() !== '') {
+ return input.value;
+ }
+ }
+
+ console.error('CSRF token not found in any of the expected locations');
+ return '';
+}
+
+function toggleStar(filename, path = '', roomId) {
+ const csrfToken = getCsrfToken();
+ if (!csrfToken) {
+ console.error('CSRF token not available');
+ return;
+ }
+
+ fetch(`/api/rooms/${roomId}/star`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': csrfToken
+ },
+ body: JSON.stringify({
+ filename: filename,
+ path: path
+ })
+ })
+ .then(r => r.json())
+ .then(res => {
+ if (res.success) {
+ // Remove the file from the current view since it's no longer starred
+ currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId));
+ renderFiles(currentFiles);
+ } else {
+ console.error('Failed to toggle star:', res.error);
+ }
+ })
+ .catch(error => {
+ console.error('Error toggling star:', error);
+ });
+}
+
+function restoreFile(filename, path = '', roomId) {
+ const csrfToken = getCsrfToken();
+ if (!csrfToken) {
+ console.error('CSRF token not available');
+ return;
+ }
+
+ fetch(`/api/rooms/${roomId}/restore`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': csrfToken
+ },
+ body: JSON.stringify({
+ filename: filename,
+ path: path
+ })
+ })
+ .then(r => r.json())
+ .then(res => {
+ if (res.success) {
+ // Remove the file from the current view since it's been restored
+ currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId));
+ renderFiles(currentFiles);
+ } else {
+ console.error('Failed to restore file:', res.error);
+ }
+ })
+ .catch(error => {
+ console.error('Error restoring file:', error);
+ });
+}
+
+function showPermanentDeleteModal(filename, path = '', roomId) {
+ fileToDelete = { filename, path, roomId };
+ document.getElementById('permanentDeleteItemName').textContent = filename;
+ const modal = new bootstrap.Modal(document.getElementById('permanentDeleteModal'));
+ modal.show();
+}
+
+function permanentDeleteFile() {
+ if (!fileToDelete) return;
+
+ const { filename, path, roomId } = fileToDelete;
+ const csrfToken = getCsrfToken();
+ if (!csrfToken) {
+ console.error('CSRF token not available');
+ return;
+ }
+
+ fetch(`/api/rooms/${roomId}/delete-permanent`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': csrfToken
+ },
+ body: JSON.stringify({
+ filename: filename,
+ path: path
+ })
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ // Check if the response is empty
+ const contentType = response.headers.get('content-type');
+ if (contentType && contentType.includes('application/json')) {
+ return response.json();
+ }
+ return { success: true }; // If no JSON response, assume success
+ })
+ .then(res => {
+ if (res.success) {
+ // Remove the file from the current view since it's been deleted
+ currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId));
+ renderFiles(currentFiles);
+ // Close the modal
+ const modal = bootstrap.Modal.getInstance(document.getElementById('permanentDeleteModal'));
+ if (modal) {
+ modal.hide();
+ }
+ } else {
+ console.error('Failed to delete file:', res.error || 'Unknown error');
+ }
+ })
+ .catch(error => {
+ console.error('Error deleting file:', error);
+ // Show error to user
+ const modal = bootstrap.Modal.getInstance(document.getElementById('permanentDeleteModal'));
+ if (modal) {
+ modal.hide();
+ }
+ // You might want to show an error message to the user here
+ });
+}
+
+function navigateToFile(roomId, filename, path, type) {
+ if (type === 'folder') {
+ window.location.href = `/room/${roomId}?path=${encodeURIComponent(path ? path + '/' + filename : filename)}`;
+ } else {
+ window.location.href = `/api/rooms/${roomId}/files/${encodeURIComponent(filename)}?path=${encodeURIComponent(path)}`;
+ }
+}
+
+function showEmptyTrashModal() {
+ const modal = new bootstrap.Modal(document.getElementById('emptyTrashModal'));
+ modal.show();
+}
+
+function emptyTrash() {
+ const csrfToken = getCsrfToken();
+ if (!csrfToken) {
+ console.error('CSRF token not available');
+ return;
+ }
+
+ fetch('/api/rooms/empty-trash', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': csrfToken
+ }
+ })
+ .then(r => r.json())
+ .then(res => {
+ if (res.success) {
+ // Clear all files from the current view
+ currentFiles = [];
+ renderFiles(currentFiles);
+ // Close the modal
+ const modal = bootstrap.Modal.getInstance(document.getElementById('emptyTrashModal'));
+ modal.hide();
+ } else {
+ console.error('Failed to empty trash:', res.error);
+ }
+ })
+ .catch(error => {
+ console.error('Error emptying trash:', error);
+ });
+}
+
+function showDetailsModal(idx) {
+ const item = currentFiles[idx];
+ const icon = item.type === 'folder'
+ ? ``
+ : ``;
+ const uploaderPic = item.uploader_profile_pic
+ ? `/uploads/profile_pics/${item.uploader_profile_pic}`
+ : '/static/default-avatar.png';
+ const detailsHtml = `
+
+
${icon}
+
+
${item.name}
+
${item.type === 'folder' ? 'Folder' : 'File'}
+
+
+
+

+
${item.uploaded_by || '-'}
+
+ ${formatDate(item.modified)}
+
+ Room:
+ Path: ${(item.path ? item.path + '/' : '') + item.name}
+ Size: ${item.size === '-' ? '-' : (item.size > 0 ? (item.size < 1024*1024 ? (item.size/1024).toFixed(1)+' KB' : (item.size/1024/1024).toFixed(2)+' MB') : '0 KB')}
+ Uploaded at: ${item.uploaded_at ? new Date(item.uploaded_at).toLocaleString() : '-'}
+ ${isTrashPage ? `Auto Delete: ${item.auto_delete ? formatDate(item.auto_delete) : 'Never'}
` : ''}
+ `;
+ document.getElementById('detailsModalBody').innerHTML = detailsHtml;
+ var modal = new bootstrap.Modal(document.getElementById('detailsModal'));
+ modal.show();
+}
+
+function formatDate(dateString) {
+ const date = new Date(dateString);
+ return date.toLocaleString();
+}
+
+// Initialize search functionality
+document.addEventListener('DOMContentLoaded', function() {
+ initializeView();
+
+ const quickSearchInput = document.getElementById('quickSearchInput');
+ const clearSearchBtn = document.getElementById('clearSearchBtn');
+ let searchTimeout = null;
+
+ quickSearchInput.addEventListener('input', function() {
+ const query = quickSearchInput.value.trim().toLowerCase();
+ clearSearchBtn.style.display = query.length > 0 ? 'block' : 'none';
+
+ if (searchTimeout) clearTimeout(searchTimeout);
+ searchTimeout = setTimeout(() => {
+ if (query.length === 0) {
+ fetchFiles();
+ return;
+ }
+
+ const filteredFiles = currentFiles.filter(file =>
+ file.name.toLowerCase().includes(query)
+ );
+ renderFiles(filteredFiles);
+ }, 200);
+ });
+
+ clearSearchBtn.addEventListener('click', function() {
+ quickSearchInput.value = '';
+ clearSearchBtn.style.display = 'none';
+ fetchFiles();
+ });
+
+ // Add event listeners for trash-specific buttons
+ if (isTrashPage) {
+ const confirmEmptyTrashBtn = document.getElementById('confirmEmptyTrash');
+ if (confirmEmptyTrashBtn) {
+ confirmEmptyTrashBtn.addEventListener('click', emptyTrash);
+ }
+
+ const confirmPermanentDeleteBtn = document.getElementById('confirmPermanentDelete');
+ if (confirmPermanentDeleteBtn) {
+ confirmPermanentDeleteBtn.addEventListener('click', permanentDeleteFile);
+ }
+ }
+});
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
index 324d0fe..01a2bbc 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -3,10 +3,12 @@
+
{% block title %}DocuPulse{% endblock %}
-
+
+
+{% block extra_js %}
+
{% endblock %}
\ No newline at end of file
diff --git a/templates/trash.html b/templates/trash.html
index 196639e..2c8faae 100644
--- a/templates/trash.html
+++ b/templates/trash.html
@@ -3,641 +3,27 @@
{% block title %}Trash - DocuPulse{% endblock %}
{% block content %}
-
-
-
-
-
Trash
-
Your deleted files and folders from all rooms
-
-
-
-