Files
docupulse/static/js/rooms/fileManager.js
2025-05-30 20:32:40 +02:00

639 lines
26 KiB
JavaScript

/**
* @fileoverview Manages file operations and state for the room interface.
* This file handles:
* - File fetching and state management
* - File operations (delete, rename, move, download)
* - File selection and batch operations
* - Star/unstar functionality
* - Navigation and path management
*/
/**
* @class FileManager
* @classdesc Manages all file-related operations and state in the room interface.
* Handles file operations, selection, and navigation.
*/
export class FileManager {
/**
* Creates a new FileManager instance.
* @param {RoomManager} roomManager - The parent RoomManager instance
*/
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);
}
/**
* Fetches files from the server for the current path.
* @async
* @returns {Promise<Array>} A promise that resolves with the array of files
* @throws {Error} If the fetch operation fails
*/
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;
}
}
/**
* Deletes a file from the server.
* @async
* @param {string} filename - The name of the file to delete
* @param {string} [path=''] - The path of the file to delete
* @throws {Error} If the delete operation fails
*/
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-CSRF-Token': 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.';
}
}
}
/**
* Renames a file on the server.
* @async
* @param {string} fileId - The ID of the file to rename
* @param {string} newName - The new name for the file
* @returns {Promise<Object>} A promise that resolves with the result of the rename operation
* @throws {Error} If the rename operation fails
*/
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 };
}
}
/**
* Moves a file to a new location.
* @async
* @param {string} fileId - The ID of the file to move
* @param {string} targetPath - The target path to move the file to
* @returns {Promise<Object>} A promise that resolves with the result of the move operation
* @throws {Error} If the move operation fails
*/
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 };
}
}
/**
* Handles the confirmed move operation after user confirmation.
* @async
* @throws {Error} If the move operation fails
*/
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-CSRF-Token': 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.';
}
}
/**
* Toggles the star status of a file.
* @async
* @param {string} filename - The name of the file to toggle star for
* @param {string} path - The path of the file
* @throws {Error} If the star toggle operation fails
*/
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-CSRF-Token': 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;
}
}
/**
* Downloads a single file.
* @async
* @param {string} filename - The name of the file to download
* @param {string} [path=''] - The path of the file
* @throws {Error} If the download operation fails
*/
async downloadFile(filename, path = '') {
console.log('[FileManager] Downloading file:', { filename, path });
let 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;
}
}
/**
* Downloads multiple selected files as a zip archive.
* @async
* @throws {Error} If the download operation fails
*/
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;
}
}
/**
* Handles the confirmed delete operation after user confirmation.
* Supports both single file and batch deletion.
* @async
* @throws {Error} If the delete operation fails
*/
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-CSRF-Token': 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-CSRF-Token': 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.';
}
}
/**
* Updates the file selection based on user interaction.
* Supports single selection, CTRL+click for multiple selection, and SHIFT+click for range selection.
* @param {number} index - The index of the file being selected
* @param {Event} event - The click event that triggered the selection
*/
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();
}
/**
* Gets an array of currently selected file objects.
* @returns {Array<Object>} Array of selected file objects
*/
getSelectedItems() {
console.log('[FileManager] Getting selected items');
return Array.from(this.selectedItems).map(index => this.currentFiles[index]);
}
/**
* Clears all file selections and updates the UI.
*/
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();
}
/**
* Navigates to the parent folder of the current path.
*/
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);
}
}