diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueDragMath.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueDragMath.kt new file mode 100644 index 00000000..7d0d8ad8 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueDragMath.kt @@ -0,0 +1,32 @@ +package com.fabledsword.minstrel.player.ui + +import kotlin.math.roundToInt + +/** + * Drag offset to row delta, rounded to the nearest row boundary. The Android + * half of web's `offsetToDelta` (web/src/lib/components/queue-row-math.ts): + * the two clients are meant to agree, and QueueDragMathTest mirrors web's + * cases so both are held to it (#2436). + * + * Ties round toward positive infinity, as JS `Math.round` does: half a row + * down moves one row, half a row up stays put. A row not yet measured + * (height 0) moves nothing. + */ +internal fun queueDragDelta(offsetPx: Float, rowHeightPx: Int): Int = + if (rowHeightPx > 0) (offsetPx / rowHeightPx).roundToInt() else 0 + +/** + * The queue index a row dragged from [index] lands on, clamped to the queue. + * A drag past either end stops at the first or last row. + */ +internal fun queueDragTarget( + index: Int, + offsetPx: Float, + rowHeightPx: Int, + queueSize: Int, +): Int { + // coerceIn throws on an empty range; a row can't be dragged in an empty + // queue, but the composable's keys can briefly outlive the list. + if (queueSize <= 0) return index + return (index + queueDragDelta(offsetPx, rowHeightPx)).coerceIn(0, queueSize - 1) +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueRow.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueRow.kt index d9b55b0a..6dee5a84 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueRow.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueRow.kt @@ -48,7 +48,6 @@ import com.fabledsword.minstrel.shared.formatDuration import com.fabledsword.minstrel.shared.widgets.LikeButton import com.fabledsword.minstrel.shared.widgets.ServerImage import com.fabledsword.minstrel.theme.LocalActionColors -import kotlin.math.roundToInt /* * A single queue row, split out of QueueScreen.kt when swipe-to-remove (#2435) @@ -278,8 +277,7 @@ private fun Modifier.queueReorderDrag( onOffsetChange(offset) }, onDragEnd = { - val delta = if (rowHeightPx > 0) (offset / rowHeightPx).roundToInt() else 0 - val target = (index + delta).coerceIn(0, queueSize - 1) + val target = queueDragTarget(index, offset, rowHeightPx, queueSize) if (target != index) onMove(index, target) offset = 0f onOffsetChange(0f) diff --git a/android/app/src/test/java/com/fabledsword/minstrel/player/ui/QueueDragMathTest.kt b/android/app/src/test/java/com/fabledsword/minstrel/player/ui/QueueDragMathTest.kt new file mode 100644 index 00000000..db08bd35 --- /dev/null +++ b/android/app/src/test/java/com/fabledsword/minstrel/player/ui/QueueDragMathTest.kt @@ -0,0 +1,78 @@ +package com.fabledsword.minstrel.player.ui + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +/** + * The queue drag math (#2436). The delta cases mirror web's + * queue-row-math.test.ts one for one, so a change to either client's + * rounding shows up as a disagreement here rather than as a drop that lands + * one row off on one platform only. + */ +class QueueDragMathTest { + + @Test + fun `zero offset moves nothing`() { + assertEquals(0, queueDragDelta(0f, 64)) + } + + @Test + fun `a full row down moves one`() { + assertEquals(1, queueDragDelta(64f, 64)) + } + + @Test + fun `just under half a row rounds down`() { + assertEquals(0, queueDragDelta(31f, 64)) + } + + @Test + fun `half a row or more rounds up`() { + assertEquals(1, queueDragDelta(32f, 64)) + } + + @Test + fun `drag up is symmetric, with ties toward positive infinity as in JS`() { + assertEquals(-1, queueDragDelta(-64f, 64)) + assertEquals(0, queueDragDelta(-32f, 64)) + } + + @Test + fun `several rows`() { + assertEquals(3, queueDragDelta(192f, 64)) + assertEquals(-2, queueDragDelta(-128f, 64)) + } + + @Test + fun `a row not yet measured moves nothing`() { + assertEquals(0, queueDragDelta(100f, 0)) + assertEquals(0, queueDragDelta(100f, -1)) + } + + @Test + fun `the measured row height is what counts`() { + assertEquals(1, queueDragDelta(56f, 56)) + assertEquals(1, queueDragDelta(28f, 56)) + } + + @Test + fun `a drag past either end stops at the first or last row`() { + assertEquals(0, queueDragTarget(index = 1, offsetPx = -640f, rowHeightPx = 64, queueSize = 5)) + assertEquals(4, queueDragTarget(index = 3, offsetPx = 640f, rowHeightPx = 64, queueSize = 5)) + } + + @Test + fun `a sub-half-row drag lands where it started`() { + assertEquals(2, queueDragTarget(index = 2, offsetPx = 20f, rowHeightPx = 64, queueSize = 5)) + } + + @Test + fun `before the row is measured a drag lands where it started`() { + assertEquals(2, queueDragTarget(index = 2, offsetPx = 300f, rowHeightPx = 0, queueSize = 5)) + } + + @Test + fun `an empty queue does not throw`() { + assertEquals(0, queueDragTarget(index = 0, offsetPx = 64f, rowHeightPx = 64, queueSize = 0)) + } +} diff --git a/web/scripts/check-tint-contrast.js b/web/scripts/check-tint-contrast.js index a86abafc..5d6af080 100644 --- a/web/scripts/check-tint-contrast.js +++ b/web/scripts/check-tint-contrast.js @@ -65,10 +65,13 @@ export function findCss(source) { } // Hues that fail AA as text even on a plain surface, so the raw hue is never -// text at all: error measures 2.64:1 on slate and 3.23 on iron in dark mode, -// against 4.5; error-fg measures 5.30 at worst. A border or outline in the -// hue is a graphic with a 3:1 floor and stays allowed. -export const TEXT_NEVER_RAW = ['error']; +// text at all. Dark mode, against 4.5: +// error 2.64 on slate, 3.23 on iron → error-fg 5.30 at worst +// accent 2.21 on slate, 3.04 on the page → accent-fg 5.62 at worst (#5318) +// Accent icons (the liked heart, active shuffle/repeat) follow too: as +// graphics they only need 3:1, and the raw accent misses even that on iron. +// A border or outline in the hue is a graphic and stays allowed. +export const TEXT_NEVER_RAW = ['error', 'accent']; /** Raw text-X classes and color: var(--fs-X) for the hues above. */ export function findRawText(source) { diff --git a/web/scripts/check-tint-contrast.test.js b/web/scripts/check-tint-contrast.test.js index 8d1e02f2..f2cf3db9 100644 --- a/web/scripts/check-tint-contrast.test.js +++ b/web/scripts/check-tint-contrast.test.js @@ -42,6 +42,13 @@ describe('hue-on-own-tint check (#3150)', () => { expect(findRawText(`.msg { color: var(--fs-error); }`)[0].fix).toBe('var(--fs-error-fg)'); }); + test('raw accent text is caught too, including hover: variants (#5318)', () => { + expect(findRawText(``)[0].fix).toBe('text-accent-fg'); + expect(findRawText(``)).toHaveLength(1); + expect(findRawText(`.pending { color: var(--fs-accent); }`)[0].fix).toBe('var(--fs-accent-fg)'); + expect(findRawText(``)).toEqual([]); + }); + test('passes error-fg, and error as a border or outline', () => { expect(findRawText(`

x

`)).toEqual([]); expect(findRawText(`
`)).toEqual([]); diff --git a/web/src/lib/components/AlphabeticalGrid.svelte b/web/src/lib/components/AlphabeticalGrid.svelte index 39781dc5..22c5bb53 100644 --- a/web/src/lib/components/AlphabeticalGrid.svelte +++ b/web/src/lib/components/AlphabeticalGrid.svelte @@ -201,7 +201,7 @@ cursor: default; } .rail-btn.pending { - color: var(--fs-accent); + color: var(--fs-accent-fg); cursor: wait; } :global(.rail-btn .spin) { diff --git a/web/src/lib/components/LikeButton.svelte b/web/src/lib/components/LikeButton.svelte index 1f9dcc30..7ce07dfa 100644 --- a/web/src/lib/components/LikeButton.svelte +++ b/web/src/lib/components/LikeButton.svelte @@ -44,5 +44,5 @@ aria-label={liked ? 'Unlike' : 'Like'} aria-pressed={liked} onclick={onClick} - class="rounded p-1 {sizeClass} {liked ? 'text-accent' : 'text-text-secondary hover:text-text-primary'}" + class="rounded p-1 {sizeClass} {liked ? 'text-accent-fg' : 'text-text-secondary hover:text-text-primary'}" >{liked ? '♥' : '♡'} diff --git a/web/src/lib/components/MobileAppDownload.svelte b/web/src/lib/components/MobileAppDownload.svelte index 6766e96d..0e16b0a9 100644 --- a/web/src/lib/components/MobileAppDownload.svelte +++ b/web/src/lib/components/MobileAppDownload.svelte @@ -48,9 +48,9 @@ - + Get the Android app diff --git a/web/src/lib/components/QueueTrackRow.svelte b/web/src/lib/components/QueueTrackRow.svelte index e14a245c..dc82e978 100644 --- a/web/src/lib/components/QueueTrackRow.svelte +++ b/web/src/lib/components/QueueTrackRow.svelte @@ -101,7 +101,7 @@ >
{#if isCurrent} - Now playing + Now playing {/if} {track.title}
diff --git a/web/src/routes/admin/+page.svelte b/web/src/routes/admin/+page.svelte index 5cc0c1a2..8c7ec2d3 100644 --- a/web/src/routes/admin/+page.svelte +++ b/web/src/routes/admin/+page.svelte @@ -290,7 +290,7 @@

Pending requests

{#if pendingCount > PREVIEW_LIMIT} -
View all {pendingCount} → + View all {pendingCount} → {/if}
@@ -523,7 +523,7 @@

Quarantine

{#if quarantineCount > PREVIEW_LIMIT} - View all {quarantineCount} → + View all {quarantineCount} → {/if}
diff --git a/web/src/routes/admin/integrations/+page.svelte b/web/src/routes/admin/integrations/+page.svelte index f98096b1..34c9cf82 100644 --- a/web/src/routes/admin/integrations/+page.svelte +++ b/web/src/routes/admin/integrations/+page.svelte @@ -424,7 +424,7 @@

Lidarr

- Search Lidarr from /discover and + Search Lidarr from /discover and route approved requests to it.

diff --git a/web/src/routes/admin/requests/+page.svelte b/web/src/routes/admin/requests/+page.svelte index 99949afe..e84ead20 100644 --- a/web/src/routes/admin/requests/+page.svelte +++ b/web/src/routes/admin/requests/+page.svelte @@ -225,7 +225,7 @@ {rowMeta(r)}
{#if r.imported_album_count > 0 || r.imported_track_count > 0} -
+
{#if r.kind === 'artist'} {r.imported_album_count} {r.imported_album_count === 1 ? 'album' : 'albums'} · {r.imported_track_count} {r.imported_track_count === 1 ? 'track' : 'tracks'} ingested diff --git a/web/src/routes/admin/tuning/+page.svelte b/web/src/routes/admin/tuning/+page.svelte index 48a6d73e..09b88a4e 100644 --- a/web/src/routes/admin/tuning/+page.svelte +++ b/web/src/routes/admin/tuning/+page.svelte @@ -495,7 +495,7 @@ {/each} {#if s.points.length > 1} {:else if s.points.length === 1}

Album not found.

- Back to Library + Back to Library
{:else if query.isError} @@ -131,7 +131,7 @@ in that genre. --> {#if album.year}

- + {album.year}

diff --git a/web/src/routes/artists/[id]/+page.svelte b/web/src/routes/artists/[id]/+page.svelte index 8da5a2e9..9e6ef1ba 100644 --- a/web/src/routes/artists/[id]/+page.svelte +++ b/web/src/routes/artists/[id]/+page.svelte @@ -73,7 +73,7 @@ {#if notFound} {:else if query.isError} diff --git a/web/src/routes/forgot-password/+page.svelte b/web/src/routes/forgot-password/+page.svelte index 4aa90b5c..00d0a610 100644 --- a/web/src/routes/forgot-password/+page.svelte +++ b/web/src/routes/forgot-password/+page.svelte @@ -71,7 +71,7 @@ {/if}

- Remembered? Log in + Remembered? Log in

diff --git a/web/src/routes/library/albums/+page.svelte b/web/src/routes/library/albums/+page.svelte index 9c0eb881..b2e6134d 100644 --- a/web/src/routes/library/albums/+page.svelte +++ b/web/src/routes/library/albums/+page.svelte @@ -65,7 +65,7 @@

No albums in your loaded library match '{filter.trim()}'. - + Search the full library →

diff --git a/web/src/routes/library/artists/+page.svelte b/web/src/routes/library/artists/+page.svelte index edaf6287..ef627be9 100644 --- a/web/src/routes/library/artists/+page.svelte +++ b/web/src/routes/library/artists/+page.svelte @@ -63,7 +63,7 @@

No artists in your loaded library match '{filter.trim()}'. - + Search the full library →

diff --git a/web/src/routes/library/genres/+page.svelte b/web/src/routes/library/genres/+page.svelte index 70f2423d..f5a81511 100644 --- a/web/src/routes/library/genres/+page.svelte +++ b/web/src/routes/library/genres/+page.svelte @@ -112,7 +112,7 @@
Browse artists → + Browse artists → {/snippet} {:else} @@ -121,7 +121,7 @@ title="No liked albums yet." > {#snippet actions()} - Browse albums → + Browse albums → {/snippet} {:else} @@ -153,7 +153,7 @@ title="No liked tracks yet." > {#snippet actions()} - Find something new → + Find something new → {/snippet} {:else} diff --git a/web/src/routes/library/years/+page.svelte b/web/src/routes/library/years/+page.svelte index 2f6f4478..ce6f19f3 100644 --- a/web/src/routes/library/years/+page.svelte +++ b/web/src/routes/library/years/+page.svelte @@ -97,7 +97,7 @@

- Forgot password? + Forgot password?

Don't have an account? - Register + Register

diff --git a/web/src/routes/now-playing/+page.svelte b/web/src/routes/now-playing/+page.svelte index 6569aa4f..07c470eb 100644 --- a/web/src/routes/now-playing/+page.svelte +++ b/web/src/routes/now-playing/+page.svelte @@ -163,7 +163,7 @@ aria-label="Shuffle" aria-pressed={player.shuffle} class="flex items-center justify-center min-h-[44px] min-w-[44px] rounded - {player.shuffle ? 'text-accent' : 'text-text-secondary hover:text-text-primary'} + {player.shuffle ? 'text-accent-fg' : 'text-text-secondary hover:text-text-primary'} focus-visible:ring-2 focus-visible:ring-accent" > @@ -216,7 +216,7 @@ aria-label={repeatLabel} aria-pressed={player.repeat !== 'off'} class="flex items-center justify-center min-h-[44px] min-w-[44px] rounded - {player.repeat !== 'off' ? 'text-accent' : 'text-text-secondary hover:text-text-primary'} + {player.repeat !== 'off' ? 'text-accent-fg' : 'text-text-secondary hover:text-text-primary'} focus-visible:ring-2 focus-visible:ring-accent" > {#if player.repeat === 'one'} diff --git a/web/src/routes/register/+page.svelte b/web/src/routes/register/+page.svelte index dd37293a..68fed9cc 100644 --- a/web/src/routes/register/+page.svelte +++ b/web/src/routes/register/+page.svelte @@ -170,7 +170,7 @@

Already have an account? - Sign in + Sign in

diff --git a/web/src/routes/requests/+page.svelte b/web/src/routes/requests/+page.svelte index 43d19a64..5a52f569 100644 --- a/web/src/routes/requests/+page.svelte +++ b/web/src/routes/requests/+page.svelte @@ -107,7 +107,7 @@ {rowMeta(r)} {#if r.imported_album_count > 0 || r.imported_track_count > 0} -
+
{#if r.kind === 'artist'} {r.imported_album_count} {r.imported_album_count === 1 ? 'album' : 'albums'} · {r.imported_track_count} {r.imported_track_count === 1 ? 'track' : 'tracks'} ingested @@ -139,7 +139,7 @@ Listen diff --git a/web/src/routes/reset-password/[token]/+page.svelte b/web/src/routes/reset-password/[token]/+page.svelte index 887ca8d7..292d6e81 100644 --- a/web/src/routes/reset-password/[token]/+page.svelte +++ b/web/src/routes/reset-password/[token]/+page.svelte @@ -89,7 +89,7 @@

- Back to login + Back to login

diff --git a/web/src/routes/search/+page.svelte b/web/src/routes/search/+page.svelte index 389ef32a..a32b2b76 100644 --- a/web/src/routes/search/+page.svelte +++ b/web/src/routes/search/+page.svelte @@ -56,7 +56,7 @@ {#if artists.total > artists.items.length} See all {artists.total} → @@ -77,7 +77,7 @@ {#if albums.total > albums.items.length} See all {albums.total} → @@ -98,7 +98,7 @@ {#if tracks.total > tracks.items.length} See all {tracks.total} → diff --git a/web/src/routes/settings/+page.svelte b/web/src/routes/settings/+page.svelte index 2f58dcfd..64bd2b79 100644 --- a/web/src/routes/settings/+page.svelte +++ b/web/src/routes/settings/+page.svelte @@ -363,7 +363,7 @@
@@ -402,7 +402,7 @@ {/if}

- Get a token at listenbrainz.org/profile. + Get a token at listenbrainz.org/profile. Tokens are stored unencrypted in this server's database — treat as sensitive.

@@ -682,7 +682,7 @@

Library

diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 75a9b272..3d1babe0 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -26,8 +26,10 @@ export default { accent: { DEFAULT: 'var(--fs-accent)', tint: 'color-mix(in srgb, var(--fs-accent) 12%, transparent)', - // Accent TEXT on an accent tint (#3150). The raw accent on its own - // tint measures 1.97:1 at worst; this measures 5.03. + // All accent TEXT and icons use this, on a tint or not (#3150, + // #5318). The raw accent measures 1.97:1 on its own tint and 2.21 on + // slate at worst; this measures 5.03 and 5.62. The raw accent stays + // for borders, rings and fills, where it is a graphic, not text. fg: 'var(--fs-accent-fg)' }, warning: { DEFAULT: 'var(--fs-warning)', fg: 'var(--fs-warning-fg)' },