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/build.gradle b/app/build.gradle
index e13d3cb..a57ae88 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.2.10'
// disabled for internal purpose (if you want to enable, you must create firebase project first)
// id 'com.google.gms.google-services'
@@ -13,14 +14,14 @@ plugins {
android {
namespace 'com.digiventure.ventnote'
- compileSdk 34
+ compileSdk 36
defaultConfig {
applicationId "com.digiventure.ventnote"
- minSdk 21
- targetSdk 34
- versionCode 41
- versionName "1.0.8"
+ minSdk 23
+ targetSdk 36
+ versionCode 42
+ versionName "1.1.0"
testInstrumentationRunner "com.digiventure.utils.CustomTestRunner"
vectorDrawables {
@@ -62,64 +63,74 @@ android {
}
composeOptions {
- kotlinCompilerExtensionVersion '1.4.0'
+ 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.13.1"
- implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.6"
+ implementation "androidx.core:core-ktx:1.17.0"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.9.2"
- 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.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.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.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'
// 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.7")
// Compose Navigation
- implementation "androidx.navigation:navigation-compose:2.8.3"
+ 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
- implementation "com.google.dagger:hilt-android:2.50"
- kapt "com.google.dagger:hilt-android-compiler:2.50"
+ 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"
// 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,52 +138,52 @@ 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.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)
- 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.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.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: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"
+ testImplementation "org.mockito.kotlin:mockito-kotlin:6.0.0"
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.4.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.2.0"
- implementation "com.google.firebase:firebase-perf-ktx:21.0.1"
+ 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.2.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/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
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/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/commons/TestTags.kt b/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt
index ab73b12..5f16d25 100644
--- a/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt
+++ b/app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt
@@ -9,15 +9,15 @@ 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"
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"
@@ -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/bottomSheet/RegularBottomSheet.kt b/app/src/main/java/com/digiventure/ventnote/components/bottomSheet/RegularBottomSheet.kt
index f53257c..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
@@ -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 ?: Modifier,
containerColor = MaterialTheme.colorScheme.background,
) {
content()
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 4363fc8..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
@@ -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
@@ -12,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
@@ -28,19 +30,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 = {
@@ -52,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,
+ )
)
}
}
@@ -66,12 +73,14 @@ fun TextDialog(
) {
Text(
text = stringResource(R.string.dismiss),
- fontSize = 16.sp,
- fontWeight = FontWeight.SemiBold
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.SemiBold,
+ )
)
}
},
- shape = RoundedCornerShape(8.dp),
+ containerColor = MaterialTheme.colorScheme.surface,
+ shape = RoundedCornerShape(16.dp),
modifier = modifier
)
}
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/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/BackupPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/backup/BackupPage.kt
index 87e7feb..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,16 +1,28 @@
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
+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 +36,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 +47,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 +76,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 {
@@ -104,14 +121,11 @@ fun BackupPage(
topBar = {
BackupPageAppBar(
authVM = authViewModel,
- onBackPressed = { navHostController.popBackStack() },
+ onBackRequest = { navHostController.popBackStack() },
scrollBehavior = rememberedScrollBehavior,
- onBackupPressed = {
- backupDatabase()
- },
- onLogoutPressed = {
+ onLogoutRequest = {
authViewModel.signOut(onCompleteSignOutCallback = {
- backupPageVM.getBackupFileList()
+ backupPageVM.clearBackupFileList()
})
}
)
@@ -119,7 +133,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,30 +142,50 @@ 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 = {
+ onRestoreRequest = {
restoreDataIdState.value = it.id
restoreConfirmationDialogState.value = true
},
- successfullyRestoredCallback = {
+ onDeleteRequest = {
+ deleteDataIdState.value = it.id
+ deleteConfirmationDialogState.value = true
+ },
+ successfullyRestoredRequest = {
+ Log.e("hehe event", "restored")
scope.launch {
snackBarHostState.showSnackbar(
message = restoredMessage,
withDismissAction = true
)
}
+ },
+ successfullyDeletedRequest = {
+ Log.e("hehe event", "deleted")
+ scope.launch {
+ snackBarHostState.showSnackbar(
+ message = deletedMessage,
+ withDismissAction = true
+ )
+ }
+ },
+ onBackupRequest = {
+ backupDatabase()
}
)
}
@@ -160,25 +195,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
+ }
+ }
+ )
+ }
+
+ if (deleteConfirmationDialogState.value) {
+ TextDialog(
+ isOpened = deleteConfirmationDialogState.value,
+ onDismissCallback = { deleteConfirmationDialogState.value = false },
+ onConfirmCallback = {
+ val selectedId = deleteDataIdState.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 = stringResource(R.string.loading),
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+}
- 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
+@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 = stringResource(R.string.sign_in_to_access_backup),
+ 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.connect_with_google_to_backup),
+ style = MaterialTheme.typography.bodyLarge,
+ 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..bba714c
--- /dev/null
+++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/button/SignInButton.kt
@@ -0,0 +1,121 @@
+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.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 authenticationFailedText = stringResource(R.string.authentication_failed)
+ val launcher = rememberLauncherForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ isLoading = false
+ if (result.resultCode == Activity.RESULT_OK) {
+ authViewModel.checkAuthState()
+ signInSuccessCallback()
+ } else {
+ Toast.makeText(context, authenticationFailedText, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ Button(
+ onClick = {
+ isLoading = true
+ launcher.launch(authViewModel.getSignInIntent())
+ },
+ 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
+ ),
+ 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 = stringResource(R.string.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,
+ 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..16d2d11
--- /dev/null
+++ b/app/src/main/java/com/digiventure/ventnote/feature/backup/components/list/BackupFileList.kt
@@ -0,0 +1,482 @@
+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.height
+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.CloudUpload
+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.Constants.EMPTY_STRING
+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,
+ successfullyRestoredRequest: () -> Unit,
+ successfullyDeletedRequest: () -> Unit,
+ onRestoreRequest: (File) -> Unit,
+ onDeleteRequest: (File) -> Unit,
+ onBackupRequest: () -> 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) {
+ when (fileRestoreState) {
+ is BackupPageVM.FileRestoreState.SyncFailed -> {
+ restoreLoadingDialogState.value = false
+ val errorMessage = "Restore notes process failed : ${fileRestoreState.errorMessage}"
+ Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show()
+ }
+
+ is BackupPageVM.FileRestoreState.SyncFinished -> {
+ restoreLoadingDialogState.value = false
+ successfullyRestoredRequest()
+ }
+
+ 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()
+ }
+
+ is BackupPageVM.FileDeleteState.SyncStarted -> {
+ restoreLoadingDialogState.value = true
+ }
+
+ else -> {}
+ }
+ }
+
+ when (backupPageUiState.listOfBackupFileState) {
+ is BackupPageVM.FileBackupListState.FileBackupListFinished -> {
+ driveBackupFileListState.value.let { backupFiles ->
+ if (backupFiles.isNullOrEmpty()) {
+ EmptyBackupListContainer(onBackupRequest)
+ }
+ else {
+ BackupListContainer(backupFiles, onRestoreRequest, onDeleteRequest, onBackupRequest)
+ }
+ }
+ }
+
+ 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(
+ onBackupRequest: () -> 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 = 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
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun BackupListContainer(
+ backupFiles: List,
+ onRestoreRequest: (File) -> Unit,
+ onDeleteRequest: (File) -> Unit,
+ onBackupRequest: () -> Unit
+) {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ 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 = EMPTY_STRING },
+ 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 = { onRestoreRequest(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 = stringResource(R.string.restore_icon),
+ modifier = Modifier.size(18.dp)
+ )
+ }
+
+ OutlinedButton(
+ onClick = { onDeleteRequest(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 = stringResource(R.string.delete_icon),
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun BackupFailedContainer(
+ onGetBackupList: () -> 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.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,
+ 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,
+ )
+ }
+ }
+}
+
+@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 = stringResource(R.string.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 71%
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..5aafdc2 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,21 @@
-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
@@ -28,23 +25,21 @@ 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
- 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.titleLarge.copy(
+ fontWeight = FontWeight.SemiBold
+ ),
)
},
colors = TopAppBarDefaults.topAppBarColors(
@@ -55,7 +50,7 @@ fun BackupPageAppBar(
Icons.AutoMirrored.Filled.ArrowBack,
stringResource(R.string.backup_nav_icon),
Modifier.semantics { }) {
- onBackPressed()
+ onBackRequest()
}
},
scrollBehavior = scrollBehavior,
@@ -63,8 +58,7 @@ fun BackupPageAppBar(
actions = {
if (authUiState.authState == AuthVM.AuthState.SignedIn) {
TrailingMenuIcons(
- onBackupPressed = onBackupPressed,
- onLogoutPressed = onLogoutPressed
+ onLogoutRequest = onLogoutRequest
)
}
}
@@ -73,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/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/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/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 829d054..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
@@ -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() {
@@ -43,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 594caff..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)
@@ -133,29 +143,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/note_creation/NoteCreationPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_creation/NoteCreationPage.kt
index 9368485..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,25 +1,18 @@
package com.digiventure.ventnote.feature.note_creation
import android.content.pm.ActivityInfo
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
+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.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 +20,31 @@ 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.platform.LocalFocusManager
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,155 +53,103 @@ 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)
- val length = viewModel.descriptionText.value.length
-
+ val focusManager = LocalFocusManager.current
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(
+ id = 0,
+ 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 = {
NoteCreationAppBar(
- descriptionTextLength = length,
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())
- ) {
- 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
- )
- }
- )
+ // Better scrolling performance with LazyColumn
+ LazyColumn(
+ modifier = Modifier
+ .padding(contentPadding)
+ .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)
+ }
+ }
+ },
+ bottomBar = {
+ EnhancedBottomAppBar {
+ addNote()
}
},
modifier = Modifier
@@ -219,30 +158,37 @@ fun NoteCreationPage(
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 71%
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..c2e8536 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,41 +1,35 @@
-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
@OptIn(ExperimentalMaterial3Api::class)
@Composable
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),
+ text = 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/note_detail/NoteDetailPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/NoteDetailPage.kt
index 3becdbe..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
@@ -3,57 +3,50 @@ 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
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
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
@@ -61,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(
@@ -76,307 +67,268 @@ 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)
-
- val noteDetailState = viewModel.noteDetail.observeAsState()
- val data = noteDetailState.value?.getOrNull()
+ // String resources - memoized for better performance
+ 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 isEditingState = viewModel.isEditing.value
- val focusManager = LocalFocusManager.current
+ val strings = remember {
+ mapOf(
+ "titleTextField" to titleTextFieldContentDescription,
+ "bodyTextField" to bodyTextFieldContentDescription,
+ "titleInput" to titleInputPlaceholder,
+ "bodyInput" to bodyInputPlaceholder,
+ "successFullyUpdatedText" to successFullyUpdatedLabel
+ )
+ }
- 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.navigateUp()
+ }
+ .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"] ?: EMPTY_STRING,
+ titleInput = strings["titleInput"] ?: EMPTY_STRING
+ )
+ }
+
+ item {
+ NoteSection(
+ viewModel = viewModel,
+ isEditingState = isEditingState,
+ bodyTextField = strings["bodyTextField"] ?: EMPTY_STRING,
+ bodyInput = strings["bodyInput"] ?: EMPTY_STRING
+ )
}
}
},
+ 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 titlePlaceholderText = stringResource(R.string.title_textField_input)
+ val notePlaceholderText = stringResource(R.string.body_textField_input)
- 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 }
- )
+ val missingFieldName = remember(
+ viewModel.titleText.value,
+ viewModel.descriptionText.value
+ ) {
+ when {
+ viewModel.titleText.value.isEmpty() -> titlePlaceholderText
+ viewModel.descriptionText.value.isEmpty() -> notePlaceholderText
+ else -> EMPTY_STRING
+ }
+ }
- 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) {
cancelDialogState.value = true
} else {
- navHostController.popBackStack()
+ navHostController.navigateUp()
}
}
}
-@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..67c3d4a
--- /dev/null
+++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/AppBar.kt
@@ -0,0 +1,58 @@
+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.titleLarge.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 = {
+ if (!isEditing) {
+ 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..e475134
--- /dev/null
+++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/navbar/EnhancedBottomAppBar.kt
@@ -0,0 +1,150 @@
+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.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(
+ 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 = stringResource(R.string.cancel),
+ onClick = onCancelClick,
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+
+ // Save button in editing mode
+ EnhancedBottomBarButton(
+ icon = Icons.Rounded.Check,
+ label = stringResource(R.string.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 = stringResource(R.string.edit),
+ onClick = onEditClick,
+ containerColor = MaterialTheme.colorScheme.secondary,
+ contentColor = MaterialTheme.colorScheme.onSecondary
+ )
+
+ // Delete button in view mode
+ EnhancedBottomBarButton(
+ icon = Icons.Rounded.DeleteOutline,
+ label = stringResource(R.string.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..02a6b47
--- /dev/null
+++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/NoteSection.kt
@@ -0,0 +1,143 @@
+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.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_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 = stringResource(R.string.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 label = "border_color"
+ val borderColor by animateColorAsState(
+ targetValue = if (isEditingState) MaterialTheme.colorScheme.primary else Color.Transparent,
+ animationSpec = tween(300),
+ label = label
+ )
+
+ 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 = MaterialTheme.typography.titleMedium.copy(
+ color = MaterialTheme.colorScheme.onSurface,
+ ),
+ 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,
+ 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_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..0f379cc
--- /dev/null
+++ b/app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/section/TitleSection.kt
@@ -0,0 +1,154 @@
+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.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_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 = stringResource(R.string.sort_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 label = "border_color"
+ val focusRequester = remember { FocusRequester() }
+ val borderColor by animateColorAsState(
+ targetValue = if (isEditingState) MaterialTheme.colorScheme.primary else Color.Transparent,
+ animationSpec = tween(300),
+ label = label
+ )
+
+ 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 = MaterialTheme.typography.titleMedium.copy(
+ color = MaterialTheme.colorScheme.onSurface,
+ ),
+ 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()
+ .semantics { contentDescription = titleTextField }
+ .focusRequester(focusRequester),
+ placeholder = {
+ if (isEditingState) {
+ 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/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/java/com/digiventure/ventnote/feature/notes/NotesPage.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/NotesPage.kt
index 5078f57..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
@@ -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,22 +21,30 @@ 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
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
@@ -42,8 +53,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,178 +71,231 @@ fun NotesPage(
viewModel: NotesPageBaseVM = hiltViewModel(),
openDrawer: () -> Unit
) {
+ 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)
- } ?: 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 ?: "",
- 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)
+ val noteIsDeletedText = stringResource(R.string.note_is_successfully_deleted)
- fun deleteNoteList() {
- scope.launch {
- viewModel.deleteNoteList()
- .onSuccess {
- deleteDialog.value = false
- viewModel.unMarkAllNote()
- viewModel.closeMarkingEvent()
+ // Memoized callbacks
+ val deleteNoteList = remember {
+ {
+ scope.launch {
+ viewModel.deleteNoteList()
+ .onSuccess {
+ showDeleteDialog = false
+ viewModel.unMarkAllNote()
+ viewModel.closeMarkingEvent()
- snackBarHostState.showSnackbar(
- message = deletedMessage,
- withDismissAction = true
- )
- }
- .onFailure {
- deleteDialog.value = false
+ snackBarHostState.showSnackbar(
+ message = noteIsDeletedText,
+ withDismissAction = true
+ )
+ }
+ .onFailure { error ->
+ showDeleteDialog = false
+ snackBarHostState.showSnackbar(
+ message = error.message.orEmpty(),
+ withDismissAction = true
+ )
+ }
+ }
+ }
+ }
- snackBarHostState.showSnackbar(
- message = it.message ?: "",
- withDismissAction = true
- )
- }
+ val onNoteClick = remember {
+ { note: NoteModel ->
+ if (isMarking) {
+ viewModel.addToMarkedNoteList(note)
+ } else {
+ viewModel.closeMarkingEvent()
+ navigationActions.navigateToDetailPage(note.id)
+ }
+ }
+ }
+
+ 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 },
+ totalNotesCount = filteredNotes.size
)
},
snackbarHost = { SnackbarHost(snackBarHostState) },
floatingActionButton = {
- 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)
- },
- icon = {
- Icon(
- imageVector = Icons.Filled.Add,
- contentDescription = stringResource(R.string.fab)
- )
- },
- containerColor = MaterialTheme.colorScheme.primary,
- contentColor = MaterialTheme.colorScheme.onPrimary
- )
+ 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
+ )
+ }
},
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)
+ 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(16.dp, 24.dp, 16.dp, 8.dp)
+ ) {
+ SearchBar(
+ query = searchQuery,
+ onQueryChange = { newQuery ->
+ viewModel.searchedTitleText.value = newQuery
+ }
+ )
+ }
+ }
+
+ items(
+ items = filteredNotes,
+ key = { note -> note.id }
+ ) { note ->
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .animateItem()
+ ) {
+ NotesItem(
+ isMarking = isMarking,
+ isMarked = note in markedNoteList,
+ data = note,
+ onClick = { onNoteClick(note) },
+ onLongClick = { onNoteLongClick(note) },
+ onCheckClick = { onNoteCheckClick(note) }
)
}
}
@@ -239,17 +305,32 @@ fun NotesPage(
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 })
+ 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 = openBottomSheet,
+ bottomSheetState = bottomSheetState,
+ 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/drawer/NavigationDrawer.kt b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/drawer/NavigationDrawer.kt
index 58f353a..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
@@ -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
@@ -84,7 +84,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)
@@ -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)
@@ -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,
@@ -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 04198b4..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
@@ -5,9 +5,15 @@ import androidx.compose.foundation.background
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.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.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -19,8 +25,8 @@ import androidx.compose.ui.semantics.semantics
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
@OptIn(ExperimentalFoundationApi::class)
@@ -31,53 +37,91 @@ 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)
Box(
modifier = Modifier
+ .fillMaxWidth()
.semantics { contentDescription = "Note item ${data.id}" }
+ .clip(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)
) {
- Box(modifier = Modifier.fillMaxSize()
- .padding(start = if(isMarked) 8.dp else 0.dp)
- .background(MaterialTheme.colorScheme.surface)) {
-
- Column(
- modifier = Modifier.fillMaxSize().padding(16.dp),
- horizontalAlignment = Alignment.Start
- ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(titleContainerShape)
+ .background(MaterialTheme.colorScheme.surfaceContainerHighest)
+ .padding(2.dp, 12.dp, 2.dp, 2.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.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)
+ style = MaterialTheme.typography.titleMedium.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ fontWeight = FontWeight.Medium,
+ ),
+ modifier = Modifier.padding(horizontal = 12.dp)
)
}
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(descriptionContainerShape)
+ .background(MaterialTheme.colorScheme.surface)
+ .padding(horizontal = 12.dp, vertical = 8.dp)
+ ) {
+ Column {
+ Text(
+ text = data.note,
+ maxLines = 4,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.Normal,
+ ),
+ modifier = Modifier.padding(bottom = 4.dp)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = DateUtil.convertDateString(
+ "EEEE, MMMM d h:mm a",
+ data.updatedAt.toString()
+ ),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodySmall.copy(
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
+ fontWeight = FontWeight.Normal,
+ ),
+ )
+ }
+ }
}
}
-}
\ 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..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
@@ -1,195 +1,356 @@
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.automirrored.filled.Sort
-import androidx.compose.material.icons.filled.ArrowDropDown
+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
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.Search
+import androidx.compose.material.icons.filled.RadioButtonUnchecked
+import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DropdownMenu
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.TextField
-import androidx.compose.material3.TextFieldDefaults
-import androidx.compose.material3.TopAppBar
-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
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
-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,
- isSearching: Boolean,
- searchedTitle: String,
+ totalNotesCount: Int = 0,
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
+ CenterAlignedTopAppBar(
+ title = {
+ if (isMarking) {
+ SelectionTitle(
+ markedNoteListSize = markedNoteListSize,
+ totalNotesCount = totalNotesCount,
+ expanded = expanded.value,
+ onToggleExpanded = { expanded.value = !expanded.value },
+ onSelectAll = {
+ selectAllCallback()
+ expanded.value = false
+ },
+ onUnselectAll = {
+ unSelectAllCallback()
+ expanded.value = false
+ },
+ onDismiss = { expanded.value = false }
)
+ } else {
+ AppTitle()
}
+ },
+ colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
+ containerColor = MaterialTheme.colorScheme.surface,
+ ),
+ navigationIcon = {
+ LeadingIcon(
+ isMarking = isMarking,
+ closeMarkingCallback = closeMarkingCallback,
+ toggleDrawerCallback = toggleDrawerCallback
+ )
+ },
+ actions = {
+ TrailingMenuIcons(
+ isMarking = isMarking,
+ markedItemsCount = markedNoteListSize,
+ sortCallback = sortCallback,
+ deleteCallback = deleteCallback
+ )
+ },
+ modifier = Modifier.semantics {
+ testTag = TestTags.TOP_APPBAR
+ }
+ )
+}
+
+@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))
- 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 if (isSearching) {
- TextField(value = searchedTitle,
- onValueChange = {
- onSearchValueChange(it)
- },
- 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 { })
- })
- } 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 },
+ text = buildAnnotatedString {
+ withStyle(
+ style = SpanStyle(
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ append(markedNoteListSize.toString())
+ }
+ withStyle(
+ style = SpanStyle(
+ color = MaterialTheme.colorScheme.onPrimary
+ )
+ ) {
+ append(" of $totalNotesCount selected")
+ }
+ },
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ 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)
)
}
- }, 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()
+
+ 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),
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ )
+ if (totalNotesCount > 0) {
+ Text(
+ text = "Select all $totalNotesCount notes",
+ style = MaterialTheme.typography.titleSmall.copy(
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
+ )
+ )
+ }
+ }
+ }
},
- deleteCallback = {
- deleteCallback()
+ 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),
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ )
+ if (markedNoteListSize > 0) {
+ Text(
+ text = "Clear selection of $markedNoteListSize notes",
+ style = MaterialTheme.typography.titleSmall.copy(
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
+ )
+ )
+ }
+ }
+ }
},
- closeSearchCallback = {
- closeSearchCallback()
- })
- }, modifier = Modifier.semantics {
- testTag = TestTags.TOP_APPBAR
- })
+ 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.titleLarge.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
+ )
}
}
}
@@ -198,46 +359,36 @@ fun LeadingIcon(
fun TrailingMenuIcons(
isMarking: Boolean,
markedItemsCount: Int,
- isSearching: Boolean,
- searchCallback: () -> Unit,
sortCallback: () -> Unit,
deleteCallback: () -> Unit,
- closeSearchCallback: () -> 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()
- }
- } else if (isSearching) {
- TopNavBarIcon(Icons.Filled.Close,
- stringResource(R.string.delete_nav_icon),
- modifier = Modifier.semantics { testTag = TestTags.CLOSE_SEARCH_ICON_BUTTON }) {
- closeSearchCallback()
+ 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(Icons.Filled.Search,
- stringResource(R.string.search_nav_icon),
- modifier = Modifier.semantics { testTag = TestTags.SEARCH_ICON_BUTTON }) {
- searchCallback()
- }
-
- TopNavBarIcon(image = Icons.AutoMirrored.Filled.Sort,
- 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.AutoMirrored.Outlined.Sort,
+ 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
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..c796bfe
--- /dev/null
+++ b/app/src/main/java/com/digiventure/ventnote/feature/notes/components/searchbar/SearchBar.kt
@@ -0,0 +1,71 @@
+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.unit.dp
+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,
+ focusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ cursorColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ focusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ ),
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = "Search",
+ tint = Color.Gray
+ )
+ },
+ textStyle = MaterialTheme.typography.titleMedium.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ ),
+ 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),
+ style = MaterialTheme.typography.titleMedium.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ ),
+ modifier = Modifier.semantics { }
+ )
+ })
+}
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..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
@@ -1,135 +1,172 @@
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.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.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.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 androidx.compose.ui.unit.sp
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)
@Composable
fun FilterSheet(
- openBottomSheet: MutableState,
+ openBottomSheet: Boolean,
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,
+ isOpened = openBottomSheet,
bottomSheetState = bottomSheetState,
- onDismissRequest = { openBottomSheet.value = false }
+ modifier = Modifier.semantics {
+ testTag = TestTags.BOTTOM_SHEET
+ },
+ onDismissRequest = { onDismiss() }
) {
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
+ FilterSection(
+ title = stringResource(R.string.sort_by),
+ icon = Icons.AutoMirrored.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)
+
+ 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,62 +174,86 @@ 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
@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(
@@ -204,7 +265,8 @@ fun FilterSheetPreview() {
FilterSheet(
openBottomSheet = openBottomSheet,
bottomSheetState = bottomSheetState,
- onDismiss = {}
+ onDismiss = {},
+ sortAndOrderData = null
) { _, _ ->
}
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..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
@@ -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(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 isSearching = mutableStateOf(false)
override val searchedTitleText = mutableStateOf("")
- override val isMarking = mutableStateOf(false)
+ override val isMarking = mutableStateOf(true)
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 441ba22..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
@@ -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
@@ -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 {
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..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
@@ -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,27 +35,30 @@ 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.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
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.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 +74,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,97 +143,129 @@ 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.copy(
+ color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f),
+ fontWeight = FontWeight.Medium
+ ),
+ )
+ }
+ }
+ }
+
+ item {
+ SelectionContainer {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleLarge.copy(
+ fontWeight = FontWeight.Medium,
+ 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 = MaterialTheme.typography.titleMedium.copy(
+ fontWeight = FontWeight.Normal,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.9f),
+ ),
+ modifier = Modifier.padding(20.dp)
+ )
+ }
+ }
+ }
+ }
}
+ },
+ bottomBar = {
+ EnhancedBottomAppBar(
+ onCancelClick = { openBottomSheet.value = true },
+ )
}
+ )
+
+ if (shareNoteDialogState.value) {
+ TextDialog(
+ title = stringResource(R.string.information),
+ description = stringResource(R.string.share_note_information),
+ isOpened = true,
+ onDismissCallback = { shareNoteDialogState.value = false },
+ modifier = Modifier.semantics { }
+ )
}
- TextDialog(
- title = stringResource(R.string.information),
- description = stringResource(R.string.share_note_information),
- isOpened = shareNoteDialogState.value,
- onDismissCallback = { shareNoteDialogState.value = false }
- )
+ if (openBottomSheet.value) {
+ ShareSheet(
+ isOpened = openBottomSheet.value,
+ bottomSheetState = bottomSheetState,
+ onDismissRequest = {
+ openBottomSheet.value = false
+ },
+ onShareRequest = {
+ handleShare()
+ }
+ )
+ }
- RegularBottomSheet(
- isOpened = openBottomSheet.value,
- bottomSheetState = bottomSheetState,
- 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)
- })
- }
+ 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/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..2759fd1 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.titleLarge.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/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/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/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/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
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..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,33 +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
-// Set of Material typography styles to start with
-val Typography = Typography(
- bodyLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 16.sp,
- lineHeight = 24.sp,
- letterSpacing = 0.5.sp
- ),
- // Other default text styles to override
- 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
+/**
+ * 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()
\ 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..e920256 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -40,8 +40,11 @@
Save
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
@@ -49,14 +52,16 @@
Input title here
Insert title
Insert note
+ Title
+ Note
Selected
Note Detail
+ Editing Note
Add New Note
Share Preview
Backup Notes
- Loading
Sort By
Title
Modified Date
@@ -64,6 +69,13 @@
Order By
Ascending
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
@@ -81,15 +93,25 @@
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
+ Note is successfully added
+
+
+ Signing in …
+ Loading backups …
+ Loading …
title textField
body textField
+ Restore Icon
+ Delete Icon
WEB_CLIENT_ID
\ 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()
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(
diff --git a/build.gradle b/build.gradle
index d2f6d8a..eeb2e61 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,17 +1,17 @@
buildscript {
dependencies {
- classpath 'com.android.tools.build:gradle:8.2.2'
+ 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 '1.8.0' apply false
- id 'com.google.dagger.hilt.android' version '2.44' 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 d63b0b2..7e2b4bc 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.14.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists