diff --git a/public/app.js b/public/app.js index fdefa1b6..8f9619ff 100644 --- a/public/app.js +++ b/public/app.js @@ -71,6 +71,8 @@ const fileSidebar = document.getElementById('file-sidebar'); const fileSidebarToggle = document.getElementById('file-sidebar-toggle'); const fileSidebarClose = document.getElementById('file-sidebar-close'); const fileSidebarUp = document.getElementById('file-sidebar-up'); +const fileSidebarSort = document.getElementById('file-sidebar-sort'); +const fileSidebarSearch = document.getElementById('file-sidebar-search'); const fileList = document.getElementById('file-list'); const fileSidebarPath = document.getElementById('file-sidebar-path'); const fileBrowser = new FileBrowser(fileList, fileSidebarPath, messageInput, (filePath) => { @@ -78,6 +80,50 @@ const fileBrowser = new FileBrowser(fileList, fileSidebarPath, messageInput, (fi const ext = name.split('.').pop()?.toLowerCase() || ''; pendingFilePaths.push({ path: filePath, name, ext }); renderAttachmentPreviews(); +}, fileSidebarSearch); + +const fileSortDropdown = document.getElementById('file-sort-dropdown'); +const fileSortMenu = document.getElementById('file-sort-menu'); +const SORT_LABELS = { name: 'Name', date: 'Date', size: 'Size' }; + +function updateSortMenuActive() { + fileSortMenu.querySelectorAll('.model-dropdown-item').forEach(el => { + el.classList.toggle('active', el.dataset.sort === fileBrowser.sortBy); + }); +} + +function closeSortMenu() { + fileSortMenu.classList.add('hidden'); + fileSortDropdown.classList.remove('open'); +} + +fileSidebarSort.title = `Sort: ${SORT_LABELS[fileBrowser.sortBy]}`; +fileSidebarSort.addEventListener('click', (e) => { + e.stopPropagation(); + const isOpen = !fileSortMenu.classList.contains('hidden'); + if (isOpen) { + closeSortMenu(); + } else { + updateSortMenuActive(); + fileSortMenu.classList.remove('hidden'); + fileSortDropdown.classList.add('open'); + } +}); + +fileSortMenu.querySelectorAll('.model-dropdown-item').forEach(el => { + el.addEventListener('click', () => { + fileBrowser.setSortBy(el.dataset.sort); + fileSidebarSort.title = `Sort: ${SORT_LABELS[el.dataset.sort]}`; + closeSortMenu(); + }); +}); + +document.addEventListener('click', (e) => { + if (!fileSortDropdown.contains(e.target)) closeSortMenu(); +}); + +fileSidebarSearch.addEventListener('input', () => { + fileBrowser.setSearchQuery(fileSidebarSearch.value); }); fileSidebarToggle.addEventListener('click', () => { diff --git a/public/file-browser.js b/public/file-browser.js index efd73805..6a27efe9 100644 --- a/public/file-browser.js +++ b/public/file-browser.js @@ -37,19 +37,29 @@ function formatSize(bytes) { return `${(bytes / (1024 * 1024)).toFixed(1)}M`; } +const SORT_MODES = ['name', 'date', 'size']; + export class FileBrowser { - constructor(container, pathEl, messageInput, onFileInserted = null) { + constructor(container, pathEl, messageInput, onFileInserted = null, searchInput = null) { this.container = container; this.pathEl = pathEl; this.messageInput = messageInput; this.onFileInserted = onFileInserted; + this.searchInput = searchInput; this.currentPath = null; + this.items = []; + this.searchQuery = ''; + this.sortBy = SORT_MODES.includes(localStorage.getItem('tau-file-sort')) + ? localStorage.getItem('tau-file-sort') + : 'name'; this.setupDropTarget(); } async load(dirPath) { this.container.innerHTML = '
Loading…
'; + this.searchQuery = ''; + if (this.searchInput) this.searchInput.value = ''; try { const url = dirPath @@ -66,12 +76,41 @@ export class FileBrowser { this.currentPath = data.path; this.pathEl.textContent = data.path; this.pathEl.title = data.path; - this.render(data.items); + this.items = data.items; + this.render(this.getVisibleItems()); } catch (err) { this.container.innerHTML = '
Failed to load
'; } } + getVisibleItems() { + const query = this.searchQuery.trim().toLowerCase(); + const filtered = query + ? this.items.filter(item => item.name.toLowerCase().includes(query)) + : this.items; + + const items = [...filtered]; + items.sort((a, b) => { + if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1; + if (this.sortBy === 'date') return b.mtime - a.mtime; + if (this.sortBy === 'size') return (b.size || 0) - (a.size || 0); + return a.name.localeCompare(b.name); + }); + return items; + } + + setSearchQuery(query) { + this.searchQuery = query; + this.render(this.getVisibleItems()); + } + + setSortBy(key) { + if (!SORT_MODES.includes(key)) return; + this.sortBy = key; + localStorage.setItem('tau-file-sort', key); + if (this.items.length) this.render(this.getVisibleItems()); + } + getParentPath() { if (!this.currentPath) return null; const sep = this.currentPath.includes('\\') ? '\\' : '/'; @@ -86,7 +125,8 @@ export class FileBrowser { this.container.innerHTML = ''; if (items.length === 0) { - this.container.innerHTML = '
Empty directory
'; + const message = this.searchQuery ? 'No matches' : 'Empty directory'; + this.container.innerHTML = `
${message}
`; return; } diff --git a/public/index.html b/public/index.html index 9531c217..c7ecfe03 100644 --- a/public/index.html +++ b/public/index.html @@ -72,12 +72,12 @@ Connecting... - + @@ -157,8 +157,21 @@ +
+ + +
+
+ +
diff --git a/public/style.css b/public/style.css index 83fd5287..7ae55ea9 100644 --- a/public/style.css +++ b/public/style.css @@ -3018,6 +3018,21 @@ body.hide-thinking .thinking-block { display: none !important; } letter-spacing: 0.04em; } +.file-sort-dropdown { + position: relative; +} + +.file-sort-menu { + left: auto; + right: 0; + min-width: 110px; +} + +.file-sidebar-search-row { + display: flex; + padding: 0 12px 8px; +} + .file-sidebar-path { padding: 6px 12px; font-size: 10px;