├── .github └── workflows │ └── android_ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── debug │ └── app-debug.aab ├── proguard-rules.pro ├── schemas │ └── com.digiventure.ventnote.config.NoteDatabase │ │ ├── 1.json │ │ └── 2.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── digiventure │ │ ├── MainActivityTest.kt │ │ ├── utils │ │ ├── BaseAcceptanceTest.kt │ │ └── CustomTestRunner.kt │ │ └── ventnote │ │ ├── NoteDetailFeature.kt │ │ └── NotesFeature.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── digiventure │ │ │ └── ventnote │ │ │ ├── MainActivity.kt │ │ │ ├── MainApplication.kt │ │ │ ├── commons │ │ │ ├── Constants.kt │ │ │ ├── DateUtil.kt │ │ │ └── TestTags.kt │ │ │ ├── components │ │ │ ├── LockScreenOrientation.kt │ │ │ ├── bottomSheet │ │ │ │ └── RegularBottomSheet.kt │ │ │ ├── dialog │ │ │ │ ├── LoadingDialog.kt │ │ │ │ └── TextDialog.kt │ │ │ └── navbar │ │ │ │ └── TopNavBarIcon.kt │ │ │ ├── config │ │ │ ├── DriveAPI.kt │ │ │ └── NoteDatabase.kt │ │ │ ├── data │ │ │ ├── google_drive │ │ │ │ ├── GoogleDriveRepository.kt │ │ │ │ └── GoogleDriveService.kt │ │ │ ├── local │ │ │ │ └── NoteDataStore.kt │ │ │ └── persistence │ │ │ │ ├── NoteDAO.kt │ │ │ │ ├── NoteLocalService.kt │ │ │ │ ├── NoteModel.kt │ │ │ │ └── NoteRepository.kt │ │ │ ├── feature │ │ │ ├── backup │ │ │ │ ├── BackupPage.kt │ │ │ │ ├── components │ │ │ │ │ ├── AppBar.kt │ │ │ │ │ ├── ListOfBackupFile.kt │ │ │ │ │ └── SignInButton.kt │ │ │ │ └── viewmodel │ │ │ │ │ ├── AuthBaseVM.kt │ │ │ │ │ ├── AuthMockVM.kt │ │ │ │ │ ├── AuthVM.kt │ │ │ │ │ ├── BackupPageBaseVM.kt │ │ │ │ │ ├── BackupPageMockVM.kt │ │ │ │ │ └── BackupPageVM.kt │ │ │ ├── note_creation │ │ │ │ ├── NoteCreationPage.kt │ │ │ │ ├── components │ │ │ │ │ └── AppBar.kt │ │ │ │ └── viewmodel │ │ │ │ │ ├── NoteCreationPageBaseVM.kt │ │ │ │ │ ├── NoteCreationPageMockVM.kt │ │ │ │ │ └── NoteCreationPageVM.kt │ │ │ ├── note_detail │ │ │ │ ├── NoteDetailPage.kt │ │ │ │ ├── components │ │ │ │ │ └── AppBar.kt │ │ │ │ └── viewmodel │ │ │ │ │ ├── NoteDetailPageBaseVM.kt │ │ │ │ │ ├── NoteDetailPageMockVM.kt │ │ │ │ │ └── NoteDetailPageVM.kt │ │ │ ├── notes │ │ │ │ ├── NotesPage.kt │ │ │ │ ├── components │ │ │ │ │ ├── drawer │ │ │ │ │ │ └── NavigationDrawer.kt │ │ │ │ │ ├── item │ │ │ │ │ │ └── NoteItem.kt │ │ │ │ │ ├── navbar │ │ │ │ │ │ └── AppBar.kt │ │ │ │ │ └── sheets │ │ │ │ │ │ └── FilterSheet.kt │ │ │ │ └── viewmodel │ │ │ │ │ ├── NotesPageBaseVM.kt │ │ │ │ │ ├── NotesPageMockVM.kt │ │ │ │ │ └── NotesPageVM.kt │ │ │ └── share_preview │ │ │ │ ├── SharePreviewPage.kt │ │ │ │ └── components │ │ │ │ └── AppBar.kt │ │ │ ├── module │ │ │ ├── ApplicationModule.kt │ │ │ ├── DatabaseModule.kt │ │ │ └── proxy │ │ │ │ └── DatabaseProxy.kt │ │ │ ├── navigation │ │ │ ├── NavGraph.kt │ │ │ ├── NoteModelParamType.kt │ │ │ ├── PageNavigation.kt │ │ │ └── Route.kt │ │ │ └── ui │ │ │ ├── ColorSchemeChoice.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ ├── java │ └── com │ │ └── digiventure │ │ ├── utils │ │ ├── BaseUnitTest.kt │ │ ├── LiveDataTestExtensions.kt │ │ └── MainDispatcherRule.kt │ │ └── ventnote │ │ ├── commons │ │ └── DateUtilsTest.kt │ │ ├── data │ │ ├── google_drive │ │ │ ├── GoogleDriveRepositoryShould.kt │ │ │ └── GoogleDriveServiceShould.kt │ │ └── persistence │ │ │ ├── NoteLocalServiceShould.kt │ │ │ └── NoteRepositoryShould.kt │ │ ├── note_creation │ │ └── NoteCreationPageVMShould.kt │ │ ├── note_detail │ │ └── NoteDetailPageVMShould.kt │ │ └── notes │ │ └── NotesPageVMShould.kt │ └── res │ └── backup.json ├── assets ├── banner.png ├── screen_five.png ├── screen_four.png ├── screen_one.png ├── screen_six.png ├── screen_three.png └── screen_two.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/workflows/android_ci.yml: -------------------------------------------------------------------------------- 1 | name: VentNote Build and Test CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | test: 9 | name: Run Unit Tests 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up JDK 17 15 | uses: actions/setup-java@v3 16 | with: 17 | java-version: '17' 18 | distribution: 'adopt' 19 | cache: gradle 20 | 21 | - name: Unit tests 22 | run: ./gradlew test --stacktrace 23 | 24 | build: 25 | runs-on: ubuntu-latest 26 | permissions: 27 | contents: read 28 | packages: write 29 | 30 | steps: 31 | - uses: actions/checkout@v3 32 | - name: Set up JDK 17 33 | uses: actions/setup-java@v3 34 | with: 35 | java-version: '17' 36 | distribution: 'adopt' 37 | cache: gradle 38 | 39 | - name: Clean project 40 | run: ./gradlew clean --stacktrace 41 | 42 | - name: Lint Debug 43 | run: ./gradlew lintDebug --stacktrace 44 | 45 | - name: Build debug APK 46 | run: ./gradlew build --stacktrace -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | /.idea 6 | /.idea/compiler.xml 7 | /.idea/misc.xml 8 | /.idea/caches 9 | /.idea/libraries 10 | /.idea/modules.xml 11 | /.idea/workspace.xml 12 | /.idea/navEditor.xml 13 | /.idea/assetWizardSettings.xml 14 | .DS_Store 15 | /build 16 | /release 17 | /captures 18 | .externalNativeBuild 19 | .cxx 20 | local.properties 21 | /schemas 22 | # For firebase connection 23 | google-services.json 24 | # For google drive api connection 25 | client_secret.json 26 | /app/release 27 | lint-baseline.xml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 | ## VentNote 8 | VentNote — Taking notes like a breeze. 9 | Note management app built with jetpack compose and newest modern android architecture guide 10 | 11 |
12 | 13 |

14 | 15 | 16 | 17 | 18 | 19 | 20 |

21 | 22 |
23 | 24 | ## 📱 Features 25 | - [v] Minimal and Aesthetic UI 26 | - [v] Create and Edit Notes 27 | - [v] Search and Find Notes 28 | - [v] Delete Notes 29 | - [v] Share Notes 30 | - [v] Dark Mode & App Color Switch 31 | - [v] In App Update 32 | - [-] Widget (Waiting for stable version) 33 | - [v] Google Drive Backup 34 | 35 |
36 | 37 | ## Supports Me 38 | Want to see more free, high-quality code and articles? Buy me a coffee and make it happen! 39 | 40 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/syubban) 41 | 42 |
43 | 44 | ## 🖨 Downloads 45 | [![Licence](https://img.shields.io/github/license/HellBus1/VentNote?style=for-the-badge&color=6650a4)](./LICENSE) 46 | [![GitHub release](https://img.shields.io/github/downloads/HellBus1/VentNote/total?color=6650a4&label=Downloads&logo=android&style=for-the-badge)](https://github.com/HellBus1/VentNote/releases) 47 |
48 | **Runs on Android 5.1 and up** 49 | 50 |
51 | 52 | ## 📑 Installation Steps 53 | The binary file consist of 3 file (source codes and debug apk) 54 | 1. [Download the app](https://github.com/HellBus1/VentNote/releases) by clicking the green button or this link. 55 | 56 | For App : 57 | 58 | 1. Locate the file and install, you might get a warning (allow install from untrusted source), that's because the app isn't from the playstore, but it's safe to install. 59 | 2. After installing, you should be able to use the app. 60 | 61 | For Source Code : 62 | 63 | 1. Clone the repositoy or download source code.zip / source code.tar.gz 64 | 2. Extract the source code 65 | 3. Open with android studio and wait the build until done 66 | 67 |
68 | 69 | ## 📑 Contribution Guide 70 | GitHub provides a comprehensive contribution guide for public repositories, which can be found here: https://docs.github.com/en/get-started/quickstart/contributing-to-projects. You can also apply this method in this repository by using the "fork & pull request" feature. In addition, please follow these rules: 71 | 72 | 1. After forking from the main repository, clone your fork to your local machine. 73 | 2. Create a branch from "staging." After making your improvements, create the pull request there. 74 | 3. To keep your fork updated and prevent conflicts, you must sync your fork (for a complete guide, read it here: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork). Make sure to do this before submitting your pull request. 75 | 4. You can also open issues, and I will be happy to work on them immediately. 76 | 77 |
78 | 79 | ## 📑 Documentation 80 | You can access the technical documentation here [documentation page](https://futuristic-gateway-d0a.notion.site/Technical-Documentation-84352179b256469b8970acf91b6cb9a0). If you're interested in learning more about the journey and how to build this app, I invite you to regularly visit [my medium](https://medium.com/@syubbanfakhriya). 81 | 82 |
83 | 84 | ## 📑 Contact 85 | Please feel free to reach out to me via email, Twitter, or LinkedIn if you have anything to discuss. And if you like this project, don't forget to leave a clap to my medium or star to show your support. 86 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'com.google.dagger.hilt.android' 5 | id 'kotlin-kapt' 6 | id 'kotlin-parcelize' 7 | 8 | // disabled for internal purpose (if you want to enable, you must create firebase project first) 9 | // id 'com.google.gms.google-services' 10 | // id 'com.google.firebase.crashlytics' 11 | // id 'com.google.firebase.firebase-perf' 12 | } 13 | 14 | android { 15 | namespace 'com.digiventure.ventnote' 16 | compileSdk 34 17 | 18 | defaultConfig { 19 | applicationId "com.digiventure.ventnote" 20 | minSdk 21 21 | targetSdk 34 22 | versionCode 41 23 | versionName "1.0.8" 24 | 25 | testInstrumentationRunner "com.digiventure.utils.CustomTestRunner" 26 | vectorDrawables { 27 | useSupportLibrary true 28 | } 29 | 30 | kapt { 31 | arguments { 32 | arg("room.schemaLocation", "$projectDir/schemas") 33 | } 34 | } 35 | } 36 | 37 | buildTypes { 38 | release { 39 | minifyEnabled true 40 | debuggable false 41 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 42 | } 43 | debug { 44 | minifyEnabled false 45 | signingConfig signingConfigs.debug 46 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 47 | } 48 | } 49 | 50 | compileOptions { 51 | sourceCompatibility JavaVersion.VERSION_17 52 | targetCompatibility JavaVersion.VERSION_17 53 | } 54 | 55 | kotlinOptions { 56 | jvmTarget = '17' 57 | } 58 | 59 | buildFeatures { 60 | compose true 61 | buildConfig true 62 | } 63 | 64 | composeOptions { 65 | kotlinCompilerExtensionVersion '1.4.0' 66 | } 67 | 68 | packagingOptions { 69 | resources { 70 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 71 | excludes += 'META-INF/*' 72 | } 73 | } 74 | 75 | lintOptions { 76 | abortOnError false 77 | } 78 | } 79 | 80 | dependencies { 81 | implementation "androidx.core:core-ktx:1.13.1" 82 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.6" 83 | 84 | implementation "androidx.activity:activity-compose:1.9.3" 85 | 86 | // Jetpack Compose 87 | implementation "androidx.compose.runtime:runtime-livedata:1.7.4" 88 | implementation "androidx.compose.ui:ui:1.7.4" 89 | implementation "androidx.compose.ui:ui-tooling-preview:1.7.4" 90 | 91 | implementation "androidx.compose.material3:material3:1.3.1" 92 | 93 | // Lifecycle Livedata 94 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6" 95 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.6" 96 | 97 | // Room 98 | def room_version = "2.6.1" 99 | implementation "androidx.room:room-ktx:$room_version" 100 | kapt "androidx.room:room-compiler:$room_version" 101 | androidTestImplementation "androidx.room:room-testing:$room_version" 102 | 103 | // Datastore 104 | implementation("androidx.datastore:datastore-preferences:1.1.1") 105 | 106 | // Compose Navigation 107 | implementation "androidx.navigation:navigation-compose:2.8.3" 108 | 109 | // Coroutines 110 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0" 111 | 112 | // Unit test 113 | testImplementation "junit:junit:4.13.2" 114 | androidTestImplementation "androidx.test.ext:junit:1.2.1" 115 | 116 | // Hilt 117 | implementation "com.google.dagger:hilt-android:2.50" 118 | kapt "com.google.dagger:hilt-android-compiler:2.50" 119 | implementation "androidx.hilt:hilt-navigation-compose:1.2.0" 120 | 121 | // Material Icon Extension 122 | implementation "androidx.compose.material:material-icons-extended:1.7.4" 123 | 124 | // So, make sure you also include that repository in your project's build.gradle file. 125 | implementation("com.google.android.play:app-update:2.1.0") 126 | // For Kotlin users also import the Kotlin extensions library for Play In-App Update: 127 | implementation("com.google.android.play:app-update-ktx:2.1.0") 128 | 129 | // Google Play API 130 | implementation "com.google.android.gms:play-services-auth:21.2.0" 131 | 132 | // Accompanist - Status Bar 133 | implementation "com.google.accompanist:accompanist-systemuicontroller:0.34.0" 134 | 135 | // Instrumented test 136 | /// Espresso (for ui interaction purpose) 137 | androidTestImplementation "androidx.test:runner:1.6.2" 138 | androidTestImplementation "androidx.test:rules:1.6.1" 139 | androidTestImplementation "androidx.test.espresso:espresso-intents:3.6.1" 140 | androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1" 141 | 142 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.7.4" 143 | debugImplementation "androidx.compose.ui:ui-tooling:1.7.4" 144 | debugImplementation "androidx.compose.ui:ui-test-manifest:1.7.4" 145 | 146 | /// Hilt test (for handling service locator when test) 147 | androidTestImplementation "com.google.dagger:hilt-android-testing:2.50" 148 | kaptAndroidTest "com.google.dagger:hilt-android-compiler:2.50" 149 | 150 | // For mocking purposes & make it visible in instrumented test 151 | testImplementation "org.mockito.kotlin:mockito-kotlin:5.2.1" 152 | testImplementation "org.mockito:mockito-inline:5.2.0" 153 | 154 | androidTestImplementation "androidx.test.ext:junit-ktx:1.2.1" 155 | 156 | testImplementation "androidx.arch.core:core-testing:2.2.0" 157 | 158 | // For unit testing coroutines 159 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0" 160 | 161 | // Import the Firebase BoM 162 | implementation platform("com.google.firebase:firebase-bom:33.4.0") 163 | // When using the BoM, you don't specify versions in Firebase library dependencies 164 | // Add the dependency for the Firebase SDK for Google Analytics 165 | implementation "com.google.firebase:firebase-analytics" 166 | implementation "com.google.firebase:firebase-crashlytics:19.2.0" 167 | implementation "com.google.firebase:firebase-perf-ktx:21.0.1" 168 | 169 | //Google sign in 170 | implementation "com.google.android.gms:play-services-auth:21.2.0" 171 | 172 | //Google Drive API 173 | implementation "com.google.http-client:google-http-client-gson:1.44.2" 174 | implementation "com.google.apis:google-api-services-drive:v3-rev136-1.25.0" 175 | implementation "com.google.api-client:google-api-client-android:1.34.0" 176 | } 177 | 178 | kapt { 179 | correctErrorTypes = true 180 | } -------------------------------------------------------------------------------- /app/debug/app-debug.aab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/debug/app-debug.aab -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -assumenosideeffects public class androidx.compose.runtime.ComposerKt { 24 | boolean isTraceInProgress(); 25 | void traceEventStart(int,int,int,java.lang.String); 26 | void traceEventEnd(); 27 | } 28 | 29 | # Add project specific ProGuard rules here. 30 | # You can control the set of applied configuration files using the 31 | # proguardFiles setting in build.gradle. 32 | # 33 | # For more details, see 34 | # http://developer.android.com/guide/developing/tools/proguard.html 35 | 36 | # If your project uses WebView with JS, uncomment the following 37 | # and specify the fully qualified class name to the JavaScript interface 38 | # class: 39 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 40 | # public *; 41 | #} 42 | 43 | # Uncomment this to preserve the line number information for 44 | # debugging stack traces. 45 | #-keepattributes SourceFile,LineNumberTable 46 | 47 | # If you keep the line number information, uncomment this to 48 | # hide the original source file name. 49 | #-renamesourcefileattribute SourceFile 50 | 51 | -assumenosideeffects public class androidx.compose.runtime.ComposerKt { 52 | boolean isTraceInProgress(); 53 | void traceEventStart(int,int,int,java.lang.String); 54 | void traceEventEnd(); 55 | } 56 | 57 | # Google Play Services Auth 58 | -keep class com.google.android.gms.auth.** { *; } 59 | -keep class com.google.android.gms.common.** { *; } 60 | -keep class com.google.api.client.** { *; } 61 | -keep class com.google.oauth.client.** { *; } 62 | -keep class com.google.http.client.** { *; } 63 | -keep class com.google.api.client.json.** { *; } 64 | -keep class com.google.api.client.http.** { *; } 65 | -keep class com.google.api.client.googleapis.** { *; } 66 | 67 | # Google Drive API 68 | -keep class com.google.api.services.drive.** { *; } 69 | -keep class com.google.api.client.googleapis.services.** { *; } 70 | 71 | # Google API Client for Android 72 | -keep class com.google.api.client.googleapis.extensions.android.** { *; } 73 | -keep class com.google.api.client.extensions.android.** { *; } 74 | 75 | # Keep all classes related to Google Auth API 76 | -keep class com.google.api.services.** { *; } 77 | -keep class com.google.auth.** { *; } 78 | -keep class com.google.api.client.auth.** { *; } 79 | -keep class com.google.http.client.auth.** { *; } 80 | -keep class com.google.oauth.client.auth.** { *; } 81 | 82 | -keep class com.google.android.gms.auth.api.signin.GoogleSignInClient { 83 | *; 84 | } 85 | -keep class com.google.android.gms.auth.api.signin.GoogleSignInOptions { 86 | *; 87 | } 88 | -keep class androidx.compose.runtime.Composable { 89 | *; 90 | } 91 | 92 | # Google HTTP Client & GSON 93 | -keepclassmembers class * { 94 | @com.google.api.client.util.Key ; 95 | } 96 | -keepattributes Signature,RuntimeVisibleAnnotations,AnnotationDefault 97 | -keepclasseswithmembers class * { 98 | @com.google.api.client.util.Value *; 99 | } 100 | 101 | # Room database 102 | -keep class androidx.room.** { *; } 103 | -keep class * extends androidx.room.RoomDatabase { 104 | (...); 105 | } 106 | -keep class * extends androidx.room.Database { 107 | (...); 108 | } 109 | -keep class * extends androidx.room.Entity { 110 | (...); 111 | } 112 | -keep class * extends androidx.room.Dao { 113 | (...); 114 | } 115 | -keepattributes EnclosingMethod 116 | -keepattributes InnerClasses 117 | -dontwarn androidx.room.** 118 | 119 | # Gson specific rules 120 | -keepattributes Signature 121 | -keepattributes *Annotation* 122 | 123 | # Keep your NoteModel class and all its fields 124 | -keep class com.digiventure.ventnote.data.persistence.NoteModel { *; } 125 | # If NoteModel has inner classes, keep them too 126 | -keep class com.digiventure.ventnote.data.persistence.NoteModel$* { *; } 127 | 128 | # Keep the GoogleDriveService class and all its methods 129 | -keep class com.digiventure.ventnote.data.google_drive.GoogleDriveService { *; } 130 | 131 | # Prevent obfuscation of the DatabaseProxy class as it is used within GoogleDriveService 132 | -keep class com.digiventure.ventnote.module.proxy.DatabaseProxy { *; } 133 | 134 | -keep class com.digiventure.ventnote.feature.backup.viewmodel.AuthVM 135 | 136 | # Keep any classes that are used in Gson serialization 137 | -keep class * implements com.google.gson.TypeAdapterFactory 138 | -keep class * implements com.google.gson.JsonSerializer 139 | -keep class * implements com.google.gson.JsonDeserializer 140 | 141 | # Keep all classes that are serialized/deserialized by Gson 142 | -keepclassmembers class * { 143 | @com.google.gson.annotations.SerializedName ; 144 | } 145 | 146 | # Common rules for all Google APIs 147 | -dontwarn com.google.api.client.extensions.android.** 148 | -dontwarn com.google.api.client.googleapis.extensions.android.** 149 | -dontwarn com.google.android.gms.** 150 | -dontwarn com.google.api.client.json.jackson2.** 151 | -dontwarn javax.annotation.** 152 | 153 | -dontwarn javax.naming.** 154 | -dontwarn javax.servlet.** 155 | -dontwarn org.apache.** 156 | -dontwarn org.ietf.jgss.** 157 | 158 | -------------------------------------------------------------------------------- /app/schemas/com.digiventure.ventnote.config.NoteDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "5ba0c0c61835cab478078d95b4bcb861", 6 | "entities": [ 7 | { 8 | "tableName": "note_table", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `note` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL)", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "title", 19 | "columnName": "title", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "note", 25 | "columnName": "note", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "createdAt", 31 | "columnName": "created_at", 32 | "affinity": "INTEGER", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "updatedAt", 37 | "columnName": "updated_at", 38 | "affinity": "INTEGER", 39 | "notNull": true 40 | } 41 | ], 42 | "primaryKey": { 43 | "autoGenerate": true, 44 | "columnNames": [ 45 | "id" 46 | ] 47 | }, 48 | "indices": [], 49 | "foreignKeys": [] 50 | } 51 | ], 52 | "views": [], 53 | "setupQueries": [ 54 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 55 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5ba0c0c61835cab478078d95b4bcb861')" 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /app/schemas/com.digiventure.ventnote.config.NoteDatabase/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 2, 5 | "identityHash": "5ba0c0c61835cab478078d95b4bcb861", 6 | "entities": [ 7 | { 8 | "tableName": "note_table", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `note` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL)", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "title", 19 | "columnName": "title", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "note", 25 | "columnName": "note", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "createdAt", 31 | "columnName": "created_at", 32 | "affinity": "INTEGER", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "updatedAt", 37 | "columnName": "updated_at", 38 | "affinity": "INTEGER", 39 | "notNull": true 40 | } 41 | ], 42 | "primaryKey": { 43 | "autoGenerate": true, 44 | "columnNames": [ 45 | "id" 46 | ] 47 | }, 48 | "indices": [], 49 | "foreignKeys": [] 50 | } 51 | ], 52 | "views": [], 53 | "setupQueries": [ 54 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 55 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5ba0c0c61835cab478078d95b4bcb861')" 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/digiventure/MainActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure 2 | 3 | import androidx.test.ext.junit.rules.activityScenarioRule 4 | import com.digiventure.utils.BaseAcceptanceTest 5 | import com.digiventure.ventnote.MainActivity 6 | import org.junit.Before 7 | import org.junit.Rule 8 | 9 | class MainActivityTest: BaseAcceptanceTest() { 10 | @get:Rule 11 | val activityRule = activityScenarioRule() 12 | 13 | @Before 14 | fun setup() { 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/digiventure/utils/BaseAcceptanceTest.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.utils 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import org.junit.runner.RunWith 5 | 6 | @RunWith(AndroidJUnit4::class) 7 | abstract class BaseAcceptanceTest { 8 | // @get:Rule(order = 0) 9 | // val composeTestRule = createComposeRule() 10 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/digiventure/utils/CustomTestRunner.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.utils 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.test.runner.AndroidJUnitRunner 6 | import dagger.hilt.android.testing.HiltTestApplication 7 | 8 | class CustomTestRunner : AndroidJUnitRunner() { 9 | override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { 10 | return super.newApplication(cl, HiltTestApplication::class.java.name, context) 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/digiventure/ventnote/NoteDetailFeature.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote 2 | 3 | import com.digiventure.utils.BaseAcceptanceTest 4 | 5 | class NoteDetailFeature: BaseAcceptanceTest() -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.compose.foundation.layout.safeDrawingPadding 8 | import androidx.compose.material3.DrawerValue 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.SnackbarHostState 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.material3.rememberDrawerState 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.rememberCoroutineScope 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.navigation.compose.rememberNavController 18 | import com.digiventure.ventnote.components.dialog.TextDialog 19 | import com.digiventure.ventnote.feature.notes.components.drawer.NavDrawer 20 | import com.digiventure.ventnote.navigation.NavGraph 21 | import com.digiventure.ventnote.navigation.PageNavigation 22 | import com.digiventure.ventnote.ui.theme.VentNoteTheme 23 | import com.google.android.play.core.appupdate.AppUpdateManager 24 | import com.google.android.play.core.appupdate.AppUpdateManagerFactory 25 | import com.google.android.play.core.install.InstallStateUpdatedListener 26 | import com.google.android.play.core.install.model.AppUpdateType 27 | import com.google.android.play.core.install.model.AppUpdateType.IMMEDIATE 28 | import com.google.android.play.core.install.model.InstallStatus 29 | import com.google.android.play.core.install.model.UpdateAvailability 30 | import dagger.hilt.android.AndroidEntryPoint 31 | import kotlinx.coroutines.launch 32 | 33 | @AndroidEntryPoint 34 | class MainActivity : ComponentActivity() { 35 | private lateinit var installStateUpdatedListener: InstallStateUpdatedListener 36 | private lateinit var appUpdateManager: AppUpdateManager 37 | private var isDialogShowed = false 38 | 39 | companion object { 40 | const val REQUEST_UPDATE_CODE = 1 41 | } 42 | 43 | override fun onCreate(savedInstanceState: Bundle?) { 44 | super.onCreate(savedInstanceState) 45 | 46 | // Check in-app update 47 | appUpdateManager = AppUpdateManagerFactory.create(this) 48 | addUpdateStatusListener() 49 | checkUpdate() 50 | 51 | enableEdgeToEdge() 52 | 53 | setContent { 54 | VentNoteTheme { 55 | val navController = rememberNavController() 56 | val navigationActions = remember(navController) { 57 | PageNavigation(navController) 58 | } 59 | 60 | val drawerState = rememberDrawerState(DrawerValue.Closed) 61 | 62 | val coroutineScope = rememberCoroutineScope() 63 | 64 | val snackBarHostState = remember { SnackbarHostState() } 65 | 66 | Surface( 67 | modifier = Modifier.safeDrawingPadding(), 68 | color = MaterialTheme.colorScheme.primary, 69 | contentColor = MaterialTheme.colorScheme.secondary 70 | ) { 71 | NavDrawer( 72 | drawerState = drawerState, 73 | onError = { 74 | 75 | }, 76 | onBackupPressed = { 77 | navigationActions.navigateToBackupPage() 78 | }, 79 | content = { 80 | NavGraph(navHostController = navController, openDrawer = { 81 | coroutineScope.launch { drawerState.open() } 82 | }) 83 | }, 84 | ) 85 | 86 | TextDialog( 87 | isOpened = isDialogShowed, 88 | onDismissCallback = { 89 | isDialogShowed = false 90 | }, 91 | onConfirmCallback = { 92 | isDialogShowed = false 93 | appUpdateManager.completeUpdate() 94 | }, 95 | title = stringResource(id = R.string.success), 96 | description = stringResource(id = R.string.update_success_text) 97 | ) 98 | } 99 | } 100 | } 101 | } 102 | 103 | private fun addUpdateStatusListener() { 104 | installStateUpdatedListener = InstallStateUpdatedListener { installState -> 105 | when (installState.installStatus()) { 106 | InstallStatus.DOWNLOADED -> { 107 | // After the update is downloaded, show a notification 108 | // and request user confirmation to restart the app. 109 | showDialogForCompleteUpdate() 110 | } 111 | 112 | InstallStatus.INSTALLED -> { 113 | appUpdateManager.unregisterListener(installStateUpdatedListener) 114 | } 115 | 116 | else -> {} 117 | } 118 | } 119 | } 120 | 121 | private fun checkUpdate() { 122 | // Before starting an update, register a listener for updates. 123 | appUpdateManager.registerListener(installStateUpdatedListener) 124 | 125 | // Returns an intent object that you use to check for an update. 126 | val appUpdateInfoTask = appUpdateManager.appUpdateInfo 127 | 128 | // Check that the platform will allow the specified type of update. 129 | appUpdateInfoTask.addOnSuccessListener { 130 | when (it.updateAvailability()) { 131 | UpdateAvailability.UPDATE_AVAILABLE -> { 132 | val updateTypes = arrayOf(AppUpdateType.FLEXIBLE, IMMEDIATE) 133 | for (type in updateTypes) { 134 | if (it.isUpdateTypeAllowed(type)) { 135 | appUpdateManager.startUpdateFlowForResult( 136 | it, 137 | type, 138 | this, 139 | REQUEST_UPDATE_CODE 140 | ) 141 | break 142 | } 143 | } 144 | } 145 | 146 | else -> {} 147 | } 148 | } 149 | } 150 | 151 | override fun onResume() { 152 | super.onResume() 153 | 154 | appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo -> 155 | if (appUpdateInfo != null) { // Check if appUpdateInfo is not null 156 | if (appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { 157 | if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) { 158 | showDialogForCompleteUpdate() 159 | } 160 | } else { 161 | if (appUpdateInfo.updateAvailability() == 162 | UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS 163 | ) { 164 | // If an in-app update is already running, resume the update. 165 | appUpdateManager.startUpdateFlowForResult( 166 | appUpdateInfo, 167 | IMMEDIATE, 168 | this, 169 | REQUEST_UPDATE_CODE 170 | ) 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | private fun showDialogForCompleteUpdate() { 178 | isDialogShowed = true 179 | } 180 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class MainApplication : Application() -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/commons/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.commons 2 | 3 | object Constants { 4 | const val CREATED_AT = "created_at" 5 | const val UPDATED_AT = "updated_at" 6 | const val TITLE = "title" 7 | const val DESCENDING = "DESC" 8 | const val ASCENDING = "ASC" 9 | const val GLOBAL_PREFERENCE = "GLOBAL_PREFERENCE" 10 | const val COLOR_SCHEME = "COLOR_SCHEME" 11 | const val COLOR_PALLET = "COLOR_PALLET" 12 | const val BACKUP_FILE_NAME = "backup" 13 | const val EMPTY_STRING = "" 14 | } 15 | 16 | object ColorPalletName { 17 | const val CRIMSON = "CRIMSON" 18 | const val PURPLE = "PURPLE" 19 | const val CADMIUM_GREEN = "CADMIUM_GREEN" 20 | const val COBALT_BLUE = "COBALT_BLUE" 21 | } 22 | 23 | object ColorSchemeName { 24 | const val DARK_MODE = "DARK_MODE" 25 | const val LIGHT_MODE = "LIGHT_MODE" 26 | } 27 | 28 | object ErrorMessage { 29 | const val FAILED_GET_NOTE_LIST_ROOM = "Failed to get list of notes" 30 | const val FAILED_DELETE_ROOM = "Failed to delete list of notes" 31 | const val FAILED_GET_NOTE_DETAIL_ROOM = "Failed to get note detail" 32 | const val FAILED_UPDATE_NOTE_ROOM = "Failed to update list of notes" 33 | const val FAILED_INSERT_NOTE_ROOM = "Failed to insert list of notes" 34 | 35 | const val FAILED_UPLOAD_DATABASE_FILE = "Failed to upload backup file" 36 | const val FAILED_RESTORE_DATABASE_FILE = "Failed to restore backup file" 37 | const val FAILED_GET_LIST_BACKUP_FILE = "Failed to get backup files" 38 | const val FAILED_DELETE_DATABASE_FILE = "Failed to delete file" 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/commons/DateUtil.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.commons 2 | 3 | import java.text.ParseException 4 | import java.text.SimpleDateFormat 5 | import java.util.Date 6 | import java.util.Locale 7 | 8 | object DateUtil { 9 | /** 10 | * Return formatted date string 11 | * @param format pattern can be see here https://developer.android.com/reference/kotlin/android/icu/text/SimpleDateFormat 12 | * @param dateString is raw date in string 13 | * */ 14 | fun convertDateString(format: String, dateString: String): String { 15 | return try { 16 | val inputDateFormat = SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.getDefault()) 17 | val inputDate = inputDateFormat.parse(dateString) 18 | 19 | val outputDateFormat = SimpleDateFormat(format, Locale.getDefault()) 20 | val outputDateString = inputDate?.let { outputDateFormat.format(it) } 21 | 22 | (outputDateString?.format(dateString) ?: Date()).toString() 23 | } catch (e: ParseException) { 24 | "" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/commons/TestTags.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.commons 2 | 3 | object TestTags { 4 | // Pages test tags 5 | const val NOTES_PAGE = "notes_feature" 6 | const val NOTE_DETAIL_PAGE = "note_detail_page" 7 | const val NOTE_CREATION_PAGE = "note_creation_page" 8 | 9 | // Appbar test tags 10 | const val TOP_APPBAR = "top_appbar" 11 | 12 | const val DELETE_ICON_BUTTON = "delete_icon_button" 13 | const val CLOSE_SEARCH_ICON_BUTTON = "close_search_icon_button" 14 | const val SEARCH_ICON_BUTTON = "search_icon_button" 15 | const val SORT_ICON_BUTTON = "sort_icon_button" 16 | const val CLOSE_SELECT_ICON_BUTTON = "close_select_icon_button" 17 | const val MENU_ICON_BUTTON = "menu_icon_button" 18 | const val TOP_APPBAR_TITLE = "top_appbar_title" 19 | const val TOP_APPBAR_TEXT_FIELD = "top_appbar_text_field" 20 | const val SELECTED_COUNT = "selected_count" 21 | const val DROPDOWN_SELECT = "dropdown_select" 22 | const val SELECT_ALL_OPTION = "select_all_option" 23 | const val UNSELECT_ALL_OPTION = "unselect_all_option" 24 | const val SELECTED_COUNT_CONTAINER = "selected_count_container" 25 | 26 | // Nav drawer test tags 27 | const val NAV_DRAWER = "nav_drawer" 28 | const val RATE_APP_TILE = "rate_app_tile" 29 | 30 | // Note lists test tags 31 | const val ADD_NOTE_FAB = "add_note_fab" 32 | const val SHARE_NOTE_FAB = "share_note_fab" 33 | const val NOTE_RV = "note_rv" 34 | const val LOADING_DIALOG = "loading_dialog" 35 | const val CONFIRMATION_DIALOG = "confirmation_dialog" 36 | 37 | // Dialog Button 38 | const val CONFIRM_BUTTON = "confirm_button" 39 | const val DISMISS_BUTTON = "dismiss_button" 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/components/LockScreenOrientation.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.components 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.DisposableEffect 8 | import androidx.compose.ui.platform.LocalContext 9 | 10 | @Composable 11 | fun LockScreenOrientation(orientation: Int) { 12 | val context = LocalContext.current 13 | DisposableEffect(Unit) { 14 | val activity = context.findActivity() ?: return@DisposableEffect onDispose {} 15 | val originalOrientation = activity.requestedOrientation 16 | activity.requestedOrientation = orientation 17 | onDispose { 18 | // restore original orientation when view disappears 19 | activity.requestedOrientation = originalOrientation 20 | } 21 | } 22 | } 23 | 24 | fun Context.findActivity(): Activity? = when (this) { 25 | is Activity -> this 26 | is ContextWrapper -> baseContext.findActivity() 27 | else -> null 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/components/bottomSheet/RegularBottomSheet.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.components.bottomSheet 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.ModalBottomSheet 6 | import androidx.compose.material3.SheetState 7 | import androidx.compose.runtime.Composable 8 | 9 | @OptIn(ExperimentalMaterial3Api::class) 10 | @Composable 11 | fun RegularBottomSheet( 12 | isOpened: Boolean, 13 | bottomSheetState: SheetState, 14 | onDismissRequest: () -> Unit, 15 | content: @Composable () -> Unit 16 | ) { 17 | if (isOpened) { 18 | ModalBottomSheet( 19 | onDismissRequest = { onDismissRequest() }, 20 | sheetState = bottomSheetState, 21 | containerColor = MaterialTheme.colorScheme.background, 22 | ) { 23 | content() 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/components/dialog/LoadingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.components.dialog 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material3.BasicAlertDialog 9 | import androidx.compose.material3.CircularProgressIndicator 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import com.digiventure.ventnote.R 22 | 23 | @OptIn(ExperimentalMaterial3Api::class) 24 | @Composable 25 | fun LoadingDialog( 26 | modifier: Modifier = Modifier, 27 | isOpened: Boolean, 28 | onDismissCallback: () -> Unit, 29 | ) { 30 | if (isOpened) { 31 | BasicAlertDialog(onDismissRequest = { onDismissCallback() }, 32 | modifier = modifier, 33 | content = { 34 | Surface(shape = RoundedCornerShape(8.dp)) { 35 | Row( 36 | modifier = Modifier.padding(16.dp), 37 | horizontalArrangement = Arrangement.Center, 38 | verticalAlignment = Alignment.CenterVertically, 39 | ) { 40 | CircularProgressIndicator( 41 | modifier = Modifier 42 | .padding(end = 16.dp) 43 | .size(24.dp), 44 | strokeWidth = 2.dp, 45 | color = MaterialTheme.colorScheme.onSurface 46 | ) 47 | Text( 48 | text = stringResource(id = R.string.loading), 49 | fontSize = 16.sp, 50 | fontWeight = FontWeight.Normal, 51 | color = MaterialTheme.colorScheme.onSurface 52 | ) 53 | } 54 | } 55 | } 56 | ) 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/components/dialog/TextDialog.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.components.dialog 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material3.AlertDialog 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Text 7 | import androidx.compose.material3.TextButton 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.compose.ui.semantics.semantics 12 | import androidx.compose.ui.semantics.testTag 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | import com.digiventure.ventnote.R 17 | import com.digiventure.ventnote.commons.TestTags 18 | 19 | @Composable 20 | fun TextDialog( 21 | modifier: Modifier = Modifier, 22 | isOpened: Boolean, 23 | onDismissCallback: () -> Unit, 24 | onConfirmCallback: (() -> Unit)? = null, 25 | title: String = stringResource(R.string.warning_title), 26 | description: String = stringResource(R.string.delete_confirmation_text), 27 | ) { 28 | if (isOpened) { 29 | AlertDialog( 30 | onDismissRequest = { onDismissCallback() }, 31 | title = { 32 | Text( 33 | text = title, 34 | fontSize = 18.sp, 35 | fontWeight = FontWeight.Medium, 36 | color = MaterialTheme.colorScheme.onSurface 37 | ) 38 | }, 39 | text = { 40 | Text( 41 | text = description, 42 | fontSize = 16.sp, 43 | color = MaterialTheme.colorScheme.onSurface 44 | ) 45 | }, 46 | confirmButton = { 47 | if (onConfirmCallback != null) { 48 | TextButton( 49 | onClick = { onConfirmCallback() }, 50 | shape = RoundedCornerShape(8.dp), 51 | modifier = Modifier.semantics { testTag = TestTags.CONFIRM_BUTTON } 52 | ) { 53 | Text( 54 | text = stringResource(R.string.confirm), 55 | fontSize = 16.sp, 56 | fontWeight = FontWeight.SemiBold 57 | ) 58 | } 59 | } 60 | }, 61 | dismissButton = { 62 | TextButton( 63 | onClick = { onDismissCallback() }, 64 | shape = RoundedCornerShape(8.dp), 65 | modifier = Modifier.semantics { testTag = TestTags.DISMISS_BUTTON } 66 | ) { 67 | Text( 68 | text = stringResource(R.string.dismiss), 69 | fontSize = 16.sp, 70 | fontWeight = FontWeight.SemiBold 71 | ) 72 | } 73 | }, 74 | shape = RoundedCornerShape(8.dp), 75 | modifier = modifier 76 | ) 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/components/navbar/TopNavBarIcon.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.components.navbar 2 | 3 | import androidx.compose.material3.Icon 4 | import androidx.compose.material3.IconButton 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.graphics.vector.ImageVector 10 | 11 | @Composable 12 | fun TopNavBarIcon( 13 | image: ImageVector, 14 | description: String, 15 | modifier: Modifier, 16 | tint: Color = MaterialTheme.colorScheme.primary, 17 | onClick: () -> Unit, 18 | ) { 19 | IconButton(onClick = { onClick() }, modifier = modifier) { 20 | Icon( 21 | imageVector = image, 22 | contentDescription = description, 23 | tint = tint, 24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/config/DriveAPI.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.config 2 | 3 | import android.content.Context 4 | import com.digiventure.ventnote.R 5 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount 6 | import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential 7 | import com.google.api.client.http.javanet.NetHttpTransport 8 | import com.google.api.client.json.gson.GsonFactory 9 | import com.google.api.services.drive.Drive 10 | import com.google.api.services.drive.DriveScopes 11 | 12 | class DriveAPI { 13 | companion object { 14 | private var instance: Drive? = null 15 | 16 | fun getInstance(context: Context, signInAccount: GoogleSignInAccount): Drive { 17 | return instance ?: synchronized(this) { 18 | instance ?: createDriveInstance(context, signInAccount).also { instance = it } 19 | } 20 | } 21 | 22 | private fun createDriveInstance(context: Context, signInAccount: GoogleSignInAccount): Drive { 23 | val scopes = listOf(DriveScopes.DRIVE_APPDATA) 24 | val credential = GoogleAccountCredential.usingOAuth2(context, scopes) 25 | credential.selectedAccount = signInAccount.account 26 | 27 | return Drive.Builder( 28 | NetHttpTransport(), 29 | GsonFactory(), 30 | credential 31 | ).apply { 32 | applicationName = context.getString(R.string.app_name) 33 | }.build() 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/config/NoteDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.config 2 | 3 | import android.content.Context 4 | import androidx.room.AutoMigration 5 | import androidx.room.Database 6 | import androidx.room.Room 7 | import androidx.room.RoomDatabase 8 | import androidx.room.TypeConverter 9 | import androidx.room.TypeConverters 10 | import com.digiventure.ventnote.commons.Constants 11 | import com.digiventure.ventnote.data.persistence.NoteDAO 12 | import com.digiventure.ventnote.data.persistence.NoteModel 13 | import java.util.Date 14 | 15 | object DateConverters { 16 | @TypeConverter 17 | fun fromTimestamp(value: Long?): Date? { 18 | return if (value == null) null else Date(value) 19 | } 20 | 21 | @TypeConverter 22 | fun dateToTimestamp(date: Date?): Long? { 23 | return date?.time 24 | } 25 | } 26 | 27 | @Database( 28 | entities = [NoteModel::class], 29 | version = 2, 30 | exportSchema = true, 31 | autoMigrations = [ 32 | AutoMigration (from = 1, to = 2) 33 | ] 34 | ) 35 | @TypeConverters(DateConverters::class) 36 | abstract class NoteDatabase: RoomDatabase() { 37 | abstract fun dao(): NoteDAO 38 | 39 | companion object{ 40 | @Volatile 41 | private var instance: NoteDatabase? = null 42 | 43 | fun getInstance(context : Context): NoteDatabase { 44 | if (instance == null) { 45 | synchronized(this) { 46 | instance = Room.databaseBuilder( 47 | context, 48 | NoteDatabase::class.java, 49 | Constants.BACKUP_FILE_NAME 50 | ).build() 51 | } 52 | } 53 | 54 | return instance!! 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/data/google_drive/GoogleDriveRepository.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.data.google_drive 2 | 3 | import com.digiventure.ventnote.commons.ErrorMessage 4 | import com.digiventure.ventnote.data.persistence.NoteModel 5 | import com.google.api.services.drive.Drive 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.catch 8 | import kotlinx.coroutines.flow.flow 9 | import javax.inject.Inject 10 | import com.google.api.services.drive.model.File as DriveFile 11 | 12 | class GoogleDriveRepository @Inject constructor( 13 | private val service: GoogleDriveService, 14 | ) { 15 | fun uploadDatabaseFile(notes: List, fileName: String, drive: Drive?): Flow> = flow { 16 | val result = service.uploadDatabaseFile(notes, fileName, drive) 17 | emit(result) 18 | }.catch { e -> 19 | emit(Result.failure(RuntimeException(ErrorMessage.FAILED_UPLOAD_DATABASE_FILE, e))) 20 | } 21 | 22 | fun restoreDatabaseFile(fileId: String, drive: Drive?): Flow> = flow { 23 | val result = service.readFile(fileId, drive) 24 | emit(result) 25 | }.catch { e -> 26 | emit(Result.failure(RuntimeException(ErrorMessage.FAILED_RESTORE_DATABASE_FILE, e))) 27 | } 28 | 29 | fun getBackupFileList(drive: Drive?): Flow>> = flow { 30 | val result = service.queryFiles(drive) 31 | val transformedResult = result.map { fileList -> 32 | fileList?.files?.toList() ?: emptyList() 33 | } 34 | emit(transformedResult) 35 | }.catch { e -> 36 | emit(Result.failure(RuntimeException(ErrorMessage.FAILED_GET_LIST_BACKUP_FILE, e))) 37 | } 38 | 39 | fun deleteFile(fileId: String, drive: Drive?): Flow> = flow { 40 | val result = service.deleteFile(fileId, drive) 41 | emit(result) 42 | }.catch { e -> 43 | emit(Result.failure(RuntimeException(ErrorMessage.FAILED_DELETE_DATABASE_FILE, e))) 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/data/google_drive/GoogleDriveService.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.data.google_drive 2 | 3 | import com.digiventure.ventnote.data.persistence.NoteModel 4 | import com.digiventure.ventnote.module.proxy.DatabaseProxy 5 | import com.google.api.client.http.ByteArrayContent 6 | import com.google.api.services.drive.Drive 7 | import com.google.api.services.drive.model.File 8 | import com.google.api.services.drive.model.FileList 9 | import com.google.gson.Gson 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.withContext 12 | import javax.inject.Inject 13 | 14 | class GoogleDriveService @Inject constructor( 15 | private val proxy: DatabaseProxy, 16 | ) { 17 | companion object { 18 | private const val FILE_MIME_TYPE = "application/json" 19 | private const val APP_DATA_FOLDER_SPACE = "appDataFolder" 20 | } 21 | 22 | /** 23 | * Uploads a database file to Google Drive as a JSON file. 24 | * 25 | * @param notes The list of all database content. 26 | * @param fileName The name of the file to be uploaded. 27 | * @param drive The Google Drive instance. 28 | * @return A `Result` object indicating success or failure. 29 | * On success, the `Result` will contain a success value (`Unit`). 30 | * On failure, the `Result` will contain an `Exception` describing the error. 31 | * @throws Exception If an error occurs during the upload. 32 | */ 33 | suspend fun uploadDatabaseFile(notes: List, fileName: String, drive: Drive?): Result = 34 | withContext(Dispatchers.IO) { 35 | return@withContext try { 36 | val metaData = getMetaData(fileName) 37 | metaData.parents = listOf(APP_DATA_FOLDER_SPACE) 38 | val jsonString = Gson().toJson(notes) 39 | val fileContent = ByteArrayContent(FILE_MIME_TYPE, jsonString.toByteArray()) 40 | val result = drive?.files()?.create(metaData, fileContent)?.execute() 41 | Result.success(result) 42 | } catch (e: Exception) { 43 | Result.failure(e) 44 | } 45 | } 46 | 47 | /** 48 | * Reads a JSON file from Google Drive and writes its contents to the database. 49 | * 50 | * @param fileId The ID of the file to be read from Google Drive. 51 | * @param drive The Google Drive instance. 52 | * @return A `Result` object indicating success or failure. 53 | * On success, the `Result` will contain a success value (`Unit`). 54 | * On failure, the `Result` will contain an `Exception` describing the error. 55 | * @throws Exception If an error occurs during the read operation. 56 | */ 57 | suspend fun readFile(fileId: String, drive: Drive?): Result = withContext(Dispatchers.IO) { 58 | return@withContext try { 59 | val jsonString = drive?.files()?.get(fileId)?.executeMediaAsInputStream()?.use { 60 | it.bufferedReader().use { reader -> reader.readText() } 61 | } 62 | 63 | val notes = jsonString?.let { 64 | Gson().fromJson(it, Array::class.java).toList() 65 | } ?: emptyList() 66 | 67 | proxy.dao().upsertNotesWithTimestamp(notes) 68 | Result.success(Unit) 69 | } catch (e: Exception) { 70 | Result.failure(e) 71 | } 72 | } 73 | 74 | /** 75 | * Queries files from Google Drive within the appDataFolder. 76 | * 77 | * @param drive The Google Drive instance. 78 | * @return A `Result` object containing a list of `DriveFile` objects on success, 79 | * or an `Exception` describing the error on failure. 80 | * @throws Exception If an error occurs during the query. 81 | */ 82 | suspend fun queryFiles(drive: Drive?): Result = withContext(Dispatchers.IO) { 83 | return@withContext try { 84 | val fileList = drive?.files()?.list()?.setSpaces(APP_DATA_FOLDER_SPACE)?.execute() 85 | Result.success(fileList) 86 | } catch (e: Exception) { 87 | Result.failure(e) 88 | } 89 | } 90 | 91 | /** 92 | * Deletes a file from Google Drive. 93 | * 94 | * @param fileId The ID of the file to be deleted. 95 | * @param drive The Google Drive instance. 96 | * @return A `Result` object indicating success or failure. 97 | * On success, the `Result` will contain a success value (`Unit`). 98 | * On failure, the `Result` will contain an `Exception` describing the error. 99 | * @throws Exception If an error occurs during the deletion. 100 | */ 101 | suspend fun deleteFile(fileId: String, drive: Drive?): Result = withContext(Dispatchers.IO) { 102 | return@withContext try { 103 | val test = drive?.files()?.delete(fileId)?.execute() 104 | Result.success(test) 105 | } catch (e: Exception) { 106 | Result.failure(e) 107 | } 108 | } 109 | 110 | /** 111 | * Creates and returns metadata for the given file name. 112 | * @param fileName The name of the file. 113 | * @return a File object with metadata. 114 | */ 115 | private fun getMetaData(fileName: String): File { 116 | return File().setMimeType(FILE_MIME_TYPE).setName(fileName) 117 | } 118 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/data/local/NoteDataStore.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.data.local 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.edit 7 | import androidx.datastore.preferences.core.stringPreferencesKey 8 | import androidx.datastore.preferences.preferencesDataStore 9 | import com.digiventure.ventnote.commons.Constants 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.map 12 | 13 | class NoteDataStore(private val context: Context) { 14 | companion object { 15 | private val Context.dataStore: DataStore by preferencesDataStore(Constants.GLOBAL_PREFERENCE) 16 | } 17 | 18 | fun getStringData(key: String): Flow { 19 | return context.dataStore.data.map { preferences -> 20 | preferences[getStringKey(key)] ?: "" 21 | } 22 | } 23 | 24 | suspend fun setStringData(key: String, data: String) { 25 | context.dataStore.edit { preferences -> 26 | preferences[getStringKey(key)] = data 27 | } 28 | } 29 | 30 | private fun getStringKey(key: String): Preferences.Key { 31 | return stringPreferencesKey(key) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/data/persistence/NoteDAO.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.data.persistence 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import androidx.room.Transaction 9 | import androidx.room.Update 10 | import kotlinx.coroutines.flow.Flow 11 | import java.util.Date 12 | 13 | @Dao 14 | interface NoteDAO { 15 | @Query("SELECT * FROM note_table ORDER BY " + 16 | " CASE WHEN :sortBy = 'title' AND :orderBy = 'ASC' THEN title END ASC, " + 17 | " CASE WHEN :sortBy = 'title' AND :orderBy = 'DESC' THEN title END DESC, " + 18 | " CASE WHEN :sortBy = 'created_at' AND :orderBy = 'ASC' THEN created_at END ASC, " + 19 | " CASE WHEN :sortBy = 'created_at' AND :orderBy = 'DESC' THEN created_at END DESC, " + 20 | " CASE WHEN :sortBy = 'updated_at' AND :orderBy = 'ASC' THEN updated_at END ASC," + 21 | " CASE WHEN :sortBy = 'updated_at' AND :orderBy = 'DESC' THEN updated_at END DESC") 22 | fun getNotes(sortBy: String, orderBy: String): Flow> 23 | 24 | @Query("SELECT * FROM note_table WHERE id = :id") 25 | fun getNoteDetail(id: Int): Flow 26 | 27 | @Query("SELECT * FROM note_table WHERE id = :id") 28 | fun getPlainNoteDetail(id: Int): NoteModel 29 | 30 | @Update 31 | fun updateNote(note: NoteModel): Int 32 | 33 | fun updateWithTimestamp(note: NoteModel): Int { 34 | return updateNote(note.apply{ 35 | updatedAt = Date(System.currentTimeMillis()) 36 | }) 37 | } 38 | 39 | @Delete 40 | fun deleteNotes(vararg notes: NoteModel): Int 41 | 42 | @Insert(onConflict = OnConflictStrategy.REPLACE) 43 | fun insertNote(note: NoteModel): Long 44 | 45 | @Transaction 46 | fun insertWithTimestamp(note: NoteModel): Long { 47 | return insertNote(note.apply{ 48 | createdAt = Date(System.currentTimeMillis()) 49 | updatedAt = Date(System.currentTimeMillis()) 50 | }) 51 | } 52 | 53 | @Insert(onConflict = OnConflictStrategy.REPLACE) 54 | fun upsertNotes(notes: List) 55 | 56 | @Transaction 57 | fun upsertNotesWithTimestamp(notes: List) { 58 | val currentTimestamp = Date(System.currentTimeMillis()) 59 | notes.forEach { note -> 60 | note.createdAt = currentTimestamp 61 | note.updatedAt = currentTimestamp 62 | } 63 | upsertNotes(notes) 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/data/persistence/NoteLocalService.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.data.persistence 2 | 3 | import com.digiventure.ventnote.commons.ErrorMessage 4 | import com.digiventure.ventnote.module.proxy.DatabaseProxy 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.catch 7 | import kotlinx.coroutines.flow.flow 8 | import kotlinx.coroutines.flow.map 9 | import javax.inject.Inject 10 | 11 | class NoteLocalService @Inject constructor( 12 | private val proxy: DatabaseProxy 13 | ) { 14 | fun getNoteList(sortBy: String, order: String): Flow>> { 15 | return proxy.dao().getNotes(sortBy, order).map { 16 | Result.success(it) 17 | }.catch { 18 | emit(Result.failure(RuntimeException(ErrorMessage.FAILED_GET_NOTE_LIST_ROOM))) 19 | } 20 | } 21 | 22 | fun deleteNoteList(vararg notes: NoteModel): Flow> = 23 | flow { 24 | val result = (proxy.dao().deleteNotes(*notes) == notes.size) 25 | emit(Result.success(result)) 26 | }.catch { 27 | emit(Result.failure(RuntimeException(ErrorMessage.FAILED_DELETE_ROOM))) 28 | } 29 | 30 | fun getNoteDetail(id: Int): Flow> { 31 | return proxy.dao().getNoteDetail(id).map { 32 | Result.success(it) 33 | }.catch { 34 | emit(Result.failure(RuntimeException(ErrorMessage.FAILED_GET_NOTE_DETAIL_ROOM))) 35 | } 36 | } 37 | 38 | fun updateNoteList(note: NoteModel): Flow> = 39 | flow { 40 | val result = proxy.dao().updateWithTimestamp(note) >= 1 41 | emit(Result.success(result)) 42 | }.catch { 43 | emit(Result.failure(RuntimeException(ErrorMessage.FAILED_UPDATE_NOTE_ROOM))) 44 | } 45 | 46 | fun insertNote(note: NoteModel): Flow> = 47 | flow { 48 | val result = proxy.dao().insertWithTimestamp(note) != -1L 49 | emit(Result.success(result)) 50 | }.catch { 51 | emit(Result.failure(RuntimeException(ErrorMessage.FAILED_INSERT_NOTE_ROOM))) 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/data/persistence/NoteModel.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.data.persistence 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.PrimaryKey 7 | import kotlinx.parcelize.Parcelize 8 | import java.util.Date 9 | 10 | @Parcelize 11 | @Entity(tableName = "note_table") 12 | data class NoteModel( 13 | @PrimaryKey(autoGenerate = true) val id: Int, 14 | @ColumnInfo(name = "title") val title: String, 15 | @ColumnInfo(name = "note") val note: String, 16 | @ColumnInfo(name = "created_at") var createdAt: Date = Date(System.currentTimeMillis()), 17 | @ColumnInfo(name = "updated_at") var updatedAt: Date = Date(System.currentTimeMillis()), 18 | ): Parcelable { 19 | constructor(title: String, note: String) : this(0, title, note) 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/data/persistence/NoteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.data.persistence 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.map 5 | import javax.inject.Inject 6 | 7 | class NoteRepository @Inject constructor( 8 | private val service: NoteLocalService 9 | ) { 10 | fun getNoteList(sortBy: String, order: String): Flow>> = 11 | service.getNoteList(sortBy, order).map { 12 | if (it.isSuccess) { 13 | Result.success(it.getOrNull() ?: listOf()) 14 | } else { 15 | Result.failure(it.exceptionOrNull()!!) 16 | } 17 | } 18 | 19 | fun deleteNoteList(vararg notes: NoteModel): Flow> = 20 | service.deleteNoteList(*notes).map { 21 | if (it.isSuccess) { 22 | Result.success(it.getOrNull() ?: false) 23 | } else { 24 | Result.failure(it.exceptionOrNull()!!) 25 | } 26 | } 27 | 28 | fun getNoteDetail(id: Int): Flow> = 29 | service.getNoteDetail(id).map { 30 | if (it.isSuccess) { 31 | Result.success(it.getOrNull() ?: NoteModel(1, "", "")) 32 | } else { 33 | Result.failure(it.exceptionOrNull()!!) 34 | } 35 | } 36 | 37 | fun updateNoteList(note: NoteModel): Flow> = 38 | service.updateNoteList(note).map { 39 | if (it.isSuccess) { 40 | Result.success(it.getOrNull() ?: false) 41 | } else { 42 | Result.failure(it.exceptionOrNull()!!) 43 | } 44 | } 45 | 46 | fun insertNote(note: NoteModel): Flow> = 47 | service.insertNote(note).map { 48 | if (it.isSuccess) { 49 | Result.success(it.getOrNull() ?: false) 50 | } else { 51 | Result.failure(it.exceptionOrNull()!!) 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/backup/components/AppBar.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.backup.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 6 | import androidx.compose.material.icons.automirrored.filled.Logout 7 | import androidx.compose.material.icons.filled.CloudUpload 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.material3.TopAppBar 12 | import androidx.compose.material3.TopAppBarDefaults 13 | import androidx.compose.material3.TopAppBarScrollBehavior 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.semantics.semantics 18 | import androidx.compose.ui.text.TextStyle 19 | import androidx.compose.ui.text.font.FontWeight 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import com.digiventure.ventnote.R 23 | import com.digiventure.ventnote.components.navbar.TopNavBarIcon 24 | import com.digiventure.ventnote.feature.backup.viewmodel.AuthBaseVM 25 | import com.digiventure.ventnote.feature.backup.viewmodel.AuthVM 26 | 27 | @OptIn(ExperimentalMaterial3Api::class) 28 | @Composable 29 | fun BackupPageAppBar( 30 | authVM: AuthBaseVM, 31 | onBackPressed: () -> Unit, 32 | onLogoutPressed: () -> Unit, 33 | onBackupPressed: () -> Unit, 34 | scrollBehavior: TopAppBarScrollBehavior) { 35 | 36 | val authUiState = authVM.uiState.value 37 | 38 | TopAppBar( 39 | title = { 40 | Text( 41 | text = stringResource(id = R.string.backup_notes), 42 | color = MaterialTheme.colorScheme.primary, 43 | modifier = Modifier.padding(start = 4.dp), 44 | style = TextStyle( 45 | fontWeight = FontWeight.SemiBold, 46 | fontSize = 20.sp 47 | ) 48 | ) 49 | }, 50 | colors = TopAppBarDefaults.topAppBarColors( 51 | containerColor = MaterialTheme.colorScheme.surface, 52 | ), 53 | navigationIcon = { 54 | TopNavBarIcon( 55 | Icons.AutoMirrored.Filled.ArrowBack, 56 | stringResource(R.string.backup_nav_icon), 57 | Modifier.semantics { }) { 58 | onBackPressed() 59 | } 60 | }, 61 | scrollBehavior = scrollBehavior, 62 | modifier = Modifier.semantics { }, 63 | actions = { 64 | if (authUiState.authState == AuthVM.AuthState.SignedIn) { 65 | TrailingMenuIcons( 66 | onBackupPressed = onBackupPressed, 67 | onLogoutPressed = onLogoutPressed 68 | ) 69 | } 70 | } 71 | ) 72 | } 73 | 74 | @Composable 75 | fun TrailingMenuIcons( 76 | onLogoutPressed: () -> Unit, 77 | onBackupPressed: () -> Unit, 78 | ) { 79 | TopNavBarIcon( 80 | Icons.Filled.CloudUpload, 81 | stringResource(R.string.backup), 82 | modifier = Modifier.semantics { }) { 83 | onBackupPressed() 84 | } 85 | 86 | TopNavBarIcon( 87 | Icons.AutoMirrored.Filled.Logout, 88 | stringResource(R.string.logout_nav_icon), 89 | modifier = Modifier.semantics { }) { 90 | onLogoutPressed() 91 | } 92 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/backup/components/SignInButton.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.backup.components 2 | 3 | import android.app.Activity 4 | import android.widget.Toast 5 | import androidx.activity.compose.rememberLauncherForActivityResult 6 | import androidx.activity.result.contract.ActivityResultContracts 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.automirrored.filled.Login 12 | import androidx.compose.material3.Button 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import com.digiventure.ventnote.R 25 | import com.digiventure.ventnote.feature.backup.viewmodel.AuthBaseVM 26 | 27 | @Composable 28 | fun SignInButton(authViewModel: AuthBaseVM, signInSuccessCallback: () -> Unit) { 29 | val context = LocalContext.current 30 | val launcher = 31 | rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { 32 | if (it.resultCode == Activity.RESULT_OK) { 33 | authViewModel.checkAuthState() 34 | signInSuccessCallback() 35 | } else { 36 | val errorMessage = "Auth Failed" 37 | Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show() 38 | } 39 | } 40 | 41 | Button( 42 | onClick = { launcher.launch(authViewModel.getSignInIntent()) }, 43 | shape = RoundedCornerShape(10.dp) 44 | ) { 45 | Row(verticalAlignment = Alignment.CenterVertically) { 46 | Icon( 47 | imageVector = Icons.AutoMirrored.Filled.Login, 48 | contentDescription = "", 49 | tint = MaterialTheme.colorScheme.onPrimary, 50 | ) 51 | Text( 52 | text = stringResource(id = R.string.sign_in_with_google), 53 | fontSize = 16.sp, 54 | fontWeight = FontWeight.Medium, 55 | modifier = Modifier.padding(start = 10.dp) 56 | ) 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/AuthBaseVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.backup.viewmodel 2 | 3 | import android.content.Intent 4 | import androidx.compose.runtime.State 5 | import kotlinx.coroutines.flow.SharedFlow 6 | 7 | interface AuthBaseVM { 8 | /** 9 | * Determine auth state 10 | * */ 11 | val uiState: State 12 | /** 13 | * Emit auth state to ui 14 | * */ 15 | val eventFlow: SharedFlow 16 | 17 | /** 18 | * Sign out 19 | * */ 20 | fun signOut(onCompleteSignOutCallback: () -> Unit) 21 | /** 22 | * To prompt google sign in page manually 23 | * */ 24 | fun getSignInIntent(): Intent 25 | /** 26 | * To check whether user is already logged in or not 27 | * control ui state 28 | * */ 29 | fun checkAuthState() 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/AuthMockVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.backup.viewmodel 2 | 3 | import android.content.Intent 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.lifecycle.ViewModel 7 | import kotlinx.coroutines.flow.MutableSharedFlow 8 | import kotlinx.coroutines.flow.SharedFlow 9 | 10 | class AuthMockVM: ViewModel(), AuthBaseVM { 11 | override val uiState: State 12 | get() = mutableStateOf(AuthVM.AuthPageState(AuthVM.AuthState.SignedIn)) 13 | override val eventFlow: SharedFlow 14 | get() = MutableSharedFlow() 15 | 16 | override fun signOut(onCompleteSignOutCallback: () -> Unit) { 17 | 18 | } 19 | 20 | override fun getSignInIntent(): Intent { 21 | return Intent() 22 | } 23 | 24 | override fun checkAuthState() { 25 | 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/AuthVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.backup.viewmodel 2 | 3 | import android.app.Application 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.digiventure.ventnote.R 9 | import com.google.android.gms.auth.api.signin.GoogleSignIn 10 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount 11 | import com.google.android.gms.auth.api.signin.GoogleSignInClient 12 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions 13 | import com.google.android.gms.common.api.Scope 14 | import com.google.api.services.drive.DriveScopes 15 | import dagger.hilt.android.lifecycle.HiltViewModel 16 | import kotlinx.coroutines.flow.MutableSharedFlow 17 | import kotlinx.coroutines.flow.asSharedFlow 18 | import kotlinx.coroutines.launch 19 | import javax.inject.Inject 20 | 21 | @HiltViewModel 22 | class AuthVM @Inject constructor( 23 | private val app: Application, 24 | ) : ViewModel(), AuthBaseVM { 25 | 26 | private val _uiState = mutableStateOf(AuthPageState()) 27 | override val uiState: State = _uiState 28 | 29 | private val _eventFlow = MutableSharedFlow() 30 | override val eventFlow = _eventFlow.asSharedFlow() 31 | 32 | private var googleSignInClient: GoogleSignInClient 33 | 34 | init { 35 | googleSignInClient = 36 | GoogleSignIn.getClient(app.applicationContext, getGoogleSignInOptions()) 37 | checkAuthState() 38 | } 39 | 40 | override fun signOut(onCompleteSignOutCallback: () -> Unit) { 41 | googleSignInClient.signOut() 42 | .addOnCompleteListener { 43 | checkAuthState() 44 | onCompleteSignOutCallback() 45 | } 46 | } 47 | 48 | override fun getSignInIntent() = googleSignInClient.signInIntent 49 | 50 | override fun checkAuthState() { 51 | val lastUser = getLastSignedUser() 52 | val authState = if (lastUser == null) AuthState.SignedOut else AuthState.SignedIn 53 | _uiState.value = AuthPageState(authState) 54 | viewModelScope.launch { 55 | _eventFlow.emit(authState) 56 | } 57 | } 58 | 59 | private fun getLastSignedUser(): GoogleSignInAccount? { 60 | return GoogleSignIn.getLastSignedInAccount(app.applicationContext) 61 | } 62 | 63 | private fun getGoogleSignInOptions(): GoogleSignInOptions { 64 | val scopeDriveAppFolder = Scope(DriveScopes.DRIVE_APPDATA) 65 | val idToken = app.getString(R.string.web_client_id) 66 | return GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) 67 | .requestEmail() 68 | .requestIdToken(idToken) 69 | .requestId() 70 | .requestScopes(scopeDriveAppFolder) 71 | .build() 72 | } 73 | 74 | data class AuthPageState( 75 | var authState: AuthState = AuthState.Loading 76 | ) 77 | 78 | sealed interface AuthState { 79 | object Loading : AuthState 80 | object SignedOut : AuthState 81 | object SignedIn : AuthState 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageBaseVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.backup.viewmodel 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.lifecycle.LiveData 5 | import com.google.api.services.drive.model.File 6 | 7 | interface BackupPageBaseVM { 8 | val uiState: State 9 | 10 | val driveBackupFileList: LiveData> 11 | 12 | fun backupDatabase() 13 | 14 | fun restoreDatabase(fileId: String) 15 | 16 | fun getBackupFileList() 17 | 18 | fun deleteDatabase(fileId: String) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageMockVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.backup.viewmodel 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MutableLiveData 7 | import androidx.lifecycle.ViewModel 8 | import com.google.api.services.drive.model.File 9 | 10 | class BackupPageMockVM: ViewModel(), BackupPageBaseVM { 11 | private val _uiState = mutableStateOf(BackupPageVM.BackupPageState()) 12 | override val uiState: State = _uiState 13 | 14 | private val dummyGoogleDriveFileOne: File = File() 15 | .setName("dummy_file_2024-20-10.db") 16 | private val dummyGoogleDriveFileTwo: File = File() 17 | .setName("dummy_file_29-90-129201.db") 18 | 19 | private val _driveBackupFileList = MutableLiveData( 20 | listOf( 21 | dummyGoogleDriveFileOne, 22 | dummyGoogleDriveFileTwo 23 | ) 24 | ) 25 | override val driveBackupFileList: LiveData> = _driveBackupFileList 26 | 27 | init { 28 | _uiState.value = _uiState.value.copy(listOfBackupFileState = BackupPageVM.FileBackupListState.FileBackupListFailed("error")) 29 | } 30 | 31 | override fun backupDatabase() { 32 | 33 | } 34 | 35 | override fun restoreDatabase(fileId: String) { 36 | 37 | } 38 | 39 | override fun getBackupFileList() { 40 | 41 | } 42 | 43 | override fun deleteDatabase(fileId: String) { 44 | 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/backup/viewmodel/BackupPageVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.backup.viewmodel 2 | 3 | import android.app.Application 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.lifecycle.LiveData 7 | import androidx.lifecycle.MutableLiveData 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.viewModelScope 10 | import com.digiventure.ventnote.commons.Constants 11 | import com.digiventure.ventnote.config.DriveAPI 12 | import com.digiventure.ventnote.data.google_drive.GoogleDriveRepository 13 | import com.digiventure.ventnote.data.persistence.NoteRepository 14 | import com.google.android.gms.auth.api.signin.GoogleSignIn 15 | import com.google.api.services.drive.Drive 16 | import com.google.api.services.drive.model.File 17 | import dagger.hilt.android.lifecycle.HiltViewModel 18 | import kotlinx.coroutines.flow.last 19 | import kotlinx.coroutines.flow.onEach 20 | import kotlinx.coroutines.launch 21 | import java.text.SimpleDateFormat 22 | import java.util.Calendar 23 | import java.util.Locale 24 | import javax.inject.Inject 25 | 26 | @HiltViewModel 27 | class BackupPageVM @Inject constructor( 28 | private val app: Application, 29 | private val repository: GoogleDriveRepository, 30 | private val databaseRepository: NoteRepository 31 | ): ViewModel(), BackupPageBaseVM { 32 | private val _uiState = mutableStateOf(BackupPageState()) 33 | override val uiState: State = _uiState 34 | 35 | private val _driveBackupFileList = MutableLiveData>() 36 | override val driveBackupFileList: LiveData> = _driveBackupFileList 37 | 38 | override fun backupDatabase() { 39 | viewModelScope.launch { 40 | val currentState = _uiState.value.copy(fileBackupState = FileBackupState.SyncStarted) 41 | _uiState.value = currentState 42 | 43 | try { 44 | val drive = getDriveInstance() 45 | databaseRepository.getNoteList(Constants.UPDATED_AT, Constants.DESCENDING) 46 | .collect { 47 | repository.uploadDatabaseFile( 48 | it.getOrDefault(listOf()), 49 | getDatabaseNameWithTimestamps(), 50 | drive 51 | ).onEach { 52 | _uiState.value = currentState.copy(fileBackupState = FileBackupState.SyncFinished) 53 | getBackupFileList() 54 | }.last() 55 | } 56 | } catch (e: Exception) { 57 | val errorMessage = e.message ?: Constants.EMPTY_STRING 58 | _uiState.value = currentState.copy(fileBackupState = FileBackupState.SyncFailed(errorMessage)) 59 | } 60 | } 61 | } 62 | 63 | override fun restoreDatabase(fileId: String) { 64 | viewModelScope.launch { 65 | val currentState = _uiState.value.copy(fileRestoreState = FileRestoreState.SyncStarted) 66 | _uiState.value = currentState 67 | 68 | try { 69 | val drive = getDriveInstance() 70 | repository.restoreDatabaseFile(fileId, drive) 71 | .onEach { 72 | _uiState.value = currentState.copy(fileRestoreState = FileRestoreState.SyncFinished) 73 | }.last() 74 | } catch (e: Exception) { 75 | val errorMessage = e.message ?: Constants.EMPTY_STRING 76 | _uiState.value = currentState.copy(fileRestoreState = FileRestoreState.SyncFailed(errorMessage)) 77 | } 78 | } 79 | } 80 | 81 | override fun getBackupFileList() { 82 | viewModelScope.launch { 83 | val currentState = _uiState.value.copy(listOfBackupFileState = FileBackupListState.FileBackupListStarted) 84 | _uiState.value = currentState 85 | 86 | try { 87 | val drive = getDriveInstance() 88 | repository.getBackupFileList(drive).collect { result -> 89 | _uiState.value = currentState.copy(listOfBackupFileState = FileBackupListState.FileBackupListFinished) 90 | if (result.isSuccess) { 91 | val files = result.getOrNull() 92 | _driveBackupFileList.value = files ?: emptyList() 93 | } else { 94 | val errorMessage = result.exceptionOrNull()?.message ?: Constants.EMPTY_STRING 95 | _uiState.value = currentState.copy( 96 | listOfBackupFileState = FileBackupListState.FileBackupListFailed( 97 | errorMessage 98 | )) 99 | } 100 | } 101 | } catch (e: Exception) { 102 | val errorMessage = e.message ?: Constants.EMPTY_STRING 103 | _uiState.value = currentState.copy( 104 | listOfBackupFileState = FileBackupListState.FileBackupListFailed( 105 | errorMessage 106 | )) 107 | } 108 | } 109 | } 110 | 111 | override fun deleteDatabase(fileId: String) { 112 | viewModelScope.launch { 113 | val currentState = _uiState.value.copy(fileDeleteState = FileDeleteState.SyncStarted) 114 | _uiState.value = currentState 115 | 116 | try { 117 | val drive = getDriveInstance() 118 | repository.deleteFile(fileId, drive) 119 | .onEach { 120 | _uiState.value = currentState.copy(fileDeleteState = FileDeleteState.SyncFinished) 121 | }.last() 122 | } catch (e: Exception) { 123 | val errorMessage = e.message ?: Constants.EMPTY_STRING 124 | _uiState.value = currentState.copy(fileDeleteState = FileDeleteState.SyncFailed(errorMessage)) 125 | } 126 | } 127 | } 128 | 129 | private fun getDriveInstance(): Drive? { 130 | return GoogleSignIn.getLastSignedInAccount(app.applicationContext)?.run { 131 | DriveAPI.getInstance(app.applicationContext, this) 132 | } 133 | } 134 | 135 | sealed class FileBackupState { 136 | object SyncInitial : FileBackupState() 137 | object SyncStarted : FileBackupState() 138 | object SyncFinished : FileBackupState() 139 | data class SyncFailed(val errorMessage: String) : FileBackupState() 140 | } 141 | 142 | sealed class FileRestoreState { 143 | object SyncInitial : FileRestoreState() 144 | object SyncStarted : FileRestoreState() 145 | object SyncFinished : FileRestoreState() 146 | data class SyncFailed(val errorMessage: String) : FileRestoreState() 147 | } 148 | 149 | sealed class FileDeleteState { 150 | object SyncInitial : FileDeleteState() 151 | object SyncStarted : FileDeleteState() 152 | object SyncFinished : FileDeleteState() 153 | data class SyncFailed(val errorMessage: String) : FileDeleteState() 154 | } 155 | 156 | sealed class FileBackupListState { 157 | object FileBackupListStarted : FileBackupListState() 158 | object FileBackupListFinished : FileBackupListState() 159 | data class FileBackupListFailed(val errorMessage: String) : FileBackupListState() 160 | } 161 | 162 | data class BackupPageState( 163 | var listOfBackupFileState: FileBackupListState = FileBackupListState.FileBackupListFinished, 164 | var fileBackupState: FileBackupState = FileBackupState.SyncInitial, 165 | var fileRestoreState: FileRestoreState = FileRestoreState.SyncInitial, 166 | var fileDeleteState: FileDeleteState = FileDeleteState.SyncInitial 167 | ) 168 | 169 | private fun getDatabaseNameWithTimestamps(): String { 170 | val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) 171 | val timestamp = dateFormat.format(Calendar.getInstance().time) 172 | val jsonFormat = ".json" 173 | return Constants.BACKUP_FILE_NAME + "_" + timestamp + jsonFormat 174 | } 175 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/note_creation/components/AppBar.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.note_creation.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.material3.TopAppBar 10 | import androidx.compose.material3.TopAppBarDefaults 11 | import androidx.compose.material3.TopAppBarScrollBehavior 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.semantics.semantics 16 | import androidx.compose.ui.text.TextStyle 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import com.digiventure.ventnote.R 21 | import com.digiventure.ventnote.components.navbar.TopNavBarIcon 22 | 23 | @OptIn(ExperimentalMaterial3Api::class) 24 | @Composable 25 | fun NoteCreationAppBar( 26 | descriptionTextLength: Int, 27 | onBackPressed: () -> Unit, 28 | scrollBehavior: TopAppBarScrollBehavior) { 29 | TopAppBar( 30 | title = { 31 | Text( 32 | text = if(descriptionTextLength > 0) "$descriptionTextLength" else stringResource(id = R.string.add_new_note), 33 | color = MaterialTheme.colorScheme.primary, 34 | modifier = Modifier.padding(start = 4.dp), 35 | style = TextStyle( 36 | fontWeight = FontWeight.SemiBold, 37 | fontSize = 20.sp 38 | ) 39 | ) 40 | }, 41 | colors = TopAppBarDefaults.topAppBarColors( 42 | containerColor = MaterialTheme.colorScheme.surface, 43 | ), 44 | navigationIcon = { 45 | TopNavBarIcon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back_nav_icon), Modifier.semantics { }) { 46 | onBackPressed() 47 | } 48 | }, 49 | scrollBehavior = scrollBehavior, 50 | modifier = Modifier.semantics { }, 51 | ) 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/note_creation/viewmodel/NoteCreationPageBaseVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.note_creation.viewmodel 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.lifecycle.MutableLiveData 5 | import com.digiventure.ventnote.data.persistence.NoteModel 6 | 7 | interface NoteCreationPageBaseVM { 8 | /** 9 | * Handling loading state 10 | * */ 11 | val loader: MutableLiveData 12 | 13 | /** 14 | * State for handling title & description TextField 15 | * */ 16 | val titleText: MutableState 17 | val descriptionText: MutableState 18 | 19 | /** 20 | * create note 21 | * @param note is a note model 22 | * */ 23 | suspend fun addNote(note: NoteModel): Result 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/note_creation/viewmodel/NoteCreationPageMockVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.note_creation.viewmodel 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import com.digiventure.ventnote.data.persistence.NoteModel 8 | 9 | class NoteCreationPageMockVM: ViewModel(), NoteCreationPageBaseVM { 10 | override val loader: MutableLiveData = MutableLiveData(false) 11 | override val titleText: MutableState = mutableStateOf("") 12 | override val descriptionText: MutableState = mutableStateOf("") 13 | 14 | override suspend fun addNote(note: NoteModel): Result { 15 | TODO("Not yet implemented") 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/note_creation/viewmodel/NoteCreationPageVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.note_creation.viewmodel 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import com.digiventure.ventnote.data.persistence.NoteModel 8 | import com.digiventure.ventnote.data.persistence.NoteRepository 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.flow.last 12 | import kotlinx.coroutines.flow.onEach 13 | import kotlinx.coroutines.withContext 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class NoteCreationPageVM @Inject constructor( 18 | private val repository: NoteRepository 19 | ): ViewModel(), NoteCreationPageBaseVM { 20 | override val loader: MutableLiveData = MutableLiveData() 21 | override val titleText: MutableState = mutableStateOf("") 22 | override val descriptionText: MutableState = mutableStateOf("") 23 | 24 | override suspend fun addNote(note: NoteModel): Result = withContext(Dispatchers.IO) { 25 | loader.postValue(true) 26 | try { 27 | repository.insertNote(note).onEach { 28 | loader.postValue(false) 29 | }.last() 30 | } catch (e: Exception) { 31 | loader.postValue(false) 32 | Result.failure(e) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/note_detail/components/AppBar.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.note_detail.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 6 | import androidx.compose.material.icons.filled.Close 7 | import androidx.compose.material.icons.filled.MoreVert 8 | import androidx.compose.material.icons.filled.Share 9 | import androidx.compose.material3.DropdownMenu 10 | import androidx.compose.material3.DropdownMenuItem 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.material3.TopAppBar 15 | import androidx.compose.material3.TopAppBarDefaults.topAppBarColors 16 | import androidx.compose.material3.TopAppBarScrollBehavior 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.res.stringResource 22 | import androidx.compose.ui.semantics.semantics 23 | import androidx.compose.ui.text.TextStyle 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.unit.DpOffset 26 | import androidx.compose.ui.unit.dp 27 | import androidx.compose.ui.unit.sp 28 | import com.digiventure.ventnote.R 29 | import com.digiventure.ventnote.components.navbar.TopNavBarIcon 30 | 31 | @OptIn(ExperimentalMaterial3Api::class) 32 | @Composable 33 | fun NoteDetailAppBar( 34 | isEditing: Boolean, 35 | descriptionTextLength: Int, 36 | onBackPressed: () -> Unit, 37 | onClosePressed: () -> Unit, 38 | onDeletePressed: () -> Unit, 39 | onSharePressed: () -> Unit, 40 | scrollBehavior: TopAppBarScrollBehavior) { 41 | 42 | val isMenuExpanded = remember { mutableStateOf(false) } 43 | 44 | TopAppBar( 45 | title = { 46 | Text( 47 | text = if(isEditing) "$descriptionTextLength" else stringResource(id = R.string.note_detail), 48 | color = MaterialTheme.colorScheme.primary, 49 | modifier = Modifier.padding(start = 4.dp), 50 | style = TextStyle( 51 | fontWeight = FontWeight.SemiBold, 52 | fontSize = 20.sp 53 | ) 54 | ) 55 | }, 56 | colors = topAppBarColors( 57 | containerColor = MaterialTheme.colorScheme.surface, 58 | ), 59 | navigationIcon = { 60 | if (isEditing) { 61 | TopNavBarIcon(Icons.Filled.Close, stringResource(R.string.back_nav_icon), Modifier.semantics { }) { 62 | onClosePressed() 63 | } 64 | } else { 65 | TopNavBarIcon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back_nav_icon), Modifier.semantics { }) { 66 | onBackPressed() 67 | } 68 | } 69 | }, 70 | actions = { 71 | TopNavBarIcon(Icons.Filled.Share, stringResource(R.string.share_nav_icon), Modifier.semantics { }) { 72 | onSharePressed() 73 | } 74 | TopNavBarIcon(Icons.Filled.MoreVert, stringResource(R.string.menu_nav_icon), Modifier.semantics { }) { 75 | isMenuExpanded.value = true 76 | } 77 | DropdownMenu( 78 | offset = DpOffset((10).dp, 0.dp), 79 | expanded = isMenuExpanded.value, 80 | onDismissRequest = { isMenuExpanded.value = false }) { 81 | DropdownMenuItem( 82 | text = { Text( 83 | text = "Delete Note", 84 | fontSize = 16.sp, 85 | modifier = Modifier.semantics { }) 86 | }, 87 | onClick = { 88 | onDeletePressed() 89 | isMenuExpanded.value = false 90 | }, 91 | ) 92 | } 93 | }, 94 | scrollBehavior = scrollBehavior, 95 | modifier = Modifier.semantics { }, 96 | ) 97 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/note_detail/viewmodel/NoteDetailPageBaseVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.note_detail.viewmodel 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.lifecycle.MutableLiveData 5 | import com.digiventure.ventnote.data.persistence.NoteModel 6 | 7 | interface NoteDetailPageBaseVM { 8 | /** 9 | * Handling loading state 10 | * */ 11 | val loader: MutableLiveData 12 | 13 | /** 14 | * Contain note detail 15 | * */ 16 | var noteDetail: MutableLiveData> 17 | 18 | /** 19 | * State for handling title & description TextField 20 | * */ 21 | var titleText: MutableState 22 | var descriptionText: MutableState 23 | 24 | /** 25 | * State for handling isEditing 26 | * */ 27 | var isEditing: MutableState 28 | 29 | /** 30 | * retrieve responsible note by it's id 31 | * @param id is a note id passed from NoteList, 32 | * */ 33 | suspend fun getNoteDetail(id: Int) 34 | 35 | /** 36 | * update single note 37 | * @param note is a note model 38 | * */ 39 | suspend fun updateNote(note: NoteModel): Result 40 | 41 | /** 42 | * delete NoteList 43 | * @param notes is a list of note 44 | */ 45 | suspend fun deleteNoteList(vararg notes: NoteModel): Result 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/note_detail/viewmodel/NoteDetailPageMockVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.note_detail.viewmodel 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import com.digiventure.ventnote.data.persistence.NoteModel 8 | 9 | 10 | class NoteDetailPageMockVM: ViewModel(), NoteDetailPageBaseVM { 11 | override val loader: MutableLiveData = MutableLiveData() 12 | override var noteDetail: MutableLiveData> = MutableLiveData() 13 | 14 | override var titleText: MutableState = mutableStateOf("This is sample title text") 15 | override var descriptionText: MutableState = mutableStateOf("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus pretium odio maximus tellus pellentesque, a dignissim massa commodo.\n") 16 | override var isEditing: MutableState = mutableStateOf(false) 17 | 18 | override suspend fun getNoteDetail(id: Int) {} 19 | override suspend fun updateNote(note: NoteModel): Result = Result.success(true) 20 | override suspend fun deleteNoteList(vararg notes: NoteModel): Result = Result.success(true) 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/note_detail/viewmodel/NoteDetailPageVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.note_detail.viewmodel 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import com.digiventure.ventnote.data.persistence.NoteModel 8 | import com.digiventure.ventnote.data.persistence.NoteRepository 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.flow.last 12 | import kotlinx.coroutines.flow.onEach 13 | import kotlinx.coroutines.withContext 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class NoteDetailPageVM @Inject constructor( 18 | private val repository: NoteRepository 19 | ): ViewModel(), NoteDetailPageBaseVM { 20 | override val loader: MutableLiveData = MutableLiveData() 21 | override var noteDetail: MutableLiveData> = MutableLiveData() 22 | 23 | override var titleText: MutableState = mutableStateOf("") 24 | override var descriptionText: MutableState = mutableStateOf("") 25 | 26 | override var isEditing: MutableState = mutableStateOf(false) 27 | 28 | override suspend fun getNoteDetail(id: Int) = withContext(Dispatchers.IO) { 29 | loader.postValue(true) 30 | repository.getNoteDetail(id) 31 | .onEach { loader.postValue(false) } 32 | .collect { 33 | noteDetail.postValue(it) 34 | } 35 | } 36 | 37 | override suspend fun updateNote(note: NoteModel): Result = withContext(Dispatchers.IO) { 38 | loader.postValue(true) 39 | try { 40 | repository.updateNoteList(note).onEach { 41 | loader.postValue(false) 42 | }.last() 43 | } catch (e: Exception) { 44 | loader.postValue(false) 45 | Result.failure(e) 46 | } 47 | } 48 | 49 | override suspend fun deleteNoteList(vararg notes: NoteModel): Result = withContext(Dispatchers.IO) { 50 | loader.postValue(true) 51 | try { 52 | repository.deleteNoteList(*notes).onEach { 53 | loader.postValue(false) 54 | }.last() 55 | } catch (e: Exception) { 56 | loader.postValue(false) 57 | Result.failure(e) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/notes/components/item/NoteItem.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.notes.components.item 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.combinedClickable 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.semantics.contentDescription 18 | import androidx.compose.ui.semantics.semantics 19 | import androidx.compose.ui.text.font.FontWeight 20 | import androidx.compose.ui.text.style.TextOverflow 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.unit.sp 23 | import com.digiventure.ventnote.commons.DateUtil 24 | import com.digiventure.ventnote.data.persistence.NoteModel 25 | 26 | @OptIn(ExperimentalFoundationApi::class) 27 | @Composable 28 | fun NotesItem( 29 | isMarking: Boolean, 30 | isMarked: Boolean, 31 | data: NoteModel, 32 | onClick: () -> Unit, 33 | onLongClick: () -> Unit, 34 | onCheckClick: () -> Unit) 35 | { 36 | val shape = RoundedCornerShape(12.dp) 37 | 38 | Box( 39 | modifier = Modifier 40 | .semantics { contentDescription = "Note item ${data.id}" } 41 | .combinedClickable( 42 | onClick = { if (isMarking) onCheckClick() else onClick() }, 43 | onLongClick = { onLongClick() } 44 | ) 45 | .clip(shape) 46 | .background(MaterialTheme.colorScheme.primary) 47 | ) { 48 | Box(modifier = Modifier.fillMaxSize() 49 | .padding(start = if(isMarked) 8.dp else 0.dp) 50 | .background(MaterialTheme.colorScheme.surface)) { 51 | 52 | Column( 53 | modifier = Modifier.fillMaxSize().padding(16.dp), 54 | horizontalAlignment = Alignment.Start 55 | ) { 56 | Text( 57 | text = data.title, 58 | maxLines = 1, 59 | overflow = TextOverflow.Ellipsis, 60 | fontWeight = FontWeight.Bold, 61 | fontSize = 16.sp, 62 | color = MaterialTheme.colorScheme.onSurface, 63 | modifier = Modifier.padding(bottom = 4.dp) 64 | ) 65 | Text( 66 | text = data.note, 67 | maxLines = 2, 68 | overflow = TextOverflow.Ellipsis, 69 | fontSize = 14.sp, 70 | color = MaterialTheme.colorScheme.onSurface, 71 | modifier = Modifier.padding(bottom = 1.dp) 72 | ) 73 | Text( 74 | text = DateUtil.convertDateString("EEEE, MMMM d h:mm a", data.createdAt.toString()), 75 | maxLines = 1, 76 | overflow = TextOverflow.Ellipsis, 77 | fontSize = 14.sp, 78 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) 79 | ) 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/notes/components/sheets/FilterSheet.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.notes.components.sheets 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.selection.selectableGroup 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material3.Button 13 | import androidx.compose.material3.ExperimentalMaterial3Api 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.RadioButton 16 | import androidx.compose.material3.Scaffold 17 | import androidx.compose.material3.SheetState 18 | import androidx.compose.material3.Text 19 | import androidx.compose.material3.TextButton 20 | import androidx.compose.material3.rememberModalBottomSheetState 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.MutableState 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.runtime.saveable.rememberSaveable 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.res.stringResource 29 | import androidx.compose.ui.text.TextStyle 30 | import androidx.compose.ui.text.font.FontWeight 31 | import androidx.compose.ui.tooling.preview.Preview 32 | import androidx.compose.ui.unit.dp 33 | import androidx.compose.ui.unit.sp 34 | import com.digiventure.ventnote.R 35 | import com.digiventure.ventnote.commons.Constants 36 | import com.digiventure.ventnote.components.bottomSheet.RegularBottomSheet 37 | 38 | @OptIn(ExperimentalMaterial3Api::class) 39 | @Composable 40 | fun FilterSheet( 41 | openBottomSheet: MutableState, 42 | bottomSheetState: SheetState, 43 | onDismiss: () -> Unit, 44 | onFilter: (sortBy: String, orderBy: String) -> Unit 45 | ) { 46 | val createdDate = stringResource(id = R.string.sort_created_date) 47 | val title = stringResource(id = R.string.sort_title) 48 | val modifiedDate = stringResource(id = R.string.sort_modified_date) 49 | val sortByOptions = listOf(title, createdDate, modifiedDate) 50 | 51 | val ascending = stringResource(id = R.string.order_ascending) 52 | val descending = stringResource(id = R.string.order_descending) 53 | val orderByOptions = listOf(ascending, descending) 54 | 55 | val selectedSortBy = remember { mutableStateOf(createdDate) } 56 | val selectedOrderBy = remember { mutableStateOf(descending) } 57 | 58 | fun convertSortBy(sortBy: String): String { 59 | return when (sortBy) { 60 | title -> Constants.TITLE 61 | createdDate -> Constants.CREATED_AT 62 | modifiedDate -> Constants.UPDATED_AT 63 | else -> { 64 | Constants.CREATED_AT 65 | } 66 | } 67 | } 68 | 69 | fun convertOrderBy(orderBy: String): String { 70 | return when (orderBy) { 71 | ascending -> Constants.ASCENDING 72 | descending -> Constants.DESCENDING 73 | else -> { 74 | Constants.DESCENDING 75 | } 76 | } 77 | } 78 | 79 | RegularBottomSheet( 80 | isOpened = openBottomSheet.value, 81 | bottomSheetState = bottomSheetState, 82 | onDismissRequest = { openBottomSheet.value = false } 83 | ) { 84 | Column( 85 | modifier = Modifier 86 | .fillMaxWidth() 87 | .padding(16.dp) 88 | ) { 89 | Text( 90 | text = stringResource(id = R.string.sort_by), 91 | style = TextStyle( 92 | fontSize = 18.sp, fontWeight = FontWeight.Bold, 93 | color = MaterialTheme.colorScheme.primary 94 | ) 95 | ) 96 | Box(modifier = Modifier.padding(8.dp)) 97 | SortByList(sortByOptions = sortByOptions, selectedValue = selectedSortBy.value) { 98 | selectedSortBy.value = it 99 | } 100 | Box(modifier = Modifier.padding(16.dp)) 101 | Text( 102 | text = stringResource(id = R.string.order_by), 103 | style = TextStyle( 104 | fontSize = 18.sp, fontWeight = FontWeight.Bold, 105 | color = MaterialTheme.colorScheme.primary 106 | ) 107 | ) 108 | Box(modifier = Modifier.padding(8.dp)) 109 | OrderByList(orderByOptions = orderByOptions, selectedValue = selectedOrderBy.value) { 110 | selectedOrderBy.value = it 111 | } 112 | Box(modifier = Modifier.padding(16.dp)) 113 | Row { 114 | TextButton( 115 | onClick = { onDismiss() }, 116 | shape = RoundedCornerShape(20), 117 | modifier = Modifier.weight(1f) 118 | ) { 119 | Text(text = stringResource(id = R.string.dismiss)) 120 | } 121 | Button( 122 | onClick = { 123 | onFilter( 124 | convertSortBy(selectedSortBy.value), 125 | convertOrderBy(selectedOrderBy.value) 126 | ) 127 | onDismiss() 128 | }, 129 | shape = RoundedCornerShape(20), 130 | modifier = Modifier.weight(1f) 131 | ) { 132 | Text(text = stringResource(id = R.string.confirm)) 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | @Composable 140 | fun SortByList( 141 | sortByOptions: List, selectedValue: String, 142 | onPress: (sortByValue: String) -> Unit 143 | ) { 144 | Column(modifier = Modifier.selectableGroup()) { 145 | sortByOptions.forEach { 146 | ListItem(title = it, selectedValue = selectedValue) { 147 | onPress(it) 148 | } 149 | } 150 | } 151 | } 152 | 153 | @Composable 154 | fun OrderByList( 155 | orderByOptions: List, selectedValue: String, 156 | onPress: (orderByValue: String) -> Unit 157 | ) { 158 | Column(modifier = Modifier.selectableGroup()) { 159 | orderByOptions.forEach { 160 | ListItem(title = it, selectedValue = selectedValue) { 161 | onPress(it) 162 | } 163 | } 164 | } 165 | } 166 | 167 | @Composable 168 | fun ListItem(title: String, selectedValue: String, onPress: () -> Unit) { 169 | Row( 170 | verticalAlignment = Alignment.CenterVertically, 171 | modifier = Modifier.clickable( 172 | indication = null, 173 | interactionSource = remember { MutableInteractionSource() } 174 | ) { onPress() } 175 | ) { 176 | Text( 177 | text = title, 178 | style = TextStyle( 179 | fontSize = 16.sp, fontWeight = FontWeight.Medium, 180 | color = MaterialTheme.colorScheme.primary 181 | ), 182 | modifier = Modifier.weight(1f) 183 | ) 184 | RadioButton(selected = (title == selectedValue), onClick = { onPress() }) 185 | } 186 | } 187 | 188 | @OptIn(ExperimentalMaterial3Api::class) 189 | @Preview 190 | @Composable 191 | fun FilterSheetPreview() { 192 | val openBottomSheet = rememberSaveable { mutableStateOf(true) } 193 | val skipPartiallyExpanded = remember { mutableStateOf(false) } 194 | val bottomSheetState = rememberModalBottomSheetState( 195 | skipPartiallyExpanded = skipPartiallyExpanded.value 196 | ) 197 | 198 | Scaffold( 199 | content = { contentPadding -> 200 | Box(modifier = Modifier.padding(contentPadding)) {} 201 | } 202 | ) 203 | 204 | FilterSheet( 205 | openBottomSheet = openBottomSheet, 206 | bottomSheetState = bottomSheetState, 207 | onDismiss = {} 208 | ) { _, _ -> 209 | 210 | } 211 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageBaseVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.notes.viewmodel 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.snapshots.SnapshotStateList 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MutableLiveData 7 | import com.digiventure.ventnote.data.persistence.NoteModel 8 | 9 | interface NotesPageBaseVM { 10 | /** 11 | * Handle loading state 12 | * */ 13 | val loader: MutableLiveData 14 | 15 | /** 16 | * Handle sorting and order data 17 | * */ 18 | val sortAndOrderData: MutableLiveData> 19 | 20 | /** 21 | * Handle sorting and order 22 | * */ 23 | fun sortAndOrder(sortBy: String, orderBy: String) 24 | 25 | /** 26 | * Handle NoteList state 27 | * */ 28 | val noteList: LiveData>> 29 | 30 | /** 31 | * 1. Toggle search field 32 | * 2. SearchField value 33 | */ 34 | val isSearching: MutableState 35 | val searchedTitleText: MutableState 36 | 37 | /** 38 | * 1. Toggle marking action 39 | * 2. List of marked note 40 | * */ 41 | val isMarking: MutableState 42 | val markedNoteList: SnapshotStateList 43 | 44 | /** 45 | * Mark all note 46 | * @param notes is list of note that will be marked 47 | * */ 48 | fun markAllNote(notes: List) 49 | 50 | /** 51 | * Un-mark all note 52 | * */ 53 | fun unMarkAllNote() 54 | 55 | /** 56 | * Mark or Un-mark a note 57 | * */ 58 | fun addToMarkedNoteList(note: NoteModel) 59 | 60 | /** 61 | * Delete list of note 62 | * @param notes is vararg of note 63 | * */ 64 | suspend fun deleteNoteList(vararg notes: NoteModel): Result 65 | 66 | /** 67 | * Close marking event 68 | * */ 69 | fun closeMarkingEvent() 70 | 71 | /** 72 | * Close search event 73 | * */ 74 | fun closeSearchEvent() 75 | 76 | fun observeNotes() 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageMockVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.notes.viewmodel 2 | 3 | import androidx.compose.runtime.mutableStateListOf 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MutableLiveData 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.liveData 9 | import com.digiventure.ventnote.data.persistence.NoteModel 10 | 11 | class NotesPageMockVM : ViewModel(), NotesPageBaseVM { 12 | override val loader = MutableLiveData() 13 | override val sortAndOrderData: MutableLiveData> = MutableLiveData() 14 | 15 | override fun sortAndOrder(sortBy: String, orderBy: String) { 16 | 17 | } 18 | 19 | override val noteList: LiveData>> = liveData { 20 | Result.success( 21 | listOf( 22 | NoteModel("", ""), 23 | NoteModel("", ""), 24 | NoteModel("", ""), 25 | NoteModel("", ""), 26 | ) 27 | ) 28 | } 29 | 30 | override val isSearching = mutableStateOf(false) 31 | override val searchedTitleText = mutableStateOf("") 32 | 33 | override val isMarking = mutableStateOf(false) 34 | override val markedNoteList = mutableStateListOf() 35 | 36 | override fun markAllNote(notes: List) {} 37 | 38 | override fun unMarkAllNote() {} 39 | 40 | override fun addToMarkedNoteList(note: NoteModel) {} 41 | 42 | override suspend fun deleteNoteList(vararg notes: NoteModel): Result = Result.success(true) 43 | 44 | override fun closeMarkingEvent() { 45 | isMarking.value = false 46 | markedNoteList.clear() 47 | } 48 | 49 | override fun closeSearchEvent() { 50 | isSearching.value = false 51 | searchedTitleText.value = "" 52 | } 53 | 54 | override fun observeNotes() { 55 | 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/notes/viewmodel/NotesPageVM.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.notes.viewmodel 2 | 3 | import androidx.compose.runtime.mutableStateListOf 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MutableLiveData 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.asFlow 9 | import androidx.lifecycle.viewModelScope 10 | import com.digiventure.ventnote.commons.Constants 11 | import com.digiventure.ventnote.data.persistence.NoteModel 12 | import com.digiventure.ventnote.data.persistence.NoteRepository 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.flow.collectLatest 16 | import kotlinx.coroutines.flow.last 17 | import kotlinx.coroutines.flow.onEach 18 | import kotlinx.coroutines.launch 19 | import kotlinx.coroutines.withContext 20 | import javax.inject.Inject 21 | 22 | @HiltViewModel 23 | class NotesPageVM @Inject constructor( 24 | private val repository: NoteRepository, 25 | ): ViewModel(), NotesPageBaseVM { 26 | override val loader = MutableLiveData() 27 | override val sortAndOrderData: MutableLiveData> = MutableLiveData( 28 | Pair(Constants.UPDATED_AT, Constants.DESCENDING) 29 | ) 30 | 31 | val defaultException = Exception("Unknown error") 32 | 33 | override val noteList: LiveData>> 34 | get() = _noteList 35 | private val _noteList = MutableLiveData>>() 36 | 37 | override fun sortAndOrder(sortBy: String, orderBy: String) { 38 | sortAndOrderData.value = Pair(sortBy, orderBy) 39 | } 40 | 41 | override val isSearching = mutableStateOf(false) 42 | override val searchedTitleText = mutableStateOf("") 43 | 44 | override val isMarking = mutableStateOf(false) 45 | override val markedNoteList = mutableStateListOf() 46 | 47 | override fun markAllNote(notes: List) { 48 | markedNoteList.addAll(notes.minus((markedNoteList).toSet())) 49 | } 50 | 51 | override fun unMarkAllNote() { 52 | markedNoteList.clear() 53 | } 54 | 55 | override fun addToMarkedNoteList(note: NoteModel) { 56 | if (note in markedNoteList) { 57 | markedNoteList.remove(note) 58 | } else { 59 | markedNoteList.add(note) 60 | } 61 | } 62 | 63 | override suspend fun deleteNoteList(vararg notes: NoteModel): Result = 64 | withContext(Dispatchers.IO) { 65 | loader.postValue(true) 66 | try { 67 | val items: List = if (notes.isEmpty()) { markedNoteList } else { notes.toList() } 68 | repository.deleteNoteList(*items.toTypedArray()).onEach { 69 | loader.postValue(false) 70 | observeNotes() 71 | }.last() 72 | } catch (e: Exception) { 73 | loader.postValue(false) 74 | Result.failure(e) 75 | } 76 | } 77 | 78 | override fun closeMarkingEvent() { 79 | isMarking.value = false 80 | markedNoteList.clear() 81 | } 82 | 83 | override fun closeSearchEvent() { 84 | isSearching.value = false 85 | searchedTitleText.value = "" 86 | } 87 | 88 | override fun observeNotes() { 89 | viewModelScope.launch { 90 | sortAndOrderData.asFlow().collectLatest { 91 | loader.postValue(true) 92 | repository.getNoteList(it.first, it.second) 93 | .onEach { 94 | loader.postValue(false) 95 | } 96 | .collect { result -> 97 | if (result.isSuccess) { 98 | _noteList.postValue(result) 99 | } else { 100 | _noteList.postValue(Result.failure( 101 | result.exceptionOrNull() ?: defaultException 102 | )) 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/feature/share_preview/components/AppBar.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.feature.share_preview.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 6 | import androidx.compose.material.icons.automirrored.filled.Help 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.TopAppBar 11 | import androidx.compose.material3.TopAppBarDefaults 12 | import androidx.compose.material3.TopAppBarScrollBehavior 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.semantics.semantics 17 | import androidx.compose.ui.text.TextStyle 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import com.digiventure.ventnote.R 22 | import com.digiventure.ventnote.components.navbar.TopNavBarIcon 23 | 24 | @OptIn(ExperimentalMaterial3Api::class) 25 | @Composable 26 | fun SharePreviewAppBar( 27 | onBackPressed: () -> Unit, 28 | onHelpPressed: () -> Unit, 29 | scrollBehavior: TopAppBarScrollBehavior) { 30 | 31 | TopAppBar( 32 | title = { 33 | Text( 34 | text = stringResource(id = R.string.share_preview), 35 | color = MaterialTheme.colorScheme.primary, 36 | modifier = Modifier.padding(start = 4.dp), 37 | style = TextStyle( 38 | fontWeight = FontWeight.SemiBold, 39 | fontSize = 20.sp 40 | ) 41 | ) 42 | }, 43 | colors = TopAppBarDefaults.topAppBarColors( 44 | containerColor = MaterialTheme.colorScheme.surface, 45 | ), 46 | navigationIcon = { 47 | TopNavBarIcon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back_nav_icon), Modifier.semantics { }) { 48 | onBackPressed() 49 | } 50 | }, 51 | actions = { 52 | TopNavBarIcon(Icons.AutoMirrored.Filled.Help, stringResource(R.string.menu_nav_icon), Modifier.semantics { }) { 53 | onHelpPressed() 54 | } 55 | }, 56 | scrollBehavior = scrollBehavior, 57 | modifier = Modifier.semantics { }, 58 | ) 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/module/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.module 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.ExecutorCoroutineDispatcher 9 | import kotlinx.coroutines.SupervisorJob 10 | import kotlinx.coroutines.asCoroutineDispatcher 11 | import java.util.concurrent.Executors 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | class ApplicationModule { 17 | @Provides 18 | @Singleton 19 | fun provideCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob()) 20 | 21 | @Provides 22 | @Singleton 23 | fun provideExecutorCoroutineDispatcher(): ExecutorCoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/module/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.module 2 | 3 | import android.app.Application 4 | import com.digiventure.ventnote.module.proxy.DatabaseProxy 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | object DatabaseModule { 14 | @Singleton 15 | @Provides 16 | fun provideDatabaseProxy(application: Application) = DatabaseProxy(application) 17 | 18 | @Provides 19 | fun provideDatabase(proxy: DatabaseProxy) = proxy.getObject() 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/module/proxy/DatabaseProxy.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.module.proxy 2 | 3 | import android.app.Application 4 | import com.digiventure.ventnote.config.NoteDatabase 5 | import com.digiventure.ventnote.data.persistence.NoteDAO 6 | 7 | 8 | interface Provider { 9 | fun getObject() : T 10 | } 11 | 12 | class DatabaseProxy( 13 | private val application: Application 14 | ) : Provider { 15 | 16 | @Volatile 17 | private var database: NoteDatabase? = null 18 | 19 | @Synchronized 20 | override fun getObject(): NoteDatabase { 21 | if (database == null) { 22 | database = NoteDatabase.getInstance(application) 23 | } 24 | return database!! 25 | } 26 | 27 | fun dao(): NoteDAO { 28 | return getObject().dao() 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/navigation/NavGraph.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.navigation.NavHostController 5 | import androidx.navigation.NavType 6 | import androidx.navigation.compose.NavHost 7 | import androidx.navigation.compose.composable 8 | import androidx.navigation.navArgument 9 | import com.digiventure.ventnote.data.persistence.NoteModel 10 | import com.digiventure.ventnote.feature.backup.BackupPage 11 | import com.digiventure.ventnote.feature.note_creation.NoteCreationPage 12 | import com.digiventure.ventnote.feature.note_detail.NoteDetailPage 13 | import com.digiventure.ventnote.feature.notes.NotesPage 14 | import com.digiventure.ventnote.feature.share_preview.SharePreviewPage 15 | 16 | @Composable 17 | fun NavGraph(navHostController: NavHostController, openDrawer: () -> Unit) { 18 | NavHost( 19 | navController = navHostController, 20 | startDestination = Route.NotesPage.routeName, 21 | ) { 22 | composable(Route.NotesPage.routeName) { 23 | NotesPage(navHostController = navHostController, openDrawer = openDrawer) 24 | } 25 | composable( 26 | route = "${Route.NoteDetailPage.routeName}/{noteId}", 27 | arguments = listOf(navArgument("noteId") { 28 | type = NavType.StringType 29 | defaultValue = "" 30 | }) 31 | ) { 32 | NoteDetailPage(navHostController = navHostController, 33 | id = it.arguments?.getString("noteId") ?: "0") 34 | } 35 | composable(Route.NoteCreationPage.routeName) { 36 | NoteCreationPage(navHostController = navHostController) 37 | } 38 | composable( 39 | route = "${Route.SharePreviewPage.routeName}/{noteData}", 40 | arguments = listOf(navArgument("noteData") { 41 | type = NoteModelParamType() 42 | }) 43 | ) { 44 | val note = it.arguments?.getParcelable("noteData") 45 | SharePreviewPage(navHostController = navHostController, note = note) 46 | } 47 | composable(Route.BackupPage.routeName) { 48 | BackupPage(navHostController = navHostController) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/navigation/NoteModelParamType.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.navigation 2 | 3 | import android.os.Bundle 4 | import androidx.navigation.NavType 5 | import com.digiventure.ventnote.data.persistence.NoteModel 6 | import com.google.gson.Gson 7 | 8 | class NoteModelParamType : NavType(isNullableAllowed = false) { 9 | override fun get(bundle: Bundle, key: String): NoteModel? { 10 | return bundle.getParcelable(key) 11 | } 12 | 13 | override fun parseValue(value: String): NoteModel { 14 | return Gson().fromJson(value, NoteModel::class.java) 15 | } 16 | 17 | override fun put(bundle: Bundle, key: String, value: NoteModel) { 18 | bundle.putParcelable(key, value) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/navigation/PageNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.navigation 2 | 3 | import androidx.navigation.NavGraph.Companion.findStartDestination 4 | import androidx.navigation.NavHostController 5 | 6 | class PageNavigation(navController: NavHostController) { 7 | val navigateToBackupPage: () -> Unit = { 8 | navController.navigate(Route.BackupPage.routeName) { 9 | popUpTo(navController.graph.findStartDestination().id) { 10 | saveState = true 11 | } 12 | launchSingleTop = true 13 | restoreState = true 14 | } 15 | } 16 | val navigateToDetailPage: (noteId: Int) -> Unit = { noteId -> 17 | val routeName = "${Route.NoteDetailPage.routeName}/${noteId}" 18 | navController.navigate(routeName) { 19 | popUpTo(navController.graph.findStartDestination().id) { 20 | saveState = true 21 | } 22 | launchSingleTop = true 23 | restoreState = true 24 | } 25 | } 26 | val navigateToCreatePage: () -> Unit = { 27 | navController.navigate(Route.NoteCreationPage.routeName) { 28 | popUpTo(navController.graph.findStartDestination().id) { 29 | saveState = true 30 | } 31 | launchSingleTop = true 32 | restoreState = true 33 | } 34 | } 35 | val navigateToNotesPage: () -> Unit = { 36 | navController.navigate(Route.NotesPage.routeName) { 37 | popUpTo(navController.graph.findStartDestination().id) { 38 | inclusive = true 39 | } 40 | } 41 | } 42 | val navigateToSharePage: (noteJson: String) -> Unit = { noteJson -> 43 | val routeName = "${Route.SharePreviewPage.routeName}/${noteJson}" 44 | navController.navigate(routeName) { 45 | popUpTo(navController.graph.findStartDestination().id) { 46 | saveState = true 47 | } 48 | launchSingleTop = true 49 | restoreState = true 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/navigation/Route.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.navigation 2 | 3 | sealed class Route(val routeName: String) { 4 | object NotesPage: Route(routeName = "notes_page") 5 | object NoteDetailPage: Route(routeName = "note_detail_page") 6 | object NoteCreationPage: Route(routeName = "note_creation_page") 7 | object SharePreviewPage: Route(routeName = "share_preview_page") 8 | object BackupPage: Route(routeName = "backup_page") 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/ui/ColorSchemeChoice.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.ui 2 | 3 | import androidx.compose.material3.ColorScheme 4 | import androidx.compose.material3.darkColorScheme 5 | import androidx.compose.material3.lightColorScheme 6 | import com.digiventure.ventnote.commons.ColorPalletName 7 | import com.digiventure.ventnote.commons.ColorSchemeName 8 | import com.digiventure.ventnote.ui.theme.CadmiumGreenDarkPrimary 9 | import com.digiventure.ventnote.ui.theme.CadmiumGreenDarkSecondary 10 | import com.digiventure.ventnote.ui.theme.CadmiumGreenLightPrimary 11 | import com.digiventure.ventnote.ui.theme.CadmiumGreenLightSecondary 12 | import com.digiventure.ventnote.ui.theme.CobaltBlueDarkPrimary 13 | import com.digiventure.ventnote.ui.theme.CobaltBlueDarkSecondary 14 | import com.digiventure.ventnote.ui.theme.CobaltBlueLightPrimary 15 | import com.digiventure.ventnote.ui.theme.CobaltBlueLightSecondary 16 | import com.digiventure.ventnote.ui.theme.CrimsonDarkPrimary 17 | import com.digiventure.ventnote.ui.theme.CrimsonDarkSecondary 18 | import com.digiventure.ventnote.ui.theme.CrimsonLightPrimary 19 | import com.digiventure.ventnote.ui.theme.CrimsonLightSecondary 20 | import com.digiventure.ventnote.ui.theme.DarkBackground 21 | import com.digiventure.ventnote.ui.theme.DarkOnSurface 22 | import com.digiventure.ventnote.ui.theme.DarkTertiary 23 | import com.digiventure.ventnote.ui.theme.LightBackground 24 | import com.digiventure.ventnote.ui.theme.LightOnSurface 25 | import com.digiventure.ventnote.ui.theme.LightTertiary 26 | import com.digiventure.ventnote.ui.theme.PurpleDarkPrimary 27 | import com.digiventure.ventnote.ui.theme.PurpleDarkSecondary 28 | import com.digiventure.ventnote.ui.theme.PurpleLightPrimary 29 | import com.digiventure.ventnote.ui.theme.PurpleLightSecondary 30 | 31 | object ColorSchemeChoice { 32 | fun getColorScheme(colorScheme: String, colorPallet: String): ColorScheme { 33 | return when (colorScheme) { 34 | ColorSchemeName.DARK_MODE -> { 35 | when (colorPallet) { 36 | ColorPalletName.CRIMSON -> DarkCrimsonScheme 37 | ColorPalletName.CADMIUM_GREEN -> DarkCadmiumGreenScheme 38 | ColorPalletName.COBALT_BLUE -> DarkCobaltBlueScheme 39 | else -> DarkPurpleScheme 40 | } 41 | } 42 | else -> { 43 | when (colorPallet) { 44 | ColorPalletName.CRIMSON -> LightCrimsonScheme 45 | ColorPalletName.CADMIUM_GREEN -> LightCadmiumGreenScheme 46 | ColorPalletName.COBALT_BLUE -> LightCobaltBlueScheme 47 | else -> LightPurpleScheme 48 | } 49 | } 50 | } 51 | } 52 | 53 | private val DarkPurpleScheme = darkColorScheme( 54 | primary = PurpleDarkPrimary, 55 | secondary = PurpleDarkSecondary, 56 | tertiary = DarkTertiary, 57 | background = DarkBackground, 58 | surface = DarkTertiary, 59 | onPrimary = DarkTertiary, 60 | onSurface = DarkOnSurface 61 | ) 62 | 63 | private val LightPurpleScheme = lightColorScheme( 64 | primary = PurpleLightPrimary, 65 | secondary = PurpleLightSecondary, 66 | tertiary = LightTertiary, 67 | background = LightBackground, 68 | surface = LightTertiary, 69 | onPrimary = LightTertiary, 70 | onSurface = LightOnSurface 71 | ) 72 | 73 | private val DarkCrimsonScheme = darkColorScheme( 74 | primary = CrimsonDarkPrimary, 75 | secondary = CrimsonDarkSecondary, 76 | tertiary = DarkTertiary, 77 | background = DarkBackground, 78 | surface = DarkTertiary, 79 | onPrimary = DarkTertiary, 80 | onSurface = DarkOnSurface 81 | ) 82 | 83 | private val LightCrimsonScheme = lightColorScheme( 84 | primary = CrimsonLightPrimary, 85 | secondary = CrimsonLightSecondary, 86 | tertiary = LightTertiary, 87 | background = LightBackground, 88 | surface = LightTertiary, 89 | onPrimary = LightTertiary, 90 | onSurface = LightOnSurface 91 | ) 92 | 93 | private val DarkCadmiumGreenScheme = darkColorScheme( 94 | primary = CadmiumGreenDarkPrimary, 95 | secondary = CadmiumGreenDarkSecondary, 96 | tertiary = DarkTertiary, 97 | background = DarkBackground, 98 | surface = DarkTertiary, 99 | onPrimary = DarkTertiary, 100 | onSurface = DarkOnSurface 101 | ) 102 | 103 | private val LightCadmiumGreenScheme = lightColorScheme( 104 | primary = CadmiumGreenLightPrimary, 105 | secondary = CadmiumGreenLightSecondary, 106 | tertiary = LightTertiary, 107 | background = LightBackground, 108 | surface = LightTertiary, 109 | onPrimary = LightTertiary, 110 | onSurface = LightOnSurface 111 | ) 112 | 113 | private val DarkCobaltBlueScheme = darkColorScheme( 114 | primary = CobaltBlueDarkPrimary, 115 | secondary = CobaltBlueDarkSecondary, 116 | tertiary = DarkTertiary, 117 | background = DarkBackground, 118 | surface = DarkTertiary, 119 | onPrimary = DarkTertiary, 120 | onSurface = DarkOnSurface 121 | ) 122 | 123 | private val LightCobaltBlueScheme = lightColorScheme( 124 | primary = CobaltBlueLightPrimary, 125 | secondary = CobaltBlueLightSecondary, 126 | tertiary = LightTertiary, 127 | background = LightBackground, 128 | surface = LightTertiary, 129 | onPrimary = LightTertiary, 130 | onSurface = LightOnSurface 131 | ) 132 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val PurpleDarkPrimary = Color(0xFFD0BCFF) 6 | val PurpleDarkSecondary = Color(0xFFCCC2DC) 7 | 8 | val PurpleLightPrimary = Color(0xFF6650a4) 9 | val PurpleLightSecondary = Color(0xFF625b71) 10 | 11 | val CrimsonDarkPrimary = Color(0xFFffb3b3) 12 | val CrimsonDarkSecondary = Color(0xFFe6bdbc) 13 | 14 | val CrimsonLightPrimary = Color(0xFFbf0030) 15 | val CrimsonLightSecondary = Color(0xFF775656) 16 | 17 | val CadmiumGreenLightPrimary = Color(0xFF006b5c) 18 | val CadmiumGreenLightSecondary = Color(0xFF4a635d) 19 | 20 | val CadmiumGreenDarkPrimary = Color(0xFF58dbc3) 21 | val CadmiumGreenDarkSecondary = Color(0xFFb1ccc4) 22 | 23 | val CobaltBlueLightPrimary = Color(0xFF2559bd) 24 | val CobaltBlueLightSecondary = Color(0xFF585e71) 25 | 26 | val CobaltBlueDarkPrimary = Color(0xFFb1c5ff) 27 | val CobaltBlueDarkSecondary = Color(0xFFc0c6dc) 28 | 29 | val DarkTertiary = Color(0xFF202125) 30 | val DarkBackground = Color(0xFF131416) 31 | val DarkOnSurface = Color(0XFFb4b8ba) 32 | 33 | val LightTertiary = Color.White 34 | val LightBackground = Color(0xFFf2f5fa) 35 | val LightOnSurface = Color(0xFF484b51) -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.LaunchedEffect 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.rememberCoroutineScope 10 | import androidx.compose.ui.platform.LocalContext 11 | import com.digiventure.ventnote.commons.ColorPalletName 12 | import com.digiventure.ventnote.commons.ColorSchemeName 13 | import com.digiventure.ventnote.commons.Constants 14 | import com.digiventure.ventnote.data.local.NoteDataStore 15 | import com.digiventure.ventnote.ui.ColorSchemeChoice 16 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 17 | import kotlinx.coroutines.flow.combine 18 | import kotlinx.coroutines.launch 19 | 20 | @Composable 21 | fun VentNoteTheme( 22 | darkTheme: Boolean = isSystemInDarkTheme(), 23 | content: @Composable () -> Unit 24 | ) { 25 | val dataStore = NoteDataStore(LocalContext.current) 26 | 27 | val scope = rememberCoroutineScope() 28 | 29 | fun setColorPallet(colorPallet: String) { 30 | scope.launch { 31 | dataStore.setStringData(Constants.COLOR_PALLET, colorPallet) 32 | } 33 | } 34 | 35 | fun setColorScheme(colorScheme: String) { 36 | scope.launch { 37 | dataStore.setStringData(Constants.COLOR_SCHEME, colorScheme) 38 | } 39 | } 40 | 41 | val colorScheme = remember { 42 | val scheme = if (darkTheme) ColorSchemeName.DARK_MODE else 43 | ColorSchemeName.LIGHT_MODE 44 | mutableStateOf( 45 | ColorSchemeChoice.getColorScheme( 46 | scheme, 47 | ColorPalletName.PURPLE 48 | ) 49 | ) 50 | } 51 | 52 | LaunchedEffect(Unit) { 53 | val colorSchemeFlow = dataStore.getStringData(Constants.COLOR_SCHEME) 54 | val colorPalletFlow = dataStore.getStringData(Constants.COLOR_PALLET) 55 | 56 | val combinedFlow = combine( 57 | colorSchemeFlow, colorPalletFlow 58 | ) { scheme, pallet -> 59 | val defaultScheme = 60 | scheme.ifEmpty { 61 | if (darkTheme) ColorSchemeName.DARK_MODE 62 | else ColorSchemeName.LIGHT_MODE 63 | } 64 | val defaultPallet = pallet.ifEmpty { ColorPalletName.PURPLE } 65 | setColorScheme(defaultScheme) 66 | setColorPallet(defaultPallet) 67 | 68 | Pair(defaultScheme, defaultPallet) 69 | } 70 | 71 | combinedFlow.collect { 72 | colorScheme.value = ColorSchemeChoice.getColorScheme( 73 | it.first, 74 | it.second 75 | ) 76 | } 77 | } 78 | 79 | val systemUiController = rememberSystemUiController() 80 | systemUiController.setStatusBarColor( 81 | darkIcons = !darkTheme, color = colorScheme.value.primary 82 | ) 83 | 84 | MaterialTheme( 85 | colorScheme = colorScheme.value, typography = Typography, content = content 86 | ) 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/digiventure/ventnote/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.digiventure.ventnote.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ), 18 | // Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | ) -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | VentNote 3 | 4 | fab 5 | 6 | VentNote 7 | 8 | 9 | close nav icon 10 | drawer nav icon 11 | search nav icon 12 | sort nav icon 13 | delete nav icon 14 | logout nav icon 15 | backup nav icon 16 | dropdown nav icon 17 | back nav icon 18 | menu nav icon 19 | share nav icon 20 | 21 | 22 | Warning 23 | Are you sure want to delete these items (it cannot be recovered)? 24 | Required 25 | %1$s is required 26 | Are you sure 27 | The text change will not be saved 28 | Information 29 | If you press \"copy\" or \"share as text\", the text structure will remain the same, but the content will be in regular text format (with the title not in bold) 30 | Success 31 | The app is successfully updated, press confirm to restart 32 | Restore Notes 33 | Are you sure you want to restore this note? This will replace the existing note. 34 | 35 | 36 | Confirm 37 | Dismiss 38 | Select All 39 | Unselect All 40 | Save 41 | Edit 42 | Add 43 | Share Note 44 | Share Note as Text 45 | Sign In With Google 46 | Refresh 47 | 48 | 49 | Input title here 50 | Insert title 51 | Insert note 52 | 53 | 54 | Selected 55 | Note Detail 56 | Add New Note 57 | Share Preview 58 | Backup Notes 59 | Loading 60 | Sort By 61 | Title 62 | Modified Date 63 | Created Date 64 | Order By 65 | Ascending 66 | Descending 67 | 68 | 69 | about us 70 | preferences 71 | settings 72 | Rate app 73 | Rate us on the Play Store! 74 | More apps 75 | Visit our developer page 76 | Version 77 | theme color 78 | theme setting 79 | Backup notes 80 | Backup note to google drive 81 | switch to dark mode 82 | switch to light mode 83 | 84 | 85 | Note is successfully deleted 86 | Note is successfully backed up 87 | Note is successfully restored 88 | Note is successfully updated 89 | 90 | 91 | title textField 92 | body textField 93 | 94 | WEB_CLIENT_ID 95 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |