2025-11-09 23:29:07 +01:00
|
|
|
import { api } from '../api.js';
|
|
|
|
|
|
|
|
|
|
export class FileList extends HTMLElement {
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.currentFolderId = null;
|
2025-11-10 15:46:40 +01:00
|
|
|
this.folderPath = [];
|
2025-11-09 23:29:07 +01:00
|
|
|
this.files = [];
|
|
|
|
|
this.folders = [];
|
2025-11-10 01:58:41 +01:00
|
|
|
this.selectedFiles = new Set();
|
|
|
|
|
this.selectedFolders = new Set();
|
2025-11-10 15:46:40 +01:00
|
|
|
this.boundHandleClick = this.handleClick.bind(this);
|
|
|
|
|
this.boundHandleDblClick = this.handleDblClick.bind(this);
|
|
|
|
|
this.boundHandleChange = this.handleChange.bind(this);
|
2025-11-09 23:29:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async connectedCallback() {
|
2025-11-10 15:46:40 +01:00
|
|
|
this.addEventListener('click', this.boundHandleClick);
|
|
|
|
|
this.addEventListener('dblclick', this.boundHandleDblClick);
|
|
|
|
|
this.addEventListener('change', this.boundHandleChange);
|
2025-11-12 05:05:48 +01:00
|
|
|
if (!this.hasAttribute('data-search-mode')) {
|
|
|
|
|
await this.loadContents(null);
|
|
|
|
|
}
|
2025-11-09 23:29:07 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
disconnectedCallback() {
|
|
|
|
|
this.removeEventListener('click', this.boundHandleClick);
|
|
|
|
|
this.removeEventListener('dblclick', this.boundHandleDblClick);
|
|
|
|
|
this.removeEventListener('change', this.boundHandleChange);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-09 23:29:07 +01:00
|
|
|
async loadContents(folderId) {
|
|
|
|
|
this.currentFolderId = folderId;
|
|
|
|
|
try {
|
2025-11-10 15:46:40 +01:00
|
|
|
if (folderId) {
|
|
|
|
|
try {
|
|
|
|
|
this.folderPath = await api.getFolderPath(folderId);
|
|
|
|
|
} catch (pathError) {
|
|
|
|
|
this.folderPath = [];
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.folderPath = [];
|
|
|
|
|
}
|
2025-11-09 23:29:07 +01:00
|
|
|
this.folders = await api.listFolders(folderId);
|
|
|
|
|
this.files = await api.listFiles(folderId);
|
2025-11-10 01:58:41 +01:00
|
|
|
this.selectedFiles.clear();
|
|
|
|
|
this.selectedFolders.clear();
|
2025-11-09 23:29:07 +01:00
|
|
|
this.render();
|
|
|
|
|
} catch (error) {
|
2025-11-10 01:58:41 +01:00
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
2025-11-10 15:46:40 +01:00
|
|
|
detail: { message: 'Failed to load folder contents', type: 'error' }
|
2025-11-10 01:58:41 +01:00
|
|
|
}));
|
2025-11-10 15:46:40 +01:00
|
|
|
this.folderPath = [];
|
|
|
|
|
this.folders = [];
|
|
|
|
|
this.files = [];
|
|
|
|
|
this.render();
|
2025-11-09 23:29:07 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setFiles(files) {
|
|
|
|
|
this.files = files;
|
|
|
|
|
this.folders = [];
|
2025-11-10 01:58:41 +01:00
|
|
|
this.selectedFiles.clear();
|
|
|
|
|
this.selectedFolders.clear();
|
2025-11-09 23:29:07 +01:00
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
2025-11-10 01:58:41 +01:00
|
|
|
const hasSelected = this.selectedFiles.size > 0 || this.selectedFolders.size > 0;
|
2025-11-10 15:46:40 +01:00
|
|
|
const totalItems = this.files.length + this.folders.length;
|
|
|
|
|
const totalSelected = this.selectedFiles.size + this.selectedFolders.size;
|
2025-11-10 01:58:41 +01:00
|
|
|
const allFilesSelected = this.files.length > 0 && this.selectedFiles.size === this.files.length;
|
|
|
|
|
const allFoldersSelected = this.folders.length > 0 && this.selectedFolders.size === this.folders.length;
|
2025-11-10 15:46:40 +01:00
|
|
|
const allSelected = totalItems > 0 && allFilesSelected && allFoldersSelected;
|
|
|
|
|
const someSelected = hasSelected && !allSelected;
|
2025-11-10 01:58:41 +01:00
|
|
|
|
2025-11-09 23:29:07 +01:00
|
|
|
this.innerHTML = `
|
|
|
|
|
<div class="file-list-container">
|
2025-11-10 15:46:40 +01:00
|
|
|
${this.folderPath.length > 0 ? `
|
|
|
|
|
<div class="breadcrumb-nav">
|
|
|
|
|
<span class="breadcrumb-item" data-folder-id="null">Home</span>
|
|
|
|
|
${this.folderPath.map((folder, index) => `
|
|
|
|
|
<span class="breadcrumb-separator">/</span>
|
|
|
|
|
<span class="breadcrumb-item ${index === this.folderPath.length - 1 ? 'breadcrumb-current' : ''}" data-folder-id="${folder.id}">${folder.name}</span>
|
|
|
|
|
`).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
2025-11-09 23:29:07 +01:00
|
|
|
<div class="file-list-header">
|
2025-11-10 15:46:40 +01:00
|
|
|
<div class="header-left">
|
|
|
|
|
<h2>Files</h2>
|
|
|
|
|
${totalItems > 0 ? `
|
|
|
|
|
<div class="selection-controls">
|
|
|
|
|
<input type="checkbox" id="select-all" ${allSelected ? 'checked' : ''} ${someSelected ? 'data-indeterminate="true"' : ''}>
|
|
|
|
|
<label for="select-all">
|
|
|
|
|
${hasSelected ? `${totalSelected} selected` : 'Select all'}
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
</div>
|
2025-11-09 23:29:07 +01:00
|
|
|
<div class="file-actions">
|
|
|
|
|
<button class="button" id="create-folder-btn">New Folder</button>
|
|
|
|
|
<button class="button button-primary" id="upload-btn">Upload</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
${hasSelected ? `
|
|
|
|
|
<div class="batch-actions">
|
|
|
|
|
<button class="button button-small button-danger" id="batch-delete-btn">Delete</button>
|
|
|
|
|
<button class="button button-small" id="batch-move-btn">Move</button>
|
|
|
|
|
<button class="button button-small" id="batch-copy-btn">Copy</button>
|
|
|
|
|
<button class="button button-small" id="batch-star-btn">Star</button>
|
|
|
|
|
<button class="button button-small" id="batch-unstar-btn">Unstar</button>
|
|
|
|
|
<button class="button button-small" id="clear-selection-btn">Clear Selection</button>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
2025-11-10 01:58:41 +01:00
|
|
|
|
2025-11-12 04:48:43 +01:00
|
|
|
${totalItems === 0 ? '<p class="empty-state">No files found.</p>' : ''}
|
2025-11-09 23:29:07 +01:00
|
|
|
<div class="file-grid">
|
|
|
|
|
${this.folders.map(folder => this.renderFolder(folder)).join('')}
|
|
|
|
|
${this.files.map(file => this.renderFile(file)).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
this.attachListeners();
|
2025-11-10 15:46:40 +01:00
|
|
|
this.updateIndeterminateState();
|
2025-11-09 23:29:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderFolder(folder) {
|
2025-11-10 01:58:41 +01:00
|
|
|
const isSelected = this.selectedFolders.has(folder.id);
|
|
|
|
|
const starIcon = folder.is_starred ? '★' : '☆'; // Filled star or empty star
|
|
|
|
|
const starAction = folder.is_starred ? 'unstar-folder' : 'star-folder';
|
2025-11-09 23:29:07 +01:00
|
|
|
return `
|
|
|
|
|
<div class="file-item folder-item" data-folder-id="${folder.id}">
|
2025-11-10 01:58:41 +01:00
|
|
|
<input type="checkbox" class="select-item" data-type="folder" data-id="${folder.id}" ${isSelected ? 'checked' : ''}>
|
2025-11-09 23:29:07 +01:00
|
|
|
<div class="file-icon">📁</div>
|
|
|
|
|
<div class="file-name">${folder.name}</div>
|
|
|
|
|
<div class="file-actions-menu">
|
|
|
|
|
<button class="action-btn" data-action="delete-folder" data-id="${folder.id}">Delete</button>
|
2025-11-10 01:58:41 +01:00
|
|
|
<button class="action-btn star-btn" data-action="${starAction}" data-id="${folder.id}">${starIcon}</button>
|
2025-11-09 23:29:07 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderFile(file) {
|
2025-11-10 01:58:41 +01:00
|
|
|
const isSelected = this.selectedFiles.has(file.id);
|
2025-11-09 23:29:07 +01:00
|
|
|
const icon = this.getFileIcon(file.mime_type);
|
|
|
|
|
const size = this.formatFileSize(file.size);
|
2025-11-10 01:58:41 +01:00
|
|
|
const starIcon = file.is_starred ? '★' : '☆'; // Filled star or empty star
|
|
|
|
|
const starAction = file.is_starred ? 'unstar-file' : 'star-file';
|
2025-11-09 23:29:07 +01:00
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<div class="file-item" data-file-id="${file.id}">
|
2025-11-10 01:58:41 +01:00
|
|
|
<input type="checkbox" class="select-item" data-type="file" data-id="${file.id}" ${isSelected ? 'checked' : ''}>
|
2025-11-09 23:29:07 +01:00
|
|
|
<div class="file-icon">${icon}</div>
|
|
|
|
|
<div class="file-name">${file.name}</div>
|
|
|
|
|
<div class="file-size">${size}</div>
|
|
|
|
|
<div class="file-actions-menu">
|
|
|
|
|
<button class="action-btn" data-action="download" data-id="${file.id}">Download</button>
|
|
|
|
|
<button class="action-btn" data-action="rename" data-id="${file.id}">Rename</button>
|
|
|
|
|
<button class="action-btn" data-action="delete" data-id="${file.id}">Delete</button>
|
|
|
|
|
<button class="action-btn" data-action="share" data-id="${file.id}">Share</button>
|
2025-11-10 01:58:41 +01:00
|
|
|
<button class="action-btn star-btn" data-action="${starAction}" data-id="${file.id}">${starIcon}</button>
|
2025-11-09 23:29:07 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getFileIcon(mimeType) {
|
|
|
|
|
if (mimeType.startsWith('image/')) return '📷';
|
|
|
|
|
if (mimeType.startsWith('video/')) return '🎥';
|
|
|
|
|
if (mimeType.startsWith('audio/')) return '🎵';
|
|
|
|
|
if (mimeType.includes('pdf')) return '📄';
|
|
|
|
|
if (mimeType.includes('text')) return '📄';
|
|
|
|
|
return '📄';
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
isEditableFile(filename, mimeType) {
|
|
|
|
|
if (mimeType && mimeType.startsWith('text/')) return true;
|
|
|
|
|
|
|
|
|
|
const editableExtensions = [
|
|
|
|
|
'txt', 'md', 'log', 'json', 'js', 'py', 'html', 'css',
|
|
|
|
|
'xml', 'yaml', 'yml', 'sh', 'bat', 'ini', 'conf', 'cfg'
|
|
|
|
|
];
|
|
|
|
|
const extension = filename.split('.').pop().toLowerCase();
|
|
|
|
|
return editableExtensions.includes(extension);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-09 23:29:07 +01:00
|
|
|
formatFileSize(bytes) {
|
|
|
|
|
if (bytes < 1024) return bytes + ' B';
|
|
|
|
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
|
|
|
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
|
|
|
|
|
return (bytes / 1073741824).toFixed(1) + ' GB';
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
handleClick(e) {
|
|
|
|
|
const target = e.target;
|
2025-11-09 23:29:07 +01:00
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
if (target.classList.contains('breadcrumb-item') && !target.classList.contains('breadcrumb-current')) {
|
|
|
|
|
const folderId = target.dataset.folderId;
|
|
|
|
|
const targetFolderId = folderId === 'null' ? null : parseInt(folderId);
|
|
|
|
|
this.loadContents(targetFolderId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-09 23:29:07 +01:00
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
if (target.id === 'upload-btn') {
|
|
|
|
|
this.dispatchEvent(new CustomEvent('upload-request', {
|
|
|
|
|
detail: { folderId: this.currentFolderId }
|
|
|
|
|
}));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-09 23:29:07 +01:00
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
if (target.id === 'create-folder-btn') {
|
|
|
|
|
this.handleCreateFolder();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-09 23:29:07 +01:00
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
if (target.id === 'clear-selection-btn') {
|
|
|
|
|
this.clearSelection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-10 01:58:41 +01:00
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
if (target.id === 'batch-delete-btn') {
|
|
|
|
|
this.handleBatchAction('delete');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (target.id === 'batch-move-btn') {
|
|
|
|
|
this.handleBatchAction('move');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (target.id === 'batch-copy-btn') {
|
|
|
|
|
this.handleBatchAction('copy');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (target.id === 'batch-star-btn') {
|
|
|
|
|
this.handleBatchAction('star');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (target.id === 'batch-unstar-btn') {
|
|
|
|
|
this.handleBatchAction('unstar');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-10 01:58:41 +01:00
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
if (target.classList.contains('action-btn')) {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const action = target.dataset.action;
|
|
|
|
|
const id = parseInt(target.dataset.id);
|
|
|
|
|
this.handleAction(action, id);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (target.classList.contains('select-item')) {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-10 01:58:41 +01:00
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
const fileItem = target.closest('.file-item:not(.folder-item)');
|
|
|
|
|
if (fileItem) {
|
|
|
|
|
const fileId = parseInt(fileItem.dataset.fileId);
|
|
|
|
|
const file = this.files.find(f => f.id === fileId);
|
2025-11-10 01:58:41 +01:00
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
if (this.isEditableFile(file.name, file.mime_type)) {
|
|
|
|
|
this.dispatchEvent(new CustomEvent('edit-file', {
|
|
|
|
|
detail: { file: file },
|
|
|
|
|
bubbles: true
|
|
|
|
|
}));
|
|
|
|
|
} else {
|
|
|
|
|
this.dispatchEvent(new CustomEvent('photo-click', {
|
|
|
|
|
detail: { photo: file },
|
|
|
|
|
bubbles: true
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleDblClick(e) {
|
|
|
|
|
if (e.target.classList.contains('select-item') ||
|
|
|
|
|
e.target.classList.contains('action-btn') ||
|
|
|
|
|
e.target.classList.contains('breadcrumb-item')) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const folderItem = e.target.closest('.folder-item');
|
|
|
|
|
if (folderItem) {
|
|
|
|
|
const folderId = parseInt(folderItem.dataset.folderId);
|
|
|
|
|
this.loadContents(folderId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleChange(e) {
|
|
|
|
|
const target = e.target;
|
|
|
|
|
|
|
|
|
|
if (target.id === 'select-all') {
|
|
|
|
|
this.toggleSelectAll(target.checked);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (target.classList.contains('select-item')) {
|
|
|
|
|
const type = target.dataset.type;
|
|
|
|
|
const id = parseInt(target.dataset.id);
|
|
|
|
|
this.toggleSelectItem(type, id, target.checked);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attachListeners() {
|
2025-11-10 01:58:41 +01:00
|
|
|
this.updateBatchActionVisibility();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggleSelectItem(type, id, checked) {
|
|
|
|
|
if (type === 'file') {
|
|
|
|
|
if (checked) {
|
|
|
|
|
this.selectedFiles.add(id);
|
|
|
|
|
} else {
|
|
|
|
|
this.selectedFiles.delete(id);
|
|
|
|
|
}
|
|
|
|
|
} else if (type === 'folder') {
|
|
|
|
|
if (checked) {
|
|
|
|
|
this.selectedFolders.add(id);
|
|
|
|
|
} else {
|
|
|
|
|
this.selectedFolders.delete(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-10 15:46:40 +01:00
|
|
|
this.updateSelectionUI();
|
2025-11-10 01:58:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggleSelectAll(checked) {
|
|
|
|
|
this.selectedFiles.clear();
|
|
|
|
|
this.selectedFolders.clear();
|
|
|
|
|
|
|
|
|
|
if (checked) {
|
|
|
|
|
this.files.forEach(file => this.selectedFiles.add(file.id));
|
|
|
|
|
this.folders.forEach(folder => this.selectedFolders.add(folder.id));
|
|
|
|
|
}
|
2025-11-10 15:46:40 +01:00
|
|
|
|
|
|
|
|
this.querySelectorAll('.select-item').forEach(checkbox => {
|
|
|
|
|
checkbox.checked = checked;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.updateSelectionUI();
|
2025-11-10 01:58:41 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
clearSelection() {
|
|
|
|
|
this.selectedFiles.clear();
|
|
|
|
|
this.selectedFolders.clear();
|
|
|
|
|
this.querySelectorAll('.select-item').forEach(checkbox => {
|
|
|
|
|
checkbox.checked = false;
|
|
|
|
|
});
|
|
|
|
|
this.updateSelectionUI();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateSelectionUI() {
|
|
|
|
|
const hasSelected = this.selectedFiles.size > 0 || this.selectedFolders.size > 0;
|
|
|
|
|
const totalItems = this.files.length + this.folders.length;
|
|
|
|
|
const totalSelected = this.selectedFiles.size + this.selectedFolders.size;
|
|
|
|
|
const allSelected = totalItems > 0 && totalSelected === totalItems;
|
|
|
|
|
|
|
|
|
|
const selectAllCheckbox = this.querySelector('#select-all');
|
|
|
|
|
const selectAllLabel = this.querySelector('label[for="select-all"]');
|
2025-11-10 01:58:41 +01:00
|
|
|
const batchActionsDiv = this.querySelector('.batch-actions');
|
2025-11-10 15:46:40 +01:00
|
|
|
|
|
|
|
|
if (selectAllCheckbox) {
|
|
|
|
|
selectAllCheckbox.checked = allSelected;
|
|
|
|
|
this.updateIndeterminateState();
|
2025-11-10 01:58:41 +01:00
|
|
|
}
|
2025-11-10 15:46:40 +01:00
|
|
|
|
|
|
|
|
if (selectAllLabel) {
|
|
|
|
|
selectAllLabel.textContent = hasSelected ? `${totalSelected} selected` : 'Select all';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasSelected && !batchActionsDiv) {
|
|
|
|
|
const container = this.querySelector('.file-list-container');
|
|
|
|
|
const header = container.querySelector('.file-list-header');
|
|
|
|
|
const batchBar = document.createElement('div');
|
|
|
|
|
batchBar.className = 'batch-actions';
|
|
|
|
|
batchBar.innerHTML = `
|
|
|
|
|
<button class="button button-small button-danger" id="batch-delete-btn">Delete</button>
|
|
|
|
|
<button class="button button-small" id="batch-move-btn">Move</button>
|
|
|
|
|
<button class="button button-small" id="batch-copy-btn">Copy</button>
|
|
|
|
|
<button class="button button-small" id="batch-star-btn">Star</button>
|
|
|
|
|
<button class="button button-small" id="batch-unstar-btn">Unstar</button>
|
|
|
|
|
<button class="button button-small" id="clear-selection-btn">Clear Selection</button>
|
|
|
|
|
`;
|
|
|
|
|
header.insertAdjacentElement('afterend', batchBar);
|
|
|
|
|
} else if (!hasSelected && batchActionsDiv) {
|
|
|
|
|
batchActionsDiv.remove();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateIndeterminateState() {
|
|
|
|
|
const selectAllCheckbox = this.querySelector('#select-all');
|
|
|
|
|
if (selectAllCheckbox) {
|
|
|
|
|
const totalItems = this.files.length + this.folders.length;
|
|
|
|
|
const totalSelected = this.selectedFiles.size + this.selectedFolders.size;
|
|
|
|
|
const hasSelected = totalSelected > 0;
|
|
|
|
|
const allSelected = totalItems > 0 && totalSelected === totalItems;
|
|
|
|
|
|
|
|
|
|
selectAllCheckbox.indeterminate = hasSelected && !allSelected;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateBatchActionVisibility() {
|
|
|
|
|
this.updateSelectionUI();
|
2025-11-10 01:58:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async handleBatchAction(action) {
|
|
|
|
|
if ((this.selectedFiles.size === 0 && this.selectedFolders.size === 0)) {
|
|
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'No items selected for batch operation.', type: 'info' }
|
|
|
|
|
}));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!confirm(`Are you sure you want to ${action} ${this.selectedFiles.size + this.selectedFolders.size} items?`)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let targetFolderId = null;
|
|
|
|
|
if (action === 'move' || action === 'copy') {
|
|
|
|
|
const folderName = prompt('Enter target folder ID (leave empty for root):');
|
|
|
|
|
if (folderName !== null) {
|
|
|
|
|
targetFolderId = folderName === '' ? null : parseInt(folderName);
|
|
|
|
|
if (folderName !== '' && isNaN(targetFolderId)) {
|
|
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'Invalid folder ID.', type: 'error' }
|
|
|
|
|
}));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return; // User cancelled
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.selectedFiles.size > 0) {
|
|
|
|
|
await api.batchFileOperations(action, Array.from(this.selectedFiles), targetFolderId);
|
|
|
|
|
}
|
|
|
|
|
if (this.selectedFolders.size > 0) {
|
|
|
|
|
await api.batchFolderOperations(action, Array.from(this.selectedFolders), targetFolderId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: `Batch ${action} successful!`, type: 'success' }
|
|
|
|
|
}));
|
|
|
|
|
await this.loadContents(this.currentFolderId); // Reload contents
|
|
|
|
|
} catch (error) {
|
|
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: `Batch ${action} failed: ` + error.message, type: 'error' }
|
|
|
|
|
}));
|
|
|
|
|
}
|
2025-11-09 23:29:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
triggerCreateFolder() {
|
|
|
|
|
this.handleCreateFolder();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async handleCreateFolder() {
|
|
|
|
|
const name = prompt('Enter folder name:');
|
|
|
|
|
if (name) {
|
|
|
|
|
try {
|
|
|
|
|
await api.createFolder(name, this.currentFolderId);
|
|
|
|
|
await this.loadContents(this.currentFolderId);
|
|
|
|
|
} catch (error) {
|
2025-11-10 01:58:41 +01:00
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'Failed to create folder: ' + error.message, type: 'error' }
|
|
|
|
|
}));
|
2025-11-09 23:29:07 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async handleAction(action, id) {
|
|
|
|
|
try {
|
|
|
|
|
switch (action) {
|
|
|
|
|
case 'download':
|
|
|
|
|
const blob = await api.downloadFile(id);
|
|
|
|
|
const file = this.files.find(f => f.id === id);
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = file.name;
|
|
|
|
|
a.click();
|
|
|
|
|
URL.revokeObjectURL(url);
|
2025-11-10 01:58:41 +01:00
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'File downloaded successfully!', type: 'success' }
|
|
|
|
|
}));
|
2025-11-09 23:29:07 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'rename':
|
|
|
|
|
const newName = prompt('Enter new name:');
|
|
|
|
|
if (newName) {
|
|
|
|
|
await api.renameFile(id, newName);
|
|
|
|
|
await this.loadContents(this.currentFolderId);
|
2025-11-10 01:58:41 +01:00
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'File renamed successfully!', type: 'success' }
|
|
|
|
|
}));
|
2025-11-09 23:29:07 +01:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'delete':
|
|
|
|
|
if (confirm('Are you sure you want to delete this file?')) {
|
|
|
|
|
await api.deleteFile(id);
|
|
|
|
|
await this.loadContents(this.currentFolderId);
|
2025-11-10 01:58:41 +01:00
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'File deleted successfully!', type: 'success' }
|
|
|
|
|
}));
|
2025-11-09 23:29:07 +01:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'delete-folder':
|
|
|
|
|
if (confirm('Are you sure you want to delete this folder?')) {
|
|
|
|
|
await api.deleteFolder(id);
|
|
|
|
|
await this.loadContents(this.currentFolderId);
|
2025-11-10 01:58:41 +01:00
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'Folder deleted successfully!', type: 'success' }
|
|
|
|
|
}));
|
2025-11-09 23:29:07 +01:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'share':
|
|
|
|
|
this.dispatchEvent(new CustomEvent('share-request', { detail: { fileId: id } }));
|
|
|
|
|
break;
|
2025-11-10 01:58:41 +01:00
|
|
|
case 'star-file':
|
|
|
|
|
await api.starFile(id);
|
|
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'File starred successfully!', type: 'success' }
|
|
|
|
|
}));
|
|
|
|
|
await this.loadContents(this.currentFolderId);
|
|
|
|
|
break;
|
|
|
|
|
case 'unstar-file':
|
|
|
|
|
await api.unstarFile(id);
|
|
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'File unstarred successfully!', type: 'success' }
|
|
|
|
|
}));
|
|
|
|
|
await this.loadContents(this.currentFolderId);
|
|
|
|
|
break;
|
|
|
|
|
case 'star-folder':
|
|
|
|
|
await api.starFolder(id);
|
|
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'Folder starred successfully!', type: 'success' }
|
|
|
|
|
}));
|
|
|
|
|
await this.loadContents(this.currentFolderId);
|
|
|
|
|
break;
|
|
|
|
|
case 'unstar-folder':
|
|
|
|
|
await api.unstarFolder(id);
|
|
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'Folder unstarred successfully!', type: 'success' }
|
|
|
|
|
}));
|
|
|
|
|
await this.loadContents(this.currentFolderId);
|
|
|
|
|
break;
|
2025-11-09 23:29:07 +01:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-11-10 01:58:41 +01:00
|
|
|
document.dispatchEvent(new CustomEvent('show-toast', {
|
|
|
|
|
detail: { message: 'Action failed: ' + error.message, type: 'error' }
|
|
|
|
|
}));
|
2025-11-09 23:29:07 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customElements.define('file-list', FileList);
|