Started room separation
This commit is contained in:
310
static/css/room.css
Normal file
310
static/css/room.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -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 = '';
|
||||||
|
|||||||
342
static/js/rooms/fileManager.js
Normal file
342
static/js/rooms/fileManager.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
238
static/js/rooms/modalManager.js
Normal file
238
static/js/rooms/modalManager.js
Normal 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 = ' '.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
131
static/js/rooms/room.js
Normal 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);
|
||||||
|
}
|
||||||
61
static/js/rooms/searchManager.js
Normal file
61
static/js/rooms/searchManager.js
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
262
static/js/rooms/uploadManager.js
Normal file
262
static/js/rooms/uploadManager.js
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
357
static/js/rooms/viewManager.js
Normal file
357
static/js/rooms/viewManager.js
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
0
templates/rooms/roombefore.html
Normal file
0
templates/rooms/roombefore.html
Normal file
Reference in New Issue
Block a user