From 5a9f05552ca594679b226d97bc0e1886fc1a598a Mon Sep 17 00:00:00 2001 From: Kenji Hikmatullah Date: Sun, 1 Dec 2024 00:24:52 +0700 Subject: [PATCH 01/28] Add functionality to also search on content --- .../java/com/digiventure/ventnote/feature/notes/NotesPage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt index 5078f57..878a957 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt @@ -72,7 +72,7 @@ fun NotesPage( val snackBarHostState = remember { SnackbarHostState() } val filteredNotes = remember(noteListState.value, viewModel.searchedTitleText.value) { noteListState.value?.getOrNull()?.filter { note -> - note.title.contains(viewModel.searchedTitleText.value, true) + note.title.contains(viewModel.searchedTitleText.value, true) || note.content.contains(viewModel.searchedTitleText.value, true) } ?: listOf() } val loadingDialog = remember { mutableStateOf(false) } From 6894b50a8953ad5ecfec1b0a67773621e885e8ec Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Thu, 17 Apr 2025 13:26:46 +0700 Subject: [PATCH 02/28] Feat : Update several dependencies & kotlin version --- app/build.gradle | 60 +++++++++++++----------- build.gradle | 6 +-- gradle/wrapper/gradle-wrapper.properties | 6 +-- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e13d3cb..03a49e9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ plugins { id 'com.google.dagger.hilt.android' id 'kotlin-kapt' id 'kotlin-parcelize' + id 'org.jetbrains.kotlin.plugin.compose' version '2.1.0' // disabled for internal purpose (if you want to enable, you must create firebase project first) // id 'com.google.gms.google-services' @@ -13,12 +14,12 @@ plugins { android { namespace 'com.digiventure.ventnote' - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "com.digiventure.ventnote" minSdk 21 - targetSdk 34 + targetSdk 35 versionCode 41 versionName "1.0.8" @@ -62,7 +63,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion '1.4.0' + kotlinCompilerExtensionVersion '1.5.15' } packagingOptions { @@ -78,33 +79,35 @@ android { } dependencies { - implementation "androidx.core:core-ktx:1.13.1" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.6" + implementation "androidx.core:core-ktx:1.16.0" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.7" - implementation "androidx.activity:activity-compose:1.9.3" + implementation "androidx.activity:activity-compose:1.10.1" // Jetpack Compose - implementation "androidx.compose.runtime:runtime-livedata:1.7.4" - implementation "androidx.compose.ui:ui:1.7.4" - implementation "androidx.compose.ui:ui-tooling-preview:1.7.4" + implementation "androidx.compose.runtime:runtime-livedata:1.7.8" + implementation "androidx.compose.ui:ui:1.7.8" + implementation "androidx.compose.ui:ui-tooling-preview:1.7.8" - implementation "androidx.compose.material3:material3:1.3.1" + implementation "androidx.compose.material3:material3:1.3.2" // Lifecycle Livedata - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.6" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7" + + implementation "androidx.compose.compiler:compiler:1.5.15" // Room - def room_version = "2.6.1" + def room_version = "2.7.0" implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" androidTestImplementation "androidx.room:room-testing:$room_version" // Datastore - implementation("androidx.datastore:datastore-preferences:1.1.1") + implementation("androidx.datastore:datastore-preferences:1.1.4") // Compose Navigation - implementation "androidx.navigation:navigation-compose:2.8.3" + implementation "androidx.navigation:navigation-compose:2.8.9" // Coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0" @@ -114,12 +117,13 @@ dependencies { androidTestImplementation "androidx.test.ext:junit:1.2.1" // Hilt - implementation "com.google.dagger:hilt-android:2.50" - kapt "com.google.dagger:hilt-android-compiler:2.50" + def dagger_version = "2.56.1" + implementation "com.google.dagger:hilt-android:$dagger_version" + kapt "com.google.dagger:hilt-android-compiler:$dagger_version" implementation "androidx.hilt:hilt-navigation-compose:1.2.0" // Material Icon Extension - implementation "androidx.compose.material:material-icons-extended:1.7.4" + implementation "androidx.compose.material:material-icons-extended:1.7.8" // So, make sure you also include that repository in your project's build.gradle file. implementation("com.google.android.play:app-update:2.1.0") @@ -127,7 +131,7 @@ dependencies { implementation("com.google.android.play:app-update-ktx:2.1.0") // Google Play API - implementation "com.google.android.gms:play-services-auth:21.2.0" + implementation "com.google.android.gms:play-services-auth:21.3.0" // Accompanist - Status Bar implementation "com.google.accompanist:accompanist-systemuicontroller:0.34.0" @@ -139,13 +143,13 @@ dependencies { androidTestImplementation "androidx.test.espresso:espresso-intents:3.6.1" androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1" - androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.7.4" - debugImplementation "androidx.compose.ui:ui-tooling:1.7.4" - debugImplementation "androidx.compose.ui:ui-test-manifest:1.7.4" + androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.7.8" + debugImplementation "androidx.compose.ui:ui-tooling:1.7.8" + debugImplementation "androidx.compose.ui:ui-test-manifest:1.7.8" /// Hilt test (for handling service locator when test) - androidTestImplementation "com.google.dagger:hilt-android-testing:2.50" - kaptAndroidTest "com.google.dagger:hilt-android-compiler:2.50" + androidTestImplementation "com.google.dagger:hilt-android-testing:$dagger_version" + kaptAndroidTest "com.google.dagger:hilt-android-compiler:$dagger_version" // For mocking purposes & make it visible in instrumented test testImplementation "org.mockito.kotlin:mockito-kotlin:5.2.1" @@ -159,15 +163,15 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0" // Import the Firebase BoM - implementation platform("com.google.firebase:firebase-bom:33.4.0") + implementation platform("com.google.firebase:firebase-bom:33.12.0") // When using the BoM, you don't specify versions in Firebase library dependencies // Add the dependency for the Firebase SDK for Google Analytics implementation "com.google.firebase:firebase-analytics" - implementation "com.google.firebase:firebase-crashlytics:19.2.0" - implementation "com.google.firebase:firebase-perf-ktx:21.0.1" + implementation "com.google.firebase:firebase-crashlytics:19.4.2" + implementation "com.google.firebase:firebase-perf-ktx:21.0.5" //Google sign in - implementation "com.google.android.gms:play-services-auth:21.2.0" + implementation "com.google.android.gms:play-services-auth:21.3.0" //Google Drive API implementation "com.google.http-client:google-http-client-gson:1.44.2" diff --git a/build.gradle b/build.gradle index d2f6d8a..ac5836f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { dependencies { - classpath 'com.android.tools.build:gradle:8.2.2' + classpath 'com.android.tools.build:gradle:8.9.1' // disable for staging purpose // classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.2' @@ -12,6 +12,6 @@ buildscript { plugins { id 'com.android.application' version '7.4.2' apply false id 'com.android.library' version '7.4.2' apply false - id 'org.jetbrains.kotlin.android' version '1.8.0' apply false - id 'com.google.dagger.hilt.android' version '2.44' apply false + id 'org.jetbrains.kotlin.android' version '2.1.0' apply false + id 'com.google.dagger.hilt.android' version '2.51.1' apply false } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d63b0b2..ff849b5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Mar 02 21:07:04 WIB 2023 +#Wed Apr 16 23:39:55 WIB 2025 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists From b0c5c6c2425aec8ef39403e71544459548813ddc Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Fri, 18 Apr 2025 11:29:54 +0700 Subject: [PATCH 03/28] Feat : Refactor deprecated implementation for in-app update, cleanup code to use recommended implementation --- README.md | 12 +- .../com/digiventure/ventnote/MainActivity.kt | 129 +++++++++--------- .../feature/backup/viewmodel/AuthVM.kt | 6 +- .../feature/backup/viewmodel/BackupPageVM.kt | 22 +-- .../feature/notes/viewmodel/NotesPageVM.kt | 2 +- .../digiventure/ventnote/navigation/Route.kt | 10 +- .../utils/LiveDataTestExtensions.kt | 2 +- 7 files changed, 94 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 101aaaa..f0cb68b 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ Note management app built with jetpack compose and newest modern android archite

- - - - - - + + + + + +


diff --git a/app/src/main/java/com/digiventure/ventnote/MainActivity.kt b/app/src/main/java/com/digiventure/ventnote/MainActivity.kt index 36d36c2..f535e7a 100644 --- a/app/src/main/java/com/digiventure/ventnote/MainActivity.kt +++ b/app/src/main/java/com/digiventure/ventnote/MainActivity.kt @@ -4,10 +4,12 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.material3.DrawerValue import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.remember @@ -20,10 +22,12 @@ import com.digiventure.ventnote.feature.notes.components.drawer.NavDrawer import com.digiventure.ventnote.navigation.NavGraph import com.digiventure.ventnote.navigation.PageNavigation import com.digiventure.ventnote.ui.theme.VentNoteTheme +import com.google.android.play.core.appupdate.AppUpdateInfo import com.google.android.play.core.appupdate.AppUpdateManager import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.appupdate.AppUpdateOptions import com.google.android.play.core.install.InstallStateUpdatedListener -import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.AppUpdateType.FLEXIBLE import com.google.android.play.core.install.model.AppUpdateType.IMMEDIATE import com.google.android.play.core.install.model.InstallStatus import com.google.android.play.core.install.model.UpdateAvailability @@ -35,21 +39,12 @@ class MainActivity : ComponentActivity() { private lateinit var installStateUpdatedListener: InstallStateUpdatedListener private lateinit var appUpdateManager: AppUpdateManager private var isDialogShowed = false - - companion object { - const val REQUEST_UPDATE_CODE = 1 - } + private lateinit var updateLauncher: ActivityResultLauncher override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - // Check in-app update - appUpdateManager = AppUpdateManagerFactory.create(this) - addUpdateStatusListener() - checkUpdate() - + startInAppUpdateCheck() enableEdgeToEdge() - setContent { VentNoteTheme { val navController = rememberNavController() @@ -61,8 +56,6 @@ class MainActivity : ComponentActivity() { val coroutineScope = rememberCoroutineScope() - val snackBarHostState = remember { SnackbarHostState() } - Surface( modifier = Modifier.safeDrawingPadding(), color = MaterialTheme.colorScheme.primary, @@ -100,80 +93,92 @@ class MainActivity : ComponentActivity() { } } + /** + * Initializes the in-app update process by creating an AppUpdateManager, + * registering an activity result launcher for handling update confirmations, + * setting up an update status listener, and initiating the update check. + */ + private fun startInAppUpdateCheck() { + appUpdateManager = AppUpdateManagerFactory.create(this) + updateLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result -> + if (result.resultCode != RESULT_OK) { + checkUpdate() + } + } + addUpdateStatusListener() + checkUpdate() + } + + /** + * Adds a listener to handle app update installation status changes. + * 1. After the update is downloaded, show a dialog and request user confirmation to restart the app. + */ private fun addUpdateStatusListener() { installStateUpdatedListener = InstallStateUpdatedListener { installState -> - when (installState.installStatus()) { - InstallStatus.DOWNLOADED -> { - // After the update is downloaded, show a notification - // and request user confirmation to restart the app. - showDialogForCompleteUpdate() - } - - InstallStatus.INSTALLED -> { - appUpdateManager.unregisterListener(installStateUpdatedListener) - } - - else -> {} + if (installState.installStatus() == InstallStatus.DOWNLOADED) { + showDialogForCompleteUpdate() + } else if (installState.installStatus() == InstallStatus.INSTALLED) { + appUpdateManager.unregisterListener(installStateUpdatedListener) } } } + /** + * Checks for available app updates and initiates the update flow if an update is available and allowed. + * 1. Before starting an update, register a listener for updates. + */ private fun checkUpdate() { - // Before starting an update, register a listener for updates. appUpdateManager.registerListener(installStateUpdatedListener) - - // Returns an intent object that you use to check for an update. val appUpdateInfoTask = appUpdateManager.appUpdateInfo - - // Check that the platform will allow the specified type of update. - appUpdateInfoTask.addOnSuccessListener { - when (it.updateAvailability()) { - UpdateAvailability.UPDATE_AVAILABLE -> { - val updateTypes = arrayOf(AppUpdateType.FLEXIBLE, IMMEDIATE) - for (type in updateTypes) { - if (it.isUpdateTypeAllowed(type)) { - appUpdateManager.startUpdateFlowForResult( - it, - type, - this, - REQUEST_UPDATE_CODE - ) - break - } + appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> + if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) { + when { + appUpdateInfo.isUpdateTypeAllowed(IMMEDIATE) -> { + startUpdateFlow(appUpdateInfo, IMMEDIATE) + return@addOnSuccessListener + } + appUpdateInfo.isUpdateTypeAllowed(FLEXIBLE) -> { + startUpdateFlow(appUpdateInfo, FLEXIBLE) + return@addOnSuccessListener } } - - else -> {} } } } + /** + * On resuming the activity, check for 2 scenarios: + * 1. If a flexible update has been downloaded and is awaiting installation, + * show a dialog prompting the user to complete the update. + * 2. If a developer-triggered immediate update is already in progress, + * resume the update flow. + */ override fun onResume() { super.onResume() appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo -> - if (appUpdateInfo != null) { // Check if appUpdateInfo is not null - if (appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { - if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) { + if (appUpdateInfo != null) { + when { + appUpdateInfo.isUpdateTypeAllowed(FLEXIBLE) && + appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED -> { showDialogForCompleteUpdate() } - } else { - if (appUpdateInfo.updateAvailability() == - UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS - ) { - // If an in-app update is already running, resume the update. - appUpdateManager.startUpdateFlowForResult( - appUpdateInfo, - IMMEDIATE, - this, - REQUEST_UPDATE_CODE - ) + appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> { + startUpdateFlow(appUpdateInfo, IMMEDIATE) } } } } } + private fun startUpdateFlow(updateInfo: AppUpdateInfo, updateType:Int) { + appUpdateManager.startUpdateFlowForResult( + updateInfo, + updateLauncher, + AppUpdateOptions.newBuilder(updateType).build() + ) + } + private fun showDialogForCompleteUpdate() { isDialogShowed = true } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/AuthVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/AuthVM.kt index 3cfdc33..6adb41e 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/AuthVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/AuthVM.kt @@ -76,8 +76,8 @@ class AuthVM @Inject constructor( ) sealed interface AuthState { - object Loading : AuthState - object SignedOut : AuthState - object SignedIn : AuthState + data object Loading : AuthState + data object SignedOut : AuthState + data object SignedIn : AuthState } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageVM.kt index 594caff..5a5bc4a 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageVM.kt @@ -133,29 +133,29 @@ class BackupPageVM @Inject constructor( } sealed class FileBackupState { - object SyncInitial : FileBackupState() - object SyncStarted : FileBackupState() - object SyncFinished : FileBackupState() + data object SyncInitial : FileBackupState() + data object SyncStarted : FileBackupState() + data object SyncFinished : FileBackupState() data class SyncFailed(val errorMessage: String) : FileBackupState() } sealed class FileRestoreState { - object SyncInitial : FileRestoreState() - object SyncStarted : FileRestoreState() - object SyncFinished : FileRestoreState() + data object SyncInitial : FileRestoreState() + data object SyncStarted : FileRestoreState() + data object SyncFinished : FileRestoreState() data class SyncFailed(val errorMessage: String) : FileRestoreState() } sealed class FileDeleteState { - object SyncInitial : FileDeleteState() - object SyncStarted : FileDeleteState() - object SyncFinished : FileDeleteState() + data object SyncInitial : FileDeleteState() + data object SyncStarted : FileDeleteState() + data object SyncFinished : FileDeleteState() data class SyncFailed(val errorMessage: String) : FileDeleteState() } sealed class FileBackupListState { - object FileBackupListStarted : FileBackupListState() - object FileBackupListFinished : FileBackupListState() + data object FileBackupListStarted : FileBackupListState() + data object FileBackupListFinished : FileBackupListState() data class FileBackupListFailed(val errorMessage: String) : FileBackupListState() } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageVM.kt index 441ba22..0c75e1e 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageVM.kt @@ -28,7 +28,7 @@ class NotesPageVM @Inject constructor( Pair(Constants.UPDATED_AT, Constants.DESCENDING) ) - val defaultException = Exception("Unknown error") + private val defaultException = Exception("Unknown error") override val noteList: LiveData>> get() = _noteList diff --git a/app/src/main/java/com/digiventure/ventnote/navigation/Route.kt b/app/src/main/java/com/digiventure/ventnote/navigation/Route.kt index 75bad46..e551f32 100644 --- a/app/src/main/java/com/digiventure/ventnote/navigation/Route.kt +++ b/app/src/main/java/com/digiventure/ventnote/navigation/Route.kt @@ -1,9 +1,9 @@ package com.digiventure.ventnote.navigation sealed class Route(val routeName: String) { - object NotesPage: Route(routeName = "notes_page") - object NoteDetailPage: Route(routeName = "note_detail_page") - object NoteCreationPage: Route(routeName = "note_creation_page") - object SharePreviewPage: Route(routeName = "share_preview_page") - object BackupPage: Route(routeName = "backup_page") + data object NotesPage: Route(routeName = "notes_page") + data object NoteDetailPage: Route(routeName = "note_detail_page") + data object NoteCreationPage: Route(routeName = "note_creation_page") + data object SharePreviewPage: Route(routeName = "share_preview_page") + data object BackupPage: Route(routeName = "backup_page") } \ No newline at end of file diff --git a/app/src/test/java/com/digiventure/utils/LiveDataTestExtensions.kt b/app/src/test/java/com/digiventure/utils/LiveDataTestExtensions.kt index 5d0739d..129712f 100644 --- a/app/src/test/java/com/digiventure/utils/LiveDataTestExtensions.kt +++ b/app/src/test/java/com/digiventure/utils/LiveDataTestExtensions.kt @@ -41,7 +41,7 @@ class LiveDataValueCapture { * Extension function to capture all values that are emitted to a LiveData during the execution of * `captureBlock`. * - * @param captureBlock a lambda that will + * @param block a lambda that will */ inline fun LiveData.captureValues(block: LiveDataValueCapture.() -> Unit) { val capture = LiveDataValueCapture() From dcfa56e5760b6cf30d9f8d7b85082709ebe04e35 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sat, 19 Apr 2025 14:23:19 +0700 Subject: [PATCH 04/28] Feat : Cleanup code to use recommended implementation --- app/build.gradle | 2 +- .../digiventure/ventnote/config/DriveAPI.kt | 12 +++++++++ .../ventnote/feature/notes/NotesPage.kt | 7 +++--- .../components/drawer/NavigationDrawer.kt | 3 ++- .../ventnote/navigation/NavGraph.kt | 25 +++++++++++++------ .../ventnote/navigation/NoteModelParamType.kt | 8 +++++- .../com/digiventure/ventnote/ui/theme/Type.kt | 7 ++++-- build.gradle | 2 +- 8 files changed, 50 insertions(+), 16 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 03a49e9..a988e44 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -117,7 +117,7 @@ dependencies { androidTestImplementation "androidx.test.ext:junit:1.2.1" // Hilt - def dagger_version = "2.56.1" + def dagger_version = "2.56.2" implementation "com.google.dagger:hilt-android:$dagger_version" kapt "com.google.dagger:hilt-android-compiler:$dagger_version" implementation "androidx.hilt:hilt-navigation-compose:1.2.0" diff --git a/app/src/main/java/com/digiventure/ventnote/config/DriveAPI.kt b/app/src/main/java/com/digiventure/ventnote/config/DriveAPI.kt index 2d8a1b7..c5071c4 100644 --- a/app/src/main/java/com/digiventure/ventnote/config/DriveAPI.kt +++ b/app/src/main/java/com/digiventure/ventnote/config/DriveAPI.kt @@ -9,6 +9,18 @@ import com.google.api.client.json.gson.GsonFactory import com.google.api.services.drive.Drive import com.google.api.services.drive.DriveScopes +/** + * Provides a helper for obtaining a Google Drive API client instance. + * + * This class encapsulates the logic for creating and managing a [Drive] client, + * ensuring that only a single instance is used throughout the application (singleton pattern). + * It handles authentication using a [GoogleSignInAccount] and scopes access to app data. + * + * Note: This implementation uses the Drive REST API, as the deprecated Drive Android API + * is no longer supported (https://developers.google.com/drive/api/guides/android-api-deprecation). + * Once the Google Drive SDK provides updated support or a new official Android API, + * this class may be updated to utilize it for improved performance and features. + */ class DriveAPI { companion object { private var instance: Drive? = null diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt index 878a957..f0de42c 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt @@ -58,6 +58,7 @@ fun NotesPage( viewModel: NotesPageBaseVM = hiltViewModel(), openDrawer: () -> Unit ) { + val emptyString = "" LockScreenOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) val navigationActions = remember(navHostController) { @@ -72,7 +73,7 @@ fun NotesPage( val snackBarHostState = remember { SnackbarHostState() } val filteredNotes = remember(noteListState.value, viewModel.searchedTitleText.value) { noteListState.value?.getOrNull()?.filter { note -> - note.title.contains(viewModel.searchedTitleText.value, true) || note.content.contains(viewModel.searchedTitleText.value, true) + note.title.contains(viewModel.searchedTitleText.value, true) || note.note.contains(viewModel.searchedTitleText.value, true) } ?: listOf() } val loadingDialog = remember { mutableStateOf(false) } @@ -92,7 +93,7 @@ fun NotesPage( noteListState.value?.onFailure { scope.launch { snackBarHostState.showSnackbar( - message = it.message ?: "", + message = it.message ?: emptyString, withDismissAction = true ) } @@ -122,7 +123,7 @@ fun NotesPage( deleteDialog.value = false snackBarHostState.showSnackbar( - message = it.message ?: "", + message = it.message ?: emptyString, withDismissAction = true ) } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt index 58f353a..16a5b9b 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt @@ -68,6 +68,7 @@ import com.digiventure.ventnote.ui.theme.CrimsonLightPrimary import com.digiventure.ventnote.ui.theme.PurpleLightPrimary import kotlinx.coroutines.launch import java.util.Locale +import androidx.core.net.toUri @Composable fun NavDrawer( @@ -84,7 +85,7 @@ fun NavDrawer( fun openPlayStore(appURL: String) { val playIntent: Intent = Intent().apply { action = Intent.ACTION_VIEW - data = Uri.parse(appURL) + data = appURL.toUri() } try { context.startActivity(playIntent) diff --git a/app/src/main/java/com/digiventure/ventnote/navigation/NavGraph.kt b/app/src/main/java/com/digiventure/ventnote/navigation/NavGraph.kt index 81cec3f..7583492 100644 --- a/app/src/main/java/com/digiventure/ventnote/navigation/NavGraph.kt +++ b/app/src/main/java/com/digiventure/ventnote/navigation/NavGraph.kt @@ -1,5 +1,6 @@ package com.digiventure.ventnote.navigation +import android.os.Build import androidx.compose.runtime.Composable import androidx.navigation.NavHostController import androidx.navigation.NavType @@ -15,6 +16,11 @@ import com.digiventure.ventnote.feature.share_preview.SharePreviewPage @Composable fun NavGraph(navHostController: NavHostController, openDrawer: () -> Unit) { + val emptyString = "" + val noteIdNavArgument = "noteId" + val noteDataNavArgument = "noteData" + val stringZero = "0" + NavHost( navController = navHostController, startDestination = Route.NotesPage.routeName, @@ -23,25 +29,30 @@ fun NavGraph(navHostController: NavHostController, openDrawer: () -> Unit) { NotesPage(navHostController = navHostController, openDrawer = openDrawer) } composable( - route = "${Route.NoteDetailPage.routeName}/{noteId}", - arguments = listOf(navArgument("noteId") { + route = "${Route.NoteDetailPage.routeName}/{${noteIdNavArgument}}", + arguments = listOf(navArgument(noteIdNavArgument) { type = NavType.StringType - defaultValue = "" + defaultValue = emptyString }) ) { NoteDetailPage(navHostController = navHostController, - id = it.arguments?.getString("noteId") ?: "0") + id = it.arguments?.getString(noteIdNavArgument) ?: stringZero) } composable(Route.NoteCreationPage.routeName) { NoteCreationPage(navHostController = navHostController) } composable( - route = "${Route.SharePreviewPage.routeName}/{noteData}", - arguments = listOf(navArgument("noteData") { + route = "${Route.SharePreviewPage.routeName}/{${noteDataNavArgument}}", + arguments = listOf(navArgument(noteDataNavArgument) { type = NoteModelParamType() }) ) { - val note = it.arguments?.getParcelable("noteData") + val note = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + it.arguments?.getParcelable(noteDataNavArgument, NoteModel::class.java) + } else { + @Suppress("DEPRECATION") + it.arguments?.getParcelable(noteDataNavArgument) + } SharePreviewPage(navHostController = navHostController, note = note) } composable(Route.BackupPage.routeName) { diff --git a/app/src/main/java/com/digiventure/ventnote/navigation/NoteModelParamType.kt b/app/src/main/java/com/digiventure/ventnote/navigation/NoteModelParamType.kt index ffcb68c..0c67d46 100644 --- a/app/src/main/java/com/digiventure/ventnote/navigation/NoteModelParamType.kt +++ b/app/src/main/java/com/digiventure/ventnote/navigation/NoteModelParamType.kt @@ -1,5 +1,6 @@ package com.digiventure.ventnote.navigation +import android.os.Build import android.os.Bundle import androidx.navigation.NavType import com.digiventure.ventnote.data.persistence.NoteModel @@ -7,7 +8,12 @@ import com.google.gson.Gson class NoteModelParamType : NavType(isNullableAllowed = false) { override fun get(bundle: Bundle, key: String): NoteModel? { - return bundle.getParcelable(key) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + bundle.getParcelable(key, NoteModel::class.java) + } else { + @Suppress("DEPRECATION") + bundle.getParcelable(key) as? NoteModel + } } override fun parseValue(value: String): NoteModel { diff --git a/app/src/main/java/com/digiventure/ventnote/ui/theme/Type.kt b/app/src/main/java/com/digiventure/ventnote/ui/theme/Type.kt index 451eda6..70233a4 100644 --- a/app/src/main/java/com/digiventure/ventnote/ui/theme/Type.kt +++ b/app/src/main/java/com/digiventure/ventnote/ui/theme/Type.kt @@ -6,7 +6,11 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp -// Set of Material typography styles to start with +/** + * Defines the typography styles for the application, based on Material Design principles. + * This set includes overrides for bodyLarge, titleLarge, and labelSmall text styles, + * using the default font family and adjusting font weight, size, line height, and letter spacing. + */ val Typography = Typography( bodyLarge = TextStyle( fontFamily = FontFamily.Default, @@ -15,7 +19,6 @@ val Typography = Typography( lineHeight = 24.sp, letterSpacing = 0.5.sp ), - // Other default text styles to override titleLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, diff --git a/build.gradle b/build.gradle index ac5836f..d666ba2 100644 --- a/build.gradle +++ b/build.gradle @@ -13,5 +13,5 @@ plugins { id 'com.android.application' version '7.4.2' apply false id 'com.android.library' version '7.4.2' apply false id 'org.jetbrains.kotlin.android' version '2.1.0' apply false - id 'com.google.dagger.hilt.android' version '2.51.1' apply false + id 'com.google.dagger.hilt.android' version '2.56.1' apply false } From f2fba28588a6957fcc7ab63ecd3f5982a55e06ea Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 10 Aug 2025 00:46:22 +0700 Subject: [PATCH 05/28] Feat feature/ui-revamp : Change note item layout, take out search component from navbar --- .../ventnote/feature/notes/NotesPage.kt | 309 +++++++++++------- .../components/drawer/NavigationDrawer.kt | 3 +- .../feature/notes/components/item/NoteItem.kt | 104 +++--- .../feature/notes/components/navbar/AppBar.kt | 228 +++++-------- .../notes/components/searchbar/SearchBar.kt | 72 ++++ .../notes/viewmodel/NotesPageBaseVM.kt | 6 - .../notes/viewmodel/NotesPageMockVM.kt | 36 +- .../feature/notes/viewmodel/NotesPageVM.kt | 6 - 8 files changed, 435 insertions(+), 329 deletions(-) create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt index f0de42c..1c8cf5c 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt @@ -1,10 +1,13 @@ package com.digiventure.ventnote.feature.notes import android.content.pm.ActivityInfo +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -18,15 +21,24 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag @@ -42,8 +54,10 @@ import com.digiventure.ventnote.commons.TestTags import com.digiventure.ventnote.components.LockScreenOrientation import com.digiventure.ventnote.components.dialog.LoadingDialog import com.digiventure.ventnote.components.dialog.TextDialog +import com.digiventure.ventnote.data.persistence.NoteModel import com.digiventure.ventnote.feature.notes.components.item.NotesItem import com.digiventure.ventnote.feature.notes.components.navbar.NotesAppBar +import com.digiventure.ventnote.feature.notes.components.searchbar.SearchBar import com.digiventure.ventnote.feature.notes.components.sheets.FilterSheet import com.digiventure.ventnote.feature.notes.viewmodel.NotesPageBaseVM import com.digiventure.ventnote.feature.notes.viewmodel.NotesPageMockVM @@ -58,115 +72,137 @@ fun NotesPage( viewModel: NotesPageBaseVM = hiltViewModel(), openDrawer: () -> Unit ) { - val emptyString = "" + val focusManager = LocalFocusManager.current + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + var searchBarHeightPx by remember { mutableFloatStateOf(0f) } + LockScreenOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) val navigationActions = remember(navHostController) { PageNavigation(navHostController) } - val noteListState = viewModel.noteList.observeAsState() - val loadingState = viewModel.loader.observeAsState() + // Observe states + val noteListState by viewModel.noteList.observeAsState() + val loadingState by viewModel.loader.observeAsState() + val searchQuery by viewModel.searchedTitleText + val isMarking by viewModel.isMarking + val markedNoteList = viewModel.markedNoteList val scope = rememberCoroutineScope() - val snackBarHostState = remember { SnackbarHostState() } - val filteredNotes = remember(noteListState.value, viewModel.searchedTitleText.value) { - noteListState.value?.getOrNull()?.filter { note -> - note.title.contains(viewModel.searchedTitleText.value, true) || note.note.contains(viewModel.searchedTitleText.value, true) - } ?: listOf() + + // Memoized filtered notes with proper dependencies + val filteredNotes by remember { + derivedStateOf { + val notes = noteListState?.getOrNull() ?: emptyList() + if (searchQuery.isBlank()) { + notes + } else { + notes.filter { note -> + note.title.contains(searchQuery, ignoreCase = true) || + note.note.contains(searchQuery, ignoreCase = true) + } + } + } } - val loadingDialog = remember { mutableStateOf(false) } - val deleteDialog = remember { mutableStateOf(false) } - val openBottomSheet = rememberSaveable { mutableStateOf(false) } - val skipPartiallyExpanded = remember { mutableStateOf(false) } + // Dialog states + var showLoadingDialog by remember { mutableStateOf(false) } + var showDeleteDialog by remember { mutableStateOf(false) } + + // Bottom sheet states + var openBottomSheet by rememberSaveable { mutableStateOf(false) } + val skipPartiallyExpanded by remember { mutableStateOf(false) } val bottomSheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = skipPartiallyExpanded.value + skipPartiallyExpanded = skipPartiallyExpanded ) - LaunchedEffect(key1 = Unit) { + // Effects + LaunchedEffect(Unit) { viewModel.observeNotes() } - LaunchedEffect(key1 = noteListState.value) { - noteListState.value?.onFailure { - scope.launch { - snackBarHostState.showSnackbar( - message = it.message ?: emptyString, - withDismissAction = true - ) - } + LaunchedEffect(noteListState) { + noteListState?.onFailure { error -> + snackBarHostState.showSnackbar( + message = error.message.orEmpty(), + withDismissAction = true + ) } } - LaunchedEffect(key1 = loadingState.value) { - loadingDialog.value = (loadingState.value == true) + LaunchedEffect(loadingState) { + showLoadingDialog = loadingState == true } - val deletedMessage = stringResource(id = R.string.note_is_successfully_deleted) + // Memoized callbacks + val deleteNoteList = remember { + { + scope.launch { + viewModel.deleteNoteList() + .onSuccess { + showDeleteDialog = false + viewModel.unMarkAllNote() + viewModel.closeMarkingEvent() - fun deleteNoteList() { - scope.launch { - viewModel.deleteNoteList() - .onSuccess { - deleteDialog.value = false - viewModel.unMarkAllNote() - viewModel.closeMarkingEvent() + snackBarHostState.showSnackbar( + message = "Note is successfully deleted", // Consider using stringResource + withDismissAction = true + ) + } + .onFailure { error -> + showDeleteDialog = false + snackBarHostState.showSnackbar( + message = error.message.orEmpty(), + withDismissAction = true + ) + } + } + } + } - snackBarHostState.showSnackbar( - message = deletedMessage, - withDismissAction = true - ) - } - .onFailure { - deleteDialog.value = false + val onNoteClick = remember { + { note: NoteModel -> + if (isMarking) { + viewModel.addToMarkedNoteList(note) + } else { + viewModel.closeMarkingEvent() + navigationActions.navigateToDetailPage(note.id) + } + } + } - snackBarHostState.showSnackbar( - message = it.message ?: emptyString, - withDismissAction = true - ) - } + val onNoteLongClick = remember { + { note: NoteModel -> + if (!isMarking) { + viewModel.isMarking.value = true + } + viewModel.addToMarkedNoteList(note) + } + } + + val onNoteCheckClick = remember { + { note: NoteModel -> + viewModel.addToMarkedNoteList(note) } } Scaffold( topBar = { NotesAppBar( - isMarking = viewModel.isMarking.value, - markedNoteListSize = viewModel.markedNoteList.size, - isSearching = viewModel.isSearching.value, - searchedTitle = viewModel.searchedTitleText.value, - toggleDrawerCallback = { - openDrawer() - }, + isMarking = isMarking, + markedNoteListSize = markedNoteList.size, + toggleDrawerCallback = openDrawer, selectAllCallback = { - viewModel.noteList.value?.getOrNull().let { - if (it != null) viewModel.markAllNote(it) + noteListState?.getOrNull()?.let { notes -> + viewModel.markAllNote(notes) } }, - unSelectAllCallback = { - viewModel.unMarkAllNote() - }, - onSearchValueChange = { - viewModel.searchedTitleText.value = it - }, - closeMarkingCallback = { - viewModel.closeMarkingEvent() - }, - searchCallback = { - viewModel.isSearching.value = true - viewModel.searchedTitleText.value = "" - }, - sortCallback = { - openBottomSheet.value = true - }, - deleteCallback = { - deleteDialog.value = true - }, - closeSearchCallback = { - viewModel.closeSearchEvent() - } + unSelectAllCallback = viewModel::unMarkAllNote, + closeMarkingCallback = viewModel::closeMarkingEvent, + sortCallback = { openBottomSheet = true }, + deleteCallback = { showDeleteDialog = true } ) }, snackbarHost = { SnackbarHost(snackBarHostState) }, @@ -174,15 +210,17 @@ fun NotesPage( ExtendedFloatingActionButton( onClick = { viewModel.closeMarkingEvent() - viewModel.closeSearchEvent() - navigationActions.navigateToCreatePage() }, modifier = Modifier.semantics { testTag = TestTags.ADD_NOTE_FAB }, text = { - Text(stringResource(R.string.add), fontSize = 16.sp, fontWeight = FontWeight.Medium) + Text( + text = stringResource(R.string.add), + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) }, icon = { Icon( @@ -195,62 +233,101 @@ fun NotesPage( ) }, content = { contentPadding -> - Box(modifier = Modifier.padding(contentPadding)) { + Box( + modifier = Modifier + .fillMaxSize() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + focusManager.clearFocus() + } + .padding(contentPadding) + ) { LazyColumn( modifier = Modifier .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) .semantics { testTag = TestTags.NOTE_RV }, verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues( - top = 24.dp, - bottom = 96.dp - ) + contentPadding = PaddingValues(bottom = 96.dp) ) { - items(items = filteredNotes) { - Box(Modifier.padding(start = 16.dp, end = 16.dp)) { - NotesItem( - isMarking = viewModel.isMarking.value, - isMarked = it in viewModel.markedNoteList, - data = it, - onClick = { - if (viewModel.isMarking.value) { - viewModel.addToMarkedNoteList(it) - } else { - viewModel.closeMarkingEvent() - viewModel.closeSearchEvent() - - navigationActions.navigateToDetailPage(it.id) - } - }, - onLongClick = { - if (!viewModel.isMarking.value) { - viewModel.isMarking.value = true - } - viewModel.addToMarkedNoteList(it) - }, - onCheckClick = { - viewModel.addToMarkedNoteList(it) + // SearchBar as first item + item(key = "search_bar") { + Box( + modifier = Modifier + .onGloballyPositioned { coords -> + searchBarHeightPx = coords.size.height.toFloat() + scrollBehavior.state.heightOffsetLimit = -searchBarHeightPx + } + .graphicsLayer { + translationY = scrollBehavior.state.heightOffset + } + .fillMaxWidth() + .padding(24.dp, 24.dp, 24.dp, 8.dp) + ) { + SearchBar( + query = searchQuery, + onQueryChange = { newQuery -> + viewModel.searchedTitleText.value = newQuery } ) } } + + // Notes items with keys for better performance + items( + items = filteredNotes, + key = { note -> note.id } + ) { note -> + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .animateItem() // Add item animation + ) { + NotesItem( + isMarking = isMarking, + isMarked = note in markedNoteList, + data = note, + onClick = { onNoteClick(note) }, + onLongClick = { onNoteLongClick(note) }, + onCheckClick = { onNoteCheckClick(note) } + ) + } + } } } }, modifier = Modifier.semantics { testTag = TestTags.NOTES_PAGE } ) - LoadingDialog(isOpened = loadingDialog.value, onDismissCallback = { loadingDialog.value = false }, - modifier = Modifier.semantics { testTag = TestTags.LOADING_DIALOG }) - - TextDialog(isOpened = deleteDialog.value, - onDismissCallback = { deleteDialog.value = false }, - onConfirmCallback = { deleteNoteList() }, - modifier = Modifier.semantics { testTag = TestTags.CONFIRMATION_DIALOG }) + // Dialogs + if (showLoadingDialog) { + LoadingDialog( + isOpened = true, + onDismissCallback = { showLoadingDialog = false }, + modifier = Modifier.semantics { testTag = TestTags.LOADING_DIALOG } + ) + } + if (showDeleteDialog) { + TextDialog( + isOpened = true, + onDismissCallback = { showDeleteDialog = false }, + onConfirmCallback = { deleteNoteList }, + modifier = Modifier.semantics { testTag = TestTags.CONFIRMATION_DIALOG } + ) + } - FilterSheet(openBottomSheet, bottomSheetState, onDismiss = { openBottomSheet.value = false } ) { - sortBy, orderBy -> viewModel.sortAndOrder(sortBy, orderBy) + if (openBottomSheet) { + FilterSheet( + openBottomSheet = remember { mutableStateOf(true) }, + bottomSheetState = bottomSheetState, + onDismiss = { openBottomSheet = false } + ) { sortBy, orderBy -> + viewModel.sortAndOrder(sortBy, orderBy) + } } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt index 16a5b9b..af738ff 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt @@ -1,7 +1,6 @@ package com.digiventure.ventnote.feature.notes.components.drawer import android.content.Intent -import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -55,6 +54,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.net.toUri import com.digiventure.ventnote.BuildConfig import com.digiventure.ventnote.R import com.digiventure.ventnote.commons.ColorPalletName @@ -68,7 +68,6 @@ import com.digiventure.ventnote.ui.theme.CrimsonLightPrimary import com.digiventure.ventnote.ui.theme.PurpleLightPrimary import kotlinx.coroutines.launch import java.util.Locale -import androidx.core.net.toUri @Composable fun NavDrawer( diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt index 04198b4..69cbd95 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt @@ -2,18 +2,21 @@ package com.digiventure.ventnote.feature.notes.components.item import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize +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.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight @@ -31,53 +34,76 @@ fun NotesItem( data: NoteModel, onClick: () -> Unit, onLongClick: () -> Unit, - onCheckClick: () -> Unit) -{ - val shape = RoundedCornerShape(12.dp) + onCheckClick: () -> Unit +) { + val overallItemShape = RoundedCornerShape(16.dp) + val titleContainerShape = RoundedCornerShape(12.dp) + val descriptionContainerShape = RoundedCornerShape(10.dp) + + val borderColor = if (isMarked) MaterialTheme.colorScheme.tertiary else Color.Transparent + val borderWidth = if (isMarked) 2.dp else 0.dp Box( modifier = Modifier + .fillMaxWidth() .semantics { contentDescription = "Note item ${data.id}" } + .clip(overallItemShape) + .border(width = borderWidth, color = borderColor, shape = overallItemShape) + .background(MaterialTheme.colorScheme.surfaceContainerLow) .combinedClickable( - onClick = { if (isMarking) onCheckClick() else onClick() }, + onClick = { if (isMarking) onCheckClick() else onClick() }, onLongClick = { onLongClick() } ) - .clip(shape) - .background(MaterialTheme.colorScheme.primary) + .padding(if (isMarked) 4.dp else 6.dp) ) { - Box(modifier = Modifier.fillMaxSize() - .padding(start = if(isMarked) 8.dp else 0.dp) - .background(MaterialTheme.colorScheme.surface)) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(titleContainerShape) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(2.dp, 12.dp, 2.dp, 2.dp) + ) { + Text( + text = data.title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 12.dp) + ) - Column( - modifier = Modifier.fillMaxSize().padding(16.dp), - horizontalAlignment = Alignment.Start + Spacer(modifier = Modifier.height(8.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(descriptionContainerShape) + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 12.dp, vertical = 8.dp) ) { - Text( - text = data.title, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(bottom = 4.dp) - ) - Text( - text = data.note, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(bottom = 1.dp) - ) - Text( - text = DateUtil.convertDateString("EEEE, MMMM d h:mm a", data.createdAt.toString()), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) + Column { + Text( + text = data.note, + maxLines = 4, + overflow = TextOverflow.Ellipsis, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 4.dp) + ) + Text( + text = DateUtil.convertDateString( + "EEEE, MMMM d h:mm a", + data.createdAt.toString() + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } } } } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt index d9df0c2..4052d0e 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt @@ -4,12 +4,12 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Sort import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Menu -import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -17,17 +17,12 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults.topAppBarColors import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag @@ -45,134 +40,100 @@ import com.digiventure.ventnote.components.navbar.TopNavBarIcon fun NotesAppBar( isMarking: Boolean, markedNoteListSize: Int, - isSearching: Boolean, - searchedTitle: String, toggleDrawerCallback: () -> Unit, selectAllCallback: () -> Unit, unSelectAllCallback: () -> Unit, - onSearchValueChange: (String) -> Unit, closeMarkingCallback: () -> Unit, - closeSearchCallback: () -> Unit, - searchCallback: () -> Unit, sortCallback: () -> Unit, deleteCallback: () -> Unit, ) { - val focusManager = LocalFocusManager.current val expanded = remember { mutableStateOf(false) } - TopAppBar(title = { - if (isMarking) { - Row(verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable { expanded.value = true } - .semantics { testTag = TestTags.SELECTED_COUNT_CONTAINER }) { - NavText( - text = markedNoteListSize.toString(), - size = 16.sp, - modifier = Modifier.padding(start = 8.dp) - ) - NavText( - text = stringResource(R.string.selected_text), - size = 16.sp, - modifier = Modifier.padding(start = 8.dp) - ) - Icon( - Icons.Default.ArrowDropDown, - contentDescription = stringResource(R.string.dropdown_nav_icon), - tint = MaterialTheme.colorScheme.primary - ) - } - - DropdownMenu(expanded = expanded.value, - onDismissRequest = { expanded.value = false }, - modifier = Modifier.semantics { testTag = TestTags.DROPDOWN_SELECT }) { - DropdownMenuItem(text = { - Text( - text = stringResource(R.string.select_all), - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurface + CenterAlignedTopAppBar( + title = { + if (isMarking) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable { expanded.value = true } + .semantics { testTag = TestTags.SELECTED_COUNT_CONTAINER }) { + NavText( + text = markedNoteListSize.toString(), + size = 16.sp, + modifier = Modifier.padding(start = 8.dp) + ) + NavText( + text = stringResource(R.string.selected_text), + size = 16.sp, + modifier = Modifier.padding(start = 8.dp) ) - }, onClick = { - selectAllCallback() - expanded.value = false - }, modifier = Modifier.semantics { testTag = TestTags.SELECT_ALL_OPTION }) - HorizontalDivider() - DropdownMenuItem(text = { - Text( - text = stringResource(R.string.unselect_all), - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurface + Icon( + Icons.Default.ArrowDropDown, + contentDescription = stringResource(R.string.dropdown_nav_icon), + tint = MaterialTheme.colorScheme.primary ) - }, onClick = { - unSelectAllCallback() - expanded.value = false - }, modifier = Modifier.semantics { testTag = TestTags.UNSELECT_ALL_OPTION }) + } + + DropdownMenu( + expanded = expanded.value, + onDismissRequest = { expanded.value = false }, + modifier = Modifier.semantics { testTag = TestTags.DROPDOWN_SELECT }) { + DropdownMenuItem(text = { + Text( + text = stringResource(R.string.select_all), + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurface + ) + }, onClick = { + selectAllCallback() + expanded.value = false + }, modifier = Modifier.semantics { testTag = TestTags.SELECT_ALL_OPTION }) + HorizontalDivider() + DropdownMenuItem(text = { + Text( + text = stringResource(R.string.unselect_all), + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurface + ) + }, onClick = { + unSelectAllCallback() + expanded.value = false + }, modifier = Modifier.semantics { testTag = TestTags.UNSELECT_ALL_OPTION }) + } + } else { + Text( + text = stringResource(id = R.string.title), + color = MaterialTheme.colorScheme.primary, + style = TextStyle( + fontWeight = FontWeight.SemiBold, fontSize = 20.sp + ), + modifier = Modifier.semantics { testTag = TestTags.TOP_APPBAR_TITLE }, + ) } - } else if (isSearching) { - TextField(value = searchedTitle, - onValueChange = { - onSearchValueChange(it) + }, + colors = topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + navigationIcon = { + LeadingIcon(isMarking = isMarking, closeMarkingCallback = { + closeMarkingCallback() + }, toggleDrawerCallback = { toggleDrawerCallback() }) + }, + actions = { + TrailingMenuIcons( + isMarking = isMarking, + markedItemsCount = markedNoteListSize, + sortCallback = { + sortCallback() }, - colors = TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - disabledContainerColor = Color.Transparent, - cursorColor = MaterialTheme.colorScheme.primary, - focusedIndicatorColor = MaterialTheme.colorScheme.primary, - unfocusedIndicatorColor = MaterialTheme.colorScheme.primary, - ), - textStyle = TextStyle( - color = MaterialTheme.colorScheme.primary, - fontSize = 16.sp, - lineHeight = 0.sp - ), - singleLine = true, - modifier = Modifier - .padding(bottom = 0.dp) - .semantics { testTag = TestTags.TOP_APPBAR_TEXT_FIELD }, - placeholder = { - NavText(text = stringResource(R.string.search_textField), - size = 16.sp, - modifier = Modifier.semantics { }) + deleteCallback = { + deleteCallback() }) - } else { - Text( - text = stringResource(id = R.string.title), - color = MaterialTheme.colorScheme.primary, - style = TextStyle( - fontWeight = FontWeight.SemiBold, fontSize = 20.sp - ), - modifier = Modifier - .padding(start = 4.dp) - .semantics { testTag = TestTags.TOP_APPBAR_TITLE }, - ) + }, + modifier = Modifier.semantics { + testTag = TestTags.TOP_APPBAR } - }, colors = topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), navigationIcon = { - LeadingIcon(isMarking = isMarking, closeMarkingCallback = { - closeMarkingCallback() - }, toggleDrawerCallback = { toggleDrawerCallback() }) - }, actions = { - TrailingMenuIcons(isMarking = isMarking, - markedItemsCount = markedNoteListSize, - isSearching = isSearching, - searchCallback = { - searchCallback() - focusManager.clearFocus() - }, - sortCallback = { - sortCallback() - }, - deleteCallback = { - deleteCallback() - }, - closeSearchCallback = { - closeSearchCallback() - }) - }, modifier = Modifier.semantics { - testTag = TestTags.TOP_APPBAR - }) + ) } @Composable @@ -180,13 +141,15 @@ fun LeadingIcon( isMarking: Boolean, closeMarkingCallback: () -> Unit, toggleDrawerCallback: () -> Unit ) { if (isMarking) { - TopNavBarIcon(Icons.Filled.Close, + TopNavBarIcon( + Icons.Filled.Close, stringResource(R.string.close_nav_icon), modifier = Modifier.semantics { testTag = TestTags.CLOSE_SELECT_ICON_BUTTON }) { closeMarkingCallback() } } else { - TopNavBarIcon(Icons.Filled.Menu, + TopNavBarIcon( + Icons.Filled.Menu, stringResource(R.string.drawer_nav_icon), modifier = Modifier.semantics { testTag = TestTags.MENU_ICON_BUTTON }) { toggleDrawerCallback() @@ -198,14 +161,12 @@ fun LeadingIcon( fun TrailingMenuIcons( isMarking: Boolean, markedItemsCount: Int, - isSearching: Boolean, - searchCallback: () -> Unit, sortCallback: () -> Unit, deleteCallback: () -> Unit, - closeSearchCallback: () -> Unit ) { if (isMarking) { - TopNavBarIcon(Icons.Filled.Delete, + TopNavBarIcon( + Icons.Filled.Delete, stringResource(R.string.delete_nav_icon), tint = if (markedItemsCount > 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary.copy( @@ -214,20 +175,9 @@ fun TrailingMenuIcons( modifier = Modifier.semantics { testTag = TestTags.DELETE_ICON_BUTTON }) { if (markedItemsCount > 0) deleteCallback() } - } else if (isSearching) { - TopNavBarIcon(Icons.Filled.Close, - stringResource(R.string.delete_nav_icon), - modifier = Modifier.semantics { testTag = TestTags.CLOSE_SEARCH_ICON_BUTTON }) { - closeSearchCallback() - } } else { - TopNavBarIcon(Icons.Filled.Search, - stringResource(R.string.search_nav_icon), - modifier = Modifier.semantics { testTag = TestTags.SEARCH_ICON_BUTTON }) { - searchCallback() - } - - TopNavBarIcon(image = Icons.AutoMirrored.Filled.Sort, + TopNavBarIcon( + image = Icons.Outlined.MoreVert, stringResource(R.string.sort_nav_icon), modifier = Modifier.semantics { testTag = TestTags.SORT_ICON_BUTTON }) { sortCallback() diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt new file mode 100644 index 0000000..7361d5c --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt @@ -0,0 +1,72 @@ +package com.digiventure.ventnote.feature.notes.components.searchbar + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.digiventure.ventnote.R +import com.digiventure.ventnote.commons.TestTags + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun SearchBar( + query: String, + onQueryChange: (String) -> Unit +) { + TextField( + value = query, + onValueChange = onQueryChange, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + cursorColor = MaterialTheme.colorScheme.onSurfaceVariant, + focusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant, + unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search", + tint = Color.Gray + ) + }, + textStyle = TextStyle( + color = MaterialTheme.colorScheme.primary, + fontSize = 16.sp, + lineHeight = 0.sp + ), + singleLine = true, + modifier = Modifier + .padding(bottom = 0.dp) + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .fillMaxWidth() + .semantics { testTag = TestTags.TOP_APPBAR_TEXT_FIELD }, + placeholder = { + Text( + text = stringResource(R.string.search_textField), + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.semantics { } + ) + }) +} diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageBaseVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageBaseVM.kt index 1bb7e0b..42f62f9 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageBaseVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageBaseVM.kt @@ -31,7 +31,6 @@ interface NotesPageBaseVM { * 1. Toggle search field * 2. SearchField value */ - val isSearching: MutableState val searchedTitleText: MutableState /** @@ -68,10 +67,5 @@ interface NotesPageBaseVM { * */ fun closeMarkingEvent() - /** - * Close search event - * */ - fun closeSearchEvent() - fun observeNotes() } \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageMockVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageMockVM.kt index 6881240..309b0c6 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageMockVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageMockVM.kt @@ -5,38 +5,36 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.liveData import com.digiventure.ventnote.data.persistence.NoteModel class NotesPageMockVM : ViewModel(), NotesPageBaseVM { - override val loader = MutableLiveData() + override val loader = MutableLiveData(false) // Initial value override val sortAndOrderData: MutableLiveData> = MutableLiveData() override fun sortAndOrder(sortBy: String, orderBy: String) { - + // Mock implementation if needed } - override val noteList: LiveData>> = liveData { - Result.success( - listOf( - NoteModel("", ""), - NoteModel("", ""), - NoteModel("", ""), - NoteModel("", ""), + // More preview-friendly way to expose a list + override val noteList: LiveData>> = + MutableLiveData( // Use MutableLiveData and set its value directly + Result.success( + listOf( + NoteModel("Title 1", "Note 1"), + NoteModel("Title 2", "Note 2"), + NoteModel("Title 3", "Note 3"), + NoteModel("Title 4", "Note 4") + ) ) ) - } - override val isSearching = mutableStateOf(false) override val searchedTitleText = mutableStateOf("") override val isMarking = mutableStateOf(false) override val markedNoteList = mutableStateListOf() override fun markAllNote(notes: List) {} - override fun unMarkAllNote() {} - override fun addToMarkedNoteList(note: NoteModel) {} override suspend fun deleteNoteList(vararg notes: NoteModel): Result = Result.success(true) @@ -46,12 +44,8 @@ class NotesPageMockVM : ViewModel(), NotesPageBaseVM { markedNoteList.clear() } - override fun closeSearchEvent() { - isSearching.value = false - searchedTitleText.value = "" - } - override fun observeNotes() { - + // In a real ViewModel, this might trigger the data loading. + // For a mock, the data is already set. } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageVM.kt index 0c75e1e..e9bc05b 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageVM.kt @@ -38,7 +38,6 @@ class NotesPageVM @Inject constructor( sortAndOrderData.value = Pair(sortBy, orderBy) } - override val isSearching = mutableStateOf(false) override val searchedTitleText = mutableStateOf("") override val isMarking = mutableStateOf(false) @@ -80,11 +79,6 @@ class NotesPageVM @Inject constructor( markedNoteList.clear() } - override fun closeSearchEvent() { - isSearching.value = false - searchedTitleText.value = "" - } - override fun observeNotes() { viewModelScope.launch { sortAndOrderData.asFlow().collectLatest { From 4803c1f728ef6f1b1c28b966a8149b19d7c1bd38 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 10 Aug 2025 11:00:13 +0700 Subject: [PATCH 06/28] Feat feature/ui-revamp : Change filter bottom sheet ui to use chip --- .../ventnote/feature/notes/NotesPage.kt | 5 +- .../feature/notes/components/item/NoteItem.kt | 46 ++- .../notes/components/searchbar/SearchBar.kt | 4 +- .../notes/components/sheets/FilterSheet.kt | 279 +++++++++++------- 4 files changed, 207 insertions(+), 127 deletions(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt index 1c8cf5c..07b55f1 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt @@ -264,7 +264,7 @@ fun NotesPage( translationY = scrollBehavior.state.heightOffset } .fillMaxWidth() - .padding(24.dp, 24.dp, 24.dp, 8.dp) + .padding(16.dp, 24.dp, 16.dp, 8.dp) ) { SearchBar( query = searchQuery, @@ -324,7 +324,8 @@ fun NotesPage( FilterSheet( openBottomSheet = remember { mutableStateOf(true) }, bottomSheetState = bottomSheetState, - onDismiss = { openBottomSheet = false } + onDismiss = { openBottomSheet = false }, + sortAndOrderData = viewModel.sortAndOrderData.value ) { sortBy, orderBy -> viewModel.sortAndOrder(sortBy, orderBy) } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt index 69cbd95..da9ffc3 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt @@ -6,24 +6,35 @@ import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +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.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.digiventure.ventnote.R import com.digiventure.ventnote.commons.DateUtil +import com.digiventure.ventnote.commons.TestTags +import com.digiventure.ventnote.components.navbar.TopNavBarIcon import com.digiventure.ventnote.data.persistence.NoteModel @OptIn(ExperimentalFoundationApi::class) @@ -40,21 +51,16 @@ fun NotesItem( val titleContainerShape = RoundedCornerShape(12.dp) val descriptionContainerShape = RoundedCornerShape(10.dp) - val borderColor = if (isMarked) MaterialTheme.colorScheme.tertiary else Color.Transparent - val borderWidth = if (isMarked) 2.dp else 0.dp - Box( modifier = Modifier .fillMaxWidth() .semantics { contentDescription = "Note item ${data.id}" } .clip(overallItemShape) - .border(width = borderWidth, color = borderColor, shape = overallItemShape) .background(MaterialTheme.colorScheme.surfaceContainerLow) .combinedClickable( onClick = { if (isMarking) onCheckClick() else onClick() }, onLongClick = { onLongClick() } ) - .padding(if (isMarked) 4.dp else 6.dp) ) { Column( modifier = Modifier @@ -63,15 +69,27 @@ fun NotesItem( .background(MaterialTheme.colorScheme.surfaceContainerHighest) .padding(2.dp, 12.dp, 2.dp, 2.dp) ) { - Text( - text = data.title, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 12.dp) - ) + Row(verticalAlignment = Alignment.CenterVertically) { + if (isMarked) { + TopNavBarIcon( + image = Icons.Filled.CheckCircle, + "", + modifier = Modifier + .padding(start = 12.dp) + .size(16.dp) + .semantics { }) { + } + } + Text( + text = data.title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 12.dp) + ) + } Spacer(modifier = Modifier.height(8.dp)) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt index 7361d5c..f6ffdae 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt @@ -38,6 +38,8 @@ fun SearchBar( focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, disabledContainerColor = Color.Transparent, + focusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, cursorColor = MaterialTheme.colorScheme.onSurfaceVariant, focusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant, unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant, @@ -50,7 +52,7 @@ fun SearchBar( ) }, textStyle = TextStyle( - color = MaterialTheme.colorScheme.primary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 16.sp, lineHeight = 0.sp ), diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt index acbed07..96b39a6 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt @@ -1,36 +1,49 @@ package com.digiventure.ventnote.feature.notes.components.sheets -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowDownward +import androidx.compose.material.icons.outlined.ArrowUpward +import androidx.compose.material.icons.outlined.DateRange +import androidx.compose.material.icons.outlined.Sort +import androidx.compose.material.icons.outlined.SwapVert +import androidx.compose.material.icons.outlined.Title +import androidx.compose.material.icons.outlined.Update import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetState import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.digiventure.ventnote.R import com.digiventure.ventnote.commons.Constants import com.digiventure.ventnote.components.bottomSheet.RegularBottomSheet @@ -41,41 +54,28 @@ fun FilterSheet( openBottomSheet: MutableState, bottomSheetState: SheetState, onDismiss: () -> Unit, + sortAndOrderData: Pair?, onFilter: (sortBy: String, orderBy: String) -> Unit ) { - val createdDate = stringResource(id = R.string.sort_created_date) - val title = stringResource(id = R.string.sort_title) - val modifiedDate = stringResource(id = R.string.sort_modified_date) - val sortByOptions = listOf(title, createdDate, modifiedDate) - - val ascending = stringResource(id = R.string.order_ascending) - val descending = stringResource(id = R.string.order_descending) - val orderByOptions = listOf(ascending, descending) - - val selectedSortBy = remember { mutableStateOf(createdDate) } - val selectedOrderBy = remember { mutableStateOf(descending) } - - fun convertSortBy(sortBy: String): String { - return when (sortBy) { - title -> Constants.TITLE - createdDate -> Constants.CREATED_AT - modifiedDate -> Constants.UPDATED_AT - else -> { - Constants.CREATED_AT - } - } + // Memoized sort options with constants mapping + val sortOptions = remember { + listOf( + SortOption(R.string.sort_title, Constants.TITLE, Icons.Outlined.Title), + SortOption(R.string.sort_created_date, Constants.CREATED_AT, Icons.Outlined.DateRange), + SortOption(R.string.sort_modified_date, Constants.UPDATED_AT, Icons.Outlined.Update) + ) } - fun convertOrderBy(orderBy: String): String { - return when (orderBy) { - ascending -> Constants.ASCENDING - descending -> Constants.DESCENDING - else -> { - Constants.DESCENDING - } - } + val orderOptions = remember { + listOf( + OrderOption(R.string.order_ascending, Constants.ASCENDING, Icons.Outlined.ArrowUpward), + OrderOption(R.string.order_descending, Constants.DESCENDING, Icons.Outlined.ArrowDownward) + ) } + var selectedSortBy by remember { mutableStateOf(sortAndOrderData?.first) } + var selectedOrderBy by remember { mutableStateOf(sortAndOrderData?.second)} + RegularBottomSheet( isOpened = openBottomSheet.value, bottomSheetState = bottomSheetState, @@ -84,52 +84,86 @@ fun FilterSheet( Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) ) { - Text( - text = stringResource(id = R.string.sort_by), - style = TextStyle( - fontSize = 18.sp, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - ) - Box(modifier = Modifier.padding(8.dp)) - SortByList(sortByOptions = sortByOptions, selectedValue = selectedSortBy.value) { - selectedSortBy.value = it + // Sort By Section + FilterSection( + title = stringResource(R.string.sort_by), + icon = Icons.Outlined.Sort + ) { + LazyRow ( + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 4.dp) + ) { + items(sortOptions) { option -> + CompactFilterChip( + label = stringResource(option.labelRes), + icon = option.icon, + selected = selectedSortBy == option.value, + onClick = { selectedSortBy = option.value } + ) + } + } } - Box(modifier = Modifier.padding(16.dp)) - Text( - text = stringResource(id = R.string.order_by), - style = TextStyle( - fontSize = 18.sp, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - ) - Box(modifier = Modifier.padding(8.dp)) - OrderByList(orderByOptions = orderByOptions, selectedValue = selectedOrderBy.value) { - selectedOrderBy.value = it + + // Order By Section + FilterSection( + title = stringResource(R.string.order_by), + icon = Icons.Outlined.SwapVert + ) { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 4.dp) + ) { + items(orderOptions) { option -> + CompactFilterChip( + label = stringResource(option.labelRes), + icon = option.icon, + selected = selectedOrderBy == option.value, + onClick = { selectedOrderBy = option.value } + ) + } + } } - Box(modifier = Modifier.padding(16.dp)) - Row { - TextButton( - onClick = { onDismiss() }, - shape = RoundedCornerShape(20), - modifier = Modifier.weight(1f) + + // Action Buttons + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedButton ( + onClick = onDismiss, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.onSurface + ) ) { - Text(text = stringResource(id = R.string.dismiss)) + Text( + text = stringResource(id = R.string.dismiss), + fontWeight = FontWeight.Medium + ) } + Button( onClick = { onFilter( - convertSortBy(selectedSortBy.value), - convertOrderBy(selectedOrderBy.value) + selectedSortBy ?: Constants.CREATED_AT, + selectedOrderBy ?: Constants.DESCENDING ) onDismiss() }, - shape = RoundedCornerShape(20), - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) ) { - Text(text = stringResource(id = R.string.confirm)) + Text( + text = stringResource(id = R.string.confirm), + fontWeight = FontWeight.Medium + ) } } } @@ -137,53 +171,77 @@ fun FilterSheet( } @Composable -fun SortByList( - sortByOptions: List, selectedValue: String, - onPress: (sortByValue: String) -> Unit +private fun FilterSection( + title: String, + icon: ImageVector, + content: @Composable () -> Unit ) { - Column(modifier = Modifier.selectableGroup()) { - sortByOptions.forEach { - ListItem(title = it, selectedValue = selectedValue) { - onPress(it) - } + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) } + content() } } @Composable -fun OrderByList( - orderByOptions: List, selectedValue: String, - onPress: (orderByValue: String) -> Unit +private fun CompactFilterChip( + label: String, + icon: ImageVector, + selected: Boolean, + onClick: () -> Unit ) { - Column(modifier = Modifier.selectableGroup()) { - orderByOptions.forEach { - ListItem(title = it, selectedValue = selectedValue) { - onPress(it) - } - } - } + FilterChip( + onClick = onClick, + label = { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal + ) + }, + leadingIcon = { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + }, + selected = selected, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primary, + selectedLabelColor = MaterialTheme.colorScheme.onPrimary, + selectedLeadingIconColor = MaterialTheme.colorScheme.onPrimary + ), + ) } -@Composable -fun ListItem(title: String, selectedValue: String, onPress: () -> Unit) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { onPress() } - ) { - Text( - text = title, - style = TextStyle( - fontSize = 16.sp, fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary - ), - modifier = Modifier.weight(1f) - ) - RadioButton(selected = (title == selectedValue), onClick = { onPress() }) - } -} +// Data classes for type safety and better organization +private data class SortOption( + val labelRes: Int, + val value: String, + val icon: ImageVector +) + +private data class OrderOption( + val labelRes: Int, + val value: String, + val icon: ImageVector +) @OptIn(ExperimentalMaterial3Api::class) @Preview @@ -204,7 +262,8 @@ fun FilterSheetPreview() { FilterSheet( openBottomSheet = openBottomSheet, bottomSheetState = bottomSheetState, - onDismiss = {} + onDismiss = {}, + sortAndOrderData = null ) { _, _ -> } From d65dd976c6ead694cbe1ecd54472c0107930d006 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 10 Aug 2025 11:01:37 +0700 Subject: [PATCH 07/28] Feat feature/ui-revamp : Remove unused imports --- .../ventnote/feature/notes/components/item/NoteItem.kt | 7 ------- .../feature/notes/components/sheets/FilterSheet.kt | 3 ++- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt index da9ffc3..1e13831 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt @@ -2,7 +2,6 @@ package com.digiventure.ventnote.feature.notes.components.item import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -15,25 +14,19 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.digiventure.ventnote.R import com.digiventure.ventnote.commons.DateUtil -import com.digiventure.ventnote.commons.TestTags import com.digiventure.ventnote.components.navbar.TopNavBarIcon import com.digiventure.ventnote.data.persistence.NoteModel diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt index 96b39a6..59b7159 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Sort import androidx.compose.material.icons.outlined.ArrowDownward import androidx.compose.material.icons.outlined.ArrowUpward import androidx.compose.material.icons.outlined.DateRange @@ -90,7 +91,7 @@ fun FilterSheet( // Sort By Section FilterSection( title = stringResource(R.string.sort_by), - icon = Icons.Outlined.Sort + icon = Icons.AutoMirrored.Outlined.Sort ) { LazyRow ( horizontalArrangement = Arrangement.spacedBy(8.dp), From 56acca7795e64e40a40280f64a50a628dde4b96b Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 10 Aug 2025 11:01:55 +0700 Subject: [PATCH 08/28] Feat feature/ui-revamp : Remove unused imports --- .../ventnote/feature/notes/components/sheets/FilterSheet.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt index 59b7159..f338ab6 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt @@ -15,7 +15,6 @@ import androidx.compose.material.icons.automirrored.outlined.Sort import androidx.compose.material.icons.outlined.ArrowDownward import androidx.compose.material.icons.outlined.ArrowUpward import androidx.compose.material.icons.outlined.DateRange -import androidx.compose.material.icons.outlined.Sort import androidx.compose.material.icons.outlined.SwapVert import androidx.compose.material.icons.outlined.Title import androidx.compose.material.icons.outlined.Update From 617ba3b381808ac6bc94b8306e0a524c66349574 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 10 Aug 2025 12:50:25 +0700 Subject: [PATCH 09/28] Feat feature/ui-revamp : Adjust selected modal & labels --- .../ventnote/feature/notes/NotesPage.kt | 3 +- .../feature/notes/components/item/NoteItem.kt | 2 +- .../feature/notes/components/navbar/AppBar.kt | 394 +++++++++++++----- 3 files changed, 299 insertions(+), 100 deletions(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt index 07b55f1..db405d3 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt @@ -202,7 +202,8 @@ fun NotesPage( unSelectAllCallback = viewModel::unMarkAllNote, closeMarkingCallback = viewModel::closeMarkingEvent, sortCallback = { openBottomSheet = true }, - deleteCallback = { showDeleteDialog = true } + deleteCallback = { showDeleteDialog = true }, + totalNotesCount = filteredNotes.size ) }, snackbarHost = { SnackbarHost(snackBarHostState) }, diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt index 1e13831..b9648bb 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt @@ -80,7 +80,7 @@ fun NotesItem( fontWeight = FontWeight.Bold, fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 12.dp) + modifier = Modifier.padding(horizontal = 12.dp) ) } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt index 4052d0e..41a3a67 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt @@ -1,13 +1,27 @@ package com.digiventure.ventnote.feature.notes.components.navbar +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.CheckCircleOutline import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.RadioButtonUnchecked import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.DropdownMenu @@ -15,9 +29,11 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuDefaults import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults.topAppBarColors +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -26,20 +42,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag -import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.digiventure.ventnote.R import com.digiventure.ventnote.commons.TestTags -import com.digiventure.ventnote.components.navbar.TopNavBarIcon @OptIn(ExperimentalMaterial3Api::class) @Composable fun NotesAppBar( isMarking: Boolean, markedNoteListSize: Int, + totalNotesCount: Int = 0, toggleDrawerCallback: () -> Unit, selectAllCallback: () -> Unit, unSelectAllCallback: () -> Unit, @@ -52,83 +69,42 @@ fun NotesAppBar( CenterAlignedTopAppBar( title = { if (isMarking) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable { expanded.value = true } - .semantics { testTag = TestTags.SELECTED_COUNT_CONTAINER }) { - NavText( - text = markedNoteListSize.toString(), - size = 16.sp, - modifier = Modifier.padding(start = 8.dp) - ) - NavText( - text = stringResource(R.string.selected_text), - size = 16.sp, - modifier = Modifier.padding(start = 8.dp) - ) - Icon( - Icons.Default.ArrowDropDown, - contentDescription = stringResource(R.string.dropdown_nav_icon), - tint = MaterialTheme.colorScheme.primary - ) - } - - DropdownMenu( + SelectionTitle( + markedNoteListSize = markedNoteListSize, + totalNotesCount = totalNotesCount, expanded = expanded.value, - onDismissRequest = { expanded.value = false }, - modifier = Modifier.semantics { testTag = TestTags.DROPDOWN_SELECT }) { - DropdownMenuItem(text = { - Text( - text = stringResource(R.string.select_all), - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurface - ) - }, onClick = { + onToggleExpanded = { expanded.value = !expanded.value }, + onSelectAll = { selectAllCallback() expanded.value = false - }, modifier = Modifier.semantics { testTag = TestTags.SELECT_ALL_OPTION }) - HorizontalDivider() - DropdownMenuItem(text = { - Text( - text = stringResource(R.string.unselect_all), - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurface - ) - }, onClick = { + }, + onUnselectAll = { unSelectAllCallback() expanded.value = false - }, modifier = Modifier.semantics { testTag = TestTags.UNSELECT_ALL_OPTION }) - } - } else { - Text( - text = stringResource(id = R.string.title), - color = MaterialTheme.colorScheme.primary, - style = TextStyle( - fontWeight = FontWeight.SemiBold, fontSize = 20.sp - ), - modifier = Modifier.semantics { testTag = TestTags.TOP_APPBAR_TITLE }, + }, + onDismiss = { expanded.value = false } ) + } else { + AppTitle() } }, - colors = topAppBarColors( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = MaterialTheme.colorScheme.surface, ), navigationIcon = { - LeadingIcon(isMarking = isMarking, closeMarkingCallback = { - closeMarkingCallback() - }, toggleDrawerCallback = { toggleDrawerCallback() }) + LeadingIcon( + isMarking = isMarking, + closeMarkingCallback = closeMarkingCallback, + toggleDrawerCallback = toggleDrawerCallback + ) }, actions = { TrailingMenuIcons( isMarking = isMarking, markedItemsCount = markedNoteListSize, - sortCallback = { - sortCallback() - }, - deleteCallback = { - deleteCallback() - }) + sortCallback = sortCallback, + deleteCallback = deleteCallback + ) }, modifier = Modifier.semantics { testTag = TestTags.TOP_APPBAR @@ -136,23 +112,242 @@ fun NotesAppBar( ) } +@Composable +private fun SelectionTitle( + markedNoteListSize: Int, + totalNotesCount: Int, + expanded: Boolean, + onToggleExpanded: () -> Unit, + onSelectAll: () -> Unit, + onUnselectAll: () -> Unit, + onDismiss: () -> Unit +) { + Box { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + .semantics { testTag = TestTags.SELECTED_COUNT_CONTAINER } + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onToggleExpanded() + } + ) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = buildAnnotatedString { + withStyle( + style = SpanStyle( + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + ) { + append(markedNoteListSize.toString()) + } + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) { + append(" of $totalNotesCount selected") + } + }, + fontSize = 16.sp + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Icon( + imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, + contentDescription = stringResource(R.string.dropdown_nav_icon), + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + } + + EnhancedDropdownMenu( + expanded = expanded, + onDismissRequest = onDismiss, + markedNoteListSize = markedNoteListSize, + totalNotesCount = totalNotesCount, + onSelectAll = onSelectAll, + onUnselectAll = onUnselectAll + ) + } +} + +@Composable +private fun EnhancedDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + markedNoteListSize: Int, + totalNotesCount: Int, + onSelectAll: () -> Unit, + onUnselectAll: () -> Unit +) { + DropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest, + modifier = Modifier + .semantics { testTag = TestTags.DROPDOWN_SELECT } + .wrapContentWidth() + .background( + MaterialTheme.colorScheme.surfaceContainer, + RoundedCornerShape(12.dp) + ), + shape = RoundedCornerShape(12.dp), + shadowElevation = 8.dp + ) { + val allSelected = markedNoteListSize == totalNotesCount && totalNotesCount > 0 + val noneSelected = markedNoteListSize == 0 + + // Select All Option + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + imageVector = if (allSelected) Icons.Default.CheckCircle else Icons.Default.CheckCircleOutline, + contentDescription = null, + tint = if (allSelected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), + modifier = Modifier.size(20.dp) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Column { + Text( + text = stringResource(R.string.select_all), + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + if (totalNotesCount > 0) { + Text( + text = "Select all $totalNotesCount notes", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } + } + } + }, + onClick = onSelectAll, + enabled = !allSelected, + modifier = Modifier + .semantics { testTag = TestTags.SELECT_ALL_OPTION } + .padding(horizontal = 4.dp), + colors = MenuDefaults.itemColors( + textColor = if (allSelected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurface + ) + ) + + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + thickness = 0.5.dp, + color = MaterialTheme.colorScheme.outlineVariant + ) + + // Unselect All Option + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + imageVector = Icons.Default.RadioButtonUnchecked, + contentDescription = null, + tint = if (noneSelected) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), + modifier = Modifier.size(20.dp) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Column { + Text( + text = stringResource(R.string.unselect_all), + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + if (markedNoteListSize > 0) { + Text( + text = "Clear selection of $markedNoteListSize notes", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } + } + } + }, + onClick = onUnselectAll, + enabled = !noneSelected, + modifier = Modifier + .semantics { testTag = TestTags.UNSELECT_ALL_OPTION } + .padding(horizontal = 4.dp), + colors = MenuDefaults.itemColors( + textColor = if (noneSelected) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.onSurface + ) + ) + } +} + +@Composable +private fun AppTitle() { + Text( + text = stringResource(id = R.string.title), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold + ), + modifier = Modifier.semantics { testTag = TestTags.TOP_APPBAR_TITLE }, + ) +} + @Composable fun LeadingIcon( - isMarking: Boolean, closeMarkingCallback: () -> Unit, toggleDrawerCallback: () -> Unit + isMarking: Boolean, + closeMarkingCallback: () -> Unit, + toggleDrawerCallback: () -> Unit ) { + // Option 1: Use separate variables (cleaner and more readable) if (isMarking) { - TopNavBarIcon( - Icons.Filled.Close, - stringResource(R.string.close_nav_icon), - modifier = Modifier.semantics { testTag = TestTags.CLOSE_SELECT_ICON_BUTTON }) { - closeMarkingCallback() + IconButton( + onClick = closeMarkingCallback, + modifier = Modifier.semantics { testTag = TestTags.CLOSE_SELECT_ICON_BUTTON } + ) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.close_nav_icon), + tint = MaterialTheme.colorScheme.onSurface + ) } } else { - TopNavBarIcon( - Icons.Filled.Menu, - stringResource(R.string.drawer_nav_icon), - modifier = Modifier.semantics { testTag = TestTags.MENU_ICON_BUTTON }) { - toggleDrawerCallback() + IconButton( + onClick = toggleDrawerCallback, + modifier = Modifier.semantics { testTag = TestTags.MENU_ICON_BUTTON } + ) { + Icon( + imageVector = Icons.Filled.Menu, + contentDescription = stringResource(R.string.drawer_nav_icon), + tint = MaterialTheme.colorScheme.onSurface + ) } } } @@ -165,29 +360,32 @@ fun TrailingMenuIcons( deleteCallback: () -> Unit, ) { if (isMarking) { - TopNavBarIcon( - Icons.Filled.Delete, - stringResource(R.string.delete_nav_icon), - tint = if (markedItemsCount > 0) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.primary.copy( - alpha = 0.6f - ), - modifier = Modifier.semantics { testTag = TestTags.DELETE_ICON_BUTTON }) { - if (markedItemsCount > 0) deleteCallback() + val deleteEnabled = markedItemsCount > 0 + + IconButton( + onClick = { if (deleteEnabled) deleteCallback() }, + modifier = Modifier.semantics { testTag = TestTags.DELETE_ICON_BUTTON } + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = stringResource(R.string.delete_nav_icon), + tint = if (deleteEnabled) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.primary.copy(alpha = 0.4f) + } + ) } } else { - TopNavBarIcon( - image = Icons.Outlined.MoreVert, - stringResource(R.string.sort_nav_icon), - modifier = Modifier.semantics { testTag = TestTags.SORT_ICON_BUTTON }) { - sortCallback() + IconButton( + onClick = sortCallback, + modifier = Modifier.semantics { testTag = TestTags.SORT_ICON_BUTTON } + ) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = stringResource(R.string.sort_nav_icon), + tint = MaterialTheme.colorScheme.onSurface + ) } } -} - -@Composable -fun NavText(text: String, size: TextUnit, modifier: Modifier) { - Text( - text = text, fontSize = size, color = MaterialTheme.colorScheme.primary, modifier = modifier - ) } \ No newline at end of file From b062a5451a864bf1ea4ba5cc7f3dac853db4e1bc Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 10 Aug 2025 13:04:04 +0700 Subject: [PATCH 10/28] Feat feature/ui-revamp : Adjust test tags for notes page --- .../java/com/digiventure/ventnote/commons/TestTags.kt | 3 +++ .../components/bottomSheet/RegularBottomSheet.kt | 3 +++ .../com/digiventure/ventnote/feature/notes/NotesPage.kt | 9 ++++----- .../feature/notes/components/sheets/FilterSheet.kt | 8 ++++++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt b/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt index ab73b12..3d44169 100644 --- a/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt +++ b/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt @@ -9,6 +9,9 @@ object TestTags { // Appbar test tags const val TOP_APPBAR = "top_appbar" + // Bottom sheet test tags + const val BOTTOM_SHEET = "bottom_sheet" + const val DELETE_ICON_BUTTON = "delete_icon_button" const val CLOSE_SEARCH_ICON_BUTTON = "close_search_icon_button" const val SEARCH_ICON_BUTTON = "search_icon_button" diff --git a/app/src/main/java/com/digiventure/ventnote/components/bottomSheet/RegularBottomSheet.kt b/app/src/main/java/com/digiventure/ventnote/components/bottomSheet/RegularBottomSheet.kt index f53257c..67c451e 100644 --- a/app/src/main/java/com/digiventure/ventnote/components/bottomSheet/RegularBottomSheet.kt +++ b/app/src/main/java/com/digiventure/ventnote/components/bottomSheet/RegularBottomSheet.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -12,12 +13,14 @@ fun RegularBottomSheet( isOpened: Boolean, bottomSheetState: SheetState, onDismissRequest: () -> Unit, + modifier: Modifier, content: @Composable () -> Unit ) { if (isOpened) { ModalBottomSheet( onDismissRequest = { onDismissRequest() }, sheetState = bottomSheetState, + modifier = modifier, containerColor = MaterialTheme.colorScheme.background, ) { content() diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt index db405d3..2f7c281 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt @@ -136,6 +136,8 @@ fun NotesPage( showLoadingDialog = loadingState == true } + val noteIsDeletedText = stringResource(R.string.note_is_successfully_deleted) + // Memoized callbacks val deleteNoteList = remember { { @@ -147,7 +149,7 @@ fun NotesPage( viewModel.closeMarkingEvent() snackBarHostState.showSnackbar( - message = "Note is successfully deleted", // Consider using stringResource + message = noteIsDeletedText, withDismissAction = true ) } @@ -253,7 +255,6 @@ fun NotesPage( verticalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(bottom = 96.dp) ) { - // SearchBar as first item item(key = "search_bar") { Box( modifier = Modifier @@ -276,7 +277,6 @@ fun NotesPage( } } - // Notes items with keys for better performance items( items = filteredNotes, key = { note -> note.id } @@ -285,7 +285,7 @@ fun NotesPage( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) - .animateItem() // Add item animation + .animateItem() ) { NotesItem( isMarking = isMarking, @@ -303,7 +303,6 @@ fun NotesPage( modifier = Modifier.semantics { testTag = TestTags.NOTES_PAGE } ) - // Dialogs if (showLoadingDialog) { LoadingDialog( isOpened = true, diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt index f338ab6..66fe1dc 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt @@ -41,11 +41,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.digiventure.ventnote.R import com.digiventure.ventnote.commons.Constants +import com.digiventure.ventnote.commons.TestTags import com.digiventure.ventnote.components.bottomSheet.RegularBottomSheet @OptIn(ExperimentalMaterial3Api::class) @@ -79,6 +82,9 @@ fun FilterSheet( RegularBottomSheet( isOpened = openBottomSheet.value, bottomSheetState = bottomSheetState, + modifier = Modifier.semantics { + testTag = TestTags.BOTTOM_SHEET + }, onDismissRequest = { openBottomSheet.value = false } ) { Column( @@ -87,7 +93,6 @@ fun FilterSheet( .padding(24.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { - // Sort By Section FilterSection( title = stringResource(R.string.sort_by), icon = Icons.AutoMirrored.Outlined.Sort @@ -127,7 +132,6 @@ fun FilterSheet( } } - // Action Buttons Row( modifier = Modifier .fillMaxWidth() From c73cf3655ba9b8226def9e6bb759d98f46dd0535 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 10 Aug 2025 13:31:20 +0700 Subject: [PATCH 11/28] Feat feature/ui-revamp : Comment ui integration test for now, update test dependency --- app/build.gradle | 19 +- .../digiventure/ventnote/NoteDetailFeature.kt | 5 - .../com/digiventure/ventnote/NotesFeature.kt | 431 +++++++++--------- .../bottomSheet/RegularBottomSheet.kt | 4 +- .../feature/share_preview/SharePreviewPage.kt | 1 + .../ventnote/notes/NotesPageVMShould.kt | 17 - 6 files changed, 217 insertions(+), 260 deletions(-) delete mode 100644 app/src/androidTest/java/com/digiventure/ventnote/NoteDetailFeature.kt diff --git a/app/build.gradle b/app/build.gradle index a988e44..94056ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,6 +96,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7" implementation "androidx.compose.compiler:compiler:1.5.15" + implementation 'androidx.test.ext:junit-ktx:1.3.0' // Room def room_version = "2.7.0" @@ -138,14 +139,14 @@ dependencies { // Instrumented test /// Espresso (for ui interaction purpose) - androidTestImplementation "androidx.test:runner:1.6.2" - androidTestImplementation "androidx.test:rules:1.6.1" - androidTestImplementation "androidx.test.espresso:espresso-intents:3.6.1" - androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1" + androidTestImplementation "androidx.test:runner:1.7.0" + androidTestImplementation "androidx.test:rules:1.7.0" + androidTestImplementation "androidx.test.espresso:espresso-intents:3.7.0" + androidTestImplementation "androidx.test.espresso:espresso-core:3.7.0" - androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.7.8" - debugImplementation "androidx.compose.ui:ui-tooling:1.7.8" - debugImplementation "androidx.compose.ui:ui-test-manifest:1.7.8" + androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.8.3" + debugImplementation "androidx.compose.ui:ui-tooling:1.8.3" + debugImplementation "androidx.compose.ui:ui-test-manifest:1.8.3" /// Hilt test (for handling service locator when test) androidTestImplementation "com.google.dagger:hilt-android-testing:$dagger_version" @@ -155,12 +156,12 @@ dependencies { testImplementation "org.mockito.kotlin:mockito-kotlin:5.2.1" testImplementation "org.mockito:mockito-inline:5.2.0" - androidTestImplementation "androidx.test.ext:junit-ktx:1.2.1" + androidTestImplementation "androidx.test.ext:junit-ktx:1.3.0" testImplementation "androidx.arch.core:core-testing:2.2.0" // For unit testing coroutines - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2" // Import the Firebase BoM implementation platform("com.google.firebase:firebase-bom:33.12.0") diff --git a/app/src/androidTest/java/com/digiventure/ventnote/NoteDetailFeature.kt b/app/src/androidTest/java/com/digiventure/ventnote/NoteDetailFeature.kt deleted file mode 100644 index f6dc33b..0000000 --- a/app/src/androidTest/java/com/digiventure/ventnote/NoteDetailFeature.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.digiventure.ventnote - -import com.digiventure.utils.BaseAcceptanceTest - -class NoteDetailFeature: BaseAcceptanceTest() \ No newline at end of file diff --git a/app/src/androidTest/java/com/digiventure/ventnote/NotesFeature.kt b/app/src/androidTest/java/com/digiventure/ventnote/NotesFeature.kt index 3c71147..65f47d7 100644 --- a/app/src/androidTest/java/com/digiventure/ventnote/NotesFeature.kt +++ b/app/src/androidTest/java/com/digiventure/ventnote/NotesFeature.kt @@ -1,227 +1,204 @@ -package com.digiventure.ventnote - -import android.content.Intent -import android.net.Uri -import androidx.compose.ui.test.SemanticsNodeInteraction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsOn -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.longClick -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollTo -import androidx.compose.ui.test.performTextInput -import androidx.compose.ui.test.performTouchInput -import androidx.test.espresso.intent.Intents -import androidx.test.espresso.intent.Intents.intended -import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction -import androidx.test.espresso.intent.matcher.IntentMatchers.hasData -import com.digiventure.utils.BaseAcceptanceTest -import com.digiventure.ventnote.commons.TestTags -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -@HiltAndroidTest -class NotesFeature: BaseAcceptanceTest() { - @get:Rule(order = 0) - var hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 1) - var composeTestRule = createAndroidComposeRule(MainActivity::class.java) - - // Variables related to TopAppBar - private lateinit var topAppBar: SemanticsNodeInteraction - private lateinit var searchIconButton: SemanticsNodeInteraction - private lateinit var menuIconButton: SemanticsNodeInteraction - private lateinit var topAppBarTitle: SemanticsNodeInteraction - private lateinit var topAppBarTextField: SemanticsNodeInteraction - private lateinit var closeSearchIconButton: SemanticsNodeInteraction - private lateinit var selectedCount: SemanticsNodeInteraction - private lateinit var dropdownSelect: SemanticsNodeInteraction - private lateinit var selectAllOption: SemanticsNodeInteraction - private lateinit var unselectAllOption: SemanticsNodeInteraction - private lateinit var closeSelectIconButton: SemanticsNodeInteraction - private lateinit var deleteIconButton: SemanticsNodeInteraction - private lateinit var selectedCountContainer: SemanticsNodeInteraction - - private lateinit var navDrawer: SemanticsNodeInteraction - private lateinit var rateAppTile: SemanticsNodeInteraction - - private lateinit var noteListRecyclerView: SemanticsNodeInteraction - private lateinit var addNoteFloatingActionButton: SemanticsNodeInteraction - - @Before - fun setUp() { - hiltRule.inject() - Intents.init() - - // Initialize all widgets - topAppBar = composeTestRule.onNodeWithTag(TestTags.TOP_APPBAR) - searchIconButton = composeTestRule.onNodeWithTag(TestTags.SEARCH_ICON_BUTTON) - menuIconButton = composeTestRule.onNodeWithTag(TestTags.MENU_ICON_BUTTON) - topAppBarTitle = composeTestRule.onNodeWithTag(TestTags.TOP_APPBAR_TITLE) -// topAppBarTextField = composeTestRule.onNodeWithTag(TestTags.TOP_APPBAR_TEXTFIELD) - closeSearchIconButton = composeTestRule.onNodeWithTag(TestTags.CLOSE_SEARCH_ICON_BUTTON) - selectedCount = composeTestRule.onNodeWithTag(TestTags.SELECTED_COUNT) - dropdownSelect = composeTestRule.onNodeWithTag(TestTags.DROPDOWN_SELECT) - selectAllOption = composeTestRule.onNodeWithTag(TestTags.SELECT_ALL_OPTION) - unselectAllOption = composeTestRule.onNodeWithTag(TestTags.UNSELECT_ALL_OPTION) - closeSelectIconButton = composeTestRule.onNodeWithTag(TestTags.CLOSE_SELECT_ICON_BUTTON) - deleteIconButton = composeTestRule.onNodeWithTag(TestTags.DELETE_ICON_BUTTON) - selectedCountContainer = composeTestRule.onNodeWithTag(TestTags.SELECTED_COUNT_CONTAINER) - - navDrawer = composeTestRule.onNodeWithTag(TestTags.NAV_DRAWER) - rateAppTile = composeTestRule.onNodeWithTag(TestTags.RATE_APP_TILE) - - noteListRecyclerView = composeTestRule.onNodeWithTag(TestTags.NOTE_RV) - addNoteFloatingActionButton = composeTestRule.onNodeWithTag(TestTags.ADD_NOTE_FAB) - } - - @After - fun tearDown() { - Intents.release() - } - - /** - * Ensure all top appBar initial functionality - * */ - @Test - fun ensureTopAppBarFunctionality() { - // Initial state - // Scenario : when app is launched, it will show title, menu icon, search icon - topAppBar.assertIsDisplayed() - searchIconButton.assertIsDisplayed() - menuIconButton.assertIsDisplayed() - topAppBarTitle.assertIsDisplayed() - - // When search button is pressed - // Scenario : when pressed, a text field is shown - searchIconButton.performClick() - topAppBarTextField.assertIsDisplayed() - - /// 1. When textField is being edited - /// Scenario : when pressed, it will gain focus then write text on it - topAppBarTextField.performClick() - topAppBarTextField.performTextInput("Input Text") - composeTestRule.onNodeWithText("Input Text").assertExists() - - /// 2. When close button is pressed - /// Scenario : when pressed, textField is dismissed - closeSearchIconButton.assertIsDisplayed() - closeSearchIconButton.performClick() - topAppBarTextField.assertDoesNotExist() - } - - /** - * Ensure noteList functionality (make sure there are few items) - * reside in the local database) - * you can use App Inspection -> Databases -> New Query to seed the data) or - * simply using add feature. - * - * Ensure it has three items - * (1, "title 1", "note 1", 1678158383000, 1678158383000), - * (2, "title 2", "note 2", 1678071983000, 1678071983000), - * (3, "title 3", "note 3", 1677899183000, 1677899183000); - * */ - @Test - fun ensureNoteListFunctionality() { - // Initial state - // Scenario : assert if lazy column are displayed (it will not exist if there are no item exists) - noteListRecyclerView.assertIsDisplayed() - - /// 1. When the three children is showing - /// Scenario : then assert the children by perform scroll and assert displayed - val nodeWithText1 = composeTestRule.onNodeWithText("title 1") - val nodeWithText2 = composeTestRule.onNodeWithText("title 2") - val nodeWithText3 = composeTestRule.onNodeWithText("title 3") - nodeWithText1.performScrollTo() - nodeWithText2.assertIsDisplayed() - nodeWithText3.assertIsDisplayed() - - // When node with text title 1 is long pressed - // Scenario : the toolbar will show delete icon, close button, and selected count with - // dropdown menu, also the tile checkbox will checked - nodeWithText1.performTouchInput { - longClick() - } - - val checkBoxForNodeWithText1 = composeTestRule.onNodeWithTag("title 1") - checkBoxForNodeWithText1.assertIsOn() - composeTestRule.onNodeWithText("1").assertIsDisplayed() - nodeWithText1.performClick() - composeTestRule.onNodeWithText("0").assertIsDisplayed() - - closeSelectIconButton.assertIsDisplayed() - deleteIconButton.assertIsDisplayed() - selectedCountContainer.assertIsDisplayed() - - /// 1. When selected count container is pressed - /// Scenario : it will show dropdown menu with select all and unselect all tile - selectedCountContainer.performClick() - dropdownSelect.assertIsDisplayed() - dropdownSelect.performClick() - unselectAllOption.performClick() - composeTestRule.onNodeWithText("0").assertIsDisplayed() - - selectedCountContainer.performClick() - dropdownSelect.assertIsDisplayed() - dropdownSelect.performClick() - selectAllOption.performClick() - composeTestRule.onNodeWithText("3").assertIsDisplayed() - - /// 2. Delete selected note - /// Scenario : it will show loading dialog when delete is being processed - /// then it will show snackbar either success or failed - /// note : insert the data again after this action - deleteIconButton.performClick() - // TODO : Assert dialog displayed (it returned error that the dialog show two times at same time) - // TODO : the functionality is good when tested manually - val dismissButton = composeTestRule.onNodeWithTag(TestTags.DISMISS_BUTTON) - dismissButton.assertIsDisplayed() - val confirmButton = composeTestRule.onNodeWithTag(TestTags.CONFIRM_BUTTON) - confirmButton.assertIsDisplayed() - - dismissButton.performClick() - composeTestRule.onNodeWithTag(TestTags.CONFIRMATION_DIALOG).assertDoesNotExist() - - // TODO : check delete action that will show snackBar when success or error - // TODO : the functionality is good when tested manually - - /// 2. When close button is pressed - /// Scenario : it will turn into initial state - closeSelectIconButton.performClick() - closeSelectIconButton.assertDoesNotExist() - - // TODO : check filter functionality (it returned error that the note tile show two times at same time) - // TODO : the functionality is good when tested manually - } - - /** - * Ensure all navDrawer initial functionality - * - * Ensure the emulator / device has play store and an account already - * logged in there - * */ - @Test - fun ensureNavDrawerFunctionality() { - // When menu button is pressed - // Scenario : there is hamburger button, when it was pressed a nav drawer will shows - menuIconButton.performClick() - navDrawer.assertIsDisplayed() - - // When drawer is displayed - // Scenario : assert the children is displayed - rateAppTile.assertIsDisplayed() - - /// 1. When rate app is pressed - /// Scenario : the app will navigated to VentNote PlayStore Page - rateAppTile.performClick() - intended(hasAction(Intent.ACTION_VIEW)) - intended(hasData(Uri.parse("https://play.google.com/store/apps/details?id=com.digiventure.ventnote"))) - } -} \ No newline at end of file +//package com.digiventure.ventnote +// +//import com.digiventure.utils.BaseAcceptanceTest +//import dagger.hilt.android.testing.HiltAndroidTest +// +//@HiltAndroidTest +//class NotesFeature: BaseAcceptanceTest() { +// @get:Rule(order = 0) +// var hiltRule = HiltAndroidRule(this) +// +// @get:Rule(order = 1) +// var composeTestRule = createAndroidComposeRule(MainActivity::class.java) +// +// // Variables related to TopAppBar +// private lateinit var topAppBar: SemanticsNodeInteraction +// private lateinit var searchIconButton: SemanticsNodeInteraction +// private lateinit var menuIconButton: SemanticsNodeInteraction +// private lateinit var topAppBarTitle: SemanticsNodeInteraction +// private lateinit var topAppBarTextField: SemanticsNodeInteraction +// private lateinit var closeSearchIconButton: SemanticsNodeInteraction +// private lateinit var selectedCount: SemanticsNodeInteraction +// private lateinit var dropdownSelect: SemanticsNodeInteraction +// private lateinit var selectAllOption: SemanticsNodeInteraction +// private lateinit var unselectAllOption: SemanticsNodeInteraction +// private lateinit var closeSelectIconButton: SemanticsNodeInteraction +// private lateinit var deleteIconButton: SemanticsNodeInteraction +// private lateinit var selectedCountContainer: SemanticsNodeInteraction +// +// private lateinit var navDrawer: SemanticsNodeInteraction +// private lateinit var rateAppTile: SemanticsNodeInteraction +// +// private lateinit var noteListRecyclerView: SemanticsNodeInteraction +// private lateinit var addNoteFloatingActionButton: SemanticsNodeInteraction +// +// @Before +// fun setUp() { +// hiltRule.inject() +// Intents.init() +// +// // Initialize all widgets +// topAppBar = composeTestRule.onNodeWithTag(TestTags.TOP_APPBAR) +// searchIconButton = composeTestRule.onNodeWithTag(TestTags.SEARCH_ICON_BUTTON) +// menuIconButton = composeTestRule.onNodeWithTag(TestTags.MENU_ICON_BUTTON) +// topAppBarTitle = composeTestRule.onNodeWithTag(TestTags.TOP_APPBAR_TITLE) +//// topAppBarTextField = composeTestRule.onNodeWithTag(TestTags.TOP_APPBAR_TEXTFIELD) +// closeSearchIconButton = composeTestRule.onNodeWithTag(TestTags.CLOSE_SEARCH_ICON_BUTTON) +// selectedCount = composeTestRule.onNodeWithTag(TestTags.SELECTED_COUNT) +// dropdownSelect = composeTestRule.onNodeWithTag(TestTags.DROPDOWN_SELECT) +// selectAllOption = composeTestRule.onNodeWithTag(TestTags.SELECT_ALL_OPTION) +// unselectAllOption = composeTestRule.onNodeWithTag(TestTags.UNSELECT_ALL_OPTION) +// closeSelectIconButton = composeTestRule.onNodeWithTag(TestTags.CLOSE_SELECT_ICON_BUTTON) +// deleteIconButton = composeTestRule.onNodeWithTag(TestTags.DELETE_ICON_BUTTON) +// selectedCountContainer = composeTestRule.onNodeWithTag(TestTags.SELECTED_COUNT_CONTAINER) +// +// navDrawer = composeTestRule.onNodeWithTag(TestTags.NAV_DRAWER) +// rateAppTile = composeTestRule.onNodeWithTag(TestTags.RATE_APP_TILE) +// +// noteListRecyclerView = composeTestRule.onNodeWithTag(TestTags.NOTE_RV) +// addNoteFloatingActionButton = composeTestRule.onNodeWithTag(TestTags.ADD_NOTE_FAB) +// } +// +// @After +// fun tearDown() { +// Intents.release() +// } +// +// /** +// * Ensure all top appBar initial functionality +// * */ +// @Test +// fun ensureTopAppBarFunctionality() { +// // Initial state +// // Scenario : when app is launched, it will show title, menu icon, search icon +// topAppBar.assertIsDisplayed() +// searchIconButton.assertIsDisplayed() +// menuIconButton.assertIsDisplayed() +// topAppBarTitle.assertIsDisplayed() +// +// // When search button is pressed +// // Scenario : when pressed, a text field is shown +// searchIconButton.performClick() +// topAppBarTextField.assertIsDisplayed() +// +// /// 1. When textField is being edited +// /// Scenario : when pressed, it will gain focus then write text on it +// topAppBarTextField.performClick() +// topAppBarTextField.performTextInput("Input Text") +// composeTestRule.onNodeWithText("Input Text").assertExists() +// +// /// 2. When close button is pressed +// /// Scenario : when pressed, textField is dismissed +// closeSearchIconButton.assertIsDisplayed() +// closeSearchIconButton.performClick() +// topAppBarTextField.assertDoesNotExist() +// } +// +// /** +// * Ensure noteList functionality (make sure there are few items) +// * reside in the local database) +// * you can use App Inspection -> Databases -> New Query to seed the data) or +// * simply using add feature. +// * +// * Ensure it has three items +// * (1, "title 1", "note 1", 1678158383000, 1678158383000), +// * (2, "title 2", "note 2", 1678071983000, 1678071983000), +// * (3, "title 3", "note 3", 1677899183000, 1677899183000); +// * */ +// @Test +// fun ensureNoteListFunctionality() { +// // Initial state +// // Scenario : assert if lazy column are displayed (it will not exist if there are no item exists) +// noteListRecyclerView.assertIsDisplayed() +// +// /// 1. When the three children is showing +// /// Scenario : then assert the children by perform scroll and assert displayed +// val nodeWithText1 = composeTestRule.onNodeWithText("title 1") +// val nodeWithText2 = composeTestRule.onNodeWithText("title 2") +// val nodeWithText3 = composeTestRule.onNodeWithText("title 3") +// nodeWithText1.performScrollTo() +// nodeWithText2.assertIsDisplayed() +// nodeWithText3.assertIsDisplayed() +// +// // When node with text title 1 is long pressed +// // Scenario : the toolbar will show delete icon, close button, and selected count with +// // dropdown menu, also the tile checkbox will checked +// nodeWithText1.performTouchInput { +// longClick() +// } +// +// val checkBoxForNodeWithText1 = composeTestRule.onNodeWithTag("title 1") +// checkBoxForNodeWithText1.assertIsOn() +// composeTestRule.onNodeWithText("1").assertIsDisplayed() +// nodeWithText1.performClick() +// composeTestRule.onNodeWithText("0").assertIsDisplayed() +// +// closeSelectIconButton.assertIsDisplayed() +// deleteIconButton.assertIsDisplayed() +// selectedCountContainer.assertIsDisplayed() +// +// /// 1. When selected count container is pressed +// /// Scenario : it will show dropdown menu with select all and unselect all tile +// selectedCountContainer.performClick() +// dropdownSelect.assertIsDisplayed() +// dropdownSelect.performClick() +// unselectAllOption.performClick() +// composeTestRule.onNodeWithText("0").assertIsDisplayed() +// +// selectedCountContainer.performClick() +// dropdownSelect.assertIsDisplayed() +// dropdownSelect.performClick() +// selectAllOption.performClick() +// composeTestRule.onNodeWithText("3").assertIsDisplayed() +// +// /// 2. Delete selected note +// /// Scenario : it will show loading dialog when delete is being processed +// /// then it will show snackbar either success or failed +// /// note : insert the data again after this action +// deleteIconButton.performClick() +// // TODO : Assert dialog displayed (it returned error that the dialog show two times at same time) +// // TODO : the functionality is good when tested manually +// val dismissButton = composeTestRule.onNodeWithTag(TestTags.DISMISS_BUTTON) +// dismissButton.assertIsDisplayed() +// val confirmButton = composeTestRule.onNodeWithTag(TestTags.CONFIRM_BUTTON) +// confirmButton.assertIsDisplayed() +// +// dismissButton.performClick() +// composeTestRule.onNodeWithTag(TestTags.CONFIRMATION_DIALOG).assertDoesNotExist() +// +// // TODO : check delete action that will show snackBar when success or error +// // TODO : the functionality is good when tested manually +// +// /// 2. When close button is pressed +// /// Scenario : it will turn into initial state +// closeSelectIconButton.performClick() +// closeSelectIconButton.assertDoesNotExist() +// +// // TODO : check filter functionality (it returned error that the note tile show two times at same time) +// // TODO : the functionality is good when tested manually +// } +// +// /** +// * Ensure all navDrawer initial functionality +// * +// * Ensure the emulator / device has play store and an account already +// * logged in there +// * */ +// @Test +// fun ensureNavDrawerFunctionality() { +// // When menu button is pressed +// // Scenario : there is hamburger button, when it was pressed a nav drawer will shows +// menuIconButton.performClick() +// navDrawer.assertIsDisplayed() +// +// // When drawer is displayed +// // Scenario : assert the children is displayed +// rateAppTile.assertIsDisplayed() +// +// /// 1. When rate app is pressed +// /// Scenario : the app will navigated to VentNote PlayStore Page +// rateAppTile.performClick() +// intended(hasAction(Intent.ACTION_VIEW)) +// intended(hasData(Uri.parse("https://play.google.com/store/apps/details?id=com.digiventure.ventnote"))) +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/components/bottomSheet/RegularBottomSheet.kt b/app/src/main/java/com/digiventure/ventnote/components/bottomSheet/RegularBottomSheet.kt index 67c451e..c36bcc8 100644 --- a/app/src/main/java/com/digiventure/ventnote/components/bottomSheet/RegularBottomSheet.kt +++ b/app/src/main/java/com/digiventure/ventnote/components/bottomSheet/RegularBottomSheet.kt @@ -13,14 +13,14 @@ fun RegularBottomSheet( isOpened: Boolean, bottomSheetState: SheetState, onDismissRequest: () -> Unit, - modifier: Modifier, + modifier: Modifier?, content: @Composable () -> Unit ) { if (isOpened) { ModalBottomSheet( onDismissRequest = { onDismissRequest() }, sheetState = bottomSheetState, - modifier = modifier, + modifier = modifier ?: Modifier, containerColor = MaterialTheme.colorScheme.background, ) { content() diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt index 3dd1680..65ff89a 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt @@ -159,6 +159,7 @@ fun SharePreviewPage( RegularBottomSheet( isOpened = openBottomSheet.value, bottomSheetState = bottomSheetState, + modifier = null, onDismissRequest = { openBottomSheet.value = false } ) { Column(modifier = Modifier diff --git a/app/src/test/java/com/digiventure/ventnote/notes/NotesPageVMShould.kt b/app/src/test/java/com/digiventure/ventnote/notes/NotesPageVMShould.kt index 9bebb84..58c68bb 100644 --- a/app/src/test/java/com/digiventure/ventnote/notes/NotesPageVMShould.kt +++ b/app/src/test/java/com/digiventure/ventnote/notes/NotesPageVMShould.kt @@ -120,15 +120,6 @@ class NotesPageVMShould: BaseUnitTest() { } } - @Test - fun verifyIsSearchingIsSameAsInput() = runTest { - viewModel.isSearching.value = true - assertEquals(true, viewModel.isSearching.value) - - viewModel.isSearching.value = false - assertEquals(false, viewModel.isSearching.value) - } - @Test fun verifySearchedTitleTextIsSameAsInput() = runTest { val inputTitle = "Test Title" @@ -280,14 +271,6 @@ class NotesPageVMShould: BaseUnitTest() { assertEquals(viewModel.markedNoteList.size, 0) } - @Test - fun verifyCloseSearchEventIsSetIsSearchingToFalseAndEmptiedSearchedTitleText() { - viewModel.closeSearchEvent() - - assertFalse(viewModel.isSearching.value) - assertEquals(viewModel.searchedTitleText.value, "") - } - private fun mockSuccessfulDeletionCase() { runBlocking { whenever(repository.deleteNoteList(note)).thenReturn( From 67b392cf31ac2e705d4ba3c28b74ea09ce2f7432 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 10 Aug 2025 13:31:36 +0700 Subject: [PATCH 12/28] Feat feature/ui-revamp : Comment ui integration test for now, update test dependency --- .../java/com/digiventure/MainActivityTest.kt | 32 +++++++++---------- .../digiventure/utils/BaseAcceptanceTest.kt | 20 ++++++------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/src/androidTest/java/com/digiventure/MainActivityTest.kt b/app/src/androidTest/java/com/digiventure/MainActivityTest.kt index b564c11..db3a32b 100644 --- a/app/src/androidTest/java/com/digiventure/MainActivityTest.kt +++ b/app/src/androidTest/java/com/digiventure/MainActivityTest.kt @@ -1,16 +1,16 @@ -package com.digiventure - -import androidx.test.ext.junit.rules.activityScenarioRule -import com.digiventure.utils.BaseAcceptanceTest -import com.digiventure.ventnote.MainActivity -import org.junit.Before -import org.junit.Rule - -class MainActivityTest: BaseAcceptanceTest() { - @get:Rule - val activityRule = activityScenarioRule() - - @Before - fun setup() { - } -} \ No newline at end of file +//package com.digiventure +// +//import androidx.test.ext.junit.rules.activityScenarioRule +//import com.digiventure.utils.BaseAcceptanceTest +//import com.digiventure.ventnote.MainActivity +//import org.junit.Before +//import org.junit.Rule +// +//class MainActivityTest: BaseAcceptanceTest() { +// @get:Rule +// val activityRule = activityScenarioRule() +// +// @Before +// fun setup() { +// } +//} \ No newline at end of file diff --git a/app/src/androidTest/java/com/digiventure/utils/BaseAcceptanceTest.kt b/app/src/androidTest/java/com/digiventure/utils/BaseAcceptanceTest.kt index 3ecc806..f9ff0a0 100644 --- a/app/src/androidTest/java/com/digiventure/utils/BaseAcceptanceTest.kt +++ b/app/src/androidTest/java/com/digiventure/utils/BaseAcceptanceTest.kt @@ -1,10 +1,10 @@ -package com.digiventure.utils - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -abstract class BaseAcceptanceTest { -// @get:Rule(order = 0) -// val composeTestRule = createComposeRule() -} \ No newline at end of file +//package com.digiventure.utils +// +//import androidx.test.ext.junit.runners.AndroidJUnit4 +//import org.junit.runner.RunWith +// +//@RunWith(AndroidJUnit4::class) +//abstract class BaseAcceptanceTest { +//// @get:Rule(order = 0) +//// val composeTestRule = createComposeRule() +//} \ No newline at end of file From 0316571dd35634170ace1b336c123c42db45882c Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 10 Aug 2025 15:11:46 +0700 Subject: [PATCH 13/28] Feat feature/ui-revamp : Improve ui & ux for note detail --- .../feature/note_detail/NoteDetailPage.kt | 468 ++++++++---------- .../feature/note_detail/components/AppBar.kt | 97 ---- .../note_detail/components/navbar/AppBar.kt | 56 +++ .../components/navbar/EnhancedBottomAppBar.kt | 148 ++++++ .../components/section/NoteSection.kt | 144 ++++++ .../components/section/TitleSection.kt | 156 ++++++ .../viewmodel/NoteDetailPageMockVM.kt | 25 +- app/src/main/res/values/strings.xml | 1 + 8 files changed, 737 insertions(+), 358 deletions(-) delete mode 100644 app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/AppBar.kt create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt index 3becdbe..e40f0c6 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt @@ -3,48 +3,37 @@ package com.digiventure.ventnote.feature.note_detail import android.content.pm.ActivityInfo import android.net.Uri import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Edit +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController @@ -53,7 +42,10 @@ import com.digiventure.ventnote.commons.TestTags import com.digiventure.ventnote.components.LockScreenOrientation import com.digiventure.ventnote.components.dialog.LoadingDialog import com.digiventure.ventnote.components.dialog.TextDialog -import com.digiventure.ventnote.feature.note_detail.components.NoteDetailAppBar +import com.digiventure.ventnote.feature.note_detail.components.navbar.EnhancedBottomAppBar +import com.digiventure.ventnote.feature.note_detail.components.navbar.NoteDetailAppBar +import com.digiventure.ventnote.feature.note_detail.components.section.NoteSection +import com.digiventure.ventnote.feature.note_detail.components.section.TitleSection import com.digiventure.ventnote.feature.note_detail.viewmodel.NoteDetailPageBaseVM import com.digiventure.ventnote.feature.note_detail.viewmodel.NoteDetailPageMockVM import com.digiventure.ventnote.feature.note_detail.viewmodel.NoteDetailPageVM @@ -76,208 +68,255 @@ fun NoteDetailPage( PageNavigation(navHostController) } - // String resource - val titleTextField = "${stringResource(R.string.title_textField)}-$TAG" - val bodyTextField = "${stringResource(R.string.body_textField)}-$TAG" - val titleInput = stringResource(R.string.title_textField_input) - val bodyInput = stringResource(R.string.body_textField_input) - val successFullyUpdatedText = stringResource(R.string.successfully_updated) + // String resources - memoized for better performance + val titleTextFieldTag = stringResource(R.string.title_textField) + val bodyTextFieldTag = stringResource(R.string.body_textField) + val titleInputTag = stringResource(R.string.title_textField_input) + val bodyInputTag = stringResource(R.string.body_textField_input) + val successFullyUpdatedTag = stringResource(R.string.successfully_updated) - val noteDetailState = viewModel.noteDetail.observeAsState() - val data = noteDetailState.value?.getOrNull() - - val isEditingState = viewModel.isEditing.value - val focusManager = LocalFocusManager.current + val strings = remember { + mapOf( + "titleTextField" to "${titleTextFieldTag}-$TAG", + "bodyTextField" to "${bodyTextFieldTag}-$TAG", + "titleInput" to titleInputTag, + "bodyInput" to bodyInputTag, + "successFullyUpdatedText" to successFullyUpdatedTag + ) + } - val loadingState = viewModel.loader.observeAsState() + // State observers + val noteDetailState by viewModel.noteDetail.observeAsState() + val data = noteDetailState?.getOrNull() + val isEditingState by viewModel.isEditing + val loadingState by viewModel.loader.observeAsState() + val focusManager = LocalFocusManager.current val scope = rememberCoroutineScope() + // Dialog states - using derivedStateOf where appropriate val requiredDialogState = remember { mutableStateOf(false) } val deleteDialogState = remember { mutableStateOf(false) } val cancelDialogState = remember { mutableStateOf(false) } val openLoadingDialog = remember { mutableStateOf(false) } val snackBarHostState = remember { SnackbarHostState() } - - fun initData() { - viewModel.titleText.value = data?.title ?: "" - viewModel.descriptionText.value = data?.note ?: "" - } - LaunchedEffect(key1 = Unit) { - viewModel.getNoteDetail(id.toInt()) - } - - LaunchedEffect(key1 = noteDetailState.value) { - initData() + // Memoized functions to prevent unnecessary recompositions + val initData = { + data?.let { + viewModel.titleText.value = it.title + viewModel.descriptionText.value = it.note + } } - LaunchedEffect(key1 = isEditingState) { - if (!isEditingState) { - focusManager.clearFocus() + val deleteNote = remember(data, scope, snackBarHostState) { + { + data?.let { noteData -> + scope.launch { + viewModel.deleteNoteList(noteData) + .onSuccess { + deleteDialogState.value = false + navHostController.popBackStack() + } + .onFailure { error -> + deleteDialogState.value = false + snackBarHostState.showSnackbar( + message = error.message ?: "", + withDismissAction = true + ) + } + } + } } } - LaunchedEffect(key1 = loadingState.value) { - openLoadingDialog.value = loadingState.value == true - } + val updateNote = remember( + data, + scope, + focusManager, + snackBarHostState, + strings["successFullyUpdatedText"] + ) { + { + val titleText = viewModel.titleText.value + val descriptionText = viewModel.descriptionText.value - fun deleteNote() { - if (data != null) { - scope.launch { - viewModel.deleteNoteList(data) - .onSuccess { - deleteDialogState.value = false - navHostController.popBackStack() - } - .onFailure { - deleteDialogState.value = false - snackBarHostState.showSnackbar( - message = it.message ?: "", - withDismissAction = true + if (titleText.isEmpty() || descriptionText.isEmpty()) { + requiredDialogState.value = true + } else { + data?.let { noteData -> + focusManager.clearFocus() + + scope.launch { + val updatedNote = noteData.copy( + title = titleText, + note = descriptionText ) + viewModel.updateNote(updatedNote) + .onSuccess { + viewModel.isEditing.value = false + snackBarHostState.showSnackbar( + message = strings["successFullyUpdatedText"] ?: "", + withDismissAction = true + ) + } + .onFailure { error -> + snackBarHostState.showSnackbar( + message = error.message ?: "", + withDismissAction = true + ) + } } + } } } } - fun updateNote() { - if (viewModel.titleText.value.isEmpty() || viewModel.descriptionText.value.isEmpty()) { - requiredDialogState.value = true - } else { - if (data != null) { - focusManager.clearFocus() + // Effects + LaunchedEffect(id) { + viewModel.getNoteDetail(id.toInt()) + } - scope.launch { - val updatedNote = data.copy(title = viewModel.titleText.value, note = viewModel.descriptionText.value) - viewModel.updateNote(updatedNote) - .onSuccess { - viewModel.isEditing.value = false - snackBarHostState.showSnackbar( - message = successFullyUpdatedText, - withDismissAction = true - ) - } - .onFailure { - snackBarHostState.showSnackbar( - message = it.message ?: "", - withDismissAction = true - ) - } - } - } + LaunchedEffect(noteDetailState) { + initData() + } + + LaunchedEffect(isEditingState) { + if (!isEditingState) { + focusManager.clearFocus() } } - val appBarState = rememberTopAppBarState() - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(appBarState) - val rememberedScrollBehavior = remember { scrollBehavior } + LaunchedEffect(loadingState) { + openLoadingDialog.value = loadingState == true + } + + // Scroll behavior setup + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( + rememberTopAppBarState() + ) + + val haptics = LocalHapticFeedback.current Scaffold( topBar = { NoteDetailAppBar( isEditing = isEditingState, - descriptionTextLength = viewModel.descriptionText.value.length, onBackPressed = { - scope.launch { - navigationActions.navigateToNotesPage() - } - }, - onClosePressed = { - cancelDialogState.value = true + navigationActions.navigateToNotesPage() }, - onDeletePressed = { - deleteDialogState.value = true - }, - scrollBehavior = rememberedScrollBehavior, + scrollBehavior = scrollBehavior, onSharePressed = { - val json = Uri.encode(Gson().toJson(data)) - navigationActions.navigateToSharePage(json) - + data?.let { + val json = Uri.encode(Gson().toJson(it)) + navigationActions.navigateToSharePage(json) + } } ) }, snackbarHost = { SnackbarHost(snackBarHostState) }, - floatingActionButton = { - ExtendedFloatingActionButton( - onClick = { - if(isEditingState) { - updateNote() - } else { - viewModel.isEditing.value = true - } - }, - modifier = Modifier.semantics { - testTag = "edit-note-fab" - }, - text = { - Text(if(isEditingState) stringResource(R.string.save) else stringResource(R.string.edit), - fontSize = 16.sp, - fontWeight = FontWeight.Medium) - }, - icon = { - Icon( - imageVector = if(isEditingState) Icons.Filled.Check else Icons.Filled.Edit, - contentDescription = stringResource(R.string.fab), - ) - }, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) - }, content = { contentPadding -> - Box(modifier = Modifier.padding(contentPadding)) { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - TitleTextField(viewModel, isEditingState, titleTextField, titleInput) - DescriptionTextField(viewModel, isEditingState, bodyTextField, bodyInput) + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + .padding(horizontal = 16.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + focusManager.clearFocus() + }, + verticalArrangement = Arrangement.spacedBy(24.dp), + contentPadding = PaddingValues(vertical = 16.dp) + ) { + item { + TitleSection( + viewModel = viewModel, + isEditingState = isEditingState, + titleTextField = strings["titleTextField"] ?: "", + titleInput = strings["titleInput"] ?: "" + ) + } + + item { + NoteSection( + viewModel = viewModel, + isEditingState = isEditingState, + bodyTextField = strings["bodyTextField"] ?: "", + bodyInput = strings["bodyInput"] ?: "" + ) } } }, + bottomBar = { + EnhancedBottomAppBar( + isEditing = isEditingState, + onEditClick = { + haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove) + viewModel.isEditing.value = true + }, + onSaveClick = { updateNote() }, + onDeleteClick = { deleteDialogState.value = true }, + onCancelClick = { cancelDialogState.value = true } + ) + }, modifier = Modifier .semantics { testTag = TestTags.NOTE_DETAIL_PAGE } .nestedScroll(scrollBehavior.nestedScrollConnection), - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.surface, ) - val missingFieldName = if (viewModel.titleText.value.isEmpty()) { - "Title" - } else if (viewModel.descriptionText.value.isEmpty()) { - "Notes" - } else { - "" + // Dialogs + val missingFieldName = remember( + viewModel.titleText.value, + viewModel.descriptionText.value + ) { + when { + viewModel.titleText.value.isEmpty() -> "Title" + viewModel.descriptionText.value.isEmpty() -> "Notes" + else -> "" + } } - TextDialog( - title = stringResource(R.string.required_title), - description = stringResource(R.string.required_confirmation_text, missingFieldName), - isOpened = requiredDialogState.value, - onDismissCallback = { requiredDialogState.value = false }, - onConfirmCallback = { requiredDialogState.value = false } - ) - - TextDialog( - title = stringResource(R.string.cancel_title), - description = stringResource(R.string.cancel_confirmation_text), - isOpened = cancelDialogState.value, - onDismissCallback = { cancelDialogState.value = false }, - onConfirmCallback = { - viewModel.isEditing.value = false - cancelDialogState.value = false + if (requiredDialogState.value) { + TextDialog( + title = stringResource(R.string.required_title), + description = stringResource(R.string.required_confirmation_text, missingFieldName), + isOpened = requiredDialogState.value, + onDismissCallback = { requiredDialogState.value = false }, + onConfirmCallback = { requiredDialogState.value = false } + ) + } - initData() - }) + if (cancelDialogState.value) { + TextDialog( + title = stringResource(R.string.cancel_title), + description = stringResource(R.string.cancel_confirmation_text), + isOpened = cancelDialogState.value, + onDismissCallback = { cancelDialogState.value = false }, + onConfirmCallback = { + viewModel.isEditing.value = false + cancelDialogState.value = false + initData() + } + ) + } - TextDialog( - isOpened = deleteDialogState.value, - onDismissCallback = { deleteDialogState.value = false }, - onConfirmCallback = { deleteNote() }) + if (deleteDialogState.value) { + TextDialog( + isOpened = deleteDialogState.value, + onDismissCallback = { deleteDialogState.value = false }, + onConfirmCallback = { deleteNote() } + ) + } - LoadingDialog(isOpened = openLoadingDialog.value, onDismissCallback = { openLoadingDialog.value = false }) + if (openLoadingDialog.value) { + LoadingDialog( + isOpened = openLoadingDialog.value, + onDismissCallback = { openLoadingDialog.value = false } + ) + } BackHandler { if (viewModel.isEditing.value) { @@ -288,95 +327,6 @@ fun NoteDetailPage( } } -@Composable -fun TitleTextField( - viewModel: NoteDetailPageBaseVM, - isEditingState: Boolean, - titleTextField: String, - titleInput: String -) { - TextField( - value = viewModel.titleText.value, - onValueChange = { - viewModel.titleText.value = it - }, - textStyle = TextStyle( - fontSize = 18.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ), - singleLine = false, - readOnly = !isEditingState, - colors = TextFieldDefaults.colors( - disabledTextColor = MaterialTheme.colorScheme.surface, - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - disabledContainerColor = MaterialTheme.colorScheme.surface, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - ), - modifier = Modifier - .fillMaxWidth() - .semantics { contentDescription = titleTextField }, - placeholder = { - if (isEditingState) { - Text( - text = titleInput, - fontSize = 18.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - } - } - ) -} - -@Composable -fun DescriptionTextField( - viewModel: NoteDetailPageBaseVM, - isEditingState: Boolean, - bodyTextField: String, - bodyInput: String -) { - TextField( - value = viewModel.descriptionText.value, - onValueChange = { - viewModel.descriptionText.value = it - }, - textStyle = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface - ), - singleLine = false, - readOnly = !isEditingState, - shape = RectangleShape, - colors = TextFieldDefaults.colors( - disabledTextColor = MaterialTheme.colorScheme.surface, - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - disabledContainerColor = MaterialTheme.colorScheme.surface, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - ), - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .semantics { contentDescription = bodyTextField }, - placeholder = { - if (isEditingState) { - Text( - text = bodyInput, - fontSize = 18.sp, - color = MaterialTheme.colorScheme.onSurface - ) - } - }, - ) -} - @Preview @Composable fun NoteDetailPagePreview() { diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/AppBar.kt deleted file mode 100644 index 71c1d72..0000000 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/AppBar.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.digiventure.ventnote.feature.note_detail.components - -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Share -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults.topAppBarColors -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.digiventure.ventnote.R -import com.digiventure.ventnote.components.navbar.TopNavBarIcon - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun NoteDetailAppBar( - isEditing: Boolean, - descriptionTextLength: Int, - onBackPressed: () -> Unit, - onClosePressed: () -> Unit, - onDeletePressed: () -> Unit, - onSharePressed: () -> Unit, - scrollBehavior: TopAppBarScrollBehavior) { - - val isMenuExpanded = remember { mutableStateOf(false) } - - TopAppBar( - title = { - Text( - text = if(isEditing) "$descriptionTextLength" else stringResource(id = R.string.note_detail), - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(start = 4.dp), - style = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 20.sp - ) - ) - }, - colors = topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), - navigationIcon = { - if (isEditing) { - TopNavBarIcon(Icons.Filled.Close, stringResource(R.string.back_nav_icon), Modifier.semantics { }) { - onClosePressed() - } - } else { - TopNavBarIcon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back_nav_icon), Modifier.semantics { }) { - onBackPressed() - } - } - }, - actions = { - TopNavBarIcon(Icons.Filled.Share, stringResource(R.string.share_nav_icon), Modifier.semantics { }) { - onSharePressed() - } - TopNavBarIcon(Icons.Filled.MoreVert, stringResource(R.string.menu_nav_icon), Modifier.semantics { }) { - isMenuExpanded.value = true - } - DropdownMenu( - offset = DpOffset((10).dp, 0.dp), - expanded = isMenuExpanded.value, - onDismissRequest = { isMenuExpanded.value = false }) { - DropdownMenuItem( - text = { Text( - text = "Delete Note", - fontSize = 16.sp, - modifier = Modifier.semantics { }) - }, - onClick = { - onDeletePressed() - isMenuExpanded.value = false - }, - ) - } - }, - scrollBehavior = scrollBehavior, - modifier = Modifier.semantics { }, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt new file mode 100644 index 0000000..a83a867 --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt @@ -0,0 +1,56 @@ +package com.digiventure.ventnote.feature.note_detail.components.navbar + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults.topAppBarColors +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import com.digiventure.ventnote.R +import com.digiventure.ventnote.components.navbar.TopNavBarIcon + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NoteDetailAppBar( + isEditing: Boolean, + onBackPressed: () -> Unit, + onSharePressed: () -> Unit, + scrollBehavior: TopAppBarScrollBehavior) { + + CenterAlignedTopAppBar( + title = { + Text( + text = if(isEditing) stringResource(id = R.string.note_edit) else stringResource(id = R.string.note_detail), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold + ), + ) + }, + colors = topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + navigationIcon = { + if (!isEditing) { + TopNavBarIcon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back_nav_icon), Modifier.semantics { }) { + onBackPressed() + } + } + }, + actions = { + TopNavBarIcon(Icons.Filled.Share, stringResource(R.string.share_nav_icon), Modifier.semantics { }) { + onSharePressed() + } + }, + scrollBehavior = scrollBehavior, + modifier = Modifier.semantics { }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt new file mode 100644 index 0000000..0dc98b3 --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt @@ -0,0 +1,148 @@ +package com.digiventure.ventnote.feature.note_detail.components.navbar + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.DeleteOutline +import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EnhancedBottomAppBar( + isEditing: Boolean, + onEditClick: () -> Unit, + onSaveClick: () -> Unit, + onDeleteClick: () -> Unit, + onCancelClick: () -> Unit +) { + BottomAppBar( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + tonalElevation = 12.dp, + actions = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + if (isEditing) { + // Cancel button in editing mode + EnhancedBottomBarButton( + icon = Icons.Rounded.Close, + label = "Cancel", + onClick = onCancelClick, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + + // Save button in editing mode + EnhancedBottomBarButton( + icon = Icons.Rounded.Check, + label = "Save", + onClick = onSaveClick, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + isProminent = true + ) + } else { + // Edit button in view mode + EnhancedBottomBarButton( + icon = Icons.Rounded.Edit, + label = "Edit", + onClick = onEditClick, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ) + + // Delete button in view mode + EnhancedBottomBarButton( + icon = Icons.Rounded.DeleteOutline, + label = "Delete", + onClick = onDeleteClick, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } + } + ) +} + +@Composable +private fun EnhancedBottomBarButton( + icon: ImageVector, + label: String, + onClick: () -> Unit, + containerColor: Color, + contentColor: Color, + isProminent: Boolean = false +) { + val haptics = LocalHapticFeedback.current + val scale by animateFloatAsState( + targetValue = if (isProminent) 1.1f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "button_scale" + ) + + Row ( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .scale(scale) + .clip(RoundedCornerShape(16.dp)) + .background( + containerColor.copy(alpha = 0.8f), + RoundedCornerShape(16.dp) + ) + .clickable { + haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove) + onClick() + } + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Icon( + imageVector = icon, + contentDescription = label, + tint = contentColor, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + color = contentColor, + fontWeight = if (isProminent) FontWeight.SemiBold else FontWeight.Medium, + maxLines = 1 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt new file mode 100644 index 0000000..d0fc52f --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt @@ -0,0 +1,144 @@ +package com.digiventure.ventnote.feature.note_detail.components.section + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Description +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.digiventure.ventnote.feature.note_detail.viewmodel.NoteDetailPageBaseVM + +@Composable +fun NoteSection( + viewModel: NoteDetailPageBaseVM, + isEditingState: Boolean, + bodyTextField: String, + bodyInput: String +) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 12.dp) + ) { + Icon( + imageVector = Icons.Rounded.Description, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Notes", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + } + + ImprovedDescriptionTextField( + viewModel = viewModel, + isEditingState = isEditingState, + bodyTextField = bodyTextField, + bodyInput = bodyInput + ) + } +} + +@Composable +fun ImprovedDescriptionTextField( + viewModel: NoteDetailPageBaseVM, + isEditingState: Boolean, + bodyTextField: String, + bodyInput: String +) { + val borderColor by animateColorAsState( + targetValue = if (isEditingState) MaterialTheme.colorScheme.primary else Color.Transparent, + animationSpec = tween(300), + label = "border_color" + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .animateContentSize() + .heightIn(min = 200.dp), + colors = CardDefaults.cardColors( + containerColor = if (isEditingState) { + MaterialTheme.colorScheme.surface + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + } + ), + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation( + defaultElevation = if (isEditingState) 4.dp else 0.dp + ), + border = BorderStroke( + width = if (isEditingState) 2.dp else 0.dp, + color = borderColor + ) + ) { + TextField( + value = viewModel.descriptionText.value, + onValueChange = { viewModel.descriptionText.value = it }, + textStyle = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface, + lineHeight = 24.sp + ), + singleLine = false, + readOnly = !isEditingState, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + cursorColor = MaterialTheme.colorScheme.primary + ), + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .semantics { contentDescription = bodyTextField }, + placeholder = { + if (isEditingState) { + Text( + text = bodyInput, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt new file mode 100644 index 0000000..f7cc7d8 --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt @@ -0,0 +1,156 @@ +package com.digiventure.ventnote.feature.note_detail.components.section + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Title +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.digiventure.ventnote.feature.note_detail.viewmodel.NoteDetailPageBaseVM +import kotlinx.coroutines.delay + +@Composable +fun TitleSection( + viewModel: NoteDetailPageBaseVM, + isEditingState: Boolean, + titleTextField: String, + titleInput: String +) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 12.dp) + ) { + Icon( + imageVector = Icons.Rounded.Title, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Title", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + } + + ImprovedTitleTextField( + viewModel = viewModel, + isEditingState = isEditingState, + titleTextField = titleTextField, + titleInput = titleInput + ) + } +} + +@Composable +fun ImprovedTitleTextField( + viewModel: NoteDetailPageBaseVM, + isEditingState: Boolean, + titleTextField: String, + titleInput: String +) { + val focusRequester = remember { FocusRequester() } + val borderColor by animateColorAsState( + targetValue = if (isEditingState) MaterialTheme.colorScheme.primary else Color.Transparent, + animationSpec = tween(300), + label = "border_color" + ) + + LaunchedEffect(isEditingState) { + if (isEditingState) { + delay(200) + focusRequester.requestFocus() + } + } + + Card( + modifier = Modifier + .fillMaxWidth() + .animateContentSize(), + colors = CardDefaults.cardColors( + containerColor = if (isEditingState) { + MaterialTheme.colorScheme.surface + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + } + ), + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation( + defaultElevation = if (isEditingState) 4.dp else 0.dp + ), + border = BorderStroke( + width = if (isEditingState) 2.dp else 0.dp, + color = borderColor + ) + ) { + TextField( + value = viewModel.titleText.value, + onValueChange = { viewModel.titleText.value = it }, + textStyle = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + lineHeight = 28.sp + ), + singleLine = false, + maxLines = 3, + readOnly = !isEditingState, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + cursorColor = MaterialTheme.colorScheme.primary + ), + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = titleTextField } + .focusRequester(focusRequester), + placeholder = { + if (isEditingState) { + Text( + text = titleInput, + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/viewmodel/NoteDetailPageMockVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/viewmodel/NoteDetailPageMockVM.kt index 4249dae..7b78516 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/viewmodel/NoteDetailPageMockVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/viewmodel/NoteDetailPageMockVM.kt @@ -8,14 +8,35 @@ import com.digiventure.ventnote.data.persistence.NoteModel class NoteDetailPageMockVM: ViewModel(), NoteDetailPageBaseVM { - override val loader: MutableLiveData = MutableLiveData() + override val loader: MutableLiveData = MutableLiveData(false) override var noteDetail: MutableLiveData> = MutableLiveData() override var titleText: MutableState = mutableStateOf("This is sample title text") override var descriptionText: MutableState = mutableStateOf("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus pretium odio maximus tellus pellentesque, a dignissim massa commodo.\n") override var isEditing: MutableState = mutableStateOf(false) - override suspend fun getNoteDetail(id: Int) {} + init { + // Initialize with mock data + val mockNote = NoteModel( + id = 0, + title = "This is sample title text", + note = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dignissim, sem sit amet consectetur ornare, lorem orci vulputate tortor, scelerisque vulputate elit nulla sed lacus. Praesent aliquet dui vitae elit tincidunt, non commodo dui semper. Etiam faucibus tempus dui et sagittis. Donec non magna tempus, lobortis leo et, vestibulum risus. Duis tincidunt est ante, ac venenatis ex lacinia sit amet. Praesent eu sem a velit feugiat condimentum. Donec mollis blandit tellus. Aliquam dignissim nulla at lacus consequat, vitae fermentum nunc vehicula. Cras vulputate dui mauris, vitae ultricies eros consectetur ac. Mauris ac velit nec quam finibus mollis id sed purus. Pellentesque blandit vehicula augue, at lobortis est tincidunt ut. Proin nec consequat neque.\n" + + "\n" + + "Praesent luctus risus nisl. Phasellus justo nunc, cursus ac ligula nec, pellentesque viverra elit. Sed a sem libero. Sed eleifend posuere leo et tincidunt. In quis ultrices diam. Fusce vitae tempor arcu, at iaculis neque. Phasellus nec ex et dolor elementum condimentum. Sed pretium suscipit lacinia. Vivamus tristique tellus urna, at pharetra massa tincidunt tempus. Nulla aliquet erat ligula, eget commodo erat congue ut. Nulla facilisi. Praesent erat arcu, cursus eget felis at, egestas tincidunt ex. Nulla hendrerit maximus neque, commodo tincidunt lacus faucibus at. Praesent elementum ut erat ac consequat.\n" + + "\n" + + "Suspendisse ac porta mi, eu congue lectus. Nulla mollis efficitur sagittis. In et nunc in ante tincidunt porttitor. Aliquam accumsan nibh nunc, eu mollis nunc tempus laoreet. Etiam placerat maximus bibendum. Duis volutpat tortor at orci lacinia, vitae ullamcorper sem cursus. Proin et tellus ac nunc tristique condimentum. Sed non maximus turpis, malesuada sodales augue. In pellentesque, nulla eu blandit ultricies, nisl ipsum malesuada nulla, eget dignissim erat odio a lectus.\n" + + "\n" + + "Suspendisse tempor sapien vel massa viverra, at faucibus ipsum tincidunt. Vestibulum euismod, dolor eu finibus tincidunt, nulla ante lobortis magna, vel fringilla felis velit et nisi. Suspendisse aliquet lacus dolor, id auctor felis dapibus eget. Donec interdum lectus vitae dolor pharetra placerat. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras sagittis elit ac dapibus volutpat. Integer in nunc in eros semper pharetra. Morbi rhoncus, turpis ac congue bibendum, massa massa efficitur quam, quis accumsan justo dolor congue diam. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum ipsum elit, dictum in laoreet eu, bibendum a sem. Curabitur efficitur mollis imperdiet. Suspendisse maximus, diam id tincidunt porta, risus mauris volutpat urna, ac aliquet ante eros non odio. Phasellus at hendrerit nibh, ut imperdiet neque.\n" + + "\n" + + "Curabitur diam turpis, pretium sed velit a, placerat laoreet tellus. Fusce vehicula, diam iaculis cursus blandit, justo justo bibendum magna, nec rutrum magna urna quis mauris. Nullam sed justo eros. Nullam ac volutpat turpis. Vivamus rutrum maximus maximus. Sed vehicula sem suscipit nibh posuere pharetra. Vestibulum non velit quis velit semper imperdiet eget ut lorem. Mauris pulvinar ex lectus, sed pharetra nulla placerat sed. Suspendisse in felis eleifend, euismod sem nec, rutrum lectus. Phasellus non lectus cursus, mattis purus in, aliquam lorem. Curabitur sagittis facilisis finibus. Duis dolor neque, tristique id feugiat eget, eleifend sit amet magna. Sed pharetra fermentum diam quis dignissim. Etiam sagittis vel diam at placerat. Aenean tempor nisl eget nunc tempus malesuada. Aliquam rhoncus, arcu nec convallis luctus, nibh sem suscipit ante, sit amet imperdiet nisi neque non velit." + ) + noteDetail.value = Result.success(mockNote) + } + + override suspend fun getNoteDetail(id: Int) { + // Mock implementation - data is already set in init + } + override suspend fun updateNote(note: NoteModel): Result = Result.success(true) override suspend fun deleteNoteList(vararg notes: NoteModel): Result = Result.success(true) } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0dc9284..cfd55e5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,6 +53,7 @@ Selected Note Detail + Editing Note Add New Note Share Preview Backup Notes From 0f3899873868e14d50dd86ddb11ba42c5b8668cf Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 10 Aug 2025 15:36:46 +0700 Subject: [PATCH 14/28] Feat feature/ui-revamp : Extract label to string.xml & adjust font size --- .../feature/note_detail/NoteDetailPage.kt | 40 ++++++++++--------- .../components/navbar/EnhancedBottomAppBar.kt | 10 +++-- .../components/section/NoteSection.kt | 13 +++--- .../components/section/TitleSection.kt | 13 +++--- .../feature/notes/components/item/NoteItem.kt | 18 ++++++--- app/src/main/res/values/strings.xml | 3 ++ 6 files changed, 59 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt index e40f0c6..ba39fe0 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt @@ -38,6 +38,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.digiventure.ventnote.R +import com.digiventure.ventnote.commons.Constants.EMPTY_STRING import com.digiventure.ventnote.commons.TestTags import com.digiventure.ventnote.components.LockScreenOrientation import com.digiventure.ventnote.components.dialog.LoadingDialog @@ -53,8 +54,6 @@ import com.digiventure.ventnote.navigation.PageNavigation import com.google.gson.Gson import kotlinx.coroutines.launch -const val TAG : String = "NoteDetailPage" - @OptIn(ExperimentalMaterial3Api::class) @Composable fun NoteDetailPage( @@ -69,19 +68,19 @@ fun NoteDetailPage( } // String resources - memoized for better performance - val titleTextFieldTag = stringResource(R.string.title_textField) - val bodyTextFieldTag = stringResource(R.string.body_textField) - val titleInputTag = stringResource(R.string.title_textField_input) - val bodyInputTag = stringResource(R.string.body_textField_input) - val successFullyUpdatedTag = stringResource(R.string.successfully_updated) + val titleTextFieldContentDescription = stringResource(R.string.title_textField) + val bodyTextFieldContentDescription = stringResource(R.string.body_textField) + val titleInputPlaceholder = stringResource(R.string.title_textField_input) + val bodyInputPlaceholder = stringResource(R.string.body_textField_input) + val successFullyUpdatedLabel = stringResource(R.string.successfully_updated) val strings = remember { mapOf( - "titleTextField" to "${titleTextFieldTag}-$TAG", - "bodyTextField" to "${bodyTextFieldTag}-$TAG", - "titleInput" to titleInputTag, - "bodyInput" to bodyInputTag, - "successFullyUpdatedText" to successFullyUpdatedTag + "titleTextField" to titleTextFieldContentDescription, + "bodyTextField" to bodyTextFieldContentDescription, + "titleInput" to titleInputPlaceholder, + "bodyInput" to bodyInputPlaceholder, + "successFullyUpdatedText" to successFullyUpdatedLabel ) } @@ -234,8 +233,8 @@ fun NoteDetailPage( TitleSection( viewModel = viewModel, isEditingState = isEditingState, - titleTextField = strings["titleTextField"] ?: "", - titleInput = strings["titleInput"] ?: "" + titleTextField = strings["titleTextField"] ?: EMPTY_STRING, + titleInput = strings["titleInput"] ?: EMPTY_STRING ) } @@ -243,8 +242,8 @@ fun NoteDetailPage( NoteSection( viewModel = viewModel, isEditingState = isEditingState, - bodyTextField = strings["bodyTextField"] ?: "", - bodyInput = strings["bodyInput"] ?: "" + bodyTextField = strings["bodyTextField"] ?: EMPTY_STRING, + bodyInput = strings["bodyInput"] ?: EMPTY_STRING ) } } @@ -268,14 +267,17 @@ fun NoteDetailPage( ) // Dialogs + val titlePlaceholderText = stringResource(R.string.title_textField_input) + val notePlaceholderText = stringResource(R.string.body_textField_input) + val missingFieldName = remember( viewModel.titleText.value, viewModel.descriptionText.value ) { when { - viewModel.titleText.value.isEmpty() -> "Title" - viewModel.descriptionText.value.isEmpty() -> "Notes" - else -> "" + viewModel.titleText.value.isEmpty() -> titlePlaceholderText + viewModel.descriptionText.value.isEmpty() -> notePlaceholderText + else -> EMPTY_STRING } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt index 0dc98b3..8a6d6c0 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt @@ -33,8 +33,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.digiventure.ventnote.R @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -59,7 +61,7 @@ fun EnhancedBottomAppBar( // Cancel button in editing mode EnhancedBottomBarButton( icon = Icons.Rounded.Close, - label = "Cancel", + label = stringResource(R.string.cancel), onClick = onCancelClick, containerColor = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer @@ -68,7 +70,7 @@ fun EnhancedBottomAppBar( // Save button in editing mode EnhancedBottomBarButton( icon = Icons.Rounded.Check, - label = "Save", + label = stringResource(R.string.save), onClick = onSaveClick, containerColor = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer, @@ -78,7 +80,7 @@ fun EnhancedBottomAppBar( // Edit button in view mode EnhancedBottomBarButton( icon = Icons.Rounded.Edit, - label = "Edit", + label = stringResource(R.string.edit), onClick = onEditClick, containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer @@ -87,7 +89,7 @@ fun EnhancedBottomAppBar( // Delete button in view mode EnhancedBottomBarButton( icon = Icons.Rounded.DeleteOutline, - label = "Delete", + label = stringResource(R.string.delete), onClick = onDeleteClick, containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt index d0fc52f..5d733ae 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt @@ -28,12 +28,14 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.digiventure.ventnote.R import com.digiventure.ventnote.feature.note_detail.viewmodel.NoteDetailPageBaseVM @Composable @@ -56,7 +58,7 @@ fun NoteSection( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Notes", + text = stringResource(R.string.notes), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface @@ -79,10 +81,11 @@ fun ImprovedDescriptionTextField( bodyTextField: String, bodyInput: String ) { + val label = "border_color" val borderColor by animateColorAsState( targetValue = if (isEditingState) MaterialTheme.colorScheme.primary else Color.Transparent, animationSpec = tween(300), - label = "border_color" + label = label ) Card( @@ -110,10 +113,10 @@ fun ImprovedDescriptionTextField( value = viewModel.descriptionText.value, onValueChange = { viewModel.descriptionText.value = it }, textStyle = TextStyle( - fontSize = 16.sp, + fontSize = 14.sp, fontWeight = FontWeight.Normal, color = MaterialTheme.colorScheme.onSurface, - lineHeight = 24.sp + lineHeight = 20.sp ), singleLine = false, readOnly = !isEditingState, @@ -134,7 +137,7 @@ fun ImprovedDescriptionTextField( if (isEditingState) { Text( text = bodyInput, - fontSize = 16.sp, + fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) ) } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt index f7cc7d8..dacb439 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt @@ -30,12 +30,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.digiventure.ventnote.R import com.digiventure.ventnote.feature.note_detail.viewmodel.NoteDetailPageBaseVM import kotlinx.coroutines.delay @@ -59,7 +61,7 @@ fun TitleSection( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Title", + text = stringResource(R.string.sort_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface @@ -82,11 +84,12 @@ fun ImprovedTitleTextField( titleTextField: String, titleInput: String ) { + val label = "border_color" val focusRequester = remember { FocusRequester() } val borderColor by animateColorAsState( targetValue = if (isEditingState) MaterialTheme.colorScheme.primary else Color.Transparent, animationSpec = tween(300), - label = "border_color" + label = label ) LaunchedEffect(isEditingState) { @@ -120,10 +123,10 @@ fun ImprovedTitleTextField( value = viewModel.titleText.value, onValueChange = { viewModel.titleText.value = it }, textStyle = TextStyle( - fontSize = 18.sp, + fontSize = 16.sp, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface, - lineHeight = 28.sp + lineHeight = 24.sp ), singleLine = false, maxLines = 3, @@ -145,7 +148,7 @@ fun ImprovedTitleTextField( if (isEditingState) { Text( text = titleInput, - fontSize = 18.sp, + fontSize = 16.sp, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) ) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt index b9648bb..d7364e0 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -77,9 +78,12 @@ fun NotesItem( text = data.title, maxLines = 1, overflow = TextOverflow.Ellipsis, - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 24.sp + ), modifier = Modifier.padding(horizontal = 12.dp) ) } @@ -98,8 +102,12 @@ fun NotesItem( text = data.note, maxLines = 4, overflow = TextOverflow.Ellipsis, - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurface, + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface, + lineHeight = 20.sp + ), modifier = Modifier.padding(bottom = 4.dp) ) Text( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cfd55e5..53dce60 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,6 +40,7 @@ Save Edit Add + Delete Share Note Share Note as Text Sign In With Google @@ -65,6 +66,8 @@ Order By Ascending Descending + Notes + Cancel about us From ebdab965f78d40ec49c6a8edd289242121450d97 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 10 Aug 2025 17:19:13 +0700 Subject: [PATCH 15/28] Feat feature/ui-revamp : Enhance share page ui/ux, adjust route navigation to prevent clearing stack when its not notes page --- .../feature/note_detail/NoteDetailPage.kt | 4 +- .../feature/share_preview/SharePreviewPage.kt | 278 ++++++++++++------ .../components/dialog/HelpDialog.kt | 51 ++++ .../components/{ => navbar}/AppBar.kt | 18 +- .../components/navbar/EnhancedBottomAppBar.kt | 112 +++++++ .../components/sheets/ShareSheet.kt | 115 ++++++++ .../ventnote/navigation/PageNavigation.kt | 33 +-- app/src/main/res/values/strings.xml | 5 + 8 files changed, 476 insertions(+), 140 deletions(-) create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/dialog/HelpDialog.kt rename app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/{ => navbar}/AppBar.kt (81%) create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/EnhancedBottomAppBar.kt create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/sheets/ShareSheet.kt diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt index ba39fe0..0d8bea3 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt @@ -115,7 +115,7 @@ fun NoteDetailPage( viewModel.deleteNoteList(noteData) .onSuccess { deleteDialogState.value = false - navHostController.popBackStack() + navHostController.navigateUp() } .onFailure { error -> deleteDialogState.value = false @@ -324,7 +324,7 @@ fun NoteDetailPage( if (viewModel.isEditing.value) { cancelDialogState.value = true } else { - navHostController.popBackStack() + navHostController.navigateUp() } } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt index 65ff89a..4a8015e 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt @@ -3,21 +3,29 @@ package com.digiventure.ventnote.feature.share_preview import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo -import androidx.compose.foundation.clickable +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.filled.Schedule +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text @@ -27,13 +35,16 @@ import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -42,12 +53,13 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.digiventure.ventnote.R import com.digiventure.ventnote.commons.DateUtil -import com.digiventure.ventnote.commons.TestTags import com.digiventure.ventnote.components.LockScreenOrientation -import com.digiventure.ventnote.components.bottomSheet.RegularBottomSheet -import com.digiventure.ventnote.components.dialog.TextDialog import com.digiventure.ventnote.data.persistence.NoteModel -import com.digiventure.ventnote.feature.share_preview.components.SharePreviewAppBar +import com.digiventure.ventnote.feature.share_preview.components.dialog.HelpDialog +import com.digiventure.ventnote.feature.share_preview.components.navbar.EnhancedBottomAppBar +import com.digiventure.ventnote.feature.share_preview.components.navbar.SharePreviewAppBar +import com.digiventure.ventnote.feature.share_preview.components.sheets.ShareSheet +import kotlinx.coroutines.launch import java.util.Date @OptIn(ExperimentalMaterial3Api::class) @@ -63,29 +75,67 @@ fun SharePreviewPage( val rememberedScrollBehavior = remember { scrollBehavior } val shareNoteDialogState = remember { mutableStateOf(false) } - val snackBarHostState = remember { SnackbarHostState() } - val date = DateUtil.convertDateString("EEE, MMM dd HH:mm yyyy", - note?.createdAt?.toString() ?: Date().toString() - ) - val title = note?.title ?: "title" - val text = note?.note ?: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras eget egestas nisi, sit amet tincidunt lorem. Aliquam pharetra, tortor nec bibendum rhoncus, lorem est placerat quam, vitae posuere dui massa eu tortor. Nullam nibh turpis, egestas id sollicitudin nec, placerat vitae libero. Nulla commodo ex orci, et commodo leo tempor vitae. Sed posuere dolor urna, vitae tempus magna lobortis ac. Fusce dignissim eros sit amet velit commodo, ac viverra augue iaculis. Aenean facilisis, est ut gravida feugiat, est sem varius neque, at ultricies lectus nisi sed urna. Ut sagittis orci ac ante convallis eleifend. Nulla nec congue purus, at sagittis felis. Sed mattis quam orci, molestie auctor orci venenatis et. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed vestibulum placerat enim eu tempor. Aenean sed augue ut eros hendrerit porttitor.\n" - val joinedText = date + "\n\n" + title + "\n\n" + text + val date = remember(note?.createdAt) { + try { + DateUtil.convertDateString( + "EEE, MMM dd HH:mm yyyy", + note?.createdAt?.toString() ?: Date().toString() + ) + } catch (e: Exception) { + DateUtil.convertDateString("EEE, MMM dd HH:mm yyyy", Date().toString()) + } + } + + val title = note?.title?.takeIf { it.isNotBlank() } ?: stringResource(R.string.empty_note_title_placeholder) + val text = note?.note?.takeIf { it.isNotBlank() } + ?: stringResource(R.string.empty_note_placeholder) + + val joinedText = remember(date, title, text) { + buildString { + append(date) + append("\n\n") + append(title) + append("\n\n") + append(text) + } + } val openBottomSheet = rememberSaveable { mutableStateOf(false) } - val skipPartiallyExpanded = remember { mutableStateOf(false) } val bottomSheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = skipPartiallyExpanded.value + skipPartiallyExpanded = false ) val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val haptic = LocalHapticFeedback.current + + val handleShare = remember { + { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + + coroutineScope.launch { + try { + shareText(joinedText, context) + } catch (e: Exception) { + snackBarHostState.showSnackbar( + message = context.getString(R.string.failed_to_share_note), + duration = SnackbarDuration.Short + ) + } finally { + openBottomSheet.value = false + } + } + } + } Scaffold( topBar = { SharePreviewAppBar( onBackPressed = { - navHostController.popBackStack() + navHostController.navigateUp() }, onHelpPressed = { shareNoteDialogState.value = true @@ -94,98 +144,130 @@ fun SharePreviewPage( ) }, snackbarHost = { SnackbarHost(snackBarHostState) }, - floatingActionButton = { - ExtendedFloatingActionButton( - onClick = { openBottomSheet.value = true }, - modifier = Modifier - .semantics { - testTag = TestTags.SHARE_NOTE_FAB - }, - text = { - Text(stringResource(R.string.share_note), fontSize = 16.sp, fontWeight = FontWeight.Medium) - }, - icon = { - Icon( - imageVector = Icons.Filled.Share, - contentDescription = stringResource(R.string.fab) - ) - }, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) - }, modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection), - containerColor = MaterialTheme.colorScheme.surface - ) { - Box(modifier = Modifier.padding(it)) { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(16.dp), - ) { - Text( - date, - fontWeight = FontWeight.Light, - fontSize = 16.sp, - modifier = Modifier.padding(bottom = 8.dp), - color = MaterialTheme.colorScheme.onSurface - ) - Text( - title, - fontWeight = FontWeight.Bold, - fontSize = 18.sp, - modifier = Modifier.padding(bottom = 16.dp), - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurface - ) + containerColor = MaterialTheme.colorScheme.surface, + content = { + Box(modifier = Modifier.padding(it)) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(vertical = 20.dp) + ) { + item { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f) + ), + shape = RoundedCornerShape(12.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Schedule, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.7f) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = date, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f), + fontWeight = FontWeight.Medium + ) + } + } + } + + item { + SelectionContainer { + Text( + text = title, + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + lineHeight = 24.sp + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxWidth() + ) + } + } + + item { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + SelectionContainer { + Text( + text = text, + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface, + lineHeight = 20.sp + ), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.9f), + modifier = Modifier.padding(20.dp) + ) + } + } + } + } } + }, + bottomBar = { + EnhancedBottomAppBar( + onCancelClick = { openBottomSheet.value = true }, + ) } - } - - TextDialog( - title = stringResource(R.string.information), - description = stringResource(R.string.share_note_information), - isOpened = shareNoteDialogState.value, - onDismissCallback = { shareNoteDialogState.value = false } ) - RegularBottomSheet( - isOpened = openBottomSheet.value, - bottomSheetState = bottomSheetState, - modifier = null, - onDismissRequest = { openBottomSheet.value = false } - ) { - Column(modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text(stringResource(R.string.share_note_as_text), - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.clickable { - openBottomSheet.value = false - shareText(joinedText, context) - }) + if (shareNoteDialogState.value) { + HelpDialog { + shareNoteDialogState.value = false } } + + if (openBottomSheet.value) { + ShareSheet( + isOpened = openBottomSheet.value, + bottomSheetState = bottomSheetState, + onDismissRequest = { + openBottomSheet.value = false + }, + onShareRequest = { + handleShare() + } + ) + } + + BackHandler { + navHostController.navigateUp() + } } fun shareText(text: String, context: Context) { - val sendIntent: Intent = Intent().apply { + val sendIntent = Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, text) type = "text/plain" } - val shareIntent = Intent.createChooser(sendIntent, null) + val shareIntent = Intent.createChooser(sendIntent, context.getString(R.string.share_note)) context.startActivity(shareIntent) } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/dialog/HelpDialog.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/dialog/HelpDialog.kt new file mode 100644 index 0000000..c198eb2 --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/dialog/HelpDialog.kt @@ -0,0 +1,51 @@ +package com.digiventure.ventnote.feature.share_preview.components.dialog + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.digiventure.ventnote.R + +@Composable +fun HelpDialog( + onDismissRequest: () -> Unit, +) { + AlertDialog( + onDismissRequest = { onDismissRequest() }, + icon = { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + title = { + Text( + text = stringResource(R.string.information), + style = MaterialTheme.typography.headlineSmall + ) + }, + text = { + Text( + text = stringResource(R.string.share_note_information), + style = MaterialTheme.typography.bodyMedium + ) + }, + confirmButton = { + TextButton( + onClick = { onDismissRequest() } + ) { + Text(stringResource(R.string.close)) + } + }, + containerColor = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(16.dp) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/AppBar.kt similarity index 81% rename from app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/AppBar.kt rename to app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/AppBar.kt index 37c937e..1722122 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/AppBar.kt @@ -1,23 +1,19 @@ -package com.digiventure.ventnote.feature.share_preview.components +package com.digiventure.ventnote.feature.share_preview.components.navbar -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.digiventure.ventnote.R import com.digiventure.ventnote.components.navbar.TopNavBarIcon @@ -28,16 +24,14 @@ fun SharePreviewAppBar( onHelpPressed: () -> Unit, scrollBehavior: TopAppBarScrollBehavior) { - TopAppBar( + CenterAlignedTopAppBar( title = { Text( text = stringResource(id = R.string.share_preview), color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(start = 4.dp), - style = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 20.sp - ) + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold + ), ) }, colors = TopAppBarDefaults.topAppBarColors( diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/EnhancedBottomAppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/EnhancedBottomAppBar.kt new file mode 100644 index 0000000..cf3dc8d --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/EnhancedBottomAppBar.kt @@ -0,0 +1,112 @@ +package com.digiventure.ventnote.feature.share_preview.components.navbar + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Share +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.digiventure.ventnote.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EnhancedBottomAppBar( + onCancelClick: () -> Unit +) { + BottomAppBar( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + tonalElevation = 12.dp, + actions = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + EnhancedBottomBarButton( + icon = Icons.Rounded.Share, + label = stringResource(R.string.share_note), + onClick = onCancelClick, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + ) +} + +@Composable +private fun EnhancedBottomBarButton( + icon: ImageVector, + label: String, + onClick: () -> Unit, + containerColor: Color, + contentColor: Color, + isProminent: Boolean = false +) { + val haptics = LocalHapticFeedback.current + val scale by animateFloatAsState( + targetValue = if (isProminent) 1.1f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "button_scale" + ) + + Row ( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .scale(scale) + .clip(RoundedCornerShape(16.dp)) + .background( + containerColor.copy(alpha = 0.8f), + RoundedCornerShape(16.dp) + ) + .clickable { + haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove) + onClick() + } + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Icon( + imageVector = icon, + contentDescription = label, + tint = contentColor, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + color = contentColor, + fontWeight = if (isProminent) FontWeight.SemiBold else FontWeight.Medium, + maxLines = 1 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/sheets/ShareSheet.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/sheets/ShareSheet.kt new file mode 100644 index 0000000..3f708d9 --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/sheets/ShareSheet.kt @@ -0,0 +1,115 @@ +package com.digiventure.ventnote.feature.share_preview.components.sheets + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SheetState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.digiventure.ventnote.R +import com.digiventure.ventnote.components.bottomSheet.RegularBottomSheet + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ShareSheet( + isOpened: Boolean, + bottomSheetState: SheetState, + onDismissRequest: () -> Unit, + onShareRequest: () -> Unit +) { + RegularBottomSheet( + isOpened = isOpened, + bottomSheetState = bottomSheetState, + modifier = null, + onDismissRequest = { onDismissRequest() } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + ShareOptionItem( + icon = Icons.Default.Share, + title = stringResource(R.string.share_note_as_text), + subtitle = stringResource(R.string.share_note_as_text_subtitle), + onClick = { onShareRequest() } + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + } +} + +@Composable +private fun ShareOptionItem( + icon: ImageVector, + title: String, + subtitle: String, + onClick: () -> Unit +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickable { onClick() }, + color = Color.Transparent + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Surface( + modifier = Modifier.size(40.dp), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f), + shape = RoundedCornerShape(10.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + } + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/navigation/PageNavigation.kt b/app/src/main/java/com/digiventure/ventnote/navigation/PageNavigation.kt index ccc2587..2ff7464 100644 --- a/app/src/main/java/com/digiventure/ventnote/navigation/PageNavigation.kt +++ b/app/src/main/java/com/digiventure/ventnote/navigation/PageNavigation.kt @@ -5,48 +5,25 @@ import androidx.navigation.NavHostController class PageNavigation(navController: NavHostController) { val navigateToBackupPage: () -> Unit = { - navController.navigate(Route.BackupPage.routeName) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } + navController.navigate(Route.BackupPage.routeName) } val navigateToDetailPage: (noteId: Int) -> Unit = { noteId -> val routeName = "${Route.NoteDetailPage.routeName}/${noteId}" - navController.navigate(routeName) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } + navController.navigate(routeName) } val navigateToCreatePage: () -> Unit = { - navController.navigate(Route.NoteCreationPage.routeName) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } + navController.navigate(Route.NoteCreationPage.routeName) } val navigateToNotesPage: () -> Unit = { navController.navigate(Route.NotesPage.routeName) { popUpTo(navController.graph.findStartDestination().id) { inclusive = true + saveState = true } } } val navigateToSharePage: (noteJson: String) -> Unit = { noteJson -> val routeName = "${Route.SharePreviewPage.routeName}/${noteJson}" - navController.navigate(routeName) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } + navController.navigate(routeName) } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 53dce60..20dc948 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,8 +41,10 @@ Edit Add Delete + Close Share Note Share Note as Text + Shares the note\'s content as a simple text message Sign In With Google Refresh @@ -50,6 +52,8 @@ Input title here Insert title Insert note + Untitled Note + Note Selected @@ -90,6 +94,7 @@ Note is successfully backed up Note is successfully restored Note is successfully updated + Failed to share note title textField From 0b8a7d17ebbef07e97b30ca90adf9cd991bea787 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 10 Aug 2025 17:29:29 +0700 Subject: [PATCH 16/28] Feat feature/ui-revamp : Remove unnecessary redeclaration of alert dialog --- .../ventnote/components/dialog/TextDialog.kt | 20 +++++--- .../note_detail/components/navbar/AppBar.kt | 6 ++- .../feature/share_preview/SharePreviewPage.kt | 13 +++-- .../components/dialog/HelpDialog.kt | 51 ------------------- 4 files changed, 27 insertions(+), 63 deletions(-) delete mode 100644 app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/dialog/HelpDialog.kt diff --git a/app/src/main/java/com/digiventure/ventnote/components/dialog/TextDialog.kt b/app/src/main/java/com/digiventure/ventnote/components/dialog/TextDialog.kt index 4363fc8..24d29d4 100644 --- a/app/src/main/java/com/digiventure/ventnote/components/dialog/TextDialog.kt +++ b/app/src/main/java/com/digiventure/ventnote/components/dialog/TextDialog.kt @@ -1,7 +1,10 @@ package com.digiventure.ventnote.components.dialog import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -28,19 +31,23 @@ fun TextDialog( if (isOpened) { AlertDialog( onDismissRequest = { onDismissCallback() }, + icon = { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, title = { Text( text = title, - fontSize = 18.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface + style = MaterialTheme.typography.headlineSmall ) }, text = { Text( text = description, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurface + style = MaterialTheme.typography.bodyMedium ) }, confirmButton = { @@ -71,7 +78,8 @@ fun TextDialog( ) } }, - shape = RoundedCornerShape(8.dp), + containerColor = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(16.dp), modifier = modifier ) } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt index a83a867..bbf0d2f 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt @@ -46,8 +46,10 @@ fun NoteDetailAppBar( } }, actions = { - TopNavBarIcon(Icons.Filled.Share, stringResource(R.string.share_nav_icon), Modifier.semantics { }) { - onSharePressed() + if (!isEditing) { + TopNavBarIcon(Icons.Filled.Share, stringResource(R.string.share_nav_icon), Modifier.semantics { }) { + onSharePressed() + } } }, scrollBehavior = scrollBehavior, diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt index 4a8015e..1669485 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -54,8 +55,8 @@ import androidx.navigation.compose.rememberNavController import com.digiventure.ventnote.R import com.digiventure.ventnote.commons.DateUtil import com.digiventure.ventnote.components.LockScreenOrientation +import com.digiventure.ventnote.components.dialog.TextDialog import com.digiventure.ventnote.data.persistence.NoteModel -import com.digiventure.ventnote.feature.share_preview.components.dialog.HelpDialog import com.digiventure.ventnote.feature.share_preview.components.navbar.EnhancedBottomAppBar import com.digiventure.ventnote.feature.share_preview.components.navbar.SharePreviewAppBar import com.digiventure.ventnote.feature.share_preview.components.sheets.ShareSheet @@ -237,9 +238,13 @@ fun SharePreviewPage( ) if (shareNoteDialogState.value) { - HelpDialog { - shareNoteDialogState.value = false - } + TextDialog( + title = stringResource(R.string.information), + description = stringResource(R.string.share_note_information), + isOpened = true, + onDismissCallback = { shareNoteDialogState.value = false }, + modifier = Modifier.semantics { } + ) } if (openBottomSheet.value) { diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/dialog/HelpDialog.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/dialog/HelpDialog.kt deleted file mode 100644 index c198eb2..0000000 --- a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/dialog/HelpDialog.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.digiventure.ventnote.feature.share_preview.components.dialog - -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Info -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.digiventure.ventnote.R - -@Composable -fun HelpDialog( - onDismissRequest: () -> Unit, -) { - AlertDialog( - onDismissRequest = { onDismissRequest() }, - icon = { - Icon( - imageVector = Icons.Default.Info, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - }, - title = { - Text( - text = stringResource(R.string.information), - style = MaterialTheme.typography.headlineSmall - ) - }, - text = { - Text( - text = stringResource(R.string.share_note_information), - style = MaterialTheme.typography.bodyMedium - ) - }, - confirmButton = { - TextButton( - onClick = { onDismissRequest() } - ) { - Text(stringResource(R.string.close)) - } - }, - containerColor = MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(16.dp) - ) -} \ No newline at end of file From 6c8fa762ece6ddda596f0be7950ff3a4a5d5ed57 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Mon, 11 Aug 2025 08:36:54 +0700 Subject: [PATCH 17/28] Feat feature/ui-revamp : Improve backup page ui/ux, still wip ensuring all page should affected --- .../ventnote/feature/backup/BackupPage.kt | 185 +++++++-- .../backup/components/ListOfBackupFile.kt | 230 ----------- .../feature/backup/components/SignInButton.kt | 59 --- .../backup/components/button/SignInButton.kt | 127 ++++++ .../backup/components/list/BackupFileList.kt | 383 ++++++++++++++++++ .../backup/components/{ => navbar}/AppBar.kt | 17 +- .../feature/backup/viewmodel/AuthMockVM.kt | 13 +- .../backup/viewmodel/BackupPageMockVM.kt | 6 +- 8 files changed, 693 insertions(+), 327 deletions(-) delete mode 100644 app/src/main/java/com/digiventure/ventnote/feature/backup/components/ListOfBackupFile.kt delete mode 100644 app/src/main/java/com/digiventure/ventnote/feature/backup/components/SignInButton.kt create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt rename app/src/main/java/com/digiventure/ventnote/feature/backup/components/{ => navbar}/AppBar.kt (89%) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt index 87e7feb..23a085a 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt @@ -1,16 +1,27 @@ package com.digiventure.ventnote.feature.backup import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CloudOff +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -24,6 +35,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -33,9 +46,9 @@ import com.digiventure.ventnote.R import com.digiventure.ventnote.commons.TestTags import com.digiventure.ventnote.components.dialog.LoadingDialog import com.digiventure.ventnote.components.dialog.TextDialog -import com.digiventure.ventnote.feature.backup.components.BackupPageAppBar -import com.digiventure.ventnote.feature.backup.components.ListOfBackupFile -import com.digiventure.ventnote.feature.backup.components.SignInButton +import com.digiventure.ventnote.feature.backup.components.button.SignInButton +import com.digiventure.ventnote.feature.backup.components.list.BackupFileList +import com.digiventure.ventnote.feature.backup.components.navbar.BackupPageAppBar import com.digiventure.ventnote.feature.backup.viewmodel.AuthBaseVM import com.digiventure.ventnote.feature.backup.viewmodel.AuthMockVM import com.digiventure.ventnote.feature.backup.viewmodel.AuthVM @@ -62,13 +75,16 @@ fun BackupPage( val loadingDialogState = remember { mutableStateOf(false) } val restoreConfirmationDialogState = remember { mutableStateOf(false) } + val deleteConfirmationDialogState = remember { mutableStateOf(false) } val stringZero = "0" val restoreDataIdState = remember { mutableStateOf(stringZero) } + val deleteDataIdState = remember { mutableStateOf(stringZero) } val context = LocalContext.current val backedUpMessage = stringResource(id = R.string.successfully_backed_up) val restoredMessage = stringResource(id = R.string.successfully_restored) + val deletedMessage = stringResource(id = R.string.note_is_successfully_deleted) fun backupDatabase() { scope.launch { @@ -119,7 +135,8 @@ fun BackupPage( snackbarHost = { SnackbarHost(snackBarHostState) }, content = { contentPadding -> Box( - modifier = Modifier.padding(contentPadding).padding(start = 16.dp, end = 16.dp), + modifier = Modifier.padding(contentPadding) + .padding(start = 16.dp, end = 16.dp), contentAlignment = Alignment.Center ) { when (authUiState.authState) { @@ -127,23 +144,30 @@ fun BackupPage( modifier = Modifier.padding(top = 16.dp).fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator(modifier = Modifier.size(32.dp)) + LoadingStateContent() } AuthVM.AuthState.SignedOut -> Box( modifier = Modifier.padding(top = 16.dp).fillMaxSize(), contentAlignment = Alignment.Center ) { - SignInButton(authViewModel, signInSuccessCallback = { - backupPageVM.getBackupFileList() - }) + SignedOutStateContent( + authViewModel = authViewModel, + onSignInSuccess = { + backupPageVM.getBackupFileList() + } + ) } AuthVM.AuthState.SignedIn -> { - ListOfBackupFile( + BackupFileList( backupPageVM = backupPageVM, onRestoreCallback = { restoreDataIdState.value = it.id restoreConfirmationDialogState.value = true }, + onDeleteCallback = { + deleteDataIdState.value = it.id + deleteConfirmationDialogState.value = true + }, successfullyRestoredCallback = { scope.launch { snackBarHostState.showSnackbar( @@ -151,6 +175,14 @@ fun BackupPage( withDismissAction = true ) } + }, + successfullyDeletedCallback = { + scope.launch { + snackBarHostState.showSnackbar( + message = deletedMessage, + withDismissAction = true + ) + } } ) } @@ -160,25 +192,128 @@ fun BackupPage( containerColor = MaterialTheme.colorScheme.background ) - LoadingDialog( - isOpened = loadingDialogState.value, - onDismissCallback = { loadingDialogState.value = false }, - modifier = Modifier.semantics { testTag = TestTags.LOADING_DIALOG } - ) + if (loadingDialogState.value) { + LoadingDialog( + isOpened = loadingDialogState.value, + onDismissCallback = { loadingDialogState.value = false }, + modifier = Modifier.semantics { testTag = TestTags.LOADING_DIALOG } + ) + } + + if (restoreConfirmationDialogState.value) { + TextDialog( + title = stringResource(R.string.restore_confirmation_title), + description = stringResource(R.string.restore_confirmation_description), + isOpened = restoreConfirmationDialogState.value, + onDismissCallback = { restoreConfirmationDialogState.value = false }, + onConfirmCallback = { + val selectedId = restoreDataIdState.value + if (selectedId != stringZero) { + backupPageVM.restoreDatabase(selectedId) + restoreConfirmationDialogState.value = false + } + } + ) + } - TextDialog( - title = stringResource(R.string.restore_confirmation_title), - description = stringResource(R.string.restore_confirmation_description), - isOpened = restoreConfirmationDialogState.value, - onDismissCallback = { restoreConfirmationDialogState.value = false }, - onConfirmCallback = { - val selectedId = restoreDataIdState.value - if (selectedId != stringZero) { - backupPageVM.restoreDatabase(selectedId) - restoreConfirmationDialogState.value = false + if (deleteConfirmationDialogState.value) { + TextDialog( + isOpened = deleteConfirmationDialogState.value, + onDismissCallback = { deleteConfirmationDialogState.value = false }, + onConfirmCallback = { + val selectedId = restoreDataIdState.value + if (selectedId != stringZero) { + backupPageVM.deleteDatabase(selectedId) + deleteConfirmationDialogState.value = false + } } + ) + } +} + +@Composable +private fun LoadingStateContent() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + CircularProgressIndicator( + modifier = Modifier.size(48.dp), + strokeWidth = 4.dp, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = "Loading...", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) } - ) + } +} + +@Composable +private fun SignedOutStateContent( + authViewModel: AuthBaseVM, + onSignInSuccess: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Card( + modifier = Modifier + .size(100.dp) + .padding(bottom = 24.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + shape = CircleShape + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.CloudOff, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + Text( + text = "Sign in to access backups", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Connect with Google to backup and restore your data securely", + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(32.dp)) + + SignInButton( + authViewModel = authViewModel, + signInSuccessCallback = onSignInSuccess + ) + } } @Preview(device = "id:pixel_xl") diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/ListOfBackupFile.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/ListOfBackupFile.kt deleted file mode 100644 index bfd0e19..0000000 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/ListOfBackupFile.kt +++ /dev/null @@ -1,230 +0,0 @@ -package com.digiventure.ventnote.feature.backup.components - -import android.widget.Toast -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CloudDownload -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTag -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.digiventure.ventnote.R -import com.digiventure.ventnote.commons.TestTags -import com.digiventure.ventnote.components.dialog.LoadingDialog -import com.digiventure.ventnote.feature.backup.viewmodel.BackupPageBaseVM -import com.digiventure.ventnote.feature.backup.viewmodel.BackupPageVM -import com.google.api.services.drive.model.File -import kotlinx.coroutines.launch - -@Composable -fun ListOfBackupFile(backupPageVM: BackupPageBaseVM, - successfullyRestoredCallback: () -> Unit, - onRestoreCallback: (File) -> Unit) { - val backupPageUiState = backupPageVM.uiState.value - val driveBackupFileListState = backupPageVM.driveBackupFileList.observeAsState() - - val context = LocalContext.current - - val scope = rememberCoroutineScope() - - val restoreLoadingDialogState = remember { mutableStateOf(false) } - - val fileRestoreState = backupPageVM.uiState.value.fileRestoreState - val fileDeleteState = backupPageVM.uiState.value.fileDeleteState - - LaunchedEffect(key1 = true, key2 = backupPageVM.uiState.value.fileDeleteState) { - scope.launch { - backupPageVM.getBackupFileList() - } - } - - LaunchedEffect( - key1 = backupPageVM.uiState.value.fileRestoreState, - key2 = backupPageVM.uiState.value.fileDeleteState - ) { - when { - fileRestoreState is BackupPageVM.FileRestoreState.SyncFailed || - fileDeleteState is BackupPageVM.FileDeleteState.SyncFailed -> { - restoreLoadingDialogState.value = false - val errorMessage = when { - fileRestoreState is BackupPageVM.FileRestoreState.SyncFailed -> - "Restore notes process failed : ${fileRestoreState.errorMessage}" - fileDeleteState is BackupPageVM.FileDeleteState.SyncFailed -> - "Delete notes process failed : ${fileDeleteState.errorMessage}" - else -> "" - } - Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show() - } - - fileRestoreState is BackupPageVM.FileRestoreState.SyncFinished || - fileDeleteState is BackupPageVM.FileDeleteState.SyncFinished -> { - restoreLoadingDialogState.value = false - successfullyRestoredCallback() - } - - fileRestoreState is BackupPageVM.FileRestoreState.SyncStarted || - fileDeleteState is BackupPageVM.FileDeleteState.SyncStarted -> { - restoreLoadingDialogState.value = true - } - - else -> {} - } - } - - when (val state = backupPageUiState.listOfBackupFileState) { - BackupPageVM.FileBackupListState.FileBackupListFinished -> { - driveBackupFileListState.value.let { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(top = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - val shape = RoundedCornerShape(12.dp) - - items(items = it ?: emptyList()) { - Box( - modifier = Modifier - .semantics { contentDescription = "" } - .clip(shape) - .background(MaterialTheme.colorScheme.surface) - ) { - val dotDelimiter = "." - Row( - modifier = Modifier.fillMaxWidth().padding(10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = it.name.substringBefore(dotDelimiter), - maxLines = 2, - overflow = TextOverflow.Ellipsis, - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier - .weight(1f) - .padding(end = 8.dp), - ) - Spacer(modifier = Modifier.padding(horizontal = 8.dp)) - Row { - OutlinedButton ( - onClick = { onRestoreCallback(it) }, - shape = RoundedCornerShape(10.dp), - contentPadding = PaddingValues( - horizontal = 2.dp, - ) - ) { - Icon( - imageVector = Icons.Filled.CloudDownload, - contentDescription = "", - tint = MaterialTheme.colorScheme.primary, - ) - } - Spacer(modifier = Modifier.padding(horizontal = 4.dp)) - OutlinedButton( - onClick = { backupPageVM.deleteDatabase(it.id) }, - shape = RoundedCornerShape(8.dp), - contentPadding = PaddingValues( - horizontal = 2.dp, - ) - ) { - Icon( - imageVector = Icons.Filled.Delete, - contentDescription = "", - tint = MaterialTheme.colorScheme.primary, - ) - } - } - } - } - } - } - } - } - - is BackupPageVM.FileBackupListState.FileBackupListFailed -> { - val errorMessage = "Get notes process failed : ${state.errorMessage}" - Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show() - - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Button( - shape = RoundedCornerShape(10.dp), - onClick = { - scope.launch { - backupPageVM.getBackupFileList() - } - }, - ) { - Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = "", - tint = MaterialTheme.colorScheme.onPrimary - ) - Spacer(modifier = Modifier.width(10.dp)) - Text( - text = stringResource(id = R.string.refresh), - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - ) - } - } - } - - is BackupPageVM.FileBackupListState.FileBackupListStarted -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - modifier = Modifier - .size(24.dp) - .padding(top = 16.dp) - ) - } - } - } - - LoadingDialog( - isOpened = restoreLoadingDialogState.value, - onDismissCallback = { restoreLoadingDialogState.value = false }, - modifier = Modifier.semantics { testTag = TestTags.LOADING_DIALOG } - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/SignInButton.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/SignInButton.kt deleted file mode 100644 index 09fd6cf..0000000 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/SignInButton.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.digiventure.ventnote.feature.backup.components - -import android.app.Activity -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Login -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.digiventure.ventnote.R -import com.digiventure.ventnote.feature.backup.viewmodel.AuthBaseVM - -@Composable -fun SignInButton(authViewModel: AuthBaseVM, signInSuccessCallback: () -> Unit) { - val context = LocalContext.current - val launcher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == Activity.RESULT_OK) { - authViewModel.checkAuthState() - signInSuccessCallback() - } else { - val errorMessage = "Auth Failed" - Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show() - } - } - - Button( - onClick = { launcher.launch(authViewModel.getSignInIntent()) }, - shape = RoundedCornerShape(10.dp) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Login, - contentDescription = "", - tint = MaterialTheme.colorScheme.onPrimary, - ) - Text( - text = stringResource(id = R.string.sign_in_with_google), - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - modifier = Modifier.padding(start = 10.dp) - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt new file mode 100644 index 0000000..cdf6958 --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt @@ -0,0 +1,127 @@ +package com.digiventure.ventnote.feature.backup.components.button + +import android.app.Activity +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.with +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Login +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.digiventure.ventnote.R +import com.digiventure.ventnote.feature.backup.viewmodel.AuthBaseVM + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun SignInButton( + authViewModel: AuthBaseVM, + signInSuccessCallback: () -> Unit +) { + val context = LocalContext.current + var isLoading by remember { mutableStateOf(false) } + + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + isLoading = false + if (result.resultCode == Activity.RESULT_OK) { + authViewModel.checkAuthState() + signInSuccessCallback() + } else { + Toast.makeText( + context, + "Authentication failed. Please try again.", + Toast.LENGTH_SHORT + ).show() + } + } + + Button( + onClick = { + isLoading = true + launcher.launch(authViewModel.getSignInIntent()) + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + shape = RoundedCornerShape(16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 4.dp, + pressedElevation = 8.dp + ), + enabled = !isLoading + ) { + AnimatedContent( + targetState = isLoading, + transitionSpec = { + fadeIn(animationSpec = tween(200)) with + fadeOut(animationSpec = tween(200)) + } + ) { loading -> + if (loading) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary + ) + Text( + text = "Signing in...", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium + ) + } + } else { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Login, // Add Google icon + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(id = R.string.sign_in_with_google), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt new file mode 100644 index 0000000..8697f96 --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt @@ -0,0 +1,383 @@ +package com.digiventure.ventnote.feature.backup.components.list + +import android.widget.Toast +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CloudDownload +import androidx.compose.material.icons.filled.CloudOff +import androidx.compose.material.icons.filled.CloudQueue +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.ErrorOutline +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.digiventure.ventnote.R +import com.digiventure.ventnote.commons.TestTags +import com.digiventure.ventnote.components.dialog.LoadingDialog +import com.digiventure.ventnote.feature.backup.viewmodel.BackupPageBaseVM +import com.digiventure.ventnote.feature.backup.viewmodel.BackupPageVM +import com.google.api.services.drive.model.File +import kotlinx.coroutines.launch + +@Composable +fun BackupFileList(backupPageVM: BackupPageBaseVM, + successfullyRestoredCallback: () -> Unit, + successfullyDeletedCallback: () -> Unit, + onRestoreCallback: (File) -> Unit, + onDeleteCallback: (File) -> Unit) { + val backupPageUiState = backupPageVM.uiState.value + val driveBackupFileListState = backupPageVM.driveBackupFileList.observeAsState() + + val context = LocalContext.current + + val scope = rememberCoroutineScope() + + val restoreLoadingDialogState = remember { mutableStateOf(false) } + + val fileRestoreState = backupPageVM.uiState.value.fileRestoreState + val fileDeleteState = backupPageVM.uiState.value.fileDeleteState + + LaunchedEffect(key1 = true, key2 = backupPageVM.uiState.value.fileDeleteState) { + scope.launch { + backupPageVM.getBackupFileList() + } + } + + LaunchedEffect( + key1 = backupPageVM.uiState.value.fileRestoreState, + key2 = backupPageVM.uiState.value.fileDeleteState + ) { + when { + fileRestoreState is BackupPageVM.FileRestoreState.SyncFailed || + fileDeleteState is BackupPageVM.FileDeleteState.SyncFailed -> { + restoreLoadingDialogState.value = false + val errorMessage = when { + fileRestoreState is BackupPageVM.FileRestoreState.SyncFailed -> + "Restore notes process failed : ${fileRestoreState.errorMessage}" + fileDeleteState is BackupPageVM.FileDeleteState.SyncFailed -> + "Delete notes process failed : ${fileDeleteState.errorMessage}" + else -> "" + } + Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show() + } + + fileRestoreState is BackupPageVM.FileRestoreState.SyncFinished-> { + restoreLoadingDialogState.value = false + successfullyRestoredCallback() + } + + fileDeleteState is BackupPageVM.FileDeleteState.SyncFinished -> { + restoreLoadingDialogState.value = false + successfullyDeletedCallback() + } + + fileRestoreState is BackupPageVM.FileRestoreState.SyncStarted || + fileDeleteState is BackupPageVM.FileDeleteState.SyncStarted -> { + restoreLoadingDialogState.value = true + } + + else -> {} + } + } + + when (val state = backupPageUiState.listOfBackupFileState) { + BackupPageVM.FileBackupListState.FileBackupListFinished -> { + driveBackupFileListState.value.let { backupFiles -> + if (backupFiles.isNullOrEmpty()) { + EmptyBackupListContainer() + } + else { + BackupListContainer(backupFiles, onRestoreCallback, onDeleteCallback) + } + } + } + + is BackupPageVM.FileBackupListState.FileBackupListFailed -> { + BackupFailedContainer(onGetBackupList = { + scope.launch { + backupPageVM.getBackupFileList() + } + }) + } + + is BackupPageVM.FileBackupListState.FileBackupListStarted -> { + FileBackupListStartedContainer() + } + } + + if (restoreLoadingDialogState.value) { + LoadingDialog( + isOpened = restoreLoadingDialogState.value, + onDismissCallback = { restoreLoadingDialogState.value = false }, + modifier = Modifier.semantics { testTag = TestTags.LOADING_DIALOG } + ) + } +} + +@Composable +fun EmptyBackupListContainer() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Icon( + imageVector = Icons.Filled.CloudOff, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "No backups found", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + Text( + text = "Create your first backup using the backup button", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + } +} + +@Composable +fun BackupListContainer( + backupFiles: List, + onRestoreCallback: (File) -> Unit, + onDeleteCallback: (File) -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(vertical = 16.dp) + ) { + items(items = backupFiles) { file -> + Card( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "" }, + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 2.dp, + hoveredElevation = 4.dp + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // File icon + Surface( + modifier = Modifier.size(48.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer + ) { + Box( + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.CloudQueue, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(24.dp) + ) + } + } + + Spacer(modifier = Modifier.width(16.dp)) + + Text( + text = file.name.substringBefore("."), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .weight(1f) + .padding(end = 16.dp), + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + FilledTonalButton( + onClick = { onRestoreCallback(file) }, + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + ) { + Icon( + imageVector = Icons.Filled.CloudDownload, + contentDescription = "Restore", + modifier = Modifier.size(18.dp) + ) + } + + OutlinedButton( + onClick = { onDeleteCallback(file) }, + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error + ), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.5f)), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = "Delete", + modifier = Modifier.size(18.dp) + ) + } + } + } + } + } + } +} + +@Composable +fun BackupFailedContainer( + onGetBackupList: () -> Unit +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Card( + modifier = Modifier + .size(100.dp) + .padding(bottom = 24.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + shape = CircleShape + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.ErrorOutline, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + Text( + text = "Failed to load backups", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + + Button( + onClick = { + onGetBackupList() + }, + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(id = R.string.refresh), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, + ) + } + } + } +} + +@Composable +fun FileBackupListStartedContainer() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + CircularProgressIndicator( + modifier = Modifier.size(48.dp), + strokeWidth = 4.dp, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = "Loading backups...", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/navbar/AppBar.kt similarity index 89% rename from app/src/main/java/com/digiventure/ventnote/feature/backup/components/AppBar.kt rename to app/src/main/java/com/digiventure/ventnote/feature/backup/components/navbar/AppBar.kt index 722e45c..bdd1aa3 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/navbar/AppBar.kt @@ -1,24 +1,22 @@ -package com.digiventure.ventnote.feature.backup.components +package com.digiventure.ventnote.feature.backup.components.navbar import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Logout import androidx.compose.material.icons.filled.CloudUpload +import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.digiventure.ventnote.R import com.digiventure.ventnote.components.navbar.TopNavBarIcon import com.digiventure.ventnote.feature.backup.viewmodel.AuthBaseVM @@ -35,16 +33,15 @@ fun BackupPageAppBar( val authUiState = authVM.uiState.value - TopAppBar( + CenterAlignedTopAppBar( title = { Text( text = stringResource(id = R.string.backup_notes), - color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(start = 4.dp), - style = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 20.sp - ) + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold + ), ) }, colors = TopAppBarDefaults.topAppBarColors( diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/AuthMockVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/AuthMockVM.kt index 732cf36..f2a943a 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/AuthMockVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/AuthMockVM.kt @@ -8,11 +8,20 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow class AuthMockVM: ViewModel(), AuthBaseVM { - override val uiState: State - get() = mutableStateOf(AuthVM.AuthPageState(AuthVM.AuthState.SignedIn)) + private val _uiState = mutableStateOf(AuthVM.AuthPageState(AuthVM.AuthState.SignedIn)) + override val uiState: State = _uiState + override val eventFlow: SharedFlow get() = MutableSharedFlow() + init { + _uiState.value = _uiState.value.copy( + authState = AuthVM.AuthState.SignedIn +// authState = AuthVM.AuthState.Loading +// authState = AuthVM.AuthState.SignedOut + ) + } + override fun signOut(onCompleteSignOutCallback: () -> Unit) { } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt index 829d054..dbe4ebf 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt @@ -21,11 +21,15 @@ class BackupPageMockVM: ViewModel(), BackupPageBaseVM { dummyGoogleDriveFileOne, dummyGoogleDriveFileTwo ) +// emptyList() ) override val driveBackupFileList: LiveData> = _driveBackupFileList init { - _uiState.value = _uiState.value.copy(listOfBackupFileState = BackupPageVM.FileBackupListState.FileBackupListFailed("error")) + _uiState.value = _uiState.value.copy( + listOfBackupFileState = BackupPageVM.FileBackupListState.FileBackupListFailed("error") +// listOfBackupFileState = BackupPageVM.FileBackupListState.FileBackupListFinished + ) } override fun backupDatabase() { From b333b3fba3eb71e7cbdf0453a983f9de7b3b3d83 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Tue, 12 Aug 2025 05:59:26 +0700 Subject: [PATCH 18/28] Feat feature/ui-revamp : Extract constant label & placeholder into separated string.xml --- .../ventnote/feature/backup/BackupPage.kt | 32 +++---- .../backup/components/button/SignInButton.kt | 16 ++-- .../backup/components/list/BackupFileList.kt | 83 ++++++++++++++----- .../backup/components/navbar/AppBar.kt | 23 ++--- .../backup/viewmodel/BackupPageMockVM.kt | 4 +- app/src/main/res/values/strings.xml | 16 +++- 6 files changed, 104 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt index 23a085a..95ae68e 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt @@ -120,12 +120,9 @@ fun BackupPage( topBar = { BackupPageAppBar( authVM = authViewModel, - onBackPressed = { navHostController.popBackStack() }, + onBackRequest = { navHostController.popBackStack() }, scrollBehavior = rememberedScrollBehavior, - onBackupPressed = { - backupDatabase() - }, - onLogoutPressed = { + onLogoutRequest = { authViewModel.signOut(onCompleteSignOutCallback = { backupPageVM.getBackupFileList() }) @@ -160,15 +157,15 @@ fun BackupPage( AuthVM.AuthState.SignedIn -> { BackupFileList( backupPageVM = backupPageVM, - onRestoreCallback = { + onRestoreRequest = { restoreDataIdState.value = it.id restoreConfirmationDialogState.value = true }, - onDeleteCallback = { + onDeleteRequest = { deleteDataIdState.value = it.id deleteConfirmationDialogState.value = true }, - successfullyRestoredCallback = { + successfullyRestoredRequest = { scope.launch { snackBarHostState.showSnackbar( message = restoredMessage, @@ -176,13 +173,16 @@ fun BackupPage( ) } }, - successfullyDeletedCallback = { + successfullyDeletedRequest = { scope.launch { snackBarHostState.showSnackbar( message = deletedMessage, withDismissAction = true ) } + }, + onBackupRequest = { + backupDatabase() } ) } @@ -221,7 +221,7 @@ fun BackupPage( isOpened = deleteConfirmationDialogState.value, onDismissCallback = { deleteConfirmationDialogState.value = false }, onConfirmCallback = { - val selectedId = restoreDataIdState.value + val selectedId = deleteDataIdState.value if (selectedId != stringZero) { backupPageVM.deleteDatabase(selectedId) deleteConfirmationDialogState.value = false @@ -247,8 +247,8 @@ private fun LoadingStateContent() { color = MaterialTheme.colorScheme.primary ) Text( - text = "Loading...", - style = MaterialTheme.typography.bodyLarge, + text = stringResource(R.string.loading), + style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -290,8 +290,8 @@ private fun SignedOutStateContent( } Text( - text = "Sign in to access backups", - style = MaterialTheme.typography.headlineSmall, + text = stringResource(R.string.sign_in_to_access_backup), + style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurface @@ -300,8 +300,8 @@ private fun SignedOutStateContent( Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Connect with Google to backup and restore your data securely", - style = MaterialTheme.typography.bodyMedium, + text = stringResource(R.string.connect_with_google_to_backup), + style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 16.dp) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt index cdf6958..88fe991 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt @@ -12,7 +12,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.with import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -47,6 +46,7 @@ fun SignInButton( val context = LocalContext.current var isLoading by remember { mutableStateOf(false) } + val authenticationFailedText = stringResource(R.string.authentication_failed); val launcher = rememberLauncherForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> @@ -55,11 +55,7 @@ fun SignInButton( authViewModel.checkAuthState() signInSuccessCallback() } else { - Toast.makeText( - context, - "Authentication failed. Please try again.", - Toast.LENGTH_SHORT - ).show() + Toast.makeText(context, authenticationFailedText, Toast.LENGTH_SHORT).show() } } @@ -68,9 +64,7 @@ fun SignInButton( isLoading = true launcher.launch(authViewModel.getSignInIntent()) }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), + modifier = Modifier.height(56.dp), shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary, @@ -100,7 +94,7 @@ fun SignInButton( color = MaterialTheme.colorScheme.onPrimary ) Text( - text = "Signing in...", + text = stringResource(R.string.signing_in), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Medium ) @@ -111,7 +105,7 @@ fun SignInButton( verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = Icons.AutoMirrored.Filled.Login, // Add Google icon + imageVector = Icons.AutoMirrored.Filled.Login, contentDescription = null, modifier = Modifier.size(20.dp) ) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt index 8697f96..579b8f4 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -21,6 +22,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CloudDownload import androidx.compose.material.icons.filled.CloudOff import androidx.compose.material.icons.filled.CloudQueue +import androidx.compose.material.icons.filled.CloudUpload import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.ErrorOutline import androidx.compose.material.icons.filled.Refresh @@ -53,6 +55,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.digiventure.ventnote.R +import com.digiventure.ventnote.commons.Constants.EMPTY_STRING import com.digiventure.ventnote.commons.TestTags import com.digiventure.ventnote.components.dialog.LoadingDialog import com.digiventure.ventnote.feature.backup.viewmodel.BackupPageBaseVM @@ -62,10 +65,11 @@ import kotlinx.coroutines.launch @Composable fun BackupFileList(backupPageVM: BackupPageBaseVM, - successfullyRestoredCallback: () -> Unit, - successfullyDeletedCallback: () -> Unit, - onRestoreCallback: (File) -> Unit, - onDeleteCallback: (File) -> Unit) { + successfullyRestoredRequest: () -> Unit, + successfullyDeletedRequest: () -> Unit, + onRestoreRequest: (File) -> Unit, + onDeleteRequest: (File) -> Unit, + onBackupRequest: () -> Unit) { val backupPageUiState = backupPageVM.uiState.value val driveBackupFileListState = backupPageVM.driveBackupFileList.observeAsState() @@ -97,19 +101,19 @@ fun BackupFileList(backupPageVM: BackupPageBaseVM, "Restore notes process failed : ${fileRestoreState.errorMessage}" fileDeleteState is BackupPageVM.FileDeleteState.SyncFailed -> "Delete notes process failed : ${fileDeleteState.errorMessage}" - else -> "" + else -> EMPTY_STRING } Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show() } fileRestoreState is BackupPageVM.FileRestoreState.SyncFinished-> { restoreLoadingDialogState.value = false - successfullyRestoredCallback() + successfullyRestoredRequest() } fileDeleteState is BackupPageVM.FileDeleteState.SyncFinished -> { restoreLoadingDialogState.value = false - successfullyDeletedCallback() + successfullyDeletedRequest() } fileRestoreState is BackupPageVM.FileRestoreState.SyncStarted || @@ -121,14 +125,14 @@ fun BackupFileList(backupPageVM: BackupPageBaseVM, } } - when (val state = backupPageUiState.listOfBackupFileState) { + when (backupPageUiState.listOfBackupFileState) { BackupPageVM.FileBackupListState.FileBackupListFinished -> { driveBackupFileListState.value.let { backupFiles -> if (backupFiles.isNullOrEmpty()) { EmptyBackupListContainer() } else { - BackupListContainer(backupFiles, onRestoreCallback, onDeleteCallback) + BackupListContainer(backupFiles, onRestoreRequest, onDeleteRequest, onBackupRequest) } } } @@ -172,14 +176,14 @@ fun EmptyBackupListContainer() { tint = MaterialTheme.colorScheme.onSurfaceVariant ) Text( - text = "No backups found", + text = stringResource(R.string.no_backup_found), style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center ) Text( - text = "Create your first backup using the backup button", - style = MaterialTheme.typography.bodyMedium, + text = stringResource(R.string.create_your_first_backup), + style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center ) @@ -190,19 +194,54 @@ fun EmptyBackupListContainer() { @Composable fun BackupListContainer( backupFiles: List, - onRestoreCallback: (File) -> Unit, - onDeleteCallback: (File) -> Unit + onRestoreRequest: (File) -> Unit, + onDeleteRequest: (File) -> Unit, + onBackupRequest: () -> Unit ) { LazyColumn( modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(vertical = 16.dp) ) { + item(key = "backup_note_button") { + Button( + onClick = { + onBackupRequest() + }, + modifier = Modifier.fillMaxWidth().height(56.dp), + shape = RoundedCornerShape(16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 4.dp, + pressedElevation = 8.dp + ), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.CloudUpload, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(id = R.string.backup_notes), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium + ) + } + } + } + items(items = backupFiles) { file -> Card( modifier = Modifier .fillMaxWidth() - .semantics { contentDescription = "" }, + .semantics { contentDescription = EMPTY_STRING }, shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface @@ -254,7 +293,7 @@ fun BackupListContainer( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { FilledTonalButton( - onClick = { onRestoreCallback(file) }, + onClick = { onRestoreRequest(file) }, shape = RoundedCornerShape(12.dp), colors = ButtonDefaults.filledTonalButtonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, @@ -264,13 +303,13 @@ fun BackupListContainer( ) { Icon( imageVector = Icons.Filled.CloudDownload, - contentDescription = "Restore", + contentDescription = stringResource(R.string.restore_icon), modifier = Modifier.size(18.dp) ) } OutlinedButton( - onClick = { onDeleteCallback(file) }, + onClick = { onDeleteRequest(file) }, shape = RoundedCornerShape(12.dp), colors = ButtonDefaults.outlinedButtonColors( contentColor = MaterialTheme.colorScheme.error @@ -280,7 +319,7 @@ fun BackupListContainer( ) { Icon( imageVector = Icons.Filled.Delete, - contentDescription = "Delete", + contentDescription = stringResource(R.string.delete_icon), modifier = Modifier.size(18.dp) ) } @@ -326,7 +365,7 @@ fun BackupFailedContainer( } Text( - text = "Failed to load backups", + text = stringResource(R.string.failed_to_load_backups), style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center @@ -374,7 +413,7 @@ fun FileBackupListStartedContainer() { color = MaterialTheme.colorScheme.primary ) Text( - text = "Loading backups...", + text = stringResource(R.string.loading_backups), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/navbar/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/navbar/AppBar.kt index bdd1aa3..a99b6da 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/navbar/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/navbar/AppBar.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Logout -import androidx.compose.material.icons.filled.CloudUpload import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -26,9 +25,8 @@ import com.digiventure.ventnote.feature.backup.viewmodel.AuthVM @Composable fun BackupPageAppBar( authVM: AuthBaseVM, - onBackPressed: () -> Unit, - onLogoutPressed: () -> Unit, - onBackupPressed: () -> Unit, + onBackRequest: () -> Unit, + onLogoutRequest: () -> Unit, scrollBehavior: TopAppBarScrollBehavior) { val authUiState = authVM.uiState.value @@ -52,7 +50,7 @@ fun BackupPageAppBar( Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.backup_nav_icon), Modifier.semantics { }) { - onBackPressed() + onBackRequest() } }, scrollBehavior = scrollBehavior, @@ -60,8 +58,7 @@ fun BackupPageAppBar( actions = { if (authUiState.authState == AuthVM.AuthState.SignedIn) { TrailingMenuIcons( - onBackupPressed = onBackupPressed, - onLogoutPressed = onLogoutPressed + onLogoutRequest = onLogoutRequest ) } } @@ -70,20 +67,12 @@ fun BackupPageAppBar( @Composable fun TrailingMenuIcons( - onLogoutPressed: () -> Unit, - onBackupPressed: () -> Unit, + onLogoutRequest: () -> Unit, ) { - TopNavBarIcon( - Icons.Filled.CloudUpload, - stringResource(R.string.backup), - modifier = Modifier.semantics { }) { - onBackupPressed() - } - TopNavBarIcon( Icons.AutoMirrored.Filled.Logout, stringResource(R.string.logout_nav_icon), modifier = Modifier.semantics { }) { - onLogoutPressed() + onLogoutRequest() } } \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt index dbe4ebf..c17258e 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt @@ -27,8 +27,8 @@ class BackupPageMockVM: ViewModel(), BackupPageBaseVM { init { _uiState.value = _uiState.value.copy( - listOfBackupFileState = BackupPageVM.FileBackupListState.FileBackupListFailed("error") -// listOfBackupFileState = BackupPageVM.FileBackupListState.FileBackupListFinished +// listOfBackupFileState = BackupPageVM.FileBackupListState.FileBackupListFailed("error") + listOfBackupFileState = BackupPageVM.FileBackupListState.FileBackupListFinished ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 20dc948..7ed2c9d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,7 +62,6 @@ Add New Note Share Preview Backup Notes - Loading Sort By Title Modified Date @@ -72,6 +71,11 @@ Descending Notes Cancel + No backups found + Create your first backup using the backup button + Failed to load backups + Sign in to access backups + Connect with Google to backup and restore your data securely about us @@ -89,16 +93,24 @@ switch to dark mode switch to light mode - + Note is successfully deleted Note is successfully backed up Note is successfully restored Note is successfully updated Failed to share note + Authentication failed. Please try again + + + Signing in … + Loading backups … + Loading … title textField body textField + Restore Icon + Delete Icon WEB_CLIENT_ID \ No newline at end of file From e883e9a7d2e1e8b25acadcef747c72f7d8528d32 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Tue, 12 Aug 2025 06:00:23 +0700 Subject: [PATCH 19/28] Feat feature/ui-revamp : Remove redundant semicolon --- .../ventnote/feature/backup/components/button/SignInButton.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt index 88fe991..bba714c 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt @@ -46,7 +46,7 @@ fun SignInButton( val context = LocalContext.current var isLoading by remember { mutableStateOf(false) } - val authenticationFailedText = stringResource(R.string.authentication_failed); + val authenticationFailedText = stringResource(R.string.authentication_failed) val launcher = rememberLauncherForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> From 2b89082d29496fa8df2baf3b97915a0482feb46e Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Tue, 12 Aug 2025 07:05:16 +0700 Subject: [PATCH 20/28] Feat feature/ui-revamp : Improve note creation ui/ux --- .../digiventure/ventnote/commons/TestTags.kt | 3 - .../feature/note_creation/NoteCreationPage.kt | 245 +++++++----------- .../components/{ => navbar}/AppBar.kt | 19 +- .../components/navbar/EnhancedBottomAppBar.kt | 113 ++++++++ .../components/section/NoteSection.kt | 133 ++++++++++ .../components/section/TitleSection.kt | 135 ++++++++++ .../ventnote/feature/notes/NotesPage.kt | 2 +- app/src/main/res/values/strings.xml | 3 +- 8 files changed, 485 insertions(+), 168 deletions(-) rename app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/{ => navbar}/AppBar.kt (79%) create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/EnhancedBottomAppBar.kt create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/section/NoteSection.kt create mode 100644 app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/section/TitleSection.kt diff --git a/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt b/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt index 3d44169..654764c 100644 --- a/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt +++ b/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt @@ -13,14 +13,11 @@ object TestTags { const val BOTTOM_SHEET = "bottom_sheet" const val DELETE_ICON_BUTTON = "delete_icon_button" - const val CLOSE_SEARCH_ICON_BUTTON = "close_search_icon_button" - const val SEARCH_ICON_BUTTON = "search_icon_button" const val SORT_ICON_BUTTON = "sort_icon_button" const val CLOSE_SELECT_ICON_BUTTON = "close_select_icon_button" const val MENU_ICON_BUTTON = "menu_icon_button" const val TOP_APPBAR_TITLE = "top_appbar_title" const val TOP_APPBAR_TEXT_FIELD = "top_appbar_text_field" - const val SELECTED_COUNT = "selected_count" const val DROPDOWN_SELECT = "dropdown_select" const val SELECT_ALL_OPTION = "select_all_option" const val UNSELECT_ALL_OPTION = "unselect_all_option" diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/NoteCreationPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/NoteCreationPage.kt index 9368485..830b7d1 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/NoteCreationPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/NoteCreationPage.kt @@ -1,25 +1,17 @@ package com.digiventure.ventnote.feature.note_creation import android.content.pm.ActivityInfo +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -27,33 +19,30 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.digiventure.ventnote.R +import com.digiventure.ventnote.commons.Constants.EMPTY_STRING import com.digiventure.ventnote.commons.TestTags import com.digiventure.ventnote.components.LockScreenOrientation import com.digiventure.ventnote.components.dialog.TextDialog import com.digiventure.ventnote.data.persistence.NoteModel -import com.digiventure.ventnote.feature.note_creation.components.NoteCreationAppBar +import com.digiventure.ventnote.feature.note_creation.components.navbar.EnhancedBottomAppBar +import com.digiventure.ventnote.feature.note_creation.components.navbar.NoteCreationAppBar +import com.digiventure.ventnote.feature.note_creation.components.section.NoteSection +import com.digiventure.ventnote.feature.note_creation.components.section.TitleSection import com.digiventure.ventnote.feature.note_creation.viewmodel.NoteCreationPageBaseVM import com.digiventure.ventnote.feature.note_creation.viewmodel.NoteCreationPageMockVM import com.digiventure.ventnote.feature.note_creation.viewmodel.NoteCreationPageVM import kotlinx.coroutines.launch -const val TAG : String = "NoteCreationPage" - @OptIn(ExperimentalMaterial3Api::class) @Composable fun NoteCreationPage( @@ -62,9 +51,12 @@ fun NoteCreationPage( ) { LockScreenOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) - // String resource - val titleTextField = "${stringResource(R.string.title_textField)}-${TAG}" - val bodyTextField = "${stringResource(R.string.body_textField)}-${TAG}" + val titleTextFieldText = stringResource(R.string.title_textField) + val bodyTextFieldText = stringResource(R.string.body_textField) + + // String resources - optimized to avoid recomputation + val titleTextField = remember { "${titleTextFieldText}-${TestTags.NOTE_CREATION_PAGE}" } + val bodyTextField = remember { "${bodyTextFieldText}-${TestTags.NOTE_CREATION_PAGE}" } val titleInput = stringResource(R.string.title_textField_input) val bodyInput = stringResource(R.string.body_textField_input) @@ -72,40 +64,45 @@ fun NoteCreationPage( val scope = rememberCoroutineScope() + // Optimized state management - using delegation for better performance val requiredDialogState = remember { mutableStateOf(false) } val cancelDialogState = remember { mutableStateOf(false) } val snackBarHostState = remember { SnackbarHostState() } - fun addNote() { - if (viewModel.titleText.value.isEmpty() || viewModel.descriptionText.value.isEmpty()) { - requiredDialogState.value = true - } else { - scope.launch { - viewModel.addNote( - NoteModel( - title = viewModel.titleText.value, - note = viewModel.descriptionText.value) - ) - .onSuccess { + // Extracted and optimized addNote function + val noteIsSuccessfullyAddedText = stringResource(R.string.successfully_added) + val addNote = remember { + { + if (viewModel.titleText.value.isEmpty() || viewModel.descriptionText.value.isEmpty()) { + requiredDialogState.value = true + } else { + scope.launch { + viewModel.addNote( + NoteModel( + title = viewModel.titleText.value, + note = viewModel.descriptionText.value + ) + ).onSuccess { navHostController.popBackStack() snackBarHostState.showSnackbar( - message = "Note successfully added", + message = noteIsSuccessfullyAddedText, withDismissAction = true ) - } - .onFailure { + }.onFailure { snackBarHostState.showSnackbar( - message = it.message ?: "", + message = it.message ?: EMPTY_STRING, withDismissAction = true ) } + } } } } + // Optimized scroll behavior - remember to prevent recreation val appBarState = rememberTopAppBarState() - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(appBarState) - val rememberedScrollBehavior = remember { scrollBehavior } + val scrollBehaviorState = TopAppBarDefaults.enterAlwaysScrollBehavior(appBarState) + val scrollBehavior = remember { scrollBehaviorState } Scaffold( topBar = { @@ -114,135 +111,81 @@ fun NoteCreationPage( onBackPressed = { cancelDialogState.value = true }, - scrollBehavior = rememberedScrollBehavior + scrollBehavior = scrollBehavior ) }, - snackbarHost = { SnackbarHost(snackBarHostState) }, - floatingActionButton = { - ExtendedFloatingActionButton( - onClick = { addNote() }, - modifier = Modifier.semantics { - testTag = "add-note-fab" - }, - text = { - Text(stringResource(R.string.save), fontSize = 16.sp, fontWeight = FontWeight.Medium) - }, - icon = { - Icon( - imageVector = Icons.Filled.Check, - contentDescription = stringResource(R.string.fab) - ) - }, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary + snackbarHost = { + SnackbarHost( + hostState = snackBarHostState, + modifier = Modifier.padding(bottom = 16.dp) ) }, content = { contentPadding -> - Box(modifier = Modifier.padding(contentPadding)) { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxSize() - .verticalScroll(rememberScrollState()) + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize() + ) { + // Better scrolling performance with LazyColumn + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 8.dp + ), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - TextField( - value = viewModel.titleText.value, - onValueChange = { - viewModel.titleText.value = it - }, - textStyle = TextStyle( - fontSize = 18.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ), - singleLine = false, - colors = TextFieldDefaults.colors( - disabledTextColor = MaterialTheme.colorScheme.surface, - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - disabledContainerColor = MaterialTheme.colorScheme.surface, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - ), - modifier = Modifier - .fillMaxWidth() - .semantics { contentDescription = titleTextField }, - placeholder = { - Text( - text = titleInput, - fontSize = 18.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - } - ) - TextField( - value = viewModel.descriptionText.value, - onValueChange = { - viewModel.descriptionText.value = it - }, - textStyle = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface - ), - singleLine = false, - shape = RectangleShape, - colors = TextFieldDefaults.colors( - disabledTextColor = MaterialTheme.colorScheme.surface, - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - disabledContainerColor = MaterialTheme.colorScheme.surface, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - ), - modifier = Modifier - .fillMaxWidth() - .fillMaxSize() - .semantics { contentDescription = bodyTextField }, - placeholder = { - Text( - text = bodyInput, - fontSize = 18.sp, - color = MaterialTheme.colorScheme.onSurface - ) - } - ) + item { + TitleSection(viewModel, titleTextField, titleInput) + } + item { + NoteSection(viewModel, bodyTextField, bodyInput) + } } } }, + bottomBar = { + EnhancedBottomAppBar { + addNote() + } + }, modifier = Modifier .semantics { testTag = TestTags.NOTE_CREATION_PAGE } .nestedScroll(scrollBehavior.nestedScrollConnection), containerColor = MaterialTheme.colorScheme.surface ) - val missingFieldName = if (viewModel.titleText.value.isEmpty()) { - "Title" - } else if (viewModel.descriptionText.value.isEmpty()) { - "Notes" - } else { - "" + // Optimized missing field calculation + val emptyTitleText = stringResource(R.string.empty_note_title_placeholder) + val emptyNoteText = stringResource(R.string.empty_note_placeholder) + val missingFieldName = remember(viewModel.titleText.value, viewModel.descriptionText.value) { + when { + viewModel.titleText.value.isEmpty() -> emptyTitleText + viewModel.descriptionText.value.isEmpty() -> emptyNoteText + else -> EMPTY_STRING + } } - TextDialog( - title = stringResource(R.string.required_title), - description = stringResource(R.string.required_confirmation_text, missingFieldName), - isOpened = requiredDialogState.value, - onDismissCallback = { requiredDialogState.value = false }, - onConfirmCallback = { requiredDialogState.value = false }) + if (requiredDialogState.value) { + TextDialog( + title = stringResource(R.string.required_title), + description = stringResource(R.string.required_confirmation_text, missingFieldName), + isOpened = requiredDialogState.value, + onDismissCallback = { requiredDialogState.value = false }, + onConfirmCallback = { requiredDialogState.value = false }) + } - TextDialog( - title = stringResource(R.string.cancel_title), - description = stringResource(R.string.cancel_confirmation_text), - isOpened = cancelDialogState.value, - onDismissCallback = { cancelDialogState.value = false }, - onConfirmCallback = { - navHostController.popBackStack() - cancelDialogState.value = false - }) + if (cancelDialogState.value) { + TextDialog( + title = stringResource(R.string.cancel_title), + description = stringResource(R.string.cancel_confirmation_text), + isOpened = cancelDialogState.value, + onDismissCallback = { cancelDialogState.value = false }, + onConfirmCallback = { + navHostController.popBackStack() + cancelDialogState.value = false + }) + } } @Preview diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/AppBar.kt similarity index 79% rename from app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/AppBar.kt rename to app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/AppBar.kt index 67b0bf0..13d78c7 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/AppBar.kt @@ -1,22 +1,18 @@ -package com.digiventure.ventnote.feature.note_creation.components +package com.digiventure.ventnote.feature.note_creation.components.navbar -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.digiventure.ventnote.R import com.digiventure.ventnote.components.navbar.TopNavBarIcon @@ -26,16 +22,15 @@ fun NoteCreationAppBar( descriptionTextLength: Int, onBackPressed: () -> Unit, scrollBehavior: TopAppBarScrollBehavior) { - TopAppBar( + + CenterAlignedTopAppBar( title = { Text( text = if(descriptionTextLength > 0) "$descriptionTextLength" else stringResource(id = R.string.add_new_note), color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(start = 4.dp), - style = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 20.sp - ) + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold + ), ) }, colors = TopAppBarDefaults.topAppBarColors( diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/EnhancedBottomAppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/EnhancedBottomAppBar.kt new file mode 100644 index 0000000..a18e20c --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/EnhancedBottomAppBar.kt @@ -0,0 +1,113 @@ +package com.digiventure.ventnote.feature.note_creation.components.navbar + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.digiventure.ventnote.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EnhancedBottomAppBar( + onSaveClick: () -> Unit, +) { + BottomAppBar( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + tonalElevation = 12.dp, + actions = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + EnhancedBottomBarButton( + icon = Icons.Rounded.Check, + label = stringResource(R.string.save), + onClick = onSaveClick, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + isProminent = true + ) + } + } + ) +} + +@Composable +private fun EnhancedBottomBarButton( + icon: ImageVector, + label: String, + onClick: () -> Unit, + containerColor: Color, + contentColor: Color, + isProminent: Boolean = false +) { + val haptics = LocalHapticFeedback.current + val scale by animateFloatAsState( + targetValue = if (isProminent) 1.1f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "button_scale" + ) + + Row ( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .scale(scale) + .clip(RoundedCornerShape(16.dp)) + .background( + containerColor.copy(alpha = 0.8f), + RoundedCornerShape(16.dp) + ) + .clickable { + haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove) + onClick() + } + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Icon( + imageVector = icon, + contentDescription = label, + tint = contentColor, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + color = contentColor, + fontWeight = if (isProminent) FontWeight.SemiBold else FontWeight.Medium, + maxLines = 1 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/section/NoteSection.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/section/NoteSection.kt new file mode 100644 index 0000000..8c6f25b --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/section/NoteSection.kt @@ -0,0 +1,133 @@ +package com.digiventure.ventnote.feature.note_creation.components.section + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Description +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.digiventure.ventnote.R +import com.digiventure.ventnote.feature.note_creation.viewmodel.NoteCreationPageBaseVM + +@Composable +fun NoteSection( + viewModel: NoteCreationPageBaseVM, + bodyTextField: String, + bodyInput: String +) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 12.dp) + ) { + Icon( + imageVector = Icons.Rounded.Description, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.notes), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + } + + ImprovedDescriptionTextField( + viewModel = viewModel, + bodyTextField = bodyTextField, + bodyInput = bodyInput + ) + } +} + +@Composable +fun ImprovedDescriptionTextField( + viewModel: NoteCreationPageBaseVM, + bodyTextField: String, + bodyInput: String +) { + val label = "border_color" + val borderColor by animateColorAsState( + targetValue = MaterialTheme.colorScheme.primary, + animationSpec = tween(300), + label = label + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .animateContentSize() + .heightIn(min = 200.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation( + defaultElevation = 4.dp + ), + border = BorderStroke( + width = 2.dp, + color = borderColor + ) + ) { + TextField( + value = viewModel.descriptionText.value, + onValueChange = { viewModel.descriptionText.value = it }, + textStyle = MaterialTheme.typography.titleMedium.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + singleLine = false, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + cursorColor = MaterialTheme.colorScheme.primary + ), + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .semantics { contentDescription = bodyTextField }, + placeholder = { + Text( + text = bodyInput, + style = MaterialTheme.typography.titleMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + ), + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/section/TitleSection.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/section/TitleSection.kt new file mode 100644 index 0000000..3ae2195 --- /dev/null +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/section/TitleSection.kt @@ -0,0 +1,135 @@ +package com.digiventure.ventnote.feature.note_creation.components.section + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Title +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.digiventure.ventnote.R +import com.digiventure.ventnote.feature.note_creation.viewmodel.NoteCreationPageBaseVM + +@Composable +fun TitleSection( + viewModel: NoteCreationPageBaseVM, + titleTextField: String, + titleInput: String +) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 12.dp) + ) { + Icon( + imageVector = Icons.Rounded.Title, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.sort_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + } + + ImprovedTitleTextField( + viewModel = viewModel, + titleTextField = titleTextField, + titleInput = titleInput + ) + } +} + +@Composable +fun ImprovedTitleTextField( + viewModel: NoteCreationPageBaseVM, + titleTextField: String, + titleInput: String +) { + val label = "border_color" + val focusRequester = remember { FocusRequester() } + val borderColor by animateColorAsState( + targetValue = MaterialTheme.colorScheme.primary, + animationSpec = tween(300), + label = label + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .animateContentSize(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation( + defaultElevation = 4.dp + ), + border = BorderStroke( + width = 2.dp, + color = borderColor + ) + ) { + TextField( + value = viewModel.titleText.value, + onValueChange = { viewModel.titleText.value = it }, + textStyle = MaterialTheme.typography.titleMedium.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + singleLine = false, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + cursorColor = MaterialTheme.colorScheme.primary + ), + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = titleTextField } + .focusRequester(focusRequester), + placeholder = { + Text( + text = titleInput, + style = MaterialTheme.typography.titleMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + fontWeight = FontWeight.Medium, + ), + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt index 2f7c281..aeda69c 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt @@ -315,7 +315,7 @@ fun NotesPage( TextDialog( isOpened = true, onDismissCallback = { showDeleteDialog = false }, - onConfirmCallback = { deleteNoteList }, + onConfirmCallback = { deleteNoteList() }, modifier = Modifier.semantics { testTag = TestTags.CONFIRMATION_DIALOG } ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7ed2c9d..e920256 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,7 +52,7 @@ Input title here Insert title Insert note - Untitled Note + Title Note @@ -100,6 +100,7 @@ Note is successfully updated Failed to share note Authentication failed. Please try again + Note is successfully added Signing in … From 0c04d2bc93b41c56ea07b6789ec6525ed6fc3722 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Tue, 12 Aug 2025 07:46:47 +0700 Subject: [PATCH 21/28] Feat feature/ui-revamp : Reduce ,manual text styling, use typography instead --- .../digiventure/ventnote/commons/TestTags.kt | 1 - .../components/dialog/LoadingDialog.kt | 8 ++-- .../ventnote/components/dialog/TextDialog.kt | 11 ++--- .../ventnote/data/persistence/NoteModel.kt | 4 +- .../backup/components/navbar/AppBar.kt | 2 +- .../feature/note_creation/NoteCreationPage.kt | 45 ++++++++++--------- .../note_creation/components/navbar/AppBar.kt | 3 +- .../note_detail/components/navbar/AppBar.kt | 2 +- .../components/section/NoteSection.kt | 12 ++--- .../components/section/TitleSection.kt | 15 +++---- .../ventnote/feature/notes/NotesPage.kt | 6 +-- .../components/drawer/NavigationDrawer.kt | 16 ++++--- .../feature/notes/components/item/NoteItem.kt | 20 ++++----- .../feature/notes/components/navbar/AppBar.kt | 29 ++++++------ .../notes/components/searchbar/SearchBar.kt | 11 ++--- .../notes/viewmodel/NotesPageMockVM.kt | 10 ++--- .../feature/share_preview/SharePreviewPage.kt | 21 +++------ .../share_preview/components/navbar/AppBar.kt | 2 +- .../com/digiventure/ventnote/ui/theme/Type.kt | 28 +----------- 19 files changed, 101 insertions(+), 145 deletions(-) diff --git a/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt b/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt index 654764c..5f16d25 100644 --- a/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt +++ b/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt @@ -29,7 +29,6 @@ object TestTags { // Note lists test tags const val ADD_NOTE_FAB = "add_note_fab" - const val SHARE_NOTE_FAB = "share_note_fab" const val NOTE_RV = "note_rv" const val LOADING_DIALOG = "loading_dialog" const val CONFIRMATION_DIALOG = "confirmation_dialog" diff --git a/app/src/main/java/com/digiventure/ventnote/components/dialog/LoadingDialog.kt b/app/src/main/java/com/digiventure/ventnote/components/dialog/LoadingDialog.kt index daf1523..b341f93 100644 --- a/app/src/main/java/com/digiventure/ventnote/components/dialog/LoadingDialog.kt +++ b/app/src/main/java/com/digiventure/ventnote/components/dialog/LoadingDialog.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.digiventure.ventnote.R @OptIn(ExperimentalMaterial3Api::class) @@ -46,9 +45,10 @@ fun LoadingDialog( ) Text( text = stringResource(id = R.string.loading), - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface, + ) ) } } diff --git a/app/src/main/java/com/digiventure/ventnote/components/dialog/TextDialog.kt b/app/src/main/java/com/digiventure/ventnote/components/dialog/TextDialog.kt index 24d29d4..1de1bd3 100644 --- a/app/src/main/java/com/digiventure/ventnote/components/dialog/TextDialog.kt +++ b/app/src/main/java/com/digiventure/ventnote/components/dialog/TextDialog.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.digiventure.ventnote.R import com.digiventure.ventnote.commons.TestTags @@ -59,8 +58,9 @@ fun TextDialog( ) { Text( text = stringResource(R.string.confirm), - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ) ) } } @@ -73,8 +73,9 @@ fun TextDialog( ) { Text( text = stringResource(R.string.dismiss), - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ) ) } }, diff --git a/app/src/main/java/com/digiventure/ventnote/data/persistence/NoteModel.kt b/app/src/main/java/com/digiventure/ventnote/data/persistence/NoteModel.kt index feb3b05..573b42d 100644 --- a/app/src/main/java/com/digiventure/ventnote/data/persistence/NoteModel.kt +++ b/app/src/main/java/com/digiventure/ventnote/data/persistence/NoteModel.kt @@ -15,6 +15,4 @@ data class NoteModel( @ColumnInfo(name = "note") val note: String, @ColumnInfo(name = "created_at") var createdAt: Date = Date(System.currentTimeMillis()), @ColumnInfo(name = "updated_at") var updatedAt: Date = Date(System.currentTimeMillis()), -): Parcelable { - constructor(title: String, note: String) : this(0, title, note) -} \ No newline at end of file +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/navbar/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/navbar/AppBar.kt index a99b6da..5aafdc2 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/navbar/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/navbar/AppBar.kt @@ -37,7 +37,7 @@ fun BackupPageAppBar( text = stringResource(id = R.string.backup_notes), modifier = Modifier.padding(start = 4.dp), color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.headlineSmall.copy( + style = MaterialTheme.typography.titleLarge.copy( fontWeight = FontWeight.SemiBold ), ) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/NoteCreationPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/NoteCreationPage.kt index 830b7d1..7e3f54c 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/NoteCreationPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/NoteCreationPage.kt @@ -1,8 +1,9 @@ package com.digiventure.ventnote.feature.note_creation import android.content.pm.ActivityInfo +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -20,6 +21,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag @@ -60,8 +62,7 @@ fun NoteCreationPage( val titleInput = stringResource(R.string.title_textField_input) val bodyInput = stringResource(R.string.body_textField_input) - val length = viewModel.descriptionText.value.length - + val focusManager = LocalFocusManager.current val scope = rememberCoroutineScope() // Optimized state management - using delegation for better performance @@ -79,6 +80,7 @@ fun NoteCreationPage( scope.launch { viewModel.addNote( NoteModel( + id = 0, title = viewModel.titleText.value, note = viewModel.descriptionText.value ) @@ -107,7 +109,6 @@ fun NoteCreationPage( Scaffold( topBar = { NoteCreationAppBar( - descriptionTextLength = length, onBackPressed = { cancelDialogState.value = true }, @@ -121,26 +122,28 @@ fun NoteCreationPage( ) }, content = { contentPadding -> - Box( + // Better scrolling performance with LazyColumn + LazyColumn( modifier = Modifier .padding(contentPadding) - .fillMaxSize() - ) { - // Better scrolling performance with LazyColumn - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues( - horizontal = 16.dp, - vertical = 8.dp - ), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - item { - TitleSection(viewModel, titleTextField, titleInput) - } - item { - NoteSection(viewModel, bodyTextField, bodyInput) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + focusManager.clearFocus() } + .fillMaxSize(), + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 8.dp + ), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + TitleSection(viewModel, titleTextField, titleInput) + } + item { + NoteSection(viewModel, bodyTextField, bodyInput) } } }, diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/AppBar.kt index 13d78c7..c2e8536 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/AppBar.kt @@ -19,14 +19,13 @@ import com.digiventure.ventnote.components.navbar.TopNavBarIcon @OptIn(ExperimentalMaterial3Api::class) @Composable fun NoteCreationAppBar( - descriptionTextLength: Int, onBackPressed: () -> Unit, scrollBehavior: TopAppBarScrollBehavior) { CenterAlignedTopAppBar( title = { Text( - text = if(descriptionTextLength > 0) "$descriptionTextLength" else stringResource(id = R.string.add_new_note), + text = stringResource(id = R.string.add_new_note), color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleLarge.copy( fontWeight = FontWeight.SemiBold diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt index bbf0d2f..67c3d4a 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt @@ -30,7 +30,7 @@ fun NoteDetailAppBar( Text( text = if(isEditing) stringResource(id = R.string.note_edit) else stringResource(id = R.string.note_detail), color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.headlineSmall.copy( + style = MaterialTheme.typography.titleLarge.copy( fontWeight = FontWeight.SemiBold ), ) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt index 5d733ae..02a6b47 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt @@ -31,10 +31,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.digiventure.ventnote.R import com.digiventure.ventnote.feature.note_detail.viewmodel.NoteDetailPageBaseVM @@ -112,11 +110,8 @@ fun ImprovedDescriptionTextField( TextField( value = viewModel.descriptionText.value, onValueChange = { viewModel.descriptionText.value = it }, - textStyle = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, + textStyle = MaterialTheme.typography.titleMedium.copy( color = MaterialTheme.colorScheme.onSurface, - lineHeight = 20.sp ), singleLine = false, readOnly = !isEditingState, @@ -137,8 +132,9 @@ fun ImprovedDescriptionTextField( if (isEditingState) { Text( text = bodyInput, - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + style = MaterialTheme.typography.titleMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + ), ) } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt index dacb439..0f379cc 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt @@ -33,10 +33,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.digiventure.ventnote.R import com.digiventure.ventnote.feature.note_detail.viewmodel.NoteDetailPageBaseVM import kotlinx.coroutines.delay @@ -122,14 +120,10 @@ fun ImprovedTitleTextField( TextField( value = viewModel.titleText.value, onValueChange = { viewModel.titleText.value = it }, - textStyle = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Medium, + textStyle = MaterialTheme.typography.titleMedium.copy( color = MaterialTheme.colorScheme.onSurface, - lineHeight = 24.sp ), singleLine = false, - maxLines = 3, readOnly = !isEditingState, colors = TextFieldDefaults.colors( focusedContainerColor = Color.Transparent, @@ -148,9 +142,10 @@ fun ImprovedTitleTextField( if (isEditingState) { Text( text = titleInput, - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + style = MaterialTheme.typography.titleMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + fontWeight = FontWeight.Medium, + ), ) } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt index aeda69c..133214e 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt @@ -45,7 +45,6 @@ import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController @@ -221,8 +220,9 @@ fun NotesPage( text = { Text( text = stringResource(R.string.add), - fontSize = 16.sp, - fontWeight = FontWeight.Medium + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Medium + ) ) }, icon = { diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt index af738ff..5c37932 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt @@ -245,16 +245,18 @@ fun NavDrawerItem( ) { Text( text = title, - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(bottom = 2.dp) + modifier = Modifier.padding(bottom = 2.dp), + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) ) Text( text = subtitle, - fontSize = 12.sp, - fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface, + ) ) } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt index d7364e0..8b031e7 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt @@ -22,11 +22,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.digiventure.ventnote.commons.DateUtil import com.digiventure.ventnote.components.navbar.TopNavBarIcon import com.digiventure.ventnote.data.persistence.NoteModel @@ -78,11 +76,9 @@ fun NotesItem( text = data.title, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.titleMedium.copy( color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 24.sp + fontWeight = FontWeight.Medium, ), modifier = Modifier.padding(horizontal = 12.dp) ) @@ -102,11 +98,9 @@ fun NotesItem( text = data.note, maxLines = 4, overflow = TextOverflow.Ellipsis, - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, + style = MaterialTheme.typography.bodyMedium.copy( color = MaterialTheme.colorScheme.onSurface, - lineHeight = 20.sp + fontWeight = FontWeight.Normal, ), modifier = Modifier.padding(bottom = 4.dp) ) @@ -117,8 +111,10 @@ fun NotesItem( ), maxLines = 1, overflow = TextOverflow.Ellipsis, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + fontWeight = FontWeight.Normal, + ), ) } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt index 41a3a67..abc43ff 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt @@ -47,7 +47,6 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.digiventure.ventnote.R import com.digiventure.ventnote.commons.TestTags @@ -161,7 +160,7 @@ private fun SelectionTitle( append(" of $totalNotesCount selected") } }, - fontSize = 16.sp + style = MaterialTheme.typography.titleMedium ) Spacer(modifier = Modifier.width(4.dp)) @@ -230,15 +229,17 @@ private fun EnhancedDropdownMenu( Column { Text( text = stringResource(R.string.select_all), - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + ) ) if (totalNotesCount > 0) { Text( text = "Select all $totalNotesCount notes", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + style = MaterialTheme.typography.titleSmall.copy( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) ) } } @@ -281,15 +282,17 @@ private fun EnhancedDropdownMenu( Column { Text( text = stringResource(R.string.unselect_all), - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + ) ) if (markedNoteListSize > 0) { Text( text = "Clear selection of $markedNoteListSize notes", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + style = MaterialTheme.typography.titleSmall.copy( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) ) } } @@ -313,7 +316,7 @@ private fun AppTitle() { Text( text = stringResource(id = R.string.title), color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.headlineSmall.copy( + style = MaterialTheme.typography.titleLarge.copy( fontWeight = FontWeight.SemiBold ), modifier = Modifier.semantics { testTag = TestTags.TOP_APPBAR_TITLE }, diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt index f6ffdae..c796bfe 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt @@ -19,9 +19,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.digiventure.ventnote.R import com.digiventure.ventnote.commons.TestTags @@ -51,10 +49,8 @@ fun SearchBar( tint = Color.Gray ) }, - textStyle = TextStyle( + textStyle = MaterialTheme.typography.titleMedium.copy( color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 16.sp, - lineHeight = 0.sp ), singleLine = true, modifier = Modifier @@ -66,8 +62,9 @@ fun SearchBar( placeholder = { Text( text = stringResource(R.string.search_textField), - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.titleMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), modifier = Modifier.semantics { } ) }) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageMockVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageMockVM.kt index 309b0c6..3b83c91 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageMockVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageMockVM.kt @@ -20,17 +20,17 @@ class NotesPageMockVM : ViewModel(), NotesPageBaseVM { MutableLiveData( // Use MutableLiveData and set its value directly Result.success( listOf( - NoteModel("Title 1", "Note 1"), - NoteModel("Title 2", "Note 2"), - NoteModel("Title 3", "Note 3"), - NoteModel("Title 4", "Note 4") + NoteModel(0, "Title 1", "Note 1"), + NoteModel(1, "Title 2", "Note 2"), + NoteModel(2, "Title 3", "Note 3"), + NoteModel(3, "Title 4", "Note 4") ) ) ) override val searchedTitleText = mutableStateOf("") - override val isMarking = mutableStateOf(false) + override val isMarking = mutableStateOf(true) override val markedNoteList = mutableStateListOf() override fun markAllNote(notes: List) {} diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt index 1669485..03d39e0 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/SharePreviewPage.kt @@ -45,11 +45,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.digiventure.ventnote.R @@ -179,9 +177,10 @@ fun SharePreviewPage( Spacer(modifier = Modifier.width(8.dp)) Text( text = date, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f), - fontWeight = FontWeight.Medium + style = MaterialTheme.typography.titleSmall.copy( + color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f), + fontWeight = FontWeight.Medium + ), ) } } @@ -191,13 +190,10 @@ fun SharePreviewPage( SelectionContainer { Text( text = title, - style = TextStyle( - fontSize = 16.sp, + style = MaterialTheme.typography.titleLarge.copy( fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface, - lineHeight = 24.sp ), - color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.fillMaxWidth() ) } @@ -215,13 +211,10 @@ fun SharePreviewPage( SelectionContainer { Text( text = text, - style = TextStyle( - fontSize = 14.sp, + style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface, - lineHeight = 20.sp + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.9f), ), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.9f), modifier = Modifier.padding(20.dp) ) } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/AppBar.kt index 1722122..2759fd1 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/AppBar.kt @@ -29,7 +29,7 @@ fun SharePreviewAppBar( Text( text = stringResource(id = R.string.share_preview), color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.headlineSmall.copy( + style = MaterialTheme.typography.titleLarge.copy( fontWeight = FontWeight.SemiBold ), ) diff --git a/app/src/main/java/com/digiventure/ventnote/ui/theme/Type.kt b/app/src/main/java/com/digiventure/ventnote/ui/theme/Type.kt index 70233a4..e014ce5 100644 --- a/app/src/main/java/com/digiventure/ventnote/ui/theme/Type.kt +++ b/app/src/main/java/com/digiventure/ventnote/ui/theme/Type.kt @@ -1,36 +1,10 @@ package com.digiventure.ventnote.ui.theme import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp /** * Defines the typography styles for the application, based on Material Design principles. * This set includes overrides for bodyLarge, titleLarge, and labelSmall text styles, * using the default font family and adjusting font weight, size, line height, and letter spacing. */ -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ), - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) -) \ No newline at end of file +val Typography = Typography() \ No newline at end of file From 632ea06bd909554c4f55b0a0e6c5a65919064c3e Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Wed, 13 Aug 2025 12:15:43 +0700 Subject: [PATCH 22/28] Feat feature/ui-revamp : Adjusting note list page based on QA testing --- .../ventnote/feature/notes/NotesPage.kt | 52 ++++++++++--------- .../components/drawer/NavigationDrawer.kt | 2 +- .../feature/notes/components/item/NoteItem.kt | 5 +- .../feature/notes/components/navbar/AppBar.kt | 4 +- .../notes/components/sheets/FilterSheet.kt | 13 +++-- 5 files changed, 40 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt index 133214e..06a4f88 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt @@ -209,31 +209,33 @@ fun NotesPage( }, snackbarHost = { SnackbarHost(snackBarHostState) }, floatingActionButton = { - ExtendedFloatingActionButton( - onClick = { - viewModel.closeMarkingEvent() - navigationActions.navigateToCreatePage() - }, - modifier = Modifier.semantics { - testTag = TestTags.ADD_NOTE_FAB - }, - text = { - Text( - text = stringResource(R.string.add), - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Medium + if (!isMarking) { + ExtendedFloatingActionButton( + onClick = { + viewModel.closeMarkingEvent() + navigationActions.navigateToCreatePage() + }, + modifier = Modifier.semantics { + testTag = TestTags.ADD_NOTE_FAB + }, + text = { + Text( + text = stringResource(R.string.add), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Medium + ) ) - ) - }, - icon = { - Icon( - imageVector = Icons.Filled.Add, - contentDescription = stringResource(R.string.fab) - ) - }, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) + }, + icon = { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = stringResource(R.string.fab) + ) + }, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + } }, content = { contentPadding -> Box( @@ -322,7 +324,7 @@ fun NotesPage( if (openBottomSheet) { FilterSheet( - openBottomSheet = remember { mutableStateOf(true) }, + openBottomSheet = openBottomSheet, bottomSheetState = bottomSheetState, onDismiss = { openBottomSheet = false }, sortAndOrderData = viewModel.sortAndOrderData.value diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt index 5c37932..7ab25d6 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt @@ -94,7 +94,7 @@ fun NavDrawer( } val appPath = "https://play.google.com/store/apps/details?id=com.digiventure.ventnote" - val devPagePath = "https://play.google.com/store/apps/developer?id=DigiVenture" + val devPagePath = "https://play.google.com/store/apps/developer?id=Mattrmost" val dataStore = NoteDataStore(LocalContext.current) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt index 8b031e7..34d72f5 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt @@ -104,10 +104,13 @@ fun NotesItem( ), modifier = Modifier.padding(bottom = 4.dp) ) + + Spacer(modifier = Modifier.height(8.dp)) + Text( text = DateUtil.convertDateString( "EEEE, MMMM d h:mm a", - data.createdAt.toString() + data.updatedAt.toString() ), maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt index abc43ff..45496cd 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Sort import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircleOutline import androidx.compose.material.icons.filled.Close @@ -22,7 +23,6 @@ import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.RadioButtonUnchecked -import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -385,7 +385,7 @@ fun TrailingMenuIcons( modifier = Modifier.semantics { testTag = TestTags.SORT_ICON_BUTTON } ) { Icon( - imageVector = Icons.Outlined.MoreVert, + imageVector = Icons.AutoMirrored.Outlined.Sort, contentDescription = stringResource(R.string.sort_nav_icon), tint = MaterialTheme.colorScheme.onSurface ) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt index 66fe1dc..af760f3 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt @@ -31,7 +31,6 @@ import androidx.compose.material3.SheetState import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -54,7 +53,7 @@ import com.digiventure.ventnote.components.bottomSheet.RegularBottomSheet @OptIn(ExperimentalMaterial3Api::class) @Composable fun FilterSheet( - openBottomSheet: MutableState, + openBottomSheet: Boolean, bottomSheetState: SheetState, onDismiss: () -> Unit, sortAndOrderData: Pair?, @@ -80,12 +79,12 @@ fun FilterSheet( var selectedOrderBy by remember { mutableStateOf(sortAndOrderData?.second)} RegularBottomSheet( - isOpened = openBottomSheet.value, + isOpened = openBottomSheet, bottomSheetState = bottomSheetState, modifier = Modifier.semantics { testTag = TestTags.BOTTOM_SHEET }, - onDismissRequest = { openBottomSheet.value = false } + onDismissRequest = { onDismiss() } ) { Column( modifier = Modifier @@ -251,10 +250,10 @@ private data class OrderOption( @Preview @Composable fun FilterSheetPreview() { - val openBottomSheet = rememberSaveable { mutableStateOf(true) } - val skipPartiallyExpanded = remember { mutableStateOf(false) } + val openBottomSheet by rememberSaveable { mutableStateOf(true) } + val skipPartiallyExpanded by remember { mutableStateOf(false) } val bottomSheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = skipPartiallyExpanded.value + skipPartiallyExpanded = skipPartiallyExpanded ) Scaffold( From 5faeb7f8bea8f690e995e10544bb13051e8d99e7 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Wed, 13 Aug 2025 12:57:36 +0700 Subject: [PATCH 23/28] Feat feature/ui-revamp : Change primary container color to primary --- .../ventnote/feature/backup/BackupPage.kt | 4 ++-- .../feature/backup/components/list/BackupFileList.kt | 12 ++++++------ .../components/navbar/EnhancedBottomAppBar.kt | 4 ++-- .../components/navbar/EnhancedBottomAppBar.kt | 8 ++++---- .../notes/components/drawer/NavigationDrawer.kt | 2 +- .../feature/notes/components/navbar/AppBar.kt | 2 +- .../components/navbar/EnhancedBottomAppBar.kt | 4 ++-- .../share_preview/components/sheets/ShareSheet.kt | 2 +- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt index 95ae68e..cfc75c5 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt @@ -272,7 +272,7 @@ private fun SignedOutStateContent( .size(100.dp) .padding(bottom = 24.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer + containerColor = MaterialTheme.colorScheme.primary ), shape = CircleShape ) { @@ -284,7 +284,7 @@ private fun SignedOutStateContent( imageVector = Icons.Filled.CloudOff, contentDescription = null, modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer + tint = MaterialTheme.colorScheme.onPrimary ) } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt index 579b8f4..e8bb8f4 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt @@ -261,7 +261,7 @@ fun BackupListContainer( Surface( modifier = Modifier.size(48.dp), shape = CircleShape, - color = MaterialTheme.colorScheme.primaryContainer + color = MaterialTheme.colorScheme.primary ) { Box( contentAlignment = Alignment.Center @@ -269,7 +269,7 @@ fun BackupListContainer( Icon( imageVector = Icons.Filled.CloudQueue, contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, + tint = MaterialTheme.colorScheme.onPrimary, modifier = Modifier.size(24.dp) ) } @@ -296,8 +296,8 @@ fun BackupListContainer( onClick = { onRestoreRequest(file) }, shape = RoundedCornerShape(12.dp), colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary ), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) ) { @@ -347,7 +347,7 @@ fun BackupFailedContainer( .size(100.dp) .padding(bottom = 24.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer + containerColor = MaterialTheme.colorScheme.primary ), shape = CircleShape ) { @@ -359,7 +359,7 @@ fun BackupFailedContainer( imageVector = Icons.Filled.ErrorOutline, contentDescription = null, modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer + tint = MaterialTheme.colorScheme.onPrimary ) } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/EnhancedBottomAppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/EnhancedBottomAppBar.kt index a18e20c..007aa77 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/EnhancedBottomAppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/EnhancedBottomAppBar.kt @@ -54,8 +54,8 @@ fun EnhancedBottomAppBar( icon = Icons.Rounded.Check, label = stringResource(R.string.save), onClick = onSaveClick, - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, isProminent = true ) } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt index 8a6d6c0..9747529 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt @@ -63,8 +63,8 @@ fun EnhancedBottomAppBar( icon = Icons.Rounded.Close, label = stringResource(R.string.cancel), onClick = onCancelClick, - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary ) // Save button in editing mode @@ -72,8 +72,8 @@ fun EnhancedBottomAppBar( icon = Icons.Rounded.Check, label = stringResource(R.string.save), onClick = onSaveClick, - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, isProminent = true ) } else { diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt index 7ab25d6..96f5e32 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt @@ -134,7 +134,7 @@ fun NavDrawer( NavDrawerItem(leftIcon = Icons.Filled.Star, title = stringResource(id = R.string.rate_app), subtitle = stringResource(id = R.string.rate_app_description), - testTagName = "", + testTagName = TestTags.RATE_APP_TILE, onClick = { openPlayStore(appPath) }) NavDrawerItem(leftIcon = Icons.Filled.Shop, diff --git a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt index 45496cd..24e49b9 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/navbar/AppBar.kt @@ -154,7 +154,7 @@ private fun SelectionTitle( } withStyle( style = SpanStyle( - color = MaterialTheme.colorScheme.onPrimaryContainer + color = MaterialTheme.colorScheme.onPrimary ) ) { append(" of $totalNotesCount selected") diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/EnhancedBottomAppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/EnhancedBottomAppBar.kt index cf3dc8d..34333e9 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/EnhancedBottomAppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/EnhancedBottomAppBar.kt @@ -54,8 +54,8 @@ fun EnhancedBottomAppBar( icon = Icons.Rounded.Share, label = stringResource(R.string.share_note), onClick = onCancelClick, - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary ) } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/sheets/ShareSheet.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/sheets/ShareSheet.kt index 3f708d9..4a24a0c 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/sheets/ShareSheet.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/sheets/ShareSheet.kt @@ -84,7 +84,7 @@ private fun ShareOptionItem( ) { Surface( modifier = Modifier.size(40.dp), - color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f), + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), shape = RoundedCornerShape(10.dp) ) { Box(contentAlignment = Alignment.Center) { From d196f830803b5c7471ff4bfa788eaea3802a3640 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Wed, 13 Aug 2025 17:50:10 +0700 Subject: [PATCH 24/28] Feat feature/ui-revamp : Add additional color panel & scheme, return primary to primary container --- .../ventnote/feature/backup/BackupPage.kt | 4 +- .../backup/components/list/BackupFileList.kt | 12 +- .../components/navbar/EnhancedBottomAppBar.kt | 4 +- .../components/navbar/EnhancedBottomAppBar.kt | 12 +- .../components/navbar/EnhancedBottomAppBar.kt | 4 +- .../components/sheets/ShareSheet.kt | 2 +- .../ventnote/ui/ColorSchemeChoice.kt | 226 +++++++++++++++--- .../digiventure/ventnote/ui/theme/Color.kt | 69 +++++- 8 files changed, 275 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt index cfc75c5..95ae68e 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt @@ -272,7 +272,7 @@ private fun SignedOutStateContent( .size(100.dp) .padding(bottom = 24.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primary + containerColor = MaterialTheme.colorScheme.primaryContainer ), shape = CircleShape ) { @@ -284,7 +284,7 @@ private fun SignedOutStateContent( imageVector = Icons.Filled.CloudOff, contentDescription = null, modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onPrimary + tint = MaterialTheme.colorScheme.onPrimaryContainer ) } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt index e8bb8f4..579b8f4 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt @@ -261,7 +261,7 @@ fun BackupListContainer( Surface( modifier = Modifier.size(48.dp), shape = CircleShape, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primaryContainer ) { Box( contentAlignment = Alignment.Center @@ -269,7 +269,7 @@ fun BackupListContainer( Icon( imageVector = Icons.Filled.CloudQueue, contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimary, + tint = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier.size(24.dp) ) } @@ -296,8 +296,8 @@ fun BackupListContainer( onClick = { onRestoreRequest(file) }, shape = RoundedCornerShape(12.dp), colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer ), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) ) { @@ -347,7 +347,7 @@ fun BackupFailedContainer( .size(100.dp) .padding(bottom = 24.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primary + containerColor = MaterialTheme.colorScheme.primaryContainer ), shape = CircleShape ) { @@ -359,7 +359,7 @@ fun BackupFailedContainer( imageVector = Icons.Filled.ErrorOutline, contentDescription = null, modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onPrimary + tint = MaterialTheme.colorScheme.onPrimaryContainer ) } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/EnhancedBottomAppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/EnhancedBottomAppBar.kt index 007aa77..a18e20c 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/EnhancedBottomAppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/navbar/EnhancedBottomAppBar.kt @@ -54,8 +54,8 @@ fun EnhancedBottomAppBar( icon = Icons.Rounded.Check, label = stringResource(R.string.save), onClick = onSaveClick, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, isProminent = true ) } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt index 9747529..e475134 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt @@ -63,8 +63,8 @@ fun EnhancedBottomAppBar( icon = Icons.Rounded.Close, label = stringResource(R.string.cancel), onClick = onCancelClick, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer ) // Save button in editing mode @@ -72,8 +72,8 @@ fun EnhancedBottomAppBar( icon = Icons.Rounded.Check, label = stringResource(R.string.save), onClick = onSaveClick, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, isProminent = true ) } else { @@ -82,8 +82,8 @@ fun EnhancedBottomAppBar( icon = Icons.Rounded.Edit, label = stringResource(R.string.edit), onClick = onEditClick, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary ) // Delete button in view mode diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/EnhancedBottomAppBar.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/EnhancedBottomAppBar.kt index 34333e9..cf3dc8d 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/EnhancedBottomAppBar.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/navbar/EnhancedBottomAppBar.kt @@ -54,8 +54,8 @@ fun EnhancedBottomAppBar( icon = Icons.Rounded.Share, label = stringResource(R.string.share_note), onClick = onCancelClick, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer ) } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/sheets/ShareSheet.kt b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/sheets/ShareSheet.kt index 4a24a0c..3f708d9 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/sheets/ShareSheet.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/sheets/ShareSheet.kt @@ -84,7 +84,7 @@ private fun ShareOptionItem( ) { Surface( modifier = Modifier.size(40.dp), - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f), shape = RoundedCornerShape(10.dp) ) { Box(contentAlignment = Alignment.Center) { diff --git a/app/src/main/java/com/digiventure/ventnote/ui/ColorSchemeChoice.kt b/app/src/main/java/com/digiventure/ventnote/ui/ColorSchemeChoice.kt index bf262dd..4f5001a 100644 --- a/app/src/main/java/com/digiventure/ventnote/ui/ColorSchemeChoice.kt +++ b/app/src/main/java/com/digiventure/ventnote/ui/ColorSchemeChoice.kt @@ -6,27 +6,81 @@ import androidx.compose.material3.lightColorScheme import com.digiventure.ventnote.commons.ColorPalletName import com.digiventure.ventnote.commons.ColorSchemeName import com.digiventure.ventnote.ui.theme.CadmiumGreenDarkPrimary +import com.digiventure.ventnote.ui.theme.CadmiumGreenDarkPrimaryContainer import com.digiventure.ventnote.ui.theme.CadmiumGreenDarkSecondary +import com.digiventure.ventnote.ui.theme.CadmiumGreenDarkSecondaryContainer +import com.digiventure.ventnote.ui.theme.CadmiumGreenDarkTertiary +import com.digiventure.ventnote.ui.theme.CadmiumGreenDarkTertiaryContainer import com.digiventure.ventnote.ui.theme.CadmiumGreenLightPrimary +import com.digiventure.ventnote.ui.theme.CadmiumGreenLightPrimaryContainer import com.digiventure.ventnote.ui.theme.CadmiumGreenLightSecondary +import com.digiventure.ventnote.ui.theme.CadmiumGreenLightSecondaryContainer +import com.digiventure.ventnote.ui.theme.CadmiumGreenLightTertiary +import com.digiventure.ventnote.ui.theme.CadmiumGreenLightTertiaryContainer import com.digiventure.ventnote.ui.theme.CobaltBlueDarkPrimary +import com.digiventure.ventnote.ui.theme.CobaltBlueDarkPrimaryContainer import com.digiventure.ventnote.ui.theme.CobaltBlueDarkSecondary +import com.digiventure.ventnote.ui.theme.CobaltBlueDarkSecondaryContainer +import com.digiventure.ventnote.ui.theme.CobaltBlueDarkTertiary +import com.digiventure.ventnote.ui.theme.CobaltBlueDarkTertiaryContainer import com.digiventure.ventnote.ui.theme.CobaltBlueLightPrimary +import com.digiventure.ventnote.ui.theme.CobaltBlueLightPrimaryContainer import com.digiventure.ventnote.ui.theme.CobaltBlueLightSecondary +import com.digiventure.ventnote.ui.theme.CobaltBlueLightSecondaryContainer +import com.digiventure.ventnote.ui.theme.CobaltBlueLightTertiary +import com.digiventure.ventnote.ui.theme.CobaltBlueLightTertiaryContainer import com.digiventure.ventnote.ui.theme.CrimsonDarkPrimary +import com.digiventure.ventnote.ui.theme.CrimsonDarkPrimaryContainer import com.digiventure.ventnote.ui.theme.CrimsonDarkSecondary +import com.digiventure.ventnote.ui.theme.CrimsonDarkSecondaryContainer +import com.digiventure.ventnote.ui.theme.CrimsonDarkTertiary +import com.digiventure.ventnote.ui.theme.CrimsonDarkTertiaryContainer import com.digiventure.ventnote.ui.theme.CrimsonLightPrimary +import com.digiventure.ventnote.ui.theme.CrimsonLightPrimaryContainer import com.digiventure.ventnote.ui.theme.CrimsonLightSecondary +import com.digiventure.ventnote.ui.theme.CrimsonLightSecondaryContainer +import com.digiventure.ventnote.ui.theme.CrimsonLightTertiary +import com.digiventure.ventnote.ui.theme.CrimsonLightTertiaryContainer import com.digiventure.ventnote.ui.theme.DarkBackground +import com.digiventure.ventnote.ui.theme.DarkOnBackground +import com.digiventure.ventnote.ui.theme.DarkOnPrimary +import com.digiventure.ventnote.ui.theme.DarkOnPrimaryContainer +import com.digiventure.ventnote.ui.theme.DarkOnSecondary +import com.digiventure.ventnote.ui.theme.DarkOnSecondaryContainer import com.digiventure.ventnote.ui.theme.DarkOnSurface -import com.digiventure.ventnote.ui.theme.DarkTertiary +import com.digiventure.ventnote.ui.theme.DarkOnSurfaceVariant +import com.digiventure.ventnote.ui.theme.DarkOnTertiary +import com.digiventure.ventnote.ui.theme.DarkOnTertiaryContainer +import com.digiventure.ventnote.ui.theme.DarkOutline +import com.digiventure.ventnote.ui.theme.DarkOutlineVariant +import com.digiventure.ventnote.ui.theme.DarkSurface +import com.digiventure.ventnote.ui.theme.DarkSurfaceVariant import com.digiventure.ventnote.ui.theme.LightBackground +import com.digiventure.ventnote.ui.theme.LightOnBackground +import com.digiventure.ventnote.ui.theme.LightOnPrimary +import com.digiventure.ventnote.ui.theme.LightOnPrimaryContainer +import com.digiventure.ventnote.ui.theme.LightOnSecondary +import com.digiventure.ventnote.ui.theme.LightOnSecondaryContainer import com.digiventure.ventnote.ui.theme.LightOnSurface -import com.digiventure.ventnote.ui.theme.LightTertiary +import com.digiventure.ventnote.ui.theme.LightOnSurfaceVariant +import com.digiventure.ventnote.ui.theme.LightOnTertiary +import com.digiventure.ventnote.ui.theme.LightOnTertiaryContainer +import com.digiventure.ventnote.ui.theme.LightOutline +import com.digiventure.ventnote.ui.theme.LightOutlineVariant +import com.digiventure.ventnote.ui.theme.LightSurface +import com.digiventure.ventnote.ui.theme.LightSurfaceVariant import com.digiventure.ventnote.ui.theme.PurpleDarkPrimary +import com.digiventure.ventnote.ui.theme.PurpleDarkPrimaryContainer import com.digiventure.ventnote.ui.theme.PurpleDarkSecondary +import com.digiventure.ventnote.ui.theme.PurpleDarkSecondaryContainer +import com.digiventure.ventnote.ui.theme.PurpleDarkTertiary +import com.digiventure.ventnote.ui.theme.PurpleDarkTertiaryContainer import com.digiventure.ventnote.ui.theme.PurpleLightPrimary +import com.digiventure.ventnote.ui.theme.PurpleLightPrimaryContainer import com.digiventure.ventnote.ui.theme.PurpleLightSecondary +import com.digiventure.ventnote.ui.theme.PurpleLightSecondaryContainer +import com.digiventure.ventnote.ui.theme.PurpleLightTertiary +import com.digiventure.ventnote.ui.theme.PurpleLightTertiaryContainer object ColorSchemeChoice { fun getColorScheme(colorScheme: String, colorPallet: String): ColorScheme { @@ -52,81 +106,185 @@ object ColorSchemeChoice { private val DarkPurpleScheme = darkColorScheme( primary = PurpleDarkPrimary, + onPrimary = DarkOnPrimary, + primaryContainer = PurpleDarkPrimaryContainer, + onPrimaryContainer = DarkOnPrimaryContainer, secondary = PurpleDarkSecondary, - tertiary = DarkTertiary, + onSecondary = DarkOnSecondary, + secondaryContainer = PurpleDarkSecondaryContainer, + onSecondaryContainer = DarkOnSecondaryContainer, + tertiary = PurpleDarkTertiary, + onTertiary = DarkOnTertiary, + tertiaryContainer = PurpleDarkTertiaryContainer, + onTertiaryContainer = DarkOnTertiaryContainer, background = DarkBackground, - surface = DarkTertiary, - onPrimary = DarkTertiary, - onSurface = DarkOnSurface + onBackground = DarkOnBackground, + surface = DarkSurface, + onSurface = DarkOnSurface, + surfaceVariant = DarkSurfaceVariant, + onSurfaceVariant = DarkOnSurfaceVariant, + outline = DarkOutline, + outlineVariant = DarkOutlineVariant ) private val LightPurpleScheme = lightColorScheme( primary = PurpleLightPrimary, + onPrimary = LightOnPrimary, + primaryContainer = PurpleLightPrimaryContainer, + onPrimaryContainer = LightOnPrimaryContainer, secondary = PurpleLightSecondary, - tertiary = LightTertiary, + onSecondary = LightOnSecondary, + secondaryContainer = PurpleLightSecondaryContainer, + onSecondaryContainer = LightOnSecondaryContainer, + tertiary = PurpleLightTertiary, + onTertiary = LightOnTertiary, + tertiaryContainer = PurpleLightTertiaryContainer, + onTertiaryContainer = LightOnTertiaryContainer, background = LightBackground, - surface = LightTertiary, - onPrimary = LightTertiary, - onSurface = LightOnSurface + onBackground = LightOnBackground, + surface = LightSurface, + onSurface = LightOnSurface, + surfaceVariant = LightSurfaceVariant, + onSurfaceVariant = LightOnSurfaceVariant, + outline = LightOutline, + outlineVariant = LightOutlineVariant ) private val DarkCrimsonScheme = darkColorScheme( primary = CrimsonDarkPrimary, + onPrimary = DarkOnPrimary, + primaryContainer = CrimsonDarkPrimaryContainer, + onPrimaryContainer = DarkOnPrimaryContainer, secondary = CrimsonDarkSecondary, - tertiary = DarkTertiary, + onSecondary = DarkOnSecondary, + secondaryContainer = CrimsonDarkSecondaryContainer, + onSecondaryContainer = DarkOnSecondaryContainer, + tertiary = CrimsonDarkTertiary, + onTertiary = DarkOnTertiary, + tertiaryContainer = CrimsonDarkTertiaryContainer, + onTertiaryContainer = DarkOnTertiaryContainer, background = DarkBackground, - surface = DarkTertiary, - onPrimary = DarkTertiary, - onSurface = DarkOnSurface + onBackground = DarkOnBackground, + surface = DarkSurface, + onSurface = DarkOnSurface, + surfaceVariant = DarkSurfaceVariant, + onSurfaceVariant = DarkOnSurfaceVariant, + outline = DarkOutline, + outlineVariant = DarkOutlineVariant ) private val LightCrimsonScheme = lightColorScheme( primary = CrimsonLightPrimary, + onPrimary = LightOnPrimary, + primaryContainer = CrimsonLightPrimaryContainer, + onPrimaryContainer = LightOnPrimaryContainer, secondary = CrimsonLightSecondary, - tertiary = LightTertiary, + onSecondary = LightOnSecondary, + secondaryContainer = CrimsonLightSecondaryContainer, + onSecondaryContainer = LightOnSecondaryContainer, + tertiary = CrimsonLightTertiary, + onTertiary = LightOnTertiary, + tertiaryContainer = CrimsonLightTertiaryContainer, + onTertiaryContainer = LightOnTertiaryContainer, background = LightBackground, - surface = LightTertiary, - onPrimary = LightTertiary, - onSurface = LightOnSurface + onBackground = LightOnBackground, + surface = LightSurface, + onSurface = LightOnSurface, + surfaceVariant = LightSurfaceVariant, + onSurfaceVariant = LightOnSurfaceVariant, + outline = LightOutline, + outlineVariant = LightOutlineVariant ) private val DarkCadmiumGreenScheme = darkColorScheme( primary = CadmiumGreenDarkPrimary, + onPrimary = DarkOnPrimary, + primaryContainer = CadmiumGreenDarkPrimaryContainer, + onPrimaryContainer = DarkOnPrimaryContainer, secondary = CadmiumGreenDarkSecondary, - tertiary = DarkTertiary, + onSecondary = DarkOnSecondary, + secondaryContainer = CadmiumGreenDarkSecondaryContainer, + onSecondaryContainer = DarkOnSecondaryContainer, + tertiary = CadmiumGreenDarkTertiary, + onTertiary = DarkOnTertiary, + tertiaryContainer = CadmiumGreenDarkTertiaryContainer, + onTertiaryContainer = DarkOnTertiaryContainer, background = DarkBackground, - surface = DarkTertiary, - onPrimary = DarkTertiary, - onSurface = DarkOnSurface + onBackground = DarkOnBackground, + surface = DarkSurface, + onSurface = DarkOnSurface, + surfaceVariant = DarkSurfaceVariant, + onSurfaceVariant = DarkOnSurfaceVariant, + outline = DarkOutline, + outlineVariant = DarkOutlineVariant ) private val LightCadmiumGreenScheme = lightColorScheme( primary = CadmiumGreenLightPrimary, + onPrimary = LightOnPrimary, + primaryContainer = CadmiumGreenLightPrimaryContainer, + onPrimaryContainer = LightOnPrimaryContainer, secondary = CadmiumGreenLightSecondary, - tertiary = LightTertiary, + onSecondary = LightOnSecondary, + secondaryContainer = CadmiumGreenLightSecondaryContainer, + onSecondaryContainer = LightOnSecondaryContainer, + tertiary = CadmiumGreenLightTertiary, + onTertiary = LightOnTertiary, + tertiaryContainer = CadmiumGreenLightTertiaryContainer, + onTertiaryContainer = LightOnTertiaryContainer, background = LightBackground, - surface = LightTertiary, - onPrimary = LightTertiary, - onSurface = LightOnSurface + onBackground = LightOnBackground, + surface = LightSurface, + onSurface = LightOnSurface, + surfaceVariant = LightSurfaceVariant, + onSurfaceVariant = LightOnSurfaceVariant, + outline = LightOutline, + outlineVariant = LightOutlineVariant ) private val DarkCobaltBlueScheme = darkColorScheme( primary = CobaltBlueDarkPrimary, + onPrimary = DarkOnPrimary, + primaryContainer = CobaltBlueDarkPrimaryContainer, + onPrimaryContainer = DarkOnPrimaryContainer, secondary = CobaltBlueDarkSecondary, - tertiary = DarkTertiary, + onSecondary = DarkOnSecondary, + secondaryContainer = CobaltBlueDarkSecondaryContainer, + onSecondaryContainer = DarkOnSecondaryContainer, + tertiary = CobaltBlueDarkTertiary, + onTertiary = DarkOnTertiary, + tertiaryContainer = CobaltBlueDarkTertiaryContainer, + onTertiaryContainer = DarkOnTertiaryContainer, background = DarkBackground, - surface = DarkTertiary, - onPrimary = DarkTertiary, - onSurface = DarkOnSurface + onBackground = DarkOnBackground, + surface = DarkSurface, + onSurface = DarkOnSurface, + surfaceVariant = DarkSurfaceVariant, + onSurfaceVariant = DarkOnSurfaceVariant, + outline = DarkOutline, + outlineVariant = DarkOutlineVariant ) private val LightCobaltBlueScheme = lightColorScheme( primary = CobaltBlueLightPrimary, + onPrimary = LightOnPrimary, + primaryContainer = CobaltBlueLightPrimaryContainer, + onPrimaryContainer = LightOnPrimaryContainer, secondary = CobaltBlueLightSecondary, - tertiary = LightTertiary, + onSecondary = LightOnSecondary, + secondaryContainer = CobaltBlueLightSecondaryContainer, + onSecondaryContainer = LightOnSecondaryContainer, + tertiary = CobaltBlueLightTertiary, + onTertiary = LightOnTertiary, + tertiaryContainer = CobaltBlueLightTertiaryContainer, + onTertiaryContainer = LightOnTertiaryContainer, background = LightBackground, - surface = LightTertiary, - onPrimary = LightTertiary, - onSurface = LightOnSurface + onBackground = LightOnBackground, + surface = LightSurface, + onSurface = LightOnSurface, + surfaceVariant = LightSurfaceVariant, + onSurfaceVariant = LightOnSurfaceVariant, + outline = LightOutline, + outlineVariant = LightOutlineVariant ) } \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/ui/theme/Color.kt b/app/src/main/java/com/digiventure/ventnote/ui/theme/Color.kt index e167af0..caf9abe 100644 --- a/app/src/main/java/com/digiventure/ventnote/ui/theme/Color.kt +++ b/app/src/main/java/com/digiventure/ventnote/ui/theme/Color.kt @@ -4,32 +4,91 @@ import androidx.compose.ui.graphics.Color val PurpleDarkPrimary = Color(0xFFD0BCFF) val PurpleDarkSecondary = Color(0xFFCCC2DC) +val PurpleDarkPrimaryContainer = Color(0xFF4f378b) +val PurpleDarkTertiary = Color(0xFFefb8c8) +val PurpleDarkSecondaryContainer = Color(0xFF4a4458) +val PurpleDarkTertiaryContainer = Color(0xFF633b48) val PurpleLightPrimary = Color(0xFF6650a4) val PurpleLightSecondary = Color(0xFF625b71) +val PurpleLightPrimaryContainer = Color(0xFFeaddff) +val PurpleLightTertiary = Color(0xFF7d5260) +val PurpleLightSecondaryContainer = Color(0xFFe8def8) +val PurpleLightTertiaryContainer = Color(0xFFffd8e4) val CrimsonDarkPrimary = Color(0xFFffb3b3) val CrimsonDarkSecondary = Color(0xFFe6bdbc) +val CrimsonDarkPrimaryContainer = Color(0xFF8c1538) +val CrimsonDarkTertiary = Color(0xFFf2b8b5) +val CrimsonDarkSecondaryContainer = Color(0xFF5d3f3e) +val CrimsonDarkTertiaryContainer = Color(0xFF4e2e2d) val CrimsonLightPrimary = Color(0xFFbf0030) val CrimsonLightSecondary = Color(0xFF775656) - -val CadmiumGreenLightPrimary = Color(0xFF006b5c) -val CadmiumGreenLightSecondary = Color(0xFF4a635d) +val CrimsonLightPrimaryContainer = Color(0xFFffdad6) +val CrimsonLightTertiary = Color(0xFF8c4a47) +val CrimsonLightSecondaryContainer = Color(0xFFffdad6) +val CrimsonLightTertiaryContainer = Color(0xFFffdad6) val CadmiumGreenDarkPrimary = Color(0xFF58dbc3) val CadmiumGreenDarkSecondary = Color(0xFFb1ccc4) +val CadmiumGreenDarkPrimaryContainer = Color(0xFF00513f) +val CadmiumGreenDarkTertiary = Color(0xFF7dd3c0) +val CadmiumGreenDarkSecondaryContainer = Color(0xFF324b47) +val CadmiumGreenDarkTertiaryContainer = Color(0xFF1e4e42) + +val CadmiumGreenLightPrimary = Color(0xFF006b5c) +val CadmiumGreenLightSecondary = Color(0xFF4a635d) +val CadmiumGreenLightPrimaryContainer = Color(0xFF7ff2db) +val CadmiumGreenLightTertiary = Color(0xFF266d5a) +val CadmiumGreenLightSecondaryContainer = Color(0xFFcce8e0) +val CadmiumGreenLightTertiaryContainer = Color(0xFF9cf4e1) val CobaltBlueLightPrimary = Color(0xFF2559bd) val CobaltBlueLightSecondary = Color(0xFF585e71) +val CobaltBlueLightPrimaryContainer = Color(0xFFdae2ff) +val CobaltBlueLightTertiary = Color(0xFF5e5d72) +val CobaltBlueLightSecondaryContainer = Color(0xFFdce2f9) +val CobaltBlueLightTertiaryContainer = Color(0xFFdde1f9) val CobaltBlueDarkPrimary = Color(0xFFb1c5ff) val CobaltBlueDarkSecondary = Color(0xFFc0c6dc) +val CobaltBlueDarkPrimaryContainer = Color(0xFF0041a3) +val CobaltBlueDarkTertiary = Color(0xFFd0c2ff) +val CobaltBlueDarkSecondaryContainer = Color(0xFF404659) +val CobaltBlueDarkTertiaryContainer = Color(0xFF2f2f5c) val DarkTertiary = Color(0xFF202125) val DarkBackground = Color(0xFF131416) val DarkOnSurface = Color(0XFFb4b8ba) - val LightTertiary = Color.White val LightBackground = Color(0xFFf2f5fa) -val LightOnSurface = Color(0xFF484b51) \ No newline at end of file +val LightOnSurface = Color(0xFF484b51) + +// Universal Dark Theme Colors +val DarkOnPrimary = Color(0xFF1a0e33) +val DarkOnSecondary = Color(0xFF1d1929) +val DarkOnTertiary = Color(0xFF1a1618) +val DarkOnPrimaryContainer = Color(0xFFe6ddff) +val DarkOnSecondaryContainer = Color(0xFFe8def8) +val DarkOnTertiaryContainer = Color(0xFFffd8e4) +val DarkOnBackground = Color(0xFFe6e1e5) +val DarkSurface = Color(0xFF1c1b1f) +val DarkSurfaceVariant = Color(0xFF49454f) +val DarkOnSurfaceVariant = Color(0xFFcac4d0) +val DarkOutline = Color(0xFF938f99) +val DarkOutlineVariant = Color(0xFF49454f) + +// Universal Light Theme Colors +val LightOnPrimary = Color.White +val LightOnSecondary = Color.White +val LightOnTertiary = Color.White +val LightOnPrimaryContainer = Color(0xFF21005d) +val LightOnSecondaryContainer = Color(0xFF1d192b) +val LightOnTertiaryContainer = Color(0xFF31111d) +val LightOnBackground = Color(0xFF1c1b1f) +val LightSurface = Color(0xFFfffbfe) +val LightSurfaceVariant = Color(0xFFe7e0ec) +val LightOnSurfaceVariant = Color(0xFF49454f) +val LightOutline = Color(0xFF79747e) +val LightOutlineVariant = Color(0xFFcac4d0) \ No newline at end of file From 3098a0cc57e8a0c98ab199ede72b20e26596e26a Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sat, 16 Aug 2025 20:31:42 +0700 Subject: [PATCH 25/28] Feat feature/ui-revamp : Revamp backup page ui --- .../ventnote/feature/backup/BackupPage.kt | 2 +- .../backup/components/list/BackupFileList.kt | 213 +++++++++++------- .../backup/viewmodel/BackupPageBaseVM.kt | 2 + .../backup/viewmodel/BackupPageMockVM.kt | 8 +- .../feature/backup/viewmodel/BackupPageVM.kt | 10 + 5 files changed, 153 insertions(+), 82 deletions(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt index 95ae68e..1845606 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt @@ -124,7 +124,7 @@ fun BackupPage( scrollBehavior = rememberedScrollBehavior, onLogoutRequest = { authViewModel.signOut(onCompleteSignOutCallback = { - backupPageVM.getBackupFileList() + backupPageVM.clearBackupFileList() }) } ) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt index 579b8f4..d0dce24 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt @@ -126,10 +126,10 @@ fun BackupFileList(backupPageVM: BackupPageBaseVM, } when (backupPageUiState.listOfBackupFileState) { - BackupPageVM.FileBackupListState.FileBackupListFinished -> { + is BackupPageVM.FileBackupListState.FileBackupListFinished -> { driveBackupFileListState.value.let { backupFiles -> if (backupFiles.isNullOrEmpty()) { - EmptyBackupListContainer() + EmptyBackupListContainer(onBackupRequest) } else { BackupListContainer(backupFiles, onRestoreRequest, onDeleteRequest, onBackupRequest) @@ -160,33 +160,88 @@ fun BackupFileList(backupPageVM: BackupPageBaseVM, } @Composable -fun EmptyBackupListContainer() { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center +fun EmptyBackupListContainer( + onBackupRequest: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) + Card( + modifier = Modifier + .size(100.dp) + .padding(bottom = 24.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + shape = CircleShape ) { - Icon( - imageVector = Icons.Filled.CloudOff, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = stringResource(R.string.no_backup_found), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center - ) - Text( - text = stringResource(R.string.create_your_first_backup), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.CloudOff, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + Text( + text = stringResource(R.string.no_backup_found), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.create_your_first_backup), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Button( + onClick = { + onBackupRequest() + }, + modifier = Modifier.height(56.dp), + shape = RoundedCornerShape(16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 4.dp, + pressedElevation = 8.dp + ), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.CloudUpload, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(id = R.string.backup_notes), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium + ) + } } } } @@ -334,66 +389,66 @@ fun BackupListContainer( fun BackupFailedContainer( onGetBackupList: () -> Unit ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) + Card( + modifier = Modifier + .size(100.dp) + .padding(bottom = 24.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + shape = CircleShape ) { - Card( - modifier = Modifier - .size(100.dp) - .padding(bottom = 24.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ), - shape = CircleShape - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Filled.ErrorOutline, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - } - - Text( - text = stringResource(R.string.failed_to_load_backups), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center - ) - - Button( - onClick = { - onGetBackupList() - }, - shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center ) { Icon( - imageVector = Icons.Filled.Refresh, + imageVector = Icons.Filled.ErrorOutline, contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(id = R.string.refresh), - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Medium, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer ) } } + + Text( + text = stringResource(R.string.failed_to_load_backups), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Button( + onClick = { + onGetBackupList() + }, + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(id = R.string.refresh), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, + ) + } } } diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageBaseVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageBaseVM.kt index 0b26086..81587ad 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageBaseVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageBaseVM.kt @@ -16,4 +16,6 @@ interface BackupPageBaseVM { fun getBackupFileList() fun deleteDatabase(fileId: String) + + fun clearBackupFileList() } \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt index c17258e..04c10e8 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt @@ -27,8 +27,8 @@ class BackupPageMockVM: ViewModel(), BackupPageBaseVM { init { _uiState.value = _uiState.value.copy( -// listOfBackupFileState = BackupPageVM.FileBackupListState.FileBackupListFailed("error") - listOfBackupFileState = BackupPageVM.FileBackupListState.FileBackupListFinished + listOfBackupFileState = BackupPageVM.FileBackupListState.FileBackupListFailed("error") +// listOfBackupFileState = BackupPageVM.FileBackupListState.FileBackupListFinished ) } @@ -47,4 +47,8 @@ class BackupPageMockVM: ViewModel(), BackupPageBaseVM { override fun deleteDatabase(fileId: String) { } + + override fun clearBackupFileList() { + + } } \ No newline at end of file diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageVM.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageVM.kt index 5a5bc4a..e035196 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageVM.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageVM.kt @@ -126,6 +126,16 @@ class BackupPageVM @Inject constructor( } } + override fun clearBackupFileList() { + _driveBackupFileList.value = emptyList() + _uiState.value = _uiState.value.copy( + listOfBackupFileState = FileBackupListState.FileBackupListFinished, + fileBackupState = FileBackupState.SyncInitial, + fileRestoreState = FileRestoreState.SyncInitial, + fileDeleteState = FileDeleteState.SyncInitial + ) + } + private fun getDriveInstance(): Drive? { return GoogleSignIn.getLastSignedInAccount(app.applicationContext)?.run { DriveAPI.getInstance(app.applicationContext, this) From 96e50c26d3f002f459f1d477de3999aa9f8c63f4 Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 17 Aug 2025 01:44:37 +0700 Subject: [PATCH 26/28] Feat feature/ui-revamp : Separate launched effect for restore state & delete state for backup page --- .../ventnote/feature/backup/BackupPage.kt | 3 ++ .../backup/components/list/BackupFileList.kt | 41 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt index 1845606..aea0f91 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt @@ -1,5 +1,6 @@ package com.digiventure.ventnote.feature.backup +import android.util.Log import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -166,6 +167,7 @@ fun BackupPage( deleteConfirmationDialogState.value = true }, successfullyRestoredRequest = { + Log.e("hehe event", "restored") scope.launch { snackBarHostState.showSnackbar( message = restoredMessage, @@ -174,6 +176,7 @@ fun BackupPage( } }, successfullyDeletedRequest = { + Log.e("hehe event", "deleted") scope.launch { snackBarHostState.showSnackbar( message = deletedMessage, diff --git a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt index d0dce24..16d2d11 100644 --- a/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt +++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt @@ -88,36 +88,41 @@ fun BackupFileList(backupPageVM: BackupPageBaseVM, } } - LaunchedEffect( - key1 = backupPageVM.uiState.value.fileRestoreState, - key2 = backupPageVM.uiState.value.fileDeleteState - ) { - when { - fileRestoreState is BackupPageVM.FileRestoreState.SyncFailed || - fileDeleteState is BackupPageVM.FileDeleteState.SyncFailed -> { + LaunchedEffect(key1 = backupPageVM.uiState.value.fileRestoreState) { + when (fileRestoreState) { + is BackupPageVM.FileRestoreState.SyncFailed -> { restoreLoadingDialogState.value = false - val errorMessage = when { - fileRestoreState is BackupPageVM.FileRestoreState.SyncFailed -> - "Restore notes process failed : ${fileRestoreState.errorMessage}" - fileDeleteState is BackupPageVM.FileDeleteState.SyncFailed -> - "Delete notes process failed : ${fileDeleteState.errorMessage}" - else -> EMPTY_STRING - } + val errorMessage = "Restore notes process failed : ${fileRestoreState.errorMessage}" Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show() } - fileRestoreState is BackupPageVM.FileRestoreState.SyncFinished-> { + is BackupPageVM.FileRestoreState.SyncFinished -> { restoreLoadingDialogState.value = false successfullyRestoredRequest() } - fileDeleteState is BackupPageVM.FileDeleteState.SyncFinished -> { + is BackupPageVM.FileRestoreState.SyncStarted -> { + restoreLoadingDialogState.value = true + } + + else -> {} + } + } + + LaunchedEffect(key1 = backupPageVM.uiState.value.fileDeleteState) { + when (fileDeleteState) { + is BackupPageVM.FileDeleteState.SyncFailed -> { + restoreLoadingDialogState.value = false + val errorMessage = "Delete notes process failed : ${fileDeleteState.errorMessage}" + Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show() + } + + is BackupPageVM.FileDeleteState.SyncFinished -> { restoreLoadingDialogState.value = false successfullyDeletedRequest() } - fileRestoreState is BackupPageVM.FileRestoreState.SyncStarted || - fileDeleteState is BackupPageVM.FileDeleteState.SyncStarted -> { + is BackupPageVM.FileDeleteState.SyncStarted -> { restoreLoadingDialogState.value = true } From fc24e3ccc527baff7c1affe860523a5689c5730a Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 17 Aug 2025 09:16:30 +0700 Subject: [PATCH 27/28] Feat chore/bump-dependencies : Update several dependencies version --- app/build.gradle | 64 +++++++++++++----------- build.gradle | 16 +++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 94056ec..471a7a1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ plugins { id 'com.google.dagger.hilt.android' id 'kotlin-kapt' id 'kotlin-parcelize' - id 'org.jetbrains.kotlin.plugin.compose' version '2.1.0' + id 'org.jetbrains.kotlin.plugin.compose' version '2.2.10' // disabled for internal purpose (if you want to enable, you must create firebase project first) // id 'com.google.gms.google-services' @@ -14,12 +14,12 @@ plugins { android { namespace 'com.digiventure.ventnote' - compileSdk 35 + compileSdk 36 defaultConfig { applicationId "com.digiventure.ventnote" - minSdk 21 - targetSdk 35 + minSdk 23 + targetSdk 36 versionCode 41 versionName "1.0.8" @@ -65,35 +65,41 @@ android { composeOptions { kotlinCompilerExtensionVersion '1.5.15' } - - packagingOptions { + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + packaging { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' excludes += 'META-INF/*' } } - lintOptions { + lint { abortOnError false } } + dependencies { - implementation "androidx.core:core-ktx:1.16.0" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.7" + implementation "androidx.core:core-ktx:1.17.0" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.9.2" implementation "androidx.activity:activity-compose:1.10.1" // Jetpack Compose - implementation "androidx.compose.runtime:runtime-livedata:1.7.8" - implementation "androidx.compose.ui:ui:1.7.8" - implementation "androidx.compose.ui:ui-tooling-preview:1.7.8" + implementation "androidx.compose.runtime:runtime-livedata:1.9.0" + implementation "androidx.compose.ui:ui:1.9.0" + implementation "androidx.compose.ui:ui-tooling-preview:1.9.0" implementation "androidx.compose.material3:material3:1.3.2" // Lifecycle Livedata - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.2" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.9.2" implementation "androidx.compose.compiler:compiler:1.5.15" implementation 'androidx.test.ext:junit-ktx:1.3.0' @@ -105,17 +111,17 @@ dependencies { androidTestImplementation "androidx.room:room-testing:$room_version" // Datastore - implementation("androidx.datastore:datastore-preferences:1.1.4") + implementation("androidx.datastore:datastore-preferences:1.1.7") // Compose Navigation - implementation "androidx.navigation:navigation-compose:2.8.9" + implementation "androidx.navigation:navigation-compose:2.9.3" // Coroutines - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2" // Unit test testImplementation "junit:junit:4.13.2" - androidTestImplementation "androidx.test.ext:junit:1.2.1" + androidTestImplementation "androidx.test.ext:junit:1.3.0" // Hilt def dagger_version = "2.56.2" @@ -132,10 +138,10 @@ dependencies { implementation("com.google.android.play:app-update-ktx:2.1.0") // Google Play API - implementation "com.google.android.gms:play-services-auth:21.3.0" + implementation "com.google.android.gms:play-services-auth:21.4.0" // Accompanist - Status Bar - implementation "com.google.accompanist:accompanist-systemuicontroller:0.34.0" + implementation "com.google.accompanist:accompanist-systemuicontroller:0.36.0" // Instrumented test /// Espresso (for ui interaction purpose) @@ -144,16 +150,16 @@ dependencies { androidTestImplementation "androidx.test.espresso:espresso-intents:3.7.0" androidTestImplementation "androidx.test.espresso:espresso-core:3.7.0" - androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.8.3" - debugImplementation "androidx.compose.ui:ui-tooling:1.8.3" - debugImplementation "androidx.compose.ui:ui-test-manifest:1.8.3" + androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.9.0" + debugImplementation "androidx.compose.ui:ui-tooling:1.9.0" + debugImplementation "androidx.compose.ui:ui-test-manifest:1.9.0" /// Hilt test (for handling service locator when test) androidTestImplementation "com.google.dagger:hilt-android-testing:$dagger_version" kaptAndroidTest "com.google.dagger:hilt-android-compiler:$dagger_version" // For mocking purposes & make it visible in instrumented test - testImplementation "org.mockito.kotlin:mockito-kotlin:5.2.1" + testImplementation "org.mockito.kotlin:mockito-kotlin:6.0.0" testImplementation "org.mockito:mockito-inline:5.2.0" androidTestImplementation "androidx.test.ext:junit-ktx:1.3.0" @@ -164,20 +170,20 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2" // Import the Firebase BoM - implementation platform("com.google.firebase:firebase-bom:33.12.0") + implementation platform("com.google.firebase:firebase-bom:34.1.0") // When using the BoM, you don't specify versions in Firebase library dependencies // Add the dependency for the Firebase SDK for Google Analytics implementation "com.google.firebase:firebase-analytics" - implementation "com.google.firebase:firebase-crashlytics:19.4.2" + implementation "com.google.firebase:firebase-crashlytics:20.0.0" implementation "com.google.firebase:firebase-perf-ktx:21.0.5" //Google sign in - implementation "com.google.android.gms:play-services-auth:21.3.0" + implementation "com.google.android.gms:play-services-auth:21.4.0" //Google Drive API - implementation "com.google.http-client:google-http-client-gson:1.44.2" + implementation "com.google.http-client:google-http-client-gson:2.0.0" implementation "com.google.apis:google-api-services-drive:v3-rev136-1.25.0" - implementation "com.google.api-client:google-api-client-android:1.34.0" + implementation "com.google.api-client:google-api-client-android:2.8.1" } kapt { diff --git a/build.gradle b/build.gradle index d666ba2..eeb2e61 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,17 @@ buildscript { dependencies { - classpath 'com.android.tools.build:gradle:8.9.1' + classpath 'com.android.tools.build:gradle:8.12.0' // disable for staging purpose -// classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.2' -// classpath 'com.google.gms:google-services:4.4.2' -// classpath 'com.google.firebase:perf-plugin:1.4.2' +// classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.6' +// classpath 'com.google.gms:google-services:4.4.3' +// classpath 'com.google.firebase:perf-plugin:2.0.1' } } // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.4.2' apply false - id 'com.android.library' version '7.4.2' apply false - id 'org.jetbrains.kotlin.android' version '2.1.0' apply false - id 'com.google.dagger.hilt.android' version '2.56.1' apply false + id 'com.android.application' version '8.12.0' apply false + id 'com.android.library' version '8.12.0' apply false + id 'org.jetbrains.kotlin.android' version '2.2.10' apply false + id 'com.google.dagger.hilt.android' version '2.56.2' apply false } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ff849b5..7e2b4bc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Apr 16 23:39:55 WIB 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From ac1bc0d4a9cb19ad5d21038c72b4512791e448ef Mon Sep 17 00:00:00 2001 From: Syubban Fakhriya Date: Sun, 17 Aug 2025 09:52:14 +0700 Subject: [PATCH 28/28] Chore staging : Update app version --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 471a7a1..a57ae88 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId "com.digiventure.ventnote" minSdk 23 targetSdk 36 - versionCode 41 - versionName "1.0.8" + versionCode 42 + versionName "1.1.0" testInstrumentationRunner "com.digiventure.utils.CustomTestRunner" vectorDrawables {