From 651de5019d2d6e42391c82e2be5b5c1146ffd7c9 Mon Sep 17 00:00:00 2001 From: Kobe Date: Sun, 25 May 2025 11:42:19 +0200 Subject: [PATCH] reformatted starred and trash pages --- routes/__pycache__/__init__.cpython-313.pyc | Bin 1422 -> 1422 bytes routes/__pycache__/room_files.cpython-313.pyc | Bin 36760 -> 36760 bytes static/css/file-grid.css | 105 +++ static/js/file-grid.js | 510 ++++++++++++++ templates/base.html | 4 +- templates/components/details_modal.html | 17 + templates/components/empty_trash_modal.html | 30 + .../components/permanent_delete_modal.html | 30 + templates/components/search_bar.html | 7 + templates/starred.html | 462 +------------ templates/trash.html | 638 +----------------- 11 files changed, 723 insertions(+), 1080 deletions(-) create mode 100644 static/css/file-grid.css create mode 100644 static/js/file-grid.js create mode 100644 templates/components/details_modal.html create mode 100644 templates/components/empty_trash_modal.html create mode 100644 templates/components/permanent_delete_modal.html create mode 100644 templates/components/search_bar.html diff --git a/routes/__pycache__/__init__.cpython-313.pyc b/routes/__pycache__/__init__.cpython-313.pyc index 9a0d19c18aff319b295e22a32731aeea4eb6d9dd..4bf5c4d9d7ce31faf4c321c0acc65a4a82a2883b 100644 GIT binary patch delta 19 ZcmeC1l9lm diff --git a/routes/__pycache__/room_files.cpython-313.pyc b/routes/__pycache__/room_files.cpython-313.pyc index 17cd6ae5a5b028ce40696b215e2b69571dd9920a..793cbc820e90034daf8af898ce6decddf4f5930d 100644 GIT binary patch delta 21 bcmbO+pJ~Q?Ca%xCyj%=GP<&$}S6@E>Nr(ow delta 21 bcmbO+pJ~Q?Ca%xCyj%=GpmK2|S6@E>M~enw 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 = '
No items found.
'; + return; + } + + if (currentView === 'list') { + let table = ` + + + + + + + ${isTrashPage ? `` : ''} + + `; + 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 += ` + + + + + + + ${isTrashPage ? `` : ''} + + `; + }); + table += '
RoomName ${(sortColumn==='name') ? (sortDirection===1?'▲':'▼') : ''}Date Modified ${(sortColumn==='modified') ? (sortDirection===1?'▲':'▼') : ''}Type ${(sortColumn==='type') ? (sortDirection===1?'▲':'▼') : ''}Size ${(sortColumn==='size') ? (sortDirection===1?'▲':'▼') : ''}Auto Delete ${(sortColumn==='auto_delete') ? (sortDirection===1?'▲':'▼') : ''}
${icon}${file.name}${formatDate(file.modified)}${file.type}${size}${file.auto_delete ? formatDate(file.auto_delete) : 'Never'}${actions}
'; + 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'}
+
+
+
+ Profile Picture + ${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
-
-
-
-
-
-
- - -
-
Files
-
- {% if current_user.is_admin %} - - {% endif %}
-
-
- - -
-
+ {% include 'components/search_bar.html' %}
- - +{% include 'components/details_modal.html' %} +{% include 'components/permanent_delete_modal.html' %} +{% include 'components/empty_trash_modal.html' %} +{% endblock %} - - - - - - - - - +{% block extra_js %} + {% endblock %} \ No newline at end of file