diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 37efc3bf..94db9134 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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 *)" ] } } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/AssetNetwork.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/AssetNetwork.kt index 1fcc467e..e9076cf8 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/AssetNetwork.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/AssetNetwork.kt @@ -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, ) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/BackendRepoResponse.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/BackendRepoResponse.kt index 0f71dbbf..2204d80f 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/BackendRepoResponse.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/BackendRepoResponse.kt @@ -28,6 +28,7 @@ data class BackendRepoResponse( val hasInstallersWindows: Boolean = false, val hasInstallersMacos: Boolean = false, val hasInstallersLinux: Boolean = false, + val downloadCount: Long = 0, ) @Serializable diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/RepoInfoNetwork.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/RepoInfoNetwork.kt index 21ec214c..0a8da7b7 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/RepoInfoNetwork.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/RepoInfoNetwork.kt @@ -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, ) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/AssetNetwork.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/AssetNetwork.kt index 109baf54..5d425dbd 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/AssetNetwork.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/AssetNetwork.kt @@ -18,4 +18,5 @@ fun AssetNetwork.toDomain(): GithubAsset = avatarUrl = uploader.avatarUrl, htmlUrl = uploader.htmlUrl, ), + downloadCount = downloadCount, ) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/BackendRepoMapper.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/BackendRepoMapper.kt index 2fc40a4e..a44bbdf9 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/BackendRepoMapper.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/BackendRepoMapper.kt @@ -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 = diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt index ea71b791..04a0f2c9 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt @@ -73,6 +73,16 @@ class BackendApiClient { } } + suspend fun getRepo(owner: String, name: String): Result = + 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 safeCall(block: () -> Result): Result = try { block() diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubAsset.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubAsset.kt index cb370a42..9149285a 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubAsset.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubAsset.kt @@ -10,4 +10,5 @@ data class GithubAsset( val size: Long, val downloadUrl: String, val uploader: GithubUser, + val downloadCount: Long = 0, ) diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubRepoSummary.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubRepoSummary.kt index 208b89d7..6d177f22 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubRepoSummary.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubRepoSummary.kt @@ -19,4 +19,5 @@ data class GithubRepoSummary( val updatedAt: String, val isFork: Boolean = false, val availablePlatforms: List = emptyList(), + val downloadCount: Long = 0, ) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index b5d390db..5c07c233 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -232,6 +232,9 @@ التفرعات النجوم المشكلات + التنزيلات + الترخيص + لا يوجد بواسطة %1$s diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index b053e64e..732cab0a 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -191,6 +191,9 @@ ফর্ক স্টার ইস্যু + ডাউনলোড + লাইসেন্স + নেই %1$s দ্বারা diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index a2cf5002..87764613 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -141,6 +141,9 @@ Bifurcaciones Estrellas Problemas + Descargas + Licencia + Ninguna por %1$s • Instalado: %1$s diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index de41a7d6..393b125c 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -141,6 +141,9 @@ Forks Étoiles Tickets + Téléchargements + Licence + Aucune par %1$s • Installé : %1$s diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 018dce77..643c02ef 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -191,6 +191,9 @@ फोर्क्स स्टार्स इश्यूज़ + डाउनलोड + लाइसेंस + कोई नहीं द्वारा %1$s diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 33ac1483..d9dc2222 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -191,6 +191,9 @@ Forks Stelle Problemi + Download + Licenza + Nessuna per %1$s diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index dc173915..e00a5bc2 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -142,6 +142,9 @@ フォーク スター 課題 + ダウンロード + ライセンス + なし %1$s 作 • インストール済み: %1$s diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 84c13d61..71905233 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -189,6 +189,9 @@ 포크 이슈 + 다운로드 + 라이선스 + 없음 %1$s 작성 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 172e0abd..e3f916b9 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -160,6 +160,9 @@ Forki Gwiazdki Zgłoszenia + Pobrania + Licencja + Brak autor: %1$s • Zainstalowana: %1$s diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index 3611ebe2..dc599567 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -156,6 +156,9 @@ Форки Звёзды Проблемы + Загрузки + Лицензия + Нет от %1$s • Установлено: %1$s diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index d55349d5..87a93022 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -190,6 +190,9 @@ Forklar Yıldızlar Sorunlar + İndirmeler + Lisans + Yok %1$s diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index cf02fca2..3b44685d 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -143,6 +143,9 @@ 分支 星标 问题 + 下载量 + 许可证 + 由 %1$s • 已安装:%1$s diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 6be02144..36cf3e95 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -240,6 +240,9 @@ Forks Stars Issues + Downloads + License + None by %1$s diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt index 03d47e50..0d39372e 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt @@ -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 @@ -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, ) } } @@ -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), @@ -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() + } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/GithubRepoSummaryUi.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/GithubRepoSummaryUi.kt index 6deb2623..36849e21 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/GithubRepoSummaryUi.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/GithubRepoSummaryUi.kt @@ -20,4 +20,5 @@ data class GithubRepoSummaryUi( val updatedAt: String, val isFork: Boolean = false, val availablePlatforms: ImmutableList = persistentListOf(), + val downloadCount: Long = 0, ) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/GithubRepoSummaryMappers.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/GithubRepoSummaryMappers.kt index 2613c234..f8402455 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/GithubRepoSummaryMappers.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/GithubRepoSummaryMappers.kt @@ -20,6 +20,7 @@ fun GithubRepoSummary.toUi(): GithubRepoSummaryUi { releasesUrl = releasesUrl, updatedAt = updatedAt, isFork = isFork, - availablePlatforms = availablePlatforms.toImmutableList() + availablePlatforms = availablePlatforms.toImmutableList(), + downloadCount = downloadCount, ) } diff --git a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt index c2f6fa04..5edb7c53 100644 --- a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt +++ b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt @@ -16,6 +16,7 @@ val detailsModule = DetailsRepositoryImpl( logger = get(), httpClient = get(), + backendApiClient = get(), localizationManager = get(), cacheManager = get(), ) diff --git a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt index 0dc15b5b..93dee432 100644 --- a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt +++ b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt @@ -17,7 +17,10 @@ import zed.rainxch.core.data.dto.RepoByIdNetwork import zed.rainxch.core.data.dto.RepoInfoNetwork import zed.rainxch.core.data.dto.UserProfileNetwork import zed.rainxch.details.data.dto.AttestationsResponse +import zed.rainxch.core.data.dto.BackendRepoResponse import zed.rainxch.core.data.mappers.toDomain +import zed.rainxch.core.data.mappers.toSummary +import zed.rainxch.core.data.network.BackendApiClient import zed.rainxch.core.data.network.executeRequest import zed.rainxch.core.data.services.LocalizationManager import zed.rainxch.core.domain.logging.GitHubStoreLogger @@ -32,6 +35,7 @@ import zed.rainxch.details.domain.repository.DetailsRepository class DetailsRepositoryImpl( private val httpClient: HttpClient, + private val backendApiClient: BackendApiClient, private val localizationManager: LocalizationManager, private val logger: GitHubStoreLogger, private val cacheManager: CacheManager, @@ -45,6 +49,8 @@ class DetailsRepositoryImpl( private val readmeHelper = ReadmeLocalizationHelper(localizationManager) + private fun BackendRepoResponse.toBackendSummary(): GithubRepoSummary = toSummary() + private fun RepoByIdNetwork.toGithubRepoSummary(): GithubRepoSummary = GithubRepoSummary( id = id, @@ -107,7 +113,17 @@ class DetailsRepositoryImpl( return cached } + // Try backend first + backendApiClient.getRepo(owner, name).getOrNull()?.let { backendRepo -> + logger.debug("Backend hit for repo $owner/$name") + val result = backendRepo.toBackendSummary() + cacheManager.put(cacheKey, result, REPO_DETAILS) + return result + } + + // Fallback to GitHub API return try { + logger.debug("Backend miss for $owner/$name, falling back to GitHub API") val result = httpClient .executeRequest { @@ -308,7 +324,23 @@ class DetailsRepositoryImpl( return cached } + // 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 + } + + // Fallback to GitHub API return try { + logger.debug("Backend miss for stats $owner/$repo, falling back to GitHub API") val info = httpClient .executeRequest { @@ -322,6 +354,8 @@ class DetailsRepositoryImpl( stars = info.stars, forks = info.forks, openIssues = info.openIssues, + license = info.license?.spdxId ?: info.license?.name, + totalDownloads = 0, ) cacheManager.put(cacheKey, result, REPO_STATS) diff --git a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/RepoStats.kt b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/RepoStats.kt index db84dc93..c48bc833 100644 --- a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/RepoStats.kt +++ b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/RepoStats.kt @@ -7,4 +7,6 @@ data class RepoStats( val stars: Int, val forks: Int, val openIssues: Int, + val license: String? = null, + val totalDownloads: Long = 0, ) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt index 7283b45c..86435c41 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt @@ -16,6 +16,49 @@ fun StatItem( label: String, stat: Int, modifier: Modifier = Modifier, +) { + StatItem(label = label, stat = stat.toLong(), modifier = modifier) +} + +@Composable +fun StatItem( + label: String, + stat: Long, + modifier: Modifier = Modifier, +) { + OutlinedCard( + modifier = modifier, + colors = + CardDefaults.outlinedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + ), + ) { + Column( + modifier = Modifier.padding(12.dp), + ) { + Text( + text = label, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.outline, + maxLines = 1, + softWrap = false, + ) + + Text( + text = formatCount(stat), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Black, + color = MaterialTheme.colorScheme.onBackground, + ) + } + } +} + +@Composable +fun TextStatItem( + label: String, + value: String, + modifier: Modifier = Modifier, ) { OutlinedCard( modifier = modifier, @@ -36,11 +79,20 @@ fun StatItem( ) Text( - text = stat.toString(), + text = value, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Black, color = MaterialTheme.colorScheme.onBackground, + maxLines = 1, + softWrap = false, ) } } } + +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() + } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt index 8ff7218e..107e63ae 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt @@ -12,6 +12,7 @@ import io.github.fletchmckee.liquid.liquefiable import org.jetbrains.compose.resources.stringResource import zed.rainxch.details.domain.model.RepoStats import zed.rainxch.details.presentation.components.StatItem +import zed.rainxch.details.presentation.components.TextStatItem import zed.rainxch.details.presentation.utils.LocalTopbarLiquidState import zed.rainxch.githubstore.core.presentation.res.* @@ -73,5 +74,42 @@ fun LazyListScope.stats( ), ) } + + Spacer(Modifier.height(12.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + StatItem( + label = stringResource(Res.string.downloads), + stat = repoStats.totalDownloads, + modifier = + Modifier + .weight(1f) + .then( + if (isLiquidGlassEnabled) { + Modifier.liquefiable(liquidState) + } else { + Modifier + }, + ), + ) + + TextStatItem( + label = stringResource(Res.string.license), + value = repoStats.license ?: stringResource(Res.string.license_none), + modifier = + Modifier + .weight(1f) + .then( + if (isLiquidGlassEnabled) { + Modifier.liquefiable(liquidState) + } else { + Modifier + }, + ), + ) + } } } diff --git a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/dto/CachedGithubRepoSummary.kt b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/dto/CachedGithubRepoSummary.kt index a4717b07..18e04e4d 100644 --- a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/dto/CachedGithubRepoSummary.kt +++ b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/dto/CachedGithubRepoSummary.kt @@ -22,4 +22,5 @@ data class CachedGithubRepoSummary( val trendingScore: Double? = null, val popularityScore: Int? = null, val availablePlatforms: List = emptyList(), + val downloadCount: Long = 0, ) diff --git a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/BackendToCachedMapper.kt b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/BackendToCachedMapper.kt index edfa4919..d9cf6a16 100644 --- a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/BackendToCachedMapper.kt +++ b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/BackendToCachedMapper.kt @@ -32,4 +32,5 @@ fun BackendRepoResponse.toCachedGithubRepoSummary(): CachedGithubRepoSummary = if (hasInstallersMacos) add(DiscoveryPlatform.Macos) if (hasInstallersLinux) add(DiscoveryPlatform.Linux) }, + downloadCount = downloadCount, ) diff --git a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/CachedGithubRepoSummaryMappers.kt b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/CachedGithubRepoSummaryMappers.kt index 10fa5b96..3b9e0ceb 100644 --- a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/CachedGithubRepoSummaryMappers.kt +++ b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/mappers/CachedGithubRepoSummaryMappers.kt @@ -26,4 +26,5 @@ fun CachedGithubRepoSummary.toGithubRepoSummary(): GithubRepoSummary = releasesUrl = releasesUrl, updatedAt = latestReleaseDate ?: updatedAt, availablePlatforms = availablePlatforms, + downloadCount = downloadCount, )