Started room separation

This commit is contained in:
2025-05-28 11:37:25 +02:00
parent 11446e00db
commit 9b98370989
11 changed files with 1905 additions and 1813 deletions

310
static/css/room.css Normal file
View File

@@ -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;
}

View File

@@ -102,6 +102,31 @@ function sortFiles(column) {
renderFiles(currentFiles); 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) { function renderFiles(files) {
if (!files) return; if (!files) return;
currentFiles = files; currentFiles = files;
@@ -127,7 +152,7 @@ function renderFiles(files) {
files.forEach((file, idx) => { files.forEach((file, idx) => {
let icon = file.type === 'folder' let icon = file.type === 'folder'
? `<i class='fas fa-folder' style='font-size:1.5rem;color:var(--primary-color);'></i>` ? `<i class='fas fa-folder' style='font-size:1.5rem;color:var(--primary-color);'></i>`
: `<i class='fas fa-file-alt' style='font-size:1.5rem;color:var(--secondary-color);'></i>`; : `<i class='fas ${getFileIcon(file.name)}' style='font-size:1.5rem;color:var(--secondary-color);'></i>`;
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 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 actionsArr = [];
let dblClickAction = ''; let dblClickAction = '';
@@ -162,7 +187,7 @@ function renderFiles(files) {
files.forEach((file, idx) => { files.forEach((file, idx) => {
let icon = file.type === 'folder' let icon = file.type === 'folder'
? `<i class='fas fa-folder' style='font-size:2.5rem;color:var(--primary-color);'></i>` ? `<i class='fas fa-folder' style='font-size:2.5rem;color:var(--primary-color);'></i>`
: `<i class='fas fa-file-alt' style='font-size:2.5rem;color:var(--secondary-color);'></i>`; : `<i class='fas ${getFileIcon(file.name)}' style='font-size:2.5rem;color:var(--secondary-color);'></i>`;
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 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 actionsArr = [];
let dblClickAction = ''; let dblClickAction = '';

View File

@@ -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);
}
}

View File

@@ -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 = `<div class="input-group"><input type="text" id="renameInput" class="form-control" value="${base}" autocomplete="off" /><span class="input-group-text">${ext}</span></div>`;
} else {
inputGroup.innerHTML = `<input type="text" id="renameInput" class="form-control" value="${filename}" autocomplete="off" />`;
}
this.renameModal.show();
setTimeout(() => {
document.getElementById('renameInput').focus();
}, 100);
}
showDetailsModal(file) {
const icon = file.type === 'folder'
? `<i class='fas fa-folder' style='font-size:2.2rem;color:var(--primary-color);'></i>`
: `<i class='fas fa-file-alt' style='font-size:2.2rem;color:var(--secondary-color);'></i>`;
const uploaderPic = file.uploader_profile_pic
? `/uploads/profile_pics/${file.uploader_profile_pic}`
: '/static/default-avatar.png';
const detailsHtml = `
<div class='d-flex align-items-center gap-3 mb-3'>
<div>${icon}</div>
<div style='min-width:0;'>
<div class='fw-bold' style='font-size:1.1rem;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' title='${file.name}'>${file.name}</div>
<div class='text-muted small'>${file.type === 'folder' ? 'Folder' : 'File'}</div>
</div>
</div>
<div class='mb-2 d-flex align-items-center gap-2'>
<img src='${uploaderPic}' alt='Profile Picture' class='rounded-circle border' style='width:28px;height:28px;object-fit:cover;'>
<span class='fw-semibold' style='font-size:0.98rem;'>${file.uploaded_by || '-'}</span>
</div>
<div class='mb-2 text-muted' style='font-size:0.92rem;'><i class='far fa-clock me-1'></i>${this.roomManager.viewManager.formatDate(file.modified)}</div>
<hr style='margin:0.7rem 0 0.5rem 0; border-color:var(--border-light);'>
<div style='font-size:0.91rem;color:var(--text-muted);'><strong style='color:var(--primary-color);'>Room:</strong> <button class='btn btn-sm' style='background-color:var(--primary-opacity-8);color:var(--primary-color);' onclick='window.location.href=\"/rooms/${this.roomManager.roomId}\"'><i class='fas fa-door-open me-1'></i>${this.roomManager.roomName}</button></div>
<div style='font-size:0.91rem;color:var(--text-muted);'><strong style='color:var(--primary-color);'>Path:</strong> <span style='word-break:break-all;'>${(file.path ? file.path + '/' : '') + file.name}</span></div>
<div style='font-size:0.91rem;color:var(--text-muted);'><strong style='color:var(--primary-color);'>Size:</strong> ${this.roomManager.viewManager.formatFileSize(file.size)}</div>
<div style='font-size:0.91rem;color:var(--text-muted);'><strong style='color:var(--primary-color);'>Uploaded at:</strong> ${file.uploaded_at ? new Date(file.uploaded_at).toLocaleString() : '-'}</div>
`;
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 = '<option value="">Root Folder</option>';
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 = '&nbsp;&nbsp;&nbsp;&nbsp;'.repeat(pathParts.length - 1);
select.innerHTML += `<option value="${folderPath}">${indent}${displayName}</option>`;
});
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.';
}
}
}

131
static/js/rooms/room.js Normal file
View File

@@ -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);
}

View File

@@ -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);
};
}
}

View File

@@ -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 = `<div class="mb-2"><strong>File type not allowed:</strong> ${file.name}</div>`;
if (uploadErrorContent.innerHTML === '') {
uploadErrorContent.innerHTML = newError + `<div class='mt-2'><strong>Allowed file types:</strong><br>${allowedTypes}</div>`;
} 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 };
}
}

View File

@@ -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 = '<div class="col"><div class="text-muted">No files in this folder</div></div>';
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 += `
<span class="d-flex align-items-center">
${index > 0 ? '<i class="fas fa-chevron-right mx-2 text-muted"></i>' : ''}
<a href="?path=${encodeURIComponent(currentPath)}" class="text-decoration-none">
${part}
</a>
</span>
`;
});
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 = `
<table class="table table-hover">
<thead>
<tr>
<th style="width: 40px;"></th>
<th style="width: 40px;"></th>
<th>Name</th>
<th>Size</th>
<th>Modified</th>
<th style="width: 100px;">Actions</th>
</tr>
</thead>
<tbody>
`;
files.forEach((file, index) => {
console.log('[ViewManager] Rendering list item:', file);
html += this.renderFileRow(file, index);
});
html += '</tbody></table>';
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 `
<tr data-index="${index}" class="file-row" style="cursor: pointer;">
<td>
<input type="checkbox" class="form-check-input select-item-checkbox"
data-index="${index}" style="margin: 0;">
</td>
<td>
<i class="fas ${icon}" style="font-size:1.5rem;color:${isFolder ? 'var(--primary-color)' : 'var(--secondary-color)'}"></i>
</td>
<td>
<span class="file-name">${file.name}</span>
</td>
<td class="text-muted">${size}</td>
<td class="text-muted">${modified}</td>
<td>
<div class="d-flex justify-content-end gap-1">
${this.renderFileActions(file, index)}
</div>
</td>
</tr>
`;
}
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 `
<div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-3">
<div class="card file-card h-100 border-0 shadow-sm position-relative" data-index="${index}">
<div class="card-body d-flex flex-column align-items-center justify-content-center text-center p-4">
<div class="mb-2">
<i class="fas ${icon}" style="font-size:2.5rem;color:${isFolder ? 'var(--primary-color)' : 'var(--secondary-color)'};"></i>
</div>
<div class="fw-bold file-name-ellipsis mb-1" title="${file.name}">${file.name}</div>
<div class="text-muted" style="font-size:0.85rem;">${modified}</div>
<div class="text-muted mb-2" style="font-size:0.85rem;">${size}</div>
</div>
<div class="card-footer bg-white border-0 d-flex justify-content-center gap-2">
${this.renderFileActions(file, index)}
</div>
</div>
</div>
`;
}
renderFileActions(file, index) {
console.log('[ViewManager] Rendering file actions:', { file, index });
const actions = [];
if (file.type === 'folder') {
actions.push(`
<button class="btn btn-sm file-action-btn" title="Open" onclick="window.roomManager.navigateToFolder('${file.name}')"
style="background-color:var(--primary-opacity-8);color:var(--primary-color);">
<i class="fas fa-folder-open"></i>
</button>
`);
} else {
if (this.roomManager.canDownload) {
actions.push(`
<button class="btn btn-sm file-action-btn" title="Download" onclick="window.roomManager.fileManager.downloadFile('${file.id}')"
style="background-color:var(--primary-opacity-8);color:var(--primary-color);">
<i class="fas fa-download"></i>
</button>
`);
}
}
if (this.roomManager.canRename) {
actions.push(`
<button class="btn btn-sm file-action-btn" title="Rename" onclick="window.roomManager.modalManager.showRenameModal('${file.id}')"
style="background-color:var(--primary-opacity-8);color:var(--primary-color);">
<i class="fas fa-edit"></i>
</button>
`);
}
if (this.roomManager.canMove) {
actions.push(`
<button class="btn btn-sm file-action-btn" title="Move" onclick="window.roomManager.modalManager.showMoveModal('${file.id}')"
style="background-color:var(--primary-opacity-8);color:var(--primary-color);">
<i class="fas fa-arrows-alt"></i>
</button>
`);
}
actions.push(`
<button class="btn btn-sm file-action-btn" title="${file.is_starred ? 'Unstar' : 'Star'}" onclick="window.roomManager.fileManager.toggleStar('${file.id}')"
style="background-color:${file.is_starred ? 'var(--warning-opacity-15)' : 'var(--primary-opacity-8)'};color:${file.is_starred ? 'var(--warning-color)' : 'var(--primary-color)'};">
<i class="fas fa-star"></i>
</button>
`);
if (this.roomManager.canDelete) {
actions.push(`
<button class="btn btn-sm file-action-btn" title="Delete" onclick="window.roomManager.modalManager.showDeleteModal('${file.id}')"
style="background-color:var(--danger-opacity-15);color:var(--danger-color);">
<i class="fas fa-trash"></i>
</button>
`);
}
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
});
}
}

View File

@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
{% block head %}{% endblock %}
<meta name="csrf-token" content="{{ csrf_token }}"> <meta name="csrf-token" content="{{ csrf_token }}">
<title>{% block title %}{% if site_settings.company_name %}DocuPulse for {{ site_settings.company_name }}{% else %}DocuPulse{% endif %}{% endblock %}</title> <title>{% block title %}{% if site_settings.company_name %}DocuPulse for {{ site_settings.company_name }}{% else %}DocuPulse{% endif %}{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">

File diff suppressed because it is too large Load Diff

View File