Skip to content

5.6.0#226

Merged
shub39 merged 11 commits intomasterfrom
dev
Feb 27, 2026
Merged

5.6.0#226
shub39 merged 11 commits intomasterfrom
dev

Conversation

@shub39
Copy link
Copy Markdown
Owner

@shub39 shub39 commented Feb 25, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability of the lyrics backend to reduce intermittent failures.
  • New Features

    • Enhanced lyrics search with a multi-strategy fallback for more reliable results.
    • Added flexible fonts for headlines, displays and sharecards.
  • Improvements

    • Refined typography across the app; updated top bars to support scroll-aware behavior and marquee titles.
    • Adjusted README badge sizing and updated changelog entries.
  • Tests

    • Added/updated tests covering lyrics/search APIs.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Refactors HTTP client usage to internal lazy clients in API classes, removes centralized HttpClientFactory and Tokens, moves title/artist normalization to domain, introduces Google SansFlex typography and flexible top app bars, and bumps multiple build dependencies; adds tests and minor import/UI tweaks.

Changes

Cohort / File(s) Summary
HTTP Client & API classes
app/src/main/java/com/shub39/rush/data/network/GeniusApi.kt, app/src/main/java/com/shub39/rush/data/network/GeniusScraper.kt, app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt
Removed constructor-injected HttpClient; each API now lazily creates an internal OkHttp-based Ktor client (ContentNegotiation, HttpTimeout, optional Logging, defaultRequest). LrcLibApi adds a multi-strategy search flow and helper to query /api/search.
DI, Factory & Token removal
app/src/main/java/com/shub39/rush/di/RushModules.kt, app/src/main/java/com/shub39/rush/data/HttpClientFactory.kt, app/src/main/java/com/shub39/rush/data/network/Tokens.kt
Deleted centralized HttpClientFactory, removed provideHttpClient() DI provider and Tokens object; APIs now reference BuildConfig tokens directly.
Domain utilities & call-site updates
app/src/main/java/com/shub39/rush/domain/Util.kt, app/src/main/java/com/shub39/rush/presentation/Util.kt, app/src/main/java/com/shub39/rush/data/listener/MediaListenerImpl.kt, app/src/main/java/com/shub39/rush/viewmodels/SavedVM.kt, app/src/main/java/com/shub39/rush/viewmodels/SearchSheetVM.kt, app/src/main/java/com/shub39/rush/data/repository/RushRepository.kt
Added domain.getMainTitle/getMainArtist, removed duplicate presentation helpers, updated imports and call sites to use domain helpers; some call sites now emit raw title/artist.
Typography & flex fonts
app/src/main/java/com/shub39/rush/presentation/ProvideTypography.kt, app/src/main/java/com/shub39/rush/presentation/provideTypography.kt, app/src/main/java/com/shub39/rush/presentation/EnumExt.kt
Introduced Google SansFlex-based FontFamily helpers (flexFont*), added provideTypography variants and preview, switched GOOGLE_SANS mapping to google_sans_flex, and made typography font param nullable.
Top app bar & UI updates
app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt, app/src/main/java/com/shub39/rush/presentation/setting/section/*
Replaced static TopAppBar with Medium/LargeFlexibleTopAppBar, added scrollBehavior (enterAlwaysScrollBehavior) with nestedScroll wiring, and updated titles/subtitles to use flex fonts.
Share / Card typography changes
app/src/main/java/com/shub39/rush/presentation/share/component/cards/*
Replaced explicit FontWeight styling with flexFontEmphasis/flexFontRounded font families across share cards; adjusted sizes while preserving layout/overflow.
Compose text & other UI tweaks
several compose files (LyricsPage, LyricsCustomisationPage, Onboarding, branding components)
Multiple text style swaps to flex fonts, some style-level changes (e.g., titleLarge→headlineMedium), added marquee for a title, and removed some FontWeight usages.
Imports & small reorganizations
various files (CurveVisualizer.kt, SyncedLyrics.kt, SharePage.kt, backup files)
Reordered/consolidated imports, removed duplicates, and adjusted OptIn/ExperimentalTime annotations.
Tests
app/src/test/java/GeniusApiTest.kt, app/src/test/java/ScraperTest.kt, app/src/test/java/LrcLibApiTest.kt
Updated tests to instantiate APIs with parameterless constructors; added LrcLibApiTest with a search test.
Build, deps & docs
gradle/libs.versions.toml, app/build.gradle.kts, visualizer-helper/build.gradle.kts, CHANGELOG.md, README.md, app/src/main/res/values-frc/strings.xml
Bumped many dependency versions, added ktfmt, removed apk renaming block and some plugin usages, enabled resValues, added CHANGELOG 5.6.0 entries, reduced README badge widths, and added empty values-frc/strings.xml.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Repo as "Repository"
    participant Lrc as "LrcLibApi"
    participant Client as "HttpClient (internal)"
    participant LrcSvc as "LRC External Service"
    Note over Lrc: LrcLibApi lazily creates HttpClient (OkHttp + Ktor config)
    Repo->>Lrc: request getLrcLyrics(title, artist, album?)
    Lrc->>Client: build/request /api/search (strategy 1..n)
    Client->>LrcSvc: HTTP GET /api/search?params
    LrcSvc-->>Client: 200 OK + payload
    Client-->>Lrc: parsed List<LrcGetDto>
    Lrc-->>Repo: first non-empty result or null
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

  • dev #153: Overlaps networking and DI refactor (GeniusApi, GeniusScraper, LrcLibApi, Tokens removal).
  • back to Android #152: Related removal/replacement of HttpClientFactory and DI wiring changes.
  • state-updates #165: Related theming/typography API changes affecting RushTheme and font handling.

Poem

🐰
A factory folded, clients wake inside,
Domain learned to tidy titles with pride,
Flex fonts sprout headlines, bold and round,
Top bars stretch and scroll without a sound,
Rush hops onward — code and carrots found.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title '5.6.0' is vague and does not describe the actual changes made in the pull request. It appears to be a version number rather than a meaningful summary of the work performed. Replace the title with a descriptive summary of the main changes, such as 'Refactor HTTP client initialization and modernize typography system' or a similar phrase that captures the primary objectives of this release.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
app/src/main/java/com/shub39/rush/data/backup/ExportImpl.kt (1)

32-32: ⚠️ Potential issue | 🟡 Minor

Remove unused @OptIn(ExperimentalTime::class) annotation and import.

This method does not use any kotlin.time APIs, and ExperimentalTime is still required for certain parts of the kotlin.time package in Kotlin 2.3.10. The annotation and import at line 24 are unused and should be removed. (Note: ExperimentalTime remains experimental in recent Kotlin versions for APIs like Int.seconds, Long.minutes, etc., though some core measurement types like Clock are available.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/backup/ExportImpl.kt` at line 32,
Remove the unused ExperimentalTime opt-in: delete the
`@OptIn`(ExperimentalTime::class) annotation and its import from ExportImpl (the
ExportImpl class / method where `@OptIn` is declared) since no kotlin.time APIs
are used; ensure no other usages of ExperimentalTime remain in that file and run
a quick compile to confirm the unused import/annotation are gone.
app/build.gradle.kts (1)

27-28: ⚠️ Potential issue | 🟡 Minor

Version mismatch: PR title says "5.6.0" but code still has "5.5.2".

appVersionName is "5.5.2" and appVersionCode is 5520, but the PR title indicates this should be version 5.6.0. If this release is intended as 5.6.0, these values need updating.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/build.gradle.kts` around lines 27 - 28, Update the version constants to
match the PR title: change the value of appVersionName and appVersionCode so
they represent 5.6.0 (update the string in appVersionName and the numeric
appVersionCode accordingly); locate and modify the appVersionName and
appVersionCode declarations in build.gradle.kts to reflect the new release
version.
app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsPage.kt (1)

340-360: ⚠️ Potential issue | 🟡 Minor

Inconsistent typography between portrait and landscape layouts.

The portrait layout (Lines 248, 258) was updated to use headlineSmall / titleSmall for the song title and artist, but the landscape layout here still uses the old titleLarge / bodyMedium styles. This looks like an oversight — the same typography migration should be applied to both orientations for visual consistency.

Proposed fix
                                            Text(
                                                text = lyricsState.song.title,
-                                                style = MaterialTheme.typography.titleLarge,
-                                                fontWeight = FontWeight.SemiBold,
+                                                style = MaterialTheme.typography.headlineSmall,
                                                maxLines = 1,
                                                overflow = TextOverflow.Ellipsis,
                                                modifier =
                                                    Modifier.padding(horizontal = 16.dp)
                                                        .basicMarquee(),
                                            )

                                            Text(
                                                text = lyricsState.song.artists,
-                                                style = MaterialTheme.typography.bodyMedium,
+                                                style = MaterialTheme.typography.titleSmall,
                                                fontWeight = FontWeight.Bold,
                                                maxLines = 1,
                                                overflow = TextOverflow.Ellipsis,
                                                modifier =
                                                    Modifier.padding(horizontal = 16.dp)
                                                        .basicMarquee(),
                                            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsPage.kt`
around lines 340 - 360, The landscape layout still uses outdated typography for
the song title and artist: change the two Text composables that render
lyricsState.song.title and lyricsState.song.artists in LyricsPage.kt to use the
same migrated styles as portrait (swap MaterialTheme.typography.titleLarge ->
MaterialTheme.typography.headlineSmall for the title and
MaterialTheme.typography.bodyMedium -> MaterialTheme.typography.titleSmall for
the artist), keeping the existing fontWeight, maxLines, overflow and
Modifier.basicMarquee() unchanged to preserve behavior.
app/src/main/java/com/shub39/rush/data/backup/RestoreImpl.kt (1)

52-53: ⚠️ Potential issue | 🟡 Minor

Null input stream silently maps to a misleading error.

If contentResolver.openInputStream returns null, input?.copyTo(output) is a no-op — the temp file stays empty. file.readText() then returns "", triggering a SerializationException that surfaces as RestoreFailedException.OldSchema. The user sees "old schema" when the real problem is that the file could not be opened.

🐛 Proposed fix — detect null input and fail fast with the correct error
-        context.contentResolver.openInputStream(path.toUri()).use { input ->
-            file.outputStream().use { output -> input?.copyTo(output) }
-        }
+        val inputStream = context.contentResolver.openInputStream(path.toUri())
+            ?: throw IllegalArgumentException("Could not open input stream for $path")
+        inputStream.use { input ->
+            file.outputStream().use { output -> input.copyTo(output) }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/backup/RestoreImpl.kt` around lines 52
- 53, The code silently proceeds when
context.contentResolver.openInputStream(path.toUri()) returns null, leaving an
empty temp file and causing a misleading
SerializationException/RestoreFailedException.OldSchema; update the block around
context.contentResolver.openInputStream(path.toUri()).use { input -> ... } to
explicitly check for null before using the stream (i.e., if input == null) and
fail fast by throwing a clear RestoreFailedException (or add a new variant)
indicating the input stream could not be opened for the given URI, rather than
continuing to write an empty file and later calling file.readText() which masks
the real error.
♻️ Duplicate comments (1)
app/src/main/java/com/shub39/rush/data/network/GeniusApi.kt (1)

41-64: Same duplicated HttpClient setup — see comment on GeniusScraper.kt.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/network/GeniusApi.kt` around lines 41
- 64, The HttpClient initialization in GeniusApi (the lazy property client)
duplicates the same setup used elsewhere (e.g., GeniusScraper); refactor this by
extracting the shared configuration into a single factory/provider function
(e.g., createSharedHttpClient or HttpClientProvider.getClient) that encapsulates
ContentNegotiation, HttpTimeout, conditional Logging (BuildConfig.DEBUG) and
defaultRequest contentType, then replace the GeniusApi.client lazy block with a
call to that shared factory so both GeniusApi and GeniusScraper reuse the same
HttpClient construction logic.
🧹 Nitpick comments (13)
app/src/main/java/com/shub39/rush/presentation/setting/section/SettingRootPage.kt (1)

92-98: Nit: variable name scrollBehaviour vs API parameter scrollBehavior.

The variable uses British spelling (Behaviour) while Compose APIs use American spelling (Behavior). Not a bug, but slightly inconsistent when reading the code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/shub39/rush/presentation/setting/section/SettingRootPage.kt`
around lines 92 - 98, Rename the British-spelled local variable scrollBehaviour
to the API-consistent scrollBehavior: change the declaration using
TopAppBarDefaults.enterAlwaysScrollBehavior() and update all usages (e.g.,
Modifier.nestedScroll(scrollBehaviour.nestedScrollConnection) and the
LargeFlexibleTopAppBar(scrollBehavior = scrollBehaviour) to use scrollBehavior
and scrollBehavior.nestedScrollConnection so names match the Compose API.
README.md (1)

4-4: Nit: capitalize "GitHub" in the alt text.

The alt text reads "Get it on github" — the official name uses a capital "H".

-[<img alt="Get it on github" src="badges/github.png" width="180px">](https://github.com/shub39/Rush/releases)
+[<img alt="Get it on GitHub" src="badges/github.png" width="180px">](https://github.com/shub39/Rush/releases)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 4, Update the alt text in the Markdown image tag [<img
alt="Get it on github" src="badges/github.png" width="180px">] to use the
correct capitalization "Get it on GitHub" so the alt attribute reads alt="Get it
on GitHub".
CHANGELOG.md (1)

4-4: Nit: capitalize "api" → "API" for consistency.

"LRCLIB" is already uppercase; "api" looks inconsistent next to it.

-- Fixed LRCLIB api not responding sometimes
+- Fixed LRCLIB API not responding sometimes
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` at line 4, Update the changelog entry string "LRCLIB api not
responding sometimes" to use uppercase API for consistency by changing it to
"LRCLIB API not responding sometimes"; modify the exact text in CHANGELOG.md
where that line appears so "api" is capitalized to "API".
app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt (1)

103-121: Consider wiring scrollBehavior for consistency with other pages.

Other pages in this PR (e.g., SettingRootPage) wire TopAppBarDefaults.enterAlwaysScrollBehavior() to LargeFlexibleTopAppBar along with a nestedScroll modifier on the Scaffold. Here, no scroll behavior is connected, so the large top bar won't collapse when the list scrolls. If this is intentional (e.g., because of the sort-button row below), no action needed — but worth confirming.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt` around
lines 103 - 121, The LargeFlexibleTopAppBar is not wired to a scrollBehavior so
it won't collapse with list scrolling; create a val scrollBehavior =
TopAppBarDefaults.enterAlwaysScrollBehavior(), pass scrollBehavior =
scrollBehavior into LargeFlexibleTopAppBar and add
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) to the surrounding
Scaffold (or top-level container) so the bar collapses consistently like
SettingRootPage.
app/src/main/java/com/shub39/rush/data/network/GeniusScraper.kt (2)

46-68: Duplicated HttpClient configuration across network classes.

This exact client setup (OkHttp engine, ContentNegotiation, HttpTimeout, conditional Logging, defaultRequest) is copy-pasted in GeniusApi, GeniusScraper, and (per the summary) LrcLibApi. While the old centralized HttpClientFactory was removed intentionally, consider extracting a shared factory function (e.g., internal fun createDefaultClient(): HttpClient) to avoid three copies drifting out of sync.

Also, connectTimeoutMillis is not set — only socketTimeoutMillis and requestTimeoutMillis. If one of the scraped endpoints is slow to establish a TCP connection, there's no bound on that wait.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/network/GeniusScraper.kt` around lines
46 - 68, Extract the duplicated HttpClient construction used in GeniusScraper,
GeniusApi and LrcLibApi into a single internal factory function (e.g., internal
fun createDefaultClient(): HttpClient) and replace the lazy client properties in
those classes to call this factory so configuration (OkHttp engine,
ContentNegotiation, HttpTimeout, conditional Logging, defaultRequest) is
centralized; also add connectTimeoutMillis to the HttpTimeout configuration so
connect, socket and request timeouts are all bounded.

66-66: Content-Type: application/json default on a scraper that only sends GET requests to HTML pages.

This header is meaningless on bodiless GET requests and won't cause issues, but it's misleading. If you extract a shared client builder (per the above comment), you could omit the defaultRequest block here and only set it in GeniusApi where JSON requests are actually made.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/network/GeniusScraper.kt` at line 66,
Remove the misleading defaultRequest { contentType(ContentType.Application.Json)
} from the GeniusScraper Ktor client configuration (locate it in
GeniusScraper.kt where the client is built) since the scraper only issues GET
requests to HTML pages; instead, ensure the JSON Content-Type is applied only
where JSON requests are made (e.g., in the GeniusApi client or request builders
such as the class/method that builds API calls for GeniusApi). If you refactor
to a shared client builder, keep the JSON default in the GeniusApi-specific
client configuration or set the header per-request in the GeniusApi methods
rather than globally in GeniusScraper.
app/src/main/java/com/shub39/rush/presentation/setting/section/BackupPage.kt (1)

125-140: Minor naming inconsistency: scrollBehaviour vs scrollBehavior.

The local variable uses British spelling (scrollBehaviour) while the API parameter uses American spelling (scrollBehavior). Consider aligning to scrollBehavior for consistency with the Compose API naming convention. Same applies in LookAndFeelPage.kt.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/presentation/setting/section/BackupPage.kt`
around lines 125 - 140, Rename the local variable scrollBehaviour to
scrollBehavior to match the Compose API naming and avoid inconsistency: change
the variable created from TopAppBarDefaults.enterAlwaysScrollBehavior() and all
its usages (e.g., nestedScroll(scrollBehaviour.nestedScrollConnection) and
scrollBehavior = scrollBehaviour passed into MediumFlexibleTopAppBar) to use
scrollBehavior instead; apply the same rename in LookAndFeelPage.kt so both
files consistently use scrollBehavior and update any references to
nestedScrollConnection accordingly.
app/src/main/java/com/shub39/rush/presentation/provideTypography.kt (1)

35-69: Flex font families are recreated on every call — consider hoisting them.

flexFontDisplay, flexFontHeadline, and flexFontTitle don't depend on any parameters or composition state. Defining them as top-level vals (alongside TYPOGRAPHY) avoids allocating new FontFamily/Font objects on each recomposition.

Proposed refactor
+@OptIn(ExperimentalTextApi::class)
+private val flexFontDisplay =
+    FontFamily(
+        Font(
+            resId = R.font.google_sans_flex,
+            variationSettings =
+                FontVariation.Settings(
+                    FontVariation.weight(900),
+                    FontVariation.slant(-6f),
+                    FontVariation.width(120f),
+                ),
+        )
+    )
+
+@OptIn(ExperimentalTextApi::class)
+private val flexFontHeadline =
+    FontFamily(
+        Font(
+            resId = R.font.google_sans_flex,
+            variationSettings =
+                FontVariation.Settings(
+                    FontVariation.weight(800),
+                    FontVariation.slant(-6f),
+                    FontVariation.width(110f),
+                ),
+        )
+    )
+
+@OptIn(ExperimentalTextApi::class)
+private val flexFontTitle =
+    FontFamily(
+        Font(
+            resId = R.font.google_sans_flex,
+            variationSettings =
+                FontVariation.Settings(
+                    FontVariation.weight(500),
+                    FontVariation.Setting("ROND", 100f),
+                ),
+        )
+    )
+
-@OptIn(ExperimentalTextApi::class)
 `@Composable`
 fun provideTypography(font: Int? = R.font.poppins_regular): Typography {
-    val flexFontDisplay = ...
-    val flexFontHeadline = ...
-    val flexFontTitle = ...
     val selectedFont = font?.let { FontFamily(Font(it)) } ?: FontFamily.Default
     ...
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/presentation/provideTypography.kt` around
lines 35 - 69, The three FontFamily vals flexFontDisplay, flexFontHeadline, and
flexFontTitle are recreated on every call; hoist them to top-level immutable
vals so they are allocated once. Move the current declarations for
flexFontDisplay, flexFontHeadline, and flexFontTitle out of the function/scope
where they currently live and place them as top-level val declarations
(alongside TYPOGRAPHY), keeping their Font(...) and FontVariation.Settings(...)
definitions identical, then have the provideTypography code reference these
top-level vals instead of local ones.
app/src/main/java/com/shub39/rush/presentation/util.kt (1)

121-134: " X " entry is redundant — contains on line 139 already uses ignoreCase = true.

Since getMainArtist calls contains(separator, ignoreCase = true) and split(..., ignoreCase = true, ...), the " x " entry already covers " X ". The explicit " X " on line 127 is dead code.

Proposed fix
     listOf(
         " & ",
         " and ",
         ", ",
         " x ",
-        " X ",
         " feat. ",
         " feat ",
         " ft. ",
         " ft ",
         " featuring ",
         " with ",
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/presentation/util.kt` around lines 121 -
134, The artistSeparators list contains a redundant " X " entry because
getMainArtist calls contains(separator, ignoreCase = true) and split(...,
ignoreCase = true), so the lowercase " x " already matches; remove the " X "
element from the artistSeparators list to eliminate dead code and keep only the
case-insensitive separators (update the list in the artistSeparators declaration
and verify getMainArtist still uses contains(..., ignoreCase = true) and
split(..., ignoreCase = true) as intended).
app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt (2)

92-143: safeCall is used only for its exception-catching side-effect; success paths bypass its HttpResponseResult conversion entirely.

All success paths use non-local returns from the inline safeCall lambda, so safeCall's responseToResult logic is never reached. This works because safeCall is inline, but it's fragile and misleading — the type parameter T and HttpResponse return type of the lambda are effectively unused. Consider extracting a simpler try/catch-based wrapper (e.g., safeCatch) that only handles exceptions, or restructuring to not rely on non-local returns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt` around lines 92
- 143, The method searchLrcLyrics currently wraps its whole body in the inline
safeCall lambda but uses non-local returns on success so safeCall's
HttpResponse→Result conversion (responseToResult) is never exercised; replace
the use of safeCall with a simple try/catch that only handles exceptions (or
introduce a new helper safeCatch) so searchLrcLyrics returns
Result.Success/Result.Error directly from its success paths and exceptions are
mapped to Result.Error in one place; update references to safeCall,
responseToResult and keep searchLrcLyrics, queryLyricsWithParams and
getMainTitle/getMainArtist intact so callers and behavior remain the same while
removing reliance on non-local returns.

100-107: Repeated filter predicate across all strategies.

The predicate { it.syncedLyrics != null || it.plainLyrics != null } is duplicated five times. Extract it into a named function or val for clarity and DRY.

Proposed fix

Add a local helper at the top of the safeCall block:

val hasLyrics: (LrcGetDto) -> Boolean = { it.syncedLyrics != null || it.plainLyrics != null }

Then replace all .filter { it.syncedLyrics != null || it.plainLyrics != null } with .filter(hasLyrics).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt` around lines 100
- 107, Extract the repeated predicate { it.syncedLyrics != null ||
it.plainLyrics != null } into a single named val or function inside the safeCall
block (e.g. val hasLyrics: (LrcGetDto) -> Boolean = { it.syncedLyrics != null ||
it.plainLyrics != null }) and replace every .filter { it.syncedLyrics != null ||
it.plainLyrics != null } in the search strategies that call
queryLyricsWithParams(...) with .filter(hasLyrics) so all five strategy results
use the shared predicate; keep the predicate local to the safeCall scope and
ensure the type matches the LrcGetDto returned by queryLyricsWithParams.
app/src/test/java/LrcLibApiTest.kt (2)

26-30: testIn helper is duplicated across three test files.

The identical testIn helper exists in GeniusApiTest.kt (lines 25-29) and ScraperTest.kt (lines 25-29). Consider extracting it into a shared test utility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/test/java/LrcLibApiTest.kt` around lines 26 - 30, Extract the
duplicated suspend-test helper into a single shared test utility by moving the
top-level function testIn(title: String, block: suspend CoroutineScope.() ->
Unit) = runBlocking { ... } out of LrcLibApiTest, GeniusApiTest and ScraperTest
and into a new test helper file (e.g., TestUtils.kt) in the test sources; keep
the exact signature and runBlocking usage so behavior doesn't change, remove the
duplicate definitions from the three test files, and update those tests to
import and call the shared testIn function instead of their local copies.

32-39: Test lacks assertions and relies on a live API.

This test hits the real LRCLIB API, making it flaky in CI if the network is unavailable or the API is down. Additionally, println(result.data) on success doesn't verify any property of the response — the test passes as long as the API returns something.

Consider:

  1. Adding actual assertions on the result (e.g., non-empty list, expected fields populated).
  2. Marking this as an integration test (e.g., using JUnit categories or a naming convention) so it can be excluded from fast CI runs.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/test/java/LrcLibApiTest.kt` around lines 32 - 39, The testSearch
function currently calls the real lrcLibApi.searchLrcLyrics and only prints
result.data, which makes it flaky and assertion-less; update the test to either
(A) mock lrcLibApi.searchLrcLyrics (or inject a test double) and assert on the
returned Result.Success payload (e.g., non-empty list and required fields
populated) instead of println, or (B) mark the test as an integration test
(e.g., add a JUnit `@Tag`("integration") or rename to include “IntegrationTest”)
and replace println with concrete assertions that verify Result.Success and
expected properties; reference the testSearch method, the
lrcLibApi.searchLrcLyrics call, and the Result.Error/Result.Success branches
when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/shub39/rush/data/backup/RestoreImpl.kt`:
- Around line 28-31: The RestoreImpl.kt code uses kotlin.io.path APIs
(createTempFile, outputStream, readText, deleteIfExists) which require Android
API 26; fix by either raising the app minSdk to 26 (update the project config)
OR enable core-library desugaring: set coreLibraryDesugaring true in the android
block and add the desugar_jdk_libs dependency
(com.android.tools:desugar_jdk_libs) so those java.nio APIs are available on
older devices; alternatively, replace the kotlin.io.path calls in RestoreImpl
(createTempFile, outputStream, readText, deleteIfExists) with equivalent
java.io/File APIs (File.createTempFile, File.outputStream/writeBytes,
File.readText, File.delete) to maintain minSdk 24 without desugaring.

In `@app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt`:
- Around line 135-142: The Strategy 5 branch in LrcLibApi.kt currently assigns
results from queryLyricsWithParams to the local variable results but never
returns them, so the function always falls through to return
Result.Error(SourceError.Data.NO_RESULTS); update the Strategy 5 block in the
method that calls queryLyricsWithParams (referencing the variable results and
the queryLyricsWithParams(trackName, artistName) call) to check if the filtered
results list is non-empty and immediately return those results (e.g.,
wrap/return as the same successful Result type the method uses) instead of
letting execution continue to the final NO_RESULTS error return.
- Around line 24-25: Move the pure string-cleaning functions getMainTitle and
getMainArtist out of the presentation layer and into the domain module, then
update all imports in consumers (notably LrcLibApi and MediaListenerImpl) to
reference the new domain package; specifically, relocate the functions from
com.shub39.rush.presentation (presentation.util.kt) into an appropriate domain
package (e.g., com.shub39.rush.domain.util), export them from the domain module,
and change import statements in LrcLibApi and MediaListenerImpl to use the new
domain package so the data layer no longer depends on presentation.

In `@app/src/main/java/com/shub39/rush/presentation/util.kt`:
- Around line 136-145: getMainArtist currently stops at the first found
separator which can leave secondary separators in the result (e.g., "Artist1
feat. Artist2 & Artist3" -> "Artist1 feat. Artist2"); update getMainArtist to
either (preferred) iterate through all entries in artistSeparators (or
repeatedly split until no separator remains) and always take the substring
before the earliest/first-occurring separator, or else reorder artistSeparators
to put "feat." variants before " & " so priority is correct; specifically modify
getMainArtist to scan all separators (or loop until no change) using
artistSeparators and return the fully cleaned main artist.

---

Outside diff comments:
In `@app/build.gradle.kts`:
- Around line 27-28: Update the version constants to match the PR title: change
the value of appVersionName and appVersionCode so they represent 5.6.0 (update
the string in appVersionName and the numeric appVersionCode accordingly); locate
and modify the appVersionName and appVersionCode declarations in
build.gradle.kts to reflect the new release version.

In `@app/src/main/java/com/shub39/rush/data/backup/ExportImpl.kt`:
- Line 32: Remove the unused ExperimentalTime opt-in: delete the
`@OptIn`(ExperimentalTime::class) annotation and its import from ExportImpl (the
ExportImpl class / method where `@OptIn` is declared) since no kotlin.time APIs
are used; ensure no other usages of ExperimentalTime remain in that file and run
a quick compile to confirm the unused import/annotation are gone.

In `@app/src/main/java/com/shub39/rush/data/backup/RestoreImpl.kt`:
- Around line 52-53: The code silently proceeds when
context.contentResolver.openInputStream(path.toUri()) returns null, leaving an
empty temp file and causing a misleading
SerializationException/RestoreFailedException.OldSchema; update the block around
context.contentResolver.openInputStream(path.toUri()).use { input -> ... } to
explicitly check for null before using the stream (i.e., if input == null) and
fail fast by throwing a clear RestoreFailedException (or add a new variant)
indicating the input stream could not be opened for the given URI, rather than
continuing to write an empty file and later calling file.readText() which masks
the real error.

In `@app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsPage.kt`:
- Around line 340-360: The landscape layout still uses outdated typography for
the song title and artist: change the two Text composables that render
lyricsState.song.title and lyricsState.song.artists in LyricsPage.kt to use the
same migrated styles as portrait (swap MaterialTheme.typography.titleLarge ->
MaterialTheme.typography.headlineSmall for the title and
MaterialTheme.typography.bodyMedium -> MaterialTheme.typography.titleSmall for
the artist), keeping the existing fontWeight, maxLines, overflow and
Modifier.basicMarquee() unchanged to preserve behavior.

---

Duplicate comments:
In `@app/src/main/java/com/shub39/rush/data/network/GeniusApi.kt`:
- Around line 41-64: The HttpClient initialization in GeniusApi (the lazy
property client) duplicates the same setup used elsewhere (e.g., GeniusScraper);
refactor this by extracting the shared configuration into a single
factory/provider function (e.g., createSharedHttpClient or
HttpClientProvider.getClient) that encapsulates ContentNegotiation, HttpTimeout,
conditional Logging (BuildConfig.DEBUG) and defaultRequest contentType, then
replace the GeniusApi.client lazy block with a call to that shared factory so
both GeniusApi and GeniusScraper reuse the same HttpClient construction logic.

---

Nitpick comments:
In `@app/src/main/java/com/shub39/rush/data/network/GeniusScraper.kt`:
- Around line 46-68: Extract the duplicated HttpClient construction used in
GeniusScraper, GeniusApi and LrcLibApi into a single internal factory function
(e.g., internal fun createDefaultClient(): HttpClient) and replace the lazy
client properties in those classes to call this factory so configuration (OkHttp
engine, ContentNegotiation, HttpTimeout, conditional Logging, defaultRequest) is
centralized; also add connectTimeoutMillis to the HttpTimeout configuration so
connect, socket and request timeouts are all bounded.
- Line 66: Remove the misleading defaultRequest {
contentType(ContentType.Application.Json) } from the GeniusScraper Ktor client
configuration (locate it in GeniusScraper.kt where the client is built) since
the scraper only issues GET requests to HTML pages; instead, ensure the JSON
Content-Type is applied only where JSON requests are made (e.g., in the
GeniusApi client or request builders such as the class/method that builds API
calls for GeniusApi). If you refactor to a shared client builder, keep the JSON
default in the GeniusApi-specific client configuration or set the header
per-request in the GeniusApi methods rather than globally in GeniusScraper.

In `@app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt`:
- Around line 92-143: The method searchLrcLyrics currently wraps its whole body
in the inline safeCall lambda but uses non-local returns on success so
safeCall's HttpResponse→Result conversion (responseToResult) is never exercised;
replace the use of safeCall with a simple try/catch that only handles exceptions
(or introduce a new helper safeCatch) so searchLrcLyrics returns
Result.Success/Result.Error directly from its success paths and exceptions are
mapped to Result.Error in one place; update references to safeCall,
responseToResult and keep searchLrcLyrics, queryLyricsWithParams and
getMainTitle/getMainArtist intact so callers and behavior remain the same while
removing reliance on non-local returns.
- Around line 100-107: Extract the repeated predicate { it.syncedLyrics != null
|| it.plainLyrics != null } into a single named val or function inside the
safeCall block (e.g. val hasLyrics: (LrcGetDto) -> Boolean = { it.syncedLyrics
!= null || it.plainLyrics != null }) and replace every .filter { it.syncedLyrics
!= null || it.plainLyrics != null } in the search strategies that call
queryLyricsWithParams(...) with .filter(hasLyrics) so all five strategy results
use the shared predicate; keep the predicate local to the safeCall scope and
ensure the type matches the LrcGetDto returned by queryLyricsWithParams.

In `@app/src/main/java/com/shub39/rush/presentation/provideTypography.kt`:
- Around line 35-69: The three FontFamily vals flexFontDisplay,
flexFontHeadline, and flexFontTitle are recreated on every call; hoist them to
top-level immutable vals so they are allocated once. Move the current
declarations for flexFontDisplay, flexFontHeadline, and flexFontTitle out of the
function/scope where they currently live and place them as top-level val
declarations (alongside TYPOGRAPHY), keeping their Font(...) and
FontVariation.Settings(...) definitions identical, then have the
provideTypography code reference these top-level vals instead of local ones.

In `@app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt`:
- Around line 103-121: The LargeFlexibleTopAppBar is not wired to a
scrollBehavior so it won't collapse with list scrolling; create a val
scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(), pass
scrollBehavior = scrollBehavior into LargeFlexibleTopAppBar and add
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) to the surrounding
Scaffold (or top-level container) so the bar collapses consistently like
SettingRootPage.

In
`@app/src/main/java/com/shub39/rush/presentation/setting/section/BackupPage.kt`:
- Around line 125-140: Rename the local variable scrollBehaviour to
scrollBehavior to match the Compose API naming and avoid inconsistency: change
the variable created from TopAppBarDefaults.enterAlwaysScrollBehavior() and all
its usages (e.g., nestedScroll(scrollBehaviour.nestedScrollConnection) and
scrollBehavior = scrollBehaviour passed into MediumFlexibleTopAppBar) to use
scrollBehavior instead; apply the same rename in LookAndFeelPage.kt so both
files consistently use scrollBehavior and update any references to
nestedScrollConnection accordingly.

In
`@app/src/main/java/com/shub39/rush/presentation/setting/section/SettingRootPage.kt`:
- Around line 92-98: Rename the British-spelled local variable scrollBehaviour
to the API-consistent scrollBehavior: change the declaration using
TopAppBarDefaults.enterAlwaysScrollBehavior() and update all usages (e.g.,
Modifier.nestedScroll(scrollBehaviour.nestedScrollConnection) and the
LargeFlexibleTopAppBar(scrollBehavior = scrollBehaviour) to use scrollBehavior
and scrollBehavior.nestedScrollConnection so names match the Compose API.

In `@app/src/main/java/com/shub39/rush/presentation/util.kt`:
- Around line 121-134: The artistSeparators list contains a redundant " X "
entry because getMainArtist calls contains(separator, ignoreCase = true) and
split(..., ignoreCase = true), so the lowercase " x " already matches; remove
the " X " element from the artistSeparators list to eliminate dead code and keep
only the case-insensitive separators (update the list in the artistSeparators
declaration and verify getMainArtist still uses contains(..., ignoreCase = true)
and split(..., ignoreCase = true) as intended).

In `@app/src/test/java/LrcLibApiTest.kt`:
- Around line 26-30: Extract the duplicated suspend-test helper into a single
shared test utility by moving the top-level function testIn(title: String,
block: suspend CoroutineScope.() -> Unit) = runBlocking { ... } out of
LrcLibApiTest, GeniusApiTest and ScraperTest and into a new test helper file
(e.g., TestUtils.kt) in the test sources; keep the exact signature and
runBlocking usage so behavior doesn't change, remove the duplicate definitions
from the three test files, and update those tests to import and call the shared
testIn function instead of their local copies.
- Around line 32-39: The testSearch function currently calls the real
lrcLibApi.searchLrcLyrics and only prints result.data, which makes it flaky and
assertion-less; update the test to either (A) mock lrcLibApi.searchLrcLyrics (or
inject a test double) and assert on the returned Result.Success payload (e.g.,
non-empty list and required fields populated) instead of println, or (B) mark
the test as an integration test (e.g., add a JUnit `@Tag`("integration") or rename
to include “IntegrationTest”) and replace println with concrete assertions that
verify Result.Success and expected properties; reference the testSearch method,
the lrcLibApi.searchLrcLyrics call, and the Result.Error/Result.Success branches
when making the change.

In `@CHANGELOG.md`:
- Line 4: Update the changelog entry string "LRCLIB api not responding
sometimes" to use uppercase API for consistency by changing it to "LRCLIB API
not responding sometimes"; modify the exact text in CHANGELOG.md where that line
appears so "api" is capitalized to "API".

In `@README.md`:
- Line 4: Update the alt text in the Markdown image tag [<img alt="Get it on
github" src="badges/github.png" width="180px">] to use the correct
capitalization "Get it on GitHub" so the alt attribute reads alt="Get it on
GitHub".

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca6b90a and 727643c.

⛔ Files ignored due to path filters (2)
  • app/src/main/res/font/google_sans.ttf is excluded by !**/*.ttf
  • app/src/main/res/font/google_sans_flex.ttf is excluded by !**/*.ttf
📒 Files selected for processing (30)
  • CHANGELOG.md
  • README.md
  • app/build.gradle.kts
  • app/src/main/java/com/shub39/rush/data/HttpClientFactory.kt
  • app/src/main/java/com/shub39/rush/data/backup/ExportImpl.kt
  • app/src/main/java/com/shub39/rush/data/backup/RestoreImpl.kt
  • app/src/main/java/com/shub39/rush/data/network/GeniusApi.kt
  • app/src/main/java/com/shub39/rush/data/network/GeniusScraper.kt
  • app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt
  • app/src/main/java/com/shub39/rush/data/network/Tokens.kt
  • app/src/main/java/com/shub39/rush/data/repository/RushRepository.kt
  • app/src/main/java/com/shub39/rush/di/RushModules.kt
  • app/src/main/java/com/shub39/rush/presentation/enumExt.kt
  • app/src/main/java/com/shub39/rush/presentation/glow.kt
  • app/src/main/java/com/shub39/rush/presentation/lyrics/component/CurveVisualizer.kt
  • app/src/main/java/com/shub39/rush/presentation/lyrics/component/SyncedLyrics.kt
  • app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsPage.kt
  • app/src/main/java/com/shub39/rush/presentation/provideTypography.kt
  • app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/component/AboutApp.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/section/BackupPage.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/section/LookAndFeelPage.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/section/SettingRootPage.kt
  • app/src/main/java/com/shub39/rush/presentation/share/SharePage.kt
  • app/src/main/java/com/shub39/rush/presentation/util.kt
  • app/src/test/java/GeniusApiTest.kt
  • app/src/test/java/LrcLibApiTest.kt
  • app/src/test/java/ScraperTest.kt
  • gradle/libs.versions.toml
  • visualizer-helper/build.gradle.kts
💤 Files with no reviewable changes (4)
  • app/src/main/java/com/shub39/rush/data/network/Tokens.kt
  • app/src/main/java/com/shub39/rush/di/RushModules.kt
  • visualizer-helper/build.gradle.kts
  • app/src/main/java/com/shub39/rush/data/HttpClientFactory.kt

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
app/src/main/java/com/shub39/rush/presentation/share/SharePage.kt (1)

154-156: ⚠️ Potential issue | 🟠 Major

Resource leak: FileOutputStream must be closed in a use {} block.

If imageBitmap.compress(...) throws (e.g. disk full, permission denied, OOM), stream.close() on line 156 is never reached, leaking the file descriptor. Use Kotlin's Closeable.use {} to guarantee closure.

🛡️ Proposed fix
-val stream = FileOutputStream(file)
-imageBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
-stream.close()
+FileOutputStream(file).use { stream ->
+    imageBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/presentation/share/SharePage.kt` around
lines 154 - 156, Wrap the FileOutputStream creation and usage in a Kotlin use {
} block to guarantee closure even if imageBitmap.compress(...) throws; replace
the manual stream creation and stream.close() around the calls to
FileOutputStream(...) / imageBitmap.compress(Bitmap.CompressFormat.PNG, 100,
stream) in SharePage.kt with FileOutputStream(file).use { stream -> ... } so the
stream is always closed; ensure any exceptions propagate or are handled as
before.
app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt (1)

98-121: ⚠️ Potential issue | 🟠 Major

Missing scrollBehavior wiring for LargeFlexibleTopAppBar.

Every other page in this PR that uses a flexible top bar (SettingRootPage, LookAndFeelPage, BackupPage) wires a TopAppBarDefaults.enterAlwaysScrollBehavior() to the bar and applies .nestedScroll(scrollBehaviour.nestedScrollConnection) on the Scaffold modifier. This page does not, so the large top bar will remain fully expanded and never collapse on scroll, consuming significant vertical space.

Proposed fix
  Scaffold(
-     modifier = modifier,
+     modifier = modifier.nestedScroll(scrollBehaviour.nestedScrollConnection),
      topBar = {
          Column {
              if (!isLandscape) {
                  LargeFlexibleTopAppBar(
+                     scrollBehavior = scrollBehaviour,
                      title = { Text(stringResource(R.string.rush_branding)) },

And add before the Scaffold:

val scrollBehaviour = TopAppBarDefaults.enterAlwaysScrollBehavior()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt` around
lines 98 - 121, The LargeFlexibleTopAppBar is missing a scrollBehavior hookup so
it won't collapse; define val scrollBehaviour =
TopAppBarDefaults.enterAlwaysScrollBehavior() just before the Scaffold, pass
scrollBehavior = scrollBehaviour into the LargeFlexibleTopAppBar call, and apply
.nestedScroll(scrollBehaviour.nestedScrollConnection) to the Scaffold's modifier
(i.e., use modifier.nestedScroll(scrollBehaviour.nestedScrollConnection)) so the
app bar collapses on scroll.
app/src/main/java/com/shub39/rush/data/backup/ExportImpl.kt (1)

39-43: ⚠️ Potential issue | 🟠 Major

Log.wtf is inappropriate in a recoverable catch block and can terminate the process

Log.wtf signals an unrecoverable programmer error ("What a Terrible Failure"). On Android, the default wtf handler can kill the process (or trigger a crash report), which directly contradicts the intent of this try/catch that recovers by returning null. Use Log.e instead.

🐛 Proposed fix
-                Log.wtf("ExportImpl", e)
+                Log.e("ExportImpl", "Failed to export songs to JSON", e)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/backup/ExportImpl.kt` around lines 39
- 43, In the catch block that currently uses Log.wtf in ExportImpl (the catch
(e: Exception) handler), replace Log.wtf("ExportImpl", e) with a recoverable
error log such as Log.e("ExportImpl", "Error exporting backup", e) so the
exception and context are recorded without invoking the WTF handler; keep the
existing return null behavior to preserve recovery.
app/build.gradle.kts (1)

27-28: ⚠️ Potential issue | 🔴 Critical

Version numbers not updated for the 5.6.0 release.

appVersionName is still "5.5.2" and appVersionCode is still 5520, but this PR is titled "5.6.0" and the changelog contains a 5.6.0 section.

🐛 Proposed fix
-val appVersionName = "5.5.2"
-val appVersionCode = 5520
+val appVersionName = "5.6.0"
+val appVersionCode = 5600
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/build.gradle.kts` around lines 27 - 28, Update the release version
constants: set appVersionName to "5.6.0" and bump appVersionCode to 5600
(calculate as 5.6.0 → 5000+600+0 = 5600) so the code symbols appVersionName and
appVersionCode reflect the 5.6.0 release.
♻️ Duplicate comments (4)
app/src/main/java/com/shub39/rush/data/backup/RestoreImpl.kt (1)

28-31: Pre-existing kotlin.io.path / minSdk 24 incompatibility remains unresolved.

The import consolidation is a clean improvement, but the underlying risk flagged in the previous review is still present: createTempFile, outputStream, readText, and deleteIfExists all wrap java.nio.file.*, which requires API 26, while the project's minSdk is 24. This PR doesn't address it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/backup/RestoreImpl.kt` around lines 28
- 31, RestoreImpl.kt still imports kotlin.io.path functions (createTempFile,
outputStream, readText, deleteIfExists) which rely on java.nio.file (API 26+)
and break minSdk 24; replace those usages with Android-compatible java.io/File
APIs: use java.io.File.createTempFile(...) or create temp file in
context.cacheDir, call File.outputStream() / File.readText() / File.delete()
instead of the kotlin.io.path variants, and remove the kotlin.io.path imports so
RestoreImpl.kt no longer references java.nio.file.
app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt (2)

24-25: ⚠️ Potential issue | 🟠 Major

Data layer imports from the presentation layer — architectural violation.

getMainTitle and getMainArtist live in com.shub39.rush.presentation, yet the data-layer class LrcLibApi imports and calls them. The data layer must not depend on the presentation layer. These are pure string-utility functions with no UI dependencies; they belong in the domain layer (e.g., com.shub39.rush.domain.util). This was flagged in a previous review and remains unresolved.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt` around lines 24
- 25, LrcLibApi is importing getMainTitle and getMainArtist from the
presentation layer (violation); move these pure string-utility functions into
the domain layer (e.g., create com.shub39.rush.domain.util.StringUtils or
similar), update their package and signatures there, change imports in LrcLibApi
to use com.shub39.rush.domain.util.getMainTitle and getMainArtist, and remove
any presentation-layer references so the data layer depends only on domain
utilities.

135-142: ⚠️ Potential issue | 🔴 Critical

Bug: Strategy 5 results are computed but never returned.

The result from queryLyricsWithParams on Line 138 is assigned to results but the function unconditionally falls through to return Result.Error(SourceError.Data.NO_RESULTS) on Line 142, discarding the Strategy 5 results entirely. This was flagged in a previous review and remains unresolved.

🐛 Proposed fix
         // Strategy 5: Try original title if different from cleaned
         if (cleanedTitle != trackName.trim()) {
             results =
                 queryLyricsWithParams(trackName = trackName.trim(), artistName = artistName.trim())
                     .filter { it.syncedLyrics != null || it.plainLyrics != null }
+
+            if (results.isNotEmpty()) return Result.Success(results)
         }
 
         return Result.Error(SourceError.Data.NO_RESULTS)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt` around lines 135
- 142, The strategy-5 search assigns results from queryLyricsWithParams to the
local variable results but then always falls through to return
Result.Error(SourceError.Data.NO_RESULTS); fix this by checking the newly
assigned results (from queryLyricsWithParams with trackName.trim() and
artistName.trim()) and returning a successful Result when they contain
syncedLyrics/plainLyrics (the same condition used to filter), otherwise continue
to the error return; locate symbols cleanedTitle, trackName, artistName,
queryLyricsWithParams, results and ensure the function returns the results when
non-empty instead of unconditionally returning
Result.Error(SourceError.Data.NO_RESULTS).
app/src/main/java/com/shub39/rush/presentation/util.kt (1)

136-145: ⚠️ Potential issue | 🟠 Major

getMainArtist still breaks after the first matching separator.

The break on Line 141 stops iteration after the first matched separator, so secondary separators in the result are never removed (e.g., "Artist1 feat. Artist2 & Artist3" → splits on " & " first → returns "Artist1 feat. Artist2" instead of "Artist1"). This was flagged in a previous review and remains unaddressed.

🐛 Proposed fix (remove the `break` and take the minimum-index split)
 fun getMainArtist(artists: String): String {
-    var cleaned = artists.trim()
-    for (separator in artistSeparators) {
-        if (cleaned.contains(separator, ignoreCase = true)) {
-            cleaned = cleaned.split(separator, ignoreCase = true, limit = 2)[0]
-            break
-        }
-    }
-    return cleaned.trim()
+    val input = artists.trim()
+    val earliest = artistSeparators
+        .mapNotNull { sep ->
+            val idx = input.indexOf(sep, ignoreCase = true)
+            if (idx >= 0) idx to sep else null
+        }
+        .minByOrNull { it.first }
+    return if (earliest != null) input.substring(0, earliest.first).trim() else input
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/presentation/util.kt` around lines 136 -
145, getMainArtist currently stops after the first matching separator due to the
break, so secondary separators in the result remain; update getMainArtist to
consider all separators from artistSeparators (do not break) and choose the
earliest split (e.g., track the minimal index or shortest left-side substring)
so the returned artist is the prefix before the first occurring separator, then
trim and return that value.
🧹 Nitpick comments (6)
app/src/main/java/com/shub39/rush/presentation/provideTypography.kt (2)

34-34: Add @FontRes to enforce resource-type safety for font.

This tightens call-site correctness for the nullable font resource parameter.

🔧 Suggested refactor
+import androidx.annotation.FontRes
@@
-fun provideTypography(font: Int? = R.font.poppins_regular): Typography {
+fun provideTypography(`@FontRes` font: Int? = R.font.poppins_regular): Typography {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/presentation/provideTypography.kt` at line
34, The provideTypography function's font parameter should be annotated with
`@FontRes` to enforce resource-type safety; update the function signature for
provideTypography(font: Int? = R.font.poppins_regular) to add the `@FontRes`
annotation on the font parameter and import androidx.annotation.FontRes so
callers and the compiler treat the parameter as a font resource (preserving
nullable and default behavior).

35-69: Wrap static FontFamily instances with remember to avoid rebuilding on recomposition.

The three flex font objects don't depend on the font parameter and remain identical across all calls. Using remember prevents unnecessary allocations when the composable recomposes (e.g., when the theme changes in RushTheme.kt).

♻️ Suggested refactor
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.text.ExperimentalTextApi
@@
 `@OptIn`(ExperimentalTextApi::class)
 `@Composable`
 fun provideTypography(font: Int? = R.font.poppins_regular): Typography {
-    val flexFontDisplay =
+    val flexFontDisplay = remember {
         FontFamily(
             Font(
                 resId = R.font.google_sans_flex,
@@
             )
         )
-    val flexFontHeadline =
+    }
+    val flexFontHeadline = remember {
         FontFamily(
             Font(
                 resId = R.font.google_sans_flex,
@@
             )
         )
-    val flexFontTitle =
+    }
+    val flexFontTitle = remember {
         FontFamily(
             Font(
                 resId = R.font.google_sans_flex,
@@
             )
         )
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/presentation/provideTypography.kt` around
lines 35 - 69, The three static FontFamily instances (flexFontDisplay,
flexFontHeadline, flexFontTitle) are rebuilt on every recomposition; wrap each
FontFamily creation with remember so they are allocated once and reused — locate
the FontFamily(...) constructions that call Font(resId =
R.font.google_sans_flex, variationSettings = ...) and change them to use
remember { FontFamily(Font(...)) } (or remember(font) if you must depend on an
external param) to prevent unnecessary allocations during recomposition.
app/src/main/java/com/shub39/rush/data/network/GeniusApi.kt (1)

42-64: Identical HttpClient setup is duplicated across GeniusApi, GeniusScraper, and LrcLibApi.

The ContentNegotiation, HttpTimeout, Logging, and defaultRequest configuration blocks are copy-pasted verbatim across three classes. Consider extracting a shared builder function (e.g., fun buildRushHttpClient(configure: HttpClientConfig<OkHttpConfig>.() -> Unit = {}): HttpClient) in a common location (e.g., HttpClientExt.kt) so all three classes delegate to it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/network/GeniusApi.kt` around lines 42
- 64, The HttpClient setup in GeniusApi (the private val client), GeniusScraper
and LrcLibApi is duplicated; extract a shared factory like fun
buildRushHttpClient(configure: HttpClientConfig<OkHttpConfig>.() -> Unit = {})
in a new HttpClientExt.kt and have each class replace its local HttpClient
creation with buildRushHttpClient { /* per-class overrides */ }; the shared
builder should install ContentNegotiation(Json{ignoreUnknownKeys=true}),
HttpTimeout (socket/request 20_000ms), conditional Logging when
BuildConfig.DEBUG, and defaultRequest contentType(Application.Json), while
allowing callers to pass additional configuration via the configure lambda.
app/src/main/java/com/shub39/rush/data/network/GeniusScraper.kt (1)

47-53: ContentNegotiation with JSON is unnecessary in GeniusScraper.

GeniusScraper only reads raw HTML via body<String>() and never deserializes JSON. Installing the JSON content negotiation plugin adds overhead without benefit.

✂️ Proposed fix
     private val client by lazy {
         HttpClient(OkHttp) {
-            install(ContentNegotiation) { json(json = Json { ignoreUnknownKeys = true }) }
-
             install(HttpTimeout) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/network/GeniusScraper.kt` around lines
47 - 53, GeniusScraper is installing ContentNegotiation/Json even though it only
reads raw HTML via body<String>(); remove the install(ContentNegotiation) {
json(...) } block from the HttpClient(OkHttp) builder in GeniusScraper, leaving
the HttpTimeout configuration intact (socketTimeoutMillis/requestTimeoutMillis)
and any other plugins; also remove any now-unused imports related to
ContentNegotiation and Json to keep the file clean.
app/src/test/java/LrcLibApiTest.kt (1)

26-30: testIn is duplicated verbatim across LrcLibApiTest, GeniusApiTest, and ScraperTest.

Consider extracting it to a shared abstract base class or a top-level utility in the test source set to avoid the triple copy.

♻️ Suggested extraction

Create a shared test utility (e.g., TestHelpers.kt in the test source set):

// app/src/test/java/TestHelpers.kt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking

fun testIn(title: String, block: suspend CoroutineScope.() -> Unit) = runBlocking {
    println("\n-- $title --")
    block.invoke(this)
    println("\n")
}

Then remove the local testIn from each test class and call the top-level function directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/test/java/LrcLibApiTest.kt` around lines 26 - 30, Extract the
duplicated testIn function into a shared test helper in the test source set and
remove the copies from LrcLibApiTest, GeniusApiTest, and ScraperTest;
specifically, create a top-level utility (e.g., TestHelpers.kt) that declares
fun testIn(title: String, block: suspend CoroutineScope.() -> Unit) =
runBlocking { ... } and import kotlinx.coroutines.CoroutineScope and runBlocking
there, then delete the local testIn declarations from the three test classes and
call the shared testIn function instead (or alternatively move testIn into a
common abstract base test class used by those tests).
app/src/main/java/com/shub39/rush/data/repository/RushRepository.kt (1)

31-32: Kotlin 2.3.10 satisfies the ≥2.1 requirement for kotlin.time.Clock.

The project uses Kotlin 2.3.10 (from gradle/libs.versions.toml), which meets the minimum requirement. The @ExperimentalTime annotation is correctly opted into at line 47.

For improved testability, consider injecting Clock as a dependency rather than using Clock.System directly—this would allow unit tests to control time deterministically when testing dateAdded. This is a low-priority refactor but worth noting for future improvements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/repository/RushRepository.kt` around
lines 31 - 32, The repository currently uses Clock.System directly which makes
date/time hard to control in tests; update RushRepository to accept a
kotlin.time.Clock as a constructor dependency (e.g., add parameter clock: Clock
= Clock.System) and replace direct uses of Clock.System with the injected clock
(references: RushRepository class and any method creating dateAdded). Ensure the
existing `@OptIn`(ExperimentalTime::class) usage remains and update any call sites
or tests that instantiate RushRepository to rely on the default or provide a
test Clock for deterministic behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/shub39/rush/data/backup/ExportImpl.kt`:
- Line 24: Remove the obsolete opt-in import and annotation: delete the import
kotlin.time.ExperimentalTime and remove the `@OptIn`(ExperimentalTime::class)
annotation near ExportImpl (the opt-in is unnecessary because Kotlin 2.3.10
stabilizes Clock/Instant and ExportImpl uses Long dateAdded in the Song data
class and getAllSongs() has no opt-in requirement). Also replace the logging
call Log.wtf("ExportImpl", e) inside the exception handler with
Log.e("ExportImpl", "message", e) (or a similar Log.e usage) so recoverable
exceptions return null without invoking fatal crash behavior.

In `@app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt`:
- Around line 92-143: searchLrcLyrics is wrapped in safeCall but never returns
an HttpResponse (it uses non-local returns) so safeCall's exception mapping is
never used; remove the safeCall wrapper and make searchLrcLyrics a plain suspend
function that relies on queryLyricsWithParams (which already uses runCatching)
for HTTP error handling, i.e., delete the surrounding safeCall { ... } and keep
the existing strategies and early returns inside searchLrcLyrics while leaving
queryLyricsWithParams and its runCatching logic unchanged.

In `@README.md`:
- Line 3: Update the badge image alt text to use the correct platform
capitalization: locate the <img> tag's alt attribute in README.md (the badge
image tag, e.g., the img element for the GitHub badge) and change "github" to
"GitHub" so the alt text matches official branding.

---

Outside diff comments:
In `@app/build.gradle.kts`:
- Around line 27-28: Update the release version constants: set appVersionName to
"5.6.0" and bump appVersionCode to 5600 (calculate as 5.6.0 → 5000+600+0 = 5600)
so the code symbols appVersionName and appVersionCode reflect the 5.6.0 release.

In `@app/src/main/java/com/shub39/rush/data/backup/ExportImpl.kt`:
- Around line 39-43: In the catch block that currently uses Log.wtf in
ExportImpl (the catch (e: Exception) handler), replace Log.wtf("ExportImpl", e)
with a recoverable error log such as Log.e("ExportImpl", "Error exporting
backup", e) so the exception and context are recorded without invoking the WTF
handler; keep the existing return null behavior to preserve recovery.

In `@app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt`:
- Around line 98-121: The LargeFlexibleTopAppBar is missing a scrollBehavior
hookup so it won't collapse; define val scrollBehaviour =
TopAppBarDefaults.enterAlwaysScrollBehavior() just before the Scaffold, pass
scrollBehavior = scrollBehaviour into the LargeFlexibleTopAppBar call, and apply
.nestedScroll(scrollBehaviour.nestedScrollConnection) to the Scaffold's modifier
(i.e., use modifier.nestedScroll(scrollBehaviour.nestedScrollConnection)) so the
app bar collapses on scroll.

In `@app/src/main/java/com/shub39/rush/presentation/share/SharePage.kt`:
- Around line 154-156: Wrap the FileOutputStream creation and usage in a Kotlin
use { } block to guarantee closure even if imageBitmap.compress(...) throws;
replace the manual stream creation and stream.close() around the calls to
FileOutputStream(...) / imageBitmap.compress(Bitmap.CompressFormat.PNG, 100,
stream) in SharePage.kt with FileOutputStream(file).use { stream -> ... } so the
stream is always closed; ensure any exceptions propagate or are handled as
before.

---

Duplicate comments:
In `@app/src/main/java/com/shub39/rush/data/backup/RestoreImpl.kt`:
- Around line 28-31: RestoreImpl.kt still imports kotlin.io.path functions
(createTempFile, outputStream, readText, deleteIfExists) which rely on
java.nio.file (API 26+) and break minSdk 24; replace those usages with
Android-compatible java.io/File APIs: use java.io.File.createTempFile(...) or
create temp file in context.cacheDir, call File.outputStream() / File.readText()
/ File.delete() instead of the kotlin.io.path variants, and remove the
kotlin.io.path imports so RestoreImpl.kt no longer references java.nio.file.

In `@app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt`:
- Around line 24-25: LrcLibApi is importing getMainTitle and getMainArtist from
the presentation layer (violation); move these pure string-utility functions
into the domain layer (e.g., create com.shub39.rush.domain.util.StringUtils or
similar), update their package and signatures there, change imports in LrcLibApi
to use com.shub39.rush.domain.util.getMainTitle and getMainArtist, and remove
any presentation-layer references so the data layer depends only on domain
utilities.
- Around line 135-142: The strategy-5 search assigns results from
queryLyricsWithParams to the local variable results but then always falls
through to return Result.Error(SourceError.Data.NO_RESULTS); fix this by
checking the newly assigned results (from queryLyricsWithParams with
trackName.trim() and artistName.trim()) and returning a successful Result when
they contain syncedLyrics/plainLyrics (the same condition used to filter),
otherwise continue to the error return; locate symbols cleanedTitle, trackName,
artistName, queryLyricsWithParams, results and ensure the function returns the
results when non-empty instead of unconditionally returning
Result.Error(SourceError.Data.NO_RESULTS).

In `@app/src/main/java/com/shub39/rush/presentation/util.kt`:
- Around line 136-145: getMainArtist currently stops after the first matching
separator due to the break, so secondary separators in the result remain; update
getMainArtist to consider all separators from artistSeparators (do not break)
and choose the earliest split (e.g., track the minimal index or shortest
left-side substring) so the returned artist is the prefix before the first
occurring separator, then trim and return that value.

---

Nitpick comments:
In `@app/src/main/java/com/shub39/rush/data/network/GeniusApi.kt`:
- Around line 42-64: The HttpClient setup in GeniusApi (the private val client),
GeniusScraper and LrcLibApi is duplicated; extract a shared factory like fun
buildRushHttpClient(configure: HttpClientConfig<OkHttpConfig>.() -> Unit = {})
in a new HttpClientExt.kt and have each class replace its local HttpClient
creation with buildRushHttpClient { /* per-class overrides */ }; the shared
builder should install ContentNegotiation(Json{ignoreUnknownKeys=true}),
HttpTimeout (socket/request 20_000ms), conditional Logging when
BuildConfig.DEBUG, and defaultRequest contentType(Application.Json), while
allowing callers to pass additional configuration via the configure lambda.

In `@app/src/main/java/com/shub39/rush/data/network/GeniusScraper.kt`:
- Around line 47-53: GeniusScraper is installing ContentNegotiation/Json even
though it only reads raw HTML via body<String>(); remove the
install(ContentNegotiation) { json(...) } block from the HttpClient(OkHttp)
builder in GeniusScraper, leaving the HttpTimeout configuration intact
(socketTimeoutMillis/requestTimeoutMillis) and any other plugins; also remove
any now-unused imports related to ContentNegotiation and Json to keep the file
clean.

In `@app/src/main/java/com/shub39/rush/data/repository/RushRepository.kt`:
- Around line 31-32: The repository currently uses Clock.System directly which
makes date/time hard to control in tests; update RushRepository to accept a
kotlin.time.Clock as a constructor dependency (e.g., add parameter clock: Clock
= Clock.System) and replace direct uses of Clock.System with the injected clock
(references: RushRepository class and any method creating dateAdded). Ensure the
existing `@OptIn`(ExperimentalTime::class) usage remains and update any call sites
or tests that instantiate RushRepository to rely on the default or provide a
test Clock for deterministic behavior.

In `@app/src/main/java/com/shub39/rush/presentation/provideTypography.kt`:
- Line 34: The provideTypography function's font parameter should be annotated
with `@FontRes` to enforce resource-type safety; update the function signature for
provideTypography(font: Int? = R.font.poppins_regular) to add the `@FontRes`
annotation on the font parameter and import androidx.annotation.FontRes so
callers and the compiler treat the parameter as a font resource (preserving
nullable and default behavior).
- Around line 35-69: The three static FontFamily instances (flexFontDisplay,
flexFontHeadline, flexFontTitle) are rebuilt on every recomposition; wrap each
FontFamily creation with remember so they are allocated once and reused — locate
the FontFamily(...) constructions that call Font(resId =
R.font.google_sans_flex, variationSettings = ...) and change them to use
remember { FontFamily(Font(...)) } (or remember(font) if you must depend on an
external param) to prevent unnecessary allocations during recomposition.

In `@app/src/test/java/LrcLibApiTest.kt`:
- Around line 26-30: Extract the duplicated testIn function into a shared test
helper in the test source set and remove the copies from LrcLibApiTest,
GeniusApiTest, and ScraperTest; specifically, create a top-level utility (e.g.,
TestHelpers.kt) that declares fun testIn(title: String, block: suspend
CoroutineScope.() -> Unit) = runBlocking { ... } and import
kotlinx.coroutines.CoroutineScope and runBlocking there, then delete the local
testIn declarations from the three test classes and call the shared testIn
function instead (or alternatively move testIn into a common abstract base test
class used by those tests).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca6b90a and 5a18499.

⛔ Files ignored due to path filters (2)
  • app/src/main/res/font/google_sans.ttf is excluded by !**/*.ttf
  • app/src/main/res/font/google_sans_flex.ttf is excluded by !**/*.ttf
📒 Files selected for processing (31)
  • CHANGELOG.md
  • README.md
  • app/build.gradle.kts
  • app/src/main/java/com/shub39/rush/data/HttpClientFactory.kt
  • app/src/main/java/com/shub39/rush/data/backup/ExportImpl.kt
  • app/src/main/java/com/shub39/rush/data/backup/RestoreImpl.kt
  • app/src/main/java/com/shub39/rush/data/network/GeniusApi.kt
  • app/src/main/java/com/shub39/rush/data/network/GeniusScraper.kt
  • app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt
  • app/src/main/java/com/shub39/rush/data/network/Tokens.kt
  • app/src/main/java/com/shub39/rush/data/repository/RushRepository.kt
  • app/src/main/java/com/shub39/rush/di/RushModules.kt
  • app/src/main/java/com/shub39/rush/presentation/enumExt.kt
  • app/src/main/java/com/shub39/rush/presentation/glow.kt
  • app/src/main/java/com/shub39/rush/presentation/lyrics/component/CurveVisualizer.kt
  • app/src/main/java/com/shub39/rush/presentation/lyrics/component/SyncedLyrics.kt
  • app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsPage.kt
  • app/src/main/java/com/shub39/rush/presentation/provideTypography.kt
  • app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/component/AboutApp.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/section/BackupPage.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/section/LookAndFeelPage.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/section/SettingRootPage.kt
  • app/src/main/java/com/shub39/rush/presentation/share/SharePage.kt
  • app/src/main/java/com/shub39/rush/presentation/util.kt
  • app/src/main/res/values-frc/strings.xml
  • app/src/test/java/GeniusApiTest.kt
  • app/src/test/java/LrcLibApiTest.kt
  • app/src/test/java/ScraperTest.kt
  • gradle/libs.versions.toml
  • visualizer-helper/build.gradle.kts
💤 Files with no reviewable changes (4)
  • app/src/main/java/com/shub39/rush/di/RushModules.kt
  • app/src/main/java/com/shub39/rush/data/HttpClientFactory.kt
  • visualizer-helper/build.gradle.kts
  • app/src/main/java/com/shub39/rush/data/network/Tokens.kt

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt (1)

42-81: Lazy HttpClient initialization is well-configured.

The internal lazy initialization keeps the API class self-contained. Configuration looks solid with proper timeouts and conditional debug logging.

One consideration: expectSuccess = true (line 79) will throw on non-2xx responses, which is caught by runCatching in queryLyricsWithParams. This silently converts server errors to empty results. For production debugging, you might want to log these failures.

♻️ Optional: Add error logging for visibility
     private suspend fun queryLyricsWithParams(
         trackName: String? = null,
         artistName: String? = null,
         albumName: String? = null,
         query: String? = null,
     ): List<LrcGetDto> =
         runCatching {
                 client
                     .get("/api/search") {
                         if (query != null) parameter("q", query)
                         if (trackName != null) parameter("track_name", trackName)
                         if (artistName != null) parameter("artist_name", artistName)
                         if (albumName != null) parameter("album_name", albumName)
                     }
                     .body<List<LrcGetDto>>()
             }
-            .getOrDefault(emptyList())
+            .onFailure { e ->
+                if (BuildConfig.DEBUG) println("LrcLib query failed: ${e.message}")
+            }
+            .getOrDefault(emptyList())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt` around lines 42
- 81, The client is configured with expectSuccess = true so non-2xx responses
thrown from HTTP calls inside LrcLibApi are being swallowed by the runCatching
in queryLyricsWithParams; update queryLyricsWithParams to log the caught
exception (include status and message where available) using the project logger
(or Android Log) before returning the empty result so server/client errors are
visible in production; specifically, in the catch block for
queryLyricsWithParams, record the Throwable (and if it's an
HttpResponseException or ResponseException extract response.status and
response.body/response.message) and emit a clear log entry referencing
LrcLibApi.queryLyricsWithParams.
app/src/main/java/com/shub39/rush/presentation/ProvideTypography.kt (1)

80-96: Wire flex presets into provideTypography() output.

The flex preset functions defined on lines 34–74 are only used in the preview (lines 104–106) and not applied to the returned Typography object. Lines 81–89 should use the flex presets for display/headline/title styles instead of selectedFont.

♻️ Proposed refactor
     return Typography(
-        displayLarge = TYPOGRAPHY.displayLarge.copy(fontFamily = selectedFont),
-        displayMedium = TYPOGRAPHY.displayMedium.copy(fontFamily = selectedFont),
-        displaySmall = TYPOGRAPHY.displaySmall.copy(fontFamily = selectedFont),
-        headlineLarge = TYPOGRAPHY.headlineLarge.copy(fontFamily = selectedFont),
-        headlineMedium = TYPOGRAPHY.headlineMedium.copy(fontFamily = selectedFont),
-        headlineSmall = TYPOGRAPHY.headlineSmall.copy(fontFamily = selectedFont),
-        titleLarge = TYPOGRAPHY.titleLarge.copy(fontFamily = selectedFont),
-        titleMedium = TYPOGRAPHY.titleMedium.copy(fontFamily = selectedFont),
-        titleSmall = TYPOGRAPHY.titleSmall.copy(fontFamily = selectedFont),
+        displayLarge = TYPOGRAPHY.displayLarge.copy(fontFamily = flexFontEmphasis()),
+        displayMedium = TYPOGRAPHY.displayMedium.copy(fontFamily = flexFontEmphasis()),
+        displaySmall = TYPOGRAPHY.displaySmall.copy(fontFamily = flexFontEmphasis()),
+        headlineLarge = TYPOGRAPHY.headlineLarge.copy(fontFamily = flexFontBold()),
+        headlineMedium = TYPOGRAPHY.headlineMedium.copy(fontFamily = flexFontBold()),
+        headlineSmall = TYPOGRAPHY.headlineSmall.copy(fontFamily = flexFontBold()),
+        titleLarge = TYPOGRAPHY.titleLarge.copy(fontFamily = flexFontRounded()),
+        titleMedium = TYPOGRAPHY.titleMedium.copy(fontFamily = flexFontRounded()),
+        titleSmall = TYPOGRAPHY.titleSmall.copy(fontFamily = flexFontRounded()),
         bodyLarge = TYPOGRAPHY.bodyLarge.copy(fontFamily = selectedFont),
         bodyMedium = TYPOGRAPHY.bodyMedium.copy(fontFamily = selectedFont),
         bodySmall = TYPOGRAPHY.bodySmall.copy(fontFamily = selectedFont),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/presentation/ProvideTypography.kt` around
lines 80 - 96, The returned Typography currently applies selectedFont to every
style; instead, wire the existing flex preset functions (the flex presets
defined earlier that are used in the preview) into the display, headline and
title groups: for displayLarge/displayMedium/displaySmall,
headlineLarge/headlineMedium/headlineSmall, and
titleLarge/titleMedium/titleSmall, call the corresponding flex preset functions
instead of using selectedFont so those presets are applied; keep using
selectedFont.copy(...) for bodyLarge/bodyMedium/bodySmall and
labelLarge/labelMedium/labelSmall as before. Ensure this change is made in
provideTypography() where DISPLAY/HEADLINE/TITLE styles are constructed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt`:
- Around line 42-81: The client is configured with expectSuccess = true so
non-2xx responses thrown from HTTP calls inside LrcLibApi are being swallowed by
the runCatching in queryLyricsWithParams; update queryLyricsWithParams to log
the caught exception (include status and message where available) using the
project logger (or Android Log) before returning the empty result so
server/client errors are visible in production; specifically, in the catch block
for queryLyricsWithParams, record the Throwable (and if it's an
HttpResponseException or ResponseException extract response.status and
response.body/response.message) and emit a clear log entry referencing
LrcLibApi.queryLyricsWithParams.

In `@app/src/main/java/com/shub39/rush/presentation/ProvideTypography.kt`:
- Around line 80-96: The returned Typography currently applies selectedFont to
every style; instead, wire the existing flex preset functions (the flex presets
defined earlier that are used in the preview) into the display, headline and
title groups: for displayLarge/displayMedium/displaySmall,
headlineLarge/headlineMedium/headlineSmall, and
titleLarge/titleMedium/titleSmall, call the corresponding flex preset functions
instead of using selectedFont so those presets are applied; keep using
selectedFont.copy(...) for bodyLarge/bodyMedium/bodySmall and
labelLarge/labelMedium/labelSmall as before. Ensure this change is made in
provideTypography() where DISPLAY/HEADLINE/TITLE styles are constructed.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a18499 and ea2f04d.

📒 Files selected for processing (15)
  • app/src/main/java/com/shub39/rush/data/backup/ExportImpl.kt
  • app/src/main/java/com/shub39/rush/data/listener/MediaListenerImpl.kt
  • app/src/main/java/com/shub39/rush/data/network/LrcLibApi.kt
  • app/src/main/java/com/shub39/rush/domain/Util.kt
  • app/src/main/java/com/shub39/rush/presentation/EnumExt.kt
  • app/src/main/java/com/shub39/rush/presentation/ErrorStringRes.kt
  • app/src/main/java/com/shub39/rush/presentation/Glow.kt
  • app/src/main/java/com/shub39/rush/presentation/ListItemExt.kt
  • app/src/main/java/com/shub39/rush/presentation/ProvideTypography.kt
  • app/src/main/java/com/shub39/rush/presentation/Scrollbar.kt
  • app/src/main/java/com/shub39/rush/presentation/Util.kt
  • app/src/main/java/com/shub39/rush/presentation/lyrics/Util.kt
  • app/src/main/java/com/shub39/rush/presentation/share/Util.kt
  • app/src/main/java/com/shub39/rush/viewmodels/SavedVM.kt
  • app/src/main/java/com/shub39/rush/viewmodels/SearchSheetVM.kt
💤 Files with no reviewable changes (2)
  • app/src/main/java/com/shub39/rush/presentation/Util.kt
  • app/src/main/java/com/shub39/rush/data/backup/ExportImpl.kt
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/java/com/shub39/rush/viewmodels/SearchSheetVM.kt

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsCustomisationPage.kt (1)

131-138: Consider applying consistent styling to both portrait and landscape TopAppBar titles.

The non-wide layout now uses flexFontBold() and basicMarquee() for the title (lines 131-138), but the wide/landscape layout at line 237 still uses a plain Text. If the styling difference is intentional (e.g., landscape has sufficient width), this is fine. Otherwise, consider applying the same treatment for visual consistency.

♻️ Optional: Apply same styling to landscape TopAppBar
 TopAppBar(
-    title = { Text(stringResource(R.string.customisations)) },
+    title = {
+        Text(
+            text = stringResource(R.string.customisations),
+            fontFamily = flexFontBold(),
+            maxLines = 1,
+            modifier = Modifier.basicMarquee(),
+        )
+    },
     colors =

Also applies to: 236-237

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsCustomisationPage.kt`
around lines 131 - 138, The wide/landscape TopAppBar title uses a plain Text
while the portrait title uses flexFontBold() and Modifier.basicMarquee(),
causing inconsistent styling; update the Text used in the wide/landscape
TopAppBar (the title lambda in LyricsCustomisationPage.kt for the wide layout)
to use the same properties: set fontFamily = flexFontBold(), maxLines = 1, and
modifier = Modifier.basicMarquee() (keeping the same stringResource call) so
both layouts render the title consistently.
app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt (1)

106-116: Consider extracting shared top-bar title/subtitle content to avoid drift.

The same Text blocks are duplicated in portrait and landscape branches; hoisting them once improves maintainability.

♻️ Proposed refactor
+    val topBarTitle: `@Composable` () -> Unit = {
+        Text(
+            text = stringResource(R.string.rush_branding),
+            fontFamily = flexFontEmphasis(),
+        )
+    }
+    val topBarSubtitle: `@Composable` () -> Unit = {
+        Text(
+            text = "${state.songsAsc.size} " + stringResource(R.string.saved),
+            fontFamily = flexFontRounded(),
+        )
+    }
+
     Scaffold(
         modifier = modifier,
         topBar = {
             Column {
                 if (!isLandscape) {
                     LargeFlexibleTopAppBar(
-                        title = {
-                            Text(
-                                text = stringResource(R.string.rush_branding),
-                                fontFamily = flexFontEmphasis(),
-                            )
-                        },
-                        subtitle = {
-                            Text(
-                                text = "${state.songsAsc.size} " + stringResource(R.string.saved),
-                                fontFamily = flexFontRounded(),
-                            )
-                        },
+                        title = topBarTitle,
+                        subtitle = topBarSubtitle,
                         actions = {
                             IconButton(onClick = onNavigateToSettings) {
                                 Icon(
@@
                 } else {
                     TopAppBar(
-                        title = {
-                            Text(
-                                text = stringResource(R.string.rush_branding),
-                                fontFamily = flexFontEmphasis(),
-                            )
-                        },
-                        subtitle = {
-                            Text(
-                                text = "${state.songsAsc.size} " + stringResource(R.string.saved),
-                                fontFamily = flexFontRounded(),
-                            )
-                        },
+                        title = topBarTitle,
+                        subtitle = topBarSubtitle,
                         actions = {
                             IconButton(onClick = onNavigateToSettings) {
                                 Icon(

Also applies to: 134-144

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt` around
lines 106 - 116, Duplicate Text title/subtitle blocks in the SavedPage
composable should be hoisted into a single reusable component to avoid drift:
create a small `@Composable` (e.g., SavedTopBarTitle or SavedTopBarContent) that
accepts the count (state.songsAsc.size) or the entire state and renders the two
Text elements (using flexFontEmphasis() and flexFontRounded() and
stringResource(R.string.rush_branding)/stringResource(R.string.saved)); then
replace the duplicated title = { ... } and subtitle = { ... } lambdas in both
portrait and landscape branches with a call to that new composable so both
branches reuse the same UI.
app/src/main/java/com/shub39/rush/presentation/setting/section/LookAndFeelPage.kt (1)

107-107: Minor: Consider using consistent American spelling for the variable name.

The variable scrollBehaviour uses British spelling while the Material 3 API uses American spelling (scrollBehavior). For consistency with the framework naming, consider renaming to scrollBehavior.

✏️ Suggested rename
-    val scrollBehaviour = TopAppBarDefaults.enterAlwaysScrollBehavior()
+    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()

And update references at lines 125 and 129 accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/shub39/rush/presentation/setting/section/LookAndFeelPage.kt`
at line 107, Rename the local variable `scrollBehaviour` to use American
spelling `scrollBehavior` to match the Material3 API (the value assigned from
TopAppBarDefaults.enterAlwaysScrollBehavior()); update all references to this
variable (e.g., uses inside the LookAndFeelPage composable where
`scrollBehaviour` is passed or accessed) to `scrollBehavior` so names are
consistent with the framework.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsCustomisationPage.kt`:
- Around line 131-138: The wide/landscape TopAppBar title uses a plain Text
while the portrait title uses flexFontBold() and Modifier.basicMarquee(),
causing inconsistent styling; update the Text used in the wide/landscape
TopAppBar (the title lambda in LyricsCustomisationPage.kt for the wide layout)
to use the same properties: set fontFamily = flexFontBold(), maxLines = 1, and
modifier = Modifier.basicMarquee() (keeping the same stringResource call) so
both layouts render the title consistently.

In `@app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt`:
- Around line 106-116: Duplicate Text title/subtitle blocks in the SavedPage
composable should be hoisted into a single reusable component to avoid drift:
create a small `@Composable` (e.g., SavedTopBarTitle or SavedTopBarContent) that
accepts the count (state.songsAsc.size) or the entire state and renders the two
Text elements (using flexFontEmphasis() and flexFontRounded() and
stringResource(R.string.rush_branding)/stringResource(R.string.saved)); then
replace the duplicated title = { ... } and subtitle = { ... } lambdas in both
portrait and landscape branches with a call to that new composable so both
branches reuse the same UI.

In
`@app/src/main/java/com/shub39/rush/presentation/setting/section/LookAndFeelPage.kt`:
- Line 107: Rename the local variable `scrollBehaviour` to use American spelling
`scrollBehavior` to match the Material3 API (the value assigned from
TopAppBarDefaults.enterAlwaysScrollBehavior()); update all references to this
variable (e.g., uses inside the LookAndFeelPage composable where
`scrollBehaviour` is passed or accessed) to `scrollBehavior` so names are
consistent with the framework.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea2f04d and 9ef2d86.

📒 Files selected for processing (22)
  • CHANGELOG.md
  • app/src/main/java/com/shub39/rush/presentation/Util.kt
  • app/src/main/java/com/shub39/rush/presentation/components/RushBranding.kt
  • app/src/main/java/com/shub39/rush/presentation/components/RushTheme.kt
  • app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsCustomisationPage.kt
  • app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsPage.kt
  • app/src/main/java/com/shub39/rush/presentation/onboarding/Onboarding.kt
  • app/src/main/java/com/shub39/rush/presentation/saved/SavedPage.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/SettingsGraph.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/component/AboutApp.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/section/BackupPage.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/section/LookAndFeelPage.kt
  • app/src/main/java/com/shub39/rush/presentation/setting/section/SettingRootPage.kt
  • app/src/main/java/com/shub39/rush/presentation/share/component/cards/AlbumArt.kt
  • app/src/main/java/com/shub39/rush/presentation/share/component/cards/ChatCard.kt
  • app/src/main/java/com/shub39/rush/presentation/share/component/cards/CoupletShareCard.kt
  • app/src/main/java/com/shub39/rush/presentation/share/component/cards/MessyCard.kt
  • app/src/main/java/com/shub39/rush/presentation/share/component/cards/QuoteShareCard.kt
  • app/src/main/java/com/shub39/rush/presentation/share/component/cards/RushedShareCard.kt
  • app/src/main/java/com/shub39/rush/presentation/share/component/cards/SpotifyShareCard.kt
  • app/src/main/java/com/shub39/rush/presentation/share/component/cards/VerticalShareCard.kt
  • app/src/main/res/values-frc/strings.xml
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/java/com/shub39/rush/presentation/setting/SettingsGraph.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/src/main/java/com/shub39/rush/presentation/setting/section/SettingRootPage.kt
  • app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsPage.kt
  • app/src/main/res/values-frc/strings.xml
  • CHANGELOG.md

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsCustomisationPage.kt (1)

131-138: Consider extracting a shared title composable to prevent layout drift.

The narrow layout now uses custom font + marquee, while the wide layout still has a plain title (Line 237). A shared composable keeps style consistent and reduces duplication.

Refactor sketch
+@Composable
+private fun CustomisationsTopBarTitle(useMarquee: Boolean) {
+    Text(
+        text = stringResource(R.string.customisations),
+        fontFamily = flexFontEmphasis(),
+        maxLines = 1,
+        modifier = if (useMarquee) Modifier.basicMarquee() else Modifier,
+    )
+}
...
-                    title = {
-                        Text(
-                            text = stringResource(R.string.customisations),
-                            fontFamily = flexFontEmphasis(),
-                            maxLines = 1,
-                            modifier = Modifier.basicMarquee(),
-                        )
-                    },
+                    title = { CustomisationsTopBarTitle(useMarquee = true) },
...
-                    title = { Text(stringResource(R.string.customisations)) },
+                    title = { CustomisationsTopBarTitle(useMarquee = false) },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsCustomisationPage.kt`
around lines 131 - 138, Extract the repeated title Text into a shared composable
(e.g., LyricsPageTitle or CustomisationTitle) that wraps Text(... text =
stringResource(R.string.customisations) ...) and applies flexFontEmphasis(),
maxLines = 1 and Modifier.basicMarquee() (accepting an optional Modifier
parameter), then replace the inline title lambdas in LyricsCustomisationPage
(the narrow layout usage shown) and the wide-layout title (the other occurrence)
with calls to this new composable so both layouts share identical styling and
avoid layout drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsCustomisationPage.kt`:
- Around line 131-138: Extract the repeated title Text into a shared composable
(e.g., LyricsPageTitle or CustomisationTitle) that wraps Text(... text =
stringResource(R.string.customisations) ...) and applies flexFontEmphasis(),
maxLines = 1 and Modifier.basicMarquee() (accepting an optional Modifier
parameter), then replace the inline title lambdas in LyricsCustomisationPage
(the narrow layout usage shown) and the wide-layout title (the other occurrence)
with calls to this new composable so both layouts share identical styling and
avoid layout drift.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ef2d86 and 84ec19c.

⛔ Files ignored due to path filters (6)
  • fastlane/metadata/android/en-US/images/phoneScreenshots/1.png is excluded by !**/*.png
  • fastlane/metadata/android/en-US/images/phoneScreenshots/2.png is excluded by !**/*.png
  • fastlane/metadata/android/en-US/images/phoneScreenshots/3.png is excluded by !**/*.png
  • fastlane/metadata/android/en-US/images/phoneScreenshots/4.png is excluded by !**/*.png
  • fastlane/metadata/android/en-US/images/phoneScreenshots/5.png is excluded by !**/*.png
  • fastlane/metadata/android/en-US/images/phoneScreenshots/6.png is excluded by !**/*.png
📒 Files selected for processing (2)
  • app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsCustomisationPage.kt
  • app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsPage.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/shub39/rush/presentation/lyrics/section/LyricsPage.kt

@shub39 shub39 merged commit 7ee0396 into master Feb 27, 2026
3 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Mar 18, 2026
@coderabbitai coderabbitai bot mentioned this pull request Mar 26, 2026
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.

1 participant