Skip to content

Commit 1c557a3

Browse files
committed
feat: Implement core GitHub repository backup functionality
This commit introduces the foundational features for a GitHub repository backup application built with Compose for Desktop. It includes the data layer, presentation layer, and dependency injection setup. - **Data Layer & Repository:** - Adds `GithubRepository` and its implementation (`GithubRepositoryImpl`) to handle interactions with the GitHub API. - Implements `getRepositories` to fetch a user's repositories, handling pagination to retrieve all entries. - Introduces `cloneRepository`, which uses `ProcessBuilder` to execute `git clone` commands, providing real-time progress updates by parsing the command's output. - Defines data models (`GithubResponse`, `GitHubRepoItem`) for deserializing API responses. - **ViewModel & State Management:** - Creates `RepoViewModel` to manage UI state (`RepoState`) and handle user intents (`RepoIntent`). - Manages fetching repositories, selecting/deselecting items, searching/filtering, and orchestrating the download process. - Calculates download progress, including total percentage and estimated time remaining (ETA). - **UI & Presentation:** - Develops the main `RepoScreen` with a `Scaffold`, `TopAppBar`, and a custom `BottomBar`. - Introduces components for user input (`GithubSelectionLayout`), repository listing (`RepoCard`), and user feedback (`ProgressDialog`, `ErrorDialog`). - Implements a `LazyColumn` to display repositories with features like "Select All" and a search bar. - Integrates custom `Filled` and `Outlined` buttons for consistent UI actions. - Adds `AppTheme` with custom light and dark color schemes inspired by GitHub's design. - **Dependency Injection & Core Utilities:** - Sets up Koin for dependency injection with dedicated modules for core services (`coreModule`), repositories (`repositoryModule`), and ViewModels (`viewModule`). - Configures a Ktor `HttpClient` with JSON serialization, logging, and timeouts. - Adds utility functions for file operations, logging (`AppLogger`), and system theme detection. - **Application Entry Point:** - Updates `main.kt` to initialize Koin, `FileKit`, set up the main application window with a minimum size, and handle theme switching.
1 parent 8564898 commit 1c557a3

37 files changed

+1862
-71
lines changed

composeApp/src/jvmMain/kotlin/com/meet/gitbackup/hub/App.kt

Lines changed: 0 additions & 49 deletions
This file was deleted.

composeApp/src/jvmMain/kotlin/com/meet/gitbackup/hub/Greeting.kt

Lines changed: 0 additions & 9 deletions
This file was deleted.

composeApp/src/jvmMain/kotlin/com/meet/gitbackup/hub/Platform.kt

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.meet.gitbackup.hub.core.utility
2+
3+
import co.touchlab.kermit.Logger
4+
import co.touchlab.kermit.StaticConfig
5+
import co.touchlab.kermit.platformLogWriter
6+
7+
object AppLogger {
8+
9+
const val isDebugBuild = true
10+
11+
val logger = Logger(
12+
config = StaticConfig(
13+
logWriterList = if (isDebugBuild) {
14+
listOf(platformLogWriter())
15+
} else {
16+
emptyList() // No logging in release
17+
}
18+
),
19+
tag = "Git-Backup-Hub"
20+
)
21+
22+
inline fun d(tag: String = "", throwable: Throwable? = null, message: () -> String) {
23+
if (isDebugBuild) {
24+
logger.d(tag = tag, throwable = throwable, message = message)
25+
}
26+
}
27+
inline fun i(tag: String = "", throwable: Throwable? = null, message: () -> String) {
28+
if (isDebugBuild) {
29+
logger.i(tag = tag, throwable = throwable, message = message)
30+
}
31+
}
32+
inline fun e(tag: String = "", throwable: Throwable? = null, message: () -> String) {
33+
if (isDebugBuild) {
34+
logger.e(tag = tag, throwable = throwable, message = message)
35+
}
36+
}
37+
inline fun w(tag: String = "", throwable: Throwable? = null, message: () -> String) {
38+
if (isDebugBuild) {
39+
logger.w(tag = tag, throwable = throwable, message = message)
40+
}
41+
}
42+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.meet.gitbackup.hub.core.utility
2+
3+
object Constant {
4+
5+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.meet.gitbackup.hub.core.utility
2+
3+
import io.ktor.client.HttpClient
4+
import io.ktor.client.engine.okhttp.OkHttp
5+
import io.ktor.client.plugins.HttpTimeout
6+
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
7+
import io.ktor.client.plugins.defaultRequest
8+
import io.ktor.client.plugins.logging.EMPTY
9+
import io.ktor.client.plugins.logging.LogLevel
10+
import io.ktor.client.plugins.logging.Logger
11+
import io.ktor.client.plugins.logging.Logging
12+
import io.ktor.http.ContentType
13+
import io.ktor.http.contentType
14+
import io.ktor.serialization.kotlinx.json.json
15+
import kotlinx.serialization.json.Json
16+
import kotlin.time.Duration.Companion.seconds
17+
18+
19+
object HttpClientFactory {
20+
fun create(): HttpClient {
21+
return HttpClient(
22+
engine = OkHttp.create()
23+
) {
24+
defaultRequest {
25+
contentType(ContentType.Application.Json)
26+
}
27+
install(HttpTimeout) {
28+
socketTimeoutMillis = 20.seconds.inWholeMilliseconds
29+
requestTimeoutMillis = 20.seconds.inWholeMilliseconds
30+
}
31+
install(ContentNegotiation) {
32+
json(Json {
33+
prettyPrint = true
34+
isLenient = true
35+
ignoreUnknownKeys = true
36+
explicitNulls = false
37+
})
38+
}
39+
install(Logging) {
40+
level = if (AppLogger.isDebugBuild) LogLevel.ALL else LogLevel.NONE
41+
logger = if (AppLogger.isDebugBuild) object : Logger {
42+
override fun log(message: String) {
43+
AppLogger.d(tag = "KtorClient", null) {
44+
message
45+
}
46+
}
47+
} else Logger.EMPTY
48+
}
49+
50+
}
51+
}
52+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.meet.gitbackup.hub.core.utility
2+
3+
import java.awt.Desktop
4+
import java.io.File
5+
6+
7+
object Utils {
8+
fun File.openFile() {
9+
if (Desktop.isDesktopSupported()) {
10+
if (isDirectory) {
11+
Desktop.getDesktop().open(this)
12+
} else {
13+
Desktop.getDesktop().open(parentFile)
14+
}
15+
}
16+
}
17+
18+
fun String.openFile() {
19+
val file = File(this)
20+
file.openFile()
21+
}
22+
fun formatMillis(millis: Long): String {
23+
val totalSeconds = millis / 1000
24+
val minutes = totalSeconds / 60
25+
val seconds = totalSeconds % 60
26+
return if (minutes > 0) "${minutes}m ${seconds}s" else "${seconds}s"
27+
}
28+
fun tagName(
29+
javaClass: Class<*>
30+
): String {
31+
return if (!javaClass.isAnonymousClass) {
32+
val name = javaClass.simpleName
33+
if (name.length <= 23) name else name.substring(0, 23) // first 23 chars
34+
} else {
35+
val name = javaClass.name
36+
if (name.length <= 23) name else name.substring(
37+
name.length - 23, name.length
38+
) // last 23 chars
39+
}
40+
}
41+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.meet.gitbackup.hub.core.utility
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.foundation.isSystemInDarkTheme
5+
import androidx.compose.runtime.DisposableEffect
6+
import androidx.compose.runtime.derivedStateOf
7+
import androidx.compose.runtime.getValue
8+
import androidx.compose.runtime.mutableStateOf
9+
import androidx.compose.runtime.remember
10+
import androidx.compose.runtime.saveable.rememberSaveable
11+
import androidx.compose.runtime.setValue
12+
import com.jthemedetecor.OsThemeDetector
13+
import java.util.function.Consumer
14+
15+
@Composable
16+
fun isSystemInDarkThemeMode(): Boolean {
17+
val detector by remember {
18+
mutableStateOf(
19+
OsThemeDetector.getDetector()
20+
)
21+
}
22+
val isSupported by remember(detector) {
23+
derivedStateOf {
24+
OsThemeDetector.isSupported()
25+
}
26+
}
27+
val isDark by remember(detector.isDark) {
28+
derivedStateOf {
29+
detector.isDark
30+
}
31+
}
32+
val isSystemThemeByFoundationLib = isSystemInDarkTheme()
33+
34+
var isSystemInDarkTheme by rememberSaveable(isDark,isSystemThemeByFoundationLib) {
35+
mutableStateOf(
36+
if (isSupported) {
37+
detector.isDark
38+
} else {
39+
isSystemThemeByFoundationLib
40+
}
41+
)
42+
}
43+
44+
DisposableEffect(isSystemInDarkTheme) {
45+
val listener = Consumer<Boolean> {
46+
if (isSupported) {
47+
isSystemInDarkTheme = it
48+
}
49+
}
50+
detector.registerListener(listener)
51+
onDispose {
52+
detector.removeListener(listener)
53+
}
54+
}
55+
return isSystemInDarkTheme
56+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.meet.gitbackup.hub.data.models
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
import kotlin.uuid.ExperimentalUuidApi
6+
import kotlin.uuid.Uuid
7+
8+
@Serializable
9+
data class GithubResponse(
10+
@SerialName("total_count")
11+
val totalCount: Int,
12+
val items: List<GitHubRepoItem>
13+
)
14+
@OptIn(ExperimentalUuidApi::class)
15+
@Serializable
16+
data class GitHubRepoItem(
17+
val uniqueId : String = Uuid.random().toString(),
18+
val name: String,
19+
@SerialName("clone_url")
20+
val cloneUrl: String,
21+
val description: String? = null,
22+
@SerialName("default_branch")
23+
val defaultBranch: String = "main",
24+
val private: Boolean = false,
25+
@SerialName("stargazers_count")
26+
val stargazersCount: Int = 0,
27+
val language: String? = null,
28+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.meet.gitbackup.hub.data.repository
2+
3+
import com.meet.gitbackup.hub.data.models.GitHubRepoItem
4+
5+
interface GithubRepository {
6+
suspend fun getRepositories(username: String, token: String?): List<GitHubRepoItem>
7+
8+
suspend fun cloneRepository(
9+
repo: GitHubRepoItem, basePath: String, token: String?,
10+
onProgress: (Int, String) -> Unit
11+
)
12+
}
13+

0 commit comments

Comments
 (0)