summaryrefslogtreecommitdiff
path: root/web/js/app.js
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-03 09:53:46 +0300
committerPaul Buetow <paul@buetow.org>2026-05-03 09:53:46 +0300
commit0f18fcd2dd98c9261f8c8ce1be1d895184567357 (patch)
tree2e87e69938772699e18a344e4db8e97c46dbe047 /web/js/app.js
parent92a59298ac32b4ee6e4f24e6c93307eaa152cd27 (diff)
feat: hybrid cache-busting for thumbnails and covers
- Add Cache-Control: no-cache headers to thumbnail and cover endpoints (GET /api/media/{id}/thumbnail, GET /api/sets/{id}/cover, GET /s/{token}/thumbnail) so browsers revalidate instead of serving stale cached images after regeneration. - Add frontend cache-busting via ?t=Date.now() after folder cover regeneration so the browser fetches the newly overwritten image. - Replace toolbar filters with inline search syntax (min:, max:, tag:, like:, type:, sort:, minsize:, maxsize:) for faster filtering. - Remove dedicated toolbar and advanced filter panel; consolidate all filtering into the search bar. - Expand filter state to support filesize_min/filesize_max.
Diffstat (limited to 'web/js/app.js')
-rw-r--r--web/js/app.js78
1 files changed, 32 insertions, 46 deletions
diff --git a/web/js/app.js b/web/js/app.js
index 307190f..76436bb 100644
--- a/web/js/app.js
+++ b/web/js/app.js
@@ -2,7 +2,7 @@ import { API } from './api.js';
import { initKeyboard } from './keyboard.js';
import { initSelection, clearSelection, select, selectByElement, next, prev, currentIndex, currentElement, navUp, navDown, navLeft, navRight } from './selection.js';
import { initPlayer, togglePlay, toggleFullscreen, toggleMinimize, toggleDetach, exitFullscreenIfNeeded, currentMediaId, currentMediaInfo, hasLoadedMedia, isPlaybackActive, seekRelative, selectAndPlay } from './player.js';
-import { initSearch, focusSearch, trigger as triggerSearch } from './search.js';
+import { initSearch, focusSearch, trigger as triggerSearch, parseQuery } from './search.js';
import { initShuffle, toggle as toggleShuffle, isOn as isShuffle } from './shuffle.js';
import { initThemes } from './themes.js';
import { initNotes, open as openNotes } from './notes.js';
@@ -76,8 +76,10 @@ async function initApp() {
initSelection();
initSearch({
onChange: (q) => {
- state.filters.search = q;
+ const parsed = parseQuery(q);
+ Object.assign(state.filters, parsed);
state.folderPath = '';
+ console.log('[search] raw:', q, 'parsed:', parsed, 'filters:', state.filters);
loadMedia();
},
input: document.getElementById('search-input'),
@@ -126,7 +128,6 @@ async function initApp() {
toggleDetach: () => toggleDetach(),
download: downloadSelected,
help: toggleHelp,
- toolbar: toggleToolbar,
backspace: () => { navigateBack(); },
sidebar: toggleSidebar,
upload: () => showUpload(),
@@ -136,8 +137,6 @@ async function initApp() {
sharesNavDown: () => sharesNav(1),
sharesCopy: copySelectedShare,
sharesDelete: deleteSelectedShare,
- focusMinDuration: () => focusFilter('filter-min-duration'),
- focusMaxDuration: () => focusFilter('filter-max-duration'),
});
initNotes(() => toast('Note saved'));
initAdmin();
@@ -147,21 +146,6 @@ async function initApp() {
initShares();
initMediaInfo();
- // Filters
- document.getElementById('filter-type')?.addEventListener('change', (e) => { state.filters.type = e.target.value; state.folderPath = ''; loadMedia(); });
- document.getElementById('filter-favorites')?.addEventListener('click', (e) => {
- state.filters.favorites = !state.filters.favorites;
- e.target.classList.toggle('active', state.filters.favorites);
- state.folderPath = '';
- loadMedia();
- });
- document.getElementById('filter-tags')?.addEventListener('change', (e) => { state.filters.tags = e.target.value; state.folderPath = ''; loadMedia(); });
- document.getElementById('filter-min-duration')?.addEventListener('change', (e) => { state.filters.minDuration = e.target.value; state.folderPath = ''; loadMedia(); });
- document.getElementById('filter-max-duration')?.addEventListener('change', (e) => { state.filters.maxDuration = e.target.value; state.folderPath = ''; loadMedia(); });
- document.getElementById('filter-toggle')?.addEventListener('click', () => {
- document.getElementById('filter-advanced')?.classList.toggle('hidden');
- });
-
// Sidebar close
document.getElementById('menu-close')?.addEventListener('click', () => {
document.getElementById('sidebar')?.classList.remove('open');
@@ -304,7 +288,7 @@ async function loadMedia() {
// When viewing a single set with no filters/search, use the browse endpoint
// so that subfolders are presented as folders to navigate into.
- if (singleSetId && !setIds && !isShuffle() && !state.filters.search && !state.filters.type && !state.filters.favorites && !state.filters.tags && !state.filters.minDuration && !state.filters.maxDuration) {
+ if (singleSetId && !setIds && !isShuffle() && !hasActiveFilters()) {
const data = await API.browse(singleSetId, state.folderPath);
updateBreadcrumb(data.current_path);
setMedia(mediaWithBrowsePath(data.media || [], data.current_path || ''));
@@ -313,6 +297,7 @@ async function loadMedia() {
resultCount.textContent = `${total} items`;
} else {
breadcrumb?.classList.add('hidden');
+ const sort = isShuffle() ? 'random' : (state.filters.sort || 'name');
const params = {
set_id: setIds ? '' : String(singleSetId || state.selectedSetId || ''),
set_ids: setIds,
@@ -322,7 +307,9 @@ async function loadMedia() {
tags: state.filters.tags || '',
min_duration: state.filters.minDuration ? String(parseFloat(state.filters.minDuration) * 60) : '',
max_duration: state.filters.maxDuration ? String(parseFloat(state.filters.maxDuration) * 60) : '',
- sort: isShuffle() ? 'random' : 'name',
+ filesize_min: state.filters.minFileSize ? String(parseInt(state.filters.minFileSize, 10) * 1024 * 1024) : '',
+ filesize_max: state.filters.maxFileSize ? String(parseInt(state.filters.maxFileSize, 10) * 1024 * 1024) : '',
+ sort,
limit: '1000',
};
const data = await API.media(params);
@@ -404,7 +391,18 @@ function renderBrowse(data) {
if (!setId) return;
const name = b.closest('.folder-card')?.dataset.name;
const folder = state.folderPath ? `${state.folderPath}/${name}` : name;
- try { await API.regenCover(setId, folder); toast('Folder cover regenerated'); loadMedia(); }
+ try {
+ await API.regenCover(setId, folder);
+ toast('Folder cover regenerated');
+ await loadMedia();
+ // Force refresh the folder cover image to bypass browser cache
+ const folderCard = document.querySelector(`.folder-card[data-name="${CSS.escape(name)}"]`);
+ const img = folderCard?.querySelector('img');
+ if (img) {
+ const base = img.src.split('?')[0];
+ img.src = `${base}?t=${Date.now()}`;
+ }
+ }
catch (err) { toast(err.message || 'Cover failed', 'error'); }
});
});
@@ -575,7 +573,17 @@ function canTraverseBrowseFolders() {
}
function hasActiveFilters() {
- return !!(state.filters.search || state.filters.type || state.filters.favorites || state.filters.tags || state.filters.minDuration || state.filters.maxDuration);
+ return !!(
+ state.filters.search ||
+ state.filters.type ||
+ state.filters.favorites ||
+ state.filters.tags ||
+ state.filters.minDuration ||
+ state.filters.maxDuration ||
+ state.filters.minFileSize ||
+ state.filters.maxFileSize ||
+ state.filters.sort
+ );
}
async function findCrossFolderPlayable(delta, preferCurrent) {
@@ -982,28 +990,6 @@ function toggleHelp() {
modal.classList.toggle('open');
}
-// Toolbar / sidebar / search visibility toggles
-function toggleToolbar() {
- const toolbar = document.getElementById('toolbar');
- toolbar?.classList.toggle('hidden');
-}
-
-function toggleSidebar() {
- const sidebar = document.getElementById('sidebar');
- const page = document.querySelector('.page');
- if (!sidebar) return;
- const open = sidebar.classList.toggle('open');
- page?.classList.toggle('has-sidebar', open);
-}
-
-function focusFilter(id) {
- const el = document.getElementById(id);
- if (!el) return;
- document.getElementById('filter-advanced')?.classList.remove('hidden');
- el.focus();
- el.select();
-}
-
function showSearch() {
const bar = document.getElementById('search-bar');
bar?.classList.remove('hidden');