feat: add download count and license info to repository details#426
feat: add download count and license info to repository details#426rainxchzed merged 4 commits intomainfrom
Conversation
- Update `RepoStats`, `GithubRepoSummary`, and `GithubAsset` models to include download counts and license information. - Enhance the repository details screen to display total downloads and license types. - Implement a `formatCount` utility to display large numbers using "K" and "M" suffixes. - Update `DetailsRepositoryImpl` to prioritize fetching repository data from the backend API, falling back to the GitHub API if necessary. - Add `getRepo` endpoint to `BackendApiClient` for fetching extended repository metadata. - Provide localized strings for "Downloads," "License," and "None" across multiple languages (Arabic, Bengali, Chinese, English, French, Hindi, Italian, Japanese, Korean, Polish, Russian, Spanish, Turkish). - Update data mappers and DTOs to handle the new `download_count` and license fields.
- Replace plain text statistics with a `FlowRow` containing custom `InfoChip` components for stars, forks, and language. - Add support for displaying download counts when available. - Implement `InfoChip` internal component for consistent metadata styling using `surfaceContainerHigh`. - Add `formatCount` utility to provide human-readable abbreviations (e.g., 1.2K, 3.5M) for repository statistics. - Update `GithubRepoSummaryUi` and associated mappers to include `downloadCount`. - Update `RepoInfoNetwork` DTO to include license information.
WalkthroughThis PR adds download-count and license data end-to-end: new backend call and DTO fields, propagated through mappers and caches into domain/UI models, UI components updated to show downloads and license, DI wired for backend-first fallbacks, and localization strings added for 13 locales. Changes
Sequence DiagramsequenceDiagram
participant Client as Details Repository
participant Backend as Backend API
participant Cache as Cache Manager
participant GitHub as GitHub REST API
Client->>Backend: getRepo(owner, name)
alt Backend Success
Backend-->>Client: BackendRepoResponse (includes downloadCount, stars, forks)
Client->>Cache: save(REPO_DETAILS, response)
Cache-->>Client: ack
Client-->>Client: map -> GithubRepoSummary / RepoStats (with downloadCount, license)
else Backend Failure
Backend-->>Client: error
Client->>GitHub: fetch repo info/stats
GitHub-->>Client: RepoInfo (may include license)
Client->>Cache: save(REPO_STATS, mappedStats)
Cache-->>Client: ack
Client-->>Client: return RepoStats (license from GitHub, downloads = 0)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt (1)
76-84: Consider URL-encodingowner/name.
ownerandnameare interpolated directly into the path. GitHub names are typically safe, but if any upstream caller ever passes a value containing/,?,#, or whitespace, the request will be malformed. A minor hardening with Ktor's URL builder or an explicit encode would future-proof this endpoint — the existinggetCategory/getTopichave the same property, so no need to block on this PR.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt` around lines 76 - 84, The getRepo function currently interpolates owner and name directly into the request path (see getRepo and similar getCategory/getTopic methods) which can break if values contain /, ?, # or whitespace; fix by building the request path with Ktor's URL builder or by encoding these path segments before calling httpClient.get (e.g., use httpClient.get { url { encodedPath += listOf("repo", encodedOwner, encodedName).joinToString("/") } } or equivalent), ensuring owner and name are passed as separate encoded path segments rather than raw string interpolation.feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt (1)
77-113: Consider extracting the repeated liquefiable modifier.The
Modifier.then(if (isLiquidGlassEnabled) Modifier.liquefiable(liquidState) else Modifier)block is now repeated five times within this composable. A small local helper (e.g.,Modifier.maybeLiquefy(liquidState, isLiquidGlassEnabled)) would make the stats layout easier to scan and maintain.♻️ Suggested helper
private fun Modifier.maybeLiquefy( state: LiquidState, enabled: Boolean, ): Modifier = if (enabled) this.liquefiable(state) else thisThen each stat becomes:
modifier = Modifier.weight(1f).maybeLiquefy(liquidState, isLiquidGlassEnabled),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt` around lines 77 - 113, Extract the repeated Modifier logic into a small extension helper (e.g., Modifier.maybeLiquefy) that takes the LiquidState and the isLiquidGlassEnabled flag and returns this.liquefiable(state) when enabled or this otherwise; then replace the repeated .then(if (isLiquidGlassEnabled) Modifier.liquefiable(liquidState) else Modifier) occurrences in the Stats composable (used on StatItem and TextStatItem modifiers) with Modifier.weight(1f).maybeLiquefy(liquidState, isLiquidGlassEnabled) to simplify and centralize the behavior.core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt (1)
25-28: Import order / grouping nit.
Icons.filled.Staris sandwiched betweenIcons.outlined.Code/DownloadandIcons.outlined.StarOutline. If import ordering is enforced by your formatter (ktlint/detekt), this will fail CI. Otherwise, fine to ignore.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt` around lines 25 - 28, Reorder the icon imports so the filled Star import is grouped consistently with the outlined imports to satisfy formatter rules: ensure androidx.compose.material.icons.filled.Star appears adjacent to other filled imports (or move it to its own filled group) and place androidx.compose.material.icons.outlined.Code, Download, and StarOutline together; update the import block containing Icons.filled.Star and Icons.outlined.Code/Download/StarOutline to follow the project's import grouping/order convention.feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt (2)
52-52: Redundant wrapper — inlinetoSummary().
toBackendSummary()is a single-line delegate totoSummary()with no added behavior. Just calltoSummary()directly at the call site (line 119) and drop this private extension.♻️ Suggested diff
- private fun BackendRepoResponse.toBackendSummary(): GithubRepoSummary = toSummary() - private fun RepoByIdNetwork.toGithubRepoSummary(): GithubRepoSummary =- val result = backendRepo.toBackendSummary() + val result = backendRepo.toSummary()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt` at line 52, Remove the redundant private extension function toBackendSummary() which merely delegates to toSummary(); delete its definition (private fun BackendRepoResponse.toBackendSummary()) and update any callers to invoke toSummary() directly (replace calls to toBackendSummary() with toSummary()) — no behavioral changes needed beyond removing the wrapper.
116-122: Backend failures are silently swallowed — consider logging the error.
backendApiClient.getRepo(owner, name).getOrNull()discards exceptions; transient backend outages will always fall through to GitHub without any signal in logs. Using.onFailure { logger.error(...) }(orfold) beforegetOrNull()would make backend issues diagnosable without changing the fallback behavior. Same pattern applies ingetRepoStatsat line 328.♻️ Suggested diff
- backendApiClient.getRepo(owner, name).getOrNull()?.let { backendRepo -> + backendApiClient.getRepo(owner, name) + .onFailure { logger.debug("Backend error for $owner/$name: ${it.message}") } + .getOrNull()?.let { backendRepo -> logger.debug("Backend hit for repo $owner/$name") - val result = backendRepo.toBackendSummary() + val result = backendRepo.toSummary() cacheManager.put(cacheKey, result, REPO_DETAILS) return result }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt` around lines 116 - 122, The backendApiClient.getRepo(...) call in DetailsRepositoryImpl currently uses getOrNull() which swallows exceptions; change the call to capture failures and log them before falling back to GitHub (e.g., call backendApiClient.getRepo(owner, name).onFailure { logger.error("Backend getRepo failed for $owner/$name", it) } or use fold to log the throwable) so errors are visible while preserving the existing fallback behavior; apply the same pattern in the getRepoStats path (where backendApiClient.getRepoStats is used) to ensure transient backend outages are logged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubRepoSummary.kt`:
- Line 22: The downloadCount field on GithubRepoSummary (downloadCount) remains
0 when mapping from the GitHub API because GithubRepoNetworkModel.toSummary()
doesn't populate it, causing RepositoryCard's `downloadCount > 0` guard to hide
downloads on the backend→GitHub fallback; update the mapping in
GithubRepoNetworkModel.toSummary() (or in DetailsRepositoryImpl's fallback
logic) to populate GithubRepoSummary.downloadCount—either by summing release
asset download counts if release data is available or by explicitly marking it
as unknown/nullable so RepositoryCard can render accordingly; reference
GithubRepoSummary.downloadCount, GithubRepoNetworkModel.toSummary(),
DetailsRepositoryImpl, and RepositoryCard when making the change.
In
`@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt`:
- Around line 550-555: formatCount uses JVM-only String.format causing non-JVM
compile failures; replace it with a locale-independent manual formatter inside
formatCount: compute the scaled value (count / 1_000_000.0 or / 1_000.0), round
to one decimal (e.g. round to nearest tenth using kotlin.math), convert to
string with Double.toString(), strip a trailing ".0" so "1.0" becomes "1", then
append the "M" or "K" suffix; keep the else branch returning count.toString().
Ensure no use of String.format or platform-specific APIs.
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt`:
- Around line 327-339: The backend branch in DetailsRepositoryImpl currently
hardcodes openIssues = 0 and license = null; update it to populate those fields
from the backend response when available (e.g., use backendRepo.openIssuesCount
and backendRepo.license/spdxId if present) and only fall back to the GitHub repo
call when those fields are missing — implement: inside the
backendApiClient.getRepo(owner, repo).getOrNull()?.let { backendRepo -> ... }
block, derive openIssues and license from backendRepo, and if either is
null/unknown call githubApiClient.getRepo(owner, repo) (or a lightweight
stats-only endpoint) to fill missing values before constructing RepoStats and
calling cacheManager.put(cacheKey, result, REPO_STATS).
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt`:
- Around line 93-98: The formatCount function uses JVM-only String.format and
must be replaced with a locale-independent implementation: in formatCount(count:
Long) compute the scaled Double (count / 1_000_000.0 or /1_000.0), round to one
decimal (e.g. val rounded = kotlin.math.round(value * 10) / 10), convert to
string via Double.toString(), strip a trailing ".0" if present, and append "M"
or "K" as appropriate so the output is stable across platforms; update the
formatCount function accordingly.
---
Nitpick comments:
In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt`:
- Around line 76-84: The getRepo function currently interpolates owner and name
directly into the request path (see getRepo and similar getCategory/getTopic
methods) which can break if values contain /, ?, # or whitespace; fix by
building the request path with Ktor's URL builder or by encoding these path
segments before calling httpClient.get (e.g., use httpClient.get { url {
encodedPath += listOf("repo", encodedOwner, encodedName).joinToString("/") } }
or equivalent), ensuring owner and name are passed as separate encoded path
segments rather than raw string interpolation.
In
`@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt`:
- Around line 25-28: Reorder the icon imports so the filled Star import is
grouped consistently with the outlined imports to satisfy formatter rules:
ensure androidx.compose.material.icons.filled.Star appears adjacent to other
filled imports (or move it to its own filled group) and place
androidx.compose.material.icons.outlined.Code, Download, and StarOutline
together; update the import block containing Icons.filled.Star and
Icons.outlined.Code/Download/StarOutline to follow the project's import
grouping/order convention.
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt`:
- Line 52: Remove the redundant private extension function toBackendSummary()
which merely delegates to toSummary(); delete its definition (private fun
BackendRepoResponse.toBackendSummary()) and update any callers to invoke
toSummary() directly (replace calls to toBackendSummary() with toSummary()) — no
behavioral changes needed beyond removing the wrapper.
- Around line 116-122: The backendApiClient.getRepo(...) call in
DetailsRepositoryImpl currently uses getOrNull() which swallows exceptions;
change the call to capture failures and log them before falling back to GitHub
(e.g., call backendApiClient.getRepo(owner, name).onFailure {
logger.error("Backend getRepo failed for $owner/$name", it) } or use fold to log
the throwable) so errors are visible while preserving the existing fallback
behavior; apply the same pattern in the getRepoStats path (where
backendApiClient.getRepoStats is used) to ensure transient backend outages are
logged.
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt`:
- Around line 77-113: Extract the repeated Modifier logic into a small extension
helper (e.g., Modifier.maybeLiquefy) that takes the LiquidState and the
isLiquidGlassEnabled flag and returns this.liquefiable(state) when enabled or
this otherwise; then replace the repeated .then(if (isLiquidGlassEnabled)
Modifier.liquefiable(liquidState) else Modifier) occurrences in the Stats
composable (used on StatItem and TextStatItem modifiers) with
Modifier.weight(1f).maybeLiquefy(liquidState, isLiquidGlassEnabled) to simplify
and centralize the behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 05ecefb7-53fe-4520-b6b8-56e407262074
📒 Files selected for processing (33)
.claude/settings.local.jsoncore/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/AssetNetwork.ktcore/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/BackendRepoResponse.ktcore/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/RepoInfoNetwork.ktcore/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/AssetNetwork.ktcore/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/BackendRepoMapper.ktcore/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubAsset.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubRepoSummary.ktcore/presentation/src/commonMain/composeResources/values-ar/strings-ar.xmlcore/presentation/src/commonMain/composeResources/values-bn/strings-bn.xmlcore/presentation/src/commonMain/composeResources/values-es/strings-es.xmlcore/presentation/src/commonMain/composeResources/values-fr/strings-fr.xmlcore/presentation/src/commonMain/composeResources/values-hi/strings-hi.xmlcore/presentation/src/commonMain/composeResources/values-it/strings-it.xmlcore/presentation/src/commonMain/composeResources/values-ja/strings-ja.xmlcore/presentation/src/commonMain/composeResources/values-ko/strings-ko.xmlcore/presentation/src/commonMain/composeResources/values-pl/strings-pl.xmlcore/presentation/src/commonMain/composeResources/values-ru/strings-ru.xmlcore/presentation/src/commonMain/composeResources/values-tr/strings-tr.xmlcore/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xmlcore/presentation/src/commonMain/composeResources/values/strings.xmlcore/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.ktcore/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/GithubRepoSummaryUi.ktcore/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/GithubRepoSummaryMappers.ktfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.ktfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.ktfeature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/RepoStats.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.ktfeature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/dto/CachedGithubRepoSummary.ktfeature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/BackendToCachedMapper.ktfeature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/CachedGithubRepoSummaryMappers.kt
| val updatedAt: String, | ||
| val isFork: Boolean = false, | ||
| val availablePlatforms: List<DiscoveryPlatform> = emptyList(), | ||
| val downloadCount: Long = 0, |
There was a problem hiding this comment.
Minor: downloadCount silently stays 0 on the GitHub-API fallback path.
Per the relevant snippet from core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/GithubRepoMapper.kt, GithubRepoNetworkModel.toSummary() does not set downloadCount, so when DetailsRepositoryImpl falls back from the backend to the GitHub API, the downloads chip will be hidden (due to the downloadCount > 0 guard in RepositoryCard) rather than showing real data. This is likely intentional since GitHub's repo endpoint doesn't expose aggregate downloads — just worth confirming the product expectation for the fallback path (silent omission vs. summing asset download counts when releases are already loaded).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubRepoSummary.kt`
at line 22, The downloadCount field on GithubRepoSummary (downloadCount) remains
0 when mapping from the GitHub API because GithubRepoNetworkModel.toSummary()
doesn't populate it, causing RepositoryCard's `downloadCount > 0` guard to hide
downloads on the backend→GitHub fallback; update the mapping in
GithubRepoNetworkModel.toSummary() (or in DetailsRepositoryImpl's fallback
logic) to populate GithubRepoSummary.downloadCount—either by summing release
asset download counts if release data is available or by explicitly marking it
as unknown/nullable so RepositoryCard can render accordingly; reference
GithubRepoSummary.downloadCount, GithubRepoNetworkModel.toSummary(),
DetailsRepositoryImpl, and RepositoryCard when making the change.
| private fun formatCount(count: Long): String = | ||
| when { | ||
| count >= 1_000_000 -> String.format("%.1fM", count / 1_000_000.0) | ||
| count >= 1_000 -> String.format("%.1fK", count / 1_000.0) | ||
| else -> count.toString() | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Is String.format available in Kotlin commonMain source set, and which overloads are locale-independent in the Kotlin stdlib?
💡 Result:
No, String.format is not available in the Kotlin commonMain source set. It is a JVM-specific extension function in the Kotlin standard library, implemented by forwarding to Java's String.format method. This is confirmed by official Kotlin documentation marking it as JVM-only, Stack Overflow reports of unresolved references in KMP commonMain (especially with wasmJS), and the absence of format functions in the common stdlib _Strings.kt source files. Instead, it appears only in JVM-specific files like StringsJVM.kt. There are no overloads of String.format in commonMain because the function itself is unavailable there. For multiplatform code, alternatives include string templates, platform-specific expect/actual implementations, or third-party libraries like mp_stools that provide sprintf-like functionality across platforms.
Citations:
- 1: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/format.html
- 2: https://kotlinlang.org/docs/strings.html
- 3: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.text/-string.html
- 4: https://stackoverflow.com/questions/79285602/kmp-unresolved-reference-string-format-with-wasmjs
- 5: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.text/format.html
- 6: https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/common/src/generated/_Strings.kt
- 7: https://github.com/JetBrains/kotlin/blob/037b3697ed635a52c283da7b2bf6ecd0961ce8f4/libraries/stdlib/common/src/generated/_Strings.kt
- 8: https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/stdlib/common/src/generated/_Strings.kt
- 9: https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt
String.format is JVM-only and unavailable in commonMain; code will not compile for non-JVM targets.
This function is in commonMain, but String.format() is a JVM-only extension function (exists only in StringsJVM.kt, not in the common stdlib). Using it here breaks the multiplatform architecture and will cause compilation failures when targeting JavaScript, Native, or WASM. Additionally, the locale-sensitive output (producing "1,5K" in German/French locales) and trailing ".0" formatting are secondary style issues.
🛠 Proposed fix
Replace String.format with locale-independent string building:
-private fun formatCount(count: Long): String =
- when {
- count >= 1_000_000 -> String.format("%.1fM", count / 1_000_000.0)
- count >= 1_000 -> String.format("%.1fK", count / 1_000.0)
- else -> count.toString()
- }
+private fun formatCount(count: Long): String {
+ fun trimmed(value: Double, suffix: String): String {
+ val tenths = (value * 10).toLong()
+ val whole = tenths / 10
+ val frac = tenths % 10
+ return if (frac == 0L) "$whole$suffix" else "$whole.$frac$suffix"
+ }
+ return when {
+ count >= 1_000_000 -> trimmed(count / 1_000_000.0, "M")
+ count >= 1_000 -> trimmed(count / 1_000.0, "K")
+ else -> count.toString()
+ }
+}This avoids JVM-only APIs and removes locale dependency.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private fun formatCount(count: Long): String = | |
| when { | |
| count >= 1_000_000 -> String.format("%.1fM", count / 1_000_000.0) | |
| count >= 1_000 -> String.format("%.1fK", count / 1_000.0) | |
| else -> count.toString() | |
| } | |
| private fun formatCount(count: Long): String { | |
| fun trimmed(value: Double, suffix: String): String { | |
| val tenths = (value * 10).toLong() | |
| val whole = tenths / 10 | |
| val frac = tenths % 10 | |
| return if (frac == 0L) "$whole$suffix" else "$whole.$frac$suffix" | |
| } | |
| return when { | |
| count >= 1_000_000 -> trimmed(count / 1_000_000.0, "M") | |
| count >= 1_000 -> trimmed(count / 1_000.0, "K") | |
| else -> count.toString() | |
| } | |
| } |
🧰 Tools
🪛 detekt (1.23.8)
[warning] 552-552: String.format("%.1fM", count / 1_000_000.0) uses implicitly default locale for string formatting.
(detekt.potential-bugs.ImplicitDefaultLocale)
[warning] 553-553: String.format("%.1fK", count / 1_000.0) uses implicitly default locale for string formatting.
(detekt.potential-bugs.ImplicitDefaultLocale)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt`
around lines 550 - 555, formatCount uses JVM-only String.format causing non-JVM
compile failures; replace it with a locale-independent manual formatter inside
formatCount: compute the scaled value (count / 1_000_000.0 or / 1_000.0), round
to one decimal (e.g. round to nearest tenth using kotlin.math), convert to
string with Double.toString(), strip a trailing ".0" so "1.0" becomes "1", then
append the "M" or "K" suffix; keep the else branch returning count.toString().
Ensure no use of String.format or platform-specific APIs.
| // Try backend first — avoids GitHub API for Chinese users | ||
| backendApiClient.getRepo(owner, repo).getOrNull()?.let { backendRepo -> | ||
| logger.debug("Backend hit for repo stats $owner/$repo") | ||
| val result = RepoStats( | ||
| stars = backendRepo.stargazersCount, | ||
| forks = backendRepo.forksCount, | ||
| openIssues = 0, | ||
| license = null, | ||
| totalDownloads = backendRepo.downloadCount, | ||
| ) | ||
| cacheManager.put(cacheKey, result, REPO_STATS) | ||
| return result | ||
| } |
There was a problem hiding this comment.
Backend path drops openIssues and license, degrading the Stats UI.
Since the backend is tried first, this path is the dominant one in production. Hardcoding openIssues = 0 and license = null means the Stats section will almost always show "0 issues" and license "None" (via Res.string.license_none), even when GitHub has real values. The GitHub fallback (lines 352–359) does populate both — so users get inconsistent UI depending on whether the backend succeeds or fails, and the more common path is the degraded one.
Options:
- Add
openIssuesCountandlicense(orspdxId) toBackendRepoResponseand populate them here. - Or, when backend succeeds but these fields are unknown, fall through to a supplementary GitHub call (e.g., only for stats), at least for
license, which is user-visible. - Or, don't show those two stat tiles when data is unknown (treat absence as "hide" rather than "0"/"None").
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt`
around lines 327 - 339, The backend branch in DetailsRepositoryImpl currently
hardcodes openIssues = 0 and license = null; update it to populate those fields
from the backend response when available (e.g., use backendRepo.openIssuesCount
and backendRepo.license/spdxId if present) and only fall back to the GitHub repo
call when those fields are missing — implement: inside the
backendApiClient.getRepo(owner, repo).getOrNull()?.let { backendRepo -> ... }
block, derive openIssues and license from backendRepo, and if either is
null/unknown call githubApiClient.getRepo(owner, repo) (or a lightweight
stats-only endpoint) to fill missing values before constructing RepoStats and
calling cacheManager.put(cacheKey, result, REPO_STATS).
| private fun formatCount(count: Long): String = | ||
| when { | ||
| count >= 1_000_000 -> String.format("%.1fM", count / 1_000_000.0) | ||
| count >= 1_000 -> String.format("%.1fK", count / 1_000.0) | ||
| else -> count.toString() | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Is kotlin.String.format(format: String, vararg args: Any?) available in Kotlin commonMain source set in Kotlin 2.0+, or is it JVM-only?
💡 Result:
No, kotlin.String.format(format: String, vararg args: Any?) is not available in the Kotlin commonMain source set in Kotlin 2.0+. It is JVM-only.
Citations:
- 1: https://stackoverflow.com/questions/79285602/kmp-unresolved-reference-string-format-with-wasmjs
- 2: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.text/format.html
- 3: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/format.html
- 4: https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/stdlib/common/src/generated/_Strings.kt
- 5: https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/stdlib/jvm/src/generated/_StringsJvm.kt
- 6: https://youtrack.jetbrains.com/issue/KT-25506/Stdlib-String.format-in-common
- 7: https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt
🏁 Script executed:
# Verify the file exists and check the code at the specified lines
if [ -f "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt" ]; then
echo "=== File found, checking lines 93-98 ==="
sed -n '90,100p' "feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt"
else
echo "File not found at expected path. Searching for StatItem.kt..."
find . -name "StatItem.kt" -type f 2>/dev/null | head -5
fiRepository: OpenHub-Store/GitHub-Store
Length of output: 356
Replace String.format() with a locale-independent formatter in this commonMain file.
String.format(String, vararg Any?) is a JVM-only API and will not compile for non-JVM targets (wasmJS, iOS, etc.). Additionally, it uses Locale.getDefault() on the JVM, so %.1f will render 1.5M as 1,5M in locales like de/fr/ru/pl—repository stats should render consistently.
♻️ Suggested fix
-private fun formatCount(count: Long): String =
- when {
- count >= 1_000_000 -> String.format("%.1fM", count / 1_000_000.0)
- count >= 1_000 -> String.format("%.1fK", count / 1_000.0)
- else -> count.toString()
- }
+private fun formatCount(count: Long): String {
+ fun compact(value: Double, suffix: String): String {
+ val rounded = (value * 10).toLong() // tenths, truncated
+ val whole = rounded / 10
+ val tenth = rounded % 10
+ return if (tenth == 0L) "$whole$suffix" else "$whole.$tenth$suffix"
+ }
+ return when {
+ count >= 1_000_000 -> compact(count / 1_000_000.0, "M")
+ count >= 1_000 -> compact(count / 1_000.0, "K")
+ else -> count.toString()
+ }
+}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt`
around lines 93 - 98, The formatCount function uses JVM-only String.format and
must be replaced with a locale-independent implementation: in formatCount(count:
Long) compute the scaled Double (count / 1_000_000.0 or /1_000.0), round to one
decimal (e.g. val rounded = kotlin.math.round(value * 10) / 10), convert to
string via Double.toString(), strip a trailing ".0" if present, and append "M"
or "K" as appropriate so the output is stable across platforms; update the
formatCount function accordingly.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt (1)
550-555:⚠️ Potential issue | 🔴 Critical
String.formatis JVM-only — breaks commonMain builds on non-JVM targets.This file is in
commonMain, butString.format(...)is a JVM-only extension (defined inStringsJVM.kt, not in the common stdlib). It will fail to compile for JS/Native/WASM targets. Detekt also flags the implicit default locale, which would render e.g."1,5K"in de/fr locales.🛠 Suggested fix (locale-independent, no JVM APIs)
-private fun formatCount(count: Long): String = - when { - count >= 1_000_000 -> String.format("%.1fM", count / 1_000_000.0) - count >= 1_000 -> String.format("%.1fK", count / 1_000.0) - else -> count.toString() - } +private fun formatCount(count: Long): String { + fun scaled(value: Long, divisor: Long, suffix: String): String { + val tenths = (value * 10 + divisor / 2) / divisor + val whole = tenths / 10 + val frac = tenths % 10 + return if (frac == 0L) "$whole$suffix" else "$whole.$frac$suffix" + } + return when { + count >= 1_000_000 -> scaled(count, 1_000_000, "M") + count >= 1_000 -> scaled(count, 1_000, "K") + else -> count.toString() + } +}As per coding guidelines: "Shared code must be in
commonMainsource set and use expect/actual for platform-specific APIs".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt` around lines 550 - 555, Replace the JVM-only String.format usage in formatCount with a locale-independent implementation: in the function formatCount(count: Long) compute the scaled double (count / 1_000_000.0 or / 1_000.0), round to one decimal place (e.g. val rounded = (scaled * 10).roundToLong() / 10.0 using kotlin.math.roundToLong), then convert to string via Double.toString() and append the "M" or "K" suffix; leave the else branch as count.toString(). This keeps the code in commonMain, avoids JVM-only APIs and locale-dependent formatting while preserving one-decimal output.
🧹 Nitpick comments (1)
core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt (1)
550-555: DuplicatedformatCounthelper.Per the PR summary, a
formatCountutility was also introduced (e.g., used byfeature/details/.../StatItem.kt). Consider extracting a single implementation intocore/presentation/.../utils/and reusing it here instead of keeping a private copy, to avoid drift (the two copies can diverge in rounding/locale handling).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt` around lines 550 - 555, The private formatCount in RepositoryCard.kt duplicates the helper used elsewhere (e.g., StatItem.kt); remove this private function and call the shared utility instead: create/verify a single formatCount utility in core/presentation/.../utils (or the existing utils file), ensure its signature matches (accepts Long and returns String) and import it into RepositoryCard.kt, then replace local calls to the private formatCount with the shared formatCount to avoid duplication and drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In
`@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt`:
- Around line 550-555: Replace the JVM-only String.format usage in formatCount
with a locale-independent implementation: in the function formatCount(count:
Long) compute the scaled double (count / 1_000_000.0 or / 1_000.0), round to one
decimal place (e.g. val rounded = (scaled * 10).roundToLong() / 10.0 using
kotlin.math.roundToLong), then convert to string via Double.toString() and
append the "M" or "K" suffix; leave the else branch as count.toString(). This
keeps the code in commonMain, avoids JVM-only APIs and locale-dependent
formatting while preserving one-decimal output.
---
Nitpick comments:
In
`@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt`:
- Around line 550-555: The private formatCount in RepositoryCard.kt duplicates
the helper used elsewhere (e.g., StatItem.kt); remove this private function and
call the shared utility instead: create/verify a single formatCount utility in
core/presentation/.../utils (or the existing utils file), ensure its signature
matches (accepts Long and returns String) and import it into RepositoryCard.kt,
then replace local calls to the private formatCount with the shared formatCount
to avoid duplication and drift.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d8897105-3bc1-43fc-abd6-1be44a433846
📒 Files selected for processing (1)
core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt
RepoStats,GithubRepoSummary, andGithubAssetmodels to include download counts and license information.formatCountutility to display large numbers using "K" and "M" suffixes.DetailsRepositoryImplto prioritize fetching repository data from the backend API, falling back to the GitHub API if necessary.getRepoendpoint toBackendApiClientfor fetching extended repository metadata.download_countand license fields.Summary by CodeRabbit
New Features
Localization