Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 1 addition & 14 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,7 @@
"permissions": {
"allow": [
"Bash(grep -r \"\\\\.FLATPAK\\\\|Flatpak\\\\|flatpak\" . --include=*.kt)",
"Bash(./gradlew :composeApp:assembleDebug)",
"Bash(./gradlew :composeApp:jvmJar)",
"Bash(./gradlew :composeApp:assembleDebug :composeApp:jvmJar)",
"Bash(./gradlew :core:data:compileDebugKotlinAndroid :core:domain:compileDebugKotlinAndroid)",
"Bash(./gradlew :feature:details:presentation:compileDebugKotlinAndroid)",
"Bash(./gradlew :feature:apps:presentation:compileDebugKotlinAndroid)",
"Bash(./gradlew :core:data:compileDebugKotlinAndroid)",
"Bash(./gradlew :core:data:compileDebugKotlinAndroid :feature:details:presentation:compileDebugKotlinAndroid :feature:apps:presentation:compileDebugKotlinAndroid)",
"Bash(./gradlew :composeApp:compileDebugKotlinAndroid :core:data:compileDebugKotlinAndroid :core:domain:compileDebugKotlinAndroid :feature:details:presentation:compileDebugKotlinAndroid :feature:apps:presentation:compileDebugKotlinAndroid)",
"Bash(./gradlew build *)",
"Bash(./gradlew compileKotlinJvm compileDebugKotlinAndroid)",
"Bash(./gradlew :core:data:compileKotlinJvm :feature:home:data:compileKotlinJvm :feature:search:data:compileKotlinJvm --rerun-tasks)",
"Bash(./gradlew :core:data:compileDebugKotlinAndroid :feature:home:data:compileDebugKotlinAndroid :feature:search:data:compileDebugKotlinAndroid --rerun-tasks)",
"Bash(./gradlew :core:data:compileKotlinJvm :feature:home:data:compileKotlinJvm :feature:search:data:compileKotlinJvm :core:data:compileDebugKotlinAndroid :feature:home:data:compileDebugKotlinAndroid :feature:search:data:compileDebugKotlinAndroid --rerun-tasks)"
"Bash(./gradlew *)"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ data class AssetNetwork(
@SerialName("size") val size: Long,
@SerialName("browser_download_url") val downloadUrl: String,
@SerialName("uploader") val uploader: OwnerNetwork,
@SerialName("download_count") val downloadCount: Long = 0,
)
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ data class BackendRepoResponse(
val hasInstallersWindows: Boolean = false,
val hasInstallersMacos: Boolean = false,
val hasInstallersLinux: Boolean = false,
val downloadCount: Long = 0,
)

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,11 @@ data class RepoInfoNetwork(
@SerialName("stargazers_count") val stars: Int,
@SerialName("forks_count") val forks: Int,
@SerialName("open_issues_count") val openIssues: Int,
val license: LicenseNetwork? = null,
)

@Serializable
data class LicenseNetwork(
@SerialName("spdx_id") val spdxId: String? = null,
val name: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ fun AssetNetwork.toDomain(): GithubAsset =
avatarUrl = uploader.avatarUrl,
htmlUrl = uploader.htmlUrl,
),
downloadCount = downloadCount,
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fun BackendRepoResponse.toSummary(): GithubRepoSummary =
releasesUrl = releasesUrl ?: "https://api.github.com/repos/$fullName/releases{/id}",
updatedAt = latestReleaseDate ?: updatedAt ?: "",
availablePlatforms = buildAvailablePlatforms(),
downloadCount = downloadCount,
)

private fun BackendRepoResponse.buildAvailablePlatforms(): List<DiscoveryPlatform> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ class BackendApiClient {
}
}

suspend fun getRepo(owner: String, name: String): Result<BackendRepoResponse> =
safeCall {
val response = httpClient.get("repo/$owner/$name")
if (response.status.isSuccess()) {
Result.success(response.body())
} else {
Result.failure(BackendException("HTTP ${response.status.value}"))
}
}

private inline fun <T> safeCall(block: () -> Result<T>): Result<T> =
try {
block()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ data class GithubAsset(
val size: Long,
val downloadUrl: String,
val uploader: GithubUser,
val downloadCount: Long = 0,
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ data class GithubRepoSummary(
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.

)
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@
<string name="forks">التفرعات</string>
<string name="stars">النجوم</string>
<string name="issues">المشكلات</string>
<string name="downloads">التنزيلات</string>
<string name="license">الترخيص</string>
<string name="license_none">لا يوجد</string>

<!-- Misc -->
<string name="by_author">بواسطة %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@
<string name="forks">ফর্ক</string>
<string name="stars">স্টার</string>
<string name="issues">ইস্যু</string>
<string name="downloads">ডাউনলোড</string>
<string name="license">লাইসেন্স</string>
<string name="license_none">নেই</string>

<!-- Misc -->
<string name="by_author">%1$s দ্বারা</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@
<string name="forks">Bifurcaciones</string>
<string name="stars">Estrellas</string>
<string name="issues">Problemas</string>
<string name="downloads">Descargas</string>
<string name="license">Licencia</string>
<string name="license_none">Ninguna</string>

<string name="by_author">por %1$s</string>
<string name="installed_version">• Instalado: %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@
<string name="forks">Forks</string>
<string name="stars">Étoiles</string>
<string name="issues">Tickets</string>
<string name="downloads">Téléchargements</string>
<string name="license">Licence</string>
<string name="license_none">Aucune</string>

<string name="by_author">par %1$s</string>
<string name="installed_version">• Installé : %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@
<string name="forks">फोर्क्स</string>
<string name="stars">स्टार्स</string>
<string name="issues">इश्यूज़</string>
<string name="downloads">डाउनलोड</string>
<string name="license">लाइसेंस</string>
<string name="license_none">कोई नहीं</string>

<!-- Misc -->
<string name="by_author">द्वारा %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@
<string name="forks">Forks</string>
<string name="stars">Stelle</string>
<string name="issues">Problemi</string>
<string name="downloads">Download</string>
<string name="license">Licenza</string>
<string name="license_none">Nessuna</string>

<!-- Misc -->
<string name="by_author">per %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@
<string name="forks">フォーク</string>
<string name="stars">スター</string>
<string name="issues">課題</string>
<string name="downloads">ダウンロード</string>
<string name="license">ライセンス</string>
<string name="license_none">なし</string>

<string name="by_author">%1$s 作</string>
<string name="installed_version">• インストール済み: %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@
<string name="forks">포크</string>
<string name="stars">별</string>
<string name="issues">이슈</string>
<string name="downloads">다운로드</string>
<string name="license">라이선스</string>
<string name="license_none">없음</string>

<!-- Misc -->
<string name="by_author">%1$s 작성</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@
<string name="forks">Forki</string>
<string name="stars">Gwiazdki</string>
<string name="issues">Zgłoszenia</string>
<string name="downloads">Pobrania</string>
<string name="license">Licencja</string>
<string name="license_none">Brak</string>

<string name="by_author">autor: %1$s</string>
<string name="installed_version">• Zainstalowana: %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
<string name="forks">Форки</string>
<string name="stars">Звёзды</string>
<string name="issues">Проблемы</string>
<string name="downloads">Загрузки</string>
<string name="license">Лицензия</string>
<string name="license_none">Нет</string>

<string name="by_author">от %1$s</string>
<string name="installed_version">• Установлено: %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@
<string name="forks">Forklar</string>
<string name="stars">Yıldızlar</string>
<string name="issues">Sorunlar</string>
<string name="downloads">İndirmeler</string>
<string name="license">Lisans</string>
<string name="license_none">Yok</string>

<!-- Misc -->
<string name="by_author">%1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@
<string name="forks">分支</string>
<string name="stars">星标</string>
<string name="issues">问题</string>
<string name="downloads">下载量</string>
<string name="license">许可证</string>
<string name="license_none">无</string>

<string name="by_author">由 %1$s</string>
<string name="installed_version">• 已安装:%1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@
<string name="forks">Forks</string>
<string name="stars">Stars</string>
<string name="issues">Issues</string>
<string name="downloads">Downloads</string>
<string name="license">License</string>
<string name="license_none">None</string>

<!-- Misc -->
<string name="by_author">by %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.OpenInBrowser
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.Update
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.StarOutline
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
Expand Down Expand Up @@ -188,37 +191,32 @@ fun RepositoryCard(

Spacer(Modifier.height(8.dp))

Row(
FlowRow(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
) {
Text(
text = "⭐ ${discoveryRepositoryUi.repository.stargazersCount}",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
InfoChip(
icon = Icons.Outlined.StarOutline,
text = formatCount(discoveryRepositoryUi.repository.stargazersCount.toLong()),
)

Text(
text = "• 🌴 ${discoveryRepositoryUi.repository.forksCount}",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
InfoChip(
icon = Icons.AutoMirrored.Outlined.CallSplit,
text = formatCount(discoveryRepositoryUi.repository.forksCount.toLong()),
)

if (discoveryRepositoryUi.repository.downloadCount > 0) {
InfoChip(
icon = Icons.Outlined.Download,
text = formatCount(discoveryRepositoryUi.repository.downloadCount),
)
}

discoveryRepositoryUi.repository.language?.let {
Text(
text = "• $it",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
InfoChip(
icon = Icons.Outlined.Code,
text = it,
)
}
}
Expand Down Expand Up @@ -351,7 +349,7 @@ fun PlatformChip(

Text(
text = platform.name,
style = MaterialTheme.typography.labelSmall,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
Expand Down Expand Up @@ -515,3 +513,43 @@ fun RepositoryCardPreview() {
)
}
}

@Composable
private fun InfoChip(
icon: androidx.compose.ui.graphics.vector.ImageVector,
text: String,
) {
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = text,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Medium,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
)
}
}
}

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()
}
Comment on lines +550 to +555
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.

Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ data class GithubRepoSummaryUi(
val updatedAt: String,
val isFork: Boolean = false,
val availablePlatforms: ImmutableList<DiscoveryPlatform> = persistentListOf(),
val downloadCount: Long = 0,
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fun GithubRepoSummary.toUi(): GithubRepoSummaryUi {
releasesUrl = releasesUrl,
updatedAt = updatedAt,
isFork = isFork,
availablePlatforms = availablePlatforms.toImmutableList()
availablePlatforms = availablePlatforms.toImmutableList(),
downloadCount = downloadCount,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ val detailsModule =
DetailsRepositoryImpl(
logger = get(),
httpClient = get(),
backendApiClient = get(),
localizationManager = get(),
cacheManager = get(),
)
Expand Down
Loading