diff --git a/build.gradle.kts b/build.gradle.kts index 062e3ee..51fe94a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,6 +11,7 @@ plugins { id("org.jetbrains.compose") version "1.1.1" } +// Remember to update Constants.APP_VERSION when changing this version val projectVersion = "0.1.0" val projectName = "Gitnuro" @@ -23,8 +24,6 @@ repositories { maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } - - dependencies { implementation(compose.desktop.currentOs) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) @@ -37,6 +36,8 @@ dependencies { testImplementation(platform("org.junit:junit-bom:5.8.2")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("io.mockk:mockk:1.12.3") + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-scalars:2.9.0") } tasks.test { diff --git a/src/main/kotlin/app/App.kt b/src/main/kotlin/app/App.kt index f5ed665..5d77241 100644 --- a/src/main/kotlin/app/App.kt +++ b/src/main/kotlin/app/App.kt @@ -48,7 +48,6 @@ class App { init { appComponent.inject(this) - println("AppStateManagerReference $appStateManager") } private val tabsFlow = MutableStateFlow>(emptyList()) diff --git a/src/main/kotlin/app/AppConstants.kt b/src/main/kotlin/app/AppConstants.kt new file mode 100644 index 0000000..95ac6a8 --- /dev/null +++ b/src/main/kotlin/app/AppConstants.kt @@ -0,0 +1,8 @@ +package app + +object AppConstants { + // Remember to update build.gradle when changing this + const val APP_VERSION = "0.1.0" + const val APP_VERSION_CODE = 1 + const val VERSION_CHECK_URL = "https://raw.githubusercontent.com/JetpackDuba/Gitnuro/main/latest.json" +} \ No newline at end of file diff --git a/src/main/kotlin/app/di/TabComponent.kt b/src/main/kotlin/app/di/TabComponent.kt index 7bb798d..99f11ae 100644 --- a/src/main/kotlin/app/di/TabComponent.kt +++ b/src/main/kotlin/app/di/TabComponent.kt @@ -1,10 +1,18 @@ package app.di +import app.di.modules.NetworkModule import app.ui.components.TabInformation import dagger.Component @TabScope -@Component(dependencies = [AppComponent::class]) +@Component( + modules = [ + NetworkModule::class, + ], + dependencies = [ + AppComponent::class + ], +) interface TabComponent { fun inject(tabInformation: TabInformation) } \ No newline at end of file diff --git a/src/main/kotlin/app/di/modules/NetworkModule.kt b/src/main/kotlin/app/di/modules/NetworkModule.kt new file mode 100644 index 0000000..b6aa01f --- /dev/null +++ b/src/main/kotlin/app/di/modules/NetworkModule.kt @@ -0,0 +1,20 @@ +package app.di.modules + +import app.updates.UpdatesService +import dagger.Provides +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.scalars.ScalarsConverterFactory +import javax.inject.Singleton + +@dagger.Module +class NetworkModule { + @Provides + fun provideWebService(): UpdatesService { + return Retrofit.Builder() + .baseUrl("https://github.com") + .addConverterFactory(ScalarsConverterFactory.create()) + .build() + .create(UpdatesService::class.java) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/ui/WelcomePage.kt b/src/main/kotlin/app/ui/WelcomePage.kt index 3113097..133ca74 100644 --- a/src/main/kotlin/app/ui/WelcomePage.kt +++ b/src/main/kotlin/app/ui/WelcomePage.kt @@ -13,16 +13,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.BiasAlignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import app.AppConstants import app.extensions.dirName import app.extensions.dirPath import app.theme.primaryTextColor import app.theme.secondaryTextColor import app.ui.dialogs.CloneDialog +import app.updates.Update import app.viewmodels.TabViewModel import openDirectoryDialog import openRepositoryDialog @@ -37,148 +40,165 @@ fun WelcomePage( ) { val appStateManager = tabViewModel.appStateManager var showCloneView by remember { mutableStateOf(false) } + var newUpdate by remember { mutableStateOf(null) } -// Crossfade(showCloneView) { -// if(it) { + LaunchedEffect(Unit) { + val latestRelease = tabViewModel.latestRelease() + if(latestRelease != null && latestRelease.appCode > AppConstants.APP_VERSION_CODE) { + newUpdate = latestRelease + } + } -// } else { - Row( + Box( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colors.background), - horizontalArrangement = Arrangement.Center, - verticalAlignment = BiasAlignment.Vertical(-0.5f), - + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Top, + modifier = Modifier.align(BiasAlignment(0f, -0.5f)) ) { - Column( - modifier = Modifier - .padding(end = 32.dp), - verticalArrangement = Arrangement.Center, - ) { - Text( - text = "Gitnuro", - fontSize = 32.sp, - color = MaterialTheme.colors.primaryTextColor, - modifier = Modifier - .padding(bottom = 16.dp), - ) + Column( + modifier = Modifier.padding(end = 32.dp), + ) { + Text( + text = "Gitnuro", + fontSize = 32.sp, + color = MaterialTheme.colors.primaryTextColor, + modifier = Modifier.padding(bottom = 16.dp), + ) - ButtonTile( - modifier = Modifier - .padding(bottom = 8.dp), - title = "Open a repository", - painter = painterResource("open.svg"), - onClick = { openRepositoryDialog(tabViewModel) } - ) + ButtonTile( + modifier = Modifier.padding(bottom = 8.dp), + title = "Open a repository", + painter = painterResource("open.svg"), + onClick = { openRepositoryDialog(tabViewModel) }) - ButtonTile( - modifier = Modifier - .padding(bottom = 8.dp), - title = "Clone a repository", - painter = painterResource("download.svg"), - onClick = { - showCloneView = true + ButtonTile( + modifier = Modifier.padding(bottom = 8.dp), + title = "Clone a repository", + painter = painterResource("download.svg"), + onClick = { + showCloneView = true + } + ) + + ButtonTile( + modifier = Modifier.padding(bottom = 8.dp), + title = "Start a local repository", + painter = painterResource("open.svg"), + onClick = { + val dir = openDirectoryDialog() + if (dir != null) tabViewModel.initLocalRepository(dir) + } + ) + + Text( + text = "About Gitnuro", + fontSize = 18.sp, + color = MaterialTheme.colors.primaryTextColor, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), + ) + + IconTextButton( + title = "Source code", + painter = painterResource("code.svg"), + onClick = { + Desktop.getDesktop().browse(URI("https://github.com/JetpackDuba/Gitnuro")) + } + ) + + IconTextButton( + title = "Report a bug", + painter = painterResource("bug.svg"), + onClick = { + Desktop.getDesktop().browse(URI("https://github.com/JetpackDuba/Gitnuro/issues")) + } + ) + + if(newUpdate != null) { + IconTextButton( + title = "New update ${newUpdate?.appVersion} available ", + painter = painterResource("grade.svg"), + iconColor = MaterialTheme.colors.secondary, + onClick = { + newUpdate?.downloadUrl?.let { + Desktop.getDesktop().browse(URI(it)) + } + } + ) } - ) + } - ButtonTile( + Column( modifier = Modifier - .padding(bottom = 8.dp), - title = "Start a local repository", - painter = painterResource("open.svg"), - onClick = { - val dir = openDirectoryDialog() - if (dir != null) - tabViewModel.initLocalRepository(dir) - } - ) + .padding(start = 32.dp), + ) { + Text( + text = "Recent", + fontSize = 18.sp, + modifier = Modifier.padding(top = 48.dp, bottom = 8.dp), + color = MaterialTheme.colors.primaryTextColor, + ) + LazyColumn { + items(items = appStateManager.latestOpenedRepositoriesPaths) { repo -> + val repoDirName = repo.dirName + val repoDirPath = repo.dirPath - Text( - text = "About Gitnuro", - fontSize = 18.sp, - color = MaterialTheme.colors.primaryTextColor, - modifier = Modifier - .padding(top = 16.dp, bottom = 8.dp), - ) - - IconTextButton( - title = "Source code", - painter = painterResource("code.svg"), - onClick = { - Desktop.getDesktop().browse(URI("https://github.com/JetpackDuba/Gitnuro")) - } - ) - - IconTextButton( - title = "Report a bug", - painter = painterResource("bug.svg"), - onClick = { - Desktop.getDesktop().browse(URI("https://github.com/JetpackDuba/Gitnuro/issues")) - } - ) - } - - Column( - modifier = Modifier - .padding(start = 32.dp), - ) { - Text( - text = "Recent", - fontSize = 18.sp, - modifier = Modifier - .padding(top = 16.dp, bottom = 8.dp), - color = MaterialTheme.colors.primaryTextColor, - ) - LazyColumn { - items(items = appStateManager.latestOpenedRepositoriesPaths) { repo -> - val repoDirName = repo.dirName - val repoDirPath = repo.dirPath - - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - TextButton( - onClick = { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton( + onClick = { tabViewModel.openRepository(repo) } - ) { + ) { + Text( + text = repoDirName, + fontSize = 14.sp, + color = MaterialTheme.colors.primary, + ) + } + Text( - text = repoDirName, + text = repoDirPath, fontSize = 14.sp, - color = MaterialTheme.colors.primary, + modifier = Modifier.padding(start = 4.dp), + color = MaterialTheme.colors.secondaryTextColor ) } - - Text( - text = repoDirPath, - fontSize = 14.sp, - modifier = Modifier.padding(start = 4.dp), - color = MaterialTheme.colors.secondaryTextColor - ) } } } } + + Text( + "Version ${AppConstants.APP_VERSION}", + color = MaterialTheme.colors.primaryTextColor, + fontSize = 12.sp, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(bottom = 16.dp, end = 16.dp) + ) } LaunchedEffect(showCloneView) { - if (showCloneView) - tabViewModel.cloneViewModel.reset() // Reset dialog before showing it + if (showCloneView) tabViewModel.cloneViewModel.reset() // Reset dialog before showing it } - if (showCloneView) - CloneDialog( - tabViewModel.cloneViewModel, - onClose = { - showCloneView = false - tabViewModel.cloneViewModel.reset() - }, - onOpenRepository = { dir -> - tabViewModel.openRepository(dir) - }, - ) + if (showCloneView) CloneDialog( + tabViewModel.cloneViewModel, + onClose = { + showCloneView = false + tabViewModel.cloneViewModel.reset() + }, + onOpenRepository = { dir -> + tabViewModel.openRepository(dir) + }, + ) } @Composable @@ -217,6 +237,7 @@ fun IconTextButton( modifier: Modifier = Modifier, title: String, painter: Painter, + iconColor: Color = MaterialTheme.colors.primary, onClick: () -> Unit, ) { TextButton( @@ -224,17 +245,14 @@ fun IconTextButton( modifier = modifier.size(width = 280.dp, height = 40.dp) ) { Row( - modifier = Modifier - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically, ) { Image( - modifier = Modifier - .padding(end = 8.dp) - .size(24.dp), + modifier = Modifier.padding(end = 8.dp).size(24.dp), painter = painter, contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colors.primary), + colorFilter = ColorFilter.tint(iconColor), ) Text( diff --git a/src/main/kotlin/app/updates/Update.kt b/src/main/kotlin/app/updates/Update.kt new file mode 100644 index 0000000..bb867f5 --- /dev/null +++ b/src/main/kotlin/app/updates/Update.kt @@ -0,0 +1,10 @@ +package app.updates + +import kotlinx.serialization.Serializable + +@Serializable +data class Update( + val appVersion: String, + val appCode: Int, + val downloadUrl: String, +) diff --git a/src/main/kotlin/app/updates/UpdatesRepository.kt b/src/main/kotlin/app/updates/UpdatesRepository.kt new file mode 100644 index 0000000..a4ba301 --- /dev/null +++ b/src/main/kotlin/app/updates/UpdatesRepository.kt @@ -0,0 +1,23 @@ +package app.updates + +import app.AppConstants +import app.AppPreferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import javax.inject.Inject + +private val updateJson = Json { + this.ignoreUnknownKeys = true +} + +class UpdatesRepository @Inject constructor( + private val updatesWebService: UpdatesService, +) { + suspend fun latestRelease(): Update? = withContext(Dispatchers.IO) { + val latestReleaseJson = updatesWebService.release(AppConstants.VERSION_CHECK_URL) + + updateJson.decodeFromString(latestReleaseJson) + } +} diff --git a/src/main/kotlin/app/updates/UpdatesWebService.kt b/src/main/kotlin/app/updates/UpdatesWebService.kt new file mode 100644 index 0000000..a0be010 --- /dev/null +++ b/src/main/kotlin/app/updates/UpdatesWebService.kt @@ -0,0 +1,9 @@ +package app.updates + +import retrofit2.http.GET +import retrofit2.http.Url + +interface UpdatesService { + @GET + suspend fun release(@Url url: String): String +} \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/TabViewModel.kt b/src/main/kotlin/app/viewmodels/TabViewModel.kt index 98961f9..f05b204 100644 --- a/src/main/kotlin/app/viewmodels/TabViewModel.kt +++ b/src/main/kotlin/app/viewmodels/TabViewModel.kt @@ -1,5 +1,6 @@ package app.viewmodels +import app.AppPreferences import app.AppStateManager import app.ErrorsManager import app.credentials.CredentialsState @@ -7,6 +8,8 @@ import app.credentials.CredentialsStateManager import app.git.* import app.newErrorNow import app.ui.SelectedItem +import app.updates.Update +import app.updates.UpdatesRepository import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -39,6 +42,7 @@ class TabViewModel @Inject constructor( private val tabState: TabState, val appStateManager: AppStateManager, private val fileChangesWatcher: FileChangesWatcher, + private val updatesRepository: UpdatesRepository, ) { val errorsManager: ErrorsManager = tabState.errorsManager val selectedItem: StateFlow = tabState.selectedItem @@ -262,6 +266,15 @@ class TabViewModel @Inject constructor( repositoryManager.initLocalRepo(repoDir) openRepository(repoDir) } + + suspend fun latestRelease(): Update? = withContext(Dispatchers.IO) { + try { + updatesRepository.latestRelease() + } catch (ex: Exception) { + ex.printStackTrace() + null + } + } } diff --git a/src/main/resources/grade.svg b/src/main/resources/grade.svg new file mode 100644 index 0000000..8788183 --- /dev/null +++ b/src/main/resources/grade.svg @@ -0,0 +1 @@ + \ No newline at end of file