Hold-to-seek acceleration for playback DPAD and seekbar (#900)

## Description
- Adds hold-to-seek acceleration for left/right DPAD input during
playback skip handling.
- Reuses one shared, duration-aware acceleration profile for both
playback DPAD and seekbar skipping.
- Extracts acceleration logic into a shared helper to keep playback and
seekbar behavior consistent.
- Slows the acceleration ramp (about one-third) to improve control
during longer holds.
- Preserves tap-on-release behavior: single taps seek on key release,
while holds seek on repeated key-down without an extra release step.
- Normalizes unknown/unset duration values to use the shortest-content
acceleration profile.
- Added unit coverage for shared seek acceleration math to prevent
accidental regression in ramp thresholds.

### Related issues
Supersedes #846  
Incorporates #784  
Closes #522

### Testing
Tested on Nvidia Shield Pro 2019.

- Quick left/right tap still performs normal skip behavior.
- Holding left/right seeks repeatedly with progressive acceleration.
- Seekbar hold uses the same acceleration profile as playback hold.
- Releasing after a hold does not trigger an extra seek step.

## AI or LLM usage
Codex 5.3 and Claude Opus 4.6 were used for implementation, debugging,
and cross-review. Changes were manually validated on device.

---------

Co-authored-by: Ray <154766448+damontecres@users.noreply.github.com>
This commit is contained in:
voc0der 2026-02-28 19:35:02 +00:00 committed by GitHub
parent 55466f803c
commit fd9063fbad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 279 additions and 27 deletions

View file

@ -0,0 +1,53 @@
package com.github.damontecres.wholphin.ui.playback
import org.junit.Assert.assertEquals
import org.junit.Test
class SeekAccelerationTest {
@Test
fun returnsOneWhenNotRepeating() {
assertEquals(1, calculateSeekAccelerationMultiplier(repeatCount = 0, durationMs = 30_000L))
}
@Test
fun unknownDurationDoesNotAccelerate() {
assertEquals(1, calculateSeekAccelerationMultiplier(repeatCount = 89, durationMs = 0L))
assertEquals(1, calculateSeekAccelerationMultiplier(repeatCount = 300, durationMs = -1L))
}
@Test
fun shortContentHasTwoTiers() {
val shortDurationMs = 20L * 60_000L
assertEquals(
1,
calculateSeekAccelerationMultiplier(repeatCount = 89, durationMs = shortDurationMs),
)
assertEquals(
2,
calculateSeekAccelerationMultiplier(repeatCount = 90, durationMs = shortDurationMs),
)
}
@Test
fun mediumContentEscalatesAcrossAllTiers() {
val mediumDurationMs = 60L * 60_000L
assertEquals(
1,
calculateSeekAccelerationMultiplier(repeatCount = 38, durationMs = mediumDurationMs),
)
assertEquals(
2,
calculateSeekAccelerationMultiplier(repeatCount = 39, durationMs = mediumDurationMs),
)
assertEquals(
3,
calculateSeekAccelerationMultiplier(repeatCount = 150, durationMs = mediumDurationMs),
)
assertEquals(
4,
calculateSeekAccelerationMultiplier(repeatCount = 225, durationMs = mediumDurationMs),
)
}
}