Layout and search fix
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 2m4s
🏗️ Build and Test JellyTau / Android Compile Check (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Successful in 23s
Build & Release / Run Tests (push) Failing after 2m45s
Build & Release / Build Linux (push) Has been skipped
Build & Release / Build Android (push) Has been skipped
Build & Release / Create Release (push) Has been skipped

This commit is contained in:
2026-07-11 19:55:55 +02:00
parent a2cd9978f0
commit 2a1f1689b4
20 changed files with 991 additions and 995 deletions
@@ -1,6 +1,7 @@
<!-- TRACES: UR-007, UR-029, UR-030 | DR-007, DR-032, DR-033 -->
<script lang="ts">
import { onMount } from "svelte";
import { onMount, onDestroy } from "svelte";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { goto } from "$app/navigation";
import { navigateBack } from "$lib/utils/navigation";
import { currentLibrary } from "$lib/stores/library";
@@ -13,7 +14,7 @@
import BackButton from "$lib/components/common/BackButton.svelte";
import ResultsCounter from "$lib/components/common/ResultsCounter.svelte";
import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload";
import type { MediaItem, Library, ItemType } from "$lib/api/types";
import type { MediaItem, Library, ItemType, SearchResult } from "$lib/api/types";
import LibraryGrid from "./LibraryGrid.svelte";
import TrackList from "./TrackList.svelte";
import AlphabetScrollBar from "./AlphabetScrollBar.svelte";
@@ -54,6 +55,31 @@
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
let initialLoadDone = false;
/**
* Payload of the backend `search-event` (mirrors Rust `SearchUpdateEvent`).
* `repo.search()` resolves instantly with cache-only (downloaded) results;
* the merged cache+server union arrives later via this event.
*/
interface SearchUpdateEvent {
requestId: number;
result: SearchResult;
}
// Monotonic id identifying the latest search request. The deferred
// `search-event` is only applied when its requestId still matches, so
// out-of-order / superseded server results never clobber fresher ones.
let searchRequestId = 0;
let unlistenSearch: UnlistenFn | null = null;
async function ensureSearchListener() {
if (unlistenSearch) return;
unlistenSearch = await listen<SearchUpdateEvent>("search-event", (event) => {
const { requestId, result } = event.payload;
if (requestId !== searchRequestId) return;
items = excludePodcasts(result.items);
});
}
$effect(() => {
sortBy = config.defaultSort;
});
@@ -82,12 +108,26 @@
// Use backend search if search query is provided, otherwise use getItems with sort
// HACK: excludePodcasts drops the "Podcasts" folder stored in the music library.
if (debouncedSearchQuery.trim()) {
const result = await repo.search(debouncedSearchQuery, {
includeItemTypes: [config.itemType],
limit: 10000,
});
items = excludePodcasts(result.items);
// Phase 1: instant cache-only (downloaded) results. The merged
// cache+server union arrives later via the `search-event` listener,
// tagged with this requestId so superseded queries are ignored.
await ensureSearchListener();
const requestId = ++searchRequestId;
const result = await repo.search(
debouncedSearchQuery,
{
includeItemTypes: [config.itemType],
limit: 10000,
},
requestId
);
// Only apply if this is still the active query.
if (requestId === searchRequestId) {
items = excludePodcasts(result.items);
}
} else {
// Leaving search — invalidate any in-flight server results.
searchRequestId++;
const result = await repo.getItems($currentLibrary.id, {
includeItemTypes: [config.itemType],
sortBy,
@@ -120,6 +160,11 @@
}, 300);
});
onDestroy(() => {
if (unlistenSearch) unlistenSearch();
if (searchTimeout) clearTimeout(searchTimeout);
});
function handleSort(newSort: string) {
sortBy = newSort;
loadItems();