Compare commits

..
4 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 4.8 2038028d42 test(web): no-op scrollIntoView in vitest setup (jsdom lacks it) — #1931
test-web / test (push) Successful in 33s
The queue auto-scroll $effect calls scrollIntoView on render, and jsdom
doesn't implement it, so QueueDrawer.test.ts threw an unhandled TypeError that
failed the run even though every assertion passed. Polyfill it as a no-op in
the shared setup; tests never assert on scroll position.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:27:35 -04:00
bvandeusenandClaude Opus 4.8 723293110d feat(player): scroll web queue to now-playing track on open (Android parity) — #1931
test-web / test (push) Failing after 38s
QueueList gains an `active` prop; when it flips true (drawer opens) or on mount
(now-playing panel) it centers the current row in view. Index/length are read
untracked so it positions once per open rather than following auto-advance,
matching the Android queue. QueueDrawer passes active={queueDrawerOpen} since
its aside is always mounted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:24:29 -04:00
bvandeusenandClaude Opus 4.8 41ebf1405b fix(player): open Android queue scrolled to now-playing track — #1929
android / Build + lint + test (push) Successful in 4m8s
QueueList used a plain LazyColumn with no hoisted state, so the queue always
opened at the top and the current track could be off-screen. Seed a
rememberLazyListState with the current index (coerced into bounds) so the list
renders already positioned on the now-playing row — no post-layout scroll flash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:11:32 -04:00
bvandeusenandClaude Opus 4.8 f2dcf2596d fix(player): render QueueDrawer inside QueryClientProvider so queue LikeButtons resolve — #1928
test-web / test (push) Successful in 40s
The queue drawer's <aside> is always mounted, so QueueTrackRow's LikeButton
(added in #1596) instantiates the moment the queue populates on first play.
LikeButton calls useQueryClient() at init; with the drawer outside the
provider it threw 'No QueryClient was found in Svelte context', aborting the
reactive flush that starts playback — so play appeared to do nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:47:21 -04:00
6 changed files with 53 additions and 67 deletions
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -86,7 +87,14 @@ private fun QueueList(
onJumpTo: (Int) -> Unit,
onToggleLike: (String) -> Unit,
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
// Open scrolled to the now-playing track so it's in view immediately.
// Seeding the initial index (rather than animating post-layout) avoids a
// flash of the list top; it's captured once per entry, so the view stays
// put as the track later auto-advances — matching "show me where I am now."
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = currentIndex.coerceIn(0, tracks.lastIndex),
)
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
itemsIndexed(items = tracks, key = { _, track -> track.id }) { index, track ->
QueueRow(
track = track,
-61
View File
@@ -1,61 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":semanticCommits"
],
"baseBranches": ["dev"],
"timezone": "America/New_York",
"schedule": ["every weekend"],
"prHourlyLimit": 2,
"prConcurrentLimit": 8,
"ignorePaths": [
"**/node_modules/**",
"**/vendor/**",
"flutter_client/**"
],
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 5am on the first day of the month"]
},
"packageRules": [
{
"description": "Auto-merge patch/minor/digest/pin bumps once CI is green",
"matchUpdateTypes": ["minor", "patch", "digest", "pin"],
"automerge": true
},
{
"description": "Hold all major bumps for manual approval via the dependency dashboard",
"matchUpdateTypes": ["major"],
"automerge": false,
"dependencyDashboardApproval": true,
"addLabels": ["deps", "deps:major"]
},
{
"description": "Group Go module updates into one PR",
"matchManagers": ["gomod"],
"groupName": "go modules"
},
{
"description": "Group CI workflow action bumps",
"matchManagers": ["github-actions"],
"groupName": "ci actions"
},
{
"description": "Group Docker base-image bumps (Dockerfile + compose)",
"matchManagers": ["dockerfile", "docker-compose"],
"groupName": "docker images"
},
{
"description": "Group the Android Gradle/Kotlin toolchain",
"matchManagers": ["gradle", "gradle-wrapper"],
"groupName": "android gradle"
},
{
"description": "Group web npm non-major bumps",
"matchManagers": ["npm"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "web npm (non-major)"
}
]
}
+5 -1
View File
@@ -35,5 +35,9 @@
transition-transform duration-200
{player.queueDrawerOpen ? 'translate-x-0' : 'translate-x-full'}"
>
<QueueList onClose={() => closeQueueDrawer()} bind:closeButtonRef={closeButton} />
<QueueList
onClose={() => closeQueueDrawer()}
active={player.queueDrawerOpen}
bind:closeButtonRef={closeButton}
/>
</aside>
+24 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { untrack } from 'svelte';
import { X } from 'lucide-svelte';
import { player } from '$lib/player/store.svelte';
import QueueTrackRow from './QueueTrackRow.svelte';
@@ -8,12 +9,33 @@
// now-playing route (visible at lg+ widths) omits it.
// closeButtonRef: bind:this hook so the drawer can focus the X for
// keyboard users on open.
// active: true when the queue is on-screen (drawer open, or the always-
// visible now-playing panel). Flipping it true scrolls the now-playing
// row into view — parity with the Android queue, which opens positioned
// on the current track.
type Props = {
onClose?: () => void;
closeButtonRef?: HTMLButtonElement;
active?: boolean;
};
let { onClose, closeButtonRef = $bindable() }: Props = $props();
let { onClose, closeButtonRef = $bindable(), active = true }: Props = $props();
let scrollBody: HTMLElement | undefined = $state();
// When the queue becomes visible, center the now-playing row in view. The
// index/length are read untracked so this fires once per open (matching the
// Android queue's open-positioned behavior) rather than following the track
// as it auto-advances. Deferred a frame so the drawer's slide-in has settled.
$effect(() => {
if (!active || !scrollBody) return;
const body = scrollBody;
const index = untrack(() => player.index);
if (untrack(() => player.queue.length) === 0) return;
requestAnimationFrame(() => {
(body.children[index] as HTMLElement | undefined)?.scrollIntoView({ block: 'center' });
});
});
function totalDurationLabel(tracks: { duration_sec: number }[]): string {
const totalSec = tracks.reduce((s, tr) => s + (tr.duration_sec ?? 0), 0);
@@ -45,7 +67,7 @@
{/if}
</div>
<div class="flex-1 overflow-y-auto">
<div bind:this={scrollBody} class="flex-1 overflow-y-auto">
{#if player.queue.length === 0}
<p class="text-text-secondary text-center p-8">No tracks queued.</p>
{:else}
+7 -2
View File
@@ -168,9 +168,14 @@
style="display: none"
></audio>
<QueueDrawer />
<QueryClientProvider client={queryClient}>
<!-- QueueDrawer must be inside the provider: its rows render LikeButton,
which calls useQueryClient() at init. The drawer's <aside> is always
mounted, so the moment the queue is populated (on first play) those
LikeButtons instantiate — outside the provider they throw
"No QueryClient was found" and abort the play flush. -->
<QueueDrawer />
{#if user.value !== null && page.url.pathname !== '/login' && page.url.pathname !== '/now-playing'}
<Shell>{@render children()}</Shell>
{:else}
+8
View File
@@ -37,6 +37,14 @@ if (typeof window !== 'undefined') {
Object.defineProperty(window, 'sessionStorage', { configurable: true, value: memSession });
}
// jsdom doesn't implement Element.prototype.scrollIntoView. Components that
// call it (queue auto-scroll to the now-playing row, the alphabetical rail)
// would throw an unhandled TypeError in tests — which fails the run even when
// every assertion passes. No-op it; tests never assert on scroll position.
if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) {
Element.prototype.scrollIntoView = () => {};
}
// W-T3 moved toast rendering out of per-page markup into a single
// <ToastHost /> mounted in +layout.svelte. Tests render individual pages
// without the layout, so we mount ToastHost here so `pushToast()` calls