Files
docupulse/templates/rooms/room.html
2025-05-25 12:20:02 +02:00

1827 lines
79 KiB
HTML

{% extends "common/base.html" %}
{% block title %}Room - DocuPulse{% endblock %}
{% block content %}
<meta name="csrf-token" content="{{ csrf_token }}">
<div class="container mt-4">
<div class="row mb-4">
<div class="col">
<h2>{{ room.name }}</h2>
<div class="text-muted">{{ room.description or 'No description' }}</div>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header d-flex justify-content-between align-items-center bg-white">
<div class="d-flex align-items-center gap-3">
<div class="btn-group btn-group-sm" role="group" style="margin-right: 0.5rem;">
<button type="button" id="gridViewBtn" class="btn btn-outline-secondary active" onclick="toggleView('grid')">
<i class="fas fa-th-large"></i>
</button>
<button type="button" id="listViewBtn" class="btn btn-outline-secondary" onclick="toggleView('list')">
<i class="fas fa-list"></i>
</button>
</div>
<h5 class="mb-0">Files</h5>
</div>
<div class="d-flex gap-2" id="actionButtonsRow">
{% if current_user.is_admin %}
<a href="{{ url_for('rooms.room_members', room_id=room.id) }}" class="btn btn-outline-primary d-flex align-items-center gap-2" style="border-color:#16767b; color:#16767b;" onmouseover="this.style.backgroundColor='#16767b'; this.style.color='white'" onmouseout="this.style.backgroundColor='transparent'; this.style.color='#16767b'">
<i class="fas fa-users"></i> Manage Members
</a>
{% endif %}
{% if current_user.is_admin or can_upload %}
<button type="button" id="newFolderBtn" class="btn btn-outline-primary d-flex align-items-center gap-2" style="border-color:#16767b; color:#16767b;" onmouseover="this.style.backgroundColor='#16767b'; this.style.color='white'" onmouseout="this.style.backgroundColor='transparent'; this.style.color='#16767b'">
<i class="fas fa-folder-plus"></i> New Folder
</button>
<form id="uploadForm" enctype="multipart/form-data" class="d-inline">
<input type="file" id="fileInput" name="file" multiple style="display:none;" />
<button type="button" id="uploadBtn" class="btn btn-primary d-flex align-items-center gap-2" style="background-color:#16767b; border:1px solid #16767b;" onmouseover="this.style.backgroundColor='#1a8a90'" onmouseout="this.style.backgroundColor='#16767b'">
<i class="fas fa-upload"></i> Upload
</button>
</form>
{% endif %}
{% if current_user.is_admin or can_download %}
<button id="downloadSelectedBtn" class="btn btn-outline-primary btn-sm d-flex align-items-center gap-2" style="display:none; border-color:#16767b; color:#16767b;" onmouseover="this.style.backgroundColor='#16767b'; this.style.color='white'" onmouseout="this.style.backgroundColor='transparent'; this.style.color='#16767b'">
<i class="fas fa-download"></i> Download Selected
</button>
{% endif %}
{% if current_user.is_admin or can_delete %}
<button id="deleteSelectedBtn" class="btn btn-outline-danger btn-sm d-flex align-items-center gap-2" style="display:none; border-color:#b91c1c; color:#b91c1c;" onmouseover="this.style.backgroundColor='#b91c1c'; this.style.color='white'" onmouseout="this.style.backgroundColor='transparent'; this.style.color='#b91c1c'">
<i class="fas fa-trash"></i> Delete Selected
</button>
{% endif %}
</div>
</div>
<div class="card-body">
<div id="uploadProgressContainer" style="display:none; margin-bottom: 1rem; width: 100%;">
<div class="progress" style="height: 1.25rem; background-color: #e6f3f4;">
<div id="uploadProgressBar" class="progress-bar" role="progressbar" style="width:0%; background-color:#16767b; color:#fff;">0%</div>
</div>
<div id="uploadProgressText" class="small text-muted mt-1"></div>
<div id="uploadError" class="alert alert-danger alert-dismissible fade show mt-2" style="display:none;" role="alert">
<div id="uploadErrorContent"></div>
<button type="button" class="btn-close" onclick="document.getElementById('uploadError').style.display='none'; document.getElementById('uploadErrorContent').innerHTML='';"></button>
</div>
</div>
<div class="d-flex align-items-center gap-2 mb-3">
<button id="upBtn" class="btn btn-outline-secondary btn-sm" hidden style="display:none;" title="Go up one level" onclick="navigateToParent()">
<i class="fas fa-arrow-up"></i>
</button>
<div id="breadcrumb" class="d-flex align-items-center gap-2"></div>
<div class="ms-auto" style="max-width: 300px; position: relative;">
<input type="text" id="quickSearchInput" class="form-control form-control-sm" placeholder="Quick search files..." autocomplete="off" style="padding-right: 2rem;" />
<button id="clearSearchBtn" type="button" style="position: absolute; right: 0.25rem; top: 50%; transform: translateY(-50%); display: none; border: none; background: transparent; font-size: 1.2rem; color: #888; cursor: pointer; z-index: 2;">&times;</button>
</div>
</div>
<div id="fileGrid" class="row g-3" style="min-height: 200px; position: relative;">
<div id="dropZoneOverlay" style="display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(22, 118, 123, 0.1); border: 2px dashed #16767b; border-radius: 8px; z-index: 1000; pointer-events: none;">
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #16767b;">
<i class="fas fa-cloud-upload-alt" style="font-size: 3rem; margin-bottom: 1rem;"></i>
<div style="font-size: 1.2rem; font-weight: 500;">Drop files here to upload</div>
</div>
</div>
</div>
<div id="fileError" class="text-danger mt-2"></div>
</div>
</div>
</div>
<div id="deleteConfirmModal" class="modal fade" tabindex="-1" aria-labelledby="deleteConfirmLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteConfirmLabel">Move to Trash</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="d-flex align-items-center gap-3 mb-3">
<i class="fas fa-trash text-danger" style="font-size: 2rem;"></i>
<div>
<h6 class="mb-1">Move to Trash</h6>
<p class="text-muted mb-0" id="deleteFileName"></p>
</div>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
This item will be moved to trash. You can restore it from the trash page within 30 days.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn" style="background-color: #b91c1c; border-color: #b91c1c;">
<i class="fas fa-trash me-1"></i>Move to Trash
</button>
</div>
</div>
</div>
</div>
<div id="newFolderModal" class="modal fade" tabindex="-1" aria-labelledby="newFolderLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="newFolderLabel">Create New Folder</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="text" id="folderNameInput" class="form-control" placeholder="Folder name" autocomplete="off" />
<div id="folderError" class="text-danger mt-2"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="createFolderBtn" style="background-color: #16767b; border-color: #16767b;">Create</button>
</div>
</div>
</div>
</div>
<div id="renameModal" class="modal fade" tabindex="-1" aria-labelledby="renameLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="renameLabel">Rename</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="renameInputGroup"></div>
<div id="renameError" class="text-danger mt-2"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirmRenameBtn" style="background-color: #16767b; border-color: #16767b;">Rename</button>
</div>
</div>
</div>
</div>
<!-- Details Modal -->
<div id="detailsModal" class="modal fade" tabindex="-1" aria-labelledby="detailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="detailsModalLabel">Item Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="detailsModalBody">
<!-- Populated by JS -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div id="overwriteConfirmModal" class="modal fade" tabindex="-1" aria-labelledby="overwriteConfirmLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="overwriteConfirmLabel">Overwrite File?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p id="overwriteFileMsg">A file with this name already exists. Do you want to overwrite <span id="overwriteFileName" class="fw-bold"></span>?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="skipOverwriteBtn">Skip</button>
<button type="button" class="btn btn-secondary" id="skipAllOverwriteBtn">Skip All</button>
<button type="button" class="btn btn-danger" id="confirmOverwriteBtn" style="background-color: #b91c1c; border-color: #b91c1c;">Overwrite</button>
<button type="button" class="btn btn-danger" id="confirmAllOverwriteBtn" style="background-color: #b91c1c; border-color: #b91c1c;">Overwrite All</button>
</div>
</div>
</div>
</div>
<!-- Move Modal -->
<div class="modal fade" id="moveModal" tabindex="-1" aria-labelledby="moveModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="moveModalLabel">Move File</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="moveTargetFolder" class="form-label">Select Target Folder</label>
<select class="form-select" id="moveTargetFolder">
<option value="">Root Folder</option>
</select>
</div>
<div id="moveError" class="text-danger"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirmMoveBtn" style="background-color:#16767b; border:1px solid #16767b;">Move</button>
</div>
</div>
</div>
</div>
<style>
.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;
}
#fileGrid.list-view table {
width: 100%;
border-collapse: collapse;
background: #fff;
}
#fileGrid.list-view th, #fileGrid.list-view td {
padding: 0.5rem 1rem;
border-bottom: 1px solid #e9ecef;
text-align: left;
font-size: 0.95rem;
vertical-align: middle;
}
#fileGrid.list-view th {
background: #f8f9fa;
color: #6c757d;
font-weight: 500;
}
#fileGrid.list-view tr:hover td {
background-color: rgba(22, 118, 123, 0.08);
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: #f8f9fa;
border-bottom: 1px solid #e9ecef;
color: #6c757d;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.btn-group.btn-group-sm .btn {
background-color: #fff;
border-color: #e9ecef;
color: #6c757d;
transition: background-color 0.15s, color 0.15s;
}
.btn-group.btn-group-sm .btn.active, .btn-group.btn-group-sm .btn:active {
background-color: #e6f3f4 !important;
color: #16767b !important;
border-color: #16767b !important;
box-shadow: none;
}
.btn-group.btn-group-sm .btn:focus {
box-shadow: 0 0 0 0.1rem #16767b33;
}
.btn-group.btn-group-sm .btn:hover:not(.active) {
background-color: #f8f9fa;
color: #16767b;
}
#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.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;
}
</style>
<script>
// Define showDetailsModal function first
function showDetailsModal(idx) {
// Check if currentFiles exists and idx is valid
if (!window.currentFiles || !Array.isArray(window.currentFiles) || idx < 0 || idx >= window.currentFiles.length) {
console.error('Invalid file index or currentFiles not initialized');
return;
}
const item = window.currentFiles[idx];
if (!item) {
console.error('File item not found at index:', idx);
return;
}
const icon = item.type === 'folder'
? `<i class='fas fa-folder' style='font-size:2.2rem;color:#16767b;'></i>`
: `<i class='fas fa-file-alt' style='font-size:2.2rem;color:#741b5f;'></i>`;
const uploaderPic = item.uploader_profile_pic
? `/uploads/profile_pics/${item.uploader_profile_pic}`
: '/static/default-avatar.png';
// Get room information from the current page
const roomId = "{{ room.id }}";
const roomName = "{{ room.name }}";
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='${item.name}'>${item.name}</div>
<div class='text-muted small'>${item.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;'>${item.uploaded_by || '-'}</span>
</div>
<div class='mb-2 text-muted' style='font-size:0.92rem;'><i class='far fa-clock me-1'></i>${formatDate(item.modified)}</div>
<hr style='margin:0.7rem 0 0.5rem 0; border-color:#e6f3f4;'>
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Room:</strong> <button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='window.location.href=\"/room/${roomId}\"'><i class='fas fa-door-open me-1'></i>${roomName}</button></div>
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Path:</strong> <span style='word-break:break-all;'>${(item.path ? item.path + '/' : '') + item.name}</span></div>
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Size:</strong> ${item.size === '-' ? '-' : (item.size > 0 ? (item.size < 1024*1024 ? (item.size/1024).toFixed(1)+' KB' : (item.size/1024/1024).toFixed(2)+' MB') : '0 KB')}</div>
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Uploaded at:</strong> ${item.uploaded_at ? new Date(item.uploaded_at).toLocaleString() : '-'}</div>
`;
document.getElementById('detailsModalBody').innerHTML = detailsHtml;
var modal = new bootstrap.Modal(document.getElementById('detailsModal'));
modal.show();
}
const roomId = "{{ room.id }}";
const canDelete = "{{ 'true' if current_user.is_admin or can_delete else 'false' }}";
const canShare = "{{ 'true' if current_user.is_admin or can_share else 'false' }}";
const canUpload = "{{ 'true' if current_user.is_admin or can_upload else 'false' }}";
const canDownload = "{{ 'true' if current_user.is_admin or can_download else 'false' }}";
const canRename = "{{ 'true' if current_user.is_admin or can_rename else 'false' }}";
const canMove = "{{ 'true' if current_user.is_admin or can_move else 'false' }}";
let fileToDelete = null;
let fileToDeletePath = '';
let currentPath = '';
let renameTarget = null;
let renameIsFile = false;
let renameOrigExt = '';
let selectedItems = [];
let currentFiles = [];
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 deleteModal = null;
let pendingUploads = null;
let pendingUploadIdx = null;
let pendingUploadFormData = null;
let pendingUploadXhr = null;
let overwriteModal = null;
let overwriteResolve = null;
let overwriteModalResult = null;
let overwriteAll = false;
let skipAll = false;
let moveModal = null;
let fileToMove = null;
let fileToMovePath = '';
let modalPromise = null;
let modalResolve = null;
// 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);
}
function formatDate(ts) {
if (!ts) return '';
const d = new Date(ts * 1000);
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
}
function renderBreadcrumb() {
const bc = document.getElementById('breadcrumb');
bc.innerHTML = '';
const parts = currentPath ? currentPath.split('/') : [];
let pathSoFar = '';
bc.innerHTML += `<a href="#" onclick="navigateTo('')" class="text-decoration-none" style="color:#16767b;">Root</a>`;
parts.forEach((part, idx) => {
pathSoFar += (pathSoFar ? '/' : '') + part;
bc.innerHTML += ` <span class="text-muted">/</span> <a href="#" onclick="navigateTo('${pathSoFar}')" class="text-decoration-none" style="color:#16767b;">${part}</a>`;
});
// Show/hide up button
const upBtn = document.getElementById('upBtn');
if (upBtn) {
if (currentPath && currentPath.trim().length > 0) {
upBtn.hidden = false;
upBtn.style.display = '';
} else {
upBtn.hidden = true;
upBtn.style.display = 'none';
}
}
}
function toggleView(view) {
currentView = view;
console.log('toggleView:', currentView);
const grid = document.getElementById('fileGrid');
const gridBtn = document.getElementById('gridViewBtn');
const listBtn = document.getElementById('listViewBtn');
if (view === 'grid') {
grid.classList.remove('list-view');
gridBtn.classList.add('active');
listBtn.classList.remove('active');
} else {
grid.classList.add('list-view');
gridBtn.classList.remove('active');
listBtn.classList.add('active');
}
renderFiles(window.currentFiles);
// Save the new preference
fetch('/api/user/preferred_view', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({ preferred_view: view })
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('Preferred view saved:', data);
})
.catch(error => console.error('Error saving preferred view:', error));
}
function sortFiles(column) {
if (sortColumn === column) {
sortDirection *= -1; // Toggle direction
} else {
sortColumn = column;
sortDirection = 1;
}
window.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(window.currentFiles);
}
// Add this function before renderFiles
function initializeSelectAllCheckbox() {
const selectAll = document.getElementById('selectAllTableCheckbox');
if (selectAll) {
// Remove any existing event listeners
const newSelectAll = selectAll.cloneNode(true);
selectAll.parentNode.replaceChild(newSelectAll, selectAll);
// Add the event listener to the new element
newSelectAll.addEventListener('change', function() {
const grid = document.getElementById('fileGrid');
const checkboxes = grid.querySelectorAll('.select-item-checkbox');
checkboxes.forEach(function(cb) { cb.checked = newSelectAll.checked; });
updateMultiSelectUI();
});
}
}
function renderFiles(files) {
// Initialize window.currentFiles if not set
if (!window.currentFiles) {
window.currentFiles = [];
}
// If files parameter is provided, update window.currentFiles
if (files) {
window.currentFiles = files;
}
console.log('renderFiles view:', currentView);
renderBreadcrumb();
const grid = document.getElementById('fileGrid');
grid.innerHTML = '';
// Use window.currentFiles as the source of truth
const filesToRender = window.currentFiles;
if (!filesToRender || !filesToRender.length) {
grid.innerHTML = '<div class="col"><div class="text-muted">No files or folders yet.</div></div>';
return;
}
if (currentView === 'list') {
// Create table header with the checkbox included
let table = `<table><thead><tr>
<th style='width:40px'><input type='checkbox' class='select-all-checkbox' style='margin-left: 0.3rem; margin-right: 1rem;'/></th>
<th onclick="sortFiles('name')" style="cursor:pointer;">Name ${(sortColumn==='name') ? (sortDirection===1?'▲':'▼') : ''}</th>
<th onclick="sortFiles('modified')" style="cursor:pointer;">Date Modified ${(sortColumn==='modified') ? (sortDirection===1?'▲':'▼') : ''}</th>
<th onclick="sortFiles('type')" style="cursor:pointer;">Type ${(sortColumn==='type') ? (sortDirection===1?'▲':'▼') : ''}</th>
<th onclick="sortFiles('size')" style="cursor:pointer;">Size ${(sortColumn==='size') ? (sortDirection===1?'▲':'▼') : ''}</th>
<th class='file-actions'></th>
</tr></thead><tbody>`;
filesToRender.forEach((file, idx) => {
let icon = file.type === 'folder'
? `<i class='fas fa-folder' style='font-size:1.5rem;color:#16767b;'></i>`
: `<i class='fas fa-file-alt' style='font-size:1.5rem;color:#741b5f;'></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 actionsArr = [];
const canRenameAction = (canRename === 'true');
let checkbox = `<input type='checkbox' class='select-item-checkbox form-check-input' data-idx='${idx}' title='Select' />`;
let dblClickAction = '';
if (file.type === 'folder') {
dblClickAction = `ondblclick=\"navigateTo('${currentPath ? currentPath + '/' : ''}${file.name}')\"`;
} else if (canDownload === 'true') {
dblClickAction = `ondblclick=\"downloadFile('${file.name}')\"`;
}
if (file.type === 'file') {
if (canDownload === 'true') {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Download' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='downloadFile("${file.name}")'><i class='fas fa-download'></i></button>`);
}
if (canRenameAction) {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Rename' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='showRenameModal("${file.name}")'><i class='fas fa-pen'></i></button>`);
}
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Details' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='showDetailsModal(${idx})'><i class='fas fa-info-circle'></i></button>`);
if (canMove === 'true') {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Move' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='showMoveModal("${file.name}", "${file.path}")'><i class='fas fa-arrows-alt'></i></button>`);
}
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='${file.starred ? 'Unstar' : 'Star'}' style='background-color:${file.starred ? 'rgba(255,215,0,0.15)' : 'rgba(22,118,123,0.08)'};color:${file.starred ? '#ffd700' : '#16767b'};' onclick='event.stopPropagation(); toggleStar("${file.name}", "${file.path || ''}")'><i class='fas fa-star'></i></button>`);
if (canDelete === true || canDelete === 'true') {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Delete' style='background-color:rgba(239,68,68,0.1);color:#b91c1c;' onclick='showDeleteModal("${file.name}", "${file.path}")'><i class='fas fa-trash'></i></button>`);
}
} else {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Open Folder' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='navigateTo(\"${currentPath ? currentPath + '/' : ''}${file.name}\")'><i class='fas fa-folder-open'></i></button>`);
if (canRenameAction) {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Rename' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='showRenameModal(\"${file.name}\")'><i class='fas fa-pen'></i></button>`);
}
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Details' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='showDetailsModal(${idx})'><i class='fas fa-info-circle'></i></button>`);
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='${file.starred ? 'Unstar' : 'Star'}' style='background-color:${file.starred ? 'rgba(255,215,0,0.15)' : 'rgba(22,118,123,0.08)'};color:${file.starred ? '#ffd700' : '#16767b'};' onclick='event.stopPropagation(); toggleStar("${file.name}", "${file.path || ''}")'><i class='fas fa-star'></i></button>`);
if (canDelete === true || canDelete === 'true') {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Delete' style='background-color:rgba(239,68,68,0.1);color:#b91c1c;' onclick='showDeleteModal("${file.name}", "${file.path}")'><i class='fas fa-trash'></i></button>`);
}
}
// Move Delete to the end if present
const deleteIdx = actionsArr.findIndex(a => a.includes('fa-trash'));
if (deleteIdx !== -1) {
const [delBtn] = actionsArr.splice(deleteIdx, 1);
actionsArr.push(delBtn);
}
const actions = actionsArr.join('');
table += `<tr ${dblClickAction} onclick='toggleSelection(${idx}, event)'>
<td class='file-icon'><span style="display:inline-flex;align-items:center;gap:1.2rem;">${checkbox}${icon}</span></td>
<td class='file-name' title='${file.name}'>${file.displayName || file.name}</td>
<td class='file-date'>${formatDate(file.modified)}</td>
<td class='file-type'>${file.type}</td>
<td class='file-size'>${size}</td>
<td class='file-actions'>${actions}</td>
</tr>`;
});
table += '</tbody></table>';
grid.innerHTML = table;
// Add event listener to the checkbox after the table is rendered
const selectAll = grid.querySelector('.select-all-checkbox');
if (selectAll) {
selectAll.addEventListener('change', function() {
const checkboxes = grid.querySelectorAll('.select-item-checkbox');
checkboxes.forEach(function(cb) { cb.checked = selectAll.checked; });
updateMultiSelectUI();
});
}
} else {
filesToRender.forEach((file, idx) => {
let icon = file.type === 'folder'
? `<i class='fas fa-folder' style='font-size:2.5rem;color:#16767b;'></i>`
: `<i class='fas fa-file-alt' style='font-size:2.5rem;color:#741b5f;'></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 actionsArr = [];
const canRenameAction = (canRename === 'true');
let checkbox = `<input type='checkbox' class='select-item-checkbox form-check-input' data-idx='${idx}' style='margin-top:0;' title='Select' />`;
let dblClickAction = '';
if (file.type === 'folder') {
dblClickAction = `ondblclick=\"navigateTo('${currentPath ? currentPath + '/' : ''}${file.name}')\"`;
} else if (canDownload === 'true') {
dblClickAction = `ondblclick=\"downloadFile('${file.name}')\"`;
}
if (file.type === 'file') {
if (canDownload === 'true') {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Download' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='downloadFile("${file.name}")'><i class='fas fa-download'></i></button>`);
}
if (canRenameAction) {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Rename' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='showRenameModal("${file.name}")'><i class='fas fa-pen'></i></button>`);
}
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Details' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='showDetailsModal(${idx})'><i class='fas fa-info-circle'></i></button>`);
if (canMove === 'true') {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Move' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='showMoveModal("${file.name}", "${file.path}")'><i class='fas fa-arrows-alt'></i></button>`);
}
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='${file.starred ? 'Unstar' : 'Star'}' style='background-color:${file.starred ? 'rgba(255,215,0,0.15)' : 'rgba(22,118,123,0.08)'};color:${file.starred ? '#ffd700' : '#16767b'};' onclick='event.stopPropagation(); toggleStar("${file.name}", "${file.path || ''}")'><i class='fas fa-star'></i></button>`);
if (canDelete === true || canDelete === 'true') {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Delete' style='background-color:rgba(239,68,68,0.1);color:#b91c1c;' onclick='showDeleteModal("${file.name}", "${file.path}")'><i class='fas fa-trash'></i></button>`);
}
} else {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Open Folder' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='navigateTo(\"${currentPath ? currentPath + '/' : ''}${file.name}\")'><i class='fas fa-folder-open'></i></button>`);
if (canRenameAction) {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Rename' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='showRenameModal(\"${file.name}\")'><i class='fas fa-pen'></i></button>`);
}
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Details' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='showDetailsModal(${idx})'><i class='fas fa-info-circle'></i></button>`);
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='${file.starred ? 'Unstar' : 'Star'}' style='background-color:${file.starred ? 'rgba(255,215,0,0.15)' : 'rgba(22,118,123,0.08)'};color:${file.starred ? '#ffd700' : '#16767b'};' onclick='event.stopPropagation(); toggleStar("${file.name}", "${file.path || ''}")'><i class='fas fa-star'></i></button>`);
if (canDelete === true || canDelete === 'true') {
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Delete' style='background-color:rgba(239,68,68,0.1);color:#b91c1c;' onclick='showDeleteModal("${file.name}", "${file.path}")'><i class='fas fa-trash'></i></button>`);
}
}
// Move Delete to the end if present
const deleteIdx = actionsArr.findIndex(a => a.includes('fa-trash'));
if (deleteIdx !== -1) {
const [delBtn] = actionsArr.splice(deleteIdx, 1);
actionsArr.push(delBtn);
}
const actions = actionsArr.join('');
grid.innerHTML += `
<div class='col'>
<div class='card file-card h-100 border-0 shadow-sm position-relative' ${dblClickAction} onclick='toggleSelection(${idx}, event)'>
<div class='card-body d-flex flex-column align-items-center justify-content-center text-center'>
<div class='mb-2 w-100 d-flex justify-content-start'>${checkbox}</div>
<div class='mb-2'>${icon}</div>
<div class='fw-semibold file-name-ellipsis' title='${file.name}'>${file.displayName || file.name}</div>
<div class='text-muted small'>${formatDate(file.modified)}</div>
<div class='text-muted small'>${size}</div>
</div>
<div class='card-footer bg-white border-0 d-flex justify-content-center gap-2'>${actions}</div>
</div>
</div>`;
});
}
updateMultiSelectUI();
}
function toggleSelection(idx, event) {
// Prevent selection if clicking on a checkbox or action button
if (event.target.classList.contains('select-item-checkbox') || event.target.closest('.file-action-btn')) {
return;
}
const checkboxes = document.querySelectorAll('.select-item-checkbox');
const checkbox = checkboxes[idx];
if (!checkbox) return;
if (event.ctrlKey) {
// CTRL + Click: Toggle individual selection
checkbox.checked = !checkbox.checked;
} else if (event.shiftKey && lastSelectedIndex !== -1) {
// SHIFT + Click: Select range
const start = Math.min(lastSelectedIndex, idx);
const end = Math.max(lastSelectedIndex, idx);
for (let i = start; i <= end; i++) {
checkboxes[i].checked = true;
}
} else {
// Normal click: Select single item
const wasChecked = checkbox.checked;
checkboxes.forEach(cb => cb.checked = false);
checkbox.checked = !wasChecked;
}
lastSelectedIndex = idx;
updateMultiSelectUI();
}
function fetchFiles() {
let url = `/api/rooms/${roomId}/files`;
if (currentPath) url += `?path=${encodeURIComponent(currentPath)}`;
console.log('Fetching files from:', url);
return fetch(url)
.then(r => {
console.log('Response status:', r.status);
if (!r.ok) {
throw new Error('Failed to load files');
}
return r.json();
})
.then(files => {
console.log('Received files:', files);
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 = '<div class="col"><div class="text-danger">Failed to load files. Please try refreshing the page.</div></div>';
});
}
function navigateTo(path) {
currentPath = path;
// Update the URL with the current path
const url = new URL(window.location);
if (currentPath) {
url.searchParams.set('path', currentPath);
} else {
url.searchParams.delete('path');
}
window.history.replaceState({}, '', url);
fetchFiles();
return false;
}
// On page load, set currentPath from URL if present
function getPathFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('path') || '';
}
function downloadFile(filename) {
let url = `/api/rooms/${roomId}/files/${encodeURIComponent(filename)}`;
if (currentPath) url += `?path=${encodeURIComponent(currentPath)}`;
window.location = url;
}
function showDeleteModal(filename, path = '') {
fileToDelete = filename;
fileToDeletePath = path;
batchDeleteItems = null;
// Get the modal element
const modalEl = document.getElementById('deleteConfirmModal');
if (!modalEl) {
console.error('Delete modal element not found');
return;
}
// Initialize the modal if it hasn't been initialized
if (!deleteModal) {
deleteModal = new bootstrap.Modal(modalEl);
}
// Update modal content
const fileNameEl = document.getElementById('deleteFileName');
const labelEl = document.getElementById('deleteConfirmLabel');
if (fileNameEl) fileNameEl.textContent = filename;
if (labelEl) labelEl.textContent = 'Move to Trash';
// Show the modal
deleteModal.show();
}
// Add event listener for the delete confirmation button
document.getElementById('confirmDeleteBtn').addEventListener('click', deleteFileConfirmed);
// Add event listener for download selected
document.getElementById('downloadSelectedBtn').addEventListener('click', function() {
const selectedCheckboxes = document.querySelectorAll('.select-item-checkbox:checked');
if (selectedCheckboxes.length === 0) return;
const selectedItems = Array.from(selectedCheckboxes).map(cb => {
const idx = parseInt(cb.dataset.idx);
return window.currentFiles[idx];
});
// Submit the request to download the zip
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch(`/api/rooms/${roomId}/download-zip`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ items: selectedItems })
})
.then(response => {
if (!response.ok) {
throw new Error('Download failed');
}
return response.blob();
})
.then(blob => {
// Create a download link and trigger it
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'download.zip';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
})
.catch(error => {
console.error('Error downloading files:', error);
document.getElementById('fileError').textContent = 'Failed to download files.';
});
});
// Add event listener for batch delete
document.getElementById('deleteSelectedBtn').addEventListener('click', function() {
const selectedCheckboxes = document.querySelectorAll('.select-item-checkbox:checked');
if (selectedCheckboxes.length === 0) return;
batchDeleteItems = Array.from(selectedCheckboxes).map(cb => {
const idx = parseInt(cb.dataset.idx);
return window.currentFiles[idx];
});
// Get the modal element
const modalEl = document.getElementById('deleteConfirmModal');
if (!modalEl) {
console.error('Delete modal element not found');
return;
}
// Initialize the modal if it hasn't been initialized
if (!deleteModal) {
deleteModal = new bootstrap.Modal(modalEl);
}
// Update modal content
const fileNameEl = document.getElementById('deleteFileName');
const labelEl = document.getElementById('deleteConfirmLabel');
if (fileNameEl) fileNameEl.textContent = `${selectedCheckboxes.length} item${selectedCheckboxes.length > 1 ? 's' : ''}`;
if (labelEl) labelEl.textContent = 'Move to Trash';
// Show the modal
deleteModal.show();
});
function deleteFileConfirmed() {
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
if (batchDeleteItems && batchDeleteItems.length) {
// Batch delete
let completed = 0;
function deleteNext() {
if (completed >= batchDeleteItems.length) {
fetchFiles();
batchDeleteItems = null;
fileToDelete = null;
fileToDeletePath = '';
deleteModal.hide();
document.getElementById('fileError').textContent = '';
return;
}
const item = batchDeleteItems[completed];
let url = `/api/rooms/${roomId}/files/${encodeURIComponent(item.name)}`;
if (item.path) url += `?path=${encodeURIComponent(item.path)}`;
fetch(url, {
method: 'DELETE',
headers: { 'X-CSRFToken': csrfToken }
})
.then(r => r.json())
.then(() => {
completed++;
deleteNext();
})
.catch(() => {
completed++;
deleteNext();
});
}
deleteNext();
return;
}
if (!fileToDelete) return;
let url = `/api/rooms/${roomId}/files/${encodeURIComponent(fileToDelete)}`;
if (fileToDeletePath) url += `?path=${encodeURIComponent(fileToDeletePath)}`;
fetch(url, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken
}
})
.then(r => r.json())
.then(res => {
if (res.success) {
fetchFiles();
fileToDelete = null;
fileToDeletePath = '';
deleteModal.hide();
document.getElementById('fileError').textContent = '';
} else {
document.getElementById('fileError').textContent = res.error || 'Delete failed.';
}
})
.catch(() => {
document.getElementById('fileError').textContent = 'Delete failed.';
});
}
function showRenameModal(filename) {
renameTarget = filename;
document.getElementById('renameError').textContent = '';
// Determine if file or folder by extension
const ext = filename.includes('.') ? filename.substring(filename.lastIndexOf('.')) : '';
const isFile = ext && filename.lastIndexOf('.') > 0;
renameIsFile = isFile;
renameOrigExt = ext;
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" />`;
}
var modal = new bootstrap.Modal(document.getElementById('renameModal'));
modal.show();
}
document.getElementById('confirmRenameBtn').addEventListener('click', function() {
if (!renameTarget) return;
let newName = document.getElementById('renameInput').value.trim();
if (renameIsFile) {
if (!newName) {
document.getElementById('renameError').textContent = 'New name is required.';
return;
}
newName = newName + renameOrigExt;
}
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
if (!newName) {
document.getElementById('renameError').textContent = 'New name is required.';
return;
}
fetch(`/api/rooms/${roomId}/rename`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
old_name: renameTarget,
new_name: newName,
path: currentPath
})
})
.then(r => r.json())
.then(res => {
if (res.success) {
fetchFiles();
var modalEl = document.getElementById('renameModal');
var modal = bootstrap.Modal.getInstance(modalEl);
modal.hide();
} else {
document.getElementById('renameError').textContent = res.error || 'Rename failed.';
}
})
.catch(() => {
document.getElementById('renameError').textContent = 'Rename failed.';
});
});
function updateMultiSelectUI() {
const checkboxes = document.querySelectorAll('.select-item-checkbox');
selectedItems = Array.from(checkboxes)
.map((cb, idx) => ({ checked: cb.checked, idx }))
.filter(item => item.checked)
.map(item => item.idx);
const selectedCount = selectedItems.length;
const downloadBtn = document.getElementById('downloadSelectedBtn');
const deleteBtn = document.getElementById('deleteSelectedBtn');
if (downloadBtn) downloadBtn.style.display = selectedCount > 0 ? 'flex' : 'none';
if (deleteBtn) deleteBtn.style.display = selectedCount > 0 ? 'flex' : 'none';
}
function toggleStar(filename, path) {
console.log('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
starButton.style.backgroundColor = isStarred ? 'rgba(22,118,123,0.08)' : 'rgba(255,215,0,0.15)';
starButton.style.color = isStarred ? '#16767b' : '#ffd700';
starButton.title = isStarred ? 'Star' : 'Unstar';
}
fetch(`/api/rooms/${roomId}/star`, {
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}`);
}
return response.json();
})
.then(data => {
console.log('Star toggle response:', data);
if (data.success) {
// Update the file's starred status in currentFiles
const fileIndex = currentFiles.findIndex(f => f.name === filename && f.path === path);
if (fileIndex !== -1) {
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 ? 'rgba(22,118,123,0.08)' : 'rgba(255,215,0,0.15)';
starButton.style.color = isStarred ? '#16767b' : '#ffd700';
starButton.title = isStarred ? 'Star' : 'Unstar';
}
console.error('Failed to toggle star:', data.error);
}
})
.catch(error => {
// Revert the button if there was an error
if (starButton) {
const isStarred = starButton.title === 'Unstar';
starButton.style.backgroundColor = isStarred ? 'rgba(22,118,123,0.08)' : 'rgba(255,215,0,0.15)';
starButton.style.color = isStarred ? '#16767b' : '#ffd700';
starButton.title = isStarred ? 'Star' : 'Unstar';
}
console.error('Error toggling star:', error);
});
}
function showMoveModal(filename, path) {
fileToMove = filename;
fileToMovePath = path;
// Clear any previous errors
document.getElementById('moveError').textContent = '';
// Fetch available folders
fetch(`/api/rooms/${roomId}/folders`)
.then(r => r.json())
.then(folders => {
const select = document.getElementById('moveTargetFolder');
select.innerHTML = '<option value="">Root Folder</option>';
// Sort folders by path to ensure proper hierarchy
folders.sort((a, b) => {
if (!a) return -1;
if (!b) return 1;
return a.localeCompare(b);
});
// Add all folders except the current one
folders.forEach(folderPath => {
// Skip the current folder and its subfolders
if (folderPath === path || (path && folderPath.startsWith(path + '/'))) {
return;
}
// Create a display name that shows the folder hierarchy
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>`;
});
// Show the modal
moveModal.show();
})
.catch(error => {
document.getElementById('moveError').textContent = 'Failed to load folders.';
console.error('Error loading folders:', error);
});
}
function moveFileConfirmed() {
const targetPath = document.getElementById('moveTargetFolder').value;
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch(`/api/rooms/${roomId}/move`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
filename: fileToMove,
source_path: fileToMovePath,
target_path: targetPath
})
})
.then(r => r.json())
.then(res => {
if (res.success) {
fetchFiles();
fileToMove = null;
fileToMovePath = '';
moveModal.hide();
document.getElementById('moveError').textContent = '';
} else {
document.getElementById('moveError').textContent = res.error || 'Move failed.';
}
})
.catch(() => {
document.getElementById('moveError').textContent = 'Move failed.';
});
}
document.addEventListener('DOMContentLoaded', function() {
currentPath = getPathFromUrl();
initializeView();
updateMultiSelectUI();
// Initialize the overwrite modal with static backdrop
overwriteModal = new bootstrap.Modal(document.getElementById('overwriteConfirmModal'), {
backdrop: 'static',
keyboard: false
});
// Initialize new folder modal
const newFolderModal = new bootstrap.Modal(document.getElementById('newFolderModal'));
// Initialize move modal
moveModal = new bootstrap.Modal(document.getElementById('moveModal'));
// Add click handler for move confirmation button
document.getElementById('confirmMoveBtn').addEventListener('click', moveFileConfirmed);
// Add click handler for new folder button
document.getElementById('newFolderBtn').addEventListener('click', function() {
document.getElementById('folderNameInput').value = '';
document.getElementById('folderError').textContent = '';
newFolderModal.show();
});
// Add click handler for create folder button
document.getElementById('createFolderBtn').addEventListener('click', function() {
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');
fetch(`/api/rooms/${roomId}/folders`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
name: folderName,
path: currentPath
})
})
.then(r => r.json())
.then(res => {
if (res.success) {
newFolderModal.hide();
fetchFiles();
} else {
if (res.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 (res.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 = res.error || 'Failed to create folder.';
}
}
})
.catch(() => {
document.getElementById('folderError').textContent = 'Failed to create folder.';
});
});
if (canUpload === true || canUpload === 'true') {
const uploadBtn = document.getElementById('uploadBtn');
const fileInput = document.getElementById('fileInput');
const uploadForm = document.getElementById('uploadForm');
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const fileGrid = document.getElementById('fileGrid');
const dropZoneOverlay = document.getElementById('dropZoneOverlay');
const uploadProgressContainer = document.getElementById('uploadProgressContainer');
const uploadProgressBar = document.getElementById('uploadProgressBar');
const uploadProgressText = document.getElementById('uploadProgressText');
let pendingUploads = [];
let pendingUploadIdx = 0;
let overwriteAll = false;
let skipAll = false;
// Prevent default drag behaviors
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
fileGrid.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
// Highlight drop zone when item is dragged over it
['dragenter', 'dragover'].forEach(eventName => {
fileGrid.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
fileGrid.addEventListener(eventName, unhighlight, false);
});
// Handle dropped files
fileGrid.addEventListener('drop', handleDrop, false);
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
function highlight(e) {
dropZoneOverlay.style.display = 'block';
}
function unhighlight(e) {
dropZoneOverlay.style.display = 'none';
}
async function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
uploadProgressContainer.style.display = 'block';
uploadProgressBar.style.width = '0%';
uploadProgressBar.classList.remove('bg-success');
uploadProgressBar.classList.add('bg-info');
uploadProgressText.textContent = '';
pendingUploads = Array.from(files);
pendingUploadIdx = 0;
overwriteAll = false;
skipAll = false;
await uploadFilesSequentially();
}
}
uploadBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', async function() {
if (!fileInput.files.length) return;
const files = Array.from(fileInput.files);
uploadProgressContainer.style.display = 'block';
uploadProgressBar.style.width = '0%';
uploadProgressBar.classList.remove('bg-success');
uploadProgressBar.classList.add('bg-info');
uploadProgressText.textContent = '';
pendingUploads = files;
pendingUploadIdx = 0;
overwriteAll = false;
skipAll = false;
await uploadFilesSequentially();
});
async function uploadFilesSequentially() {
console.log('[Upload] Starting uploadFilesSequentially');
let completedFiles = 0;
let currentFileIndex = 0;
const updateProgress = () => {
if (!pendingUploads || currentFileIndex >= pendingUploads.length) {
// All files processed, set to 100%
uploadProgressBar.style.width = '100%';
uploadProgressBar.textContent = '100%';
uploadProgressText.textContent = 'Upload complete!';
return;
}
const progress = Math.round((completedFiles / pendingUploads.length) * 100);
uploadProgressBar.style.width = progress + '%';
uploadProgressBar.textContent = progress + '%';
uploadProgressText.textContent = `Uploading ${pendingUploads[currentFileIndex].name} (${currentFileIndex + 1}/${pendingUploads.length})`;
};
const processNextFile = async () => {
if (currentFileIndex >= pendingUploads.length) {
// All files processed
uploadProgressBar.style.width = '100%';
uploadProgressBar.textContent = '100%';
uploadProgressBar.classList.remove('bg-info');
uploadProgressBar.classList.add('bg-success');
uploadProgressText.textContent = 'Upload complete!';
// Reset state
pendingUploads = null;
pendingUploadIdx = null;
overwriteAll = false;
skipAll = false;
// Hide progress after delay
setTimeout(() => {
uploadProgressContainer.style.display = 'none';
uploadProgressText.textContent = '';
uploadProgressBar.style.backgroundColor = '#16767b';
uploadProgressBar.style.color = '#fff';
}, 3000);
// Refresh file list
await fetchFiles();
return;
}
const file = pendingUploads[currentFileIndex];
console.log(`[Upload] Processing file ${currentFileIndex + 1}/${pendingUploads.length}: ${file.name}`);
const formData = new FormData(uploadForm);
if (currentPath) {
formData.append('path', currentPath);
}
formData.set('file', file);
try {
updateProgress();
let uploadFormData = formData;
if (overwriteAll) {
console.log(`[Upload] Using overwrite for file: ${file.name}`);
uploadFormData = new FormData(uploadForm);
if (currentPath) {
uploadFormData.append('path', currentPath);
}
uploadFormData.set('file', file);
uploadFormData.append('overwrite', 'true');
}
const response = await fetch(`/api/rooms/${roomId}/files/upload`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken },
body: uploadFormData
});
const resJson = await response.json();
if (response.ok) {
console.log(`[Upload] Success: ${file.name}`);
completedFiles++;
currentFileIndex++;
updateProgress();
await processNextFile();
} else if (response.status === 400 && resJson.error === 'File type not allowed') {
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');
// Append new error message
const newError = `<div class="mb-2"><strong>File type not allowed:</strong> ${file.name}</div>`;
if (uploadErrorContent.innerHTML === '') {
// First error, add the allowed types list
uploadErrorContent.innerHTML = newError + `<div class='mt-2'><strong>Allowed file types:</strong><br>${allowedTypes}</div>`;
} else {
// Append to existing errors
uploadErrorContent.innerHTML = newError + uploadErrorContent.innerHTML;
}
uploadError.style.display = 'block';
uploadProgressBar.style.backgroundColor = '#ef4444';
uploadProgressBar.style.color = '#fff';
currentFileIndex++;
updateProgress();
await processNextFile();
} else if (response.status === 409) {
console.log(`[Upload] Conflict for ${file.name}:`, resJson.error);
if (resJson.error.includes('exists in the trash')) {
console.log('[Upload] Auto-overwriting trashed file');
uploadFormData.append('overwrite', 'true');
const retryResponse = await fetch(`/api/rooms/${roomId}/files/upload`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken },
body: uploadFormData
});
if (retryResponse.ok) {
completedFiles++;
currentFileIndex++;
updateProgress();
await processNextFile();
}
return;
}
if (overwriteAll) {
uploadFormData.append('overwrite', 'true');
const retryResponse = await fetch(`/api/rooms/${roomId}/files/upload`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken },
body: uploadFormData
});
if (retryResponse.ok) {
completedFiles++;
currentFileIndex++;
updateProgress();
await processNextFile();
}
return;
}
if (skipAll) {
console.log(`[Upload] Skip All is set, skipping conflicting file: ${file.name}`);
completedFiles++;
currentFileIndex++;
updateProgress();
await processNextFile();
return;
}
uploadProgressText.textContent = resJson.error || `Conflict for ${file.name}`;
uploadProgressBar.style.backgroundColor = '#ef4444';
uploadProgressBar.style.color = '#fff';
const userChoice = await showOverwriteModal(file.name);
console.log(`[Upload] Modal choice for ${file.name}:`, userChoice);
if (userChoice === 'overwrite' || userChoice === 'overwrite_all') {
if (userChoice === 'overwrite_all') {
overwriteAll = true;
}
uploadFormData.append('overwrite', 'true');
const retryResponse = await fetch(`/api/rooms/${roomId}/files/upload`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken },
body: uploadFormData
});
if (retryResponse.ok) {
completedFiles++;
currentFileIndex++;
updateProgress();
await processNextFile();
}
} else if (userChoice === 'skip' || userChoice === 'skip_all') {
if (userChoice === 'skip_all') {
skipAll = true;
}
completedFiles++;
currentFileIndex++;
updateProgress();
await processNextFile();
}
} else {
console.error(`[Upload] Error for ${file.name}:`, resJson.error);
uploadProgressText.textContent = resJson.error || `Error uploading ${file.name}`;
uploadProgressBar.style.backgroundColor = '#ef4444';
uploadProgressBar.style.color = '#fff';
currentFileIndex++;
updateProgress();
await processNextFile();
}
} catch (err) {
console.error('[Upload] Upload error:', err);
uploadProgressText.textContent = `Error uploading ${file.name}`;
uploadProgressBar.style.backgroundColor = '#ef4444';
uploadProgressBar.style.color = '#fff';
currentFileIndex++;
updateProgress();
await processNextFile();
}
};
// Start processing files
await processNextFile();
}
// Robust modal show logic
function showOverwriteModal(filename) {
console.log('[Modal] showOverwriteModal called for', filename);
// If there's an existing modal promise, resolve it with skip
if (modalResolve) {
modalResolve('skip');
modalResolve = null;
}
// Create a new promise for this modal interaction
modalPromise = new Promise((resolve) => {
modalResolve = resolve;
// Update modal content
document.getElementById('overwriteFileName').textContent = filename;
// Get modal elements
const modalEl = document.getElementById('overwriteConfirmModal');
// Ensure modal is properly initialized with static backdrop
if (!overwriteModal) {
overwriteModal = new bootstrap.Modal(modalEl, {
backdrop: 'static',
keyboard: false
});
}
// Get fresh button references
const confirmOverwriteBtn = document.getElementById('confirmOverwriteBtn');
const skipOverwriteBtn = document.getElementById('skipOverwriteBtn');
const confirmAllOverwriteBtn = document.getElementById('confirmAllOverwriteBtn');
const skipAllOverwriteBtn = document.getElementById('skipAllOverwriteBtn');
// Remove any existing event listeners by cloning and replacing
const newConfirmOverwriteBtn = confirmOverwriteBtn.cloneNode(true);
const newSkipOverwriteBtn = skipOverwriteBtn.cloneNode(true);
const newConfirmAllOverwriteBtn = confirmAllOverwriteBtn.cloneNode(true);
const newSkipAllOverwriteBtn = skipAllOverwriteBtn.cloneNode(true);
confirmOverwriteBtn.parentNode.replaceChild(newConfirmOverwriteBtn, confirmOverwriteBtn);
skipOverwriteBtn.parentNode.replaceChild(newSkipOverwriteBtn, skipOverwriteBtn);
confirmAllOverwriteBtn.parentNode.replaceChild(newConfirmAllOverwriteBtn, confirmAllOverwriteBtn);
skipAllOverwriteBtn.parentNode.replaceChild(newSkipAllOverwriteBtn, skipAllOverwriteBtn);
let modalClosed = false;
// Function to handle modal resolution
const resolveModal = (choice) => {
if (!modalClosed) {
modalClosed = true;
if (modalResolve) {
modalResolve(choice);
modalResolve = null;
}
overwriteModal.hide();
}
};
// Add click handlers directly to the buttons
newConfirmOverwriteBtn.onclick = () => {
console.log('[Modal] Overwrite clicked');
resolveModal('overwrite');
};
newSkipOverwriteBtn.onclick = () => {
console.log('[Modal] Skip clicked');
resolveModal('skip');
};
newConfirmAllOverwriteBtn.onclick = () => {
console.log('[Modal] Overwrite All clicked');
overwriteAll = true;
resolveModal('overwrite_all');
};
newSkipAllOverwriteBtn.onclick = () => {
console.log('[Modal] Skip All clicked');
skipAll = true;
resolveModal('skip_all');
};
// Prevent modal from being closed by clicking outside or pressing escape
modalEl.addEventListener('click', (e) => {
if (e.target === modalEl) {
e.preventDefault();
e.stopPropagation();
}
});
// Add modal close handler
const handleModalClose = (e) => {
if (!modalClosed && modalResolve) {
console.log('[Modal] Modal closed without explicit choice');
e.preventDefault();
e.stopPropagation();
// Re-show the modal if it was closed without a choice
overwriteModal.show();
}
};
// Remove any existing close handler and add new one
modalEl.removeEventListener('hidden.bs.modal', handleModalClose);
modalEl.addEventListener('hidden.bs.modal', handleModalClose);
// Show the modal
overwriteModal.show();
// Ensure modal is visible and focused
modalEl.style.display = 'block';
modalEl.classList.add('show');
modalEl.focus();
});
return modalPromise;
}
}
});
function navigateToParent() {
if (!currentPath) return;
const parts = currentPath.split('/');
parts.pop(); // Remove the last part
const parentPath = parts.join('/');
navigateTo(parentPath);
}
</script>
{% endblock %}