Add browser search/filter for media items
- Search bar appears when browsing inside a folder - Client-side filtering with 200ms debounce - Clear button and search icon - Hides at root level, resets on navigation - Localized placeholder (en/ru) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1261,6 +1261,68 @@
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Browser Search */
|
||||
.browser-search-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.browser-search-icon {
|
||||
position: absolute;
|
||||
left: 0.6rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.browser-search-input {
|
||||
width: 100%;
|
||||
padding: 0.4rem 2rem 0.4rem 2rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.813rem;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.browser-search-input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.browser-search-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.browser-search-clear {
|
||||
position: absolute;
|
||||
right: 0.4rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 0.15rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.browser-search-clear:hover {
|
||||
color: var(--text-primary);
|
||||
background: transparent !important;
|
||||
transform: translateY(-50%) !important;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
background: var(--bg-tertiary);
|
||||
@@ -1853,6 +1915,10 @@
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.browser-search-wrapper {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.browser-toolbar-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@@ -196,6 +196,13 @@
|
||||
<span data-i18n="browser.play_all">Play All</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="browser-search-wrapper" id="browserSearchWrapper" style="display: none;">
|
||||
<svg class="browser-search-icon" viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
<input type="text" id="browserSearchInput" class="browser-search-input" data-i18n-placeholder="browser.search" placeholder="Search..." oninput="onBrowserSearch()">
|
||||
<button class="browser-search-clear" id="browserSearchClear" onclick="clearBrowserSearch()" style="display: none;">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="browser-toolbar-right">
|
||||
<label class="items-per-page-label">
|
||||
<span data-i18n="browser.items_per_page">Items per page:</span>
|
||||
|
||||
@@ -1460,6 +1460,8 @@ let totalItems = 0;
|
||||
let mediaFolders = {};
|
||||
let viewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
|
||||
let cachedItems = null;
|
||||
let browserSearchTerm = '';
|
||||
let browserSearchTimer = null;
|
||||
|
||||
// Load media folders on page load
|
||||
async function loadMediaFolders() {
|
||||
@@ -1490,6 +1492,10 @@ function showRootFolders() {
|
||||
currentFolderId = '';
|
||||
currentPath = '';
|
||||
currentOffset = 0;
|
||||
cachedItems = null;
|
||||
|
||||
// Hide search at root level
|
||||
showBrowserSearch(false);
|
||||
|
||||
// Render breadcrumb with just "Home" (not clickable at root)
|
||||
const breadcrumb = document.getElementById('breadcrumb');
|
||||
@@ -1550,6 +1556,9 @@ function showRootFolders() {
|
||||
}
|
||||
|
||||
async function browsePath(folderId, path, offset = 0, nocache = false) {
|
||||
// Clear search when navigating
|
||||
showBrowserSearch(false);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
if (!token) {
|
||||
@@ -1582,6 +1591,9 @@ async function browsePath(folderId, path, offset = 0, nocache = false) {
|
||||
renderBrowserItems(cachedItems);
|
||||
renderPagination();
|
||||
|
||||
// Show search bar when inside a folder
|
||||
showBrowserSearch(true);
|
||||
|
||||
// Show/hide Play All button based on whether media items exist
|
||||
const hasMedia = data.items.some(item => item.is_media);
|
||||
document.getElementById('playAllBtn').style.display = hasMedia ? '' : 'none';
|
||||
@@ -2072,6 +2084,54 @@ function refreshBrowser() {
|
||||
}
|
||||
}
|
||||
|
||||
// Browser search
|
||||
function onBrowserSearch() {
|
||||
const input = document.getElementById('browserSearchInput');
|
||||
const clearBtn = document.getElementById('browserSearchClear');
|
||||
const term = input.value.trim();
|
||||
|
||||
clearBtn.style.display = term ? 'flex' : 'none';
|
||||
|
||||
// Debounce: wait 200ms after typing stops
|
||||
if (browserSearchTimer) clearTimeout(browserSearchTimer);
|
||||
browserSearchTimer = setTimeout(() => {
|
||||
browserSearchTerm = term.toLowerCase();
|
||||
applyBrowserSearch();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function clearBrowserSearch() {
|
||||
const input = document.getElementById('browserSearchInput');
|
||||
input.value = '';
|
||||
document.getElementById('browserSearchClear').style.display = 'none';
|
||||
browserSearchTerm = '';
|
||||
applyBrowserSearch();
|
||||
input.focus();
|
||||
}
|
||||
|
||||
function applyBrowserSearch() {
|
||||
if (!cachedItems) return;
|
||||
|
||||
if (!browserSearchTerm) {
|
||||
renderBrowserItems(cachedItems);
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = cachedItems.filter(item =>
|
||||
item.name.toLowerCase().includes(browserSearchTerm)
|
||||
);
|
||||
renderBrowserItems(filtered);
|
||||
}
|
||||
|
||||
function showBrowserSearch(visible) {
|
||||
document.getElementById('browserSearchWrapper').style.display = visible ? '' : 'none';
|
||||
if (!visible) {
|
||||
document.getElementById('browserSearchInput').value = '';
|
||||
document.getElementById('browserSearchClear').style.display = 'none';
|
||||
browserSearchTerm = '';
|
||||
}
|
||||
}
|
||||
|
||||
function setViewMode(mode) {
|
||||
if (mode === viewMode) return;
|
||||
viewMode = mode;
|
||||
@@ -2084,7 +2144,7 @@ function setViewMode(mode) {
|
||||
|
||||
// Re-render current view from cache (no network request)
|
||||
if (currentFolderId && cachedItems) {
|
||||
renderBrowserItems(cachedItems);
|
||||
applyBrowserSearch();
|
||||
} else {
|
||||
showRootFolders();
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"browser.view_grid": "Grid view",
|
||||
"browser.view_compact": "Compact view",
|
||||
"browser.view_list": "List view",
|
||||
"browser.search": "Search...",
|
||||
"browser.items_per_page": "Items per page:",
|
||||
"browser.page": "Page",
|
||||
"browser.previous": "Previous",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"browser.view_grid": "Сетка",
|
||||
"browser.view_compact": "Компактный вид",
|
||||
"browser.view_list": "Список",
|
||||
"browser.search": "Поиск...",
|
||||
"browser.items_per_page": "Элементов на странице:",
|
||||
"browser.page": "Страница",
|
||||
"browser.previous": "Предыдущая",
|
||||
|
||||
Reference in New Issue
Block a user