Files
docupulse/templates/starred.html
2025-05-25 10:31:22 +02:00

468 lines
20 KiB
HTML

{% extends "base.html" %}
{% block title %}Starred - 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>Starred Items</h2>
<div class="text-muted">Your starred files and folders from all rooms</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>
<div class="card-body">
<div class="d-flex align-items-center gap-2 mb-3">
<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 row-cols-1 row-cols-md-2 row-cols-lg-4 g-4"></div>
<div id="fileError" class="text-danger mt-2"></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>
<script>
let currentView = 'grid';
let lastSelectedIndex = -1;
let sortColumn = 'name'; // Set default sort column to name
let sortDirection = 1; // 1 for ascending, -1 for descending
let batchDeleteItems = null;
let currentFiles = [];
// Initialize the view and fetch files
async function initializeView() {
try {
const response = await fetch('/api/user/preferred_view');
const data = await response.json();
currentView = data.preferred_view || 'grid';
} catch (error) {
console.error('Error fetching preferred view:', error);
currentView = 'grid';
}
// First fetch files
await fetchFiles();
// Then toggle view after files are loaded
toggleView(currentView);
// Sort files by name by default
sortFiles('name');
}
function formatDate(ts) {
if (!ts) return '';
const d = new Date(ts * 1000);
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
}
function toggleView(view) {
currentView = view;
const grid = document.getElementById('fileGrid');
const gridBtn = document.getElementById('gridViewBtn');
const listBtn = document.getElementById('listViewBtn');
if (view === 'grid') {
grid.classList.remove('table-mode');
gridBtn.classList.add('active');
listBtn.classList.remove('active');
} else {
grid.classList.add('table-mode');
gridBtn.classList.remove('active');
listBtn.classList.add('active');
}
renderFiles(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;
}
currentFiles.sort((a, b) => {
let valA = a[column];
let valB = b[column];
// For size, convert to number
if (column === 'size') {
valA = typeof valA === 'number' ? valA : 0;
valB = typeof valB === 'number' ? valB : 0;
}
// For name/type, compare as strings
if (typeof valA === 'string' && typeof valB === 'string') {
return valA.localeCompare(valB) * sortDirection;
}
// For date (modified), compare as numbers
return (valA - valB) * sortDirection;
});
renderFiles(currentFiles);
}
function renderFiles(files) {
if (!files) return;
currentFiles = files;
const grid = document.getElementById('fileGrid');
grid.innerHTML = '';
if (!files.length) {
grid.innerHTML = '<div class="col"><div class="text-muted">No starred items yet.</div></div>';
return;
}
if (currentView === 'list') {
let table = `<table><thead><tr>
<th></th>
<th>Room</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>`;
files.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 = [];
let dblClickAction = '';
if (file.type === 'folder') {
dblClickAction = `ondblclick=\"window.location.href='/room/${file.room_id}?path=${encodeURIComponent(file.path ? file.path + '/' + file.name : file.name)}'\"`;
} else {
dblClickAction = `ondblclick=\"window.location.href='/api/rooms/${file.room_id}/files/${encodeURIComponent(file.name)}?path=${encodeURIComponent(file.path)}'\"`;
}
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}", ${file.room_id})'><i class='fas fa-star'></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='event.stopPropagation();showDetailsModal(${idx})'><i class='fas fa-info-circle'></i></button>`);
const actions = actionsArr.join('');
table += `<tr ${dblClickAction} onclick='navigateToFile(${file.room_id}, "${file.name}", "${file.path}", "${file.type}")'>
<td class='file-icon'><span style="display:inline-flex;align-items:center;gap:1.2rem;">${icon}</span></td>
<td class='room-name'><button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='event.stopPropagation();window.location.href="/rooms/${file.room_id}"'><i class='fas fa-door-open me-1'></i>${file.room_name || 'Room ' + file.room_id}</button></td>
<td class='file-name' title='${file.name}'>${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;
} else {
files.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 = [];
let dblClickAction = '';
if (file.type === 'folder') {
dblClickAction = `ondblclick=\"window.location.href='/room/${file.room_id}?path=${encodeURIComponent(file.path ? file.path + '/' + file.name : file.name)}'\"`;
} else {
dblClickAction = `ondblclick=\"window.location.href='/api/rooms/${file.room_id}/files/${encodeURIComponent(file.name)}?path=${encodeURIComponent(file.path)}'\"`;
}
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}", ${file.room_id})'><i class='fas fa-star'></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='event.stopPropagation();showDetailsModal(${idx})'><i class='fas fa-info-circle'></i></button>`);
const actions = actionsArr.join('');
grid.innerHTML += `
<div class='col'>
<div class='card file-card h-100 border-0 shadow-sm position-relative' ${dblClickAction} onclick='navigateToFile(${file.room_id}, "${file.name}", "${file.path}", "${file.type}")'>
<div class='card-body d-flex flex-column align-items-center justify-content-center text-center'>
<div class='mb-2'>${icon}</div>
<div class='fw-semibold file-name-ellipsis' title='${file.name}'>${file.name}</div>
<div class='text-muted small'>${formatDate(file.modified)}</div>
<div class='text-muted small'>${size}</div>
<div class='mt-2'><button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='event.stopPropagation();window.location.href="/rooms/${file.room_id}"'><i class='fas fa-door-open me-1'></i>${file.room_name || 'Room ' + file.room_id}</button></div>
</div>
<div class='card-footer bg-white border-0 d-flex justify-content-center gap-2'>${actions}</div>
</div>
</div>`;
});
}
}
function fetchFiles() {
fetch('/api/rooms/starred')
.then(r => r.json())
.then(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 toggleStar(filename, path = '', roomId) {
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch(`/api/rooms/${roomId}/star`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
filename: filename,
path: path
})
})
.then(r => r.json())
.then(res => {
if (res.success) {
// Remove the file from the current view since it's no longer starred
currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId));
renderFiles(currentFiles);
} else {
console.error('Failed to toggle star:', res.error);
}
})
.catch(error => {
console.error('Error toggling star:', error);
});
}
function navigateToFile(roomId, filename, path, type) {
if (file.type === 'folder') {
window.location.href = `/room/${roomId}?path=${encodeURIComponent(path ? path + '/' + filename : filename)}`;
} else {
window.location.href = `/api/rooms/${roomId}/files/${encodeURIComponent(filename)}?path=${encodeURIComponent(path)}`;
}
}
function showDetailsModal(idx) {
const item = currentFiles[idx];
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';
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/${item.room_id}\"'><i class='fas fa-door-open me-1'></i>${item.room_name || 'Room ' + item.room_id}</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();
}
// Live search
document.addEventListener('DOMContentLoaded', function() {
initializeView();
const quickSearchInput = document.getElementById('quickSearchInput');
const clearSearchBtn = document.getElementById('clearSearchBtn');
let searchTimeout = null;
quickSearchInput.addEventListener('input', function() {
const query = quickSearchInput.value.trim().toLowerCase();
clearSearchBtn.style.display = query.length > 0 ? 'block' : 'none';
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
if (query.length === 0) {
fetchFiles();
return;
}
const filteredFiles = currentFiles.filter(file =>
file.name.toLowerCase().includes(query)
);
renderFiles(filteredFiles);
}, 200);
});
clearSearchBtn.addEventListener('click', function() {
quickSearchInput.value = '';
clearSearchBtn.style.display = 'none';
fetchFiles();
});
});
</script>
<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 .file-action-btn {
cursor: pointer;
}
#fileGrid.table-mode {
padding: 0;
}
#fileGrid.table-mode table {
width: 100%;
border-collapse: collapse;
background: #fff;
}
#fileGrid.table-mode th, #fileGrid.table-mode td {
padding: 0.5rem 1rem;
border-bottom: 1px solid #e9ecef;
text-align: left;
font-size: 0.95rem;
vertical-align: middle;
}
#fileGrid.table-mode th {
background: #f8f9fa;
color: #6c757d;
font-weight: 500;
}
#fileGrid.table-mode tr:hover td {
background-color: rgba(22, 118, 123, 0.08);
transition: background 0.15s;
}
#fileGrid.table-mode .file-icon {
width: 40px;
text-align: center;
}
#fileGrid.table-mode .file-actions {
min-width: 90px;
text-align: right;
}
#fileGrid.table-mode .file-action-btn {
opacity: 1;
pointer-events: auto;
min-width: 28px;
min-height: 28px;
font-size: 0.875rem;
margin-left: 0.25rem;
}
/* Disable text selection for file grid and table rows/cards */
#fileGrid, #fileGrid * {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
#fileGrid .card.file-card {
cursor: pointer;
}
#fileGrid.table-mode tr {
cursor: pointer;
}
.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;
}
</style>
{% endblock %}