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;
|
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 {
|
.view-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
@@ -1853,6 +1915,10 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.browser-search-wrapper {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
.browser-toolbar-right {
|
.browser-toolbar-right {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,13 @@
|
|||||||
<span data-i18n="browser.play_all">Play All</span>
|
<span data-i18n="browser.play_all">Play All</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<div class="browser-toolbar-right">
|
||||||
<label class="items-per-page-label">
|
<label class="items-per-page-label">
|
||||||
<span data-i18n="browser.items_per_page">Items per page:</span>
|
<span data-i18n="browser.items_per_page">Items per page:</span>
|
||||||
|
|||||||
@@ -1460,6 +1460,8 @@ let totalItems = 0;
|
|||||||
let mediaFolders = {};
|
let mediaFolders = {};
|
||||||
let viewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
|
let viewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
|
||||||
let cachedItems = null;
|
let cachedItems = null;
|
||||||
|
let browserSearchTerm = '';
|
||||||
|
let browserSearchTimer = null;
|
||||||
|
|
||||||
// Load media folders on page load
|
// Load media folders on page load
|
||||||
async function loadMediaFolders() {
|
async function loadMediaFolders() {
|
||||||
@@ -1490,6 +1492,10 @@ function showRootFolders() {
|
|||||||
currentFolderId = '';
|
currentFolderId = '';
|
||||||
currentPath = '';
|
currentPath = '';
|
||||||
currentOffset = 0;
|
currentOffset = 0;
|
||||||
|
cachedItems = null;
|
||||||
|
|
||||||
|
// Hide search at root level
|
||||||
|
showBrowserSearch(false);
|
||||||
|
|
||||||
// Render breadcrumb with just "Home" (not clickable at root)
|
// Render breadcrumb with just "Home" (not clickable at root)
|
||||||
const breadcrumb = document.getElementById('breadcrumb');
|
const breadcrumb = document.getElementById('breadcrumb');
|
||||||
@@ -1550,6 +1556,9 @@ function showRootFolders() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function browsePath(folderId, path, offset = 0, nocache = false) {
|
async function browsePath(folderId, path, offset = 0, nocache = false) {
|
||||||
|
// Clear search when navigating
|
||||||
|
showBrowserSearch(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -1582,6 +1591,9 @@ async function browsePath(folderId, path, offset = 0, nocache = false) {
|
|||||||
renderBrowserItems(cachedItems);
|
renderBrowserItems(cachedItems);
|
||||||
renderPagination();
|
renderPagination();
|
||||||
|
|
||||||
|
// Show search bar when inside a folder
|
||||||
|
showBrowserSearch(true);
|
||||||
|
|
||||||
// Show/hide Play All button based on whether media items exist
|
// Show/hide Play All button based on whether media items exist
|
||||||
const hasMedia = data.items.some(item => item.is_media);
|
const hasMedia = data.items.some(item => item.is_media);
|
||||||
document.getElementById('playAllBtn').style.display = hasMedia ? '' : 'none';
|
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) {
|
function setViewMode(mode) {
|
||||||
if (mode === viewMode) return;
|
if (mode === viewMode) return;
|
||||||
viewMode = mode;
|
viewMode = mode;
|
||||||
@@ -2084,7 +2144,7 @@ function setViewMode(mode) {
|
|||||||
|
|
||||||
// Re-render current view from cache (no network request)
|
// Re-render current view from cache (no network request)
|
||||||
if (currentFolderId && cachedItems) {
|
if (currentFolderId && cachedItems) {
|
||||||
renderBrowserItems(cachedItems);
|
applyBrowserSearch();
|
||||||
} else {
|
} else {
|
||||||
showRootFolders();
|
showRootFolders();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,7 @@
|
|||||||
"browser.view_grid": "Grid view",
|
"browser.view_grid": "Grid view",
|
||||||
"browser.view_compact": "Compact view",
|
"browser.view_compact": "Compact view",
|
||||||
"browser.view_list": "List view",
|
"browser.view_list": "List view",
|
||||||
|
"browser.search": "Search...",
|
||||||
"browser.items_per_page": "Items per page:",
|
"browser.items_per_page": "Items per page:",
|
||||||
"browser.page": "Page",
|
"browser.page": "Page",
|
||||||
"browser.previous": "Previous",
|
"browser.previous": "Previous",
|
||||||
|
|||||||
@@ -124,6 +124,7 @@
|
|||||||
"browser.view_grid": "Сетка",
|
"browser.view_grid": "Сетка",
|
||||||
"browser.view_compact": "Компактный вид",
|
"browser.view_compact": "Компактный вид",
|
||||||
"browser.view_list": "Список",
|
"browser.view_list": "Список",
|
||||||
|
"browser.search": "Поиск...",
|
||||||
"browser.items_per_page": "Элементов на странице:",
|
"browser.items_per_page": "Элементов на странице:",
|
||||||
"browser.page": "Страница",
|
"browser.page": "Страница",
|
||||||
"browser.previous": "Предыдущая",
|
"browser.previous": "Предыдущая",
|
||||||
|
|||||||
Reference in New Issue
Block a user