├── .gitattributes ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── highlightedFiles.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── noteappcompose │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── noteappcompose │ │ │ ├── MainActivity.kt │ │ │ ├── NoteApp.kt │ │ │ ├── data │ │ │ ├── di │ │ │ │ └── modules │ │ │ │ │ ├── CoroutinesModule.kt │ │ │ │ │ ├── DatabaseModule.kt │ │ │ │ │ ├── NetworkModule.kt │ │ │ │ │ └── RepositoryModule.kt │ │ │ ├── repositories │ │ │ │ ├── NoteRepositoryImpl.kt │ │ │ │ └── UserRepositoryImpl.kt │ │ │ ├── source │ │ │ │ ├── local │ │ │ │ │ └── dataSource │ │ │ │ │ │ └── UserLocalDataSource.kt │ │ │ │ └── remote │ │ │ │ │ ├── dataSource │ │ │ │ │ ├── NoteSocketDataSource.kt │ │ │ │ │ ├── NotesRemoteDataSource.kt │ │ │ │ │ └── UserRemoteDataSource.kt │ │ │ │ │ ├── endPoints │ │ │ │ │ ├── NotesApiService.kt │ │ │ │ │ └── UserApiService.kt │ │ │ │ │ ├── requestModels │ │ │ │ │ ├── AddNoteRequestModel.kt │ │ │ │ │ ├── LoginRequestModel.kt │ │ │ │ │ └── RegisterRequestModel.kt │ │ │ │ │ └── responseModels │ │ │ │ │ ├── BaseApiResponse.kt │ │ │ │ │ ├── NoteResponseModel.kt │ │ │ │ │ └── UserResponseModel.kt │ │ │ └── utilities │ │ │ │ ├── Constants.kt │ │ │ │ └── Extensions.kt │ │ │ ├── domain │ │ │ ├── models │ │ │ │ ├── NoteModel.kt │ │ │ │ ├── UserModel.kt │ │ │ │ └── ValidateResult.kt │ │ │ ├── repositories │ │ │ │ ├── NoteRepository.kt │ │ │ │ └── UserRepository.kt │ │ │ ├── usecases │ │ │ │ ├── AddEditNoteUseCase.kt │ │ │ │ ├── GetAllNotesUseCase.kt │ │ │ │ ├── GetNoteDetailsUseCase.kt │ │ │ │ ├── InitSocketUseCase.kt │ │ │ │ ├── IsUserSplashUseCase.kt │ │ │ │ ├── LoginUseCase.kt │ │ │ │ ├── RegisterUseCase.kt │ │ │ │ ├── SearchUseCase.kt │ │ │ │ ├── UploadImageUseCase.kt │ │ │ │ ├── ValidateEmailUseCase.kt │ │ │ │ ├── ValidateNoteDescriptionUseCase.kt │ │ │ │ ├── ValidateNoteSubtitleUseCase.kt │ │ │ │ ├── ValidateNoteTitleUseCase.kt │ │ │ │ ├── ValidatePasswordUseCase.kt │ │ │ │ ├── ValidateUserNameUseCase.kt │ │ │ │ └── ValidateWebLinkUseCase.kt │ │ │ └── utilitites │ │ │ │ └── Exceptions.kt │ │ │ └── presentation │ │ │ ├── homeScreen │ │ │ ├── HomeScreen.kt │ │ │ ├── HomeViewModel.kt │ │ │ ├── components │ │ │ │ ├── NoteItem.kt │ │ │ │ ├── SearchField.kt │ │ │ │ └── StaggeredVerticalGrid.kt │ │ │ └── uiStates │ │ │ │ ├── HomeUiEvent.kt │ │ │ │ ├── NoteItemUiState.kt │ │ │ │ └── NotesUiState.kt │ │ │ ├── loginScreen │ │ │ ├── LoginScreen.kt │ │ │ ├── LoginViewModel.kt │ │ │ ├── components │ │ │ │ └── OutlineInputField.kt │ │ │ └── uiStates │ │ │ │ ├── LoginUiEvent.kt │ │ │ │ └── LoginUiState.kt │ │ │ ├── noteDetailsScreen │ │ │ ├── NoteDetailScreen.kt │ │ │ ├── NoteDetailsViewModel.kt │ │ │ ├── components │ │ │ │ ├── BottomSheetItem.kt │ │ │ │ ├── ColorBox.kt │ │ │ │ ├── NoteInputField.kt │ │ │ │ ├── ToolBarIconButton.kt │ │ │ │ └── UrlDialog.kt │ │ │ └── uiStates │ │ │ │ ├── InputFieldUiState.kt │ │ │ │ ├── LinkUiState.kt │ │ │ │ ├── NoteDetailsEvent.kt │ │ │ │ └── NoteDetailsUiState.kt │ │ │ ├── registerScreen │ │ │ ├── RegisterScreen.kt │ │ │ ├── RegisterViewModel.kt │ │ │ ├── components │ │ │ │ └── LoadingButton.kt │ │ │ └── uiStates │ │ │ │ ├── RegisterUiEvent.kt │ │ │ │ └── RegisterUiState.kt │ │ │ ├── splashScreen │ │ │ ├── SplashScreen.kt │ │ │ ├── SplashViewModel.kt │ │ │ └── uiStates │ │ │ │ └── SplashUiEvent.kt │ │ │ ├── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ │ └── utilities │ │ │ ├── Constants.kt │ │ │ └── Screen.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── jetpack_compose_icon.png │ │ ├── font │ │ ├── ubuntu_bold.ttf │ │ ├── ubuntu_medium.ttf │ │ └── ubuntu_regular.ttf │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── example │ └── noteappcompose │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | NoteAppCompose -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/highlightedFiles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Adel Ayman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComposeNotesAppRetrofit :book: 2 | 3 | 4 | LinkedIn Badge 5 | 6 | 7 | **Notes App** That's Built With Kotlin to keep and organize user's notes. This repository contains a Notes App This is an educational App.Use and run this App to learn more about Apps design and best practices you must follow.That's built with Kotlin language with compose, That's implements Coroutines,Retrofit for HTTP requests,ktor-client for WebSocket requests,clean architecture,hilt,StateFlow,Navigation component,etc... this is client side you can find server side as api built with KTOR here -> [SocketNotesApi](https://github.com/adelayman1/SocketNoteApiKtor/) this project implement http and websocket you can find the same API but without websocket(Http requests only) here ->[HttpNotesApi](https://github.com/adelayman1/HttpNotesApiKtor/) 8 | 9 | ![](https://user-images.githubusercontent.com/85571327/206874180-5b9fb911-c883-49f8-add2-82e8cfad6b37.png) 10 | 11 | ## Features 12 | - Login with email and password 13 | - Signup with email and password 14 | - Search 15 | - Add Notes 16 | - Get Notes with realtime changes 17 | - Edit Notes 18 | - Change Note Color 19 | - Add Link 20 | - Add Image 21 | - Delete Note 22 | - Validate WebLink 23 | - Validate Email And Password 24 | - Validate Empty Fields 25 | 26 | ## App Overview 27 | 28 | - in this screen you can signin with email and password 29 | 30 | 31 | 32 | 33 | - in this screen you can signup with email and password 34 | 35 | 36 | 37 | 38 | - this screen view all user notes 39 | 40 | 41 | 42 | 43 | - this screen view note details and edit note 44 | 45 | 46 | 47 | 48 | **Bottom Sheet Content** 49 | 50 | 51 | 52 | 53 | 54 | **Add Link** 55 | 56 | 57 | 58 | 59 | 60 | **Fields Validation** 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | **Add Image** 71 | 72 | 73 | 74 | 75 | ## Built With 🛠 76 | 77 | * [Kotlin](https://kotlinlang.org/) 78 | * [Jetpack Compose](https://developer.android.com/jetpack/compose) 79 | * [Coroutines](https://developer.android.com/kotlin/coroutines) 80 | * [StateFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/) 81 | * MVVM architecture 82 | * Clean architecture 83 | * [Navigation component](https://developer.android.com/guide/navigation) 84 | * [Hilt](https://developer.android.com/training/dependency-injection/hilt-jetpack) 85 | * [Retrofit2](https://square.github.io/retrofit/) 86 | * [Ktor-Client](https://ktor.io/docs/getting-started-ktor-client-multiplatform-mobile.html) 87 | * [Coil](https://coil-kt.github.io/coil/compose/) 88 | * [Serialization](https://kotlinlang.org/docs/serialization.html/) 89 | * [Accompanist Permissions](https://github.com/google/accompanist/) 90 | * [LazyVerticalStaggeredGrid](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/staggeredgrid/package-summary/) 91 | * Single activity concept 92 | * Repository pattern 93 | 94 | ## Project Structure 95 | ``` 96 | noteappcompose 97 | ┣ data 98 | ┃ ┣ di 99 | ┃ ┃ ┗ modules 100 | ┃ ┃ ┃ ┣ CoroutinesModule.kt 101 | ┃ ┃ ┃ ┣ DatabaseModule.kt 102 | ┃ ┃ ┃ ┗ RepositoryModule.kt 103 | ┃ ┣ repositories 104 | ┃ ┃ ┣ NoteRepositoryImpl.kt 105 | ┃ ┃ ┗ UserRepositoryImpl.kt 106 | ┃ ┗ source 107 | ┃ ┃ ┣ local 108 | ┃ ┃ ┃ ┗ dataSource 109 | ┃ ┃ ┃ ┃ ┗ UserLocalDataSource.kt 110 | ┃ ┃ ┗ remote 111 | ┃ ┃ ┃ ┣ dataSource 112 | ┃ ┃ ┃ ┃ ┣ NoteSocketDataSource.kt 113 | ┃ ┃ ┃ ┃ ┣ NotesRemoteDataSource.kt 114 | ┃ ┃ ┃ ┃ ┗ UserRemoteDataSource.kt 115 | ┃ ┃ ┃ ┣ endPoints 116 | ┃ ┃ ┃ ┃ ┣ NotesApiService.kt 117 | ┃ ┃ ┃ ┃ ┗ UserApiService.kt 118 | ┃ ┃ ┃ ┣ requestModels 119 | ┃ ┃ ┃ ┃ ┣ AddNoteRequestModel.kt 120 | ┃ ┃ ┃ ┃ ┣ LoginRequestModel.kt 121 | ┃ ┃ ┃ ┃ ┗ RegisterRequestModel.kt 122 | ┃ ┃ ┃ ┗ responseModels 123 | ┃ ┃ ┃ ┃ ┣ BaseApiResponse.kt 124 | ┃ ┃ ┃ ┃ ┣ NoteResponseModel.kt 125 | ┃ ┃ ┃ ┃ ┗ UserResponseModel.kt 126 | ┣ domain 127 | ┃ ┣ models 128 | ┃ ┃ ┣ NoteModel.kt 129 | ┃ ┃ ┣ UserModel.kt 130 | ┃ ┃ ┗ ValidateResult.kt 131 | ┃ ┣ repositories 132 | ┃ ┃ ┣ NoteRepository.kt 133 | ┃ ┃ ┗ UserRepository.kt 134 | ┃ ┣ usecases 135 | ┃ ┃ ┣ AddEditNoteUseCase.kt 136 | ┃ ┃ ┣ GetAllNotesUseCase.kt 137 | ┃ ┃ ┣ GetNoteDetailsUseCase.kt 138 | ┃ ┃ ┣ IsUserSplashUseCase.kt 139 | ┃ ┃ ┣ LoginUseCase.kt 140 | ┃ ┃ ┣ RegisterUseCase.kt 141 | ┃ ┃ ┣ SearchUseCase.kt 142 | ┃ ┃ ┣ UploadImageUseCase.kt 143 | ┃ ┃ ┣ ValidateEmailUseCase.kt 144 | ┃ ┃ ┣ ValidateNoteDescriptionUseCase.kt 145 | ┃ ┃ ┣ ValidateNoteSubtitleUseCase.kt 146 | ┃ ┃ ┣ ValidateNoteTitleUseCase.kt 147 | ┃ ┃ ┣ ValidatePasswordUseCase.kt 148 | ┃ ┃ ┣ ValidateUserNameUseCase.kt 149 | ┃ ┃ ┗ ValidateWebLinkUseCase.kt 150 | ┃ ┗ utilitites 151 | ┃ ┃ ┗ Exceptions.kt 152 | ┣ presentation 153 | ┃ ┣ homeScreen 154 | ┃ ┃ ┣ components 155 | ┃ ┃ ┃ ┣ NoteItem.kt 156 | ┃ ┃ ┃ ┣ SearchField.kt 157 | ┃ ┃ ┃ ┗ StaggeredVerticalGrid.kt 158 | ┃ ┃ ┣ uiStates 159 | ┃ ┃ ┃ ┣ HomeUiEvent.kt 160 | ┃ ┃ ┃ ┣ NoteItemUiState.kt 161 | ┃ ┃ ┃ ┗ NotesUiState.kt 162 | ┃ ┃ ┣ HomeScreen.kt 163 | ┃ ┃ ┗ HomeViewModel.kt 164 | ┃ ┣ loginScreen 165 | ┃ ┃ ┣ components 166 | ┃ ┃ ┃ ┗ OutlineInputField.kt 167 | ┃ ┃ ┣ uiStates 168 | ┃ ┃ ┃ ┣ LoginUiEvent.kt 169 | ┃ ┃ ┃ ┗ LoginUiState.kt 170 | ┃ ┃ ┣ LoginScreen.kt 171 | ┃ ┃ ┗ LoginViewModel.kt 172 | ┃ ┣ noteDetailsScreen 173 | ┃ ┃ ┣ components 174 | ┃ ┃ ┃ ┣ BottomSheetItem.kt 175 | ┃ ┃ ┃ ┣ ColorBox.kt 176 | ┃ ┃ ┃ ┣ NoteInputField.kt 177 | ┃ ┃ ┃ ┣ ToolBarIconButton.kt 178 | ┃ ┃ ┃ ┗ UrlDialog.kt 179 | ┃ ┃ ┣ uiStates 180 | ┃ ┃ ┃ ┣ InputFieldUiState.kt 181 | ┃ ┃ ┃ ┣ LinkUiState.kt 182 | ┃ ┃ ┃ ┣ NoteDetailsEvent.kt 183 | ┃ ┃ ┃ ┗ NoteDetailsUiState.kt 184 | ┃ ┃ ┣ NoteDetailScreen.kt 185 | ┃ ┃ ┗ NoteDetailsViewModel.kt 186 | ┃ ┣ registerScreen 187 | ┃ ┃ ┣ components 188 | ┃ ┃ ┃ ┗ LoadingButton.kt 189 | ┃ ┃ ┣ uiStates 190 | ┃ ┃ ┃ ┣ RegisterUiEvent.kt 191 | ┃ ┃ ┃ ┗ RegisterUiState.kt 192 | ┃ ┃ ┣ RegisterScreen.kt 193 | ┃ ┃ ┗ RegisterViewModel.kt 194 | ┃ ┣ splashScreen 195 | ┃ ┃ ┣ uiStates 196 | ┃ ┃ ┃ ┗ SplashUiEvent.kt 197 | ┃ ┃ ┣ SplashScreen.kt 198 | ┃ ┃ ┗ SplashViewModel.kt 199 | ┃ ┣ theme 200 | ┃ ┃ ┣ Color.kt 201 | ┃ ┃ ┣ Theme.kt 202 | ┃ ┃ ┗ Type.kt 203 | ┃ ┗ utility 204 | ┃ ┃ ┣ Constants.kt 205 | ┃ ┃ ┗ Screen.kt 206 | ┣ MainActivity.kt 207 | ┗ NoteApp.kt 208 | ``` 209 | 210 | ## LICENSE 211 | ```MIT License 212 | 213 | Copyright (c) 2022 adelayman1 214 | 215 | Permission is hereby granted, free of charge, to any person obtaining a copy 216 | of this software and associated documentation files (the "Software"), to deal 217 | in the Software without restriction, including without limitation the rights 218 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 219 | copies of the Software, and to permit persons to whom the Software is 220 | furnished to do so, subject to the following conditions: 221 | 222 | The above copyright notice and this permission notice shall be included in all 223 | copies or substantial portions of the Software. 224 | 225 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 226 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 227 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 228 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 229 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 230 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 231 | SOFTWARE.``` 232 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'dagger.hilt.android.plugin' 6 | id 'org.jetbrains.kotlin.plugin.serialization' 7 | } 8 | 9 | android { 10 | namespace 'com.example.noteappcompose' 11 | compileSdk 33 12 | 13 | defaultConfig { 14 | applicationId "com.example.noteappcompose" 15 | minSdk 24 16 | targetSdk 33 17 | versionCode 1 18 | versionName "1.0" 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | vectorDrawables { 22 | useSupportLibrary true 23 | } 24 | } 25 | 26 | buildTypes { 27 | release { 28 | minifyEnabled false 29 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 30 | } 31 | } 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | buildFeatures { 40 | compose true 41 | } 42 | composeOptions { 43 | kotlinCompilerExtensionVersion '1.3.1' 44 | } 45 | packagingOptions { 46 | resources { 47 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 48 | } 49 | } 50 | } 51 | 52 | dependencies { 53 | 54 | implementation 'androidx.core:core-ktx:1.8.0' 55 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' 56 | implementation 'androidx.activity:activity-compose:1.5.1' 57 | implementation "androidx.compose.ui:ui:$compose_version" 58 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 59 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 60 | implementation "com.google.accompanist:accompanist-systemuicontroller:0.27.0" 61 | implementation 'androidx.compose.material3:material3:1.0.0' 62 | implementation 'androidx.compose.material:material:1.3.1' 63 | testImplementation 'junit:junit:4.13.2' 64 | androidTestImplementation 'androidx.test.ext:junit:1.1.4' 65 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' 66 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 67 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 68 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" 69 | implementation "androidx.compose.material:material-icons-extended:$compose_version" 70 | //retrofit and gson 71 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 72 | implementation 'com.google.code.gson:gson:2.9.0' 73 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0' 74 | //Okhttp 75 | implementation 'com.squareup.okhttp3:okhttp:4.1.0' 76 | implementation("com.squareup.okhttp3:logging-interceptor:4.10.0") 77 | //sdp & ssp 78 | implementation 'com.github.Kaaveh:sdp-compose:1.1.0' 79 | //hilt 80 | kapt "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.4.2" 81 | implementation "com.google.dagger:hilt-android:2.38.1" 82 | kapt "com.google.dagger:hilt-android-compiler:2.38.1" 83 | // implementation "androidx.room:room-runtime:2.4.3" 84 | // kapt "androidx.room:room-compiler:2.4.3" 85 | // implementation "androidx.room:room-ktx:2.4.3" 86 | // Compose dependencies 87 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0-beta01" 88 | implementation "androidx.navigation:navigation-compose:2.4.0-alpha09" 89 | implementation "androidx.hilt:hilt-navigation-compose:1.0.0-alpha03" 90 | // Coroutines 91 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0' 92 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1' 93 | //LazyVerticalStaggeredGrid 94 | implementation 'androidx.compose.foundation:foundation:1.3.1' 95 | //permissions 96 | implementation "com.google.accompanist:accompanist-permissions:0.23.1" 97 | implementation("io.coil-kt:coil-compose:2.2.2") 98 | //web socket 99 | // implementation 'com.github.tinder.scarlet:scarlet-protocol-websocket-okhttp:0.2.4' 100 | // implementation 'com.github.tinder.scarlet:scarlet-message-adapter-gson:0.2.4' 101 | // implementation 'com.tinder.scarlet:scarlet:0.1.12' 102 | // implementation "com.github.tinder.scarlet:scarlet-lifecycle-android:0.2.4" 103 | // implementation 'com.github.tinder.scarlet:scarlet-stream-adapter-coroutines:0.2.4' 104 | //ktor--------------------------- 105 | def ktor_version = "1.6.3" 106 | implementation "io.ktor:ktor-client-core:$ktor_version" 107 | implementation "io.ktor:ktor-client-cio:$ktor_version" 108 | implementation "io.ktor:ktor-client-serialization:$ktor_version" 109 | implementation "io.ktor:ktor-client-websockets:$ktor_version" 110 | implementation "io.ktor:ktor-client-logging:$ktor_version" 111 | implementation "ch.qos.logback:logback-classic:1.2.6" 112 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0" 113 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/noteappcompose/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.noteappcompose", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material.ExperimentalMaterialApi 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.Surface 10 | import androidx.compose.ui.Modifier 11 | import androidx.navigation.NavType 12 | import androidx.navigation.compose.NavHost 13 | import androidx.navigation.compose.composable 14 | import androidx.navigation.compose.rememberNavController 15 | import androidx.navigation.navArgument 16 | import com.example.noteappcompose.presentation.homeScreen.HomeScreen 17 | import com.example.noteappcompose.presentation.loginScreen.LoginScreen 18 | import com.example.noteappcompose.presentation.noteDetailsScreen.NoteDetailsScreen 19 | import com.example.noteappcompose.presentation.registerScreen.RegisterScreen 20 | import com.example.noteappcompose.presentation.splashScreen.SplashScreen 21 | import com.example.noteappcompose.presentation.theme.LightGray 22 | import com.example.noteappcompose.presentation.theme.NoteAppComposeTheme 23 | import com.example.noteappcompose.presentation.utilities.Screen 24 | import dagger.hilt.android.AndroidEntryPoint 25 | 26 | @AndroidEntryPoint 27 | class MainActivity : ComponentActivity() { 28 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | setContent { 32 | NoteAppComposeTheme { 33 | // A surface container using the 'background' color from the theme 34 | Surface( 35 | modifier = Modifier.fillMaxSize(), color = LightGray 36 | ) { 37 | val navController = rememberNavController() 38 | NavHost( 39 | navController = navController, startDestination = Screen.SplashScreen.route 40 | ) { 41 | composable(route = Screen.SplashScreen.route) { 42 | SplashScreen( 43 | navController = navController 44 | ) 45 | } 46 | composable(route = Screen.HomeScreen.route) { 47 | HomeScreen( 48 | navController = navController 49 | ) 50 | } 51 | composable(route = Screen.LoginScreen.route) { 52 | LoginScreen( 53 | navController = navController 54 | ) 55 | } 56 | composable(route = Screen.RegisterScreen.route) { 57 | RegisterScreen( 58 | navController = navController 59 | ) 60 | } 61 | composable(route = Screen.NoteDetailsScreen.route + "?noteId={noteId}", 62 | arguments = listOf(navArgument( 63 | name = "noteId" 64 | ) { 65 | type = NavType.StringType 66 | defaultValue = "-1" 67 | })) { 68 | NoteDetailsScreen( 69 | navController = navController 70 | ) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/NoteApp.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class NoteApp : Application() -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/di/modules/CoroutinesModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.di.modules 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.SupervisorJob 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | class CoroutinesModule { 16 | @Singleton 17 | @Provides 18 | fun provideCoroutineScope(): CoroutineScope { 19 | return CoroutineScope(SupervisorJob() + Dispatchers.IO) 20 | } 21 | @Singleton 22 | @Provides 23 | fun providesCoroutineDispatcher(): CoroutineDispatcher { 24 | return Dispatchers.IO 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/di/modules/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.di.modules 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import com.example.noteappcompose.data.utilities.Constants.USER_SHARED_PREFERENCES_KEY 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.qualifiers.ApplicationContext 10 | import dagger.hilt.components.SingletonComponent 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | class DatabaseModule { 16 | @Singleton 17 | @Provides 18 | fun provideSharedPreference(@ApplicationContext context: Context): SharedPreferences { 19 | return context.getSharedPreferences(USER_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/di/modules/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.di.modules 2 | 3 | import android.content.SharedPreferences 4 | import com.example.noteappcompose.data.source.remote.endPoints.NotesApiService 5 | import com.example.noteappcompose.data.source.remote.endPoints.UserApiService 6 | import com.example.noteappcompose.data.utilities.Constants.BASE_HTTP_URL 7 | import com.example.noteappcompose.data.utilities.Constants.USER_TOKEN_SHARED_PREFERENCES_KEY 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.components.SingletonComponent 12 | import io.ktor.client.HttpClient 13 | import io.ktor.client.engine.cio.CIO 14 | import io.ktor.client.features.json.JsonFeature 15 | import io.ktor.client.features.json.serializer.KotlinxSerializer 16 | import io.ktor.client.features.logging.Logging 17 | import io.ktor.client.features.websocket.WebSockets 18 | import okhttp3.OkHttpClient 19 | import okhttp3.logging.HttpLoggingInterceptor 20 | import retrofit2.Retrofit 21 | import retrofit2.converter.gson.GsonConverterFactory 22 | import java.util.concurrent.TimeUnit 23 | import javax.inject.Singleton 24 | 25 | @Module 26 | @InstallIn(SingletonComponent::class) 27 | class NetworkModule { 28 | @Singleton 29 | @Provides 30 | fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { 31 | return Retrofit.Builder() 32 | .client(okHttpClient) 33 | .baseUrl(BASE_HTTP_URL) 34 | .addConverterFactory(GsonConverterFactory.create()) 35 | .build() 36 | } 37 | 38 | @Singleton 39 | @Provides 40 | fun provideUserApiService(retrofit: Retrofit): UserApiService { 41 | return retrofit.create(UserApiService::class.java) 42 | } 43 | 44 | @Singleton 45 | @Provides 46 | fun provideNotesApiService(retrofit: Retrofit): NotesApiService { 47 | return retrofit.create(NotesApiService::class.java) 48 | } 49 | 50 | @Provides 51 | @Singleton 52 | fun provideLoggingInterceptor(): HttpLoggingInterceptor { 53 | return HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) 54 | } 55 | 56 | @Provides 57 | @Singleton 58 | fun provideOkHttpClient( 59 | interceptor: HttpLoggingInterceptor, 60 | prefs: SharedPreferences 61 | ): OkHttpClient { 62 | return OkHttpClient.Builder() 63 | .addNetworkInterceptor { chain -> 64 | var request = chain.request() 65 | if (request.header("No-Authentication") == null) { 66 | val authToken: String = prefs.getString(USER_TOKEN_SHARED_PREFERENCES_KEY, null) ?: "guest" 67 | request = request.newBuilder() 68 | .addHeader("Authorization", authToken) 69 | .build(); 70 | } 71 | chain.proceed(request) 72 | } 73 | .addInterceptor(interceptor) 74 | .connectTimeout(120, TimeUnit.SECONDS) 75 | .readTimeout(120, TimeUnit.SECONDS) 76 | .writeTimeout(120, TimeUnit.SECONDS) 77 | .build(); 78 | } 79 | 80 | @Provides 81 | @Singleton 82 | fun provideHttpClient(): HttpClient { 83 | return HttpClient(CIO) { 84 | install(Logging) 85 | install(WebSockets) 86 | install(JsonFeature) { 87 | serializer = KotlinxSerializer() 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/di/modules/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.di.modules 2 | 3 | import com.example.noteappcompose.data.repositories.NoteRepositoryImpl 4 | import com.example.noteappcompose.data.repositories.UserRepositoryImpl 5 | import com.example.noteappcompose.domain.repositories.NoteRepository 6 | import com.example.noteappcompose.domain.repositories.UserRepository 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import javax.inject.Singleton 12 | 13 | @Module(includes = [DatabaseModule::class]) 14 | @InstallIn(SingletonComponent::class) 15 | class RepositoryModule { 16 | @Singleton 17 | @Provides 18 | fun provideNoteRepository(noteRepositoryImpl: NoteRepositoryImpl): NoteRepository { 19 | return noteRepositoryImpl 20 | } 21 | @Singleton 22 | @Provides 23 | fun provideUserRepository(userRepositoryImpl: UserRepositoryImpl): UserRepository { 24 | return userRepositoryImpl 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/repositories/NoteRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.repositories 2 | 3 | import com.example.noteappcompose.data.source.remote.dataSource.NoteSocketDataSource 4 | import com.example.noteappcompose.data.source.remote.dataSource.NotesRemoteDataSource 5 | import com.example.noteappcompose.data.utilities.getErrorMessageFromResponse 6 | import com.example.noteappcompose.data.utilities.isDataHasGotSuccessfully 7 | import com.example.noteappcompose.domain.models.NoteModel 8 | import com.example.noteappcompose.domain.repositories.NoteRepository 9 | import kotlinx.coroutines.CoroutineDispatcher 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | import okhttp3.MediaType.Companion.toMediaType 15 | import okhttp3.MultipartBody 16 | import okhttp3.RequestBody.Companion.toRequestBody 17 | import javax.inject.Inject 18 | 19 | 20 | class NoteRepositoryImpl @Inject constructor( 21 | private val notesRemoteDataSource: NotesRemoteDataSource, 22 | private val noteSocketDataSource: NoteSocketDataSource, 23 | private val externalScope: CoroutineScope, 24 | private val ioDispatcher: CoroutineDispatcher 25 | ) : NoteRepository { 26 | override suspend fun getNotes(): List { 27 | val getNotesResult = notesRemoteDataSource.getAllNotes() 28 | return if (getNotesResult.isDataHasGotSuccessfully()) { 29 | getNotesResult.body()?.data!!.map { it.toNoteModel() } 30 | } else { 31 | throw Exception(getNotesResult.getErrorMessageFromResponse()) 32 | } 33 | } 34 | 35 | override suspend fun getNoteDetails(noteId: String): NoteModel { 36 | val getNoteDetailsResult = notesRemoteDataSource.getNoteDetails(noteId) 37 | return if (getNoteDetailsResult.isDataHasGotSuccessfully()) { 38 | getNoteDetailsResult.body()!!.data!!.toNoteModel() 39 | } else { 40 | throw Exception(getNoteDetailsResult.getErrorMessageFromResponse()) 41 | } 42 | } 43 | 44 | override suspend fun searchNotes(searchWord: String): List { 45 | val getSearchResult = notesRemoteDataSource.searchNotes(searchWord = searchWord) 46 | return if (getSearchResult.isDataHasGotSuccessfully()) { 47 | getSearchResult.body()!!.data!!.map { it.toNoteModel() } 48 | } else { 49 | throw Exception(getSearchResult.getErrorMessageFromResponse()) 50 | } 51 | } 52 | 53 | override suspend fun getNewNotes(): Flow = withContext(ioDispatcher) { 54 | noteSocketDataSource.observeNotes() 55 | } 56 | 57 | override suspend fun insertNote(note: String) { 58 | return externalScope.launch { 59 | noteSocketDataSource.sendNote(note) 60 | }.join() 61 | } 62 | 63 | override suspend fun uploadImage(imageAsByte: ByteArray, extension: String): String { 64 | val uploadImageResult = notesRemoteDataSource.updateImage( 65 | MultipartBody.Part.createFormData( 66 | "image", 67 | "image.$extension", 68 | imageAsByte.toRequestBody("*/*".toMediaType()) 69 | ) 70 | ) 71 | return if (uploadImageResult.isDataHasGotSuccessfully()) { 72 | uploadImageResult.body()!!.data!! 73 | } else { 74 | throw Exception(uploadImageResult.getErrorMessageFromResponse()) 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/repositories/UserRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.repositories 2 | 3 | import com.example.noteappcompose.data.source.local.dataSource.UserLocalDataSource 4 | import com.example.noteappcompose.data.source.remote.dataSource.UserRemoteDataSource 5 | import com.example.noteappcompose.data.source.remote.requestModels.LoginRequestModel 6 | import com.example.noteappcompose.data.source.remote.requestModels.RegisterRequestModel 7 | import com.example.noteappcompose.data.utilities.isDataHasGotSuccessfully 8 | import com.example.noteappcompose.domain.models.UserModel 9 | import com.example.noteappcompose.domain.repositories.UserRepository 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.async 12 | import javax.inject.Inject 13 | 14 | class UserRepositoryImpl @Inject constructor( 15 | var userRemoteDataSource: UserRemoteDataSource, 16 | var userLocalDataSource: UserLocalDataSource, 17 | val externalScope: CoroutineScope 18 | ) : UserRepository { 19 | override suspend fun login(email: String, password: String): UserModel { 20 | return externalScope.async { 21 | userRemoteDataSource.login(LoginRequestModel(email, password)) 22 | .also { 23 | if (it.isDataHasGotSuccessfully()) { 24 | userLocalDataSource.saveUserToken(it.body()!!.data!!.userToken) 25 | } else throw Exception(it.body()!!.message) 26 | } 27 | }.await().body()!!.data!!.toUserModel() 28 | } 29 | 30 | override suspend fun register(email: String, name: String, password: String): UserModel { 31 | return externalScope.async { 32 | userRemoteDataSource.register(RegisterRequestModel(name, email, password)) 33 | .also { 34 | if (it.isDataHasGotSuccessfully()) { 35 | userLocalDataSource.saveUserToken(it.body()!!.data!!.userToken) 36 | } else throw Exception(it.body()?.message) 37 | } 38 | }.await().body()!!.data!!.toUserModel() 39 | } 40 | 41 | override suspend fun getUserToken(): String? { 42 | return userLocalDataSource.getUserToken() 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/source/local/dataSource/UserLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.source.local.dataSource 2 | 3 | import android.content.SharedPreferences 4 | import com.example.noteappcompose.data.utilities.Constants.USER_TOKEN_SHARED_PREFERENCES_KEY 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import javax.inject.Inject 8 | 9 | class UserLocalDataSource @Inject constructor(var prefs: SharedPreferences) { 10 | suspend fun saveUserToken(authorizationKey: String) = withContext(Dispatchers.IO) { 11 | val editor = prefs.edit() 12 | editor.putString(USER_TOKEN_SHARED_PREFERENCES_KEY, authorizationKey) 13 | return@withContext editor.commit() 14 | } 15 | 16 | suspend fun getUserToken(): String? = 17 | withContext(Dispatchers.IO) { 18 | prefs.getString(USER_TOKEN_SHARED_PREFERENCES_KEY, null) 19 | } 20 | 21 | suspend fun deleteUserToken() = 22 | withContext(Dispatchers.IO) { 23 | val editor = prefs.edit() 24 | editor.remove(USER_TOKEN_SHARED_PREFERENCES_KEY) 25 | return@withContext editor.commit() 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/source/remote/dataSource/NoteSocketDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.source.remote.dataSource 2 | 3 | import com.example.noteappcompose.data.source.remote.responseModels.NoteResponseModel 4 | import com.example.noteappcompose.data.utilities.Constants.BASE_SOCKET_URL 5 | import com.example.noteappcompose.domain.models.NoteModel 6 | import com.example.noteappcompose.domain.utilitites.CannotJoinSessionException 7 | import io.ktor.client.HttpClient 8 | import io.ktor.client.features.websocket.webSocketSession 9 | import io.ktor.client.request.url 10 | import io.ktor.http.cio.websocket.Frame 11 | import io.ktor.http.cio.websocket.WebSocketSession 12 | import io.ktor.http.cio.websocket.close 13 | import io.ktor.http.cio.websocket.readText 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.flow.filter 16 | import kotlinx.coroutines.flow.flow 17 | import kotlinx.coroutines.flow.map 18 | import kotlinx.coroutines.flow.receiveAsFlow 19 | import kotlinx.coroutines.isActive 20 | import kotlinx.serialization.decodeFromString 21 | import kotlinx.serialization.json.Json 22 | import javax.inject.Inject 23 | import javax.inject.Singleton 24 | 25 | @Singleton 26 | class NoteSocketDataSource @Inject constructor(var client: HttpClient) { 27 | private var socketSession: WebSocketSession? = null 28 | suspend fun joinSession(userToken: String) { 29 | try { 30 | socketSession = client.webSocketSession { url("$BASE_SOCKET_URL/notes-socket?userToken=${userToken}") } 31 | if (socketSession?.isActive == false) throw CannotJoinSessionException() 32 | } catch (e: Exception) { 33 | throw e 34 | } 35 | } 36 | 37 | suspend fun sendNote(note: String) { 38 | try { 39 | socketSession?.send(Frame.Text(note)) 40 | } catch (e: Exception) { 41 | throw e 42 | } 43 | } 44 | 45 | fun observeNotes(): Flow { 46 | return try { 47 | socketSession?.incoming 48 | ?.receiveAsFlow() 49 | ?.filter { it is Frame.Text } 50 | ?.map { 51 | fromTextFrameToNoteModel(it) 52 | } ?: flow { } 53 | } catch (e: Exception) { 54 | throw e 55 | } 56 | } 57 | private fun fromTextFrameToNoteModel(frame:Frame)= Json.decodeFromString((frame as? Frame.Text)?.readText() ?: "").toNoteModel() 58 | 59 | suspend fun destroySession() { 60 | socketSession?.close() 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/source/remote/dataSource/NotesRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.source.remote.dataSource 2 | 3 | import com.example.noteappcompose.data.source.remote.endPoints.NotesApiService 4 | import com.example.noteappcompose.data.source.remote.responseModels.BaseApiResponse 5 | import com.example.noteappcompose.data.source.remote.responseModels.NoteResponseModel 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | import okhttp3.MultipartBody 9 | import retrofit2.Response 10 | import javax.inject.Inject 11 | 12 | class NotesRemoteDataSource @Inject constructor(var notesApiService: NotesApiService) { 13 | suspend fun getAllNotes(): Response>> = 14 | withContext(Dispatchers.IO) { 15 | notesApiService.getAllNotes() 16 | } 17 | 18 | suspend fun getAllNotesKtor(): Response>> = 19 | withContext(Dispatchers.IO) { 20 | notesApiService.getAllNotes() 21 | } 22 | 23 | suspend fun updateImage(imageMultiPart : MultipartBody.Part): Response> = 24 | withContext(Dispatchers.IO) { 25 | notesApiService.uploadImage(imageMultiPart) 26 | } 27 | 28 | suspend fun getNoteDetails(noteId: String): Response> = 29 | withContext(Dispatchers.IO) { 30 | notesApiService.getNoteDetails(noteId) 31 | } 32 | 33 | suspend fun searchNotes(searchWord: String): Response>> = 34 | withContext(Dispatchers.IO) { 35 | notesApiService.searchNotes(searchWord) 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/source/remote/dataSource/UserRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.source.remote.dataSource 2 | 3 | import com.example.noteappcompose.data.source.remote.endPoints.UserApiService 4 | import com.example.noteappcompose.data.source.remote.requestModels.LoginRequestModel 5 | import com.example.noteappcompose.data.source.remote.requestModels.RegisterRequestModel 6 | import com.example.noteappcompose.data.source.remote.responseModels.BaseApiResponse 7 | import com.example.noteappcompose.data.source.remote.responseModels.UserResponseModel 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.withContext 10 | import retrofit2.Response 11 | import javax.inject.Inject 12 | 13 | class UserRemoteDataSource @Inject constructor(var userApiService: UserApiService) { 14 | 15 | suspend fun login(loginRequestModel: LoginRequestModel): Response> = 16 | withContext(Dispatchers.IO) { 17 | userApiService.login(loginBody = loginRequestModel) 18 | } 19 | 20 | suspend fun register(registerRequestModel: RegisterRequestModel): Response> = 21 | withContext(Dispatchers.IO) { 22 | userApiService.register(registerBody = registerRequestModel) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/source/remote/endPoints/NotesApiService.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.source.remote.endPoints 2 | 3 | import com.example.noteappcompose.data.source.remote.responseModels.BaseApiResponse 4 | import com.example.noteappcompose.data.source.remote.responseModels.NoteResponseModel 5 | import okhttp3.MultipartBody 6 | import retrofit2.Response 7 | import retrofit2.http.GET 8 | import retrofit2.http.Multipart 9 | import retrofit2.http.POST 10 | import retrofit2.http.Part 11 | import retrofit2.http.Path 12 | import retrofit2.http.Query 13 | 14 | interface NotesApiService { 15 | @POST("notes/image") 16 | @Multipart() 17 | suspend fun uploadImage(@Part image: MultipartBody.Part): Response> 18 | 19 | @GET("notes") 20 | suspend fun getAllNotes(): Response>> 21 | 22 | @GET("notes/search") 23 | suspend fun searchNotes(@Query("search_word") searchWord:String): Response>> 24 | 25 | @GET("notes/{note_id}") 26 | suspend fun getNoteDetails(@Path("note_id") noteId: String): Response> 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/source/remote/endPoints/UserApiService.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.source.remote.endPoints 2 | 3 | import com.example.noteappcompose.data.source.remote.requestModels.LoginRequestModel 4 | import com.example.noteappcompose.data.source.remote.requestModels.RegisterRequestModel 5 | import com.example.noteappcompose.data.source.remote.responseModels.BaseApiResponse 6 | import com.example.noteappcompose.data.source.remote.responseModels.UserResponseModel 7 | import retrofit2.Response 8 | import retrofit2.http.Body 9 | import retrofit2.http.Headers 10 | import retrofit2.http.POST 11 | 12 | interface UserApiService { 13 | @POST("user/login") 14 | @Headers("No-Authentication:true") 15 | suspend fun login(@Body loginBody: LoginRequestModel): Response> 16 | 17 | @POST("user/register") 18 | @Headers("No-Authentication:true") 19 | suspend fun register(@Body registerBody: RegisterRequestModel): Response> 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/source/remote/requestModels/AddNoteRequestModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.source.remote.requestModels 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class AddNoteRequestModel( 7 | val id:Int?=null, 8 | val title: String, 9 | val subtitle: String, 10 | val description:String, 11 | val image:String?, 12 | val webLink:String?, 13 | val color: Int 14 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/source/remote/requestModels/LoginRequestModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.source.remote.requestModels 2 | 3 | data class LoginRequestModel( 4 | val email: String, 5 | val password: String 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/source/remote/requestModels/RegisterRequestModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.source.remote.requestModels 2 | 3 | data class RegisterRequestModel( 4 | val name: String, 5 | val email: String, 6 | val password: String 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/source/remote/responseModels/BaseApiResponse.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.source.remote.responseModels 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | class BaseApiResponse(var status: Boolean, var message: String, var data: T?) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/source/remote/responseModels/NoteResponseModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.source.remote.responseModels 2 | 3 | import com.example.noteappcompose.domain.models.NoteModel 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class NoteResponseModel( 8 | val noteId: String, 9 | val userToken: String, 10 | val title: String, 11 | val subtitle: String, 12 | val description: String, 13 | val date: String, 14 | val color: Int, 15 | val image: String?, 16 | val webLink: String? 17 | ) { 18 | fun toNoteModel() = NoteModel( 19 | id = noteId, 20 | noteTitle = title, 21 | noteSubtitle = subtitle, 22 | noteText = description, 23 | dateTime = date, 24 | noteColor = color, 25 | imageLink = image, 26 | webLink = webLink 27 | ) 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/source/remote/responseModels/UserResponseModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.source.remote.responseModels 2 | 3 | import com.example.noteappcompose.domain.models.UserModel 4 | 5 | data class UserResponseModel( 6 | val userID: String, 7 | val userToken: String, 8 | val userName: String, 9 | val email: String 10 | ) { 11 | fun toUserModel(): UserModel = UserModel(token = userToken, email = email) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/utilities/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.utilities 2 | 3 | object Constants { 4 | const val BASE_HTTP_URL = "http://192.168.1.13:4040/" 5 | const val BASE_SOCKET_URL = "ws://192.168.1.13:4040" 6 | const val USER_TOKEN_SHARED_PREFERENCES_KEY:String = "UserToken" 7 | const val USER_SHARED_PREFERENCES_KEY:String = "user" 8 | const val MINIMUM_USER_NAME_LENGTH:Int = 3 9 | const val MINIMUM_TITLE_LENGTH:Int = 2 10 | const val CREATE_NEW_NOTE_STATE_ID:String = "-1" 11 | const val MINIMUM_SUBTITLE_LENGTH:Int = 2 12 | const val MINIMUM_DESCRIPTION_LENGTH:Int = 2 13 | val VALID_PASSWORD_REGEX = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[^A-Za-z0-9]).{8,}\$".toRegex() 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/data/utilities/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.data.utilities 2 | 3 | import com.example.noteappcompose.data.source.remote.responseModels.BaseApiResponse 4 | import com.example.noteappcompose.domain.models.ValidateResult 5 | import org.json.JSONObject 6 | import retrofit2.Response 7 | 8 | fun Response>.isDataHasGotSuccessfully() = isSuccessful && body()?.data != null && code() == 200 9 | fun ValidateResult.isFieldDataValid() = error == null 10 | fun Response>.getErrorMessageFromResponse(): String = JSONObject(errorBody()!!.string()).getString("message") -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/models/NoteModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.models 2 | 3 | 4 | data class NoteModel( 5 | val id: String, 6 | val noteTitle: String, 7 | val noteSubtitle: String, 8 | val noteText: String, 9 | val dateTime: String, 10 | val imageLink: String?, 11 | val noteColor: Int, 12 | val webLink: String?, 13 | ) 14 | //ake version offline - make version online and name it with API- make news app online and offline -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/models/UserModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.models 2 | 3 | data class UserModel(val token:String,val email:String) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/models/ValidateResult.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.models 2 | 3 | data class ValidateResult( 4 | var error: String? = null 5 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/repositories/NoteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.repositories 2 | 3 | import com.example.noteappcompose.domain.models.NoteModel 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface NoteRepository { 7 | suspend fun getNotes(): List 8 | suspend fun getNoteDetails(noteId:String): NoteModel 9 | suspend fun searchNotes(searchWord:String): List 10 | suspend fun insertNote(note: String) 11 | suspend fun getNewNotes(): Flow 12 | suspend fun uploadImage(imageAsByte: ByteArray, extension:String): String 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/repositories/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.repositories 2 | 3 | import com.example.noteappcompose.domain.models.UserModel 4 | 5 | interface UserRepository { 6 | suspend fun login(email:String,password:String): UserModel 7 | 8 | suspend fun register(email:String,name:String,password:String): UserModel 9 | 10 | suspend fun getUserToken(): String? 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/AddEditNoteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.data.source.remote.requestModels.AddNoteRequestModel 4 | import com.example.noteappcompose.data.utilities.Constants.CREATE_NEW_NOTE_STATE_ID 5 | import com.example.noteappcompose.data.utilities.isFieldDataValid 6 | import com.example.noteappcompose.domain.repositories.NoteRepository 7 | import com.example.noteappcompose.domain.utilitites.InvalidInputTextException 8 | import kotlinx.serialization.encodeToString 9 | import kotlinx.serialization.json.Json 10 | import javax.inject.Inject 11 | 12 | class AddEditNoteUseCase @Inject constructor( 13 | private val noteRepository: NoteRepository, 14 | private val validateNoteTitleUseCase: ValidateNoteTitleUseCase, 15 | private val validateNoteSubtitleUseCase: ValidateNoteSubtitleUseCase, 16 | private val validateNoteDescriptionUseCase: ValidateNoteDescriptionUseCase, 17 | private val validateWebLinkUseCase: ValidateWebLinkUseCase 18 | ) { 19 | public suspend operator fun invoke( 20 | id: String?, 21 | title: String, 22 | subtitle: String, 23 | description: String, 24 | imageLink: String?, 25 | webLink: String?, 26 | color: Int, 27 | ) { 28 | validateFields(title, subtitle, description, webLink) 29 | noteRepository.insertNote( 30 | Json.encodeToString( 31 | AddNoteRequestModel( 32 | id = if (id == CREATE_NEW_NOTE_STATE_ID) null else id?.toInt(), 33 | title = title, 34 | subtitle = subtitle, 35 | description = description, 36 | image = imageLink, 37 | color = color, 38 | webLink = webLink 39 | ) 40 | ) 41 | ) 42 | } 43 | 44 | private fun validateFields( 45 | title: String, 46 | subtitle: String, 47 | description: String, 48 | webLink: String?, 49 | ) { 50 | validateNoteTitle(title) 51 | validateNoteSubtitle(subtitle) 52 | validateNoteDescription(description) 53 | validateNoteWebLink(webLink) 54 | } 55 | 56 | private fun validateNoteTitle(title: String) { 57 | val validateNoteTitleResult = validateNoteTitleUseCase(title) 58 | if (!validateNoteTitleResult.isFieldDataValid()) { 59 | throw InvalidInputTextException(errorMsg = validateNoteTitleResult.error ?: "") 60 | } 61 | } 62 | 63 | private fun validateNoteSubtitle(subtitle: String) { 64 | val validateNoteSubtitleResult = validateNoteSubtitleUseCase(subtitle) 65 | if (!validateNoteSubtitleResult.isFieldDataValid()) { 66 | throw InvalidInputTextException(errorMsg = validateNoteSubtitleResult.error ?: "") 67 | } 68 | } 69 | 70 | private fun validateNoteDescription(description: String) { 71 | val validateNoteDescriptionResult = validateNoteDescriptionUseCase(description) 72 | if (!validateNoteDescriptionResult.isFieldDataValid()) { 73 | throw InvalidInputTextException(errorMsg = validateNoteDescriptionResult.error ?: "") 74 | } 75 | } 76 | 77 | private fun validateNoteWebLink(webLink: String?) { 78 | if (!webLink.isNullOrEmpty()) { 79 | val validateNoteWebLinkResult = validateWebLinkUseCase(webLink ?: "") 80 | if (!validateNoteWebLinkResult.isFieldDataValid()) { 81 | throw InvalidInputTextException(errorMsg = validateNoteWebLinkResult.error ?: "") 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/GetAllNotesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.domain.models.NoteModel 4 | import com.example.noteappcompose.domain.repositories.NoteRepository 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.flow 7 | import javax.inject.Inject 8 | 9 | class GetAllNotesUseCase @Inject constructor( 10 | private val noteRepository: NoteRepository, 11 | private val initSocketUseCase: InitSocketUseCase 12 | ) { 13 | private var allNotes: List = emptyList() 14 | suspend operator fun invoke(): Flow> { 15 | initSocketUseCase.invoke() 16 | return flow { 17 | allNotes = noteRepository.getNotes() 18 | emit(allNotes) 19 | noteRepository.getNewNotes().collect { newNote -> 20 | val newNotesList = mergeNewNoteToNotesList(newNote) 21 | allNotes = newNotesList 22 | emit(newNotesList) 23 | } 24 | } 25 | } 26 | 27 | private fun mergeNewNoteToNotesList(newNote: NoteModel): List { 28 | return if (isNoteAlreadyExists(newNote.id)) { 29 | editNoteInList(newNote) 30 | } else { 31 | addNoteInFirstListPosition(newNote) 32 | } 33 | } 34 | 35 | private fun editNoteInList(note: NoteModel): List { 36 | return allNotes.toMutableList().apply { 37 | this[getNoteIndexById(note.id)] = note 38 | } 39 | } 40 | 41 | private fun addNoteInFirstListPosition(newNote: NoteModel): List { 42 | return allNotes.toMutableList().apply { 43 | add(0, newNote) 44 | } 45 | } 46 | 47 | private fun isNoteAlreadyExists(noteId: String): Boolean { 48 | return isIndexInList(getNoteIndexById(noteId)) 49 | } 50 | 51 | private fun getNoteIndexById(noteId: String): Int { 52 | return allNotes.indexOfFirst { it.id == noteId } 53 | } 54 | 55 | private fun isIndexInList(indexOfNoteId: Int) = indexOfNoteId >= 0 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/GetNoteDetailsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.domain.models.NoteModel 4 | import com.example.noteappcompose.domain.repositories.NoteRepository 5 | import com.example.noteappcompose.domain.utilitites.InvalidNoteIdException 6 | import javax.inject.Inject 7 | 8 | class GetNoteDetailsUseCase @Inject constructor(private val noteRepository: NoteRepository) { 9 | public suspend operator fun invoke(noteId: String): NoteModel { 10 | if (!isNoteIdValid(noteId)) { 11 | throw InvalidNoteIdException() 12 | } 13 | return noteRepository.getNoteDetails(noteId) ?: throw InvalidNoteIdException() 14 | } 15 | 16 | private fun isNoteIdValid(noteId: String) = noteId.toInt() >= 0 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/InitSocketUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.data.source.remote.dataSource.NoteSocketDataSource 4 | import com.example.noteappcompose.domain.repositories.UserRepository 5 | import com.example.noteappcompose.domain.utilitites.UserLoggedInException 6 | import com.example.noteappcompose.domain.utilitites.UserNotLoggedInException 7 | import javax.inject.Inject 8 | 9 | class InitSocketUseCase @Inject constructor( 10 | private val userRepository: UserRepository, 11 | private val socketDataSource: NoteSocketDataSource 12 | ) { 13 | public suspend operator fun invoke() { 14 | if (userRepository.getUserToken().isNullOrBlank()) 15 | throw UserNotLoggedInException() 16 | socketDataSource.joinSession(userRepository.getUserToken()!!) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/IsUserSplashUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.domain.repositories.UserRepository 4 | import javax.inject.Inject 5 | 6 | class IsUserSplashUseCase @Inject constructor(private val userRepository: UserRepository) { 7 | public suspend operator fun invoke(): Boolean { 8 | return userRepository.getUserToken().isNullOrBlank() 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/LoginUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.data.utilities.isFieldDataValid 4 | import com.example.noteappcompose.domain.models.UserModel 5 | import com.example.noteappcompose.domain.repositories.UserRepository 6 | import com.example.noteappcompose.domain.utilitites.InvalidInputTextException 7 | import com.example.noteappcompose.domain.utilitites.UserLoggedInException 8 | import javax.inject.Inject 9 | 10 | class LoginUseCase @Inject constructor( 11 | private val userRepository: UserRepository, 12 | private val validateEmailUseCase: ValidateEmailUseCase, 13 | private val validatePasswordUseCase: ValidatePasswordUseCase 14 | ) { 15 | public suspend operator fun invoke(email: String, password: String): UserModel { 16 | if (!userRepository.getUserToken().isNullOrBlank()) 17 | throw UserLoggedInException() 18 | validateFields(email, password) 19 | return userRepository.login(email, password) 20 | } 21 | 22 | private fun validateFields(email: String, password: String) { 23 | validateEmail(email) 24 | validatePassword(password) 25 | } 26 | 27 | private fun validateEmail(email: String) { 28 | val validateEmailResult = validateEmailUseCase(email) 29 | if (!validateEmailResult.isFieldDataValid()) throw InvalidInputTextException( 30 | validateEmailResult.error ?: "" 31 | ) 32 | } 33 | 34 | private fun validatePassword(password: String) { 35 | val validatePasswordResult = validatePasswordUseCase(password) 36 | if (!validatePasswordResult.isFieldDataValid()) throw InvalidInputTextException( 37 | validatePasswordResult.error ?: "" 38 | ) 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/RegisterUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.data.utilities.isFieldDataValid 4 | import com.example.noteappcompose.domain.models.UserModel 5 | import com.example.noteappcompose.domain.repositories.UserRepository 6 | import com.example.noteappcompose.domain.utilitites.InvalidInputTextException 7 | import com.example.noteappcompose.domain.utilitites.UserLoggedInException 8 | import javax.inject.Inject 9 | 10 | class RegisterUseCase @Inject constructor( 11 | private val userRepository: UserRepository, 12 | private val validateUserNameUseCase: ValidateUserNameUseCase, 13 | private val validateEmailUseCase: ValidateEmailUseCase, 14 | private val validatePasswordUseCase: ValidatePasswordUseCase 15 | ) { 16 | public suspend operator fun invoke(name: String, email: String, password: String): UserModel { 17 | if (!userRepository.getUserToken().isNullOrBlank()) 18 | throw UserLoggedInException() 19 | validateFields(name, email, password) 20 | return userRepository.register(email, name, password) 21 | } 22 | 23 | private fun validateFields(userName: String, email: String, password: String) { 24 | validateUserName(userName) 25 | validateEmail(email) 26 | validatePassword(password) 27 | } 28 | 29 | private fun validateUserName(userName: String) { 30 | val validateUserNameResult = validateUserNameUseCase(userName) 31 | if (!validateUserNameResult.isFieldDataValid()) throw InvalidInputTextException( 32 | validateUserNameResult.error ?: "" 33 | ) 34 | } 35 | 36 | private fun validateEmail(email: String) { 37 | val validateEmailResult = validateEmailUseCase(email) 38 | if (!validateEmailResult.isFieldDataValid()) throw InvalidInputTextException( 39 | validateEmailResult.error ?: "" 40 | ) 41 | } 42 | 43 | private fun validatePassword(password: String) { 44 | val validatePasswordResult = validatePasswordUseCase(password) 45 | if (!validatePasswordResult.isFieldDataValid()) throw InvalidInputTextException( 46 | validatePasswordResult.error ?: "" 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/SearchUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.domain.models.NoteModel 4 | import com.example.noteappcompose.domain.repositories.NoteRepository 5 | import com.example.noteappcompose.domain.repositories.UserRepository 6 | import com.example.noteappcompose.domain.utilitites.InvalidInputTextException 7 | import com.example.noteappcompose.domain.utilitites.UserNotLoggedInException 8 | import javax.inject.Inject 9 | 10 | class SearchUseCase @Inject constructor( 11 | private val noteRepository: NoteRepository, 12 | private val userRepository: UserRepository, 13 | ) { 14 | public suspend operator fun invoke(searchWord: String): List { 15 | if (userRepository.getUserToken().isNullOrBlank()) 16 | throw UserNotLoggedInException() 17 | validateSearchWord(searchWord) 18 | return noteRepository.searchNotes(searchWord) 19 | } 20 | 21 | private fun validateSearchWord(searchWord: String) { 22 | if (searchWord.isBlank()) 23 | throw InvalidInputTextException("Invalid search word") 24 | if (searchWord.length < 2) 25 | throw InvalidInputTextException("Search keyword is very short") 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/UploadImageUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import com.example.noteappcompose.domain.repositories.NoteRepository 6 | import dagger.hilt.android.qualifiers.ApplicationContext 7 | import javax.inject.Inject 8 | 9 | class UploadImageUseCase @Inject constructor( 10 | private val noteRepository: NoteRepository, 11 | @ApplicationContext private val context: Context 12 | ) { 13 | public suspend operator fun invoke(imageUri: Uri): String { 14 | val imageAsByte = convertImageFromUriToByte(imageUri) 15 | val extension = getImageExtensionByUri(imageUri) 16 | val uploadImageResult = noteRepository.uploadImage(imageAsByte!!, extension) 17 | return uploadImageResult; 18 | } 19 | 20 | private fun convertImageFromUriToByte(imageUri: Uri) = 21 | context.contentResolver.openInputStream(imageUri)?.buffered()?.use { it.readBytes() } 22 | 23 | private fun getImageTypeByUri(imageUri: Uri) = context.contentResolver.getType(imageUri) 24 | private fun getImageExtensionByUri(imageUri: Uri): String { 25 | val imageType = getImageTypeByUri(imageUri) 26 | val imageExtension = imageType!!.substring(imageType.indexOf("/") + 1) 27 | return imageExtension 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/ValidateEmailUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import android.util.Patterns 4 | import com.example.noteappcompose.domain.models.ValidateResult 5 | import javax.inject.Inject 6 | 7 | class ValidateEmailUseCase @Inject constructor() { 8 | operator fun invoke(email: String): ValidateResult { 9 | if (email.isBlank()) 10 | return ValidateResult(error = "Please enter email") 11 | if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) 12 | return ValidateResult(error = "Please enter valid email") 13 | return ValidateResult() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/ValidateNoteDescriptionUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.data.utilities.Constants.MINIMUM_DESCRIPTION_LENGTH 4 | import com.example.noteappcompose.domain.models.ValidateResult 5 | import javax.inject.Inject 6 | 7 | class ValidateNoteDescriptionUseCase @Inject constructor() { 8 | operator fun invoke(description: String): ValidateResult { 9 | if (description.isBlank()) 10 | return ValidateResult(error = "Please enter description") 11 | if (description.trim().length < MINIMUM_DESCRIPTION_LENGTH) 12 | return ValidateResult(error = "Description is so short") 13 | return ValidateResult() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/ValidateNoteSubtitleUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.data.utilities.Constants.MINIMUM_SUBTITLE_LENGTH 4 | import com.example.noteappcompose.domain.models.ValidateResult 5 | import javax.inject.Inject 6 | 7 | class ValidateNoteSubtitleUseCase @Inject constructor() { 8 | operator fun invoke(subtitle: String): ValidateResult { 9 | if (subtitle.isBlank()) 10 | return ValidateResult(error = "Please enter subtitle") 11 | if (subtitle.trim().length < MINIMUM_SUBTITLE_LENGTH) 12 | return ValidateResult(error = "Subtitle is so short") 13 | if (subtitle.contains(".")) 14 | return ValidateResult(error = "Subtitle is not valid") 15 | return ValidateResult() 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/ValidateNoteTitleUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.data.utilities.Constants.MINIMUM_TITLE_LENGTH 4 | import com.example.noteappcompose.domain.models.ValidateResult 5 | import javax.inject.Inject 6 | 7 | class ValidateNoteTitleUseCase @Inject constructor() { 8 | operator fun invoke(title: String): ValidateResult { 9 | if (title.isBlank()) 10 | return ValidateResult(error = "Please enter title") 11 | if (title.length < MINIMUM_TITLE_LENGTH) 12 | return ValidateResult(error = "Title is so short") 13 | return ValidateResult() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/ValidatePasswordUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.data.utilities.Constants.VALID_PASSWORD_REGEX 4 | import com.example.noteappcompose.domain.models.ValidateResult 5 | import javax.inject.Inject 6 | 7 | class ValidatePasswordUseCase @Inject constructor() { 8 | operator fun invoke(password: String): ValidateResult { 9 | if (password.isBlank()) 10 | return ValidateResult(error = "Please enter password") 11 | if (!password.matches(VALID_PASSWORD_REGEX)) 12 | return ValidateResult(error = "Please enter valid password") 13 | return ValidateResult() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/ValidateUserNameUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import com.example.noteappcompose.data.utilities.Constants.MINIMUM_USER_NAME_LENGTH 4 | import com.example.noteappcompose.domain.models.ValidateResult 5 | import javax.inject.Inject 6 | 7 | class ValidateUserNameUseCase @Inject constructor() { 8 | operator fun invoke(name: String): ValidateResult { 9 | if (name.isBlank()) 10 | return ValidateResult(error = "Please enter name") 11 | if (name.length < MINIMUM_USER_NAME_LENGTH) 12 | return ValidateResult(error = "Please enter valid name") 13 | return ValidateResult() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/usecases/ValidateWebLinkUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.usecases 2 | 3 | import android.util.Patterns 4 | import com.example.noteappcompose.domain.models.ValidateResult 5 | import javax.inject.Inject 6 | 7 | class ValidateWebLinkUseCase @Inject constructor() { 8 | operator fun invoke(webLink: String): ValidateResult { 9 | if (webLink.isBlank()) 10 | return ValidateResult(error = "Please enter url") 11 | if (!Patterns.WEB_URL.matcher(webLink).matches()) 12 | return ValidateResult(error = "Url is not valid") 13 | return ValidateResult() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/domain/utilitites/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.domain.utilitites 2 | 3 | class InvalidNoteIdException: Exception("ID is not valid") 4 | class InvalidInputTextException(errorMsg:String): Exception(errorMsg) 5 | class UserLoggedInException: Exception("User Is Already Logged In") 6 | class UserNotLoggedInException: Exception("User Is Not Logged In") 7 | class CannotJoinSessionException: Exception("can't connect to socket") -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/homeScreen/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.homeScreen 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.foundation.ExperimentalFoundationApi 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid 14 | import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells 15 | import androidx.compose.foundation.lazy.staggeredgrid.items 16 | import androidx.compose.material.CircularProgressIndicator 17 | import androidx.compose.material.ExperimentalMaterialApi 18 | import androidx.compose.material.Scaffold 19 | import androidx.compose.material.icons.Icons 20 | import androidx.compose.material.icons.filled.Add 21 | import androidx.compose.material.icons.filled.Image 22 | import androidx.compose.material.icons.filled.Language 23 | import androidx.compose.material.rememberScaffoldState 24 | import androidx.compose.material3.BottomAppBar 25 | import androidx.compose.material3.ExperimentalMaterial3Api 26 | import androidx.compose.material3.FloatingActionButton 27 | import androidx.compose.material3.FloatingActionButtonDefaults 28 | import androidx.compose.material3.Icon 29 | import androidx.compose.material3.IconButton 30 | import androidx.compose.material3.Text 31 | import androidx.compose.runtime.Composable 32 | import androidx.compose.runtime.LaunchedEffect 33 | import androidx.compose.ui.Alignment 34 | import androidx.compose.ui.Modifier 35 | import androidx.compose.ui.graphics.Color 36 | import androidx.compose.ui.text.font.FontWeight 37 | import androidx.hilt.navigation.compose.hiltViewModel 38 | import androidx.navigation.NavController 39 | import com.example.noteappcompose.data.utilities.Constants.CREATE_NEW_NOTE_STATE_ID 40 | import com.example.noteappcompose.presentation.homeScreen.components.NoteItem 41 | import com.example.noteappcompose.presentation.homeScreen.components.SearchField 42 | import com.example.noteappcompose.presentation.homeScreen.uiStates.HomeUiEvent 43 | import com.example.noteappcompose.presentation.theme.BottomBarContainer 44 | import com.example.noteappcompose.presentation.theme.BottomBarIcon 45 | import com.example.noteappcompose.presentation.theme.LightGray 46 | import com.example.noteappcompose.presentation.theme.Orange 47 | import com.example.noteappcompose.presentation.theme.UbuntuFont 48 | import com.example.noteappcompose.presentation.utilities.Screen 49 | import ir.kaaveh.sdpcompose.sdp 50 | import ir.kaaveh.sdpcompose.ssp 51 | import kotlinx.coroutines.flow.collectLatest 52 | 53 | @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) 54 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "UnusedMaterialScaffoldPaddingParameter") 55 | @ExperimentalMaterial3Api 56 | @Composable 57 | fun HomeScreen( 58 | navController: NavController, 59 | viewModel: HomeViewModel = hiltViewModel() 60 | ) { 61 | val scaffoldState = rememberScaffoldState() 62 | LaunchedEffect(key1 = true) { 63 | viewModel.eventFlow.collectLatest { event -> 64 | when (event) { 65 | is HomeViewModel.UiEvent.ShowMessage -> scaffoldState.snackbarHostState.showSnackbar( 66 | event.message 67 | ) 68 | } 69 | } 70 | } 71 | Scaffold( 72 | modifier = Modifier.background(LightGray), 73 | scaffoldState = scaffoldState, 74 | bottomBar = { 75 | BottomAppBar(containerColor = BottomBarContainer, 76 | contentColor = BottomBarIcon, 77 | actions = { 78 | IconButton(onClick = { /* doSomething() */ }) { 79 | //region Add Icon 80 | Icon( 81 | Icons.Default.Add, 82 | modifier = Modifier.size(22.sdp), 83 | contentDescription = "Localized description" 84 | ) 85 | //endregion 86 | } 87 | IconButton(onClick = { /* doSomething() */ }) { 88 | Icon( 89 | Icons.Default.Image, 90 | modifier = Modifier.size(22.sdp), 91 | contentDescription = "Localized description", 92 | ) 93 | } 94 | IconButton(onClick = { /* doSomething() */ }) { 95 | Icon( 96 | Icons.Default.Language, 97 | modifier = Modifier.size(22.sdp), 98 | contentDescription = "Localized description" 99 | ) 100 | } 101 | }, 102 | floatingActionButton = { 103 | FloatingActionButton( 104 | onClick = { 105 | navController.navigate( 106 | Screen.NoteDetailsScreen.route + "?noteId=$CREATE_NEW_NOTE_STATE_ID" 107 | ) 108 | }, 109 | containerColor = Orange, 110 | elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation() 111 | ) { 112 | Icon(Icons.Filled.Add, "Localized description", tint = LightGray) 113 | } 114 | }) 115 | }, 116 | ) { 117 | Column( 118 | horizontalAlignment = Alignment.Start, 119 | modifier = Modifier 120 | .fillMaxSize() 121 | .background(LightGray) 122 | .padding(vertical = 20.sdp, horizontal = 12.sdp) 123 | ) { 124 | Text( 125 | text = "My Notes", 126 | fontFamily = UbuntuFont, 127 | fontWeight = FontWeight.Bold, 128 | fontSize = 20.ssp, 129 | color = Color.White, 130 | ) 131 | Spacer(Modifier.size(15.sdp)) 132 | SearchField( 133 | value = viewModel.notesUiState.searchText, 134 | onValueChange = { 135 | viewModel.onEvent(HomeUiEvent.SearchTextChanged(it)) 136 | }, 137 | onSearch = { 138 | viewModel.onEvent(HomeUiEvent.Search) 139 | } 140 | ) 141 | if (viewModel.notesUiState.isLoading) { 142 | CircularProgressIndicator(modifier = Modifier.padding(top = 50.sdp, start = 50.sdp).size(30.sdp)) 143 | } else { 144 | LazyVerticalStaggeredGrid( 145 | columns = StaggeredGridCells.Fixed(2), 146 | horizontalArrangement = Arrangement.spacedBy(10.sdp), 147 | content = { 148 | if (!viewModel.notesUiState.searchResult.isEmpty()) { 149 | item() { 150 | Text( 151 | text = "Search Result", 152 | modifier = Modifier.padding(top = 12.sdp), 153 | fontFamily = UbuntuFont, 154 | fontWeight = FontWeight.Bold, 155 | fontSize = 15.ssp, 156 | color = Color.White, 157 | ) 158 | } 159 | items(viewModel.notesUiState.searchResult) { 160 | NoteItem( 161 | note = it, 162 | modifier = Modifier 163 | .padding(top = 12.sdp) 164 | .fillMaxWidth(), 165 | onClick = { 166 | navController.navigate( 167 | Screen.NoteDetailsScreen.route + "?noteId=${it.id}" 168 | ) 169 | } 170 | ) 171 | } 172 | item() { 173 | Text( 174 | text = "ALl Notes", 175 | modifier = Modifier.padding(top = 12.sdp), 176 | fontFamily = UbuntuFont, 177 | fontWeight = FontWeight.Bold, 178 | fontSize = 15.ssp, 179 | color = Color.White, 180 | ) 181 | } 182 | } 183 | items(viewModel.notesUiState.notes, key = { it.id }) { 184 | NoteItem( 185 | note = it, 186 | modifier = Modifier 187 | .padding(top = 12.sdp) 188 | .fillMaxWidth(), 189 | onClick = { 190 | navController.navigate( 191 | Screen.NoteDetailsScreen.route + "?noteId=${it.id}" 192 | ) 193 | } 194 | ) 195 | } 196 | } 197 | ) 198 | } 199 | 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/homeScreen/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.homeScreen 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.example.noteappcompose.data.source.remote.dataSource.NoteSocketDataSource 9 | import com.example.noteappcompose.domain.usecases.GetAllNotesUseCase 10 | import com.example.noteappcompose.domain.usecases.SearchUseCase 11 | import com.example.noteappcompose.presentation.homeScreen.uiStates.HomeUiEvent 12 | import com.example.noteappcompose.presentation.homeScreen.uiStates.NoteItemUiState 13 | import com.example.noteappcompose.presentation.homeScreen.uiStates.NotesUiState 14 | import dagger.hilt.android.lifecycle.HiltViewModel 15 | import kotlinx.coroutines.flow.MutableSharedFlow 16 | import kotlinx.coroutines.flow.SharedFlow 17 | import kotlinx.coroutines.flow.asSharedFlow 18 | import kotlinx.coroutines.launch 19 | import javax.inject.Inject 20 | 21 | @HiltViewModel 22 | class HomeViewModel @Inject constructor( 23 | private val getAllNotesUseCase: GetAllNotesUseCase, 24 | private val searchUseCase: SearchUseCase, 25 | ) : ViewModel() { 26 | var notesUiState by mutableStateOf(NotesUiState(isLoading = true)) 27 | 28 | private var _eventFlow = MutableSharedFlow() 29 | val eventFlow: SharedFlow = _eventFlow.asSharedFlow() 30 | 31 | init { 32 | viewModelScope.launch { 33 | try { 34 | notesUiState = notesUiState.copy( 35 | isLoading = true 36 | ) 37 | getAllNotesUseCase.invoke().collect { 38 | val newNotesList = it.map { note -> 39 | NoteItemUiState( 40 | id = note.id, 41 | noteTitle = note.noteTitle, 42 | noteSubtitle = note.noteSubtitle, 43 | noteDate = note.dateTime, 44 | noteColor = note.noteColor, 45 | imageLink = note.imageLink, 46 | isImageVisible = !note.imageLink.isNullOrBlank() 47 | ) 48 | } 49 | notesUiState = notesUiState.copy( 50 | notes = newNotesList, 51 | isLoading = false 52 | ) 53 | } 54 | } catch (e: Exception) { 55 | _eventFlow.emit(UiEvent.ShowMessage(e.message.toString())) 56 | notesUiState = notesUiState.copy( 57 | isLoading = false 58 | ) 59 | } 60 | } 61 | } 62 | 63 | fun search() { 64 | viewModelScope.launch { 65 | if (notesUiState.searchText.trim().isBlank()) 66 | notesUiState = notesUiState.copy(searchResult = emptyList()) 67 | else { 68 | try { 69 | notesUiState = notesUiState.copy( 70 | isLoading = true 71 | ) 72 | val searchResult = searchUseCase(notesUiState.searchText) 73 | val newSearchedList = searchResult.map { note -> 74 | NoteItemUiState( 75 | id = note.id, 76 | noteTitle = note.noteTitle, 77 | noteSubtitle = note.noteSubtitle, 78 | noteDate = note.dateTime, 79 | noteColor = note.noteColor, 80 | imageLink = note.imageLink, 81 | isImageVisible = !note.imageLink.isNullOrBlank() 82 | ) 83 | } 84 | notesUiState = notesUiState.copy(searchResult = newSearchedList , isLoading = false) 85 | } catch (e: Exception) { 86 | _eventFlow.emit(UiEvent.ShowMessage(e.message.toString())) 87 | notesUiState = notesUiState.copy( 88 | isLoading = false 89 | ) 90 | } 91 | } 92 | } 93 | } 94 | 95 | fun onEvent(action: HomeUiEvent) { 96 | when (action) { 97 | is HomeUiEvent.SearchTextChanged -> notesUiState = 98 | notesUiState.copy(searchText = action.text) 99 | 100 | HomeUiEvent.Search -> search() 101 | } 102 | } 103 | 104 | sealed class UiEvent { 105 | data class ShowMessage(var message: String) : UiEvent() 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/homeScreen/components/NoteItem.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.homeScreen.components 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.wrapContentHeight 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material3.Card 8 | import androidx.compose.material3.CardDefaults 9 | import androidx.compose.material3.CircularProgressIndicator 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.text.font.FontWeight 17 | import coil.compose.SubcomposeAsyncImage 18 | import com.example.noteappcompose.presentation.homeScreen.uiStates.NoteItemUiState 19 | import com.example.noteappcompose.presentation.theme.NoteSubtitle 20 | import com.example.noteappcompose.presentation.theme.UbuntuFont 21 | import ir.kaaveh.sdpcompose.sdp 22 | import ir.kaaveh.sdpcompose.ssp 23 | 24 | @OptIn(ExperimentalMaterial3Api::class) 25 | @Composable 26 | fun NoteItem(note: NoteItemUiState, modifier: Modifier = Modifier, onClick: () -> Unit) { 27 | Card( 28 | modifier = modifier, 29 | onClick = onClick, 30 | shape = RoundedCornerShape(10.sdp), 31 | colors = CardDefaults.cardColors(containerColor = Color(note.noteColor)), 32 | ) { 33 | if(note.isImageVisible){ 34 | SubcomposeAsyncImage( 35 | model = note.imageLink, 36 | loading = { 37 | CircularProgressIndicator() 38 | }, 39 | contentDescription = null, 40 | modifier = Modifier.fillMaxWidth().wrapContentHeight().clip(RoundedCornerShape(topEnd = 10.sdp, topStart = 10.sdp)) 41 | ) 42 | } 43 | Text( 44 | text = note.noteTitle, 45 | fontFamily = UbuntuFont, 46 | fontWeight = FontWeight.Bold, 47 | fontSize = 13.ssp, 48 | color = Color.White, 49 | modifier = Modifier.padding(top = 8.sdp, end = 8.sdp, start = 8.sdp) 50 | ) 51 | Text( 52 | text = note.noteSubtitle, 53 | fontFamily = UbuntuFont, 54 | fontWeight = FontWeight.Normal, 55 | fontSize = 12.ssp, 56 | color = NoteSubtitle, 57 | modifier = Modifier.padding( 58 | top = 4.sdp, end = 8.sdp, start = 8.sdp, bottom = 4.sdp 59 | ) 60 | ) 61 | Text( 62 | text = note.noteDate, 63 | fontFamily = UbuntuFont, 64 | fontWeight = FontWeight.Normal, 65 | fontSize = 7.ssp, 66 | color = NoteSubtitle, 67 | modifier = Modifier.padding(all = 8.sdp) 68 | ) 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/homeScreen/components/SearchField.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.homeScreen.components 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.foundation.text.KeyboardActions 7 | import androidx.compose.foundation.text.KeyboardOptions 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.filled.Search 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TextField 14 | import androidx.compose.material3.TextFieldDefaults 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.text.input.ImeAction 19 | import com.example.noteappcompose.presentation.theme.HintGray 20 | import com.example.noteappcompose.presentation.theme.SearchGray 21 | import com.example.noteappcompose.presentation.theme.SearchIcon 22 | import ir.kaaveh.sdpcompose.sdp 23 | import ir.kaaveh.sdpcompose.ssp 24 | 25 | @OptIn(ExperimentalMaterial3Api::class) 26 | @Composable 27 | fun SearchField( 28 | value: String, 29 | onValueChange: (String) -> Unit, 30 | onSearch: () -> Unit, 31 | modifier: Modifier = Modifier.fillMaxWidth() 32 | ) { 33 | TextField( 34 | value = value, 35 | onValueChange = onValueChange, 36 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), 37 | keyboardActions = KeyboardActions( 38 | onSearch = { onSearch() } 39 | ), 40 | leadingIcon = { 41 | Icon( 42 | imageVector = Icons.Default.Search, 43 | contentDescription = null, 44 | tint = SearchIcon 45 | ) 46 | }, 47 | colors = TextFieldDefaults.textFieldColors( 48 | containerColor = SearchGray, 49 | focusedIndicatorColor = Color.Transparent, 50 | unfocusedIndicatorColor = Color.Transparent, 51 | disabledIndicatorColor = Color.Transparent 52 | ), 53 | placeholder = { 54 | Text("Search", fontSize = 11.ssp, color = HintGray) 55 | }, 56 | shape = RoundedCornerShape(8.sdp), 57 | modifier = modifier 58 | .height(35.sdp) 59 | ) 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/homeScreen/components/StaggeredVerticalGrid.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.homeScreen.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.layout.Layout 6 | import androidx.compose.ui.unit.Dp 7 | import kotlin.math.ceil 8 | 9 | @Composable 10 | fun StaggeredVerticalGrid( 11 | modifier: Modifier = Modifier, 12 | maxColumnWidth: Dp, 13 | children: @Composable () -> Unit 14 | ) { 15 | Layout( 16 | content = children, 17 | modifier = modifier 18 | ) { measurables, constraints -> 19 | check(constraints.hasBoundedWidth) { 20 | "Unbounded width not supported" 21 | } 22 | val columns = ceil(constraints.maxWidth / maxColumnWidth.toPx()).toInt() 23 | val columnWidth = constraints.maxWidth / columns 24 | val itemConstraints = constraints.copy(maxWidth = columnWidth) 25 | val colHeights = IntArray(columns) { 0 } // track each column's height 26 | val placeables = measurables.map { measurable -> 27 | val column = shortestColumn(colHeights) 28 | val placeable = measurable.measure(itemConstraints) 29 | colHeights[column] += placeable.height 30 | placeable 31 | } 32 | 33 | val height = colHeights.maxOrNull()?.coerceIn(constraints.minHeight, constraints.maxHeight) 34 | ?: constraints.minHeight 35 | layout( 36 | width = constraints.maxWidth, 37 | height = height 38 | ) { 39 | val colY = IntArray(columns) { 0 } 40 | placeables.forEach { placeable -> 41 | val column = shortestColumn(colY) 42 | placeable.place( 43 | x = columnWidth * column, 44 | y = colY[column] 45 | ) 46 | colY[column] += placeable.height 47 | } 48 | } 49 | } 50 | } 51 | 52 | private fun shortestColumn(colHeights: IntArray): Int { 53 | var minHeight = Int.MAX_VALUE 54 | var column = 0 55 | colHeights.forEachIndexed { index, height -> 56 | if (height < minHeight) { 57 | minHeight = height 58 | column = index 59 | } 60 | } 61 | return column 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/homeScreen/uiStates/HomeUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.homeScreen.uiStates 2 | 3 | sealed class HomeUiEvent { 4 | data class SearchTextChanged(var text:String):HomeUiEvent() 5 | object Search:HomeUiEvent() 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/homeScreen/uiStates/NoteItemUiState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.homeScreen.uiStates 2 | 3 | import androidx.compose.ui.graphics.toArgb 4 | import com.example.noteappcompose.presentation.utilities.Constants 5 | 6 | data class NoteItemUiState( 7 | var id:String = "-1", 8 | var noteTitle: String = "", 9 | var noteSubtitle: String = "", 10 | var noteDate: String = "", 11 | var noteColor: Int = Constants.noteColorsList[0].toArgb(), 12 | var imageLink: String? = null, 13 | var isImageVisible: Boolean = false 14 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/homeScreen/uiStates/NotesUiState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.homeScreen.uiStates 2 | 3 | data class NotesUiState( 4 | var isLoading: Boolean = true, 5 | var notes: List = emptyList(), 6 | var searchText: String="", 7 | var searchResult: List = emptyList() 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/loginScreen/LoginScreen.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.loginScreen 2 | 3 | 4 | import android.annotation.SuppressLint 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.material.Scaffold 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.filled.Password 17 | import androidx.compose.material.rememberScaffoldState 18 | import androidx.compose.material3.ExperimentalMaterial3Api 19 | import androidx.compose.material3.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.LaunchedEffect 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.text.font.FontWeight 26 | import androidx.compose.ui.text.input.KeyboardType 27 | import androidx.hilt.navigation.compose.hiltViewModel 28 | import androidx.navigation.NavController 29 | import com.example.noteappcompose.presentation.loginScreen.components.OutlineInputField 30 | import com.example.noteappcompose.presentation.loginScreen.uiStates.LoginUiEvent 31 | import com.example.noteappcompose.presentation.registerScreen.components.LoadingButton 32 | import com.example.noteappcompose.presentation.theme.HintGray 33 | import com.example.noteappcompose.presentation.theme.LightGray 34 | import com.example.noteappcompose.presentation.theme.UbuntuFont 35 | import com.example.noteappcompose.presentation.utilities.Screen 36 | import ir.kaaveh.sdpcompose.sdp 37 | import ir.kaaveh.sdpcompose.ssp 38 | import kotlinx.coroutines.flow.collectLatest 39 | 40 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "UnusedMaterialScaffoldPaddingParameter") 41 | @ExperimentalMaterial3Api 42 | @Composable 43 | fun LoginScreen( 44 | navController: NavController, 45 | viewModel: LoginViewModel = hiltViewModel() 46 | ) { 47 | var scaffoldState = rememberScaffoldState() 48 | LaunchedEffect(key1 = true) { 49 | viewModel.eventFlow.collectLatest { event -> 50 | when (event) { 51 | LoginViewModel.UiEvent.LoginSuccess -> navController.navigate(Screen.HomeScreen.route) { 52 | popUpTo( 53 | 0 54 | ) 55 | } 56 | 57 | is LoginViewModel.UiEvent.ShowMessage -> scaffoldState.snackbarHostState.showSnackbar( 58 | event.error 59 | ) 60 | } 61 | } 62 | } 63 | Scaffold( 64 | modifier = Modifier.background(LightGray), 65 | scaffoldState = scaffoldState 66 | ) { 67 | Column( 68 | horizontalAlignment = Alignment.Start, 69 | modifier = Modifier 70 | .fillMaxSize() 71 | .background(LightGray) 72 | .padding(vertical = 25.sdp, horizontal = 15.sdp) 73 | ) { 74 | Text( 75 | text = "Welcome back!", 76 | fontFamily = UbuntuFont, 77 | fontWeight = FontWeight.Bold, 78 | fontSize = 18.ssp, 79 | color = Color.White, 80 | ) 81 | 82 | Text( 83 | text = "Sign in to your account", 84 | fontFamily = UbuntuFont, 85 | fontWeight = FontWeight.Normal, 86 | fontSize = 12.ssp, 87 | color = HintGray, 88 | modifier = Modifier.padding(vertical = 10.sdp), 89 | ) 90 | Spacer(Modifier.size(30.sdp)) 91 | OutlineInputField( 92 | text = viewModel.loginUiState.emailUiState.text, 93 | hint = "Email", 94 | modifier = Modifier.fillMaxWidth(), 95 | onValueChange = { viewModel.onEvent(LoginUiEvent.EmailChanged(it)) }, 96 | keyboardType = KeyboardType.Email, 97 | isErrorVisible = viewModel.loginUiState.emailUiState.errorMessage != null, 98 | error = viewModel.loginUiState.emailUiState.errorMessage 99 | ) 100 | Spacer(Modifier.size(20.sdp)) 101 | OutlineInputField( 102 | text = viewModel.loginUiState.passwordUiState.text, 103 | hint = "Password", 104 | modifier = Modifier.fillMaxWidth(), 105 | icon = Icons.Default.Password, 106 | onValueChange = { viewModel.onEvent(LoginUiEvent.PasswordChanged(it)) }, 107 | keyboardType = KeyboardType.Password, 108 | isErrorVisible = viewModel.loginUiState.passwordUiState.errorMessage != null, 109 | error = viewModel.loginUiState.passwordUiState.errorMessage 110 | ) 111 | LoadingButton( 112 | onClick = { viewModel.login() }, 113 | modifier = Modifier 114 | .padding(vertical = 20.sdp) 115 | .fillMaxWidth() 116 | .height(40.sdp), 117 | isEnabled = !viewModel.loginUiState.isLoading, 118 | isLoading = viewModel.loginUiState.isLoading 119 | ) 120 | 121 | // TextButton(onClick = { /*TODO*/ }) { 122 | Text( 123 | text = "don't have an account? sign up", 124 | modifier = Modifier 125 | .padding(top = 5.sdp) 126 | .clickable { 127 | navController.navigate(Screen.RegisterScreen.route) 128 | }, 129 | fontFamily = UbuntuFont, 130 | fontWeight = FontWeight.Normal, 131 | fontSize = 13.ssp, 132 | color = Color.White, 133 | ) 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/loginScreen/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.loginScreen 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.example.noteappcompose.domain.usecases.LoginUseCase 9 | import com.example.noteappcompose.domain.usecases.ValidateEmailUseCase 10 | import com.example.noteappcompose.domain.usecases.ValidatePasswordUseCase 11 | import com.example.noteappcompose.domain.utilitites.InvalidInputTextException 12 | import com.example.noteappcompose.presentation.loginScreen.uiStates.LoginUiEvent 13 | import com.example.noteappcompose.presentation.loginScreen.uiStates.LoginUiState 14 | import dagger.hilt.android.lifecycle.HiltViewModel 15 | import kotlinx.coroutines.flow.MutableSharedFlow 16 | import kotlinx.coroutines.flow.SharedFlow 17 | import kotlinx.coroutines.flow.asSharedFlow 18 | import kotlinx.coroutines.launch 19 | import javax.inject.Inject 20 | 21 | @HiltViewModel 22 | class LoginViewModel @Inject constructor( 23 | var loginUseCase: LoginUseCase, 24 | var validateEmailUseCase: ValidateEmailUseCase, 25 | var validatePasswordUseCase: ValidatePasswordUseCase 26 | ) : ViewModel() { 27 | var loginUiState by mutableStateOf(LoginUiState(isLoading = false)) 28 | private set; 29 | 30 | private var _eventFlow = MutableSharedFlow() 31 | val eventFlow: SharedFlow = _eventFlow.asSharedFlow() 32 | 33 | fun login() { 34 | viewModelScope.launch { 35 | loginUiState = loginUiState.copy(isLoading = true) 36 | val emailValidationResult = 37 | validateEmailUseCase(loginUiState.emailUiState.text) 38 | val passwordValidationResult = 39 | validatePasswordUseCase(loginUiState.passwordUiState.text) 40 | val hasValidationError = listOf( 41 | emailValidationResult, 42 | passwordValidationResult 43 | ).any { it.error != null } 44 | if (hasValidationError) { 45 | loginUiState = loginUiState.copy( 46 | emailUiState = loginUiState.emailUiState.copy( 47 | errorMessage = emailValidationResult.error 48 | ), 49 | passwordUiState = loginUiState.passwordUiState.copy( 50 | errorMessage = passwordValidationResult.error 51 | ), 52 | ) 53 | loginUiState = loginUiState.copy(isLoading = false) 54 | } else { 55 | try { 56 | var loginResult = loginUseCase(loginUiState.emailUiState.text, loginUiState.passwordUiState.text) 57 | 58 | if (loginResult.token.isNotBlank()) 59 | _eventFlow.emit(UiEvent.LoginSuccess) 60 | else 61 | _eventFlow.emit(UiEvent.ShowMessage("Unknown Error")) 62 | loginUiState = loginUiState.copy(isLoading = false) 63 | } catch (e: InvalidInputTextException) { 64 | loginUiState = loginUiState.copy( 65 | emailUiState = loginUiState.emailUiState.copy( 66 | errorMessage = validateEmailUseCase(loginUiState.emailUiState.text).error 67 | ), 68 | passwordUiState = loginUiState.passwordUiState.copy( 69 | errorMessage = validatePasswordUseCase(loginUiState.passwordUiState.text).error 70 | ), 71 | ) 72 | loginUiState = loginUiState.copy(isLoading = false) 73 | } catch (e: Exception) { 74 | e.printStackTrace() 75 | loginUiState = loginUiState.copy(isLoading = false) 76 | _eventFlow.emit(UiEvent.ShowMessage(e.message.toString())) 77 | } 78 | } 79 | } 80 | } 81 | 82 | fun onEvent(action: LoginUiEvent) { 83 | loginUiState = when (action) { 84 | is LoginUiEvent.EmailChanged -> loginUiState.copy( 85 | emailUiState = loginUiState.emailUiState.copy( 86 | errorMessage = null, 87 | text = action.text 88 | ) 89 | ) 90 | 91 | is LoginUiEvent.PasswordChanged -> loginUiState.copy( 92 | passwordUiState = loginUiState.passwordUiState.copy( 93 | errorMessage = null, 94 | text = action.text 95 | ) 96 | ) 97 | } 98 | } 99 | 100 | sealed class UiEvent { 101 | object LoginSuccess : UiEvent() 102 | data class ShowMessage(var error: String) : UiEvent() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/loginScreen/components/OutlineInputField.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.loginScreen.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.foundation.text.KeyboardOptions 9 | import androidx.compose.foundation.text.selection.TextSelectionColors 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.Email 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.OutlinedTextField 15 | import androidx.compose.material3.Text 16 | import androidx.compose.material3.TextFieldDefaults 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.vector.ImageVector 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.text.input.KeyboardType 23 | import com.example.noteappcompose.presentation.theme.BottomBarIcon 24 | import com.example.noteappcompose.presentation.theme.Orange 25 | import com.example.noteappcompose.presentation.theme.Red 26 | import com.example.noteappcompose.presentation.theme.UbuntuFont 27 | import ir.kaaveh.sdpcompose.sdp 28 | import ir.kaaveh.sdpcompose.ssp 29 | 30 | @OptIn(ExperimentalMaterial3Api::class) 31 | @Composable 32 | fun OutlineInputField( 33 | text: String, 34 | hint: String, 35 | modifier: Modifier = Modifier, 36 | icon:ImageVector = Icons.Default.Email, 37 | error: String? = null, 38 | isErrorVisible: Boolean = false, 39 | onValueChange: (String) -> Unit, 40 | keyboardType: KeyboardType 41 | ) { 42 | Column { 43 | OutlinedTextField( 44 | value = text, 45 | onValueChange = onValueChange, 46 | modifier = modifier, 47 | singleLine = true, 48 | keyboardOptions = KeyboardOptions(keyboardType = keyboardType), 49 | label = { 50 | Text(hint, color = BottomBarIcon) 51 | }, 52 | leadingIcon = { 53 | Icon( 54 | icon, 55 | contentDescription = hint, 56 | tint = BottomBarIcon 57 | ) 58 | }, 59 | shape = RoundedCornerShape(5.sdp), 60 | colors = TextFieldDefaults.outlinedTextFieldColors( 61 | textColor = Color.White, 62 | focusedBorderColor = BottomBarIcon, 63 | selectionColors = TextSelectionColors( 64 | handleColor = Orange, 65 | backgroundColor = Orange.copy(alpha = 0.4f) 66 | ), 67 | unfocusedBorderColor = BottomBarIcon, 68 | cursorColor = Orange 69 | ), 70 | ) 71 | AnimatedVisibility(isErrorVisible) { 72 | Text( 73 | text = error ?: "", 74 | modifier = Modifier 75 | .padding(start = 13.sdp, top = 3.sdp) 76 | .fillMaxWidth(), 77 | fontFamily = UbuntuFont, 78 | fontWeight = FontWeight.Normal, 79 | fontSize = 10.ssp, 80 | color = Red 81 | ) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/loginScreen/uiStates/LoginUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.loginScreen.uiStates 2 | 3 | sealed class LoginUiEvent { 4 | data class EmailChanged(var text:String):LoginUiEvent() 5 | data class PasswordChanged(var text:String):LoginUiEvent() 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/loginScreen/uiStates/LoginUiState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.loginScreen.uiStates 2 | 3 | import com.example.noteappcompose.presentation.noteDetailsScreen.uiStates.InputFieldUiState 4 | 5 | data class LoginUiState( 6 | val isLoading: Boolean = false, 7 | val emailUiState: InputFieldUiState = InputFieldUiState(), 8 | val passwordUiState: InputFieldUiState = InputFieldUiState() 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/noteDetailsScreen/NoteDetailScreen.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.noteDetailsScreen 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.net.Uri 6 | import android.util.Log 7 | import androidx.activity.compose.rememberLauncherForActivityResult 8 | import androidx.activity.result.contract.ActivityResultContracts 9 | import androidx.compose.animation.AnimatedVisibility 10 | import androidx.compose.animation.slideInHorizontally 11 | import androidx.compose.foundation.background 12 | import androidx.compose.foundation.layout.Arrangement 13 | import androidx.compose.foundation.layout.Box 14 | import androidx.compose.foundation.layout.Column 15 | import androidx.compose.foundation.layout.Row 16 | import androidx.compose.foundation.layout.Spacer 17 | import androidx.compose.foundation.layout.defaultMinSize 18 | import androidx.compose.foundation.layout.fillMaxSize 19 | import androidx.compose.foundation.layout.fillMaxWidth 20 | import androidx.compose.foundation.layout.height 21 | import androidx.compose.foundation.layout.padding 22 | import androidx.compose.foundation.layout.size 23 | import androidx.compose.foundation.layout.width 24 | import androidx.compose.foundation.layout.wrapContentHeight 25 | import androidx.compose.foundation.rememberScrollState 26 | import androidx.compose.foundation.shape.CircleShape 27 | import androidx.compose.foundation.shape.RoundedCornerShape 28 | import androidx.compose.foundation.verticalScroll 29 | import androidx.compose.material.BottomSheetScaffold 30 | import androidx.compose.material.BottomSheetState 31 | import androidx.compose.material.BottomSheetValue 32 | import androidx.compose.material.CircularProgressIndicator 33 | import androidx.compose.material.ExperimentalMaterialApi 34 | import androidx.compose.material.IconButton 35 | import androidx.compose.material.icons.Icons 36 | import androidx.compose.material.icons.filled.ArrowBackIos 37 | import androidx.compose.material.icons.filled.Delete 38 | import androidx.compose.material.icons.filled.Done 39 | import androidx.compose.material.icons.filled.Image 40 | import androidx.compose.material.icons.filled.Language 41 | import androidx.compose.material.rememberBottomSheetScaffoldState 42 | import androidx.compose.material3.ExperimentalMaterial3Api 43 | import androidx.compose.material3.Icon 44 | import androidx.compose.material3.Text 45 | import androidx.compose.runtime.Composable 46 | import androidx.compose.runtime.LaunchedEffect 47 | import androidx.compose.ui.Alignment 48 | import androidx.compose.ui.Modifier 49 | import androidx.compose.ui.draw.clip 50 | import androidx.compose.ui.graphics.Color 51 | import androidx.compose.ui.graphics.toArgb 52 | import androidx.compose.ui.platform.LocalContext 53 | import androidx.compose.ui.text.TextStyle 54 | import androidx.compose.ui.text.font.FontWeight 55 | import androidx.compose.ui.text.style.TextDecoration 56 | import androidx.hilt.navigation.compose.hiltViewModel 57 | import androidx.navigation.NavController 58 | import coil.compose.SubcomposeAsyncImage 59 | import com.example.noteappcompose.presentation.noteDetailsScreen.components.BottomSheetItem 60 | import com.example.noteappcompose.presentation.noteDetailsScreen.components.ColorBox 61 | import com.example.noteappcompose.presentation.noteDetailsScreen.components.NoteInputField 62 | import com.example.noteappcompose.presentation.noteDetailsScreen.components.ToolBarIconButton 63 | import com.example.noteappcompose.presentation.noteDetailsScreen.components.UrlDialog 64 | import com.example.noteappcompose.presentation.noteDetailsScreen.uiStates.NoteDetailsEvent 65 | import com.example.noteappcompose.presentation.theme.BottomBarIcon 66 | import com.example.noteappcompose.presentation.theme.BottomSheet 67 | import com.example.noteappcompose.presentation.theme.LightGray 68 | import com.example.noteappcompose.presentation.theme.Orange 69 | import com.example.noteappcompose.presentation.theme.Red 70 | import com.example.noteappcompose.presentation.theme.SubtitleGray 71 | import com.example.noteappcompose.presentation.theme.UbuntuFont 72 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 73 | import com.google.accompanist.permissions.rememberMultiplePermissionsState 74 | import ir.kaaveh.sdpcompose.sdp 75 | import ir.kaaveh.sdpcompose.ssp 76 | import kotlinx.coroutines.flow.collectLatest 77 | 78 | @OptIn(ExperimentalPermissionsApi::class) 79 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") 80 | @ExperimentalMaterialApi 81 | @ExperimentalMaterial3Api 82 | @Composable 83 | fun NoteDetailsScreen( 84 | viewModel: NoteDetailsViewModel = hiltViewModel(), 85 | navController: NavController 86 | ) { 87 | val context = LocalContext.current 88 | val permissionsState = rememberMultiplePermissionsState( 89 | permissions = listOf( 90 | Manifest.permission.READ_EXTERNAL_STORAGE, 91 | ) 92 | ) 93 | val imagePermissionLauncher = rememberLauncherForActivityResult( 94 | contract = ActivityResultContracts.OpenDocument(), 95 | onResult = { uri: Uri? -> 96 | if (uri != null) { 97 | viewModel.onEvent(NoteDetailsEvent.SelectedImage(uri)) 98 | } 99 | // viewModel.onEvent(NoteDetailsEvent.SelectedImage(uri)) 100 | } 101 | ) 102 | val bottomState = BottomSheetState(BottomSheetValue.Collapsed) 103 | val scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = bottomState) 104 | LaunchedEffect(key1 = true) { 105 | viewModel.eventFlow.collectLatest { event -> 106 | when (event) { 107 | NoteDetailsViewModel.UiEvent.SaveNoteSuccess -> navController.navigateUp() 108 | is NoteDetailsViewModel.UiEvent.ShowMessage -> scaffoldState.snackbarHostState.showSnackbar( 109 | event.error 110 | ) 111 | } 112 | } 113 | } 114 | BottomSheetScaffold( 115 | sheetContent = { 116 | Column( 117 | modifier = Modifier 118 | .fillMaxWidth() 119 | .defaultMinSize(minHeight = 195.sdp), 120 | horizontalAlignment = Alignment.CenterHorizontally 121 | ) { 122 | 123 | Box( 124 | modifier = Modifier 125 | .height(40.sdp) 126 | .fillMaxWidth(), 127 | contentAlignment = Alignment.Center 128 | ) { 129 | Text( 130 | text = "properties", 131 | fontFamily = UbuntuFont, 132 | fontWeight = FontWeight.Medium, 133 | color = Color.White, 134 | fontSize = 13.ssp, 135 | ) 136 | } 137 | 138 | Row( 139 | modifier = Modifier 140 | .padding(top = 10.sdp) 141 | .fillMaxWidth(), 142 | horizontalArrangement = Arrangement.Start, 143 | verticalAlignment = Alignment.CenterVertically 144 | ) { 145 | viewModel.noteDetailsUiState.noteColors.forEach { 146 | val colorAsInt = it.toArgb() 147 | Spacer(modifier = Modifier.size(10.sdp)) 148 | ColorBox( 149 | color = it, 150 | onClick = { 151 | viewModel.onEvent(NoteDetailsEvent.ClickColor(colorAsInt)) 152 | }, 153 | isCheckIconVisible = viewModel.noteDetailsUiState.noteColor == colorAsInt 154 | ) 155 | } 156 | Spacer(modifier = Modifier.weight(1f)) 157 | Text( 158 | text = "Pick Color", 159 | modifier = Modifier.padding(horizontal = 5.sdp), 160 | fontFamily = UbuntuFont, 161 | fontWeight = FontWeight.Medium, 162 | color = Color.White, 163 | fontSize = 13.ssp, 164 | ) 165 | } 166 | Spacer(modifier = Modifier.size(20.sdp)) 167 | BottomSheetItem(icon = Icons.Filled.Image, text = "Add Image", onClick = { 168 | val permissionState = permissionsState.permissions.get(0) 169 | when { 170 | permissionState.hasPermission -> { 171 | Log.d("aaaaaaaaaaaaaaaa", "kkjjkjjkj1") 172 | imagePermissionLauncher.launch( 173 | arrayOf( 174 | "image/png", 175 | "image/jpg", 176 | "image/jpeg" 177 | ) 178 | ) 179 | } 180 | 181 | permissionState.shouldShowRationale -> { 182 | Log.d("aaaaaaaaaaaaaaaa", "kkjjkjjkj2") 183 | permissionsState.launchMultiplePermissionRequest() 184 | } 185 | 186 | !permissionState.shouldShowRationale && !permissionState.hasPermission -> { 187 | Log.d("aaaaaaaaaaaaaaaa", "kkjjkjjkj3") 188 | // TODO("SNAKBAR") 189 | // Text( 190 | // text = "Camera permission was permanently" + 191 | // "denied. You can enable it in the app" + 192 | // "settings." 193 | // ) 194 | } 195 | } 196 | }) 197 | Spacer(modifier = Modifier.size(7.sdp)) 198 | 199 | BottomSheetItem( 200 | icon = Icons.Filled.Language, 201 | text = "Add URL", 202 | onClick = { viewModel.onEvent(NoteDetailsEvent.ShowUrlDialog) } 203 | ) 204 | Spacer(modifier = Modifier.size(7.sdp)) 205 | } 206 | }, 207 | modifier = Modifier.background(LightGray), 208 | backgroundColor = LightGray, 209 | scaffoldState = scaffoldState, 210 | sheetPeekHeight = 40.sdp, 211 | sheetShape = RoundedCornerShape(topStart = 15.sdp, topEnd = 15.sdp), 212 | sheetBackgroundColor = BottomSheet 213 | ) { 214 | if (viewModel.noteDetailsUiState.isLoading) { 215 | CircularProgressIndicator(modifier = Modifier.padding(top = 50.sdp, start = 50.sdp).size(30.sdp)) 216 | } else { 217 | Column( 218 | modifier = Modifier 219 | .verticalScroll(rememberScrollState()) 220 | .fillMaxSize() 221 | .background(LightGray) 222 | .padding(vertical = 11.sdp, horizontal = 0.sdp), 223 | ) { 224 | if (viewModel.noteDetailsUiState.linkUiState.isLinkDialogVisible) { 225 | UrlDialog( 226 | text = viewModel.noteDetailsUiState.linkUiState.typedLink, 227 | onTextChange = { viewModel.onEvent(NoteDetailsEvent.UrlTextChanged(it)) }, 228 | onClickAdd = { viewModel.onEvent(NoteDetailsEvent.AddUrlDialog) }, 229 | onClickDismiss = { viewModel.onEvent(NoteDetailsEvent.DismissUrlDialog) }, 230 | error = viewModel.noteDetailsUiState.linkUiState.linkError, 231 | isErrorVisible = viewModel.noteDetailsUiState.linkUiState.linkError != null 232 | ) 233 | } 234 | Row(modifier = Modifier.padding(start = 5.sdp, end = 5.sdp)) { 235 | ToolBarIconButton( 236 | icon = Icons.Default.ArrowBackIos, 237 | onClick = { navController.navigateUp() }, 238 | modifier = Modifier.size(20.sdp) 239 | ) 240 | Spacer(modifier = Modifier.weight(1f)) 241 | ToolBarIconButton( 242 | icon = Icons.Default.Done, 243 | onClick = { viewModel.onEvent(NoteDetailsEvent.SaveNote) }, 244 | modifier = Modifier.size(20.sdp) 245 | ) 246 | } 247 | NoteInputField( 248 | text = viewModel.noteDetailsUiState.titleInputFieldUiState.text, 249 | hint = "Note title", 250 | error = viewModel.noteDetailsUiState.titleInputFieldUiState.errorMessage, 251 | isErrorVisible = viewModel.noteDetailsUiState.titleInputFieldUiState.errorMessage != null, 252 | modifier = Modifier.fillMaxWidth(), 253 | onValueChange = { 254 | viewModel.onEvent(NoteDetailsEvent.TitleChanged(it)) 255 | }, 256 | textStyle = TextStyle( 257 | fontFamily = UbuntuFont, 258 | fontWeight = FontWeight.Bold, 259 | fontSize = 16.ssp, 260 | ) 261 | ) 262 | Text( 263 | text = viewModel.noteDetailsUiState.dateTime, 264 | modifier = Modifier 265 | .padding(start = 13.sdp) 266 | .fillMaxWidth(), 267 | fontFamily = UbuntuFont, 268 | fontWeight = FontWeight.Normal, 269 | fontSize = 10.ssp, 270 | color = BottomBarIcon 271 | ) 272 | Spacer(modifier = Modifier.size(8.sdp)) 273 | Row( 274 | modifier = Modifier 275 | .fillMaxWidth() 276 | .padding(start = 13.sdp) 277 | ) { 278 | Box( 279 | modifier = Modifier 280 | .padding(top = 4.sdp) 281 | .width(width = 5.sdp) 282 | .height(35.sdp) 283 | .clip(RoundedCornerShape(10.sdp)) 284 | .background(Color(viewModel.noteDetailsUiState.noteColor)) 285 | ) 286 | NoteInputField( 287 | text = viewModel.noteDetailsUiState.subtitleInputFieldUiState.text, 288 | hint = "Note subtitle", 289 | modifier = Modifier.fillMaxWidth(), 290 | error = viewModel.noteDetailsUiState.subtitleInputFieldUiState.errorMessage, 291 | isErrorVisible = viewModel.noteDetailsUiState.subtitleInputFieldUiState.errorMessage != null, 292 | onValueChange = { 293 | viewModel.onEvent(NoteDetailsEvent.SubtitleChanged(it)) 294 | }, 295 | textStyle = TextStyle( 296 | fontFamily = UbuntuFont, 297 | fontWeight = FontWeight.Medium, 298 | fontSize = 13.ssp, 299 | ), 300 | textColor = SubtitleGray 301 | ) 302 | } 303 | Spacer(modifier = Modifier.size(5.sdp)) 304 | AnimatedVisibility(visible = viewModel.noteDetailsUiState.linkUiState.isLinkVisible) { 305 | Row( 306 | modifier = Modifier.padding(start = 12.sdp, end = 12.sdp), 307 | verticalAlignment = Alignment.CenterVertically 308 | ) { 309 | Text( 310 | text = viewModel.noteDetailsUiState.linkUiState.finalLink ?: "", 311 | modifier = Modifier.weight(1f), 312 | fontFamily = UbuntuFont, 313 | fontWeight = FontWeight.Normal, 314 | fontSize = 13.ssp, 315 | color = Orange, 316 | textDecoration = TextDecoration.Underline 317 | ) 318 | IconButton(onClick = { viewModel.onEvent(NoteDetailsEvent.DeleteUrl) }) { 319 | Icon( 320 | Icons.Default.Delete, 321 | contentDescription = "delete url", 322 | modifier = Modifier.size(20.sdp), 323 | tint = Red 324 | ) 325 | } 326 | } 327 | } 328 | AnimatedVisibility( 329 | viewModel.noteDetailsUiState.isImageVisible, 330 | enter = slideInHorizontally() 331 | ) { 332 | Column { 333 | Spacer(modifier = Modifier.size(5.sdp)) 334 | Box( 335 | contentAlignment = Alignment.TopEnd, 336 | modifier = Modifier.padding(horizontal = 15.sdp) 337 | ) { 338 | if (viewModel.noteDetailsUiState.imageLink == null) { 339 | CircularProgressIndicator() 340 | } else { 341 | SubcomposeAsyncImage( 342 | model = viewModel.noteDetailsUiState.imageLink ?: "", 343 | loading = { 344 | CircularProgressIndicator() 345 | }, 346 | contentDescription = null 347 | ) 348 | IconButton( 349 | onClick = { viewModel.onEvent(NoteDetailsEvent.DeleteImage) }, 350 | Modifier 351 | .padding(top = 10.sdp, end = 10.sdp) 352 | .size(25.sdp) 353 | .clip(CircleShape) 354 | .background(Red) 355 | .padding(4.sdp) 356 | ) { 357 | Icon( 358 | imageVector = Icons.Default.Delete, 359 | contentDescription = null, 360 | tint = Color.White, 361 | ) 362 | } 363 | } 364 | } 365 | } 366 | } 367 | NoteInputField( 368 | text = viewModel.noteDetailsUiState.descriptionInputFieldUiState.text, 369 | hint = "Type note here", 370 | modifier = Modifier 371 | .fillMaxWidth() 372 | .wrapContentHeight(), 373 | error = viewModel.noteDetailsUiState.descriptionInputFieldUiState.errorMessage, 374 | isErrorVisible = viewModel.noteDetailsUiState.descriptionInputFieldUiState.errorMessage != null, 375 | onValueChange = { 376 | viewModel.onEvent(NoteDetailsEvent.NoteTextChanged(it)) 377 | }, 378 | singleLine = false, 379 | textStyle = TextStyle( 380 | fontFamily = UbuntuFont, 381 | fontWeight = FontWeight.Normal, 382 | fontSize = 13.ssp, 383 | ) 384 | ) 385 | } 386 | } 387 | } 388 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/noteDetailsScreen/NoteDetailsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.noteDetailsScreen 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.SavedStateHandle 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import com.example.noteappcompose.data.utilities.Constants.CREATE_NEW_NOTE_STATE_ID 10 | import com.example.noteappcompose.domain.usecases.AddEditNoteUseCase 11 | import com.example.noteappcompose.domain.usecases.GetNoteDetailsUseCase 12 | import com.example.noteappcompose.domain.usecases.UploadImageUseCase 13 | import com.example.noteappcompose.domain.usecases.ValidateNoteDescriptionUseCase 14 | import com.example.noteappcompose.domain.usecases.ValidateNoteSubtitleUseCase 15 | import com.example.noteappcompose.domain.usecases.ValidateNoteTitleUseCase 16 | import com.example.noteappcompose.domain.usecases.ValidateWebLinkUseCase 17 | import com.example.noteappcompose.domain.utilitites.InvalidInputTextException 18 | import com.example.noteappcompose.presentation.noteDetailsScreen.uiStates.LinkUiState 19 | import com.example.noteappcompose.presentation.noteDetailsScreen.uiStates.NoteDetailsEvent 20 | import com.example.noteappcompose.presentation.noteDetailsScreen.uiStates.NoteDetailsUiState 21 | import dagger.hilt.android.lifecycle.HiltViewModel 22 | import kotlinx.coroutines.flow.MutableSharedFlow 23 | import kotlinx.coroutines.flow.SharedFlow 24 | import kotlinx.coroutines.flow.asSharedFlow 25 | import kotlinx.coroutines.launch 26 | import javax.inject.Inject 27 | 28 | @HiltViewModel 29 | class NoteDetailsViewModel @Inject constructor( 30 | var addEditNoteUseCase: AddEditNoteUseCase, 31 | var getNoteDetailsUseCase: GetNoteDetailsUseCase, 32 | var validateWebLinkUseCase: ValidateWebLinkUseCase, 33 | var validateNoteTitleUseCase: ValidateNoteTitleUseCase, 34 | var validateNoteSubtitleUseCase: ValidateNoteSubtitleUseCase, 35 | var validateNoteDescriptionUseCase: ValidateNoteDescriptionUseCase, 36 | var uploadImageUseCase: UploadImageUseCase, 37 | savedStateHandle: SavedStateHandle 38 | ) : ViewModel() { 39 | var noteDetailsUiState by mutableStateOf(NoteDetailsUiState()) 40 | private set; 41 | 42 | private var _eventFlow = MutableSharedFlow() 43 | val eventFlow: SharedFlow = _eventFlow.asSharedFlow() 44 | 45 | 46 | init { 47 | savedStateHandle.get("noteId")?.let { noteId -> 48 | if (noteId != CREATE_NEW_NOTE_STATE_ID) { 49 | viewModelScope.launch { 50 | try { 51 | noteDetailsUiState = noteDetailsUiState.copy(isLoading = true) 52 | getNoteDetailsUseCase(noteId).also { note -> 53 | noteDetailsUiState = noteDetailsUiState.copy(isLoading = true) 54 | val linkUiState = LinkUiState( 55 | finalLink = note.webLink, 56 | isLinkVisible = !note.webLink.isNullOrBlank(), 57 | ) 58 | noteDetailsUiState = noteDetailsUiState.copy( 59 | id = noteId, 60 | titleInputFieldUiState = noteDetailsUiState.titleInputFieldUiState.copy( 61 | text = note.noteTitle 62 | ), 63 | subtitleInputFieldUiState = noteDetailsUiState.subtitleInputFieldUiState.copy( 64 | text = note.noteSubtitle 65 | ), 66 | descriptionInputFieldUiState = noteDetailsUiState.descriptionInputFieldUiState.copy( 67 | text = note.noteText 68 | ), 69 | dateTime = note.dateTime, 70 | linkUiState = linkUiState, 71 | imageLink = note.imageLink, 72 | isImageVisible = !note.imageLink.isNullOrBlank(), 73 | noteColor = note.noteColor, 74 | isLoading = false 75 | ) 76 | } 77 | }catch (e:Exception){ 78 | noteDetailsUiState = noteDetailsUiState.copy(isLoading = false) 79 | _eventFlow.emit(UiEvent.ShowMessage(e.message.toString())) 80 | } 81 | 82 | } 83 | } 84 | } 85 | } 86 | 87 | fun onEvent(action: NoteDetailsEvent) { 88 | when (action) { 89 | is NoteDetailsEvent.TitleChanged -> noteDetailsUiState = 90 | noteDetailsUiState.copy( 91 | titleInputFieldUiState = noteDetailsUiState.titleInputFieldUiState.copy( 92 | text = action.text, 93 | errorMessage = null 94 | ) 95 | ) 96 | 97 | is NoteDetailsEvent.SubtitleChanged -> noteDetailsUiState = 98 | noteDetailsUiState.copy( 99 | subtitleInputFieldUiState = noteDetailsUiState.subtitleInputFieldUiState.copy( 100 | text = action.text, 101 | errorMessage = null 102 | ) 103 | ) 104 | 105 | is NoteDetailsEvent.NoteTextChanged -> noteDetailsUiState = 106 | noteDetailsUiState.copy( 107 | descriptionInputFieldUiState = noteDetailsUiState.descriptionInputFieldUiState.copy( 108 | text = action.text, 109 | errorMessage = null 110 | ) 111 | ) 112 | 113 | is NoteDetailsEvent.ClickColor -> { 114 | noteDetailsUiState = noteDetailsUiState.copy( 115 | noteColor = action.color, 116 | ) 117 | } 118 | 119 | is NoteDetailsEvent.SaveNote -> { 120 | viewModelScope.launch { 121 | val titleValidationResult = 122 | validateNoteTitleUseCase(noteDetailsUiState.titleInputFieldUiState.text) 123 | val subtitleValidationResult = 124 | validateNoteSubtitleUseCase(noteDetailsUiState.subtitleInputFieldUiState.text) 125 | val descriptionValidationResult = 126 | validateNoteDescriptionUseCase(noteDetailsUiState.descriptionInputFieldUiState.text) 127 | val hasValidationError = listOf( 128 | titleValidationResult, 129 | subtitleValidationResult, 130 | descriptionValidationResult 131 | ).any { it.error != null } 132 | if (hasValidationError) { 133 | noteDetailsUiState = noteDetailsUiState.copy( 134 | titleInputFieldUiState = noteDetailsUiState.titleInputFieldUiState.copy( 135 | errorMessage = titleValidationResult.error 136 | ), 137 | subtitleInputFieldUiState = noteDetailsUiState.subtitleInputFieldUiState.copy( 138 | errorMessage = subtitleValidationResult.error 139 | ), 140 | descriptionInputFieldUiState = noteDetailsUiState.descriptionInputFieldUiState.copy( 141 | errorMessage = descriptionValidationResult.error 142 | ) 143 | ) 144 | } else { 145 | try { 146 | noteDetailsUiState = noteDetailsUiState.copy(isLoading = true) 147 | addEditNoteUseCase( 148 | id = noteDetailsUiState.id, 149 | title = noteDetailsUiState.titleInputFieldUiState.text, 150 | subtitle = noteDetailsUiState.subtitleInputFieldUiState.text, 151 | description = noteDetailsUiState.descriptionInputFieldUiState.text, 152 | imageLink = noteDetailsUiState.imageLink, 153 | color = noteDetailsUiState.noteColor, 154 | webLink = noteDetailsUiState.linkUiState.finalLink 155 | ) 156 | noteDetailsUiState = noteDetailsUiState.copy(isLoading = false) 157 | _eventFlow.emit(UiEvent.SaveNoteSuccess) 158 | } catch (e: InvalidInputTextException) { 159 | noteDetailsUiState = noteDetailsUiState.copy( 160 | titleInputFieldUiState = noteDetailsUiState.titleInputFieldUiState.copy( 161 | errorMessage = validateNoteTitleUseCase(noteDetailsUiState.titleInputFieldUiState.text).error 162 | ), 163 | subtitleInputFieldUiState = noteDetailsUiState.subtitleInputFieldUiState.copy( 164 | errorMessage = validateNoteSubtitleUseCase(noteDetailsUiState.subtitleInputFieldUiState.text).error 165 | ), 166 | descriptionInputFieldUiState = noteDetailsUiState.descriptionInputFieldUiState.copy( 167 | errorMessage = validateNoteDescriptionUseCase(noteDetailsUiState.descriptionInputFieldUiState.text).error 168 | ), 169 | ) 170 | _eventFlow.emit(UiEvent.ShowMessage(e.message.toString())) 171 | noteDetailsUiState = noteDetailsUiState.copy(isLoading = false) 172 | } catch (e: Exception) { 173 | noteDetailsUiState = noteDetailsUiState.copy(isLoading = false) 174 | _eventFlow.emit(UiEvent.ShowMessage(e.message.toString())) 175 | } 176 | } 177 | } 178 | } 179 | 180 | is NoteDetailsEvent.AddUrlDialog -> { 181 | val webLinkValidationResult = 182 | validateWebLinkUseCase(webLink = noteDetailsUiState.linkUiState.typedLink) 183 | if (webLinkValidationResult.error == null) { 184 | noteDetailsUiState = 185 | noteDetailsUiState.copy( 186 | linkUiState = noteDetailsUiState.linkUiState.copy( 187 | isLinkDialogVisible = false, 188 | finalLink = noteDetailsUiState.linkUiState.typedLink, 189 | isLinkVisible = noteDetailsUiState.linkUiState.typedLink.isNotBlank(), 190 | linkError = null 191 | ), 192 | ) 193 | } else { 194 | noteDetailsUiState = 195 | noteDetailsUiState.copy( 196 | linkUiState = noteDetailsUiState.linkUiState.copy( 197 | linkError = webLinkValidationResult.error 198 | ), 199 | ) 200 | } 201 | } 202 | 203 | is NoteDetailsEvent.DismissUrlDialog -> noteDetailsUiState = 204 | noteDetailsUiState.copy( 205 | linkUiState = noteDetailsUiState.linkUiState.copy( 206 | isLinkDialogVisible = false, 207 | linkError = null 208 | ), 209 | ) 210 | 211 | is NoteDetailsEvent.UrlTextChanged -> noteDetailsUiState = 212 | noteDetailsUiState.copy( 213 | linkUiState = noteDetailsUiState.linkUiState.copy( 214 | typedLink = action.text, 215 | linkError = null, 216 | ), 217 | ) 218 | 219 | NoteDetailsEvent.ShowUrlDialog -> noteDetailsUiState = 220 | noteDetailsUiState.copy( 221 | linkUiState = noteDetailsUiState.linkUiState.copy( 222 | isLinkDialogVisible = true 223 | ), 224 | ) 225 | 226 | NoteDetailsEvent.DeleteUrl -> noteDetailsUiState = 227 | noteDetailsUiState.copy( 228 | linkUiState = noteDetailsUiState.linkUiState.copy( 229 | finalLink = null, 230 | isLinkVisible = false 231 | ), 232 | ) 233 | 234 | is NoteDetailsEvent.SelectedImage -> { 235 | viewModelScope.launch { 236 | try { 237 | noteDetailsUiState = noteDetailsUiState.copy(isLoading = true) 238 | var uploadImageResult = uploadImageUseCase(action.image) 239 | noteDetailsUiState = noteDetailsUiState.copy( 240 | isImageVisible = true, 241 | imageLink = uploadImageResult, 242 | isLoading = false 243 | ) 244 | } catch (e: Exception) { 245 | _eventFlow.emit(UiEvent.ShowMessage(e.message.toString())) 246 | noteDetailsUiState = noteDetailsUiState.copy(isLoading = false) 247 | } 248 | } 249 | 250 | } 251 | 252 | NoteDetailsEvent.DeleteImage -> noteDetailsUiState = 253 | noteDetailsUiState.copy( 254 | isImageVisible = false, 255 | imageLink = null 256 | ) 257 | } 258 | } 259 | sealed class UiEvent { 260 | object SaveNoteSuccess : UiEvent() 261 | data class ShowMessage(var error: String) : UiEvent() 262 | } 263 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/noteDetailsScreen/components/BottomSheetItem.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.noteDetailsScreen.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.vector.ImageVector 16 | import androidx.compose.ui.text.font.FontWeight 17 | import com.example.noteappcompose.presentation.theme.BottomBarIcon 18 | import com.example.noteappcompose.presentation.theme.UbuntuFont 19 | import ir.kaaveh.sdpcompose.sdp 20 | import ir.kaaveh.sdpcompose.ssp 21 | 22 | @Composable 23 | fun BottomSheetItem(icon: ImageVector, text: String, onClick: () -> Unit,color:Color = BottomBarIcon) { 24 | Row( 25 | Modifier 26 | .padding(start = 10.sdp) 27 | .fillMaxWidth() 28 | .height(35.sdp) 29 | .clickable { 30 | onClick() 31 | }, 32 | verticalAlignment = Alignment.CenterVertically 33 | ) { 34 | Icon( 35 | imageVector = icon, 36 | contentDescription = null, 37 | modifier = Modifier.size(22.sdp), 38 | tint = color 39 | ) 40 | Text( 41 | text = text, 42 | modifier = Modifier.padding(start = 10.sdp), 43 | fontFamily = UbuntuFont, 44 | fontWeight = FontWeight.Medium, 45 | color = color, 46 | fontSize = 12.ssp, 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/noteDetailsScreen/components/ColorBox.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.noteDetailsScreen.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.shape.CircleShape 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.Check 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import com.example.noteappcompose.presentation.theme.SearchGray 18 | import ir.kaaveh.sdpcompose.sdp 19 | 20 | @Composable 21 | fun ColorBox( 22 | color: Color= SearchGray, 23 | isCheckIconVisible: Boolean = false, 24 | onClick:() -> Unit, 25 | ) { 26 | Box( 27 | contentAlignment= Alignment.Center, 28 | modifier = Modifier 29 | .size(35.sdp) 30 | .border( 31 | width = 2.sdp, 32 | color = color, 33 | shape = CircleShape 34 | ).clickable { 35 | onClick() 36 | }, 37 | ){ 38 | if(isCheckIconVisible){ 39 | 40 | Icon( 41 | imageVector = Icons.Filled.Check, 42 | contentDescription = "contentDescription", 43 | modifier = Modifier 44 | .size(23.sdp) 45 | .background(color, CircleShape) 46 | .padding(4.sdp), 47 | tint = Color.White 48 | ) 49 | }else{ 50 | Box( 51 | modifier = Modifier 52 | .size(23.sdp) 53 | .background(color, CircleShape) 54 | .padding(4.sdp) 55 | ) 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/noteDetailsScreen/components/NoteInputField.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.noteDetailsScreen.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.text.selection.TextSelectionColors 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.TextField 11 | import androidx.compose.material3.TextFieldDefaults 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.text.TextStyle 16 | import androidx.compose.ui.text.font.FontWeight 17 | import com.example.noteappcompose.presentation.theme.HintGray 18 | import com.example.noteappcompose.presentation.theme.Orange 19 | import com.example.noteappcompose.presentation.theme.Red 20 | import com.example.noteappcompose.presentation.theme.UbuntuFont 21 | import ir.kaaveh.sdpcompose.sdp 22 | import ir.kaaveh.sdpcompose.ssp 23 | 24 | @OptIn(ExperimentalMaterial3Api::class) 25 | @Composable 26 | fun NoteInputField( 27 | text: String, 28 | hint: String, 29 | modifier: Modifier = Modifier, 30 | error: String? = null, 31 | isErrorVisible: Boolean = false, 32 | onValueChange: (String) -> Unit, 33 | textStyle: TextStyle = TextStyle(), 34 | singleLine: Boolean = true, 35 | textColor: Color = Color.White 36 | ) { 37 | Column { 38 | 39 | TextField( 40 | value = text, 41 | onValueChange = onValueChange, 42 | singleLine = singleLine, 43 | modifier = modifier, 44 | colors = TextFieldDefaults.textFieldColors( 45 | textColor = textColor, 46 | selectionColors = TextSelectionColors( 47 | handleColor = Orange, 48 | backgroundColor = Orange.copy(alpha = 0.4f) 49 | ), 50 | containerColor = Color.Transparent, 51 | cursorColor = Orange, 52 | focusedIndicatorColor = Color.Transparent, 53 | unfocusedIndicatorColor = Color.Transparent, 54 | disabledIndicatorColor = Color.Transparent 55 | ), 56 | textStyle = textStyle, 57 | placeholder = { Text(hint, style = textStyle, color = HintGray) }, 58 | 59 | ) 60 | AnimatedVisibility(isErrorVisible){ 61 | Text( 62 | text = error?:"", 63 | modifier = Modifier 64 | .padding(start = 13.sdp) 65 | .fillMaxWidth(), 66 | fontFamily = UbuntuFont, 67 | fontWeight = FontWeight.Normal, 68 | fontSize = 10.ssp, 69 | color = Red 70 | ) 71 | } 72 | } 73 | // TextField( 74 | // value = text, 75 | // onValueChange = { }, 76 | // singleLine = singleLine, 77 | // modifier = modifier, 78 | // fontSize = fontSize, 79 | // placeholder = Text(hint, color = HintGray), 80 | // fontFamily = font, 81 | // ) 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/noteDetailsScreen/components/ToolBarIconButton.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.noteDetailsScreen.components 2 | 3 | import androidx.compose.material.IconButton 4 | import androidx.compose.material3.Icon 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import com.example.noteappcompose.presentation.theme.BottomBarIcon 9 | 10 | @Composable 11 | fun ToolBarIconButton(icon: ImageVector, modifier: Modifier =Modifier,onClick: () -> Unit) { 12 | IconButton(onClick = onClick) { 13 | Icon( 14 | icon, 15 | contentDescription = null, 16 | modifier = modifier, 17 | tint = BottomBarIcon 18 | ) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/noteDetailsScreen/components/UrlDialog.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.noteDetailsScreen.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material.icons.Icons 13 | import androidx.compose.material.icons.filled.Language 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.Surface 16 | import androidx.compose.material3.Text 17 | import androidx.compose.material3.TextButton 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.text.TextStyle 23 | import androidx.compose.ui.text.font.FontWeight 24 | import androidx.compose.ui.window.Dialog 25 | import com.example.noteappcompose.presentation.theme.Orange 26 | import com.example.noteappcompose.presentation.theme.SearchGray 27 | import com.example.noteappcompose.presentation.theme.UbuntuFont 28 | import ir.kaaveh.sdpcompose.sdp 29 | import ir.kaaveh.sdpcompose.ssp 30 | 31 | @Composable 32 | fun UrlDialog( 33 | text: String, 34 | error: String? = null, 35 | isErrorVisible: Boolean = false, 36 | onTextChange: (String) -> Unit, 37 | onClickAdd: () -> Unit, 38 | onClickDismiss: () -> Unit 39 | ) { 40 | Dialog(onDismissRequest = { onClickDismiss() }) { 41 | Surface( 42 | shape = RoundedCornerShape(10.sdp), 43 | color = SearchGray 44 | ) { 45 | Box() { 46 | Column(Modifier.padding(8.sdp)) { 47 | Row( 48 | modifier = Modifier.fillMaxWidth(), 49 | verticalAlignment = Alignment.CenterVertically 50 | ) { 51 | Icon( 52 | imageVector = Icons.Filled.Language, 53 | contentDescription = null, 54 | modifier = Modifier 55 | .size(25.sdp), 56 | tint = Color.White 57 | ) 58 | Text( 59 | text = "Add URL", 60 | modifier = Modifier.padding(start = 8.sdp), 61 | fontFamily = UbuntuFont, 62 | fontWeight = FontWeight.Medium, 63 | color = Color.White, 64 | fontSize = 14.ssp, 65 | ) 66 | } 67 | NoteInputField( 68 | text = text, 69 | hint = "Enter Url", 70 | error = error, 71 | isErrorVisible = isErrorVisible, 72 | onValueChange = onTextChange, 73 | textStyle = TextStyle(fontSize = 13.ssp), 74 | modifier = Modifier 75 | .fillMaxWidth() 76 | .height(50.sdp) 77 | ) 78 | Row( 79 | modifier = Modifier.fillMaxWidth(), 80 | horizontalArrangement = Arrangement.End 81 | ) { 82 | TextButton(onClick = onClickDismiss) { 83 | Text(text = "CANCEL", color = Orange) 84 | } 85 | TextButton(onClick = onClickAdd) { 86 | Text(text = "ADD", color = Orange) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/noteDetailsScreen/uiStates/InputFieldUiState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.noteDetailsScreen.uiStates 2 | 3 | data class InputFieldUiState(var text:String = "", var errorMessage:String? = null) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/noteDetailsScreen/uiStates/LinkUiState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.noteDetailsScreen.uiStates 2 | 3 | data class LinkUiState( 4 | var finalLink: String? = null, 5 | var typedLink:String ="", 6 | var linkError: String? = null, 7 | var isLinkVisible: Boolean = false, 8 | var isLinkDialogVisible: Boolean = false 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/noteDetailsScreen/uiStates/NoteDetailsEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.noteDetailsScreen.uiStates 2 | 3 | import android.net.Uri 4 | 5 | sealed class NoteDetailsEvent { 6 | data class ClickColor(var color: Int) : NoteDetailsEvent() 7 | data class TitleChanged(var text: String) : NoteDetailsEvent() 8 | data class SubtitleChanged(var text: String) : NoteDetailsEvent() 9 | data class NoteTextChanged(var text: String) : NoteDetailsEvent() 10 | object SaveNote : NoteDetailsEvent() 11 | object DismissUrlDialog : NoteDetailsEvent() 12 | object AddUrlDialog : NoteDetailsEvent() 13 | object ShowUrlDialog : NoteDetailsEvent() 14 | object DeleteUrl : NoteDetailsEvent() 15 | 16 | data class UrlTextChanged(var text: String) : NoteDetailsEvent() 17 | data class SelectedImage(var image: Uri) : NoteDetailsEvent() 18 | object DeleteImage : NoteDetailsEvent() 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/noteDetailsScreen/uiStates/NoteDetailsUiState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.noteDetailsScreen.uiStates 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.toArgb 5 | import com.example.noteappcompose.presentation.utilities.Constants 6 | import java.text.SimpleDateFormat 7 | import java.util.Date 8 | import java.util.Locale 9 | 10 | data class NoteDetailsUiState( 11 | val isLoading:Boolean = false, 12 | var id: String = "-1", 13 | var titleInputFieldUiState: InputFieldUiState = InputFieldUiState(), 14 | var subtitleInputFieldUiState: InputFieldUiState = InputFieldUiState(), 15 | var descriptionInputFieldUiState: InputFieldUiState = InputFieldUiState(), 16 | var noteColors: List = Constants.noteColorsList, 17 | var noteColor: Int = noteColors[0].toArgb(), 18 | var dateTime: String = SimpleDateFormat("EEEE, dd MMMM yyyy HH:mm", Locale.getDefault()).format( 19 | Date() 20 | ), 21 | var linkUiState: LinkUiState = LinkUiState(), 22 | var imageLink: String? = null, 23 | var isImageVisible: Boolean = false 24 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/registerScreen/RegisterScreen.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.registerScreen 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.material.Scaffold 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.filled.DriveFileRenameOutline 16 | import androidx.compose.material.icons.filled.Email 17 | import androidx.compose.material.rememberScaffoldState 18 | import androidx.compose.material3.ExperimentalMaterial3Api 19 | import androidx.compose.material3.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.LaunchedEffect 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.text.font.FontWeight 26 | import androidx.compose.ui.text.input.KeyboardType 27 | import androidx.hilt.navigation.compose.hiltViewModel 28 | import androidx.navigation.NavController 29 | import com.example.noteappcompose.presentation.loginScreen.components.OutlineInputField 30 | import com.example.noteappcompose.presentation.registerScreen.components.LoadingButton 31 | import com.example.noteappcompose.presentation.registerScreen.uiStates.RegisterUiEvent 32 | import com.example.noteappcompose.presentation.theme.HintGray 33 | import com.example.noteappcompose.presentation.theme.LightGray 34 | import com.example.noteappcompose.presentation.theme.UbuntuFont 35 | import com.example.noteappcompose.presentation.utilities.Screen 36 | import ir.kaaveh.sdpcompose.sdp 37 | import ir.kaaveh.sdpcompose.ssp 38 | import kotlinx.coroutines.flow.collectLatest 39 | 40 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "UnusedMaterialScaffoldPaddingParameter") 41 | @ExperimentalMaterial3Api 42 | @Composable 43 | fun RegisterScreen( 44 | navController: NavController, 45 | viewModel: RegisterViewModel = hiltViewModel() 46 | ) { 47 | var scaffoldState = rememberScaffoldState() 48 | LaunchedEffect(key1 = true) { 49 | viewModel.eventFlow.collectLatest { event -> 50 | when (event) { 51 | 52 | RegisterViewModel.UiEvent.RegisterSuccess -> navController.navigate(Screen.HomeScreen.route) { 53 | popUpTo( 54 | 0 55 | ) 56 | } 57 | 58 | is RegisterViewModel.UiEvent.ShowMessage -> scaffoldState.snackbarHostState.showSnackbar( 59 | event.error 60 | ) 61 | } 62 | } 63 | } 64 | Scaffold( 65 | modifier = Modifier.background(LightGray), 66 | scaffoldState = scaffoldState 67 | ) { 68 | Column( 69 | horizontalAlignment = Alignment.Start, 70 | modifier = Modifier 71 | .fillMaxSize() 72 | .background(LightGray) 73 | .padding(vertical = 25.sdp, horizontal = 15.sdp) 74 | ) { 75 | Text( 76 | text = "Welcome with us!", 77 | fontFamily = UbuntuFont, 78 | fontWeight = FontWeight.Bold, 79 | fontSize = 18.ssp, 80 | color = Color.White, 81 | ) 82 | Text( 83 | text = "Create new account", 84 | fontFamily = UbuntuFont, 85 | fontWeight = FontWeight.Normal, 86 | fontSize = 12.ssp, 87 | color = HintGray, 88 | modifier = Modifier.padding(vertical = 10.sdp), 89 | ) 90 | Spacer(Modifier.size(30.sdp)) 91 | OutlineInputField( 92 | text = viewModel.registerUiState.nameUiState.text, 93 | onValueChange = { viewModel.onEvent(RegisterUiEvent.NameChanged(it)) }, 94 | hint = "Name", 95 | modifier = Modifier.fillMaxWidth(), 96 | icon = Icons.Default.DriveFileRenameOutline, 97 | keyboardType = KeyboardType.Text, 98 | isErrorVisible = viewModel.registerUiState.nameUiState.errorMessage != null, 99 | error = viewModel.registerUiState.nameUiState.errorMessage 100 | ) 101 | Spacer(Modifier.size(20.sdp)) 102 | OutlineInputField( 103 | text = viewModel.registerUiState.emailUiState.text, 104 | onValueChange = { viewModel.onEvent(RegisterUiEvent.EmailChanged(it)) }, 105 | hint = "Email", 106 | modifier = Modifier.fillMaxWidth(), 107 | icon = Icons.Default.Email, 108 | keyboardType = KeyboardType.Email, 109 | isErrorVisible = viewModel.registerUiState.emailUiState.errorMessage != null, 110 | error = viewModel.registerUiState.emailUiState.errorMessage 111 | ) 112 | Spacer(Modifier.size(20.sdp)) 113 | OutlineInputField( 114 | text = viewModel.registerUiState.passwordUiState.text, 115 | onValueChange = { viewModel.onEvent(RegisterUiEvent.PasswordChanged(it)) }, 116 | hint = "Password", 117 | modifier = Modifier.fillMaxWidth(), 118 | icon = Icons.Default.Email, 119 | keyboardType = KeyboardType.Password, 120 | isErrorVisible = viewModel.registerUiState.passwordUiState.errorMessage != null, 121 | error = viewModel.registerUiState.passwordUiState.errorMessage 122 | ) 123 | LoadingButton( 124 | onClick = { viewModel.register() }, 125 | modifier = Modifier 126 | .padding(vertical = 20.sdp) 127 | .fillMaxWidth() 128 | .height(40.sdp), 129 | isEnabled = !viewModel.registerUiState.isLoading, 130 | isLoading = viewModel.registerUiState.isLoading 131 | ) 132 | /* Button( 133 | onClick = { viewModel.register() }, 134 | modifier = Modifier 135 | .padding(vertical = 20.sdp) 136 | .fillMaxWidth() 137 | .height(40.sdp), 138 | shape = RoundedCornerShape(5.sdp), 139 | enabled = !viewModel.registerUiState.isLoading, 140 | colors = ButtonDefaults.buttonColors( 141 | containerColor = Orange 142 | ) 143 | ) { 144 | if (viewModel.registerUiState.isLoading) { 145 | CircularProgressIndicator(modifier = Modifier.size(20.sdp)) 146 | } else 147 | Text(text = "Continue", fontSize = 13.ssp) 148 | }*/ 149 | // TextButton(onClick = { /*TODO*/ }) { 150 | Text( 151 | text = "have an account already? login now", 152 | modifier = Modifier 153 | .padding(top = 5.sdp) 154 | .clickable { 155 | navController.navigate(Screen.LoginScreen.route) 156 | }, 157 | fontFamily = UbuntuFont, 158 | fontWeight = FontWeight.Normal, 159 | fontSize = 13.ssp, 160 | color = Color.White, 161 | ) 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/registerScreen/RegisterViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.registerScreen 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.example.noteappcompose.domain.usecases.RegisterUseCase 9 | import com.example.noteappcompose.domain.usecases.ValidateEmailUseCase 10 | import com.example.noteappcompose.domain.usecases.ValidatePasswordUseCase 11 | import com.example.noteappcompose.domain.usecases.ValidateUserNameUseCase 12 | import com.example.noteappcompose.domain.utilitites.InvalidInputTextException 13 | import com.example.noteappcompose.presentation.registerScreen.uiStates.RegisterUiEvent 14 | import com.example.noteappcompose.presentation.registerScreen.uiStates.RegisterUiState 15 | import dagger.hilt.android.lifecycle.HiltViewModel 16 | import kotlinx.coroutines.flow.MutableSharedFlow 17 | import kotlinx.coroutines.flow.SharedFlow 18 | import kotlinx.coroutines.flow.asSharedFlow 19 | import kotlinx.coroutines.launch 20 | import javax.inject.Inject 21 | 22 | @HiltViewModel 23 | class RegisterViewModel @Inject constructor( 24 | var validateEmailUseCase: ValidateEmailUseCase, 25 | var validatePasswordUseCase: ValidatePasswordUseCase, 26 | var validateUserNameUseCase: ValidateUserNameUseCase, 27 | var registerUseCase: RegisterUseCase 28 | ) : ViewModel() { 29 | var registerUiState by mutableStateOf(RegisterUiState(isLoading = false)) 30 | private set; 31 | 32 | private var _eventFlow = MutableSharedFlow() 33 | val eventFlow: SharedFlow = _eventFlow.asSharedFlow() 34 | 35 | fun register() { 36 | viewModelScope.launch { 37 | registerUiState = registerUiState.copy(isLoading = true) 38 | val nameValidation = 39 | validateUserNameUseCase(registerUiState.nameUiState.text) 40 | val emailValidationResult = 41 | validateEmailUseCase(registerUiState.emailUiState.text) 42 | val passwordValidationResult = 43 | validatePasswordUseCase(registerUiState.passwordUiState.text) 44 | val hasValidationError = listOf( 45 | nameValidation, 46 | emailValidationResult, 47 | passwordValidationResult 48 | ).any { it.error != null } 49 | if (hasValidationError) { 50 | registerUiState = registerUiState.copy( 51 | nameUiState = registerUiState.nameUiState.copy( 52 | errorMessage = nameValidation.error 53 | ), 54 | emailUiState = registerUiState.emailUiState.copy( 55 | errorMessage = emailValidationResult.error 56 | ), 57 | passwordUiState = registerUiState.passwordUiState.copy( 58 | errorMessage = passwordValidationResult.error 59 | ), 60 | ) 61 | registerUiState = registerUiState.copy(isLoading = false) 62 | } else { 63 | try { 64 | val registerResult = registerUseCase( 65 | registerUiState.nameUiState.text, 66 | registerUiState.emailUiState.text, 67 | registerUiState.passwordUiState.text 68 | ) 69 | 70 | if (registerResult.token.isNotBlank()) 71 | _eventFlow.emit(UiEvent.RegisterSuccess) 72 | else 73 | _eventFlow.emit(UiEvent.ShowMessage("Unknown Error")) 74 | registerUiState = registerUiState.copy(isLoading = false) 75 | } catch (e: InvalidInputTextException) { 76 | registerUiState = registerUiState.copy( 77 | nameUiState = registerUiState.nameUiState.copy( 78 | errorMessage = validateUserNameUseCase(registerUiState.nameUiState.text).error 79 | ), 80 | emailUiState = registerUiState.emailUiState.copy( 81 | errorMessage = validateEmailUseCase(registerUiState.emailUiState.text).error 82 | ), 83 | passwordUiState = registerUiState.passwordUiState.copy( 84 | errorMessage = validatePasswordUseCase(registerUiState.passwordUiState.text).error 85 | ), 86 | ) 87 | registerUiState = registerUiState.copy(isLoading = false) 88 | } catch (e: Exception) { 89 | e.printStackTrace() 90 | registerUiState = registerUiState.copy(isLoading = false) 91 | _eventFlow.emit(UiEvent.ShowMessage(e.message.toString())) 92 | } 93 | } 94 | } 95 | } 96 | fun onEvent(action: RegisterUiEvent) { 97 | registerUiState = when (action) { 98 | is RegisterUiEvent.NameChanged -> registerUiState.copy( 99 | nameUiState = registerUiState.nameUiState.copy( 100 | errorMessage = null, 101 | text = action.text 102 | ) 103 | ) 104 | 105 | is RegisterUiEvent.EmailChanged -> registerUiState.copy( 106 | emailUiState = registerUiState.emailUiState.copy( 107 | errorMessage = null, 108 | text = action.text 109 | ) 110 | ) 111 | 112 | is RegisterUiEvent.PasswordChanged -> registerUiState.copy( 113 | passwordUiState = registerUiState.passwordUiState.copy( 114 | errorMessage = null, 115 | text = action.text 116 | ) 117 | ) 118 | } 119 | } 120 | sealed class UiEvent { 121 | object RegisterSuccess : UiEvent() 122 | data class ShowMessage(var error: String) : UiEvent() 123 | } 124 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/registerScreen/components/LoadingButton.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.registerScreen.components 2 | 3 | import androidx.compose.foundation.layout.size 4 | import androidx.compose.foundation.shape.RoundedCornerShape 5 | import androidx.compose.material.CircularProgressIndicator 6 | import androidx.compose.material3.Button 7 | import androidx.compose.material3.ButtonDefaults 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import com.example.noteappcompose.presentation.theme.Orange 12 | import ir.kaaveh.sdpcompose.sdp 13 | import ir.kaaveh.sdpcompose.ssp 14 | 15 | @Composable 16 | fun LoadingButton( 17 | onClick: () -> Unit, 18 | modifier: Modifier = Modifier, 19 | isEnabled: Boolean = true, 20 | isLoading: Boolean = false 21 | ) { 22 | Button( 23 | onClick = onClick, 24 | modifier = modifier, 25 | shape = RoundedCornerShape(5.sdp), 26 | enabled = isEnabled, 27 | colors = ButtonDefaults.buttonColors( 28 | containerColor = Orange 29 | ) 30 | ) { 31 | if (isLoading) { 32 | CircularProgressIndicator(modifier = Modifier.size(20.sdp)) 33 | } else 34 | Text(text = "Continue", fontSize = 13.ssp) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/registerScreen/uiStates/RegisterUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.registerScreen.uiStates 2 | 3 | sealed class RegisterUiEvent { 4 | data class NameChanged(var text:String):RegisterUiEvent() 5 | data class EmailChanged(var text:String):RegisterUiEvent() 6 | data class PasswordChanged(var text:String):RegisterUiEvent() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/registerScreen/uiStates/RegisterUiState.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.registerScreen.uiStates 2 | 3 | import com.example.noteappcompose.presentation.noteDetailsScreen.uiStates.InputFieldUiState 4 | 5 | data class RegisterUiState( 6 | val isLoading: Boolean = false, 7 | val nameUiState: InputFieldUiState = InputFieldUiState(), 8 | val emailUiState: InputFieldUiState = InputFieldUiState(), 9 | val passwordUiState: InputFieldUiState = InputFieldUiState() 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/splashScreen/SplashScreen.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.splashScreen 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.LaunchedEffect 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.painterResource 11 | import androidx.hilt.navigation.compose.hiltViewModel 12 | import androidx.navigation.NavController 13 | import com.example.noteappcompose.R 14 | import com.example.noteappcompose.presentation.splashScreen.uiStates.SplashUiEvent 15 | import kotlinx.coroutines.flow.collectLatest 16 | 17 | 18 | @Composable 19 | fun SplashScreen( 20 | navController: NavController, 21 | viewModel: SplashViewModel = hiltViewModel() 22 | ) { 23 | LaunchedEffect(key1 = true) { 24 | viewModel.eventFlow.collectLatest { event -> 25 | when (event) { 26 | SplashUiEvent.HomeScreen -> navController.navigate(event.screen.route){ 27 | popUpTo(0) 28 | } 29 | SplashUiEvent.LoginScreen -> navController.navigate(event.screen.route){ 30 | popUpTo(0) 31 | } 32 | } 33 | } 34 | } 35 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 36 | Image( 37 | painter = painterResource(id = R.drawable.jetpack_compose_icon), 38 | contentDescription = null 39 | ) 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/splashScreen/SplashViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.splashScreen 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.example.noteappcompose.domain.usecases.IsUserSplashUseCase 6 | import com.example.noteappcompose.presentation.splashScreen.uiStates.SplashUiEvent 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.flow.MutableSharedFlow 9 | import kotlinx.coroutines.flow.SharedFlow 10 | import kotlinx.coroutines.flow.asSharedFlow 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class SplashViewModel @Inject constructor(isUserSplashUseCase: IsUserSplashUseCase) : ViewModel() { 16 | private var _eventFlow = MutableSharedFlow() 17 | val eventFlow: SharedFlow = _eventFlow.asSharedFlow() 18 | 19 | init { 20 | viewModelScope.launch { 21 | var isSplash = isUserSplashUseCase() 22 | _eventFlow.emit(if(isSplash) SplashUiEvent.LoginScreen else SplashUiEvent.HomeScreen) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/splashScreen/uiStates/SplashUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.splashScreen.uiStates 2 | 3 | import com.example.noteappcompose.presentation.utilities.Screen 4 | 5 | sealed class SplashUiEvent(var screen:Screen){ 6 | object HomeScreen:SplashUiEvent(Screen.HomeScreen) 7 | object LoginScreen:SplashUiEvent(Screen.LoginScreen) 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) 12 | 13 | val Green = Color(0xFF35c759) 14 | 15 | val LightGray = Color(0xFF292929) 16 | val HintGray = Color(0xFF7B7B7B) 17 | val SearchGray = Color(0xFF333333) 18 | val BottomBarContainer = Color(0xFF2F2D2E) 19 | val BottomBarIcon = Color(0xFFA4A4A4) 20 | val Orange = Color(0xFFFDBE3B) 21 | val NoteSubtitle = Color(0xFFFAFAFA) 22 | val BottomSheet = Color(0xFF1F1F1F) 23 | 24 | val SearchIcon = Color(0xFFDBDBDB) 25 | val SubtitleGray = Color(0xFFCECECE) 26 | 27 | val Blue = Color(0xFF3A52Fc) 28 | val Red = Color(0xFFFF4842) 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.darkColorScheme 6 | import androidx.compose.material3.lightColorScheme 7 | import androidx.compose.runtime.Composable 8 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 9 | 10 | private val DarkColorScheme = darkColorScheme( 11 | primary = LightGray, 12 | secondary = LightGray, 13 | tertiary = LightGray 14 | ) 15 | 16 | private val LightColorScheme = lightColorScheme( 17 | primary = LightGray, 18 | secondary = LightGray, 19 | tertiary = LightGray 20 | 21 | /* Other default colors to override 22 | background = Color(0xFFFFFBFE), 23 | surface = Color(0xFFFFFBFE), 24 | onPrimary = Color.White, 25 | onSecondary = Color.White, 26 | onTertiary = Color.White, 27 | onBackground = Color(0xFF1C1B1F), 28 | onSurface = Color(0xFF1C1B1F), 29 | */ 30 | ) 31 | 32 | @Composable 33 | fun NoteAppComposeTheme( 34 | darkTheme: Boolean = isSystemInDarkTheme(), 35 | // Dynamic color is available on Android 12+ 36 | // dynamicColor: Boolean = true, 37 | content: @Composable () -> Unit 38 | ) { 39 | val systemUiController = rememberSystemUiController() 40 | systemUiController.setStatusBarColor(LightGray) 41 | val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme 42 | // val colorScheme = when { 43 | // dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 44 | // val context = LocalContext.current 45 | // if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 46 | // } 47 | // darkTheme -> DarkColorScheme 48 | // else -> LightColorScheme 49 | // } 50 | // val view = LocalView.current 51 | // if (!view.isInEditMode) { 52 | // SideEffect { 53 | // (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() 54 | // ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme 55 | // } 56 | // } 57 | 58 | MaterialTheme( 59 | colorScheme = colorScheme, 60 | typography = Typography, 61 | content = content 62 | ) 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.Font 6 | import androidx.compose.ui.text.font.FontFamily 7 | import androidx.compose.ui.text.font.FontWeight 8 | import androidx.compose.ui.unit.sp 9 | import com.example.noteappcompose.R 10 | 11 | // Set of Material typography styles to start with 12 | val Typography = Typography( 13 | bodyLarge = TextStyle( 14 | fontFamily = FontFamily.Default, 15 | fontWeight = FontWeight.Normal, 16 | fontSize = 16.sp, 17 | lineHeight = 24.sp, 18 | letterSpacing = 0.5.sp 19 | ) 20 | ) 21 | val UbuntuFont = FontFamily( 22 | Font(R.font.ubuntu_regular, FontWeight.Normal), 23 | Font(R.font.ubuntu_medium, FontWeight.Medium), 24 | Font(R.font.ubuntu_bold, FontWeight.Bold), 25 | ) 26 | /* Other default text styles to override 27 | titleLarge = TextStyle( 28 | fontFamily = FontFamily.Default, 29 | fontWeight = FontWeight.Normal, 30 | fontSize = 22.sp, 31 | lineHeight = 28.sp, 32 | letterSpacing = 0.sp 33 | ), 34 | labelSmall = TextStyle( 35 | fontFamily = FontFamily.Default, 36 | fontWeight = FontWeight.Medium, 37 | fontSize = 11.sp, 38 | lineHeight = 16.sp, 39 | letterSpacing = 0.5.sp 40 | ) 41 | */ -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/utilities/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.utilities 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.example.noteappcompose.presentation.theme.Blue 5 | import com.example.noteappcompose.presentation.theme.Orange 6 | import com.example.noteappcompose.presentation.theme.Red 7 | import com.example.noteappcompose.presentation.theme.SearchGray 8 | import javax.inject.Singleton 9 | 10 | @Singleton 11 | object Constants { 12 | val noteColorsList = listOf(SearchGray, Orange, Red, Blue, Color.Black) 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/noteappcompose/presentation/utilities/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.example.noteappcompose.presentation.utilities 2 | 3 | sealed class Screen(val route: String) { 4 | object LoginScreen: Screen("login_screen") 5 | object RegisterScreen: Screen("register_screen") 6 | object HomeScreen: Screen("home_screen") 7 | object NoteDetailsScreen: Screen("note_details_screen") 8 | object SplashScreen: Screen("splash_screen") 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/jetpack_compose_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/drawable/jetpack_compose_icon.png -------------------------------------------------------------------------------- /app/src/main/res/font/ubuntu_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/font/ubuntu_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/ubuntu_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/font/ubuntu_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/ubuntu_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/font/ubuntu_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | NoteAppCompose 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |