Skip to content

Commit 4a9391a

Browse files
committed
refactor(home): Revise home categories and data fetching logic
This commit refactors the home screen categories, replacing "New" and "Recently Updated" with "Hot Release" and "Most Popular". The underlying data fetching logic has been updated to support these new categories, including the ability to fetch from a cached data source before falling back to the live API. - **feat(home)!**: Renamed home categories from `NEW` and `RECENTLY_UPDATED` to `HOT_RELEASE` and `MOST_POPULAR`. - Updated `HomeCategory` enum, string resources, and all related components (`HomeViewModel`, `HomeRepository`). - **refactor(home,data)**: Moved `HomeCategory` enum from the `presentation` to the `domain` module to better align with clean architecture principles. - **feat(home,data)**: Implemented fetching from cached data sources for "Hot Release" and "Most Popular" categories, with a fallback to the live GitHub API. - **refactor(home,data)**: Reorganized `CachedRepositoriesDataSource` by extracting its implementation into a new `impl` subpackage. - **refactor(home,repo)**: Simplified and improved the live API search queries for all home categories, adjusting parameters like date ranges and star counts for better relevance. - **chore(deps)**: Added the Kotlin serialization plugin to the `composeApp` module.
1 parent dc652e9 commit 4a9391a

17 files changed

Lines changed: 259 additions & 296 deletions

File tree

composeApp/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
22

33
plugins {
44
alias(libs.plugins.convention.cmp.application)
5+
alias(libs.plugins.kotlin.serialization)
56
alias(libs.plugins.compose.hot.reload)
67
}
78

core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import zed.rainxch.core.data.local.db.dao.FavoriteRepoDao
1212
import zed.rainxch.core.data.local.db.dao.InstalledAppDao
1313
import zed.rainxch.core.data.local.db.dao.StarredRepoDao
1414
import zed.rainxch.core.data.local.db.dao.UpdateHistoryDao
15+
import zed.rainxch.core.data.logging.KermitLogger
1516
import zed.rainxch.core.data.network.createGitHubHttpClient
1617
import zed.rainxch.core.data.repository.AuthenticationStateImpl
1718
import zed.rainxch.core.data.repository.FavouritesRepositoryImpl
@@ -20,6 +21,7 @@ import zed.rainxch.core.data.repository.RateLimitRepositoryImpl
2021
import zed.rainxch.core.data.repository.StarredRepositoryImpl
2122
import zed.rainxch.core.data.repository.ThemesRepositoryImpl
2223
import zed.rainxch.core.domain.getPlatform
24+
import zed.rainxch.core.domain.logging.GitHubStoreLogger
2325
import zed.rainxch.core.domain.model.Platform
2426
import zed.rainxch.core.domain.repository.AuthenticationState
2527
import zed.rainxch.core.domain.repository.FavouritesRepository
@@ -34,6 +36,10 @@ val coreModule = module {
3436
CoroutineScope(Dispatchers.IO + SupervisorJob())
3537
}
3638

39+
single<GitHubStoreLogger> {
40+
KermitLogger
41+
}
42+
3743
single<Platform> {
3844
getPlatform()
3945
}

core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/interceptor/RateLimitInterceptor.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package zed.rainxch.core.data.network.interceptor
22

33
import io.ktor.client.HttpClient
44
import io.ktor.client.plugins.HttpClientPlugin
5+
import io.ktor.client.statement.HttpReceivePipeline
56
import io.ktor.client.statement.HttpResponse
7+
import io.ktor.client.statement.HttpResponsePipeline
68
import io.ktor.http.Headers
79
import io.ktor.util.AttributeKey
810
import zed.rainxch.core.domain.model.RateLimitException
@@ -31,7 +33,7 @@ class RateLimitInterceptor(
3133
}
3234

3335
override fun install(plugin: RateLimitInterceptor, scope: HttpClient) {
34-
scope.receivePipeline.intercept(io.ktor.client.statement.HttpResponsePipeline.Receive) {
36+
scope.receivePipeline.intercept(HttpReceivePipeline.State) {
3537
val response = subject
3638

3739
parseRateLimitFromHeaders(response.headers)?.let { rateLimitInfo ->
Lines changed: 2 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,9 @@
11
package zed.rainxch.home.data.data_source
22

3-
import io.ktor.client.HttpClient
4-
import io.ktor.client.plugins.HttpRequestRetry
5-
import io.ktor.client.plugins.HttpRequestTimeoutException
6-
import io.ktor.client.plugins.HttpTimeout
7-
import io.ktor.client.request.get
8-
import io.ktor.client.statement.HttpResponse
9-
import io.ktor.client.statement.bodyAsText
10-
import io.ktor.http.isSuccess
11-
import kotlinx.coroutines.Dispatchers
12-
import kotlinx.coroutines.withContext
13-
import kotlinx.serialization.Serializable
14-
import kotlinx.serialization.SerializationException
15-
import kotlinx.serialization.json.Json
16-
import zed.rainxch.core.domain.logging.GitHubStoreLogger
17-
import zed.rainxch.core.domain.model.GithubRepoSummary
18-
import zed.rainxch.core.domain.model.GithubUser
19-
import zed.rainxch.core.domain.model.Platform
20-
import zed.rainxch.home.data.dto.CachedGithubRepoSummary
213
import zed.rainxch.home.data.dto.CachedRepoResponse
224

235
interface CachedRepositoriesDataSource {
246
suspend fun getCachedTrendingRepos(): CachedRepoResponse?
25-
}
26-
27-
class CachedRepositoriesDataSourceImpl(
28-
private val platform: Platform,
29-
private val logger: GitHubStoreLogger
30-
) : CachedRepositoriesDataSource {
31-
private val json = Json {
32-
ignoreUnknownKeys = true
33-
isLenient = true
34-
}
35-
36-
private val httpClient = HttpClient {
37-
install(HttpTimeout) {
38-
requestTimeoutMillis = 15_000
39-
connectTimeoutMillis = 10_000
40-
socketTimeoutMillis = 15_000
41-
}
42-
43-
install(HttpRequestRetry) {
44-
maxRetries = 2
45-
retryOnServerErrors(maxRetries = 2)
46-
exponentialDelay()
47-
}
48-
49-
expectSuccess = false
50-
}
51-
52-
override suspend fun getCachedTrendingRepos(): CachedRepoResponse? {
53-
return withContext(Dispatchers.IO) {
54-
try {
55-
val platformName = when (platform) {
56-
Platform.ANDROID -> "android"
57-
Platform.WINDOWS -> "windows"
58-
Platform.MACOS -> "macos"
59-
Platform.LINUX -> "linux"
60-
}
61-
62-
val url = "$BASE_URL/$platformName.json"
63-
64-
logger.debug("🔍 Fetching cached trending repos from: $url")
65-
66-
val response: HttpResponse = httpClient.get(url)
67-
68-
logger.debug("📥 Response status: ${response.status.value} ${response.status.description}")
69-
70-
when {
71-
response.status.isSuccess() -> {
72-
val responseText = response.bodyAsText()
73-
logger.debug("📄 Response body length: ${responseText.length} characters")
74-
75-
val cachedData = json.decodeFromString<CachedRepoResponse>(responseText)
76-
77-
logger.debug("✓ Successfully loaded ${cachedData.repositories.size} cached repos")
78-
logger.debug("✓ Last updated: ${cachedData.lastUpdated}")
79-
80-
cachedData
81-
}
82-
83-
response.status.value == 404 -> {
84-
logger.warn("⚠️ Cached data not found (404) - may not be generated yet")
85-
logger.warn("⚠️ URL attempted: $url")
86-
null
87-
}
88-
89-
else -> {
90-
val errorBody = response.bodyAsText()
91-
logger.error("❌ Failed to fetch cached repos: HTTP ${response.status.value}")
92-
logger.error("❌ Response body: ${errorBody.take(500)}")
93-
null
94-
}
95-
}
96-
} catch (e: HttpRequestTimeoutException) {
97-
logger.error("⏱️ Timeout fetching cached trending repos: ${e.message}")
98-
e.printStackTrace()
99-
null
100-
} catch (e: SerializationException) {
101-
logger.error("🔧 JSON parsing error: ${e.message}")
102-
e.printStackTrace()
103-
null
104-
} catch (e: Exception) {
105-
logger.error("💥 Error fetching cached trending repos: ${e.message}")
106-
logger.error("💥 Exception type: ${e::class.simpleName}")
107-
e.printStackTrace()
108-
null
109-
}
110-
}
111-
}
112-
113-
private companion object {
114-
private const val BASE_REPO_URL = "https://raw.githubusercontent.com/rainxchzed/Github-Store"
115-
private const val BASE_URL = "$BASE_REPO_URL/main/cached-data/trending"
116-
}
7+
suspend fun getCachedHotReleaseRepos(): CachedRepoResponse?
8+
suspend fun getCachedMostPopularRepos(): CachedRepoResponse?
1179
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package zed.rainxch.home.data.data_source.impl
2+
3+
import io.ktor.client.HttpClient
4+
import io.ktor.client.plugins.HttpRequestRetry
5+
import io.ktor.client.plugins.HttpRequestTimeoutException
6+
import io.ktor.client.plugins.HttpTimeout
7+
import io.ktor.client.request.get
8+
import io.ktor.client.statement.HttpResponse
9+
import io.ktor.client.statement.bodyAsText
10+
import io.ktor.http.isSuccess
11+
import kotlinx.coroutines.Dispatchers
12+
import kotlinx.coroutines.withContext
13+
import kotlinx.serialization.SerializationException
14+
import kotlinx.serialization.json.Json
15+
import zed.rainxch.core.domain.logging.GitHubStoreLogger
16+
import zed.rainxch.core.domain.model.Platform
17+
import zed.rainxch.home.data.data_source.CachedRepositoriesDataSource
18+
import zed.rainxch.home.data.dto.CachedRepoResponse
19+
import zed.rainxch.home.domain.model.HomeCategory
20+
21+
class CachedRepositoriesDataSourceImpl(
22+
private val platform: Platform,
23+
private val logger: GitHubStoreLogger
24+
) : CachedRepositoriesDataSource {
25+
private val json = Json {
26+
ignoreUnknownKeys = true
27+
isLenient = true
28+
}
29+
30+
private val httpClient = HttpClient {
31+
install(HttpTimeout) {
32+
requestTimeoutMillis = 15_000
33+
connectTimeoutMillis = 10_000
34+
socketTimeoutMillis = 15_000
35+
}
36+
37+
install(HttpRequestRetry) {
38+
maxRetries = 2
39+
retryOnServerErrors(maxRetries = 2)
40+
exponentialDelay()
41+
}
42+
43+
expectSuccess = false
44+
}
45+
46+
override suspend fun getCachedTrendingRepos(): CachedRepoResponse? {
47+
return fetchCachedReposForCategory(HomeCategory.TRENDING)
48+
}
49+
50+
override suspend fun getCachedHotReleaseRepos(): CachedRepoResponse? {
51+
return fetchCachedReposForCategory(HomeCategory.HOT_RELEASE)
52+
}
53+
54+
override suspend fun getCachedMostPopularRepos(): CachedRepoResponse? {
55+
return fetchCachedReposForCategory(HomeCategory.MOST_POPULAR)
56+
}
57+
58+
private suspend fun fetchCachedReposForCategory(
59+
category: HomeCategory
60+
): CachedRepoResponse? {
61+
return withContext(Dispatchers.IO) {
62+
try {
63+
val platformName = when (platform) {
64+
Platform.ANDROID -> "android"
65+
Platform.WINDOWS -> "windows"
66+
Platform.MACOS -> "macos"
67+
Platform.LINUX -> "linux"
68+
}
69+
70+
val base = when (category) {
71+
HomeCategory.TRENDING -> TRENDING_FULL_URL
72+
HomeCategory.HOT_RELEASE -> HOT_RELEASE_FULL_URL
73+
HomeCategory.MOST_POPULAR -> MOST_POPULAR_FULL_URL
74+
}
75+
76+
val url = "$base/$platformName.json"
77+
78+
logger.debug("🔍 Fetching cached repos from: $url")
79+
80+
val response: HttpResponse = httpClient.get(url)
81+
82+
logger.debug("📥 Response status: ${response.status.value} ${response.status.description}")
83+
84+
when {
85+
response.status.isSuccess() -> {
86+
val responseText = response.bodyAsText()
87+
logger.debug("📄 Response body length: ${responseText.length} characters")
88+
89+
val cachedData = json.decodeFromString<CachedRepoResponse>(responseText)
90+
91+
logger.debug("✓ Successfully loaded ${cachedData.repositories.size} cached repos")
92+
logger.debug("✓ Last updated: ${cachedData.lastUpdated}")
93+
94+
cachedData
95+
}
96+
97+
response.status.value == 404 -> {
98+
logger.warn("⚠️ Cached data not found (404) - may not be generated yet")
99+
logger.warn("⚠️ URL attempted: $url")
100+
null
101+
}
102+
103+
else -> {
104+
val errorBody = response.bodyAsText()
105+
logger.error("❌ Failed to fetch cached repos: HTTP ${response.status.value}")
106+
logger.error("❌ Response body: ${errorBody.take(500)}")
107+
null
108+
}
109+
}
110+
} catch (e: HttpRequestTimeoutException) {
111+
logger.error("⏱️ Timeout fetching cached repos: ${e.message}")
112+
e.printStackTrace()
113+
null
114+
} catch (e: SerializationException) {
115+
logger.error("🔧 JSON parsing error: ${e.message}")
116+
e.printStackTrace()
117+
null
118+
} catch (e: Exception) {
119+
logger.error("💥 Error fetching cached repos: ${e.message}")
120+
logger.error("💥 Exception type: ${e::class.simpleName}")
121+
e.printStackTrace()
122+
null
123+
}
124+
}
125+
}
126+
127+
private companion object {
128+
private const val BASE_REPO_URL = "https://raw.githubusercontent.com/OpenHub-Store/api/refs/heads"
129+
private const val TRENDING_FULL_URL = "$BASE_REPO_URL/main/cached-data/trending"
130+
private const val HOT_RELEASE_FULL_URL = "$BASE_REPO_URL/main/cached-data/new-releases"
131+
private const val MOST_POPULAR_FULL_URL = "$BASE_REPO_URL/main/cached-data/most-popular"
132+
133+
134+
}
135+
}

feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/di/SharedModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package zed.rainxch.home.data.di
22

33
import org.koin.dsl.module
44
import zed.rainxch.home.data.data_source.CachedRepositoriesDataSource
5-
import zed.rainxch.home.data.data_source.CachedRepositoriesDataSourceImpl
5+
import zed.rainxch.home.data.data_source.impl.CachedRepositoriesDataSourceImpl
66
import zed.rainxch.home.data.repository.HomeRepositoryImpl
77
import zed.rainxch.home.domain.repository.HomeRepository
88

0 commit comments

Comments
 (0)