Fix sleep bug, fix menu return
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m1s
Traceability Validation / Check Requirement Traces (push) Successful in 23s
Build & Release / Run Tests (push) Successful in 4m7s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 19m5s
Build & Release / Build Linux (push) Successful in 16m20s
Build & Release / Build Android (push) Successful in 19m12s
Build & Release / Create Release (push) Successful in 8s

This commit is contained in:
2026-07-01 23:49:51 +02:00
parent 342f95cac1
commit 75014ee00f
22 changed files with 880 additions and 149 deletions
+21 -32
View File
@@ -4,6 +4,7 @@
import { writable, derived } from "svelte/store";
import type { MediaItem } from "$lib/api/types";
import { auth } from "./auth";
import { buildHeroMix } from "$lib/utils/heroMix";
/** A single "by genre" row: the genre name plus the series in it. */
export interface GenreRow {
@@ -45,36 +46,11 @@ function createTvStore() {
const { subscribe, set, update } = writable<TvState>(initialState);
/**
* Build the hero rotation. Prefer in-progress episodes (most personal),
* then fall back to next-up, then recently added. De-duplicates by id and
* prefers items that carry backdrop/primary artwork for a good banner.
*/
function buildHero(
continueWatching: MediaItem[],
nextUp: MediaItem[],
recentlyAdded: MediaItem[]
): MediaItem[] {
const hasArt = (i: MediaItem) =>
!!(i.backdropImageTags && i.backdropImageTags.length > 0) ||
!!(i.parentBackdropImageTags && i.parentBackdropImageTags.length > 0) ||
!!i.primaryImageTag;
const result: MediaItem[] = [];
const seen = new Set<string>();
for (const pool of [continueWatching, nextUp, recentlyAdded]) {
for (const item of pool) {
if (result.length >= 6) break;
if (hasArt(item) && !seen.has(item.id)) {
seen.add(item.id);
result.push(item);
}
}
}
return result.slice(0, 6);
}
/** Artwork check for hero candidates: own/parent backdrop or primary image. */
const hasArt = (i: MediaItem) =>
!!(i.backdropImageTags && i.backdropImageTags.length > 0) ||
!!(i.parentBackdropImageTags && i.parentBackdropImageTags.length > 0) ||
!!i.primaryImageTag;
async function loadSections(libraryId: string) {
update(s => ({
@@ -86,17 +62,30 @@ function createTvStore() {
try {
const repo = auth.getRepository();
const [resume, nextUp, latest] = await Promise.all([
const [resume, nextUp, latest, surprise] = await Promise.all([
repo.getResumeItems(libraryId, SECTION_LIMIT),
repo.getNextUpEpisodes(undefined, SECTION_LIMIT),
repo.getLatestItems(libraryId, SECTION_LIMIT),
// Random pool so the hero rotation changes between visits (SortBy=Random
// shuffles server-side online, and via SQLite RANDOM() offline).
repo
.getItems(libraryId, {
includeItemTypes: ["Series"],
sortBy: "Random",
recursive: true,
limit: SECTION_LIMIT,
})
.then(r => r.items)
.catch(() => [] as MediaItem[]),
]);
// Resume items are already video-only from the server, but keep episodes
// (and the occasional movie that lives in a mixed library) defensively.
const continueWatching = resume.filter(i => i.type === "Episode" || i.type === "Movie");
const heroItems = buildHero(continueWatching, nextUp, latest);
// Mix the hero: in-progress episodes first (most personal), then next-up,
// recent additions, and random series from across the library.
const heroItems = buildHeroMix([continueWatching, nextUp, latest, surprise], hasArt);
update(s => ({
...s,