feat(library): focused music/TV/movie landing screens + self-draining download queue
Library screens:
- Add dedicated music, TV, and movie landing pages (hero banner +
horizontal carousels) backed by new music/tv/movies stores.
- Route tvshows libraries to /library/tv; surface rediscover ("haven't
listened to in a while") albums via a new repository method across
online/offline/hybrid repos plus the repository_get_rediscover_albums
command.
- Add an A-Z jump bar for long alphabetically-sorted lists, with grid
index anchors in LibraryGrid/LibraryListView/TrackList.
- Filter the "Podcasts" folder out of music library queries.
Downloads:
- Add a backend queue pump: enqueue_download / enqueue_video_downloads
persist the resolved stream URL + target dir on each row (migration
017), and the pump starts up to max_concurrent and drains the rest
automatically as slots free, instead of the frontend silently dropping
items past the concurrency limit. Album/series/season buttons now
enqueue rather than calling start_download directly.
Other fixes:
- Hybrid search now returns instant cache results and pushes the merged
cache+server union via a request-id-tagged search-event, so superseded
queries can't clobber fresher results.
- URL-encode SearchTerm / genres / item types in online repo requests.
- Android: pause on audio-becoming-noisy (headphone/BT disconnect).
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
import { goto } from "$app/navigation";
|
||||
import { currentLibrary } from "$lib/stores/library";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { shouldShowAudioMiniPlayer } from "$lib/stores/player";
|
||||
import { isAndroid } from "$lib/stores/appState";
|
||||
import SearchBar from "$lib/components/common/SearchBar.svelte";
|
||||
import SortButtonGroup from "$lib/components/common/SortButtonGroup.svelte";
|
||||
import type { SortOption } from "$lib/components/common/SortButtonGroup.svelte";
|
||||
@@ -13,6 +15,8 @@
|
||||
import type { MediaItem, Library, ItemType } from "$lib/api/types";
|
||||
import LibraryGrid from "./LibraryGrid.svelte";
|
||||
import TrackList from "./TrackList.svelte";
|
||||
import AlphabetScrollBar from "./AlphabetScrollBar.svelte";
|
||||
import { excludePodcasts } from "$lib/utils/podcastFilter";
|
||||
|
||||
/**
|
||||
* Generic media list page supporting Albums, Artists, Playlists, and Tracks
|
||||
@@ -41,6 +45,7 @@
|
||||
|
||||
let items = $state<MediaItem[]>([]);
|
||||
let loading = $state(true);
|
||||
let gridWrapper = $state<HTMLDivElement | null>(null);
|
||||
let searchQuery = $state("");
|
||||
let debouncedSearchQuery = $state("");
|
||||
let sortBy = $state<string>("");
|
||||
@@ -74,12 +79,13 @@
|
||||
const repo = auth.getRepository();
|
||||
|
||||
// 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 = result.items;
|
||||
items = excludePodcasts(result.items);
|
||||
} else {
|
||||
const result = await repo.getItems($currentLibrary.id, {
|
||||
includeItemTypes: [config.itemType],
|
||||
@@ -88,7 +94,7 @@
|
||||
recursive: true,
|
||||
limit: 10000,
|
||||
});
|
||||
items = result.items;
|
||||
items = excludePodcasts(result.items);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to load ${config.itemType}:`, e);
|
||||
@@ -142,6 +148,54 @@
|
||||
goto(`/library/${track.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== A-Z jump bar =====
|
||||
// Bucket a name to its index letter: A-Z, or "#" for digits/symbols/empty.
|
||||
function letterFor(name: string): string {
|
||||
const first = (name ?? "").trim().charAt(0).toUpperCase();
|
||||
return first >= "A" && first <= "Z" ? first : "#";
|
||||
}
|
||||
|
||||
// Only meaningful when the list is sorted alphabetically and long enough to scroll.
|
||||
const isAlphaSorted = $derived(sortBy === "SortName");
|
||||
const showAlphaBar = $derived(
|
||||
isAlphaSorted &&
|
||||
!loading &&
|
||||
!debouncedSearchQuery.trim() &&
|
||||
items.length > 30
|
||||
);
|
||||
|
||||
const availableLetters = $derived.by(() => {
|
||||
const set = new Set<string>();
|
||||
if (showAlphaBar) {
|
||||
for (const item of items) set.add(letterFor(item.name));
|
||||
}
|
||||
return set;
|
||||
});
|
||||
|
||||
// First item index for each letter, honouring current ascending/descending order.
|
||||
const firstIndexForLetter = $derived.by(() => {
|
||||
const map = new Map<string, number>();
|
||||
items.forEach((item, index) => {
|
||||
const letter = letterFor(item.name);
|
||||
if (!map.has(letter)) map.set(letter, index);
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
function jumpToLetter(letter: string) {
|
||||
const index = firstIndexForLetter.get(letter);
|
||||
if (index === undefined || !gridWrapper) return;
|
||||
const target = gridWrapper.querySelector(`[data-grid-index="${index}"]`);
|
||||
target?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
|
||||
// Bottom space the layout's <main> reserves for the nav / mini-player bars.
|
||||
// Mirrors src/routes/library/+layout.svelte so the A-Z strip ends just above
|
||||
// whichever bars are visible.
|
||||
const bottomGap = $derived(
|
||||
$shouldShowAudioMiniPlayer ? ($isAndroid ? "11rem" : "7rem") : "5rem"
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
@@ -192,10 +246,19 @@
|
||||
<p>No {config.title.toLowerCase()} found</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#if config.displayComponent === "grid"}
|
||||
<LibraryGrid items={items} onItemClick={handleItemClick} musicContent={["MusicAlbum", "MusicArtist", "Audio", "Playlist"].includes(config.itemType)} />
|
||||
{:else if config.displayComponent === "tracklist"}
|
||||
<TrackList tracks={items} onTrackClick={handleTrackClick} />
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<div bind:this={gridWrapper} class="flex-1 min-w-0">
|
||||
{#if config.displayComponent === "grid"}
|
||||
<LibraryGrid items={items} onItemClick={handleItemClick} musicContent={["MusicAlbum", "MusicArtist", "Audio", "Playlist"].includes(config.itemType)} />
|
||||
{:else if config.displayComponent === "tracklist"}
|
||||
<TrackList tracks={items} onTrackClick={handleTrackClick} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if showAlphaBar}
|
||||
<div class="sticky top-2 self-start flex-shrink-0 h-fit">
|
||||
<AlphabetScrollBar {availableLetters} onJump={jumpToLetter} {bottomGap} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user