From 16658889a202aca6c4e0b29fc356bea6ebfe598d Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Thu, 20 Aug 2026 23:11:30 +0200 Subject: [PATCH] fix(home): restart the hero banner timer on a manual change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rotation interval was installed once when the banner mounted and never touched again, so a swipe, arrow or dot tap inherited whatever was left of the running countdown — swiping 5.5s into a 6s interval moved the banner on half a second later. The timer moves into heroRotation.ts as a small restartable object so it can be unit-tested, and every manual navigation path restarts it from that moment. Verified red-first: with restart() reverted to leave a running timer alone, the regression test fails. Release 0.9.1. --- CHANGELOG.md | 14 ++++ docs/defect-windows.md | 1 + docs/requirements.md | 1 + package.json | 2 +- packaging/arch/PKGBUILD | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/lib/components/home/HeroBanner.svelte | 32 ++++++-- src/lib/components/home/heroRotation.test.ts | 82 ++++++++++++++++++++ src/lib/components/home/heroRotation.ts | 40 ++++++++++ 11 files changed, 168 insertions(+), 12 deletions(-) create mode 100644 src/lib/components/home/heroRotation.test.ts create mode 100644 src/lib/components/home/heroRotation.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 177517e0..332f71d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,20 @@ generated trace matrix lives in [docs/traceability.md](docs/traceability.md). For how long each fixed defect had been shipping before it was found, see [docs/defect-windows.md](docs/defect-windows.md). +## v0.9.1 + +A one-line fix to the home screen, released on its own because it is the kind of +small wrongness you notice every time. + +### 🐛 Fixes + +- **Swiping the hero banner now buys you a full six seconds.** The rotation + timer was started once when the banner appeared and then left alone, so a + swipe, arrow or dot tap inherited whatever was left of the running countdown + — swipe five and a half seconds in and the banner moved on half a second + later, before you had read the title. Any manual change now restarts the + countdown from that moment. (UR-034 → DR-038) + ## v0.9.0 An audit release. One new setting you asked for, two naming bugs that only ever diff --git a/docs/defect-windows.md b/docs/defect-windows.md index 43c418c0..92c8395e 100644 --- a/docs/defect-windows.md +++ b/docs/defect-windows.md @@ -36,6 +36,7 @@ went unexercised until a later feature leaned on them. | `download_album` read its track list from the local cache (DR-173) | v0.0.1 | **v0.5.5** | ~8 weeks | pickaxe | | Device profile carried no `MaxAudioChannels` (DR-141) | v0.0.1 | **v0.4.6** | ~7 weeks | absence | | Streaming ceiling fixed at 20 Mbps with no way to lower it (UR-074) | v0.0.1 | **v0.5.3** (as a feature) | ~7.5 weeks | pickaxe | +| Hero banner auto-rotation never restarted after a manual swipe (DR-038) | v0.0.1 | **v0.9.1** | ~8.5 weeks | pickaxe | ### Why they took so long to surface diff --git a/docs/requirements.md b/docs/requirements.md index 4c95fdec..b0756705 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -698,6 +698,7 @@ Internal architecture, components, and application logic. | UT-205 | Queued download paths cannot escape the download root — traversal, absolute and `..` forms are refused — while the four real path shapes the app builds, including the absolute one `download_series` produces, come back unchanged; and a completed download cannot register a file outside the root | DR-211 | Done | | UT-206 | The offline item-type filter is bound rather than interpolated (a value containing a quote and `OR 1=1` matches nothing instead of disabling the `WHERE`), `build_get_items_endpoint` percent-encodes its values while preserving the commas Jellyfin splits on, and volume normalisation clamps out-of-range input and maps NaN to a finite value | DR-212 | Done | | UT-200 | The stream a player could only restart is refused its retry: the handoff transcode answers yes to `player_retry_restarts_stream` while music, video and a downloaded episode answer no, and the Kotlin decision starts permissive, flips on a non-resumable load, and is restored by the next ordinary one | DR-203 | Done | +| UT-207 | The hero banner's rotation timer restarts from the moment of a manual change: a swipe 5.5s into a 6s interval waits a further 6s instead of firing the leftover 500ms, repeated restarts never stack timers, and `stop()` ends rotation | DR-038 | Done | ### Integration Tests diff --git a/package.json b/package.json index 2d07a5c9..d0e25a79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jellytau", - "version": "0.9.0", + "version": "0.9.1", "description": "A cross-platform Jellyfin client built with Tauri, SvelteKit and Rust.", "author": "Duncan Tourolle ", "license": "MIT", diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index c37f768e..c0ff89f0 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -8,7 +8,7 @@ # tarball/VCS URL and drop the local-copy prepare() step. pkgname=jellytau -pkgver=0.9.0 +pkgver=0.9.1 pkgrel=1 pkgdesc="A cross-platform Jellyfin client" arch=('x86_64') diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b15f2c88..fa4682db 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2018,7 +2018,7 @@ dependencies = [ [[package]] name = "jellytau" -version = "0.9.0" +version = "0.9.1" dependencies = [ "aes-gcm", "async-trait", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 497f2e0e..fb4d41c4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jellytau" -version = "0.9.0" +version = "0.9.1" description = "A cross-platform Jellyfin client" authors = ["Duncan Tourolle "] license = "MIT" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 07541ec0..2433fb7e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "JellyTau", - "version": "0.9.0", + "version": "0.9.1", "identifier": "com.dtourolle.jellytau", "build": { "beforeDevCommand": "bun run dev", diff --git a/src/lib/components/home/HeroBanner.svelte b/src/lib/components/home/HeroBanner.svelte index 018bea7c..aced0a72 100644 --- a/src/lib/components/home/HeroBanner.svelte +++ b/src/lib/components/home/HeroBanner.svelte @@ -3,6 +3,7 @@ import { goto } from "$app/navigation"; import type { MediaItem } from "$lib/api/types"; import CachedImage from "$lib/components/common/CachedImage.svelte"; + import { createRotationTimer, type RotationTimer } from "./heroRotation"; interface Props { items: MediaItem[]; @@ -13,7 +14,7 @@ let { items, autoRotate = true, interval = 6000 }: Props = $props(); let currentIndex = $state(0); - let intervalId: number | null = null; + let rotation: RotationTimer | null = null; // Touch/swipe state let touchStartX = $state(0); @@ -70,8 +71,22 @@ currentIndex = (currentIndex - 1 + items.length) % items.length; } + // Manual navigation (swipe, arrows, dots) restarts the countdown, so the + // banner always waits a full interval after the last change instead of + // firing whatever was left of the previous one. + function showNext() { + next(); + rotation?.restart(); + } + + function showPrev() { + prev(); + rotation?.restart(); + } + function goToIndex(idx: number) { currentIndex = idx; + rotation?.restart(); } // Touch/swipe handlers @@ -96,10 +111,10 @@ if (Math.abs(diff) > swipeThreshold) { if (diff > 0) { // Swiped left - go to next - next(); + showNext(); } else { // Swiped right - go to previous - prev(); + showPrev(); } } @@ -110,9 +125,12 @@ // Auto-rotate logic $effect(() => { if (autoRotate && items.length > 1) { - intervalId = window.setInterval(next, interval); + const timer = createRotationTimer(interval, next); + rotation = timer; + timer.restart(); return () => { - if (intervalId) clearInterval(intervalId); + timer.stop(); + rotation = null; }; } }); @@ -219,7 +237,7 @@