Skip to content

feat: add download count and license info to repository details#426

Merged
rainxchzed merged 4 commits intomainfrom
details-download-lisence-feat
Apr 17, 2026
Merged

feat: add download count and license info to repository details#426
rainxchzed merged 4 commits intomainfrom
details-download-lisence-feat

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented Apr 17, 2026

  • 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.

Summary by CodeRabbit

  • New Features

    • Repository download counts now shown in repo cards and details (hidden when zero).
    • License information displayed in repo stats with a sensible "none" fallback.
    • Stats layout refreshed to use compact K/M number formatting and chip-style badges for cleaner wrapping and spacing.
  • Localization

    • Added translations for downloads and license labels in AR, BN, ES, FR, HI, IT, JA, KO, PL, RU, TR, and ZH (Simplified).

- 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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 17, 2026

Walkthrough

This 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

Cohort / File(s) Summary
Build Configuration
\.claude/settings.local.json
Replaced many task-specific ./gradlew allow-list entries with a single Bash(./gradlew *) rule.
Backend Network Layer
core/data/src/commonMain/kotlin/.../BackendApiClient.kt
Added suspend fun getRepo(owner: String, name: String): Result<BackendRepoResponse> with HTTP GET and error handling wrapped in safeCall.
Data DTOs
core/data/src/commonMain/kotlin/.../AssetNetwork.kt, BackendRepoResponse.kt, RepoInfoNetwork.kt
Added downloadCount: Long = 0 to AssetNetwork and BackendRepoResponse; added LicenseNetwork and license: LicenseNetwork? = null to RepoInfoNetwork.
Data Mappers
core/data/src/commonMain/kotlin/.../mappers/AssetNetwork.kt, BackendRepoMapper.kt
Propagated downloadCount through mapping into domain models.
Domain Models
core/domain/src/commonMain/kotlin/.../GithubAsset.kt, GithubRepoSummary.kt
Added downloadCount: Long = 0 to domain models.
Home Cache & Mappers
feature/home/data/src/commonMain/kotlin/.../CachedGithubRepoSummary.kt, .../BackendToCachedMapper.kt, .../CachedGithubRepoSummaryMappers.kt
Added downloadCount to cached DTO and mapped it through cached↔domain conversions.
Details Repository & DI
feature/details/data/src/commonMain/kotlin/.../SharedModule.kt, .../DetailsRepositoryImpl.kt
Wired BackendApiClient into DetailsRepositoryImpl; implemented backend-first fetches for repo/details and stats, caching on success and falling back to GitHub API on failure; maps downloadCount and license fields.
Details Domain & UI Models
feature/details/domain/.../RepoStats.kt, core/presentation/.../model/GithubRepoSummaryUi.kt
Added license: String? and totalDownloads: Long to RepoStats; added downloadCount to UI summary model.
Presentation — Repo Card & Stats
core/presentation/.../components/RepositoryCard.kt, feature/details/presentation/.../components/StatItem.kt, .../sections/Stats.kt
Refactored stats display to InfoChip/StatItem components, introduced formatCount(Long), show downloads chip when >0, and render license text with license_none fallback.
Localization
core/presentation/src/commonMain/composeResources/values*/strings*.xml (13 files)
Added downloads, license, and license_none strings across 13 locales (en, ar, bn, es, fr, hi, it, ja, ko, pl, ru, tr, zh-rCN).

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through DTOs, mappers, and UI seas,

I fetched download counts on a backend breeze,
Chips and strings in thirteen tongues bloom,
Cache saves the day, fallbacks resume,
Hop, hop, hop — a tiny feature blooms! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main changes: adding download count and license information to repository details across models, UI, and data layers.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch details-download-lisence-feat

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
Contributor

@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

🧹 Nitpick comments (5)
core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt (1)

76-84: Consider URL-encoding owner/name.

owner and name are 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 existing getCategory/getTopic have 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 this

Then 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.Star is sandwiched between Icons.outlined.Code/Download and Icons.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 — inline toSummary().

toBackendSummary() is a single-line delegate to toSummary() with no added behavior. Just call toSummary() 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(...) } (or fold) before getOrNull() would make backend issues diagnosable without changing the fallback behavior. Same pattern applies in getRepoStats at 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

📥 Commits

Reviewing files that changed from the base of the PR and between bb4a83e and 649da36.

📒 Files selected for processing (33)
  • .claude/settings.local.json
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/AssetNetwork.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/BackendRepoResponse.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/RepoInfoNetwork.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/AssetNetwork.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/BackendRepoMapper.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubAsset.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubRepoSummary.kt
  • core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml
  • core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml
  • core/presentation/src/commonMain/composeResources/values-es/strings-es.xml
  • core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml
  • core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml
  • core/presentation/src/commonMain/composeResources/values-it/strings-it.xml
  • core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml
  • core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml
  • core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml
  • core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml
  • core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml
  • core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt
  • core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/GithubRepoSummaryUi.kt
  • core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/GithubRepoSummaryMappers.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt
  • feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/RepoStats.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt
  • feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/dto/CachedGithubRepoSummary.kt
  • feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/BackendToCachedMapper.kt
  • feature/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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +550 to +555
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()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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:


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.

Suggested 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()
}
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.

Comment on lines +327 to +339
// 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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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 openIssuesCount and license (or spdxId) to BackendRepoResponse and 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).

Comment on lines +93 to +98
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()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 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
fi

Repository: 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.

Copy link
Copy Markdown
Contributor

@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.

♻️ Duplicate comments (1)
core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt (1)

550-555: ⚠️ Potential issue | 🔴 Critical

String.format is JVM-only — breaks commonMain builds on non-JVM targets.

This file is in commonMain, but String.format(...) is a JVM-only extension (defined in StringsJVM.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 commonMain source 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: Duplicated formatCount helper.

Per the PR summary, a formatCount utility was also introduced (e.g., used by feature/details/.../StatItem.kt). Consider extracting a single implementation into core/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

📥 Commits

Reviewing files that changed from the base of the PR and between 649da36 and 4f8b676.

📒 Files selected for processing (1)
  • core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt

@rainxchzed rainxchzed merged commit 1c1b236 into main Apr 17, 2026
1 check passed
@rainxchzed rainxchzed deleted the details-download-lisence-feat branch April 17, 2026 11:54
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