feat(library): genre sliders, artist links, and navigation utils
- music landing: diverse per-genre album sliders (online counts / offline wide-probe fallback) and home-screen library shortcuts - add ArtistLinks component and shared navigation/genreDiversity utils - player/playback-mode refinements across Rust and frontend
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
import { auth, isAuthenticated, isLoading as isAuthLoading, currentUser } from "$lib/stores/auth";
|
||||
import { library } from "$lib/stores/library";
|
||||
import { currentMedia, isPlaying, playbackPosition, playbackDuration, shouldShowAudioMiniPlayer } from "$lib/stores/player";
|
||||
import { isShuffle, repeatMode, hasNext as hasNextStore, hasPrevious as hasPreviousStore } from "$lib/stores/queue";
|
||||
import { useScrollGuard } from "$lib/composables/useScrollGuard";
|
||||
import Search from "$lib/components/Search.svelte";
|
||||
import MiniPlayer from "$lib/components/player/MiniPlayer.svelte";
|
||||
@@ -23,16 +24,15 @@
|
||||
let showFullPlayer = $state(false);
|
||||
let showOverflowMenu = $state(false);
|
||||
let showSleepTimerModal = $state(false);
|
||||
let shuffle = $state(false);
|
||||
let repeat = $state<"off" | "all" | "one">("off");
|
||||
let hasNext = $state(false);
|
||||
let hasPrevious = $state(false);
|
||||
// Queue status (shuffle, repeat, hasNext, hasPrevious) is event-driven via the
|
||||
// queue store, which listens for queue_changed events from the backend. This
|
||||
// updates instantly when toggling shuffle/repeat (no polling lag).
|
||||
const shuffle = $derived($isShuffle);
|
||||
const repeat = $derived($repeatMode);
|
||||
const hasNext = $derived($hasNextStore);
|
||||
const hasPrevious = $derived($hasPreviousStore);
|
||||
let isAndroid = $state(false);
|
||||
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let failedAttempts = 0;
|
||||
const MAX_SILENT_FAILURES = 3; // Don't log errors for first 3 attempts
|
||||
|
||||
onMount(() => {
|
||||
// Detect platform
|
||||
try {
|
||||
@@ -42,17 +42,7 @@
|
||||
console.error("Platform detection failed:", err);
|
||||
}
|
||||
|
||||
// Poll for queue status (shuffle, repeat, hasNext, hasPrevious)
|
||||
// Position/duration come from player events (pushed every 250ms)
|
||||
// Add a small delay before starting polling to ensure player is ready on Android
|
||||
const timeoutId = setTimeout(() => {
|
||||
updateQueueStatus(); // Initial update
|
||||
pollInterval = setInterval(updateQueueStatus, 1000);
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
scrollGuard.cleanup();
|
||||
};
|
||||
});
|
||||
@@ -64,26 +54,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function updateQueueStatus() {
|
||||
try {
|
||||
const queue = await commands.playerGetQueue();
|
||||
|
||||
// Reset failure counter on success
|
||||
failedAttempts = 0;
|
||||
|
||||
hasNext = queue.hasNext;
|
||||
hasPrevious = queue.hasPrevious;
|
||||
shuffle = queue.shuffle;
|
||||
repeat = queue.repeat as "off" | "all" | "one";
|
||||
} catch (e) {
|
||||
failedAttempts++;
|
||||
// Only log errors after initial attempts (player may still be initializing)
|
||||
if (failedAttempts > MAX_SILENT_FAILURES) {
|
||||
console.error("[Queue Status] Error:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await auth.logout();
|
||||
library.reset();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount, untrack } from "svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigateBack } from "$lib/utils/navigation";
|
||||
import { commands } from "$lib/api/bindings";
|
||||
import type { MediaItem, Library } from "$lib/api/types";
|
||||
import { library, libraryItems, isLibraryLoading, currentLibrary, libraries } from "$lib/stores/library";
|
||||
@@ -23,6 +24,8 @@
|
||||
import CrewLinks from "$lib/components/library/CrewLinks.svelte";
|
||||
import GenreTags from "$lib/components/library/GenreTags.svelte";
|
||||
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
||||
import BackButton from "$lib/components/common/BackButton.svelte";
|
||||
import ArtistLinks from "$lib/components/library/ArtistLinks.svelte";
|
||||
|
||||
interface SeasonData {
|
||||
season: MediaItem;
|
||||
@@ -281,6 +284,13 @@
|
||||
// Navigate to series page without the episode param
|
||||
goto(`/library/${itemId}`);
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
// Prefer real history so the user returns to wherever they came from
|
||||
// (libraries grid, a list page, a parent album/artist, search, etc.),
|
||||
// falling back to the libraries overview on a fresh deep-link.
|
||||
navigateBack("/library");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
@@ -315,6 +325,9 @@
|
||||
{:else if item}
|
||||
<!-- Person Detail View - shown for Person items -->
|
||||
{#if item.type === "Person"}
|
||||
<div class="pt-4">
|
||||
<BackButton onClick={goBack} label="Back" />
|
||||
</div>
|
||||
<PersonDetailView person={item} />
|
||||
<!-- Episode Focus View - shown when navigating with ?episode param -->
|
||||
{:else if item.type === "Series" && focusedEpisode}
|
||||
@@ -326,8 +339,13 @@
|
||||
/>
|
||||
{:else}
|
||||
<div class="space-y-8">
|
||||
<!-- Back navigation -->
|
||||
<div class="pt-4">
|
||||
<BackButton onClick={goBack} label="Back" />
|
||||
</div>
|
||||
|
||||
<!-- Header with item info -->
|
||||
<div class="flex gap-6 pt-4">
|
||||
<div class="flex gap-6">
|
||||
<!-- Poster -->
|
||||
<div class="flex-shrink-0 w-48">
|
||||
{#if item.primaryImageTag}
|
||||
@@ -362,10 +380,17 @@
|
||||
{#if item.parentIndexNumber && item.indexNumber}, {/if}
|
||||
{#if item.indexNumber}Episode {item.indexNumber}{/if}
|
||||
</p>
|
||||
{:else if item.productionYear || item.artists?.length}
|
||||
{:else if item.artistItems?.length || item.artists?.length}
|
||||
<p class="text-lg text-gray-400 mt-1">
|
||||
{item.artists?.join(", ") || item.productionYear}
|
||||
<ArtistLinks
|
||||
artistItems={item.artistItems}
|
||||
artists={item.artists}
|
||||
linkClass="text-lg text-[var(--color-jellyfin)] hover:underline"
|
||||
textClass="text-gray-400"
|
||||
/>
|
||||
</p>
|
||||
{:else if item.productionYear}
|
||||
<p class="text-lg text-gray-400 mt-1">{item.productionYear}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigateBack } from "$lib/utils/navigation";
|
||||
import { currentLibrary } from "$lib/stores/library";
|
||||
import { movies } from "$lib/stores/movies";
|
||||
import { isServerReachable } from "$lib/stores/connectivity";
|
||||
@@ -85,7 +86,7 @@
|
||||
<div class="flex items-center justify-between px-4">
|
||||
<h1 class="text-3xl font-bold text-white">{$currentLibrary?.name ?? "Movies"}</h1>
|
||||
<button
|
||||
onclick={() => goto("/library")}
|
||||
onclick={() => navigateBack("/library")}
|
||||
class="p-2 rounded-lg hover:bg-white/10 transition-colors text-gray-400 hover:text-white"
|
||||
title="Back to libraries"
|
||||
aria-label="Back to libraries"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigateBack } from "$lib/utils/navigation";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { currentLibrary } from "$lib/stores/library";
|
||||
import { music } from "$lib/stores/music";
|
||||
@@ -86,6 +87,7 @@
|
||||
const newlyAdded = $derived($music.newlyAdded);
|
||||
const playlists = $derived($music.playlists);
|
||||
const rediscover = $derived($music.rediscover);
|
||||
const genreRows = $derived($music.genreRows);
|
||||
const isLoading = $derived($music.isLoading);
|
||||
const hasContent = $derived(
|
||||
heroItems.length > 0 ||
|
||||
@@ -106,7 +108,7 @@
|
||||
<div class="flex items-center justify-between px-4">
|
||||
<h1 class="text-3xl font-bold text-white">Music</h1>
|
||||
<button
|
||||
onclick={() => goto("/library")}
|
||||
onclick={() => navigateBack("/library")}
|
||||
class="p-2 rounded-lg hover:bg-white/10 transition-colors text-gray-400 hover:text-white"
|
||||
title="Back to libraries"
|
||||
aria-label="Back to libraries"
|
||||
@@ -160,6 +162,16 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- One slider per genre -->
|
||||
{#each genreRows as row (row.id)}
|
||||
<Carousel
|
||||
title={row.name}
|
||||
items={row.items}
|
||||
onItemClick={handleItemClick}
|
||||
showAll={() => goto("/library/music/genres")}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if !hasContent}
|
||||
<p class="px-4 text-gray-400">Nothing here yet. Start playing some music to fill this page.</p>
|
||||
{/if}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigateBack } from "$lib/utils/navigation";
|
||||
import { currentLibrary } from "$lib/stores/library";
|
||||
import { tv } from "$lib/stores/tv";
|
||||
import { isServerReachable } from "$lib/stores/connectivity";
|
||||
@@ -92,7 +93,7 @@
|
||||
<div class="flex items-center justify-between px-4">
|
||||
<h1 class="text-3xl font-bold text-white">{$currentLibrary?.name ?? "TV Shows"}</h1>
|
||||
<button
|
||||
onclick={() => goto("/library")}
|
||||
onclick={() => navigateBack("/library")}
|
||||
class="p-2 rounded-lg hover:bg-white/10 transition-colors text-gray-400 hover:text-white"
|
||||
title="Back to libraries"
|
||||
aria-label="Back to libraries"
|
||||
|
||||
Reference in New Issue
Block a user