From 041d8ddab6368a7143cea4f74928d45d1dff2963 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sat, 13 Sep 2025 23:15:51 +0200 Subject: [PATCH 1/2] add sync icons in top bar --- .../gitnote/manager/StorageManager.kt | 27 +++- .../gitnote/ui/screen/app/grid/TopGrid.kt | 151 ++++++++++++++---- .../gitnote/ui/viewmodel/GridViewModel.kt | 9 +- .../main/res/drawable/cloud_alert_24px.xml | 9 ++ 4 files changed, 163 insertions(+), 33 deletions(-) create mode 100644 app/src/main/res/drawable/cloud_alert_24px.xml 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 77be1377..e1a21e9f 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 @@ -7,6 +7,8 @@ import io.github.wiiznokes.gitnote.data.AppPreferences import io.github.wiiznokes.gitnote.data.room.Note import io.github.wiiznokes.gitnote.data.room.NoteFolder import io.github.wiiznokes.gitnote.data.room.RepoDatabase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlin.Result.Companion.failure @@ -14,6 +16,21 @@ import kotlin.Result.Companion.success private const val TAG = "StorageManager" +sealed interface SyncState { + + object Ok : SyncState + + object Error : SyncState + + object Pull : SyncState + + object Push : SyncState + + fun isLoading(): Boolean { + return this is Pull || this is Push + } +} + class StorageManager { @@ -26,9 +43,11 @@ class StorageManager { private val gitManager: GitManager = MyApp.appModule.gitManager - private val locker = Mutex() + private val _syncState: MutableStateFlow = MutableStateFlow(SyncState.Ok) + val syncState: StateFlow = _syncState + suspend fun updateDatabaseAndRepo(): Result = locker.withLock { Log.d(TAG, "updateDatabaseAndRepo") @@ -46,17 +65,20 @@ class StorageManager { } if (remoteUrl.isNotEmpty()) { + _syncState.emit(SyncState.Pull) gitManager.pull(cred).onFailure { uiHelper.makeToast(it.message) } } if (remoteUrl.isNotEmpty()) { + _syncState.emit(SyncState.Push) // todo: maybe async this call gitManager.push(cred).onFailure { uiHelper.makeToast(it.message) } } + _syncState.emit(SyncState.Ok) updateDatabaseWithoutLocker() @@ -279,6 +301,7 @@ class StorageManager { } if (remoteUrl.isNotEmpty()) { + _syncState.emit(SyncState.Pull) gitManager.pull(cred).onFailure { it.printStackTrace() } @@ -302,6 +325,7 @@ class StorageManager { } if (remoteUrl.isNotEmpty()) { + _syncState.emit(SyncState.Push) gitManager.push(cred).onFailure { it.printStackTrace() } @@ -309,6 +333,7 @@ class StorageManager { prefs.databaseCommit.update(gitManager.lastCommit()) + _syncState.emit(SyncState.Ok) return success(payload) } } \ No newline at end of file diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/TopGrid.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/TopGrid.kt index e7f593c1..e0176a65 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/TopGrid.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/TopGrid.kt @@ -3,6 +3,13 @@ package io.github.wiiznokes.gitnote.ui.screen.app.grid import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -16,10 +23,14 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CloudDone +import androidx.compose.material.icons.filled.CloudDownload +import androidx.compose.material.icons.filled.CloudUpload import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Menu import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material3.DrawerState +import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -27,6 +38,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -35,22 +47,27 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.lifecycle.viewModelScope import io.github.wiiznokes.gitnote.R +import io.github.wiiznokes.gitnote.manager.SyncState import io.github.wiiznokes.gitnote.ui.component.CustomDropDown import io.github.wiiznokes.gitnote.ui.component.CustomDropDownModel import io.github.wiiznokes.gitnote.ui.component.SimpleIcon import io.github.wiiznokes.gitnote.ui.viewmodel.GridViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.roundToInt @@ -172,40 +189,49 @@ private fun SearchBar( }, trailingIcon = if (queryTextField.text.isEmpty()) { { - Box { - val expanded = remember { mutableStateOf(false) } - IconButton( - onClick = { - expanded.value = true + Row( + verticalAlignment = Alignment.CenterVertically + ) { + + val syncState = vm.syncState.collectAsState() + + SyncStateIcon(syncState.value) + + Box { + val expanded = remember { mutableStateOf(false) } + IconButton( + onClick = { + expanded.value = true + } + ) { + SimpleIcon( + imageVector = Icons.Rounded.MoreVert, + tint = MaterialTheme.colorScheme.onSurface + ) } - ) { - SimpleIcon( - imageVector = Icons.Rounded.MoreVert, - tint = MaterialTheme.colorScheme.onSurface - ) - } - val readOnlyMode = vm.prefs.isReadOnlyModeActive.getAsState().value + val readOnlyMode = vm.prefs.isReadOnlyModeActive.getAsState().value - CustomDropDown( - expanded = expanded, - options = listOf( - CustomDropDownModel( - text = stringResource(R.string.settings), - onClick = onSettingsClick - ), - CustomDropDownModel( - text = if (readOnlyMode) stringResource( - R.string.read_only_mode_deactive - ) else stringResource(R.string.read_only_mode_activate), - onClick = { - vm.viewModelScope.launch { - vm.prefs.isReadOnlyModeActive.update(!readOnlyMode) + CustomDropDown( + expanded = expanded, + options = listOf( + CustomDropDownModel( + text = stringResource(R.string.settings), + onClick = onSettingsClick + ), + CustomDropDownModel( + text = if (readOnlyMode) stringResource( + R.string.read_only_mode_deactive + ) else stringResource(R.string.read_only_mode_activate), + onClick = { + vm.viewModelScope.launch { + vm.prefs.isReadOnlyModeActive.update(!readOnlyMode) + } } - } - ), + ), + ) ) - ) + } } } } else { @@ -306,3 +332,70 @@ private fun SelectableTopBar( } } } + + + + +@Composable +private fun SyncStateIcon( + state: SyncState +) { + var modifier: Modifier = Modifier + + if (state.isLoading()) { + + val infiniteTransition = rememberInfiniteTransition() + val alpha = infiniteTransition.animateFloat( + initialValue = 0.3f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 500), + repeatMode = RepeatMode.Reverse + ) + ) + + modifier = modifier.alpha(alpha.value) + } + + when (state) { + is SyncState.Error -> { + Icon( + painter = painterResource(R.drawable.cloud_alert_24px), + contentDescription = "Sync Error", + modifier = modifier, + ) + } + + is SyncState.Ok -> { + var visible by remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + delay(1000) + visible = false + } + + AnimatedVisibility( + visible = visible, + exit = fadeOut(animationSpec = tween(durationMillis = 500)) + ) { + Icon( + imageVector = Icons.Default.CloudDone, + contentDescription = "Sync Done", + modifier = modifier, + ) + } + } + + is SyncState.Pull -> Icon( + imageVector = Icons.Default.CloudDownload, + contentDescription = "Pulling", + modifier = modifier, + ) + + is SyncState.Push -> Icon( + imageVector = Icons.Default.CloudUpload, + contentDescription = "Pushing", + modifier = modifier, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt index 18e4f3d9..86354c5f 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt @@ -46,11 +46,14 @@ class GridViewModel : ViewModel() { val uiHelper = MyApp.appModule.uiHelper private val _query = MutableStateFlow("") - val query = _query + val query: StateFlow = _query + + val syncState = storageManager.syncState private val _isRefreshing = MutableStateFlow(false) - val isRefreshing: StateFlow - get() = _isRefreshing.asStateFlow() + val isRefreshing: StateFlow = _isRefreshing.asStateFlow() + + private val _currentNoteFolderRelativePath = MutableStateFlow( if (prefs.rememberLastOpenedFolder.getBlocking()) { diff --git a/app/src/main/res/drawable/cloud_alert_24px.xml b/app/src/main/res/drawable/cloud_alert_24px.xml new file mode 100644 index 00000000..53bc270a --- /dev/null +++ b/app/src/main/res/drawable/cloud_alert_24px.xml @@ -0,0 +1,9 @@ + + + From a342d58ea3b08bdaf331be299d4f6c79b24e5cd4 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sat, 13 Sep 2025 23:19:10 +0200 Subject: [PATCH 2/2] remove toast for successful operations --- .../github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt | 7 ------- .../github/wiiznokes/gitnote/ui/viewmodel/edit/TextVM.kt | 2 -- 2 files changed, 9 deletions(-) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt index 86354c5f..4172e041 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt @@ -163,11 +163,6 @@ class GridViewModel : ViewModel() { val currentSelectedNotes = selectedNotes.value unselectAllNotes() storageManager.deleteNotes(currentSelectedNotes) - uiHelper.makeToast( - uiHelper.getQuantityString( - R.plurals.success_notes_delete, currentSelectedNotes.size - ) - ) } } @@ -175,14 +170,12 @@ class GridViewModel : ViewModel() { selectNote(note, false) CoroutineScope(Dispatchers.IO).launch { storageManager.deleteNote(note) - uiHelper.makeToast(uiHelper.getQuantityString(R.plurals.success_notes_delete, 1)) } } fun deleteFolder(noteFolder: NoteFolder) { CoroutineScope(Dispatchers.IO).launch { storageManager.deleteNoteFolder(noteFolder) - uiHelper.makeToast(uiHelper.getQuantityString(R.plurals.success_noteFolders_delete, 1)) } } diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/edit/TextVM.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/edit/TextVM.kt index 425f6b1a..0540a2a1 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/edit/TextVM.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/edit/TextVM.kt @@ -364,7 +364,6 @@ open class TextVM() : ViewModel() { return@launch } - uiHelper.makeToast(uiHelper.getString(R.string.success_note_update)) } return success(newNote) } @@ -409,7 +408,6 @@ open class TextVM() : ViewModel() { uiHelper.makeToast(it.message) return@launch } - uiHelper.makeToast(uiHelper.getString(R.string.success_note_create)) } return success(note)