Apply the global Card slider to preset-set rows too

resolveCardHeight now stacks the preset base, the global Card percent,
and the per-row multiplier: effective = base * globalCardSizePercent
/ 100 * cardSizeMultiplier / 100. Before this commit, a row with an
absolute preset heightDp (Favorite-Episode at 148, live-TV at 96, etc.)
was insulated from the global slider - the user had to drag the
per-row Card slider on that specific row to scale it, which made the
global slider feel inconsistent. Now the global reaches every row;
preset rows still get to set their own anchor and the per-row
multiplier still adjusts them independently.

Signature change: globalCardHeightDp (a pre-scaled dp value) is
replaced by globalCardSizePercent (raw percent) so the resolver can
apply the global multiplication itself, and baseCardHeightDp is
renamed to naturalCardHeightDp since it now serves both as the
fallback base and as the no-override sentinel. The composable caller
(resolvedCardHeight) drops its branch to pick between cardHeightDp and
episodeCardHeightDp - it just passes cardSizePercent through.

Tests cover preset compounding (148 * 150% * 100% = 222; 148 * 50% =
74; 96 * 125% = 120) plus the existing natural-base, sentinel, and
per-row override cases retargeted at the new signature.
This commit is contained in:
Justin Visser 2026-05-26 15:30:24 +02:00
parent bafaee7fe4
commit d350207789
2 changed files with 43 additions and 53 deletions

View file

@ -10,18 +10,13 @@ import com.github.damontecres.wholphin.ui.AspectRatio
import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.Cards
@Composable @Composable
internal fun HomeRowViewOptions.resolvedCardHeight(): Dp { internal fun HomeRowViewOptions.resolvedCardHeight(): Dp =
val custom = LocalInterfaceCustomization.current resolveCardHeight(
val naturalBase = naturalCardBaseDp(aspectRatio)
val globalForThisRow =
if (naturalBase == Cards.HEIGHT_EPISODE) custom.episodeCardHeightDp else custom.cardHeightDp
return resolveCardHeight(
heightDp = heightDp, heightDp = heightDp,
cardSizeMultiplier = cardSizeMultiplier, cardSizeMultiplier = cardSizeMultiplier,
globalCardHeightDp = globalForThisRow, globalCardSizePercent = LocalInterfaceCustomization.current.cardSizePercent,
baseCardHeightDp = naturalBase, naturalCardHeightDp = naturalCardBaseDp(aspectRatio),
).dp ).dp
}
// SQUARE / FOUR_THREE rows piggyback on the poster (TALL) global; they either store an // SQUARE / FOUR_THREE rows piggyback on the poster (TALL) global; they either store an
// explicit override (Music preset rows at 100dp, etc.) or compose well enough at poster size. // explicit override (Music preset rows at 100dp, etc.) or compose well enough at poster size.
@ -31,15 +26,18 @@ internal fun naturalCardBaseDp(aspectRatio: AspectRatio): Int =
AspectRatio.TALL, AspectRatio.SQUARE, AspectRatio.FOUR_THREE -> Cards.HEIGHT_2X3_DP AspectRatio.TALL, AspectRatio.SQUARE, AspectRatio.FOUR_THREE -> Cards.HEIGHT_2X3_DP
} }
// The global Card slider, the per-row multiplier, and any preset-set absolute heightDp
// all stack. Preset 148dp + global 150% + per-row 100% = 222dp. The base is the row's
// preset if present, otherwise the natural height for its aspect.
internal fun resolveCardHeight( internal fun resolveCardHeight(
heightDp: Int, heightDp: Int,
cardSizeMultiplier: Int, cardSizeMultiplier: Int,
globalCardHeightDp: Int, globalCardSizePercent: Int,
baseCardHeightDp: Int, naturalCardHeightDp: Int,
): Int { ): Int {
val isOverride = heightDp > 0 && heightDp != baseCardHeightDp val isOverride = heightDp > 0 && heightDp != naturalCardHeightDp
val base = if (isOverride) heightDp else globalCardHeightDp val base = if (isOverride) heightDp else naturalCardHeightDp
return base * cardSizeMultiplier / 100 return base * globalCardSizePercent / 100 * cardSizeMultiplier / 100
} }
// Scales density (and therefore both dp layouts and sp text) linearly by uiScalePercent. // Scales density (and therefore both dp layouts and sp text) linearly by uiScalePercent.

View file

@ -44,57 +44,49 @@ class InterfaceHelpersTest {
} }
@Test @Test
fun `resolveCardHeight uses global when heightDp is sentinel 0`() { fun `resolveCardHeight uses natural base when heightDp is sentinel 0`() {
assertEquals(172, resolveCardHeight(0, 100, 172, Cards.HEIGHT_2X3_DP)) // Sentinel 0 falls back to the natural base; global percent scales it.
assertEquals(258, resolveCardHeight(0, 100, 258, Cards.HEIGHT_2X3_DP)) // global at 150% assertEquals(172, resolveCardHeight(0, 100, 100, Cards.HEIGHT_2X3_DP))
assertEquals(258, resolveCardHeight(0, 100, 150, Cards.HEIGHT_2X3_DP))
} }
@Test @Test
fun `resolveCardHeight uses global when heightDp equals legacy default 172`() { fun `resolveCardHeight treats stored heightDp matching natural base as no override`() {
// A1 fix: legacy stored heightDp=172 (the pre-PR default) is treated as "no override". // Pre-PR builds wrote heightDp=172 (the poster natural base) into JSON. Episode rows
assertEquals(172, resolveCardHeight(Cards.HEIGHT_2X3_DP, 100, 172, Cards.HEIGHT_2X3_DP)) // wrote 128. Both must be transparent to the global slider so existing users get
assertEquals(258, resolveCardHeight(Cards.HEIGHT_2X3_DP, 100, 258, Cards.HEIGHT_2X3_DP)) // immediate scaling without a reset.
assertEquals(172, resolveCardHeight(Cards.HEIGHT_2X3_DP, 100, 100, Cards.HEIGHT_2X3_DP))
assertEquals(258, resolveCardHeight(Cards.HEIGHT_2X3_DP, 100, 150, Cards.HEIGHT_2X3_DP))
assertEquals(128, resolveCardHeight(Cards.HEIGHT_EPISODE, 100, 100, Cards.HEIGHT_EPISODE))
assertEquals(192, resolveCardHeight(Cards.HEIGHT_EPISODE, 100, 150, Cards.HEIGHT_EPISODE))
} }
@Test @Test
fun `resolveCardHeight honours non-default per-row override`() { fun `resolveCardHeight honours per-row override`() {
// Display Presets set absolute per-row heights like 128 or 148. // Preset rows (148) and live-TV (96) take precedence over the natural base.
assertEquals(128, resolveCardHeight(128, 100, 172, Cards.HEIGHT_2X3_DP)) assertEquals(148, resolveCardHeight(148, 100, 100, Cards.HEIGHT_2X3_DP))
assertEquals(148, resolveCardHeight(148, 100, 172, Cards.HEIGHT_2X3_DP)) assertEquals(96, resolveCardHeight(96, 100, 100, Cards.HEIGHT_EPISODE))
} }
@Test @Test
fun `resolveCardHeight applies cardSizeMultiplier on top of global`() { fun `resolveCardHeight applies global percent on top of per-row override`() {
// Global card height 172, multiplier 120 -> 206 dp. // Preset 148 with global 150% renders at 222dp. The user moves one slider; every
assertEquals(206, resolveCardHeight(0, 120, 172, Cards.HEIGHT_2X3_DP)) // row, including their preset-set ones, follows.
// Global 172, multiplier 75 -> 129. assertEquals(222, resolveCardHeight(148, 100, 150, Cards.HEIGHT_2X3_DP))
assertEquals(129, resolveCardHeight(0, 75, 172, Cards.HEIGHT_2X3_DP)) // Same at 50%: preset 148 * 0.5 = 74.
assertEquals(74, resolveCardHeight(148, 100, 50, Cards.HEIGHT_2X3_DP))
// Live-TV 96 * 125% = 120.
assertEquals(120, resolveCardHeight(96, 100, 125, Cards.HEIGHT_EPISODE))
} }
@Test @Test
fun `resolveCardHeight applies cardSizeMultiplier on top of per-row override`() { fun `resolveCardHeight applies cardSizeMultiplier and global percent together`() {
// Preset row at 148 with multiplier 120 -> 177. // Natural 172 * global 120% * per-row 110% = 226.
assertEquals(177, resolveCardHeight(148, 120, 172, Cards.HEIGHT_2X3_DP)) assertEquals(226, resolveCardHeight(0, 110, 120, Cards.HEIGHT_2X3_DP))
// Episode preset at 128 with multiplier 50 -> 64. // Preset 148 * global 150% * per-row 110% = 244.
assertEquals(64, resolveCardHeight(128, 50, 172, Cards.HEIGHT_2X3_DP)) assertEquals(244, resolveCardHeight(148, 110, 150, Cards.HEIGHT_2X3_DP))
} // Per-row alone (global at 100%) still works: preset 148 * 120% = 177.
assertEquals(177, resolveCardHeight(148, 120, 100, Cards.HEIGHT_2X3_DP))
@Test
fun `resolveCardHeight uses global when heightDp equals episode natural base 128`() {
// For an episode-aspect row, the caller passes baseCardHeightDp = HEIGHT_EPISODE (128)
// and globalCardHeightDp = episodeCardHeightDp. The stored heightDp = 128 then must
// not be treated as an override - the global Card slider should reach this row.
assertEquals(128, resolveCardHeight(Cards.HEIGHT_EPISODE, 100, 128, Cards.HEIGHT_EPISODE))
// Global at 150%: episodeCardHeightDp = 128 * 150 / 100 = 192.
assertEquals(192, resolveCardHeight(Cards.HEIGHT_EPISODE, 100, 192, Cards.HEIGHT_EPISODE))
}
@Test
fun `resolveCardHeight honours per-row override even when row is episode-natural`() {
// A user who manually drags an episode row to heightDp = 200 still wins over the global.
assertEquals(200, resolveCardHeight(200, 100, 192, Cards.HEIGHT_EPISODE))
// Live-TV-style 96.dp override stays absolute.
assertEquals(96, resolveCardHeight(96, 100, 192, Cards.HEIGHT_EPISODE))
} }
@Test @Test