269 lines
9.0 KiB
JavaScript
269 lines
9.0 KiB
JavaScript
|
|
export class BaseFileList extends HTMLElement {
|
||
|
|
constructor() {
|
||
|
|
super();
|
||
|
|
this.files = [];
|
||
|
|
this.folders = [];
|
||
|
|
this.selectedFiles = new Set();
|
||
|
|
this.selectedFolders = new Set();
|
||
|
|
this.boundHandleClick = this.handleClick.bind(this);
|
||
|
|
this.boundHandleDblClick = this.handleDblClick.bind(this);
|
||
|
|
this.boundHandleChange = this.handleChange.bind(this);
|
||
|
|
}
|
||
|
|
|
||
|
|
connectedCallback() {
|
||
|
|
this.addEventListener('click', this.boundHandleClick);
|
||
|
|
this.addEventListener('dblclick', this.boundHandleDblClick);
|
||
|
|
this.addEventListener('change', this.boundHandleChange);
|
||
|
|
}
|
||
|
|
|
||
|
|
disconnectedCallback() {
|
||
|
|
this.removeEventListener('click', this.boundHandleClick);
|
||
|
|
this.removeEventListener('dblclick', this.boundHandleDblClick);
|
||
|
|
this.removeEventListener('change', this.boundHandleChange);
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
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 '📄';
|
||
|
|
}
|
||
|
|
|
||
|
|
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';
|
||
|
|
}
|
||
|
|
|
||
|
|
renderFolder(folder) {
|
||
|
|
const isSelected = this.selectedFolders.has(folder.id);
|
||
|
|
const starIcon = folder.is_starred ? '★' : '☆';
|
||
|
|
const starAction = folder.is_starred ? 'unstar-folder' : 'star-folder';
|
||
|
|
const actions = this.getFolderActions(folder);
|
||
|
|
|
||
|
|
return `
|
||
|
|
<div class="file-item folder-item ${isSelected ? 'selected' : ''}" data-folder-id="${folder.id}">
|
||
|
|
<input type="checkbox" class="select-item" data-type="folder" data-id="${folder.id}" ${isSelected ? 'checked' : ''}>
|
||
|
|
<div class="file-icon">📁</div>
|
||
|
|
<div class="file-name">${folder.name}</div>
|
||
|
|
<div class="file-actions-menu">
|
||
|
|
${actions}
|
||
|
|
</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 ? '★' : '☆';
|
||
|
|
const starAction = file.is_starred ? 'unstar-file' : 'star-file';
|
||
|
|
const actions = this.getFileActions(file);
|
||
|
|
|
||
|
|
return `
|
||
|
|
<div class="file-item ${isSelected ? 'selected' : ''}" 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">
|
||
|
|
${actions}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
getFolderActions(folder) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
getFileActions(file) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
handleClick(e) {
|
||
|
|
const target = e.target;
|
||
|
|
|
||
|
|
if (target.id === 'clear-selection-btn') {
|
||
|
|
this.clearSelection();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
|
||
|
|
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) {
|
||
|
|
const folderItem = e.target.closest('.folder-item');
|
||
|
|
if (folderItem) {
|
||
|
|
const folderId = parseInt(folderItem.dataset.folderId);
|
||
|
|
this.dispatchEvent(new CustomEvent('folder-open', { detail: { 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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const item = this.querySelector(`[data-${type}-id="${id}"]`);
|
||
|
|
if (item) {
|
||
|
|
if (checked) {
|
||
|
|
item.classList.add('selected');
|
||
|
|
} else {
|
||
|
|
item.classList.remove('selected');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
this.updateSelectionUI();
|
||
|
|
}
|
||
|
|
|
||
|
|
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.querySelectorAll('.select-item').forEach(checkbox => {
|
||
|
|
checkbox.checked = checked;
|
||
|
|
});
|
||
|
|
|
||
|
|
this.querySelectorAll('.file-item').forEach(item => {
|
||
|
|
if (checked) {
|
||
|
|
item.classList.add('selected');
|
||
|
|
} else {
|
||
|
|
item.classList.remove('selected');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.updateSelectionUI();
|
||
|
|
}
|
||
|
|
|
||
|
|
clearSelection() {
|
||
|
|
this.selectedFiles.clear();
|
||
|
|
this.selectedFolders.clear();
|
||
|
|
this.querySelectorAll('.select-item').forEach(checkbox => {
|
||
|
|
checkbox.checked = false;
|
||
|
|
});
|
||
|
|
this.querySelectorAll('.file-item').forEach(item => {
|
||
|
|
item.classList.remove('selected');
|
||
|
|
});
|
||
|
|
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"]');
|
||
|
|
const batchActionsDiv = this.querySelector('.batch-actions');
|
||
|
|
|
||
|
|
if (selectAllCheckbox) {
|
||
|
|
selectAllCheckbox.checked = allSelected;
|
||
|
|
this.updateIndeterminateState();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (selectAllLabel) {
|
||
|
|
selectAllLabel.textContent = hasSelected ? `${totalSelected} selected` : 'Select all';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (hasSelected && !batchActionsDiv) {
|
||
|
|
this.createBatchActionsBar();
|
||
|
|
} else if (!hasSelected && batchActionsDiv) {
|
||
|
|
batchActionsDiv.remove();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
createBatchActionsBar() {
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async handleAction(action, id) {
|
||
|
|
}
|
||
|
|
}
|