├── .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 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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 |
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 | 
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 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/noteappcompose/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteappcompose
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | compose_version = '1.3.0'
4 | }
5 | }// Top-level build file where you can add configuration options common to all sub-projects/modules.
6 | plugins {
7 | id 'com.android.application' version '8.0.0' apply false
8 | id 'com.android.library' version '8.0.0' apply false
9 | id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
10 | id 'com.google.dagger.hilt.android' version '2.42' apply false
11 | id "org.jetbrains.kotlin.plugin.serialization"version '1.5.21'
12 | // classpath "org.jetbrains.kotlin:kotlin-serialization:1.5.21"
13 | // id 'com.android.application' version '7.2.2' apply false
14 | // id 'com.android.library' version '7.2.2' apply false
15 | // id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
16 | // id 'com.google.dagger.hilt.android' version '2.41' apply false
17 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.nonFinalResIds=false
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adelayman1/ComposeNotesAppRetrofit/cbda8fd5f7e2b1c87b06dc2a47867a4d491fd45c/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Nov 25 18:14:18 EET 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | maven { url 'https://jitpack.io' }
13 | mavenCentral()
14 | }
15 | }
16 | rootProject.name = "NoteAppCompose"
17 | include ':app'
18 |
--------------------------------------------------------------------------------