import { api } from '../api.js';
export class FileList extends HTMLElement {
constructor() {
super();
this.currentFolderId = null;
this.files = [];
this.folders = [];
this.selectedFiles = new Set();
this.selectedFolders = new Set();
}
async connectedCallback() {
await this.loadContents(null);
}
async loadContents(folderId) {
this.currentFolderId = folderId;
try {
this.folders = await api.listFolders(folderId);
this.files = await api.listFiles(folderId);
this.selectedFiles.clear();
this.selectedFolders.clear();
this.render();
} catch (error) {
console.error('Failed to load contents:', error);
document.dispatchEvent(new CustomEvent('show-toast', {
detail: { message: 'Failed to load contents: ' + error.message, type: 'error' }
}));
}
}
setFiles(files) {
this.files = files;
this.folders = [];
this.selectedFiles.clear();
this.selectedFolders.clear();
this.render();
}
render() {
const hasSelected = this.selectedFiles.size > 0 || this.selectedFolders.size > 0;
const allFilesSelected = this.files.length > 0 && this.selectedFiles.size === this.files.length;
const allFoldersSelected = this.folders.length > 0 && this.selectedFolders.size === this.folders.length;
const allSelected = (this.files.length + this.folders.length) > 0 && allFilesSelected && allFoldersSelected;
this.innerHTML = `
<div class="file-list-container">
<div class="file-list-header">
<h2>Files</h2>
<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>
<div class="batch-actions" style="display: ${hasSelected ? 'flex' : 'none'};">
<input type="checkbox" id="select-all" ${allSelected ? 'checked' : ''}>
<label for="select-all">Select All</label>
<button class="button button-small button-danger" id="batch-delete-btn">Delete Selected</button>
<button class="button button-small" id="batch-move-btn">Move Selected</button>
<button class="button button-small" id="batch-copy-btn">Copy Selected</button>
<button class="button button-small" id="batch-star-btn">Star Selected</button>
<button class="button button-small" id="batch-unstar-btn">Unstar Selected</button>
</div>
<div class="file-grid">
${this.folders.map(folder => this.renderFolder(folder)).join('')}
${this.files.map(file => this.renderFile(file)).join('')}
</div>
</div>
`;
this.attachListeners();
}
renderFolder(folder) {
const isSelected = this.selectedFolders.has(folder.id);
const starIcon = folder.is_starred ? '&#9733;' : '&#9734;'; // Filled star or empty star
const starAction = folder.is_starred ? 'unstar-folder' : 'star-folder';
return `
<div class="file-item folder-item" data-folder-id="${folder.id}">
<input type="checkbox" class="select-item" data-type="folder" data-id="${folder.id}" ${isSelected ? 'checked' : ''}>
<div class="file-icon">&#128193;</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>
<button class="action-btn star-btn" data-action="${starAction}" data-id="${folder.id}">${starIcon}</button>
</div>
</div>
`;
}
renderFile(file) {
const isSelected = this.selectedFiles.has(file.id);
const icon = this.getFileIcon(file.mime_type);
const size = this.formatFileSize(file.size);
const starIcon = file.is_starred ? '&#9733;' : '&#9734;'; // Filled star or empty star
const starAction = file.is_starred ? 'unstar-file' : 'star-file';
return `
<div class="file-item" data-file-id="${file.id}">
<input type="checkbox" class="select-item" data-type="file" data-id="${file.id}" ${isSelected ? 'checked' : ''}>
<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>
<button class="action-btn star-btn" data-action="${starAction}" data-id="${file.id}">${starIcon}</button>
</div>
</div>
`;
}
getFileIcon(mimeType) {
if (mimeType.startsWith('image/')) return '&#128247;';
if (mimeType.startsWith('video/')) return '&#127909;';
if (mimeType.startsWith('audio/')) return '&#127925;';
if (mimeType.includes('pdf')) return '&#128196;';
if (mimeType.includes('text')) return '&#128196;';
return '&#128196;';
}
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';
}
attachListeners() {
this.querySelector('#upload-btn')?.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('upload-request'));
});
this.querySelector('#create-folder-btn')?.addEventListener('click', async () => {
await this.handleCreateFolder();
});
this.querySelectorAll('.folder-item').forEach(item => {
item.addEventListener('dblclick', () => {
const folderId = parseInt(item.dataset.folderId);
this.dispatchEvent(new CustomEvent('folder-open', { detail: { folderId } }));
});
});
this.querySelectorAll('.file-item:not(.folder-item)').forEach(item => {
item.addEventListener('click', (e) => {
if (!e.target.classList.contains('action-btn') && !e.target.classList.contains('select-item')) {
const fileId = parseInt(item.dataset.fileId);
const file = this.files.find(f => f.id === fileId);
this.dispatchEvent(new CustomEvent('photo-click', {
detail: { photo: file },
bubbles: true
}));
}
});
});
this.querySelectorAll('.action-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const action = btn.dataset.action;
const id = parseInt(btn.dataset.id);
await this.handleAction(action, id);
});
});
this.querySelectorAll('.select-item').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const type = e.target.dataset.type;
const id = parseInt(e.target.dataset.id);
this.toggleSelectItem(type, id, e.target.checked);
});
});
this.querySelector('#select-all')?.addEventListener('change', (e) => {
this.toggleSelectAll(e.target.checked);
});
this.querySelector('#batch-delete-btn')?.addEventListener('click', () => this.handleBatchAction('delete'));
this.querySelector('#batch-move-btn')?.addEventListener('click', () => this.handleBatchAction('move'));
this.querySelector('#batch-copy-btn')?.addEventListener('click', () => this.handleBatchAction('copy'));
this.querySelector('#batch-star-btn')?.addEventListener('click', () => this.handleBatchAction('star'));
this.querySelector('#batch-unstar-btn')?.addEventListener('click', () => this.handleBatchAction('unstar'));
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);
}
}
this.updateBatchActionVisibility();
}
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));
}
this.render(); // Re-render to update checkboxes
}
updateBatchActionVisibility() {
const batchActionsDiv = this.querySelector('.batch-actions');
if (batchActionsDiv) {
if (this.selectedFiles.size > 0 || this.selectedFolders.size > 0) {
batchActionsDiv.style.display = 'flex';
} else {
batchActionsDiv.style.display = 'none';
}
}
}
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' }
}));
}
}
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) {
document.dispatchEvent(new CustomEvent('show-toast', {
detail: { message: 'Failed to create folder: ' + error.message, type: 'error' }
}));
}
}
}
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);
document.dispatchEvent(new CustomEvent('show-toast', {
detail: { message: 'File downloaded successfully!', type: 'success' }
}));
break;
case 'rename':
const newName = prompt('Enter new name:');
if (newName) {
await api.renameFile(id, newName);
await this.loadContents(this.currentFolderId);
document.dispatchEvent(new CustomEvent('show-toast', {
detail: { message: 'File renamed successfully!', type: 'success' }
}));
}
break;
case 'delete':
if (confirm('Are you sure you want to delete this file?')) {
await api.deleteFile(id);
await this.loadContents(this.currentFolderId);
document.dispatchEvent(new CustomEvent('show-toast', {
detail: { message: 'File deleted successfully!', type: 'success' }
}));
}
break;
case 'delete-folder':
if (confirm('Are you sure you want to delete this folder?')) {
await api.deleteFolder(id);
await this.loadContents(this.currentFolderId);
document.dispatchEvent(new CustomEvent('show-toast', {
detail: { message: 'Folder deleted successfully!', type: 'success' }
}));
}
break;
case 'share':
this.dispatchEvent(new CustomEvent('share-request', { detail: { fileId: id } }));
break;
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;
}
} catch (error) {
document.dispatchEvent(new CustomEvent('show-toast', {
detail: { message: 'Action failed: ' + error.message, type: 'error' }
}));
}
}
}
customElements.define('file-list', FileList);