├── .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 | [](https://www.buymeacoffee.com/syubban)
41 |
42 |
43 |
44 | ## 🖨 Downloads
45 | [](./LICENSE)
46 | [](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 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/digiventure/utils/BaseUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.digiventure.utils
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import org.junit.Rule
5 | import org.junit.runner.RunWith
6 | import org.mockito.junit.MockitoJUnitRunner
7 |
8 | @RunWith(MockitoJUnitRunner::class)
9 | abstract class BaseUnitTest {
10 | @get:Rule
11 | var coroutinesTestRule = MainDispatcherRule()
12 |
13 | @get:Rule
14 | var instantTaskExecutor = InstantTaskExecutorRule()
15 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/digiventure/utils/LiveDataTestExtensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2019 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.digiventure.utils
18 |
19 | import androidx.lifecycle.LiveData
20 | import androidx.lifecycle.Observer
21 |
22 | /**
23 | * Represents a list of capture values from a LiveData.
24 | */
25 | class LiveDataValueCapture {
26 |
27 | private val lock = Any()
28 |
29 | private val _values = mutableListOf()
30 | val values: List
31 | get() = synchronized(lock) {
32 | _values.toList() // copy to avoid returning reference to mutable list
33 | }
34 |
35 | fun addValue(value: T?) = synchronized(lock) {
36 | _values += value
37 | }
38 | }
39 |
40 | /**
41 | * Extension function to capture all values that are emitted to a LiveData during the execution of
42 | * `captureBlock`.
43 | *
44 | * @param captureBlock a lambda that will
45 | */
46 | inline fun LiveData.captureValues(block: LiveDataValueCapture.() -> Unit) {
47 | val capture = LiveDataValueCapture()
48 | val observer = Observer {
49 | capture.addValue(it)
50 | }
51 | observeForever(observer)
52 | try {
53 | capture.block()
54 | } finally {
55 | removeObserver(observer)
56 | }
57 | }
58 |
59 | /**
60 | * Get the current value from a LiveData without needing to register an observer.
61 | */
62 | fun LiveData.getValueForTest(): T? {
63 | var value: T? = null
64 | val observer = Observer {
65 | value = it
66 | }
67 | observeForever(observer)
68 | removeObserver(observer)
69 | return value
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/test/java/com/digiventure/utils/MainDispatcherRule.kt:
--------------------------------------------------------------------------------
1 | package com.digiventure.utils
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.ExperimentalCoroutinesApi
5 | import kotlinx.coroutines.test.TestDispatcher
6 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
7 | import kotlinx.coroutines.test.resetMain
8 | import kotlinx.coroutines.test.setMain
9 | import org.junit.rules.TestWatcher
10 | import org.junit.runner.Description
11 |
12 | // Reusable JUnit4 TestRule to override the Main dispatcher
13 | @OptIn(ExperimentalCoroutinesApi::class)
14 | class MainDispatcherRule(
15 | private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
16 | ) : TestWatcher() {
17 | override fun starting(description: Description) {
18 | Dispatchers.setMain(testDispatcher)
19 | }
20 |
21 | override fun finished(description: Description) {
22 | Dispatchers.resetMain()
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/digiventure/ventnote/commons/DateUtilsTest.kt:
--------------------------------------------------------------------------------
1 | package com.digiventure.ventnote.commons
2 |
3 | import com.digiventure.utils.BaseUnitTest
4 | import org.junit.Assert.assertEquals
5 | import org.junit.Test
6 |
7 | class DateUtilsTest: BaseUnitTest() {
8 | @Test
9 | fun convertDateStringShouldReturnValidFormattedString() {
10 | val expectedDateString = "Thu, Jan 1"
11 |
12 | assertEquals(expectedDateString, DateUtil.convertDateString(
13 | "EEE, MMM d",
14 | "Thu Jan 01 07:00:00 GMT+07:00 1970"
15 | ))
16 | }
17 |
18 | @Test
19 | fun convertDateStringShouldReturnEmptyStringWhenError() {
20 | val expectedDateString = ""
21 |
22 | assertEquals(expectedDateString, DateUtil.convertDateString(
23 | "EEE, MMM d",
24 | "Thu Jn 01 07:00:00 GMT+07:00 1970"
25 | ))
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/digiventure/ventnote/data/google_drive/GoogleDriveRepositoryShould.kt:
--------------------------------------------------------------------------------
1 | package com.digiventure.ventnote.data.google_drive
2 |
3 | import com.digiventure.utils.BaseUnitTest
4 | import com.digiventure.ventnote.data.persistence.NoteModel
5 | import com.google.api.services.drive.Drive
6 | import com.google.api.services.drive.model.File
7 | import com.google.api.services.drive.model.FileList
8 | import junit.framework.Assert.assertEquals
9 | import kotlinx.coroutines.flow.first
10 | import kotlinx.coroutines.test.runTest
11 | import org.junit.Before
12 | import org.junit.Test
13 | import org.mockito.Mockito.mock
14 | import org.mockito.kotlin.times
15 | import org.mockito.kotlin.verify
16 | import org.mockito.kotlin.whenever
17 |
18 | class GoogleDriveRepositoryShould: BaseUnitTest() {
19 | private val service: GoogleDriveService = mock()
20 | private val noteList: List = listOf()
21 | private val fileName: String = "backup.json"
22 | private val fileId: String = "1"
23 | private val drive: Drive = mock()
24 | private val driveFileList: FileList = FileList()
25 | private val exception: Exception = RuntimeException("Failed to process")
26 |
27 | private lateinit var repository: GoogleDriveRepository
28 |
29 | @Before
30 | fun setup() {
31 | repository = GoogleDriveRepository(service)
32 | }
33 |
34 | @Test
35 | fun emitsResultSuccess_whenUploadDatabaseIsSuccess() = runTest {
36 | val file = File()
37 | whenever(service.uploadDatabaseFile(noteList, fileName, drive)).thenReturn(
38 | Result.success(file)
39 | )
40 |
41 | val actualResult = repository.uploadDatabaseFile(noteList, fileName, drive).first()
42 |
43 | verify(service, times(1)).uploadDatabaseFile(noteList, fileName, drive)
44 | assertEquals(Result.success(file), actualResult)
45 | }
46 |
47 | @Test
48 | fun emitsResultFailure_whenUploadDatabaseIsThrowingException() = runTest {
49 | whenever(service.uploadDatabaseFile(noteList, fileName, drive)).thenReturn(
50 | Result.failure(exception)
51 | )
52 |
53 | val actualResult = repository.uploadDatabaseFile(noteList, fileName, drive).first()
54 |
55 | verify(service, times(1)).uploadDatabaseFile(noteList, fileName, drive)
56 | val expectedResultMessage = Result.failure(exception).exceptionOrNull()?.message
57 | val actualResultMessage = actualResult.exceptionOrNull()?.message
58 | assertEquals(expectedResultMessage, actualResultMessage)
59 | }
60 |
61 | @Test
62 | fun emitsResultSuccess_whenRestoreDatabaseIsSuccess() = runTest {
63 | whenever(service.readFile(fileId, drive)).thenReturn(
64 | Result.success(Unit)
65 | )
66 |
67 | val actualResult = repository.restoreDatabaseFile(fileId, drive).first()
68 |
69 | verify(service, times(1)).readFile(fileId, drive)
70 | assertEquals(Result.success(Unit), actualResult)
71 | }
72 |
73 | @Test
74 | fun emitsResultFailure_whenRestoreDatabaseIsThrowingException() = runTest {
75 | whenever(service.readFile(fileId, drive)).thenReturn(
76 | Result.failure(exception)
77 | )
78 |
79 | val actualResult = repository.restoreDatabaseFile(fileId, drive).first()
80 |
81 | verify(service, times(1)).readFile(fileId, drive)
82 | val expectedResultMessage = Result.failure(exception).exceptionOrNull()?.message
83 | val actualResultMessage = actualResult.exceptionOrNull()?.message
84 | assertEquals(expectedResultMessage, actualResultMessage)
85 | }
86 |
87 | @Test
88 | fun emitsResultSuccessWithThreeListOfBackupFile_whenGetBackupFileListIsSuccess() = runTest {
89 | driveFileList.setFiles(listOf(File(), File(), File()))
90 | whenever(service.queryFiles(drive)).thenReturn(
91 | Result.success(driveFileList)
92 | )
93 |
94 | val actualResult = repository.getBackupFileList(drive).first()
95 |
96 | verify(service, times(1)).queryFiles(drive)
97 | assertEquals(3, actualResult.getOrNull()?.size)
98 | }
99 |
100 | @Test
101 | fun emitsResultFailure_whenGetBackupFileListIsThrowingException() = runTest {
102 | whenever(service.queryFiles(drive)).thenReturn(
103 | Result.failure(exception)
104 | )
105 |
106 | val actualResult = repository.getBackupFileList(drive).first()
107 |
108 | verify(service, times(1)).queryFiles(drive)
109 | val expectedResultMessage = Result.failure(exception).exceptionOrNull()?.message
110 | val actualResultMessage = actualResult.exceptionOrNull()?.message
111 | assertEquals(expectedResultMessage, actualResultMessage)
112 | }
113 |
114 | @Test
115 | fun emitsResultSuccess_whenDeleteFileIsSuccess() = runTest {
116 | whenever(service.deleteFile(fileId, drive)).thenReturn(
117 | Result.success(null)
118 | )
119 |
120 | val actualResult = repository.deleteFile(fileId, drive).first()
121 |
122 | verify(service, times(1)).deleteFile(fileId, drive)
123 | assertEquals(Result.success(null), actualResult)
124 | }
125 |
126 | @Test
127 | fun emitsResultFailure_whenDeleteFileIsThrowingException() = runTest {
128 | whenever(service.deleteFile(fileId, drive)).thenReturn(
129 | Result.failure(exception)
130 | )
131 |
132 | val actualResult = repository.deleteFile(fileId, drive).first()
133 |
134 | verify(service, times(1)).deleteFile(fileId, drive)
135 | val expectedResultMessage = Result.failure(exception).exceptionOrNull()?.message
136 | val actualResultMessage = actualResult.exceptionOrNull()?.message
137 | assertEquals(expectedResultMessage, actualResultMessage)
138 | }
139 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/digiventure/ventnote/data/google_drive/GoogleDriveServiceShould.kt:
--------------------------------------------------------------------------------
1 | package com.digiventure.ventnote.data.google_drive
2 |
3 | import com.digiventure.utils.BaseUnitTest
4 | import com.digiventure.ventnote.data.persistence.NoteDAO
5 | import com.digiventure.ventnote.data.persistence.NoteModel
6 | import com.digiventure.ventnote.module.proxy.DatabaseProxy
7 | import com.google.api.services.drive.Drive
8 | import com.google.api.services.drive.model.File
9 | import com.google.api.services.drive.model.FileList
10 | import junit.framework.Assert.assertEquals
11 | import junit.framework.Assert.assertTrue
12 | import kotlinx.coroutines.test.runTest
13 | import okio.IOException
14 | import org.junit.Before
15 | import org.junit.Test
16 | import org.mockito.kotlin.any
17 | import org.mockito.kotlin.mock
18 | import org.mockito.kotlin.times
19 | import org.mockito.kotlin.verify
20 | import org.mockito.kotlin.whenever
21 |
22 | class GoogleDriveServiceShould: BaseUnitTest() {
23 | private val proxy: DatabaseProxy = mock()
24 | private val dao: NoteDAO = mock()
25 | private val noteList: List = listOf()
26 | private val fileName: String = "backup.json"
27 | private val fileId: String = "1"
28 | private val drive: Drive = mock()
29 |
30 | private lateinit var service: GoogleDriveService
31 |
32 | @Before
33 | fun setup() {
34 | service = GoogleDriveService(proxy)
35 | }
36 |
37 | @Test
38 | fun returnResultSuccess_whenUploadProcessIsSuccess() = runTest {
39 | val filesMock = mock()
40 | val createMock = mock()
41 | val driveFile = File()
42 | whenever(drive.files()).thenReturn(filesMock)
43 | whenever(filesMock.create(any(), any())).thenReturn(createMock)
44 | whenever(createMock.execute()).thenReturn(driveFile)
45 |
46 | val result = service.uploadDatabaseFile(noteList, fileName, drive)
47 |
48 | assertTrue(result.isSuccess)
49 | assertEquals(driveFile, result.getOrNull())
50 | verify(drive, times(1)).files()
51 | verify(filesMock, times(1)).create(any(), any())
52 | }
53 |
54 | @Test
55 | fun returnResultFailure_whenUploadProcessThrowsIOExceptionWhileCreateDriveFile() = runTest {
56 | val filesMock = mock()
57 | val exception = IOException()
58 | whenever(drive.files()).thenReturn(filesMock)
59 | whenever(filesMock.create(any(), any())).thenThrow(exception)
60 |
61 | val result = service.uploadDatabaseFile(noteList, fileName, drive)
62 |
63 | assertTrue(result.isFailure)
64 | assertEquals(exception, result.exceptionOrNull())
65 | }
66 |
67 | @Test
68 | fun returnResultSuccess_whenReadFileProcessIsSuccess() = runTest {
69 | val filesMock = mock()
70 | val getMock = mock()
71 | val inputStream = this::class.java.classLoader?.getResourceAsStream("/src/test/res/backup.json")
72 | whenever(drive.files()).thenReturn(filesMock)
73 | whenever(filesMock.get(fileId)).thenReturn(getMock)
74 | whenever(getMock.executeMediaAsInputStream()).thenReturn(inputStream)
75 | whenever(dao.upsertNotesWithTimestamp(any())).thenAnswer { }
76 | whenever(proxy.dao()).thenReturn(dao)
77 |
78 | val result = service.readFile(fileId, drive)
79 |
80 | assertTrue(result.isSuccess)
81 | verify(proxy.dao(), times(1)).upsertNotesWithTimestamp(any())
82 | }
83 |
84 | @Test
85 | fun returnsFailureResult_whenReadFileProcessThrowsIOExceptionWhileGetBackupFileFromDrive() = runTest {
86 | val filesMock = mock()
87 | val exception = IOException()
88 | whenever(drive.files()).thenReturn(filesMock)
89 | whenever(filesMock.get(fileId)).thenThrow(exception)
90 |
91 | val result = service.readFile(fileId, drive)
92 |
93 | assertTrue(result.isFailure)
94 | assertEquals(exception, result.exceptionOrNull())
95 | }
96 |
97 | @Test
98 | fun returnResultFailure_whenReadFileProcessThrowsExceptionWhileUpsertNoteListToDatabase() = runTest {
99 | val filesMock = mock()
100 | val getMock = mock()
101 | val exception = Exception()
102 | val inputStream = this::class.java.classLoader?.getResourceAsStream("/src/test/res/backup.json")
103 | whenever(drive.files()).thenReturn(filesMock)
104 | whenever(filesMock.get(fileId)).thenReturn(getMock)
105 | whenever(getMock.executeMediaAsInputStream()).thenReturn(inputStream)
106 | whenever(dao.upsertNotesWithTimestamp(any())).thenAnswer {
107 | throw exception
108 | }
109 | whenever(proxy.dao()).thenReturn(dao)
110 |
111 | val result = service.readFile(fileId, drive)
112 |
113 | assertTrue(result.isFailure)
114 | assertEquals(exception, result.exceptionOrNull())
115 | }
116 |
117 | @Test
118 | fun returnResultSuccess_whenQueryFilesProcessIsSuccess() = runTest {
119 | val filesMock = mock()
120 | val fileList = mock()
121 | val fileListAfterSetSpace = mock()
122 | val driveFileList = mock()
123 | whenever(drive.files()).thenReturn(filesMock)
124 | whenever(filesMock.list()).thenReturn(fileList)
125 | whenever(fileList.setSpaces(any())).thenReturn(fileListAfterSetSpace)
126 | whenever(fileListAfterSetSpace.execute()).thenReturn(driveFileList)
127 |
128 | val result = service.queryFiles(drive)
129 |
130 | assertTrue(result.isSuccess)
131 | assertEquals(driveFileList, result.getOrNull())
132 | }
133 |
134 | @Test
135 | fun returnResultFailure_whenQueryFilesProcessThrowsIOExceptionWhileGettingDriveFiles() = runTest {
136 | val filesMock = mock()
137 | val fileList = mock()
138 | val fileListAfterSetSpace = mock()
139 | val exception = IOException()
140 | whenever(drive.files()).thenReturn(filesMock)
141 | whenever(filesMock.list()).thenReturn(fileList)
142 | whenever(fileList.setSpaces(any())).thenReturn(fileListAfterSetSpace)
143 | whenever(fileListAfterSetSpace.execute()).thenThrow(exception)
144 |
145 | val result = service.queryFiles(drive)
146 |
147 | assertTrue(result.isFailure)
148 | assertEquals(exception, result.exceptionOrNull())
149 | }
150 |
151 | @Test
152 | fun returnResultSuccess_whenDeleteFileProcessIsSuccess() = runTest {
153 | val filesMock = mock()
154 | val fileDelete = mock()
155 | whenever(drive.files()).thenReturn(filesMock)
156 | whenever(filesMock.delete(fileId)).thenReturn(fileDelete)
157 | whenever(fileDelete.execute()).thenAnswer { null }
158 |
159 | val result = service.deleteFile(fileId, drive)
160 |
161 | assertTrue(result.isSuccess)
162 | }
163 |
164 | @Test
165 | fun returnResultFailure_whenDeleteFileProcessThrowsIOExceptionWhileDeletingDriveFile() = runTest {
166 | val filesMock = mock()
167 | val fileDelete = mock()
168 | val exception = IOException()
169 | whenever(drive.files()).thenReturn(filesMock)
170 | whenever(filesMock.delete(fileId)).thenReturn(fileDelete)
171 | whenever(fileDelete.execute()).thenAnswer {
172 | throw exception
173 | }
174 |
175 | val result = service.deleteFile(fileId, drive)
176 |
177 | assertTrue(result.isFailure)
178 | assertEquals(exception, result.exceptionOrNull())
179 | }
180 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/digiventure/ventnote/data/persistence/NoteLocalServiceShould.kt:
--------------------------------------------------------------------------------
1 | package com.digiventure.ventnote.data.persistence
2 |
3 | import com.digiventure.utils.BaseUnitTest
4 | import com.digiventure.ventnote.commons.Constants
5 | import com.digiventure.ventnote.module.proxy.DatabaseProxy
6 | import kotlinx.coroutines.flow.first
7 | import kotlinx.coroutines.flow.flow
8 | import kotlinx.coroutines.runBlocking
9 | import kotlinx.coroutines.test.runTest
10 | import org.junit.Assert.assertEquals
11 | import org.junit.Before
12 | import org.junit.Test
13 | import org.mockito.kotlin.mock
14 | import org.mockito.kotlin.times
15 | import org.mockito.kotlin.verify
16 | import org.mockito.kotlin.whenever
17 |
18 | class NoteLocalServiceShould: BaseUnitTest() {
19 | private val proxy: DatabaseProxy = mock()
20 | private val dao: NoteDAO = mock()
21 | private val noteList = mock>()
22 | private val note = mock()
23 | private val sortBy = Constants.CREATED_AT
24 | private val orderBy = Constants.DESCENDING
25 |
26 | private val id = 1
27 |
28 | private val detailException = RuntimeException("Failed to get note detail")
29 | private val listException = RuntimeException("Failed to get list of notes")
30 | private val deleteException = RuntimeException("Failed to delete list of notes")
31 | private val updateException = RuntimeException("Failed to update list of notes")
32 | private val insertException = RuntimeException("Failed to insert list of notes")
33 |
34 | private lateinit var service: NoteLocalService
35 |
36 | @Before
37 | fun setup() {
38 | whenever(proxy.dao()).thenReturn(dao)
39 | service = NoteLocalService(proxy)
40 | }
41 |
42 | /**
43 | * Test suite for getNoteDetail from dao
44 | * */
45 | @Test
46 | fun getNoteDetailFromDAO() = runTest {
47 | stubSuccessfulGetDetailCase()
48 |
49 | service.getNoteDetail(id).first()
50 |
51 | verify(dao, times(1)).getNoteDetail(id)
52 | }
53 |
54 | @Test
55 | fun emitsFlowOfNoteAndEmitsThem() = runTest {
56 | stubSuccessfulGetDetailCase()
57 |
58 | assertEquals(note, dao.getNoteDetail(id).first())
59 | }
60 |
61 | @Test
62 | fun emitsErrorResultWhenGetDetailsFails() = runTest {
63 | stubErrorGetDetailCase()
64 |
65 | val actualResult = service.getNoteDetail(id).first()
66 | val actualException = actualResult.exceptionOrNull()
67 |
68 | assertEquals(detailException.message, actualException?.message)
69 | }
70 |
71 | private fun stubSuccessfulGetDetailCase() {
72 | runBlocking {
73 | whenever(dao.getNoteDetail(id)).thenReturn(
74 | flow {
75 | emit(note)
76 | }
77 | )
78 | }
79 | }
80 |
81 | private fun stubErrorGetDetailCase() {
82 | runBlocking {
83 | whenever(dao.getNoteDetail(id)).thenReturn(
84 | flow {
85 | throw detailException
86 | }
87 | )
88 | }
89 | }
90 |
91 | /**
92 | * Test suite for getNoteList from dao
93 | * */
94 | @Test
95 | fun getNoteListFromDAO() = runTest {
96 | stubSuccessfulGetListNoteCase()
97 |
98 | service.getNoteList(sortBy, orderBy).first()
99 |
100 | verify(dao, times(1)).getNotes(sortBy, orderBy)
101 | }
102 |
103 | @Test
104 | fun emitsFlowOfNoteListAndEmitsThem() = runTest {
105 | stubSuccessfulGetListNoteCase()
106 |
107 | assertEquals(Result.success(noteList), service.getNoteList(sortBy, orderBy).first())
108 | }
109 |
110 | @Test
111 | fun emitsErrorResultWhenGetNoteListFails() = runTest {
112 | stubErrorGetListNoteCase()
113 |
114 | val actualResult = service.getNoteList(sortBy, orderBy).first()
115 | val actualException = actualResult.exceptionOrNull()
116 |
117 | assertEquals(listException.message, actualException?.message)
118 | }
119 |
120 | private fun stubSuccessfulGetListNoteCase() {
121 | runBlocking {
122 | whenever(dao.getNotes(sortBy, orderBy)).thenReturn(
123 | flow {
124 | emit(noteList)
125 | }
126 | )
127 | }
128 | }
129 |
130 | private fun stubErrorGetListNoteCase() {
131 | runBlocking {
132 | whenever(dao.getNotes(sortBy, orderBy)).thenReturn(
133 | flow {
134 | throw listException
135 | }
136 | )
137 | }
138 | }
139 |
140 | /**
141 | * Test suite for deleteNoteList from dao
142 | * */
143 | @Test
144 | fun deleteNoteListFromDAO() = runTest {
145 | service.deleteNoteList(note).first()
146 |
147 | verify(dao, times(1)).deleteNotes(note)
148 | }
149 |
150 | @Test
151 | fun emitsFlowOfBooleanThatDeletedCountSameAsRequestedCount() = runTest {
152 | runBlocking { whenever(dao.deleteNotes(note)).thenReturn(1) }
153 | assertEquals(Result.success(true), service.deleteNoteList(note).first())
154 |
155 | runBlocking { whenever(dao.deleteNotes(note)).thenReturn(0) }
156 | assertEquals(Result.success(false), service.deleteNoteList(note).first())
157 | }
158 |
159 | @Test
160 | fun emitsErrorWhenDeletionFails() = runTest {
161 | runBlocking {
162 | whenever(dao.deleteNotes(note)).thenThrow(deleteException)
163 | }
164 |
165 | assertEquals(
166 | deleteException.message,
167 | service.deleteNoteList(note).first().exceptionOrNull()?.message
168 | )
169 | }
170 |
171 | /**
172 | * Test suite for updateNote from dao
173 | * */
174 | @Test
175 | fun updateNoteFromDAO() = runTest {
176 | service.updateNoteList(note).first()
177 |
178 | verify(dao, times(1)).updateWithTimestamp(note)
179 | }
180 |
181 | @Test
182 | fun emitsFlowOfBooleanThatUpdatedCountSameAsRequestedCount() = runTest {
183 | runBlocking { whenever(dao.updateWithTimestamp(note)).thenReturn(1) }
184 | assertEquals(Result.success(true), service.updateNoteList(note).first())
185 |
186 | runBlocking { whenever(dao.updateWithTimestamp(note)).thenReturn(0) }
187 | assertEquals(Result.success(false), service.updateNoteList(note).first())
188 | }
189 |
190 | @Test
191 | fun emitsErrorWhenUpdateFails() = runTest {
192 | runBlocking {
193 | whenever(dao.updateWithTimestamp(note)).thenThrow(updateException)
194 | }
195 |
196 | assertEquals(
197 | updateException.message,
198 | service.updateNoteList(note).first().exceptionOrNull()?.message
199 | )
200 | }
201 |
202 | /**
203 | * Test suite for insertNote from dao
204 | * */
205 | @Test
206 | fun insertNoteFromDAO() = runTest {
207 | service.insertNote(note).first()
208 |
209 | verify(dao, times(1)).insertWithTimestamp(note)
210 | }
211 |
212 | @Test
213 | fun emitsFlowOfBooleanThatReturnedIdIsNegativeOrNot() = runTest {
214 | runBlocking { whenever(dao.insertWithTimestamp(note)).thenReturn(1) }
215 | assertEquals(Result.success(true), service.insertNote(note).first())
216 |
217 | runBlocking { whenever(dao.insertWithTimestamp(note)).thenReturn(-1) }
218 | assertEquals(Result.success(false), service.insertNote(note).first())
219 | }
220 |
221 | @Test
222 | fun emitsErrorWhenInsertionFails() = runTest {
223 | runBlocking { whenever(dao.insertWithTimestamp(note)).thenThrow(insertException) }
224 |
225 | assertEquals(
226 | insertException.message,
227 | service.insertNote(note).first().exceptionOrNull()?.message
228 | )
229 | }
230 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/digiventure/ventnote/data/persistence/NoteRepositoryShould.kt:
--------------------------------------------------------------------------------
1 | package com.digiventure.ventnote.data.persistence
2 |
3 | import com.digiventure.utils.BaseUnitTest
4 | import com.digiventure.ventnote.commons.Constants
5 | import kotlinx.coroutines.flow.first
6 | import kotlinx.coroutines.flow.flow
7 | import kotlinx.coroutines.flow.flowOf
8 | import kotlinx.coroutines.runBlocking
9 | import kotlinx.coroutines.test.runTest
10 | import org.junit.Assert.assertEquals
11 | import org.junit.Before
12 | import org.junit.Test
13 | import org.mockito.Mockito.mock
14 | import org.mockito.kotlin.times
15 | import org.mockito.kotlin.verify
16 | import org.mockito.kotlin.whenever
17 |
18 | class NoteRepositoryShould: BaseUnitTest() {
19 | private val service: NoteLocalService = mock()
20 | private val noteList = mock>()
21 | private val note = mock()
22 | private val sortBy = Constants.CREATED_AT
23 | private val orderBy = Constants.DESCENDING
24 |
25 | private val id = 1
26 |
27 | private val exception = RuntimeException("Failed to get list of notes")
28 | private val deletionException = RuntimeException("Failed to delete list of notes")
29 | private val noteDetailException = RuntimeException("Failed to get note detail")
30 | private val updateException = RuntimeException("Failed to update list of notes")
31 | private val insertException = RuntimeException("Failed to insert list of notes")
32 |
33 | private lateinit var repository: NoteRepository
34 |
35 | @Before
36 | fun setup() {
37 | repository = NoteRepository(service)
38 | }
39 |
40 | /**
41 | * Test suite for get noteDetail
42 | * */
43 | @Test
44 | fun getNoteDetailFromService() = runTest {
45 | mockSuccessfulGetNoteCase()
46 |
47 | repository.getNoteDetail(id).first()
48 |
49 | verify(service, times(1)).getNoteDetail(id)
50 | }
51 |
52 | @Test
53 | fun emitsNoteDetailFromService() = runTest {
54 | mockSuccessfulGetNoteCase()
55 |
56 | assertEquals(Result.success(note), repository.getNoteDetail(id).first())
57 | }
58 |
59 | @Test
60 | fun propagateWhenGetNoteDetailError() = runTest {
61 | mockFailureGetNoteCase()
62 |
63 | assertEquals(noteDetailException, repository.getNoteDetail(id).first().exceptionOrNull())
64 | }
65 |
66 | private fun mockSuccessfulGetNoteCase() {
67 | runBlocking {
68 | whenever(service.getNoteDetail(id)).thenReturn(
69 | flow {
70 | emit(Result.success(note))
71 | }
72 | )
73 | }
74 | }
75 |
76 | private fun mockFailureGetNoteCase() {
77 | runBlocking {
78 | whenever(service.getNoteDetail(id)).thenReturn(
79 | flow {
80 | emit(Result.failure(noteDetailException))
81 | }
82 | )
83 | }
84 | }
85 |
86 | /**
87 | * Test suite for get noteList
88 | * */
89 | @Test
90 | fun getNoteListFromService() = runTest {
91 | mockSuccessfulGetNoteListCase()
92 |
93 | repository.getNoteList(sortBy, orderBy)
94 |
95 | verify(service, times(1)).getNoteList(sortBy, orderBy)
96 | }
97 |
98 | @Test
99 | fun emitsFlowOfNoteListFromService() = runTest {
100 | mockSuccessfulGetNoteListCase()
101 |
102 | assertEquals(Result.success(noteList), repository.getNoteList(sortBy, orderBy).first())
103 | }
104 |
105 | @Test
106 | fun propagateWhenGetNoteListError() = runTest {
107 | mockFailureGetNoteListCase()
108 |
109 | assertEquals(exception, repository.getNoteList(sortBy, orderBy).first().exceptionOrNull())
110 | }
111 |
112 | private fun mockSuccessfulGetNoteListCase() {
113 | runBlocking {
114 | whenever(service.getNoteList(sortBy, orderBy)).thenReturn(
115 | flow {
116 | emit(Result.success(noteList))
117 | }
118 | )
119 | }
120 | }
121 |
122 | private fun mockFailureGetNoteListCase() {
123 | runBlocking {
124 | whenever(service.getNoteList(sortBy, orderBy)).thenReturn(
125 | flow {
126 | emit(Result.failure(exception))
127 | }
128 | )
129 | }
130 | }
131 |
132 | /**
133 | * Test suite for delete noteList
134 | * */
135 | @Test
136 | fun deleteNoteListFromService() = runTest {
137 | mockSuccessfulDeletionCase()
138 |
139 | repository.deleteNoteList(note)
140 |
141 | verify(service, times(1)).deleteNoteList(note)
142 | }
143 |
144 | @Test
145 | fun emitBooleanAfterDeleteNoteListFromService() = runTest {
146 | mockSuccessfulDeletionCase()
147 |
148 | assertEquals(true, repository.deleteNoteList(note).first().getOrNull())
149 | }
150 |
151 | @Test
152 | fun propagateErrorWhenDeleteNoteListError() = runTest {
153 | mockFailureDeletionCase()
154 |
155 | assertEquals(deletionException, repository.deleteNoteList(note).first().exceptionOrNull())
156 | }
157 |
158 | private fun mockSuccessfulDeletionCase() {
159 | runBlocking {
160 | whenever(service.deleteNoteList(note)).thenReturn(
161 | flow {
162 | emit(Result.success(true))
163 | }
164 | )
165 | }
166 | }
167 |
168 | private fun mockFailureDeletionCase() {
169 | runBlocking {
170 | whenever(service.deleteNoteList(note)).thenReturn(
171 | flow {
172 | emit(Result.failure(deletionException))
173 | }
174 | )
175 | }
176 | }
177 |
178 | /**
179 | * Test suite for update noteList
180 | * */
181 | @Test
182 | fun updateNoteListFromService() = runTest {
183 | mockSuccessfulUpdateCase()
184 |
185 | repository.updateNoteList(note)
186 |
187 | verify(service, times(1)).updateNoteList(note)
188 | }
189 |
190 | @Test
191 | fun emitBooleanAfterUpdateNoteListFromService() = runTest {
192 | mockSuccessfulUpdateCase()
193 |
194 | assertEquals(true, repository.updateNoteList(note).first().getOrNull())
195 | }
196 |
197 | @Test
198 | fun propagateErrorWhenUpdateNoteListError() = runTest {
199 | mockFailureUpdateCase()
200 |
201 | assertEquals(updateException, repository.updateNoteList(note).first().exceptionOrNull())
202 | }
203 |
204 | private fun mockSuccessfulUpdateCase() {
205 | runBlocking {
206 | whenever(service.updateNoteList(note)).thenReturn(
207 | flow {
208 | emit(Result.success(true))
209 | }
210 | )
211 | }
212 | }
213 |
214 | private fun mockFailureUpdateCase() {
215 | runBlocking {
216 | whenever(service.updateNoteList(note)).thenReturn(
217 | flow {
218 | emit(Result.failure(updateException))
219 | }
220 | )
221 | }
222 | }
223 |
224 | /**
225 | * Test suite for insert note
226 | * */
227 | @Test
228 | fun insertNoteFromService() = runTest {
229 | mockSuccessfulInsertCase()
230 |
231 | repository.insertNote(note)
232 |
233 | verify(service, times(1)).insertNote(note)
234 | }
235 |
236 | @Test
237 | fun emitBooleanAfterInsertNoteFromService() = runTest {
238 | mockSuccessfulInsertCase()
239 |
240 | assertEquals(true, service.insertNote(note).first().getOrNull())
241 | }
242 |
243 | @Test
244 | fun propagateErrorWhenInsertNoteError() = runTest {
245 | mockFailureInsertCase()
246 |
247 | assertEquals(insertException, repository.insertNote(note).first().exceptionOrNull())
248 | }
249 |
250 | private fun mockSuccessfulInsertCase() {
251 | runBlocking {
252 | whenever(service.insertNote(note)).thenReturn(
253 | flowOf(Result.success(true))
254 | )
255 | }
256 | }
257 |
258 | private fun mockFailureInsertCase() {
259 | runBlocking {
260 | whenever(service.insertNote(note)).thenReturn(
261 | flowOf(Result.failure(insertException))
262 | )
263 | }
264 | }
265 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/digiventure/ventnote/note_creation/NoteCreationPageVMShould.kt:
--------------------------------------------------------------------------------
1 | package com.digiventure.ventnote.note_creation
2 |
3 | import com.digiventure.utils.BaseUnitTest
4 | import com.digiventure.utils.captureValues
5 | import com.digiventure.ventnote.data.persistence.NoteModel
6 | import com.digiventure.ventnote.data.persistence.NoteRepository
7 | import com.digiventure.ventnote.feature.note_creation.viewmodel.NoteCreationPageVM
8 | import kotlinx.coroutines.flow.flowOf
9 | import kotlinx.coroutines.runBlocking
10 | import kotlinx.coroutines.test.runTest
11 | import org.junit.Assert.assertEquals
12 | import org.junit.Before
13 | import org.junit.Test
14 | import org.mockito.kotlin.mock
15 | import org.mockito.kotlin.times
16 | import org.mockito.kotlin.verify
17 | import org.mockito.kotlin.whenever
18 |
19 | class NoteCreationPageVMShould: BaseUnitTest() {
20 | private val repository: NoteRepository = mock()
21 | private val note = mock()
22 |
23 | private lateinit var viewModel: NoteCreationPageVM
24 |
25 | private val expected = Result.success(true)
26 | private val exception = RuntimeException("Failed to insert list of notes")
27 |
28 | @Before
29 | fun setup() {
30 | viewModel = NoteCreationPageVM(repository)
31 | }
32 |
33 | /**
34 | * Test suite for add note from repository
35 | * */
36 | @Test
37 | fun addNoteFromRepository() = runTest {
38 | mockSuccessfulAddNoteCase()
39 |
40 | viewModel.addNote(note)
41 |
42 | verify(repository, times(1)).insertNote(note)
43 | }
44 |
45 | @Test
46 | fun emitsNoteIdIsNotNegativeFromRepository() = runTest {
47 | mockSuccessfulAddNoteCase()
48 |
49 | val result = viewModel.addNote(note)
50 |
51 | assertEquals(expected, result)
52 | }
53 |
54 | @Test
55 | fun emitsErrorWhenAddNoteReceiveError() = runTest {
56 | mockErrorAddNoteCase()
57 |
58 | val result = viewModel.addNote(note)
59 |
60 | assertEquals(exception.message, result.exceptionOrNull()?.message)
61 | }
62 |
63 | @Test
64 | fun showLoaderWhileAddNote() = runTest {
65 | mockSuccessfulAddNoteCase()
66 |
67 | viewModel.loader.captureValues {
68 | viewModel.addNote(note)
69 |
70 | assertEquals(true, values.first())
71 | }
72 | }
73 |
74 | @Test
75 | fun closeLoaderAfterAddNoteSuccess() = runTest {
76 | mockSuccessfulAddNoteCase()
77 |
78 | viewModel.loader.captureValues {
79 | viewModel.addNote(note)
80 |
81 | assertEquals(false, values.last())
82 | }
83 | }
84 |
85 | @Test
86 | fun closeLoaderAfterAddNoteError() = runTest {
87 | mockErrorAddNoteCase()
88 |
89 | viewModel.loader.captureValues {
90 | viewModel.addNote(note)
91 |
92 | assertEquals(false, values.last())
93 | }
94 | }
95 |
96 | private fun mockSuccessfulAddNoteCase() {
97 | runBlocking {
98 | whenever(repository.insertNote(note)).thenReturn(
99 | flowOf(expected)
100 | )
101 | }
102 | }
103 |
104 | private fun mockErrorAddNoteCase() {
105 | runBlocking {
106 | whenever(repository.insertNote(note)).thenReturn(
107 | flowOf(Result.failure(exception))
108 | )
109 | }
110 | }
111 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/digiventure/ventnote/note_detail/NoteDetailPageVMShould.kt:
--------------------------------------------------------------------------------
1 | package com.digiventure.ventnote.note_detail
2 |
3 | import com.digiventure.utils.BaseUnitTest
4 | import com.digiventure.utils.captureValues
5 | import com.digiventure.utils.getValueForTest
6 | import com.digiventure.ventnote.data.persistence.NoteModel
7 | import com.digiventure.ventnote.data.persistence.NoteRepository
8 | import com.digiventure.ventnote.feature.note_detail.viewmodel.NoteDetailPageVM
9 | import kotlinx.coroutines.flow.flow
10 | import kotlinx.coroutines.flow.flowOf
11 | import kotlinx.coroutines.runBlocking
12 | import kotlinx.coroutines.test.runTest
13 | import org.junit.Assert.assertEquals
14 | import org.junit.Before
15 | import org.junit.Test
16 | import org.mockito.Mockito
17 | import org.mockito.kotlin.mock
18 | import org.mockito.kotlin.times
19 | import org.mockito.kotlin.verify
20 | import org.mockito.kotlin.whenever
21 |
22 | class NoteDetailPageVMShould: BaseUnitTest() {
23 | private val repository: NoteRepository = mock()
24 | private val note = Mockito.mock()
25 | private val id = 1
26 |
27 | private val expected = Result.success(note)
28 | private val exception = RuntimeException("Failed to get note detail")
29 |
30 | private val expectedDeletion = Result.success(true)
31 | private val exceptionDeletion = RuntimeException("Failed to delete list of notes")
32 |
33 | private val expectedUpdating = Result.success(true)
34 | private val exceptionUpdating = RuntimeException("Failed to delete list of notes")
35 |
36 | private lateinit var viewModel: NoteDetailPageVM
37 |
38 | @Before
39 | fun setup() {
40 | viewModel = NoteDetailPageVM(repository)
41 | }
42 |
43 | /**
44 | * Test suite for get detail from repository
45 | * */
46 | @Test
47 | fun getNoteDetailFromRepository() = runTest {
48 | mockSuccessfulGetNoteListCase()
49 | viewModel.getNoteDetail(id)
50 |
51 | viewModel.noteDetail.getValueForTest()
52 |
53 | verify(repository, times(1)).getNoteDetail(id)
54 | }
55 |
56 | @Test
57 | fun emitsNoteDetailFromRepository() = runTest {
58 | mockSuccessfulGetNoteListCase()
59 | viewModel.getNoteDetail(id)
60 |
61 | assertEquals(expected, viewModel.noteDetail.getValueForTest())
62 | }
63 |
64 | @Test
65 | fun emitsErrorWhenGetNoteDetailReceiveError() = runTest {
66 | mockErrorGetNoteListCase()
67 | viewModel.getNoteDetail(id)
68 |
69 | assertEquals(Result.failure(exception), viewModel.noteDetail.getValueForTest())
70 | }
71 |
72 | @Test
73 | fun showLoaderWhileLoadingNoteDetail() = runTest {
74 | mockSuccessfulGetNoteListCase()
75 |
76 | viewModel.loader.captureValues {
77 | viewModel.getNoteDetail(id)
78 |
79 | assertEquals(true, values.first())
80 | }
81 | }
82 |
83 | @Test
84 | fun closeLoaderAfterNoteDetailLoaded() = runTest {
85 | mockSuccessfulGetNoteListCase()
86 |
87 | viewModel.loader.captureValues {
88 | viewModel.getNoteDetail(id)
89 |
90 | assertEquals(false, values.last())
91 | }
92 | }
93 |
94 | @Test
95 | fun closeLoaderAfterGetNoteDetailError() = runTest {
96 | mockErrorGetNoteListCase()
97 |
98 | viewModel.loader.captureValues {
99 | viewModel.getNoteDetail(id)
100 |
101 | assertEquals(false, values.last())
102 | }
103 | }
104 |
105 | private fun mockSuccessfulGetNoteListCase() {
106 | runBlocking {
107 | whenever(repository.getNoteDetail(id)).thenReturn(
108 | flow {
109 | emit(Result.success(note))
110 | }
111 | )
112 | }
113 | }
114 |
115 | private fun mockErrorGetNoteListCase() {
116 | runBlocking {
117 | whenever(repository.getNoteDetail(id)).thenReturn(
118 | flow {
119 | emit(Result.failure(exception))
120 | }
121 | )
122 | }
123 | }
124 |
125 | /**
126 | * Test suite for update note from repository
127 | * */
128 | @Test
129 | fun updateNoteFromRepository() = runTest {
130 | mockSuccessfulUpdateCase()
131 |
132 | viewModel.updateNote(note)
133 |
134 | verify(repository, times(1)).updateNoteList(note)
135 | }
136 |
137 | @Test
138 | fun emitsBooleanOfUpdatingLengthFromRepository() = runTest {
139 | mockSuccessfulUpdateCase()
140 |
141 | val result = viewModel.updateNote(note)
142 |
143 | assertEquals(expectedUpdating, result)
144 | }
145 |
146 | @Test
147 | fun emitsErrorWhenUpdatingError() = runTest {
148 | mockErrorUpdateCase()
149 |
150 | val result = viewModel.updateNote(note)
151 |
152 | assertEquals(Result.failure(exceptionUpdating), result)
153 | }
154 |
155 | @Test
156 | fun showLoaderWhileUpdateNote() = runTest {
157 | mockSuccessfulUpdateCase()
158 |
159 | viewModel.loader.captureValues {
160 | viewModel.updateNote(note)
161 |
162 | assertEquals(true, values.first())
163 | }
164 | }
165 |
166 | @Test
167 | fun closeLoaderAfterUpdateNoteSuccess() = runTest {
168 | mockSuccessfulUpdateCase()
169 |
170 | viewModel.loader.captureValues {
171 | viewModel.updateNote(note)
172 |
173 | assertEquals(false, values.last())
174 | }
175 | }
176 |
177 | @Test
178 | fun closeLoaderAfterUpdateNoteError() = runTest {
179 | mockErrorUpdateCase()
180 |
181 | viewModel.loader.captureValues {
182 | viewModel.updateNote(note)
183 |
184 | assertEquals(false, values.last())
185 | }
186 | }
187 |
188 | private fun mockSuccessfulUpdateCase() {
189 | runBlocking {
190 | whenever(repository.updateNoteList(note)).thenReturn(
191 | flowOf(expectedUpdating)
192 | )
193 | }
194 | }
195 |
196 | private fun mockErrorUpdateCase() {
197 | runBlocking {
198 | whenever(repository.updateNoteList(note)).thenReturn(
199 | flowOf(Result.failure(exceptionUpdating))
200 | )
201 | }
202 | }
203 |
204 | /**
205 | * Test suite for delete note from repository
206 | * */
207 | @Test
208 | fun deleteNoteListFromRepository() = runTest {
209 | mockSuccessfulDeletionCase()
210 |
211 | viewModel.deleteNoteList(note)
212 |
213 | verify(repository, times(1)).deleteNoteList(note)
214 | }
215 |
216 | @Test
217 | fun emitsBooleanOfDeletionLengthFromRepository() = runTest {
218 | mockSuccessfulDeletionCase()
219 |
220 | val result = viewModel.deleteNoteList(note)
221 |
222 | assertEquals(expectedDeletion, result)
223 | }
224 |
225 | @Test
226 | fun emitsErrorWhenDeletionError() = runTest {
227 | mockErrorDeletionCase()
228 |
229 | val result = viewModel.deleteNoteList(note)
230 |
231 | assertEquals(Result.failure(exceptionDeletion), result)
232 | }
233 |
234 | @Test
235 | fun showLoaderWhileDeletingNote() = runTest {
236 | mockSuccessfulDeletionCase()
237 |
238 | viewModel.loader.captureValues {
239 | viewModel.deleteNoteList(note)
240 |
241 | assertEquals(true, values.first())
242 | }
243 | }
244 |
245 | @Test
246 | fun closeLoaderAfterDeleteNoteSuccess() = runTest {
247 | mockSuccessfulDeletionCase()
248 |
249 | viewModel.loader.captureValues {
250 | viewModel.deleteNoteList(note)
251 |
252 | assertEquals(false, values.last())
253 | }
254 | }
255 |
256 | @Test
257 | fun closeLoaderAfterDeleteNoteError() = runTest {
258 | mockErrorDeletionCase()
259 |
260 | viewModel.loader.captureValues {
261 | viewModel.deleteNoteList(note)
262 |
263 | assertEquals(false, values.last())
264 | }
265 | }
266 |
267 | private fun mockSuccessfulDeletionCase() {
268 | runBlocking {
269 | whenever(repository.deleteNoteList(note)).thenReturn(
270 | flowOf(expectedDeletion)
271 | )
272 | }
273 | }
274 |
275 | private fun mockErrorDeletionCase() {
276 | runBlocking {
277 | whenever(repository.deleteNoteList(note)).thenReturn(
278 | flowOf(Result.failure(exceptionDeletion))
279 | )
280 | }
281 | }
282 | }
--------------------------------------------------------------------------------
/app/src/test/res/backup.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/app/src/test/res/backup.json
--------------------------------------------------------------------------------
/assets/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/assets/banner.png
--------------------------------------------------------------------------------
/assets/screen_five.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/assets/screen_five.png
--------------------------------------------------------------------------------
/assets/screen_four.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/assets/screen_four.png
--------------------------------------------------------------------------------
/assets/screen_one.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/assets/screen_one.png
--------------------------------------------------------------------------------
/assets/screen_six.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/assets/screen_six.png
--------------------------------------------------------------------------------
/assets/screen_three.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/assets/screen_three.png
--------------------------------------------------------------------------------
/assets/screen_two.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/assets/screen_two.png
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | dependencies {
3 | classpath 'com.android.tools.build:gradle:8.2.2'
4 |
5 | // disable for staging purpose
6 | // classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.2'
7 | // classpath 'com.google.gms:google-services:4.4.2'
8 | // classpath 'com.google.firebase:perf-plugin:1.4.2'
9 | }
10 | }
11 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
12 | plugins {
13 | id 'com.android.application' version '7.4.2' apply false
14 | id 'com.android.library' version '7.4.2' apply false
15 | id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
16 | id 'com.google.dagger.hilt.android' version '2.44' apply false
17 | }
18 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.nonFinalResIds=true
25 | #org.gradle.unsafe.configuration-cache=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HellBus1/VentNote/fd3562adb4487a1fe7d99e2e3d0b0bdae5833d31/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Mar 02 21:07:04 WIB 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "VentNote"
16 | include ':app'
17 |
--------------------------------------------------------------------------------