Skip to content

refactor(player): centralize format helpers + unify title/artist tap handling#820

Merged
rukamori merged 5 commits into
ArchiveTuneApp:devfrom
adam-adrian:refactor/player-format-and-title-artist
Jun 18, 2026
Merged

refactor(player): centralize format helpers + unify title/artist tap handling#820
rukamori merged 5 commits into
ArchiveTuneApp:devfrom
adam-adrian:refactor/player-format-and-title-artist

Conversation

@adam-adrian

Copy link
Copy Markdown
Contributor

Pull Request

Summary

This PR bundles two related player-UI refactors.

(1) FormatEntity display helpers: extracts 5 reusable extension functions (containerLabel, codecLabel, formattedBitrate, formattedSampleRate, formattedFileSize) into FormatEntity.kt, replacing ~70 lines of duplicated inline parsing across 6 queue composables and a private duplicate in PlayerComponents.kt; the inline logic named its variable codec while actually extracting the container, now honestly named container.

(2) Title/artist tap handling: centralizes the duplicated title/artist tap behavior into a single rememberPlayerTitleActions() helper and adds a reusable ClickableArtists primitive so each artist name is individually tappable, with Classic, V7, V8 and V9 now sharing one implementation instead of hand-rolling their own lambdas. It also fixes a real bug where tapping the song title used state.snapTo(...) — which jumped without animation and left the bottom-sheet anchor stale — so the player now collapses reliably via collapseSoft(). No format display changes; per-style visual rendering is preserved.

Linked Work

Change Type

  • Feature
  • Bug fix
  • UI / UX
  • Performance / memory
  • Playback / Media3
  • Lyrics / provider integration
  • Search / YouTube / network data
  • Local library / Room database
  • Widgets / notification / shortcuts
  • Settings / preferences / DataStore
  • Discord / Last.fm / ListenBrainz / external integration
  • Localization / strings / fastlane metadata
  • Build / Gradle / CI / release packaging
  • Dependency update
  • Documentation only

Affected Surfaces

  • :app
  • :innertube
  • :betterlyrics
  • :kugou
  • :lrclib
  • :lastfm
  • :simpmusic
  • :paxsenix
  • :unison
  • :canvas
  • :shazamkit
  • :spotifycore
  • server
  • fastlane
  • .github
  • Other:

Screenshots / Recordings

Format row is unchanged. Title-tap now animates the player closed; artist tap targets a specific artist.

Before After
WEBM • 141 kbps • 3 MB WEBM • 141 kbps • 3 MB

Behavior Notes

  • Format display unchanged: all 9 player styles (V1–V9) show the exact same codec / bitrate / size text as before. The refactor only changes how the strings are produced, not what they contain.
  • Variable naming fix: inline val codec = mimeType.substringAfter("/") was extracting the container, not the codec; now val container = currentFormat.containerLabel().
  • Local file bitrate edge case: formattedBitrate() returns "Unknown" or not showed at all when bitrate == 0 (local files), instead of the previous "0 kbps".
  • Per-artist tap (UX change): tapping an artist in V7/V8/V9 now opens that specific artist's page (previously: always opened the first artist). Classic (V1-V4 and V6) already behaved this way.
  • Title-tap collapse fix: tapping the song title now animates the player closed and keeps the sheet's collapsed/expanded state in sync (previously snapTo jumped with no animation and left the anchor stale, so the player failed to collapse reliably from the home screen).
  • Per-style visuals preserved: fonts, colors, alignment, and layout for each style are untouched; only the shared tap/format logic moved.

Architecture Checklist

  • The change preserves UDF flow: UI -> ViewModel -> UseCase/domain -> Repository/data.
  • Screen state is represented structurally with Loading, Success, Empty, and Error where this PR introduces or changes screen state. (N/A — no screen-state changes.)
  • Composables receive immutable, UI-specific domain models instead of raw Room, network, or service entities. (Extension functions on FormatEntity are pure; PlayerTitleActions is @Immutable.)
  • Business work is not triggered directly from composition.
  • Exceptions are surfaced through explicit state or result types instead of being swallowed.
  • No runBlocking is introduced in app execution paths.

Compose / Material Checklist

  • UI state is hoisted out of composable layout code.
  • Reactive state is collected with collectAsStateWithLifecycle(). (N/A — no new state collection.)
  • New UI models are annotated with @Immutable or @Stable. (PlayerTitleActions is @Immutable.)
  • Lazy layouts use stable key values and explicit contentType. (N/A — no lazy-layout changes.)
  • Non-primitive constants, structural lambdas, and allocation-heavy objects in hot paths are remembered. (rememberPlayerTitleActions and the ClickableArtists annotated string are remembered.)
  • Rapidly changing inputs such as scroll, gesture, animation, or playback progress use derivedStateOf where appropriate. (N/A.)
  • UI strings come from stringResource() and duplicated visible strings on the same screen are avoided. (No new user-facing strings added.)
  • Interactive elements keep a minimum 48dp touch target.
  • Material 3 / Material 3 Expressive tokens are used for color, typography, shape, and motion instead of hardcoded UI values.
  • Edge-to-edge layouts handle and consume WindowInsets correctly when this PR changes screen layout. (N/A — no layout/inset changes.)

Concurrency / Performance Checklist

  • Business coroutines are scoped to viewModelScope or an existing lifecycle-owned application/service scope. (N/A.)
  • Disk, database, network, parsing, and heavy mapping work is dispatched to Dispatchers.IO or Dispatchers.Default. (N/A — lightweight string ops.)
  • Cancellation is handled explicitly where long-running work is involved. (N/A.)
  • The change avoids blocking the main thread.
  • The change avoids unnecessary allocations in recomposition loops. (Annotated string and joined artist line are memoized.)
  • Large lists, images, lyrics payloads, and network responses are bounded, streamed, cached, paged, or mapped off the main thread as appropriate. (N/A.)

Data / Persistence Checklist

  • Room entity, DAO, or database changes include a schema update under app/schemas. (N/A — only pure extension functions added to existing FormatEntity; no schema change.)
  • Database migrations preserve existing user data and fail clearly when migration is impossible. (N/A.)
  • DataStore or preference-key changes are backward compatible. (N/A.)
  • Network DTOs remain isolated from UI models. (N/A.)
  • Provider responses handle missing, malformed, regional, or rate-limited data without crashing the app. (N/A.)

Playback / Integration Checklist

  • Media3 playback, queue, cache, and service behavior remains stable across foreground, background, notification, and process recreation paths. (No playback logic changed.)
  • Audio focus, notification controls, widgets, shortcuts, and media session actions are considered when playback behavior changes. (N/A.)
  • External integrations handle absent credentials, revoked auth, network failure, and API changes. (N/A.)
  • Native, AAR, or ABI-sensitive changes account for mobile/tv and all ABI variants. (N/A.)

Localization / Assets Checklist

  • User-facing strings are added to the base resources and translated resources are updated or intentionally left for translation follow-up. (No new user-facing strings; the existing "Copied" toasts are unchanged by this PR.)
  • Unused string resources, drawables, and metadata are removed when replaced.
  • Image assets have explicit display dimensions and are decoded at the displayed size. (N/A.)
  • Fastlane metadata, screenshots, icons, or release text are updated when user-facing store behavior changes. (N/A.)

Privacy / Security Checklist

  • No secrets, keys, tokens, keystores, signing files, private certificates, or local machine paths are committed.
  • Logs do not expose access tokens, cookies, auth headers, user identifiers, listening history, or local file paths.
  • New network calls are justified by the feature and use existing client, proxy, timeout, and error-handling patterns. (No new network calls.)
  • User data remains local unless the PR explicitly documents the integration and consent path.

Verification

  • Android Studio sync: passed
  • Manual device/emulator verification: Tested on Poco X7 Pro: title tap → album + collapse, per-artist tap on V1–V9, format row unchanged
  • UI screenshot/recording attached:
  • Accessibility or touch-target review:
  • Regression areas checked: queue format row, all 9 player styles, bottom-sheet collapse/expand state
  • CI expectation: build passes

Reviewer Focus

  • FormatEntity.kt — the 5 new extension functions and the codeccontainer naming correction.
  • PlayerTitleActions.kt — single source of truth for title/artist tap + clipboard behavior; note collapseSoft() vs the old snapTo().
  • PlayerArtistText.ktClickableArtists hit-tests taps to the correct artist span via the static text layout (see the in-file note on marquee).
  • PlayerComponents.kt — Classic/V8/V9 now delegate to the shared helper/primitive; String artist params became List<MediaMetadata.Artist> through the V8/V9 pipeline.
  • V7 inherits V8's controls via V8PlayerControlsContent in Player.kt.

Release Notes

Tapping a song's title now smoothly collapses the player, and tapping an artist opens that specific artist's page across all player styles.

… labeling

Move codecLabel(), containerLabel(), formattedBitrate(), formattedSampleRate(),
and formattedFileSize() into FormatEntity.kt as public extensions,
replacing ~70 lines of duplicated inline parsing scattered across 6 queue composables
and the private codecLabel() in PlayerComponents.kt.

The inline parsing in QueueComponents named its variable 'codec' while actually
extracting the container from mimeType (e.g. 'WEBM' instead of 'opus').
Variable is now honestly named 'container'. User-facing display is unchanged.

Bonus: bitrate formatting now returns 'Unknown' instead of '0 kbps' for local
files where bitrate is not available.
Extract the duplicated title/artist tap behavior into a single
rememberPlayerTitleActions() helper, and add a reusable ClickableArtists
primitive that makes each artist name individually tappable. Classic, V7,
V8 and V9 now share this behavior instead of each hand-rolling its own
lambdas, removing prior feature drift (per-artist navigation previously
existed only in the classic style).

Visual rendering stays per-style: adding a new player style no longer
requires touching shared title/artist code.
Tapping the song title navigated to the album but used snapTo to move the
sheet, which jumps without animation and — crucially — does not update the
sheet's internal anchor. The anchor could then go stale (e.g. still
EXPANDED), so on the next recomposition the sheet snapped back open,
making "tap title to collapse" appear broken on the home screen while it
worked elsewhere.

Use collapseSoft() instead, which animates and updates the anchor,
matching the artist-tap behavior.
- Remove unused PlayerTitleActions.onFirstArtistClick (dead code)
- ClickableArtists: use detectTapGestures instead of a standing
  awaitPointerEventScope loop; memoize the AnnotatedString; restore
  per-artist annotation tags; document the marquee hit-test caveat
- Drop the now-unused clipboardManager/context params threaded through
  PlayerTitleSection / PlayerControlsContent (and remove dead imports)
@rukamori rukamori merged commit dd9e308 into ArchiveTuneApp:dev Jun 18, 2026
2 checks passed
@adam-adrian adam-adrian deleted the refactor/player-format-and-title-artist branch June 18, 2026 05:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants