diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index 3631d539..f5c6207d 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -11,6 +11,7 @@ import androidx.room.Upsert import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import io.github.wiiznokes.gitnote.data.platform.NodeFs +import io.github.wiiznokes.gitnote.manager.Progress import io.github.wiiznokes.gitnote.ui.model.GridNote import io.github.wiiznokes.gitnote.ui.model.SortOrder import io.github.wiiznokes.gitnote.ui.screen.app.DrawerFolderModel @@ -29,7 +30,7 @@ interface RepoDatabaseDao { // todo: use @Transaction // todo: don't clear the all database each time - suspend fun clearAndInit(rootPath: String, timestamps: HashMap) { + suspend fun clearAndInit(rootPath: String, timestamps: HashMap, progressCb: ((Progress) -> Unit)? = null) { Log.d(TAG, "clearAndInit") clearDatabase() @@ -83,6 +84,7 @@ interface RepoDatabaseDao { ) //Log.d(TAG, "add noteFolder: $noteFolder") insertNoteFolder(noteFolder) + progressCb?.invoke(Progress.GeneratingDatabase(noteFolder.relativePath)) initRec(nodeFs) } } diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/manager/GitManager.kt b/app/src/main/java/io/github/wiiznokes/gitnote/manager/GitManager.kt index b346628d..37719c95 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/manager/GitManager.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/manager/GitManager.kt @@ -50,7 +50,8 @@ class GitManager { private val uiHelper = MyApp.appModule.uiHelper private val locker = Mutex() - private var isRepoInitialized = false + var isRepoInitialized = false + private set private var isLibInitialized = false private suspend fun safelyAccessLibGit2(f: suspend () -> T): Result = locker.withLock { @@ -95,23 +96,21 @@ class GitManager { isRepoInitialized = true } - private var actualCb: ((Int) -> Unit)? = null + private var actualCb: ((Int) -> Boolean)? = null /** * This function is called from native code */ @Keep - fun progressCb(progress: Int) { - if (actualCb != null) { - actualCb?.invoke(progress) - } + fun progressCb(progress: Int): Boolean { + return actualCb?.invoke(progress) != false } suspend fun cloneRepo( repoPath: String, repoUrl: String, cred: Cred?, - progressCallback: (Int) -> Unit + progressCallback: (Int) -> Boolean ): Result = safelyAccessLibGit2 { Log.d(TAG, "clone repo: $repoPath, $repoUrl, $cred") diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/manager/StorageManager.kt b/app/src/main/java/io/github/wiiznokes/gitnote/manager/StorageManager.kt index ce847a3e..95bd3a59 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/manager/StorageManager.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/manager/StorageManager.kt @@ -31,6 +31,12 @@ sealed interface SyncState { } } +sealed class Progress { + data object Timestamps: Progress() + + data class GeneratingDatabase(val path: String): Progress() +} + class StorageManager { @@ -95,7 +101,7 @@ class StorageManager { * The caller must ensure that all files has been committed * to keep the database in sync with the remote repo */ - private suspend fun updateDatabaseWithoutLocker(force: Boolean = false): Result { + private suspend fun updateDatabaseWithoutLocker(force: Boolean = false, progressCb: ((Progress) -> Unit)? = null): Result { val fsCommit = gitManager.lastCommit() val databaseCommit = prefs.databaseCommit.get() @@ -109,9 +115,10 @@ class StorageManager { val repoPath = prefs.repoPath() Log.d(TAG, "repoPath = $repoPath") + progressCb?.invoke(Progress.Timestamps) val timestamps = gitManager.getTimestamps().getOrThrow() - dao.clearAndInit(repoPath, timestamps) + dao.clearAndInit(repoPath, timestamps, progressCb) prefs.databaseCommit.update(fsCommit) return success(Unit) @@ -120,8 +127,8 @@ class StorageManager { /** * See the documentation of [updateDatabaseWithoutLocker] */ - suspend fun updateDatabase(force: Boolean = false): Result = locker.withLock { - updateDatabaseWithoutLocker(force) + suspend fun updateDatabase(force: Boolean = false, progressCb: ((Progress) -> Unit)? = null): Result = locker.withLock { + updateDatabaseWithoutLocker(force, progressCb) } /** diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/component/SetupPage.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/component/SetupPage.kt index dfdca4fc..90619a0b 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/component/SetupPage.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/component/SetupPage.kt @@ -71,12 +71,13 @@ private fun SetupTitle( @Composable fun NextButton( + modifier: Modifier = Modifier, text: String, onClick: () -> Unit, enabled: Boolean = true, ) { Button( - modifier = Modifier + modifier = modifier .fillMaxWidth(), onClick = onClick, enabled = enabled diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/destination/RemoteDestination.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/destination/RemoteDestination.kt index 70816d3f..183028d1 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/destination/RemoteDestination.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/destination/RemoteDestination.kt @@ -35,6 +35,8 @@ sealed interface RemoteDestination : Parcelable { val url: String, ) : RemoteDestination + @Parcelize + data object Cloning : RemoteDestination // @Parcelize // data class LoadKeysFromDevice( // val provider: Provider?, diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/DrawerScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/DrawerScreen.kt index 63753d33..960ae700 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/DrawerScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/DrawerScreen.kt @@ -86,17 +86,12 @@ fun DrawerScreen( missingDelimiterValue = "" ) - if (drawerState.isOpen) { - + val scope = rememberCoroutineScope() + BackHandler(enabled = drawerState.isOpen) { if (currentNoteFolderRelativePath.isEmpty()) { - val scope = rememberCoroutineScope() - BackHandler { - scope.launch { drawerState.close() } - } + scope.launch { drawerState.close() } } else { - BackHandler { - vm.openFolder(getParent(currentNoteFolderRelativePath)) - } + vm.openFolder(getParent(currentNoteFolderRelativePath)) } } diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/AuthorizeGitNoteScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/AuthorizeGitNoteScreen.kt index 24e5fb4a..e8e76310 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/AuthorizeGitNoteScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/AuthorizeGitNoteScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.tooling.preview.Preview import io.github.wiiznokes.gitnote.R import io.github.wiiznokes.gitnote.ui.component.AppPage import io.github.wiiznokes.gitnote.ui.viewmodel.InitState -import io.github.wiiznokes.gitnote.ui.viewmodel.InitState.AuthState private const val TAG = "AuthorizeGitNoteScreen" @@ -32,12 +31,12 @@ fun AuthorizeGitNoteScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, onBackClick = onBackClick, - onBackClickEnabled = authState.isClickable() + onBackClickEnabled = !authState.isLoading() ) { LaunchedEffect(authState) { Log.d(TAG, "LaunchedEffect: $authState, hash=${vmHashCode}") - if (authState is AuthState.Success) { + if (authState is InitState.AuthentificationSuccess) { onSuccess() } } @@ -49,7 +48,7 @@ fun AuthorizeGitNoteScreen( val intent = getLaunchOAuthScreenIntent() ctx.startActivity(intent) }, - enabled = authState.isClickable() && authState != AuthState.Success + enabled = !authState.isLoading() && authState != InitState.AuthentificationSuccess ) { if (!authState.isLoading()) { Text(text = stringResource(R.string.authorize_gitnote)) @@ -66,7 +65,7 @@ private fun AuthorizeGitNoteScreenPreview() { AuthorizeGitNoteScreen( onBackClick = {}, - authState = AuthState.Idle, + authState = InitState.Idle, onSuccess = {}, getLaunchOAuthScreenIntent = { Intent() diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/CloningScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/CloningScreen.kt new file mode 100644 index 00000000..52088434 --- /dev/null +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/CloningScreen.kt @@ -0,0 +1,51 @@ +package io.github.wiiznokes.gitnote.ui.screen.setup.remote + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.wiiznokes.gitnote.ui.component.AppPage +import io.github.wiiznokes.gitnote.ui.viewmodel.InitState + +private const val TAG = "CloningScreen" + + +@Composable +fun CloningScreen( + cloneState: InitState, + onCancel: () -> Unit, +) { + AppPage( + title = "Clone", + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + onBackClickEnabled = !cloneState.isLoading() + ) { + Text(text = cloneState.message()) + + Spacer(Modifier.height(20.dp)) + + Button( + onClick = onCancel, + enabled = cloneState !is InitState.CalculatingTimestamps && cloneState !is InitState.GeneratingDatabase + ) { + Text("Cancel") + } + } +} + +@Preview +@Composable +private fun PickRepoScreenPreview() { + + CloningScreen( + cloneState = InitState.Cloning(50), + onCancel = {}, + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/GenerateNewKeysScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/GenerateNewKeysScreen.kt index 93b0cb68..26fe5683 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/GenerateNewKeysScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/GenerateNewKeysScreen.kt @@ -53,6 +53,7 @@ fun GenerateNewKeysScreen( url: String, vm: SetupViewModelI, generateSshKeys: () -> Pair, + onClone: () -> Unit, onSuccess: () -> Unit, ) { @@ -61,7 +62,7 @@ fun GenerateNewKeysScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, onBackClick = onBackClick, - onBackClickEnabled = cloneState.isClickable() + onBackClickEnabled = !cloneState.isLoading() ) { val publicKey = rememberSaveable { mutableStateOf("") } @@ -161,9 +162,7 @@ fun GenerateNewKeysScreen( ) { SetupButton( - text = if (cloneState.isLoading()) { - cloneState.message() - } else stringResource(R.string.clone_repo), + text = stringResource(R.string.clone_repo), onClick = { vm.cloneRepo( storageConfig = storageConfig, @@ -174,8 +173,9 @@ fun GenerateNewKeysScreen( ), onSuccess = onSuccess ) + + onClone() }, - enabled = cloneState.isClickable() ) } } @@ -187,13 +187,14 @@ fun GenerateNewKeysScreen( private fun GenerateNewKeysScreenPreview() { GenerateNewKeysScreen( onBackClick = {}, - cloneState = InitState.CloneState.Idle, + cloneState = InitState.Idle, provider = GithubProvider(), storageConfig = StorageConfiguration.App, url = "url", vm = SetupViewModelMock(), generateSshKeys = { "aaaaaaaaaaaabbbbbbbbbbbbb" to "aaaaaaaaaaaabbbbbbbbbbbbb" }, - onSuccess = {} + onSuccess = {}, + onClone = {} ) } \ No newline at end of file diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/PickRepoScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/PickRepoScreen.kt index ba0132b9..8813761b 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/PickRepoScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/PickRepoScreen.kt @@ -2,9 +2,12 @@ package io.github.wiiznokes.gitnote.ui.screen.setup.remote import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -58,6 +61,7 @@ fun PickRepoScreen( userInfo: UserInfo, repos: List, storageConfig: StorageConfiguration, + onClone: () -> Unit, onSuccess: () -> Unit ) { @@ -66,7 +70,7 @@ fun PickRepoScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, onBackClick = onBackClick, - onBackClickEnabled = authStep2State.isClickable(), + onBackClickEnabled = !authStep2State.isLoading(), disableVerticalScroll = true ) { @@ -117,12 +121,14 @@ fun PickRepoScreen( singleLine = true, ) + Spacer(Modifier.height(15.dp)) val showCreate = nameText.isNotEmpty() && !repos.contains { it.name == nameText } LazyColumn( modifier = Modifier.weight(1f), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(3.dp) ) { if (showCreate) { @@ -205,13 +211,9 @@ fun PickRepoScreen( } - NextButton( - text = if (!authStep2State.isLoading()) { - stringResource(R.string.next) - } else { - authStep2State.message() - }, + modifier = Modifier.padding(vertical = 10.dp), + text = stringResource(R.string.next), onClick = { val selected = selected.value @@ -233,8 +235,9 @@ fun PickRepoScreen( ) } + onClone() }, - enabled = selected.value !is Selected.None && authStep2State.isClickable(), + enabled = selected.value !is Selected.None, ) } } @@ -246,7 +249,7 @@ fun PickRepoScreen( private fun PickRepoScreenPreview() { PickRepoScreen( onBackClick = {}, - authStep2State = InitState.AuthStep2.Idle, + authStep2State = InitState.Idle, vm = SetupViewModelMock(), userInfo = UserInfo(username = "", name = "", email = ""), repos = listOf( @@ -260,8 +263,19 @@ private fun PickRepoScreenPreview() { RepoInfo(name = "repoName8", owner = "wiiz", url = "repoName1", 7), RepoInfo(name = "repoName9", owner = "wiiz", url = "repoName1", 8), RepoInfo(name = "repoName10", owner = "wiiz", url = "repoName1", 9), + RepoInfo(name = "repoName1", owner = "wiiz", url = "repoName1", 0), + RepoInfo(name = "repoName2", owner = "wiiz", url = "repoName1", 1), + RepoInfo(name = "repoName3", owner = "wiiz", url = "repoName1", 2), + RepoInfo(name = "repoName4", owner = "wiiz", url = "repoName1", 3), + RepoInfo(name = "repoName5", owner = "wiiz", url = "repoName1", 4), + RepoInfo(name = "repoName6", owner = "wiiz", url = "repoName1", 5), + RepoInfo(name = "repoName7", owner = "wiiz", url = "repoName1", 6), + RepoInfo(name = "repoName8", owner = "wiiz", url = "repoName1", 7), + RepoInfo(name = "repoName9", owner = "wiiz", url = "repoName1", 8), + RepoInfo(name = "repoName10", owner = "wiiz", url = "repoName1", 9), ), storageConfig = StorageConfiguration.App, - onSuccess = {} + onSuccess = {}, + onClone = {} ) } \ No newline at end of file diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/RemoteNav.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/RemoteNav.kt index 58d2a92f..0ccad738 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/RemoteNav.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/RemoteNav.kt @@ -1,5 +1,6 @@ package io.github.wiiznokes.gitnote.ui.screen.setup.remote +import androidx.activity.compose.BackHandler import androidx.compose.animation.ContentTransform import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -42,6 +43,13 @@ fun RemoteScreen( NavBackHandler(navController) + val initState = vm.initState.collectAsState().value + + BackHandler( + enabled = initState.isLoading() + ) { + // do nothing + } AnimatedNavHost( controller = navController, @@ -69,8 +77,11 @@ fun RemoteScreen( is AuthorizeGitNote -> AuthorizeGitNoteScreen( onBackClick = { navController.pop() }, - authState = vm.initState.collectAsState().value, - onSuccess = { navController.navigate(PickRepo) }, + authState = initState, + onSuccess = { + navController.navigate(PickRepo) + vm.setStateToIdle() + }, getLaunchOAuthScreenIntent = { vm.getLaunchOAuthScreenIntent() }, vmHashCode = vm.hashCode() ) @@ -102,12 +113,13 @@ fun RemoteScreen( is PickRepo -> PickRepoScreen( onBackClick = { navController.pop() }, - authStep2State = vm.initState.collectAsState().value, + authStep2State = initState, vm = vm, userInfo = vm.userInfo, repos = vm.repos, storageConfig = storageConfig, onSuccess = onInitSuccess, + onClone = { navController.navigate(RemoteDestination.Cloning) } ) is SelectGenerateNewKeys -> SelectGenerateNewKeysScreen( @@ -123,13 +135,22 @@ fun RemoteScreen( is GenerateNewKeys -> GenerateNewKeysScreen( onBackClick = { navController.pop() }, - cloneState = vm.initState.collectAsState().value, + cloneState = initState, provider = vm.provider, storageConfig = storageConfig, url = remoteDestination.url, vm = vm, generateSshKeys = ::generateSshKeysLib, onSuccess = onInitSuccess, + onClone = { navController.navigate(RemoteDestination.Cloning) } + ) + + RemoteDestination.Cloning -> CloningScreen( + cloneState = initState, + onCancel = { + if (vm.cancelClone()) + navController.pop() + } ) } } diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/SetupViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/SetupViewModel.kt index c294cec7..f4f16d8a 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/SetupViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/SetupViewModel.kt @@ -11,6 +11,7 @@ import io.github.wiiznokes.gitnote.R import io.github.wiiznokes.gitnote.data.AppPreferences import io.github.wiiznokes.gitnote.data.platform.NodeFs import io.github.wiiznokes.gitnote.helper.UiHelper +import io.github.wiiznokes.gitnote.manager.Progress import io.github.wiiznokes.gitnote.manager.generateSshKeysLib import io.github.wiiznokes.gitnote.provider.GithubProvider import io.github.wiiznokes.gitnote.provider.Provider @@ -19,9 +20,6 @@ import io.github.wiiznokes.gitnote.provider.RepoInfo import io.github.wiiznokes.gitnote.provider.UserInfo import io.github.wiiznokes.gitnote.ui.model.Cred import io.github.wiiznokes.gitnote.ui.model.StorageConfiguration -import io.github.wiiznokes.gitnote.ui.viewmodel.InitState.AuthState -import io.github.wiiznokes.gitnote.ui.viewmodel.InitState.AuthStep2 -import io.github.wiiznokes.gitnote.ui.viewmodel.InitState.CloneState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -99,6 +97,21 @@ class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewM folder.create() } + private var shouldCancel = false + fun cancelClone(): Boolean { + if (gitManager.isRepoInitialized) { + return false + } + shouldCancel = true + return true + } + + fun setStateToIdle() { + viewModelScope.launch { + _initState.emit(InitState.Idle) + } + } + fun createLocalRepo(storageConfig: StorageConfiguration, onSuccess: () -> Unit) { CoroutineScope(Dispatchers.IO).launch { @@ -163,6 +176,12 @@ class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewM viewModelScope.launch { f() } } + private fun runCloneJob(f: suspend () -> Unit) { + CoroutineScope(Dispatchers.IO).launch { + f() + } + } + override fun cloneRepo( storageConfig: StorageConfiguration, remoteUrl: String, @@ -170,35 +189,62 @@ class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewM onSuccess: () -> Unit ) { - CoroutineScope(Dispatchers.IO).launch { - if (storageConfig is StorageConfiguration.App) { - prepareLocalStorageRepoPath() - } + runCloneJob { + cloneRepoInternal( + storageConfig = storageConfig, + remoteUrl = remoteUrl, + cred = cred, + onSuccess = onSuccess + ) + } + } - _initState.emit(CloneState.Cloning(0)) + suspend fun cloneRepoInternal( + storageConfig: StorageConfiguration, + remoteUrl: String, + cred: Cred?, + onSuccess: () -> Unit + ) { + shouldCancel = false - gitManager.cloneRepo( - repoPath = storageConfig.repoPath(), - repoUrl = remoteUrl, - cred = cred, - progressCallback = { - _initState.tryEmit(CloneState.Cloning(it)) - } - ).onFailure { - uiHelper.makeToast(it.message) - _initState.emit(CloneState.Error) - return@launch + if (storageConfig is StorageConfiguration.App) { + prepareLocalStorageRepoPath() + } + + _initState.emit(InitState.Cloning(0)) + + gitManager.cloneRepo( + repoPath = storageConfig.repoPath(), + repoUrl = remoteUrl, + cred = cred, + progressCallback = { + _initState.tryEmit(InitState.Cloning(it)) + !shouldCancel } + ).onFailure { + _initState.emit(InitState.Error(if (shouldCancel) "Clone canceled" else it.message)) + return + } + if (shouldCancel) return - prefs.initRepo(storageConfig) - prefs.remoteUrl.update(remoteUrl) + prefs.initRepo(storageConfig) + prefs.remoteUrl.update(remoteUrl) - prefs.updateCred(cred) + prefs.updateCred(cred) - storageManager.updateDatabase() + storageManager.updateDatabase( + progressCb = { + viewModelScope.launch { + _initState.emit(when (it) { + is Progress.GeneratingDatabase -> InitState.GeneratingDatabase(it.path) + Progress.Timestamps -> InitState.CalculatingTimestamps + }) + } + } + ) + + onSuccess() - onSuccess() - } } @@ -219,36 +265,36 @@ class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewM CoroutineScope(Dispatchers.IO).launch { - _initState.emit(AuthState.GetAccessToken) + _initState.emit(InitState.GettingAccessToken) token = try { provider!!.exchangeCodeForAccessToken(code) } catch (e: Exception) { Log.e(TAG, "exchangeCodeForAccessToken: ${e.message}, $e") - _initState.emit(AuthState.Error) + _initState.emit(InitState.Error(e.message)) return@launch } - _initState.emit(AuthState.FetchRepos) + _initState.emit(InitState.FetchingRepos) repos = try { provider!!.fetchUserRepos(token = token) } catch (e: Exception) { Log.e(TAG, "fetchUserRepos: ${e.message}, $e") - _initState.emit(AuthState.Error) + _initState.emit(InitState.Error(e.message)) return@launch } - _initState.emit(AuthState.GetUserInfo) + _initState.emit(InitState.GettingUserInfo) userInfo = try { provider!!.getUserInfo(token = token) } catch (e: Exception) { Log.e(TAG, "getUserInfo: ${e.message}, $e") - _initState.emit(AuthState.Error) + _initState.emit(InitState.Error(e.message)) return@launch } Log.d(TAG, "emit: Success") - _initState.emit(AuthState.Success) + _initState.emit(InitState.AuthentificationSuccess) } } @@ -258,10 +304,10 @@ class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewM onSuccess: () -> Unit ) { - CoroutineScope(Dispatchers.IO).launch { + runCloneJob { val (publicKey, privateKey) = generateSshKeysLib() - _initState.emit(AuthStep2.AddDeployKey) + _initState.emit(InitState.AddingDeployKey) try { provider!!.addDeployKeyToRepo( token = token, @@ -270,11 +316,11 @@ class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewM ) } catch (e: Exception) { Log.e(TAG, "addDeployKey: ${e.message}, $e") - _initState.emit(AuthStep2.Error) - return@launch + _initState.emit(InitState.Error(e.message)) + return@runCloneJob } - cloneRepo( + cloneRepoInternal( storageConfig = storageConfig, remoteUrl = provider!!.sshCloneUrlFromRepoName(repoName), cred = Cred.Ssh( @@ -294,9 +340,9 @@ class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewM onSuccess: () -> Unit ) { - CoroutineScope(Dispatchers.IO).launch { + runCloneJob { - _initState.emit(AuthStep2.CreateRepo) + _initState.emit(InitState.CreatingRemoteRepo) try { provider!!.createNewRepo( token = token, @@ -304,13 +350,13 @@ class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewM ) } catch (e: Exception) { Log.e(TAG, "createNewRepoOnRemoteGithub: ${e.message}, $e") - _initState.emit(AuthStep2.Error) - return@launch + _initState.emit(InitState.Error(e.message)) + return@runCloneJob } val (publicKey, privateKey) = generateSshKeysLib() - _initState.emit(AuthStep2.AddDeployKey) + _initState.emit(InitState.AddingDeployKey) try { provider!!.addDeployKeyToRepo( token = token, @@ -319,11 +365,11 @@ class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewM ) } catch (e: Exception) { Log.e(TAG, "addDeployKey: ${e.message}, $e") - _initState.emit(AuthStep2.Error) - return@launch + _initState.emit(InitState.Error(e.message)) + return@runCloneJob } - cloneRepo( + cloneRepoInternal( storageConfig = storageConfig, remoteUrl = provider!!.sshCloneUrlFromRepoName(repoName), cred = Cred.Ssh( @@ -341,72 +387,39 @@ class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewM sealed class InitState { - data object Idle : InitState() - - open fun isClickable(): Boolean = true - open fun isLoading(): Boolean = false - open fun message(): String = "" - - sealed class AuthState : InitState() { - data object Idle : AuthState() - data object GetAccessToken : AuthState() - data object FetchRepos : AuthState() - data object GetUserInfo : AuthState() - data object Success : AuthState() - data object Error : AuthState() - - override fun isClickable(): Boolean = this is Idle || this is Error || this is Success - override fun isLoading(): Boolean = - this is GetAccessToken || this is FetchRepos || this is GetUserInfo - - override fun message(): String { - return when (this) { - Error -> "Error" - FetchRepos -> "Fetching repositories" - GetAccessToken -> "Getting the access token" - GetUserInfo -> "Getting user information" - Idle -> "" - Success -> "Success" - } - } - } + data object Idle: InitState() + data class Error(val message: String? = null): InitState() - sealed class AuthStep2 : InitState() { - data object Idle : AuthStep2() - data object CreateRepo : AuthStep2() - data object AddDeployKey : AuthStep2() - data object Error : AuthStep2() - - override fun isClickable(): Boolean = this is Idle || this is Error - override fun isLoading(): Boolean = this is CreateRepo || this is AddDeployKey - - override fun message(): String { - return when (this) { - AddDeployKey -> "Adding deploy key to the repository" - CreateRepo -> "Creating the repository" - Error -> "Error" - Idle -> "" - } - } - } + data object GettingAccessToken: InitState() + data object FetchingRepos: InitState() + data object GettingUserInfo: InitState() + data object AuthentificationSuccess: InitState() - sealed class CloneState : InitState() { - data object Idle : CloneState() - data class Cloning(val percent: Int) : CloneState() - data object Error : CloneState() + data object CreatingRemoteRepo: InitState() + data object AddingDeployKey: InitState() + data class Cloning(val percent: Int): InitState() - override fun isClickable(): Boolean = this is Idle || this is Error - override fun isLoading(): Boolean = this is Cloning + data object CalculatingTimestamps: InitState() + data class GeneratingDatabase(val path: String): InitState() - override fun message(): String { - return when (this) { - is Cloning -> "$percent %" - Error -> "Error" - Idle -> "" - } - } + fun message(): String { + return when (this) { + AddingDeployKey -> "Adding deploy key" + CalculatingTimestamps -> "Calculating timestamps" + is Cloning -> "Cloning: $percent %" + CreatingRemoteRepo -> "Creating repository" + is Error -> if (message != null) "Error: $message" else "Error" + FetchingRepos -> "Fetching repositories" + is GeneratingDatabase -> "Generating database, path: $path" + GettingAccessToken -> "Getting the access token" + GettingUserInfo -> "Getting user information" + Idle -> "" + AuthentificationSuccess -> "" + } } -} + + fun isLoading(): Boolean = this !is Idle && this !is Error && this !is AuthentificationSuccess +} \ No newline at end of file diff --git a/app/src/main/rust/src/lib.rs b/app/src/main/rust/src/lib.rs index 02631c55..d4e35d09 100644 --- a/app/src/main/rust/src/lib.rs +++ b/app/src/main/rust/src/lib.rs @@ -230,16 +230,19 @@ mod callback { callback_class, } } - pub fn progress(&mut self, progress: i32) { + pub fn progress(&mut self, progress: i32) -> bool { match self.env.call_method( &self.callback_class, "progressCb", - "(I)V", + "(I)Z", &[progress.into()], ) { - Ok(_) => {} + Ok(res) => { + res.z().unwrap() + } Err(e) => { error!("{e}"); + true } } } diff --git a/app/src/main/rust/src/libgit2/mod.rs b/app/src/main/rust/src/libgit2/mod.rs index 5c58aa41..9ae3cb86 100644 --- a/app/src/main/rust/src/libgit2/mod.rs +++ b/app/src/main/rust/src/libgit2/mod.rs @@ -128,12 +128,11 @@ pub fn clone_repo( .credentials(move |_url, _username_from_url, _allowed_types| credential_helper(&cred)); } + callbacks.transfer_progress(|stats: Progress| { let progress = stats.indexed_objects() as f32 / stats.total_objects() as f32 * 100.; - cb.progress(progress as i32); - - true + cb.progress(progress as i32) }); let mut fetch_options = FetchOptions::new();