545 lines
23 KiB
JavaScript
545 lines
23 KiB
JavaScript
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 = '';
|
|
this.fileToMove = null;
|
|
this.fileToMovePath = '';
|
|
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(filename, path = '') {
|
|
console.log('[FileManager] Deleting file:', { filename, path });
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
|
|
try {
|
|
let url = `/api/rooms/${this.roomManager.roomId}/files/${encodeURIComponent(filename)}`;
|
|
if (path) {
|
|
url += `?path=${encodeURIComponent(path)}`;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRFToken': csrfToken
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
console.log('[FileManager] File deleted successfully');
|
|
await this.fetchFiles();
|
|
// Clear any existing error message
|
|
const errorEl = document.getElementById('fileError');
|
|
if (errorEl) {
|
|
errorEl.textContent = '';
|
|
}
|
|
} else {
|
|
console.error('[FileManager] Failed to delete file:', result.error);
|
|
const errorEl = document.getElementById('fileError');
|
|
if (errorEl) {
|
|
errorEl.textContent = result.error || 'Failed to delete file.';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('[FileManager] Error deleting file:', error);
|
|
const errorEl = document.getElementById('fileError');
|
|
if (errorEl) {
|
|
errorEl.textContent = 'Failed to delete file. Please try again.';
|
|
}
|
|
}
|
|
}
|
|
|
|
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] Starting moveFile...');
|
|
console.log('[FileManager] Parameters:', { fileId, targetPath });
|
|
|
|
try {
|
|
const file = this.currentFiles.find(f => f.id === fileId);
|
|
console.log('[FileManager] Found file to move:', file);
|
|
|
|
if (!file) {
|
|
console.error('[FileManager] File not found with ID:', fileId);
|
|
throw new Error('File not found');
|
|
}
|
|
|
|
console.log('[FileManager] Sending move request...');
|
|
const response = await fetch(`/api/rooms/${this.roomManager.roomId}/move`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
|
},
|
|
body: JSON.stringify({
|
|
filename: file.name,
|
|
source_path: file.path || '',
|
|
target_path: targetPath
|
|
})
|
|
});
|
|
|
|
console.log('[FileManager] Move response status:', response.status);
|
|
const result = await response.json();
|
|
console.log('[FileManager] Move response data:', result);
|
|
|
|
if (result.success) {
|
|
console.log('[FileManager] Move successful, updating view...');
|
|
this.currentFiles = this.currentFiles.filter(f => f.id !== fileId);
|
|
await this.roomManager.viewManager.renderFiles(this.currentFiles);
|
|
console.log('[FileManager] View updated after move');
|
|
return { success: true, message: 'File moved successfully' };
|
|
} else {
|
|
console.error('[FileManager] Move failed:', result.message);
|
|
throw new Error(result.message || 'Failed to move file');
|
|
}
|
|
} catch (error) {
|
|
console.error('[FileManager] Error in moveFile:', error);
|
|
console.error('[FileManager] Error details:', {
|
|
message: error.message,
|
|
stack: error.stack
|
|
});
|
|
return { success: false, message: error.message };
|
|
}
|
|
}
|
|
|
|
async moveFileConfirmed() {
|
|
console.log('[FileManager] Starting moveFileConfirmed...');
|
|
console.log('[FileManager] Current state:', {
|
|
fileToMove: this.fileToMove,
|
|
fileToMovePath: this.fileToMovePath,
|
|
currentFiles: this.currentFiles
|
|
});
|
|
|
|
if (!this.fileToMove) {
|
|
console.error('[FileManager] No file selected for move operation');
|
|
document.getElementById('moveError').textContent = 'No file selected for move.';
|
|
return;
|
|
}
|
|
|
|
const targetPath = document.getElementById('moveTargetFolder').value;
|
|
console.log('[FileManager] Selected target path:', targetPath);
|
|
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
console.log('[FileManager] CSRF Token:', csrfToken ? 'Present' : 'Missing');
|
|
|
|
// Get the file from currentFiles using the filename
|
|
console.log('[FileManager] Attempting to find file with name:', this.fileToMove);
|
|
const file = this.currentFiles.find(f => f.name === this.fileToMove);
|
|
console.log('[FileManager] Found file:', file);
|
|
|
|
if (!file) {
|
|
console.error('[FileManager] File not found in currentFiles. Available files:', this.currentFiles);
|
|
document.getElementById('moveError').textContent = 'File not found.';
|
|
return;
|
|
}
|
|
|
|
console.log('[FileManager] Preparing move request with data:', {
|
|
filename: file.name,
|
|
source_path: this.fileToMovePath,
|
|
target_path: targetPath
|
|
});
|
|
|
|
try {
|
|
const url = `/api/rooms/${this.roomManager.roomId}/move`;
|
|
console.log('[FileManager] Sending request to:', url);
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
filename: file.name,
|
|
source_path: this.fileToMovePath,
|
|
target_path: targetPath
|
|
})
|
|
});
|
|
|
|
console.log('[FileManager] Response status:', response.status);
|
|
const result = await response.json();
|
|
console.log('[FileManager] Response data:', result);
|
|
|
|
if (result.success) {
|
|
console.log('[FileManager] Move successful, refreshing files...');
|
|
await this.fetchFiles();
|
|
this.fileToMove = null;
|
|
this.fileToMovePath = '';
|
|
this.roomManager.modalManager.moveModal.hide();
|
|
document.getElementById('moveError').textContent = '';
|
|
console.log('[FileManager] Move operation completed successfully');
|
|
} else {
|
|
console.error('[FileManager] Move failed:', result.error);
|
|
document.getElementById('moveError').textContent = result.error || 'Move failed.';
|
|
}
|
|
} catch (error) {
|
|
console.error('[FileManager] Error during move operation:', error);
|
|
console.error('[FileManager] Error details:', {
|
|
message: error.message,
|
|
stack: error.stack
|
|
});
|
|
document.getElementById('moveError').textContent = 'Move failed.';
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
async downloadFile(filename, path = '') {
|
|
console.log('[FileManager] Downloading file:', { filename, path });
|
|
const url = `/api/rooms/${this.roomManager.roomId}/files/${encodeURIComponent(filename)}`;
|
|
if (path) {
|
|
url += `?path=${encodeURIComponent(path)}`;
|
|
}
|
|
console.log('[FileManager] Download URL:', url);
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const downloadUrl = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = downloadUrl;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(downloadUrl);
|
|
document.body.removeChild(a);
|
|
console.log('[FileManager] Download initiated');
|
|
} catch (error) {
|
|
console.error('[FileManager] Error downloading file:', error);
|
|
document.getElementById('fileError').textContent = 'Failed to download file. Please try again.';
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Filter out folders and get only file IDs
|
|
const fileIds = selectedItems
|
|
.filter(item => item.type !== 'folder')
|
|
.map(item => item.id);
|
|
|
|
if (fileIds.length === 0) {
|
|
console.log('[FileManager] No files to download (only folders selected)');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/rooms/${this.roomManager.roomId}/download-zip`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
|
},
|
|
body: JSON.stringify({ items: selectedItems })
|
|
});
|
|
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);
|
|
document.getElementById('fileError').textContent = 'Failed to download files. Please try again.';
|
|
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.';
|
|
}
|
|
}
|
|
|
|
updateSelection(index, event) {
|
|
console.log('[FileManager] Updating selection:', { index, 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[index];
|
|
if (!checkbox) return;
|
|
|
|
if (event.ctrlKey) {
|
|
// CTRL + Click: Toggle individual selection
|
|
checkbox.checked = !checkbox.checked;
|
|
if (checkbox.checked) {
|
|
this.selectedItems.add(index);
|
|
} else {
|
|
this.selectedItems.delete(index);
|
|
}
|
|
} else if (event.shiftKey && this.lastSelectedIndex !== -1) {
|
|
// SHIFT + Click: Select range
|
|
const start = Math.min(this.lastSelectedIndex, index);
|
|
const end = Math.max(this.lastSelectedIndex, index);
|
|
for (let i = start; i <= end; i++) {
|
|
checkboxes[i].checked = true;
|
|
this.selectedItems.add(i);
|
|
}
|
|
} else {
|
|
// Normal click: Select single item
|
|
const wasChecked = checkbox.checked;
|
|
checkboxes.forEach(cb => {
|
|
cb.checked = false;
|
|
this.selectedItems.delete(parseInt(cb.dataset.index));
|
|
});
|
|
checkbox.checked = !wasChecked;
|
|
if (!wasChecked) {
|
|
this.selectedItems.add(index);
|
|
}
|
|
}
|
|
|
|
this.lastSelectedIndex = index;
|
|
this.roomManager.viewManager.updateMultiSelectUI();
|
|
}
|
|
|
|
getSelectedItems() {
|
|
console.log('[FileManager] Getting selected items');
|
|
return Array.from(this.selectedItems).map(index => this.currentFiles[index]);
|
|
}
|
|
|
|
clearSelection() {
|
|
console.log('[FileManager] Clearing selection');
|
|
this.selectedItems.clear();
|
|
this.lastSelectedIndex = -1;
|
|
const checkboxes = document.querySelectorAll('.select-item-checkbox');
|
|
checkboxes.forEach(cb => cb.checked = false);
|
|
this.roomManager.viewManager.updateMultiSelectUI();
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|