├── .gitignore
├── .idea
├── .gitignore
├── .name
├── compiler.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinc.xml
├── misc.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── release
│ ├── app-release.apk
│ └── output-metadata.json
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── example
│ │ └── noteswithrestapi
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── noteswithrestapi
│ │ │ ├── authentication_feature
│ │ │ ├── data
│ │ │ │ ├── remote
│ │ │ │ │ ├── AccountsApiService.kt
│ │ │ │ │ └── dto
│ │ │ │ │ │ ├── EmailVerificationResendResultDataDto.kt
│ │ │ │ │ │ ├── EmailVerificationResendResultDto.kt
│ │ │ │ │ │ ├── EmailVerificationResultDto.kt
│ │ │ │ │ │ ├── IsAuthenticatedResultDataDto.kt
│ │ │ │ │ │ ├── IsAuthenticatedResultDto.kt
│ │ │ │ │ │ ├── LoginResultDataDto.kt
│ │ │ │ │ │ ├── LoginResultDto.kt
│ │ │ │ │ │ ├── RegisterResultDataDto.kt
│ │ │ │ │ │ ├── RegisterResultDto.kt
│ │ │ │ │ │ ├── ResetPasswordConfirmResultDto.kt
│ │ │ │ │ │ └── ResetPasswordResultDto.kt
│ │ │ │ └── repository
│ │ │ │ │ └── AuthenticationRepositoryImpl.kt
│ │ │ ├── di
│ │ │ │ └── AuthenticationModule.kt
│ │ │ ├── domain
│ │ │ │ ├── error
│ │ │ │ │ └── AuthenticationAppError.kt
│ │ │ │ ├── model
│ │ │ │ │ └── credentials
│ │ │ │ │ │ ├── LoginCredentials.kt
│ │ │ │ │ │ ├── PasswordResetConfirmCredentials.kt
│ │ │ │ │ │ ├── RegisterCredentials.kt
│ │ │ │ │ │ ├── ResendEmailVerificationCredentials.kt
│ │ │ │ │ │ ├── SendPasswordResetCodeCredentials.kt
│ │ │ │ │ │ └── VerifyEmailCredentials.kt
│ │ │ │ ├── repository
│ │ │ │ │ └── AuthenticationRepository.kt
│ │ │ │ └── usecase
│ │ │ │ │ ├── ConfirmPasswordResetUseCase.kt
│ │ │ │ │ ├── IsLoggedInUseCase.kt
│ │ │ │ │ ├── LoginUseCase.kt
│ │ │ │ │ ├── RegisterUseCase.kt
│ │ │ │ │ ├── ResendEmailVerificationCodeUseCase.kt
│ │ │ │ │ ├── SendPasswordResetCodeUseCase.kt
│ │ │ │ │ └── VerifyEmailUseCase.kt
│ │ │ └── presentation
│ │ │ │ ├── AuthenticationGraph.kt
│ │ │ │ ├── components
│ │ │ │ └── OTPTextField.kt
│ │ │ │ ├── confirm_password_reset
│ │ │ │ ├── ConfirmPasswordResetEvent.kt
│ │ │ │ ├── ConfirmPasswordResetScreen.kt
│ │ │ │ ├── ConfirmPasswordResetState.kt
│ │ │ │ ├── ConfirmPasswordResetUiEvent.kt
│ │ │ │ └── ConfirmPasswordResetViewModel.kt
│ │ │ │ ├── login
│ │ │ │ ├── LoginEvent.kt
│ │ │ │ ├── LoginScreen.kt
│ │ │ │ ├── LoginState.kt
│ │ │ │ ├── LoginUiEvent.kt
│ │ │ │ └── LoginViewModel.kt
│ │ │ │ ├── register
│ │ │ │ ├── RegisterEvent.kt
│ │ │ │ ├── RegisterScreen.kt
│ │ │ │ ├── RegisterState.kt
│ │ │ │ ├── RegisterUiEvent.kt
│ │ │ │ └── RegisterViewModel.kt
│ │ │ │ ├── reset_password
│ │ │ │ ├── ResetPasswordEvent.kt
│ │ │ │ ├── ResetPasswordScreen.kt
│ │ │ │ ├── ResetPasswordState.kt
│ │ │ │ ├── ResetPasswordUiEvent.kt
│ │ │ │ └── ResetPasswordViewModel.kt
│ │ │ │ └── verify_email
│ │ │ │ ├── VerifyEmailEvent.kt
│ │ │ │ ├── VerifyEmailScreen.kt
│ │ │ │ ├── VerifyEmailState.kt
│ │ │ │ ├── VerifyEmailUiEvent.kt
│ │ │ │ └── VerifyEmailViewModel.kt
│ │ │ ├── core
│ │ │ ├── NoteApplication.kt
│ │ │ ├── data
│ │ │ │ ├── network
│ │ │ │ │ ├── HttpCustomErrorCode.kt
│ │ │ │ │ ├── NetworkConstants.kt
│ │ │ │ │ └── NetworkUtil.kt
│ │ │ │ └── token
│ │ │ │ │ ├── TokenManager.kt
│ │ │ │ │ └── TokenManagerEncryptedSharedPreferences.kt
│ │ │ ├── di
│ │ │ │ └── AppModule.kt
│ │ │ ├── domain
│ │ │ │ ├── model
│ │ │ │ │ ├── AppError.kt
│ │ │ │ │ └── AppResponse.kt
│ │ │ │ └── paginator
│ │ │ │ │ ├── DefaultPaginator.kt
│ │ │ │ │ └── Paginator.kt
│ │ │ ├── presentation
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── MainScreen.kt
│ │ │ │ ├── components
│ │ │ │ │ ├── AppDrawerContent.kt
│ │ │ │ │ ├── ButtonComponent.kt
│ │ │ │ │ ├── InputFieldComponent.kt
│ │ │ │ │ ├── LoadingFailedComponent.kt
│ │ │ │ │ ├── OutlinedButtonComponent.kt
│ │ │ │ │ ├── ProgressIndicatorComponent.kt
│ │ │ │ │ └── pull_refresh
│ │ │ │ │ │ ├── PullRefresh.kt
│ │ │ │ │ │ ├── PullRefreshIndicator.kt
│ │ │ │ │ │ ├── PullRefreshIndicatorTransform.kt
│ │ │ │ │ │ └── RememberPullRefreshState.kt
│ │ │ │ ├── navigation
│ │ │ │ │ ├── Graph.kt
│ │ │ │ │ ├── NavigationItem.kt
│ │ │ │ │ ├── RootNavGraph.kt
│ │ │ │ │ └── Screen.kt
│ │ │ │ └── theme
│ │ │ │ │ └── ui
│ │ │ │ │ └── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Shapes.kt
│ │ │ │ │ ├── Theme.kt
│ │ │ │ │ └── Type.kt
│ │ │ └── utils
│ │ │ │ ├── Constants.kt
│ │ │ │ └── TimeUtil.kt
│ │ │ ├── note_feature
│ │ │ ├── data
│ │ │ │ ├── mapper
│ │ │ │ │ └── NoteMapper.kt
│ │ │ │ ├── remote
│ │ │ │ │ ├── NoteApiService.kt
│ │ │ │ │ └── dto
│ │ │ │ │ │ ├── GetNotesResultDto.kt
│ │ │ │ │ │ └── NoteDto.kt
│ │ │ │ └── repository
│ │ │ │ │ └── NoteRepositoryImpl.kt
│ │ │ ├── di
│ │ │ │ └── NoteModule.kt
│ │ │ ├── domain
│ │ │ │ ├── error
│ │ │ │ │ └── NoteError.kt
│ │ │ │ ├── model
│ │ │ │ │ └── Note.kt
│ │ │ │ ├── repository
│ │ │ │ │ └── NoteRepository.kt
│ │ │ │ └── usecase
│ │ │ │ │ ├── AddNoteUseCase.kt
│ │ │ │ │ ├── DeleteNoteUseCase.kt
│ │ │ │ │ ├── EditNoteUseCase.kt
│ │ │ │ │ ├── GetNoteUseCase.kt
│ │ │ │ │ └── GetNotesUseCase.kt
│ │ │ └── presentation
│ │ │ │ ├── NoteGraph.kt
│ │ │ │ ├── add_note
│ │ │ │ ├── AddNoteEvent.kt
│ │ │ │ ├── AddNoteScreen.kt
│ │ │ │ ├── AddNoteState.kt
│ │ │ │ ├── AddNoteUiEvent.kt
│ │ │ │ └── AddNoteViewModel.kt
│ │ │ │ ├── components
│ │ │ │ └── NoteComponent.kt
│ │ │ │ ├── edit_note
│ │ │ │ ├── EditNoteEvent.kt
│ │ │ │ ├── EditNoteScreen.kt
│ │ │ │ ├── EditNoteState.kt
│ │ │ │ ├── EditNoteUiEvent.kt
│ │ │ │ └── EditNoteViewModel.kt
│ │ │ │ ├── notes
│ │ │ │ ├── NotesEvent.kt
│ │ │ │ ├── NotesScreen.kt
│ │ │ │ ├── NotesState.kt
│ │ │ │ └── NotesViewModel.kt
│ │ │ │ └── search_note
│ │ │ │ ├── SearchNoteEvent.kt
│ │ │ │ ├── SearchNoteScreen.kt
│ │ │ │ ├── SearchNoteState.kt
│ │ │ │ └── SearchNoteViewModel.kt
│ │ │ └── profile_feature
│ │ │ ├── data
│ │ │ ├── mapper
│ │ │ │ └── UserMapper.kt
│ │ │ ├── remote
│ │ │ │ ├── ProfileApiService.kt
│ │ │ │ └── dto
│ │ │ │ │ ├── GetUserResultDto.kt
│ │ │ │ │ ├── LogoutResultDto.kt
│ │ │ │ │ └── UserData.kt
│ │ │ └── repository
│ │ │ │ └── ProfileRepositoryImpl.kt
│ │ │ ├── di
│ │ │ └── ProfileModule.kt
│ │ │ ├── domain
│ │ │ ├── model
│ │ │ │ └── user
│ │ │ │ │ └── User.kt
│ │ │ ├── repository
│ │ │ │ └── ProfileRepository.kt
│ │ │ └── usecase
│ │ │ │ ├── GetUserUseCase.kt
│ │ │ │ └── LogoutUseCase.kt
│ │ │ └── presentation
│ │ │ ├── ProfileGraph.kt
│ │ │ └── profile
│ │ │ ├── ProfileEvent.kt
│ │ │ ├── ProfileScreen.kt
│ │ │ ├── ProfileState.kt
│ │ │ ├── ProfileUiEvent.kt
│ │ │ └── ProfileViewModel.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── amico_confirm.png
│ │ ├── amico_notes.png
│ │ ├── amico_reset.png
│ │ ├── amico_signin.png
│ │ ├── amico_signup.png
│ │ └── ic_launcher_background.xml
│ │ ├── font
│ │ ├── roboto_bold.ttf
│ │ ├── roboto_light.ttf
│ │ ├── roboto_medium.ttf
│ │ ├── roboto_regular.ttf
│ │ └── ud_digi_kyokasho_nk_b.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
│ └── noteswithrestapi
│ └── ExampleUnitTest.kt
├── art
├── dark
│ ├── add_note_dark.png
│ ├── confirm_email.png
│ ├── edit_note_dark.png
│ ├── login_dark.png
│ ├── nav_drawer_dark.png
│ ├── notes_dark.png
│ ├── profile_dark.png
│ ├── register_dark.png
│ ├── reset_password_dark.png
│ ├── reset_pswrd_confirm_dark.png
│ └── search_notes_dark.png
├── light
│ ├── add_note_light.png
│ ├── confirm_email_light.png
│ ├── edit_note_light.png
│ ├── login_light.png
│ ├── nav_drawer_light.png
│ ├── notes_light.png
│ ├── proifle_light.png
│ ├── register_light.png
│ ├── reset_password.png
│ ├── reset_password_confirm_light.png
│ └── search_notes_light.png
└── thumbnail.png
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.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 | Notes With REST API
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Notes with REST API
2 | - Creade, Read, Update, Delete notes using REST API
3 | - [REST API](https://github.com/ItamiOWM/drf-notes)
4 |
5 | 
6 |
7 | ## Tech Stack
8 |
9 | - [Android Architecture Components](https://developer.android.com/topic/architecture)
10 |
11 | - Android Support Libraries
12 |
13 | - [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel)
14 |
15 | - [Jetpack Compose](https://developer.android.com/jetpack/compose/documentation)
16 |
17 | - [Jetpack Navigation Compose Animation](https://google.github.io/accompanist/navigation-animation/)
18 |
19 | - [Coroutines](https://developer.android.com/kotlin/coroutines)
20 |
21 | - [Dagger-Hilt](https://developer.android.com/training/dependency-injection/hilt-android)
22 |
23 | - [Retorift](https://square.github.io/retrofit/)
24 |
25 | - [Encrypted Shared Preferences](https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences)
26 |
27 | - [System ui conroller](https://google.github.io/accompanist/systemuicontroller/)
28 |
29 | - [Compose Material 3](https://developer.android.com/jetpack/androidx/releases/compose-material3)
30 |
31 | - [Notes REST API](https://github.com/ItamiOWM/drf-notes)
32 |
33 |
34 | ## Screenshots (Dark Theme):
35 |
36 | 
37 | 
38 | 
39 | 
40 | 
41 | 
42 | 
43 | 
44 | 
45 | 
46 | 
47 |
48 |
49 |
50 | ## Screenshots (Light Theme):
51 |
52 | 
53 | 
54 | 
55 | 
56 | 
57 | 
58 | 
59 | 
60 | 
61 | 
62 | 
63 |
64 |
65 |
--------------------------------------------------------------------------------
/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 | }
7 |
8 | android {
9 | namespace 'com.example.noteswithrestapi'
10 | compileSdk 33
11 |
12 | defaultConfig {
13 | applicationId "com.example.noteswithrestapi"
14 | minSdk 26
15 | targetSdk 33
16 | versionCode 1
17 | versionName "1.0"
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary true
22 | }
23 | }
24 |
25 | buildTypes {
26 | release {
27 | minifyEnabled false
28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
29 | }
30 | }
31 | compileOptions {
32 | sourceCompatibility JavaVersion.VERSION_17
33 | targetCompatibility JavaVersion.VERSION_17
34 | }
35 | kotlinOptions {
36 | jvmTarget = '17'
37 | }
38 | buildFeatures {
39 | compose true
40 | }
41 | composeOptions {
42 | kotlinCompilerExtensionVersion '1.4.6'
43 | }
44 | packagingOptions {
45 | resources {
46 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
47 | }
48 | }
49 | }
50 |
51 | dependencies {
52 |
53 | implementation 'androidx.core:core-ktx:1.10.1'
54 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
55 | implementation 'androidx.activity:activity-compose:1.7.1'
56 | implementation platform('androidx.compose:compose-bom:2023.04.01')
57 | implementation 'androidx.compose.ui:ui'
58 | implementation 'androidx.compose.ui:ui-graphics'
59 | implementation 'androidx.compose.ui:ui-tooling-preview'
60 | implementation 'androidx.compose.material3:material3'
61 | // implementation 'com.google.android.material:material:1.9.0'
62 | testImplementation 'junit:junit:4.13.2'
63 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
64 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
65 | androidTestImplementation platform('androidx.compose:compose-bom:2023.04.01')
66 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
67 | debugImplementation 'androidx.compose.ui:ui-tooling'
68 | debugImplementation 'androidx.compose.ui:ui-test-manifest'
69 |
70 | //Compose Navigation
71 | implementation("androidx.navigation:navigation-compose:$compose_nav_version")
72 |
73 | //Navigation animations
74 | implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_animation_version"
75 |
76 | //ViewModel Compose
77 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$compose_lifecycle_viewmodel_version"
78 |
79 | //Material Icons
80 | implementation "androidx.compose.material:material-icons-extended:$compose_material_icons_version"
81 |
82 | //Dagger-Hilt
83 | implementation "com.google.dagger:hilt-android:$dagger_hilt_version"
84 | kapt "com.google.dagger:hilt-android-compiler:$dagger_hilt_version"
85 | kapt "androidx.hilt:hilt-compiler:$hilt_compiler_version"
86 | implementation "androidx.hilt:hilt-navigation-compose:$hilt_nav_compose_version"
87 |
88 | //Splash Screen API
89 | implementation("androidx.core:core-splashscreen:$splash_screen_api_version")
90 |
91 | //Control system ui
92 | implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_systemui_controller_version"
93 |
94 | //Retrofit
95 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
96 | implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
97 | implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
98 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
99 |
100 | //EncryptedSharedPreferences
101 | implementation "androidx.security:security-crypto:$crypto_version"
102 | }
--------------------------------------------------------------------------------
/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/release/app-release.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/release/app-release.apk
--------------------------------------------------------------------------------
/app/release/output-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "artifactType": {
4 | "type": "APK",
5 | "kind": "Directory"
6 | },
7 | "applicationId": "com.example.noteswithrestapi",
8 | "variantName": "release",
9 | "elements": [
10 | {
11 | "type": "SINGLE",
12 | "filters": [],
13 | "attributes": [],
14 | "versionCode": 1,
15 | "versionName": "1.0",
16 | "outputFile": "app-release.apk"
17 | }
18 | ],
19 | "elementType": "File"
20 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/noteswithrestapi/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi
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.noteswithrestapi", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/AccountsApiService.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.data.remote
2 |
3 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.EmailVerificationResendResultDto
4 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.EmailVerificationResultDto
5 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.IsAuthenticatedResultDto
6 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.LoginResultDto
7 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.RegisterResultDto
8 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.ResetPasswordConfirmResultDto
9 | import com.example.noteswithrestapi.authentication_feature.data.remote.dto.ResetPasswordResultDto
10 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.LoginCredentials
11 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.PasswordResetConfirmCredentials
12 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.RegisterCredentials
13 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.ResendEmailVerificationCredentials
14 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.SendPasswordResetCodeCredentials
15 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.VerifyEmailCredentials
16 | import retrofit2.Response
17 | import retrofit2.http.Body
18 | import retrofit2.http.POST
19 |
20 | interface AccountsApiService {
21 |
22 | @POST("accounts/login/")
23 | suspend fun login(
24 | @Body loginCredentials: LoginCredentials,
25 | ): Response
26 |
27 |
28 | @POST("accounts/check-token/")
29 | suspend fun isAuthenticated(
30 | @Body token: String,
31 | ): Response
32 |
33 |
34 | @POST("accounts/register/")
35 | suspend fun register(
36 | @Body registerCredentials: RegisterCredentials,
37 | ): Response
38 |
39 |
40 | @POST("accounts/verify-email/")
41 | suspend fun verifyEmail(
42 | @Body verifyEmailCredentials: VerifyEmailCredentials,
43 | ): Response
44 |
45 |
46 | @POST("accounts/verify-email/resend/")
47 | suspend fun resendEmailVerification(
48 | @Body resendEmailVerificationCredentials: ResendEmailVerificationCredentials,
49 | ): Response
50 |
51 |
52 | @POST("accounts/reset-password/")
53 | suspend fun sendPasswordResetCode(
54 | @Body sendPasswordResetCodeCredentials: SendPasswordResetCodeCredentials,
55 | ): Response
56 |
57 |
58 | @POST("accounts/reset-password/confirm/")
59 | suspend fun passwordResetConfirm(
60 | @Body passwordResetConfirmCredentials: PasswordResetConfirmCredentials,
61 | ): Response
62 |
63 |
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/EmailVerificationResendResultDataDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto
2 |
3 | data class EmailVerificationResendResultDataDto(
4 | val email: String?
5 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/EmailVerificationResendResultDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class EmailVerificationResendResultDto(
6 | @SerializedName("data") val emailVerificationResendResultDataDto: EmailVerificationResendResultDataDto?,
7 | @SerializedName("message") val message: String?
8 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/EmailVerificationResultDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class EmailVerificationResultDto(
6 | @SerializedName("message") val message: String?
7 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/IsAuthenticatedResultDataDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto
2 |
3 | data class IsAuthenticatedResultDataDto(
4 | val auth: String,
5 | val user: String
6 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/IsAuthenticatedResultDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class IsAuthenticatedResultDto(
6 | @SerializedName("data") val isAuthenticatedResultDataDto: IsAuthenticatedResultDataDto,
7 | val message: String
8 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/LoginResultDataDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto
2 |
3 | data class LoginResultDataDto(
4 | val email: String?,
5 | val token: String?
6 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/LoginResultDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class LoginResultDto(
6 | @SerializedName("data") val loginResultDataDto: LoginResultDataDto?,
7 | @SerializedName("message") val message: String?
8 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/RegisterResultDataDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto
2 |
3 | data class RegisterResultDataDto(
4 | val email: String?,
5 | val is_active: Boolean?,
6 | val password: String?
7 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/RegisterResultDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class RegisterResultDto(
6 | @SerializedName("data") val registerResultDataDto: RegisterResultDataDto,
7 | @SerializedName("message") val message: String
8 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/ResetPasswordConfirmResultDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class ResetPasswordConfirmResultDto(
6 | @SerializedName("message") val message: String?
7 | )
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/data/remote/dto/ResetPasswordResultDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.data.remote.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class ResetPasswordResultDto(
6 | @SerializedName("message") val message: String?
7 | )
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/di/AuthenticationModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.di
2 |
3 | import com.example.noteswithrestapi.authentication_feature.data.remote.AccountsApiService
4 | import com.example.noteswithrestapi.authentication_feature.data.repository.AuthenticationRepositoryImpl
5 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository
6 | import dagger.Binds
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import retrofit2.Retrofit
12 | import javax.inject.Singleton
13 |
14 | @Module
15 | @InstallIn(SingletonComponent::class)
16 | interface AuthenticationModule {
17 |
18 |
19 | @Binds
20 | @Singleton
21 | fun bindAuthenticationRepository(
22 | authenticationRepositoryImpl: AuthenticationRepositoryImpl
23 | ): AuthenticationRepository
24 |
25 |
26 | companion object {
27 |
28 | @Provides
29 | @Singleton
30 | fun provideAccountsApiService(
31 | retrofit: Retrofit
32 | ): AccountsApiService {
33 | return retrofit.create(AccountsApiService::class.java)
34 | }
35 |
36 | }
37 |
38 |
39 |
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/error/AuthenticationAppError.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.error
2 |
3 | import com.example.noteswithrestapi.core.domain.model.AppError
4 |
5 | open class AuthenticationAppError: AppError() {
6 |
7 | object EmptyEmailError: AuthenticationAppError()
8 |
9 | object EmptyPasswordError: AuthenticationAppError()
10 |
11 | object EmptyRepeatPasswordError: AuthenticationAppError()
12 |
13 | object InvalidEmailOrPasswordError: AuthenticationAppError()
14 |
15 | object InvalidVerificationCodeError: AuthenticationAppError()
16 |
17 | object PasswordsDoNotMatchError: AuthenticationAppError()
18 |
19 | object ShortPasswordError: AuthenticationAppError()
20 |
21 | object UserAlreadyExist: AuthenticationAppError()
22 |
23 | object UserDoesNotExist: AuthenticationAppError()
24 |
25 | object InvalidEmailError: AuthenticationAppError()
26 |
27 | object EmailNotVerifiedError: AuthenticationAppError()
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/model/credentials/LoginCredentials.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.model.credentials
2 |
3 | data class LoginCredentials(
4 | val email: String,
5 | val password: String
6 | )
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/model/credentials/PasswordResetConfirmCredentials.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.model.credentials
2 |
3 | data class PasswordResetConfirmCredentials(
4 | val email: String,
5 | val new_password: String,
6 | val password_reset_code: String,
7 | )
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/model/credentials/RegisterCredentials.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.model.credentials
2 |
3 | data class RegisterCredentials(
4 | val email: String,
5 | val password: String,
6 | val confirmPassword: String,
7 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/model/credentials/ResendEmailVerificationCredentials.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.model.credentials
2 |
3 | data class ResendEmailVerificationCredentials(
4 | val email: String
5 | )
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/model/credentials/SendPasswordResetCodeCredentials.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.model.credentials
2 |
3 | data class SendPasswordResetCodeCredentials(
4 | val email: String
5 | )
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/model/credentials/VerifyEmailCredentials.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.model.credentials
2 |
3 | data class VerifyEmailCredentials(
4 | val email: String,
5 | val verification_code: String
6 | )
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/repository/AuthenticationRepository.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.repository
2 |
3 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.LoginCredentials
4 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.PasswordResetConfirmCredentials
5 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.SendPasswordResetCodeCredentials
6 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.RegisterCredentials
7 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.ResendEmailVerificationCredentials
8 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.VerifyEmailCredentials
9 | import com.example.noteswithrestapi.core.domain.model.AppResponse
10 |
11 | interface AuthenticationRepository {
12 |
13 | suspend fun login(loginCredentials: LoginCredentials): AppResponse //Return token
14 |
15 | suspend fun isLoggedIn(): AppResponse
16 |
17 | suspend fun register(registerCredentials: RegisterCredentials): AppResponse
18 |
19 | suspend fun verifyEmail(verifyEmailCredentials: VerifyEmailCredentials): AppResponse
20 |
21 | suspend fun resendEmailVerification(
22 | resendEmailVerificationCredentials: ResendEmailVerificationCredentials,
23 | ): AppResponse
24 |
25 | suspend fun sendPasswordResetCode(sendPasswordResetCodeCredentials: SendPasswordResetCodeCredentials): AppResponse
26 |
27 | suspend fun confirmPasswordReset(passwordResetConfirmCredentials: PasswordResetConfirmCredentials): AppResponse
28 |
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/ConfirmPasswordResetUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.PasswordResetConfirmCredentials
4 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository
5 | import com.example.noteswithrestapi.core.domain.model.AppResponse
6 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError
7 | import javax.inject.Inject
8 |
9 | class ConfirmPasswordResetUseCase @Inject constructor(
10 | private val authenticationRepository: AuthenticationRepository,
11 | ) {
12 |
13 | suspend operator fun invoke(
14 | email: String,
15 | password: String,
16 | confirmPassword: String,
17 | code: String,
18 | ): AppResponse {
19 |
20 | if (email.isEmpty()) {
21 | return AppResponse.failed(AuthenticationAppError.EmptyEmailError)
22 | }
23 |
24 | if (password.isEmpty()) {
25 | return AppResponse.failed(AuthenticationAppError.EmptyPasswordError)
26 | }
27 |
28 | if (password.length < 8) {
29 | return AppResponse.failed(AuthenticationAppError.ShortPasswordError)
30 | }
31 |
32 | if (password != confirmPassword) {
33 | return AppResponse.failed(AuthenticationAppError.PasswordsDoNotMatchError)
34 | }
35 |
36 | val passwordResetCodeCredentials = PasswordResetConfirmCredentials(email, password, code)
37 |
38 | return authenticationRepository.confirmPasswordReset(passwordResetCodeCredentials)
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/IsLoggedInUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository
4 | import com.example.noteswithrestapi.core.domain.model.AppResponse
5 | import javax.inject.Inject
6 |
7 | class IsLoggedInUseCase @Inject constructor(
8 | private val authenticationRepository: AuthenticationRepository
9 | ) {
10 |
11 | suspend operator fun invoke(): AppResponse {
12 | return authenticationRepository.isLoggedIn()
13 | }
14 |
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/LoginUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.LoginCredentials
4 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository
5 | import com.example.noteswithrestapi.core.domain.model.AppResponse
6 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError
7 | import javax.inject.Inject
8 |
9 | class LoginUseCase @Inject constructor(
10 | private val authenticationRepository: AuthenticationRepository,
11 | ) {
12 |
13 | suspend operator fun invoke(email: String, password: String): AppResponse {
14 | if (email.isBlank()) {
15 | return AppResponse.failed(AuthenticationAppError.EmptyEmailError)
16 | }
17 | if (password.isBlank()) {
18 | return AppResponse.failed(AuthenticationAppError.EmptyPasswordError)
19 | }
20 | val loginCredentials = LoginCredentials(email, password)
21 | return authenticationRepository.login(loginCredentials)
22 |
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/RegisterUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.RegisterCredentials
4 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository
5 | import com.example.noteswithrestapi.core.domain.model.AppResponse
6 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError
7 | import javax.inject.Inject
8 |
9 | class RegisterUseCase @Inject constructor(
10 | private val authenticationRepository: AuthenticationRepository
11 | ) {
12 |
13 | suspend operator fun invoke(
14 | email: String,
15 | password: String,
16 | confirmPassword: String,
17 | ): AppResponse {
18 | if (email.isEmpty()) {
19 | return AppResponse.failed(AuthenticationAppError.EmptyEmailError)
20 | }
21 | if (password.isEmpty()) {
22 | return AppResponse.failed(AuthenticationAppError.EmptyPasswordError)
23 | }
24 | if (confirmPassword.isEmpty()) {
25 | return AppResponse.failed(AuthenticationAppError.EmptyRepeatPasswordError)
26 | }
27 | if (password.count() < 8) {
28 | return AppResponse.failed(AuthenticationAppError.ShortPasswordError)
29 | }
30 | if (password != confirmPassword) {
31 | return AppResponse.failed(AuthenticationAppError.PasswordsDoNotMatchError)
32 | }
33 |
34 | val registerCredentials = RegisterCredentials(email, password, confirmPassword)
35 | return authenticationRepository.register(registerCredentials)
36 | }
37 |
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/ResendEmailVerificationCodeUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.ResendEmailVerificationCredentials
4 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository
5 | import com.example.noteswithrestapi.core.domain.model.AppResponse
6 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError
7 | import javax.inject.Inject
8 |
9 | class ResendEmailVerificationCodeUseCase @Inject constructor(
10 | private val authenticationRepository: AuthenticationRepository,
11 | ) {
12 |
13 | suspend operator fun invoke(email: String): AppResponse {
14 | if (email.isEmpty()) {
15 | return AppResponse.failed(AuthenticationAppError.EmptyEmailError)
16 | }
17 |
18 | val resendEmailVerificationCredentials = ResendEmailVerificationCredentials(email)
19 |
20 | return authenticationRepository.resendEmailVerification(resendEmailVerificationCredentials)
21 | }
22 |
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/SendPasswordResetCodeUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.SendPasswordResetCodeCredentials
4 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository
5 | import com.example.noteswithrestapi.core.domain.model.AppResponse
6 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError
7 | import javax.inject.Inject
8 |
9 | class SendPasswordResetCodeUseCase @Inject constructor(
10 | private val authenticationRepository: AuthenticationRepository,
11 | ) {
12 |
13 | suspend operator fun invoke(email: String): AppResponse {
14 | if (email.isEmpty()) {
15 | return AppResponse.failed(AuthenticationAppError.EmptyEmailError)
16 | }
17 |
18 | val sendPasswordResetCodeCredentials = SendPasswordResetCodeCredentials(email)
19 |
20 | return authenticationRepository.sendPasswordResetCode(sendPasswordResetCodeCredentials)
21 |
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/domain/usecase/VerifyEmailUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.domain.usecase
2 |
3 | import androidx.core.text.isDigitsOnly
4 | import com.example.noteswithrestapi.authentication_feature.domain.model.credentials.VerifyEmailCredentials
5 | import com.example.noteswithrestapi.authentication_feature.domain.repository.AuthenticationRepository
6 | import com.example.noteswithrestapi.core.domain.model.AppResponse
7 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError
8 | import javax.inject.Inject
9 |
10 | class VerifyEmailUseCase @Inject constructor(
11 | private val authenticationRepository: AuthenticationRepository,
12 | ) {
13 |
14 | suspend operator fun invoke(email: String, code: String): AppResponse {
15 | if (email.isEmpty()) {
16 | return AppResponse.failed(AuthenticationAppError.EmptyEmailError)
17 | }
18 |
19 | if (code.length < 6) {
20 | return AppResponse.failed(AuthenticationAppError.InvalidVerificationCodeError)
21 | }
22 |
23 | if (!code.isDigitsOnly()) {
24 | return AppResponse.failed(AuthenticationAppError.InvalidVerificationCodeError)
25 | }
26 |
27 | val verifyEmailCredentials = VerifyEmailCredentials(email, code)
28 |
29 | return authenticationRepository.verifyEmail(verifyEmailCredentials)
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/components/OTPTextField.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.width
11 | import androidx.compose.foundation.layout.wrapContentHeight
12 | import androidx.compose.foundation.text.BasicTextField
13 | import androidx.compose.foundation.text.KeyboardOptions
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.text.input.KeyboardType
21 | import androidx.compose.ui.text.style.TextAlign
22 | import androidx.compose.ui.unit.Dp
23 | import androidx.compose.ui.unit.TextUnit
24 | import androidx.compose.ui.unit.dp
25 | import androidx.compose.ui.unit.sp
26 |
27 | @Composable
28 | fun OTPTextField(
29 | modifier: Modifier = Modifier,
30 | textValue: () -> String = { "" },
31 | onTextValueChange: (String) -> Unit,
32 | charColor: Color = MaterialTheme.colorScheme.tertiary,
33 | underlineColor: Color = MaterialTheme.colorScheme.primary,
34 | charBackground: Color = Color.Transparent,
35 | charSize: TextUnit = 26.sp,
36 | containerSize: Dp = charSize.value.dp * 2,
37 | length: Int = 6,
38 | enabled: Boolean = true,
39 | password: Boolean = false,
40 | passwordChar: String = "",
41 | keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
42 | ) {
43 | BasicTextField(
44 | modifier = modifier,
45 | value = textValue(),
46 | onValueChange = {
47 | if (it.length <= length) {
48 | onTextValueChange.invoke(it)
49 | }
50 | },
51 | enabled = enabled,
52 | keyboardOptions = keyboardOptions,
53 | decorationBox = {
54 | Row(horizontalArrangement = Arrangement.spacedBy(0.dp)) {
55 | repeat(length) { index ->
56 | CharView(
57 | index = index,
58 | text = textValue(),
59 | charColor = charColor,
60 | underlineColor = underlineColor,
61 | charSize = charSize,
62 | containerSize = containerSize,
63 | charBackground = charBackground,
64 | password = password,
65 | passwordChar = passwordChar,
66 | )
67 | }
68 | }
69 | })
70 | }
71 |
72 | @Composable
73 | private fun CharView(
74 | index: Int,
75 | text: String,
76 | charColor: Color,
77 | underlineColor: Color,
78 | charSize: TextUnit,
79 | containerSize: Dp,
80 | charBackground: Color = Color.Transparent,
81 | password: Boolean = false,
82 | passwordChar: String = "",
83 | ) {
84 |
85 | val modifier = Modifier
86 | .width(containerSize)
87 | .background(charBackground)
88 |
89 | Column(
90 | horizontalAlignment = Alignment.CenterHorizontally,
91 | verticalArrangement = Arrangement.Center,
92 | ) {
93 | val char = when {
94 | index >= text.length -> ""
95 | password -> passwordChar
96 | else -> text[index].toString()
97 | }
98 | Text(
99 | text = char,
100 | color = charColor,
101 | modifier = modifier.wrapContentHeight(),
102 | style = MaterialTheme.typography.bodyLarge,
103 | fontSize = charSize,
104 | textAlign = TextAlign.Center,
105 | )
106 | Spacer(modifier = Modifier.height(2.dp))
107 | Box(
108 | modifier = Modifier
109 | .background(underlineColor)
110 | .height(1.5.dp)
111 | .width(containerSize / 1.75f)
112 | )
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/confirm_password_reset/ConfirmPasswordResetEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.confirm_password_reset
2 |
3 | sealed class ConfirmPasswordResetEvent {
4 |
5 | data class OnPasswordInputValueChange(val newValue: String): ConfirmPasswordResetEvent()
6 |
7 | data class OnConfirmPasswordInputValueChange(val newValue: String): ConfirmPasswordResetEvent()
8 |
9 | object OnChangePasswordVisualTransformation: ConfirmPasswordResetEvent()
10 |
11 | object OnChangeConfirmPasswordVisualTransformation: ConfirmPasswordResetEvent()
12 |
13 | data class OnCodeInputValueChange(val newValue: String): ConfirmPasswordResetEvent()
14 |
15 | object OnConfirmClick: ConfirmPasswordResetEvent()
16 |
17 | object OnResendCodeClick: ConfirmPasswordResetEvent()
18 |
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/confirm_password_reset/ConfirmPasswordResetState.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.confirm_password_reset
2 |
3 | data class ConfirmPasswordResetState(
4 | val passwordInput: String = "",
5 | val passwordError: String? = null,
6 | val isPasswordShown: Boolean = false,
7 |
8 | val confirmPasswordInput: String = "",
9 | val confirmPasswordError: String? = null,
10 | val isConfirmPasswordShown: Boolean = false,
11 |
12 | val codeInput: String = "",
13 |
14 | val isLoading: Boolean = false,
15 | val errorMessage: String? = null,
16 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/confirm_password_reset/ConfirmPasswordResetUiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.confirm_password_reset
2 |
3 | sealed class ConfirmPasswordResetUiEvent {
4 |
5 | object OnConfirmPasswordResetSuccess : ConfirmPasswordResetUiEvent()
6 |
7 | data class ShowSnackbar(val message: String) : ConfirmPasswordResetUiEvent()
8 |
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/login/LoginEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.login
2 |
3 | sealed class LoginEvent {
4 |
5 | object OnSignInClick: LoginEvent()
6 |
7 | data class OnPasswordInputValueChange(val newValue: String): LoginEvent()
8 |
9 | data class OnEmailInputValueChange(val newValue: String): LoginEvent()
10 |
11 | object OnChangePasswordVisualTransformation: LoginEvent()
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/login/LoginState.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.login
2 |
3 | data class LoginState(
4 |
5 | val emailInput: String = "",
6 | val emailErrorMessage: String? = null,
7 |
8 | val passwordInput: String = "",
9 | val isPasswordShown: Boolean = false,
10 | val passwordErrorMessage: String? = null,
11 |
12 | val errorMessage: String? = null,
13 | val isLoading: Boolean = false,
14 | )
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/login/LoginUiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.login
2 |
3 | sealed class LoginUiEvent {
4 |
5 | object SuccessfullyLoggedIn: LoginUiEvent()
6 |
7 | data class EmailNotVerified(val email: String): LoginUiEvent()
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/register/RegisterEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.register
2 |
3 | sealed class RegisterEvent {
4 |
5 | object OnSignUpClick: RegisterEvent()
6 |
7 | data class OnEmailInputValueChange(val newValue: String): RegisterEvent()
8 |
9 | data class OnPasswordInputValueChange(val newValue: String): RegisterEvent()
10 |
11 | data class OnConfirmPasswordInputValueChange(val newValue: String): RegisterEvent()
12 |
13 | object OnChangePasswordVisualTransformation: RegisterEvent()
14 |
15 | object OnChangeConfirmPasswordVisualTransformation: RegisterEvent()
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/register/RegisterState.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.register
2 |
3 | data class RegisterState(
4 | val emailInput: String = "",
5 | val emailErrorMessage: String? = null,
6 |
7 | val passwordInput: String = "",
8 | val isPasswordShown: Boolean = false,
9 | val passwordErrorMessage: String? = null,
10 |
11 | val confirmPasswordInput: String = "",
12 | val isConfirmPasswordShown: Boolean = false,
13 | val confirmPasswordErrorMessage: String? = null,
14 |
15 | val errorMessage: String? = null,
16 | val isLoading: Boolean = false,
17 | )
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/register/RegisterUiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.register
2 |
3 | sealed class RegisterUiEvent {
4 |
5 | data class OnRegisterSuccessful(val email: String): RegisterUiEvent()
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/reset_password/ResetPasswordEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.reset_password
2 |
3 | sealed class ResetPasswordEvent {
4 |
5 | data class OnEmailInputValueChange(val newValue: String): ResetPasswordEvent()
6 |
7 | object OnResetPasswordClick: ResetPasswordEvent()
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/reset_password/ResetPasswordState.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.reset_password
2 |
3 | data class ResetPasswordState(
4 | val emailInput: String = "",
5 | val emailError: String? = null,
6 |
7 | val errorMessage: String? = null,
8 | val isLoading: Boolean = false,
9 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/reset_password/ResetPasswordUiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.reset_password
2 |
3 | sealed class ResetPasswordUiEvent {
4 |
5 | data class OnResetCodeSentSuccessfully(val email: String): ResetPasswordUiEvent()
6 |
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/reset_password/ResetPasswordViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.reset_password
2 |
3 | import android.app.Application
4 | import android.util.Log
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import com.example.noteswithrestapi.R
11 | import com.example.noteswithrestapi.authentication_feature.domain.usecase.SendPasswordResetCodeUseCase
12 | import com.example.noteswithrestapi.core.domain.model.AppError
13 | import com.example.noteswithrestapi.core.domain.model.AppResponse
14 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError
15 | import dagger.hilt.android.lifecycle.HiltViewModel
16 | import kotlinx.coroutines.channels.Channel
17 | import kotlinx.coroutines.flow.receiveAsFlow
18 | import kotlinx.coroutines.launch
19 | import javax.inject.Inject
20 |
21 | @HiltViewModel
22 | class ResetPasswordViewModel @Inject constructor(
23 | private val sendPasswordResetCodeUseCase: SendPasswordResetCodeUseCase,
24 | private val application: Application,
25 | ) : ViewModel() {
26 |
27 |
28 | private val _uiEvent = Channel()
29 | val uiEvent = _uiEvent.receiveAsFlow()
30 |
31 | var resetPasswordState by mutableStateOf(ResetPasswordState())
32 | private set
33 |
34 |
35 | fun onEvent(event: ResetPasswordEvent) {
36 | when (event) {
37 | is ResetPasswordEvent.OnResetPasswordClick -> {
38 | sendResetCode(resetPasswordState.emailInput)
39 | }
40 |
41 | is ResetPasswordEvent.OnEmailInputValueChange -> {
42 | resetPasswordState = resetPasswordState.copy(
43 | emailInput = event.newValue,
44 | emailError = null
45 | )
46 | }
47 | }
48 | }
49 |
50 |
51 | private fun sendResetCode(email: String) {
52 | viewModelScope.launch {
53 | resetPasswordState = resetPasswordState.copy(isLoading = true)
54 |
55 | when (val result = sendPasswordResetCodeUseCase(email)) {
56 | is AppResponse.Success -> {
57 | resetPasswordState = resetPasswordState.copy(isLoading = false)
58 | _uiEvent.send(ResetPasswordUiEvent.OnResetCodeSentSuccessfully(email))
59 | }
60 |
61 | is AppResponse.Failed -> {
62 | resetPasswordState = resetPasswordState.copy(isLoading = false)
63 | handleAppError(result.error)
64 | }
65 | }
66 |
67 | }
68 | }
69 |
70 |
71 | private fun handleAppError(error: AppError) {
72 | when (error) {
73 |
74 | is AppError.ServerError -> {
75 | resetPasswordState = resetPasswordState.copy(
76 | errorMessage = application.getString(R.string.error_server)
77 | )
78 | }
79 |
80 | is AppError.PoorNetworkConnectionError -> {
81 | resetPasswordState = resetPasswordState.copy(
82 | errorMessage = application.getString(R.string.error_poor_network_connection),
83 | )
84 | }
85 |
86 | is AppError.GeneralError -> {
87 | resetPasswordState = resetPasswordState.copy(
88 | errorMessage = application.getString(error.messageResId),
89 | )
90 | }
91 |
92 | is AuthenticationAppError -> {
93 | handleAuthAppError(error)
94 | }
95 |
96 | else -> {
97 | resetPasswordState = resetPasswordState.copy(
98 | errorMessage = application.getString(R.string.error_unknown),
99 | )
100 | Log.e("UNEXPECTED_ERROR", error.toString())
101 | }
102 | }
103 | }
104 |
105 | private fun handleAuthAppError(error: AuthenticationAppError) {
106 | when (error) {
107 | is AuthenticationAppError.EmptyEmailError -> {
108 | resetPasswordState = resetPasswordState.copy(
109 | emailError = application.getString(R.string.error_empty_email),
110 | )
111 | }
112 |
113 | is AuthenticationAppError.InvalidEmailError -> {
114 | resetPasswordState = resetPasswordState.copy(
115 | emailError = application.getString(R.string.error_invalid_email),
116 | )
117 | }
118 |
119 | is AuthenticationAppError.UserDoesNotExist -> {
120 | resetPasswordState = resetPasswordState.copy(
121 | errorMessage = application.getString(R.string.error_user_does_not_exists),
122 | )
123 | }
124 |
125 | else -> {
126 | resetPasswordState = resetPasswordState.copy(
127 | errorMessage = application.getString(R.string.error_unknown),
128 | )
129 | Log.e("UNEXPECTED_ERROR", error.toString())
130 | }
131 | }
132 | }
133 |
134 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/verify_email/VerifyEmailEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.verify_email
2 |
3 | sealed class VerifyEmailEvent {
4 |
5 | data class OnCodeInputValueChange(val newValue: String) : VerifyEmailEvent()
6 |
7 | object OnConfirmClick: VerifyEmailEvent()
8 |
9 | object OnResendCodeClick: VerifyEmailEvent()
10 |
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/verify_email/VerifyEmailState.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.verify_email
2 |
3 | data class VerifyEmailState(
4 | val codeInput: String = "",
5 | val codeError: String? = null,
6 |
7 | val isLoading: Boolean = false,
8 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/verify_email/VerifyEmailUiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.verify_email
2 |
3 | sealed class VerifyEmailUiEvent {
4 |
5 | object OnEmailVerifiedSuccessfully: VerifyEmailUiEvent()
6 |
7 | data class ShowSnackbar(val message: String): VerifyEmailUiEvent()
8 |
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/authentication_feature/presentation/verify_email/VerifyEmailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.authentication_feature.presentation.verify_email
2 |
3 | import android.app.Application
4 | import android.util.Log
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import androidx.lifecycle.SavedStateHandle
9 | import androidx.lifecycle.ViewModel
10 | import androidx.lifecycle.viewModelScope
11 | import com.example.noteswithrestapi.R
12 | import com.example.noteswithrestapi.authentication_feature.domain.usecase.ResendEmailVerificationCodeUseCase
13 | import com.example.noteswithrestapi.authentication_feature.domain.usecase.VerifyEmailUseCase
14 | import com.example.noteswithrestapi.core.domain.model.AppError
15 | import com.example.noteswithrestapi.core.domain.model.AppResponse
16 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError
17 | import com.example.noteswithrestapi.core.presentation.navigation.Screen
18 | import dagger.hilt.android.lifecycle.HiltViewModel
19 | import kotlinx.coroutines.channels.Channel
20 | import kotlinx.coroutines.flow.receiveAsFlow
21 | import kotlinx.coroutines.launch
22 | import javax.inject.Inject
23 |
24 | @HiltViewModel
25 | class VerifyEmailViewModel @Inject constructor(
26 | private val verifyEmailUseCase: VerifyEmailUseCase,
27 | private val resendEmailVerificationCodeUseCase: ResendEmailVerificationCodeUseCase,
28 | private val savedStateHandle: SavedStateHandle,
29 | private val application: Application,
30 | ) : ViewModel() {
31 |
32 |
33 | private val _uiEvent = Channel()
34 | val uiEvent = _uiEvent.receiveAsFlow()
35 |
36 | var verifyEmailState by mutableStateOf(VerifyEmailState())
37 | private set
38 |
39 | private lateinit var email: String
40 |
41 | init {
42 | viewModelScope.launch {
43 | savedStateHandle.get(Screen.EMAIL_ARG)?.let { email ->
44 | this@VerifyEmailViewModel.email = email
45 | } ?: throw RuntimeException("Email argument wasn't passed")
46 | }
47 | }
48 |
49 |
50 | fun onEvent(event: VerifyEmailEvent) {
51 | when (event) {
52 | is VerifyEmailEvent.OnCodeInputValueChange -> {
53 | verifyEmailState = verifyEmailState.copy(
54 | codeInput = event.newValue,
55 | codeError = null
56 | )
57 | }
58 |
59 | VerifyEmailEvent.OnConfirmClick -> {
60 | verifyEmail(email, verifyEmailState.codeInput)
61 | }
62 |
63 | VerifyEmailEvent.OnResendCodeClick -> {
64 | resendEmail(email)
65 | }
66 | }
67 | }
68 |
69 | private fun verifyEmail(email: String, code: String) {
70 | viewModelScope.launch {
71 | verifyEmailState = verifyEmailState.copy(isLoading = true)
72 |
73 | when (val result = verifyEmailUseCase(email, code)) {
74 | is AppResponse.Success -> {
75 | verifyEmailState = verifyEmailState.copy(isLoading = false)
76 | _uiEvent.send(VerifyEmailUiEvent.OnEmailVerifiedSuccessfully)
77 | }
78 |
79 | is AppResponse.Failed -> {
80 | verifyEmailState = verifyEmailState.copy(isLoading = false)
81 | handleAppError(result.error)
82 | }
83 | }
84 |
85 | }
86 | }
87 |
88 | private fun resendEmail(email: String) {
89 | verifyEmailState = verifyEmailState.copy(isLoading = true)
90 | viewModelScope.launch {
91 | when (val result = resendEmailVerificationCodeUseCase(email)) {
92 | is AppResponse.Success -> {
93 | verifyEmailState = verifyEmailState.copy(isLoading = false, codeError = null)
94 | _uiEvent.send(VerifyEmailUiEvent.ShowSnackbar(application.getString(R.string.text_code_resent)))
95 | }
96 |
97 | is AppResponse.Failed -> {
98 | verifyEmailState = verifyEmailState.copy(isLoading = false, codeError = null)
99 | handleAppError(result.error)
100 | }
101 | }
102 | }
103 | }
104 |
105 |
106 | private fun handleAppError(error: AppError) {
107 | when (error) {
108 | is AppError.GeneralError -> {
109 | verifyEmailState = verifyEmailState.copy(
110 | codeError = application.getString(error.messageResId)
111 | )
112 | }
113 |
114 | is AppError.PoorNetworkConnectionError -> {
115 | verifyEmailState = verifyEmailState.copy(
116 | codeError = application.getString(R.string.error_poor_network_connection)
117 | )
118 | }
119 |
120 | is AppError.ServerError -> {
121 | verifyEmailState = verifyEmailState.copy(
122 | codeError = application.getString(R.string.error_server)
123 | )
124 | }
125 |
126 | is AuthenticationAppError -> {
127 | handleAuthAppError(error)
128 | }
129 |
130 | else -> {
131 | verifyEmailState = verifyEmailState.copy(
132 | codeError = application.getString(R.string.error_unknown)
133 | )
134 | Log.e("UNKNOWN_ERROR", error.toString())
135 | }
136 | }
137 | }
138 |
139 | private fun handleAuthAppError(error: AuthenticationAppError) {
140 | when (error) {
141 | is AuthenticationAppError.InvalidVerificationCodeError -> {
142 | verifyEmailState = verifyEmailState.copy(
143 | codeError = application.getString(R.string.error_invalid_verification_code)
144 | )
145 | }
146 |
147 | else -> {
148 | verifyEmailState = verifyEmailState.copy(
149 | codeError = application.getString(R.string.error_unknown)
150 | )
151 | Log.e("UNKNOWN_ERROR", error.toString())
152 | }
153 | }
154 | }
155 |
156 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/NoteApplication.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class NoteApplication : Application()
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/data/network/HttpCustomErrorCode.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.data.network
2 |
3 | enum class HttpCustomErrorCode(val code: String) {
4 |
5 | //Authentication errors
6 | InvalidToken("4001"),
7 | InvalidEmailOrPassword("4002"),
8 | InvalidEmail("4003"),
9 | EmailAlreadyVerified("4004"),
10 | EmailNotVerified("4005"),
11 | UsedDoesNotExist("4006"),
12 | UsedAlreadyExist("4007"),
13 | InvalidVerificationCode("4008"),
14 | InvalidPasswordResetCode("4009"),
15 | InvalidCredentials("4010"),
16 | ShortPassword("4011"),
17 |
18 | NotFound("404"),
19 | Unauthorized("401"),
20 | Forbidden("403"),
21 |
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/data/network/NetworkConstants.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.data.network
2 |
3 | object NetworkConstants {
4 |
5 | const val API_BASE_URL = "http://192.168.127.29:8000/"
6 |
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/data/network/NetworkUtil.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.data.network
2 |
3 | import com.example.noteswithrestapi.R
4 | import com.example.noteswithrestapi.core.domain.model.AppError
5 | import com.example.noteswithrestapi.authentication_feature.domain.error.AuthenticationAppError
6 | import okhttp3.MediaType.Companion.toMediaTypeOrNull
7 | import okhttp3.RequestBody.Companion.toRequestBody
8 | import org.json.JSONObject
9 | import retrofit2.Response
10 |
11 | fun createRequestBody(vararg params: Pair) =
12 | JSONObject(mapOf(*params)).toString()
13 | .toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
14 |
15 |
16 | fun Response.getAppErrorByHttpErrorCode(): AppError {
17 | val code = this.getHttpErrorCode() ?: return AppError.GeneralError(R.string.error_unknown)
18 | if (code.toInt() in 500..599) {
19 | return AppError.ServerError
20 | }
21 | return parseHttpErrorCodeToAppError(code)
22 | }
23 |
24 | fun parseHttpErrorCodeToAppError(code: String): AppError {
25 | return when (code) {
26 |
27 | HttpCustomErrorCode.InvalidVerificationCode.code -> AuthenticationAppError.InvalidVerificationCodeError
28 | HttpCustomErrorCode.ShortPassword.code -> AuthenticationAppError.ShortPasswordError
29 | HttpCustomErrorCode.InvalidVerificationCode.code -> AuthenticationAppError.InvalidVerificationCodeError
30 | HttpCustomErrorCode.InvalidPasswordResetCode.code -> AuthenticationAppError.InvalidVerificationCodeError
31 | HttpCustomErrorCode.InvalidEmail.code -> AuthenticationAppError.InvalidEmailError
32 | HttpCustomErrorCode.UsedAlreadyExist.code -> AuthenticationAppError.UserAlreadyExist
33 | HttpCustomErrorCode.UsedDoesNotExist.code -> AuthenticationAppError.UserDoesNotExist
34 | HttpCustomErrorCode.InvalidEmailOrPassword.code -> AuthenticationAppError.InvalidEmailOrPasswordError
35 | HttpCustomErrorCode.EmailNotVerified.code -> AuthenticationAppError.EmailNotVerifiedError
36 |
37 | HttpCustomErrorCode.InvalidCredentials.code -> AppError.InvalidCredentialsError
38 | HttpCustomErrorCode.NotFound.code -> AppError.NotFoundError
39 | HttpCustomErrorCode.Unauthorized.code -> AppError.UnauthorizedError
40 | HttpCustomErrorCode.Forbidden.code -> AppError.ForbiddenError
41 | else -> AppError.GeneralError(R.string.error_unknown)
42 | }
43 | }
44 |
45 |
46 | fun Response.getHttpErrorCode(): String? {
47 | try {
48 | if (this.code() in 500..599) {
49 | return this.code().toString()
50 | }
51 | if (this.code() in 400..499) {
52 | return this.errorBody()?.string()?.let { jsonString ->
53 | JSONObject(jsonString).getString("code")
54 | } ?: this.code().toString()
55 | }
56 | return null
57 | } catch (e: Exception) {
58 | return this.code().toString()
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/data/token/TokenManager.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.data.token
2 |
3 | interface TokenManager {
4 |
5 | val authToken: String?
6 |
7 | fun saveToken(token: String)
8 |
9 | fun removeToken()
10 |
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/data/token/TokenManagerEncryptedSharedPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.data.token
2 |
3 | import android.content.Context
4 | import androidx.security.crypto.EncryptedSharedPreferences
5 | import androidx.security.crypto.MasterKey
6 | import dagger.hilt.android.qualifiers.ApplicationContext
7 | import javax.inject.Inject
8 |
9 | class TokenManagerEncryptedSharedPreferences @Inject constructor(
10 | @ApplicationContext context: Context,
11 | ): TokenManager {
12 |
13 | private val masterKey = MasterKey.Builder(context)
14 | .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
15 | .build()
16 |
17 | private val sharedPreferences = EncryptedSharedPreferences.create(
18 | context,
19 | TOKEN_STORAGE_FILE_NAME,
20 | masterKey,
21 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
22 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
23 | )
24 |
25 |
26 | override val authToken: String?
27 | get() = sharedPreferences.getString(AUTH_TOKEN_KEY, null)
28 |
29 | override fun saveToken(token: String) {
30 | sharedPreferences.edit()
31 | .putString(AUTH_TOKEN_KEY, token)
32 | .apply()
33 | }
34 |
35 | override fun removeToken() {
36 | sharedPreferences.edit()
37 | .remove(AUTH_TOKEN_KEY)
38 | .apply()
39 | }
40 |
41 |
42 | companion object {
43 |
44 | private const val TOKEN_STORAGE_FILE_NAME = "token_storage"
45 |
46 | private const val AUTH_TOKEN_KEY = "auth_token_key"
47 |
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.di
2 |
3 | import com.example.noteswithrestapi.core.data.token.TokenManager
4 | import com.example.noteswithrestapi.core.data.token.TokenManagerEncryptedSharedPreferences
5 | import com.example.noteswithrestapi.core.data.network.NetworkConstants
6 | import dagger.Binds
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import okhttp3.OkHttpClient
12 | import okhttp3.logging.HttpLoggingInterceptor
13 | import retrofit2.Retrofit
14 | import retrofit2.converter.gson.GsonConverterFactory
15 | import javax.inject.Singleton
16 |
17 | @Module
18 | @InstallIn(SingletonComponent::class)
19 | interface AppModule {
20 |
21 | @Binds
22 | @Singleton
23 | fun bindTokenManager(
24 | tokenManagerEncryptedSharedPreferences: TokenManagerEncryptedSharedPreferences
25 | ): TokenManager
26 |
27 | companion object {
28 |
29 | @Provides
30 | @Singleton
31 | fun provideNoteApiRetrofit(
32 | okHttpClient: OkHttpClient
33 | ): Retrofit {
34 | return Retrofit.Builder()
35 | .baseUrl(NetworkConstants.API_BASE_URL)
36 | .addConverterFactory(GsonConverterFactory.create())
37 | .client(okHttpClient)
38 | .build()
39 | }
40 |
41 | @Singleton
42 | @Provides
43 | fun provideOkHttpClient(): OkHttpClient {
44 | return OkHttpClient.Builder()
45 | .addInterceptor(HttpLoggingInterceptor().apply {
46 | level = HttpLoggingInterceptor.Level.BODY
47 | }).build()
48 | }
49 |
50 | }
51 |
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/domain/model/AppError.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.domain.model
2 |
3 | open class AppError(
4 | message: String? = null,
5 | cause: Exception? = null
6 | ): Error(message, cause) {
7 |
8 | //Common Errors
9 | data class GeneralError(val messageResId: Int): AppError()
10 |
11 | object PoorNetworkConnectionError: AppError()
12 |
13 | object ServerError: AppError()
14 |
15 | object InvalidCredentialsError: AppError()
16 |
17 | object NotFoundError: AppError()
18 |
19 | object UnauthorizedError: AppError()
20 |
21 | object ForbiddenError: AppError()
22 |
23 |
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/domain/model/AppResponse.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.domain.model
2 |
3 |
4 | sealed class AppResponse {
5 |
6 | //Success state
7 | data class Success(val data: T) : AppResponse()
8 |
9 | //Failed state
10 | data class Failed(val error: AppError) : AppResponse()
11 |
12 |
13 | companion object {
14 |
15 | //GET SUCCESS STATE
16 | fun success(data: T) = Success(data)
17 |
18 | //GET FAILED STATE
19 | fun failed(error: AppError) = Failed(error)
20 |
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/domain/paginator/DefaultPaginator.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.domain.paginator
2 |
3 | import com.example.noteswithrestapi.core.domain.model.AppError
4 | import com.example.noteswithrestapi.core.domain.model.AppResponse
5 |
6 | class DefaultPaginator(
7 | private val initialKey: Key,
8 | private inline val onLoadUpdated: (Boolean) -> Unit,
9 | private inline val onRequest: suspend (nextKey: Key) -> AppResponse>,
10 | private inline val getNextKey: suspend (List- ) -> Key,
11 | private inline val onError: suspend (AppError) -> Unit,
12 | private inline val onSuccess: suspend (items: List
- , newKey: Key) -> Unit,
13 | ) : Paginator {
14 |
15 | private var currentKey = initialKey
16 | private var isMakingRequest = false
17 |
18 | override suspend fun loadNextPage() {
19 | if (isMakingRequest) {
20 | return
21 | }
22 | isMakingRequest = true
23 | onLoadUpdated(true)
24 | val result = onRequest(currentKey)
25 | isMakingRequest = false
26 | when (result) {
27 | is AppResponse.Success -> {
28 | val items = result.data
29 | currentKey = getNextKey(items)
30 | onSuccess(items, currentKey)
31 | onLoadUpdated(false)
32 | return
33 | }
34 |
35 | is AppResponse.Failed -> {
36 | onError(result.error)
37 | onLoadUpdated(false)
38 | return
39 | }
40 | }
41 | }
42 |
43 | override fun reset() {
44 | currentKey = initialKey
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/domain/paginator/Paginator.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.domain.paginator
2 |
3 | interface Paginator {
4 |
5 | suspend fun loadNextPage()
6 |
7 | fun reset()
8 |
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.animation.ExperimentalAnimationApi
7 | import androidx.compose.material3.MaterialTheme
8 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme
9 | import com.google.accompanist.navigation.animation.rememberAnimatedNavController
10 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
11 | import dagger.hilt.android.AndroidEntryPoint
12 |
13 | @AndroidEntryPoint
14 | class MainActivity : ComponentActivity() {
15 |
16 | @OptIn(ExperimentalAnimationApi::class)
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | setContent {
20 | NotesWithRESTAPITheme() {
21 | val systemUiController = rememberSystemUiController().apply {
22 | this.setSystemBarsColor(MaterialTheme.colorScheme.surface)
23 | }
24 | val navController = rememberAnimatedNavController()
25 | MainScreen(navController = navController)
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/MainScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation
2 |
3 | import androidx.compose.material3.DrawerValue
4 | import androidx.compose.material3.ExperimentalMaterial3Api
5 | import androidx.compose.material3.ModalNavigationDrawer
6 | import androidx.compose.material3.rememberDrawerState
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.rememberCoroutineScope
9 | import androidx.navigation.NavHostController
10 | import androidx.navigation.compose.currentBackStackEntryAsState
11 | import com.example.noteswithrestapi.core.presentation.components.AppDrawerContent
12 | import com.example.noteswithrestapi.core.presentation.navigation.NavigationItem
13 | import com.example.noteswithrestapi.core.presentation.navigation.RootNavGraph
14 | import kotlinx.coroutines.launch
15 |
16 |
17 | @OptIn(ExperimentalMaterial3Api::class)
18 | @Composable
19 | fun MainScreen(
20 | navController: NavHostController,
21 | ) {
22 |
23 | val navBackstackEntry = navController.currentBackStackEntryAsState()
24 |
25 | val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
26 |
27 | val scope = rememberCoroutineScope()
28 |
29 | ModalNavigationDrawer(
30 | drawerState = drawerState,
31 | gesturesEnabled = false,
32 | drawerContent = {
33 | navBackstackEntry.value?.destination?.parent?.route?.let { currentGraph ->
34 | AppDrawerContent(
35 | items = listOf(
36 | NavigationItem.NotesFeature,
37 | NavigationItem.ProfileFeature
38 | ),
39 | currentRoute = currentGraph,
40 | onItemClick = { navItem ->
41 | scope.launch {
42 | drawerState.close()
43 | }
44 | if (navItem.route != currentGraph) {
45 | navController.navigate(navItem.route)
46 | }
47 | }
48 | )
49 | }
50 | },
51 | content = {
52 | RootNavGraph(
53 | navController = navController,
54 | onShowNavigationDrawer = {
55 | scope.launch {
56 | drawerState.open()
57 | }
58 | }
59 | )
60 | }
61 | )
62 |
63 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/components/AppDrawerContent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.components
2 |
3 | import androidx.compose.foundation.layout.Spacer
4 | import androidx.compose.foundation.layout.height
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material3.ExperimentalMaterial3Api
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.ModalDrawerSheet
10 | import androidx.compose.material3.NavigationDrawerItem
11 | import androidx.compose.material3.NavigationDrawerItemDefaults
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.*
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.res.stringResource
17 | import androidx.compose.ui.unit.dp
18 | import com.example.noteswithrestapi.core.presentation.navigation.NavigationItem
19 |
20 |
21 | @OptIn(ExperimentalMaterial3Api::class)
22 | @Composable
23 | fun AppDrawerContent(
24 | items: List,
25 | currentRoute: String,
26 | onItemClick: (NavigationItem) -> Unit,
27 | ) {
28 |
29 | ModalDrawerSheet {
30 | Spacer(Modifier.height(12.dp))
31 | items.forEach { item ->
32 | NavigationDrawerItem(
33 | icon = { Icon(item.icon, contentDescription = null) },
34 | label = { Text(stringResource(id = item.nameId)) },
35 | selected = item.route == currentRoute,
36 | onClick = {
37 | onItemClick(item)
38 | },
39 | colors = NavigationDrawerItemDefaults.colors(
40 | selectedContainerColor = MaterialTheme.colorScheme.secondary,
41 | unselectedContainerColor = Color.Transparent
42 | ),
43 | modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
44 | )
45 | }
46 | }
47 |
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/components/ButtonComponent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.foundation.layout.width
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.rounded.Login
11 | import androidx.compose.material3.Button
12 | import androidx.compose.material3.ButtonDefaults
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.graphics.Shape
21 | import androidx.compose.ui.graphics.vector.ImageVector
22 | import androidx.compose.ui.text.font.FontWeight
23 | import androidx.compose.ui.tooling.preview.Preview
24 | import androidx.compose.ui.unit.dp
25 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme
26 |
27 | @Composable
28 | fun ButtonComponent(
29 | modifier: Modifier = Modifier,
30 | text: String,
31 | leadingIcon: ImageVector? = null,
32 | backgroundColor: Color = MaterialTheme.colorScheme.secondary,
33 | contentColor: Color = MaterialTheme.colorScheme.onSecondary,
34 | contentPadding: PaddingValues = PaddingValues(start = 32.dp, end = 40.dp, top = 14.dp, bottom = 14.dp),
35 | enabled: () -> Boolean,
36 | shape: Shape = MaterialTheme.shapes.large,
37 | onButtonClick: () -> Unit,
38 | ) {
39 |
40 | Button(
41 | modifier = modifier,
42 | colors = ButtonDefaults.buttonColors(
43 | containerColor = backgroundColor,
44 | contentColor = contentColor
45 | ),
46 | shape = shape,
47 | enabled = enabled(),
48 | contentPadding = contentPadding,
49 | onClick = {
50 | onButtonClick()
51 | },
52 | ) {
53 | Row(
54 | modifier = Modifier,
55 | horizontalArrangement = Arrangement.Center,
56 | verticalAlignment = Alignment.CenterVertically
57 | ) {
58 | if (leadingIcon != null) {
59 | Icon(
60 | imageVector = leadingIcon,
61 | contentDescription = text,
62 | modifier = Modifier.size(18.dp),
63 | )
64 | Spacer(modifier = Modifier.width(9.dp))
65 | }
66 | Text(
67 | text = text,
68 | style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)
69 | )
70 | }
71 | }
72 | }
73 |
74 | @Preview(showBackground = false)
75 | @Composable
76 | fun ButtonComponentPreview() {
77 | NotesWithRESTAPITheme(darkTheme = true) {
78 | ButtonComponent(
79 | text = "Login",
80 | enabled = { true },
81 | leadingIcon = Icons.Rounded.Login,
82 | onButtonClick = {}
83 | )
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/components/InputFieldComponent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.components
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.interaction.MutableInteractionSource
5 | import androidx.compose.foundation.interaction.collectIsFocusedAsState
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.fillMaxHeight
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.foundation.text.KeyboardOptions
14 | import androidx.compose.material.icons.Icons
15 | import androidx.compose.material.icons.rounded.Email
16 | import androidx.compose.material3.ExperimentalMaterial3Api
17 | import androidx.compose.material3.Icon
18 | import androidx.compose.material3.IconButton
19 | import androidx.compose.material3.MaterialTheme
20 | import androidx.compose.material3.Text
21 | import androidx.compose.material3.TextField
22 | import androidx.compose.material3.TextFieldDefaults
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.graphics.Color
28 | import androidx.compose.ui.graphics.vector.ImageVector
29 | import androidx.compose.ui.res.stringResource
30 | import androidx.compose.ui.text.font.FontWeight
31 | import androidx.compose.ui.text.input.KeyboardType
32 | import androidx.compose.ui.text.input.VisualTransformation
33 | import androidx.compose.ui.tooling.preview.Preview
34 | import androidx.compose.ui.unit.dp
35 | import androidx.compose.ui.unit.sp
36 | import com.example.noteswithrestapi.R
37 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme
38 |
39 |
40 | val inputFieldDefaultModifier = Modifier
41 | .padding(start = 25.dp, end = 25.dp)
42 |
43 |
44 | @OptIn(ExperimentalMaterial3Api::class)
45 | @Composable
46 | fun InputFieldComponent(
47 | modifier: Modifier = Modifier,
48 | textValue: () -> String,
49 | onValueChanged: (String) -> Unit,
50 | error: () -> String?,
51 | label: String,
52 | leadingIcon: ImageVector,
53 | trailingIcon: ImageVector? = null,
54 | onTrailingIconClicked: (() -> Unit)? = null,
55 | keyboardType: KeyboardType = KeyboardType.Ascii,
56 | visualTransformation: VisualTransformation = VisualTransformation.None,
57 | textColor: Color = MaterialTheme.colorScheme.primary,
58 | labelColor: Color = MaterialTheme.colorScheme.onBackground,
59 | selectedBackgroundColor: Color = MaterialTheme.colorScheme.secondary,
60 | unselectedBackgroundColor: Color = Color.Transparent,
61 | errorColor: Color = MaterialTheme.colorScheme.error,
62 | enabled: () -> Boolean = { true },
63 | ) {
64 |
65 | val textFieldInteractionSource = remember { MutableInteractionSource() }
66 |
67 | val isFocused = textFieldInteractionSource.collectIsFocusedAsState()
68 |
69 |
70 | Column(
71 | modifier = modifier
72 | ) {
73 | AnimatedVisibility(visible = error() != null) {
74 | error()?.let { error ->
75 | Text(
76 | text = error,
77 | color = errorColor,
78 | style = MaterialTheme.typography.bodySmall.copy(
79 | fontSize = 12.sp,
80 | fontWeight = FontWeight.Medium
81 | ),
82 | modifier = Modifier.padding(start = 5.dp)
83 | )
84 | }
85 | }
86 | TextField(
87 | modifier = Modifier
88 | .fillMaxWidth()
89 | .padding(top = 3.dp)
90 | .height(60.dp),
91 | value = textValue(),
92 | onValueChange = onValueChanged,
93 | textStyle = MaterialTheme.typography.bodyMedium.copy(color = textColor),
94 | visualTransformation = visualTransformation,
95 | keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
96 | shape = MaterialTheme.shapes.medium,
97 | colors = TextFieldDefaults.textFieldColors(
98 | cursorColor = textColor,
99 | focusedIndicatorColor = Color.Transparent,
100 | unfocusedIndicatorColor = Color.Transparent,
101 | containerColor = if (isFocused.value) selectedBackgroundColor else unselectedBackgroundColor,
102 | disabledIndicatorColor = Color.Transparent,
103 | ),
104 | interactionSource = textFieldInteractionSource,
105 | maxLines = 1,
106 | enabled = enabled(),
107 | label = {
108 | Box(modifier = Modifier.fillMaxHeight(0.55f)) {
109 | Text(
110 | text = label,
111 | color = labelColor,
112 | style = MaterialTheme.typography.bodyMedium.copy(
113 | fontWeight = FontWeight.Medium,
114 | fontSize = if (isFocused.value) 12.sp else 14.sp,
115 | color = MaterialTheme.colorScheme.onBackground
116 | ),
117 | modifier = Modifier.align(Alignment.CenterStart)
118 | )
119 | }
120 |
121 | },
122 | leadingIcon = {
123 | Icon(
124 | imageVector = leadingIcon,
125 | contentDescription = stringResource(R.string.desc_leading_icon),
126 | modifier = Modifier.size(32.dp),
127 | tint = textColor
128 | )
129 | },
130 | trailingIcon = {
131 | IconButton(
132 | onClick = {
133 | onTrailingIconClicked?.invoke()
134 | }
135 | ) {
136 | if (trailingIcon != null) {
137 | Icon(
138 | imageVector = trailingIcon,
139 | contentDescription = stringResource(R.string.desc_leading_icon),
140 | modifier = Modifier.size(27.dp),
141 | tint = textColor
142 | )
143 | }
144 | }
145 | }
146 | )
147 | }
148 | }
149 |
150 |
151 | @Preview
152 | @Composable
153 | fun InputFieldComponentPreview() {
154 | NotesWithRESTAPITheme(darkTheme = true) {
155 | InputFieldComponent(
156 | textValue = { "" },
157 | onValueChanged = {},
158 | label = "EMAIL",
159 | error = { "Maga shooher" },
160 | leadingIcon = Icons.Rounded.Email
161 | )
162 | }
163 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/components/LoadingFailedComponent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.size
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.rounded.Replay
8 | import androidx.compose.material3.Icon
9 | import androidx.compose.material3.IconButton
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.tooling.preview.Preview
17 | import androidx.compose.ui.unit.dp
18 | import com.example.noteswithrestapi.R
19 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme
20 |
21 |
22 | @Composable
23 | fun LoadingFailedComponent(
24 | errorMessage: String,
25 | onRetryClick: (() -> Unit)?,
26 | modifier: Modifier = Modifier,
27 | ) {
28 |
29 | Column(
30 | modifier = modifier,
31 | horizontalAlignment = Alignment.CenterHorizontally
32 | ) {
33 | Text(
34 | text = errorMessage,
35 | style = MaterialTheme.typography.bodyLarge,
36 | color = MaterialTheme.colorScheme.error
37 | )
38 | if (onRetryClick != null) {
39 | Text(
40 | text = stringResource(R.string.text_try_again),
41 | style = MaterialTheme.typography.bodyLarge,
42 | color = MaterialTheme.colorScheme.primary
43 | )
44 | IconButton(
45 | onClick = {
46 | onRetryClick()
47 | },
48 | ) {
49 | Icon(
50 | imageVector = Icons.Rounded.Replay,
51 | contentDescription = stringResource(R.string.desc_retry_icon),
52 | tint = MaterialTheme.colorScheme.primary,
53 | modifier = Modifier.size(30.dp)
54 | )
55 | }
56 | }
57 | }
58 |
59 | }
60 |
61 |
62 | @Preview
63 | @Composable
64 | fun LoadingFailedComponentPreview() {
65 | NotesWithRESTAPITheme {
66 | LoadingFailedComponent(
67 | errorMessage = "Failed to load notes",
68 | onRetryClick = { },
69 | modifier = Modifier.fillMaxWidth()
70 | )
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/components/OutlinedButtonComponent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.components
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.size
9 | import androidx.compose.foundation.layout.width
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.rounded.Login
12 | import androidx.compose.material3.ButtonDefaults
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.OutlinedButton
16 | import androidx.compose.material3.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.graphics.Shape
22 | import androidx.compose.ui.graphics.vector.ImageVector
23 | import androidx.compose.ui.text.font.FontWeight
24 | import androidx.compose.ui.tooling.preview.Preview
25 | import androidx.compose.ui.unit.dp
26 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme
27 |
28 | @Composable
29 | fun OutlinedButtonComponent(
30 | modifier: Modifier = Modifier,
31 | text: String,
32 | leadingIcon: ImageVector? = null,
33 | borderColor: Color = MaterialTheme.colorScheme.tertiary,
34 | contentColor: Color = MaterialTheme.colorScheme.tertiary,
35 | contentPadding: PaddingValues = PaddingValues(start = 32.dp, end = 40.dp, top = 14.dp, bottom = 14.dp),
36 | enabled: () -> Boolean,
37 | shape: Shape = MaterialTheme.shapes.large,
38 | onButtonClick: () -> Unit,
39 | ) {
40 |
41 | OutlinedButton(
42 | modifier = modifier,
43 | colors = ButtonDefaults.outlinedButtonColors(
44 | contentColor = contentColor
45 | ),
46 | border = BorderStroke(2.dp, borderColor),
47 | shape = shape,
48 | enabled = enabled(),
49 | contentPadding = contentPadding,
50 | onClick = {
51 | onButtonClick()
52 | },
53 | ) {
54 | Row(
55 | modifier = Modifier,
56 | horizontalArrangement = Arrangement.Center,
57 | verticalAlignment = Alignment.CenterVertically
58 | ) {
59 | if (leadingIcon != null) {
60 | Icon(
61 | imageVector = leadingIcon,
62 | contentDescription = text,
63 | modifier = Modifier.size(18.dp),
64 | )
65 | Spacer(modifier = Modifier.width(9.dp))
66 | }
67 | Text(
68 | text = text,
69 | style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)
70 | )
71 | }
72 | }
73 | }
74 |
75 | @Preview(showBackground = false)
76 | @Composable
77 | fun OutlinedButtonComponentPreview() {
78 | NotesWithRESTAPITheme(darkTheme = false) {
79 | OutlinedButtonComponent(
80 | text = "Login",
81 | enabled = { true },
82 | leadingIcon = Icons.Rounded.Login,
83 | onButtonClick = {}
84 | )
85 | }
86 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/components/ProgressIndicatorComponent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.components
2 |
3 | import androidx.compose.material3.CircularProgressIndicator
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.graphics.Color
8 |
9 |
10 | @Composable
11 | fun ProgressIndicatorComponent(
12 | isLoading: () -> Boolean,
13 | modifier: Modifier = Modifier,
14 | color: Color = MaterialTheme.colorScheme.primary,
15 | ) {
16 |
17 | if (isLoading()) {
18 | CircularProgressIndicator(
19 | modifier = modifier,
20 | color = color
21 | )
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/components/pull_refresh/PullRefresh.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.components.pull_refresh
2 |
3 | import androidx.compose.ui.Modifier
4 | import androidx.compose.ui.geometry.Offset
5 | import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
6 | import androidx.compose.ui.input.nestedscroll.NestedScrollSource
7 | import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Drag
8 | import androidx.compose.ui.input.nestedscroll.nestedScroll
9 | import androidx.compose.ui.platform.debugInspectorInfo
10 | import androidx.compose.ui.platform.inspectable
11 | import androidx.compose.ui.unit.Velocity
12 |
13 |
14 | fun Modifier.pullRefresh(
15 | state: PullRefreshState,
16 | enabled: Boolean = true,
17 | ) = inspectable(inspectorInfo = debugInspectorInfo {
18 | name = "pullRefresh"
19 | properties["state"] = state
20 | properties["enabled"] = enabled
21 | }) {
22 | Modifier.pullRefresh(state::onPull, state::onRelease, enabled)
23 | }
24 |
25 |
26 | fun Modifier.pullRefresh(
27 | onPull: (pullDelta: Float) -> Float,
28 | onRelease: suspend (flingVelocity: Float) -> Float,
29 | enabled: Boolean = true,
30 | ) = inspectable(inspectorInfo = debugInspectorInfo {
31 | name = "pullRefresh"
32 | properties["onPull"] = onPull
33 | properties["onRelease"] = onRelease
34 | properties["enabled"] = enabled
35 | }) {
36 | Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))
37 | }
38 |
39 | private class PullRefreshNestedScrollConnection(
40 | private val onPull: (pullDelta: Float) -> Float,
41 | private val onRelease: suspend (flingVelocity: Float) -> Float,
42 | private val enabled: Boolean,
43 | ) : NestedScrollConnection {
44 |
45 | override fun onPreScroll(
46 | available: Offset,
47 | source: NestedScrollSource,
48 | ): Offset = when {
49 | !enabled -> Offset.Zero
50 | source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
51 | else -> Offset.Zero
52 | }
53 |
54 | override fun onPostScroll(
55 | consumed: Offset,
56 | available: Offset,
57 | source: NestedScrollSource,
58 | ): Offset = when {
59 | !enabled -> Offset.Zero
60 | source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
61 | else -> Offset.Zero
62 | }
63 |
64 | override suspend fun onPreFling(available: Velocity): Velocity {
65 | return Velocity(0f, onRelease(available.y))
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/components/pull_refresh/PullRefreshIndicatorTransform.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.components.pull_refresh
2 |
3 | import androidx.compose.animation.core.LinearOutSlowInEasing
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.draw.drawWithContent
6 | import androidx.compose.ui.graphics.drawscope.clipRect
7 | import androidx.compose.ui.graphics.graphicsLayer
8 | import androidx.compose.ui.platform.debugInspectorInfo
9 | import androidx.compose.ui.platform.inspectable
10 |
11 |
12 | fun Modifier.pullRefreshIndicatorTransform(
13 | state: PullRefreshState,
14 | scale: Boolean = false,
15 | ) = inspectable(inspectorInfo = debugInspectorInfo {
16 | name = "pullRefreshIndicatorTransform"
17 | properties["state"] = state
18 | properties["scale"] = scale
19 | }) {
20 | Modifier
21 | .drawWithContent {
22 | clipRect(
23 | top = 0f,
24 | left = -Float.MAX_VALUE,
25 | right = Float.MAX_VALUE,
26 | bottom = Float.MAX_VALUE
27 | ) {
28 | this@drawWithContent.drawContent()
29 | }
30 | }
31 | .graphicsLayer {
32 | translationY = state.position - size.height
33 |
34 | if (scale && !state.refreshing) {
35 | val scaleFraction = LinearOutSlowInEasing
36 | .transform(state.position / state.threshold)
37 | .coerceIn(0f, 1f)
38 | scaleX = scaleFraction
39 | scaleY = scaleFraction
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/navigation/Graph.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.navigation
2 |
3 | object Graph {
4 |
5 | const val ROOT = "root_graph"
6 |
7 | const val AUTH_GRAPH = "auth_graph"
8 |
9 | const val NOTE_GRAPH = "note_graph"
10 |
11 | const val PROFILE_GRAPH = "profile_graph"
12 |
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/navigation/NavigationItem.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.navigation
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.rounded.AccountCircle
6 | import androidx.compose.material.icons.rounded.Notes
7 | import androidx.compose.ui.graphics.vector.ImageVector
8 | import com.example.noteswithrestapi.R
9 |
10 | sealed class NavigationItem(
11 | @StringRes val nameId: Int,
12 | @StringRes val descId: Int,
13 | val route: String,
14 | val icon: ImageVector,
15 | ) {
16 |
17 | object NotesFeature: NavigationItem(
18 | nameId = R.string.title_notes,
19 | descId = R.string.desc_notes_screen,
20 | route = Graph.NOTE_GRAPH,
21 | icon = Icons.Rounded.Notes
22 | )
23 |
24 | object ProfileFeature: NavigationItem(
25 | nameId = R.string.title_profile,
26 | descId = R.string.desc_profile_screen,
27 | route = Graph.PROFILE_GRAPH,
28 | icon = Icons.Rounded.AccountCircle
29 | )
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/navigation/RootNavGraph.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.navigation
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import androidx.navigation.NavHostController
7 | import com.example.noteswithrestapi.authentication_feature.presentation.authGraph
8 | import com.example.noteswithrestapi.note_feature.presentation.noteGraph
9 | import com.example.noteswithrestapi.profile_feature.presentation.profileGraph
10 | import com.google.accompanist.navigation.animation.AnimatedNavHost
11 |
12 |
13 | @OptIn(ExperimentalAnimationApi::class)
14 | @Composable
15 | fun RootNavGraph(
16 | modifier: Modifier = Modifier,
17 | startGraphRoute: String = Graph.AUTH_GRAPH,
18 | navController: NavHostController,
19 | onShowNavigationDrawer: () -> Unit,
20 | ) {
21 |
22 | AnimatedNavHost(
23 | modifier = modifier,
24 | navController = navController,
25 | route = Graph.ROOT,
26 | startDestination = startGraphRoute,
27 | ) {
28 | authGraph(navController)
29 | noteGraph(
30 | navController = navController,
31 | onShowNavigationDrawer = onShowNavigationDrawer
32 | )
33 | profileGraph(
34 | navController = navController,
35 | onShowNavigationDrawer = onShowNavigationDrawer
36 | )
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/navigation/Screen.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.navigation
2 |
3 | sealed class Screen(protected val route: String, vararg params: String) {
4 |
5 | val fullRoute: String = if (params.isEmpty()) route else {
6 | val builder = StringBuilder(route)
7 | params.forEach { builder.append("/{${it}}") }
8 | builder.toString()
9 | }
10 |
11 |
12 | //Authentication feature
13 | object Login: Screen(LOGIN_SCREEN_ROUTE)
14 |
15 | object Register: Screen(REGISTER_SCREEN_ROUTE)
16 |
17 | object ResetPassword: Screen(RESET_PASSWORD_SCREEN_ROUTE)
18 |
19 | object ConfirmPasswordReset: Screen(CONFIRM_PASSWORD_RESET_SCREEN_ROUTE, EMAIL_ARG) {
20 | fun getRouteWithArgs(email: String) = route.appendParams(
21 | EMAIL_ARG to email
22 | )
23 | }
24 |
25 | object VerifyEmail: Screen(VERIFY_EMAIL_SCREEN_ROUTE, EMAIL_ARG) {
26 | fun getRouteWithArgs(email: String) = route.appendParams(
27 | EMAIL_ARG to email
28 | )
29 | }
30 |
31 |
32 | //Note feature
33 | object Notes: Screen(NOTES_SCREEN_ROUTE)
34 |
35 | object AddNote: Screen(ADD_NOTE_SCREEN_ROUTE)
36 |
37 | object EditNote: Screen(EDIT_NOTE_SCREEN_ROUTE, NOTE_ID_ARG) {
38 | fun getRouteWithArgs(noteId: Int) = route.appendParams(
39 | NOTE_ID_ARG to noteId
40 | )
41 | }
42 |
43 | object SearchNote: Screen(SEARCH_NOTE_SCREEN_ROUTE)
44 |
45 |
46 | //Profile feature
47 | object Profile: Screen(PROFILE_SCREEN_ROUTE)
48 |
49 |
50 | companion object {
51 |
52 | //Authentication feature
53 | private const val LOGIN_SCREEN_ROUTE = "login"
54 | private const val REGISTER_SCREEN_ROUTE = "register"
55 | private const val RESET_PASSWORD_SCREEN_ROUTE = "reset_password"
56 | private const val CONFIRM_PASSWORD_RESET_SCREEN_ROUTE = "confirm_password_reset"
57 | private const val VERIFY_EMAIL_SCREEN_ROUTE = "confirm_email"
58 |
59 | const val EMAIL_ARG = "email_arg"
60 |
61 | //Note feature
62 | private const val NOTES_SCREEN_ROUTE = "notes"
63 | private const val EDIT_NOTE_SCREEN_ROUTE = "edit_notes"
64 | private const val ADD_NOTE_SCREEN_ROUTE = "add_note"
65 | private const val SEARCH_NOTE_SCREEN_ROUTE = "search_note"
66 | private const val SEARCH_NOTE_RESULT_SCREEN_ROUTE = "search_note_result"
67 |
68 | const val NOTE_ID_ARG = "note_id_arg"
69 |
70 |
71 | //Profile feature
72 | private const val PROFILE_SCREEN_ROUTE = "profile"
73 |
74 | }
75 |
76 |
77 | internal fun String.appendParams(vararg params: Pair): String {
78 | val builder = StringBuilder(this)
79 |
80 | params.forEach {
81 | it.second?.toString()?.let { arg ->
82 | builder.append("/$arg")
83 | }
84 | }
85 |
86 | return builder.toString()
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/theme/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.theme.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 |
6 | val primary_dark = Color(0XFFC8C6CC)
7 | val surface_dark = Color(0XFF211F26)
8 | val onSurface_dark = Color(0XFFC8C6CC)
9 | val background_dark = Color(0XFF141218)
10 | val onBackground_dark = Color(0XFF47454D)
11 | val secondary_dark = Color(0XFF282438)
12 | val onSecondary_dark = Color(0XFFC8C6CC)
13 | val tertiary_dark = Color(0XFFAEA7CB)
14 |
15 | val primary_light = Color(0XFF353040)
16 | val surface_light = Color(0XFFD7D5DE)
17 | val onSurface_light = Color(0XFF353040)
18 | val background_light = Color(0XFFFAF7FF)
19 | val onBackground_light = Color(0XFF8C8C8C)
20 | val secondary_light = Color(0XFFAEA7CB)
21 | val onSecondary_light = Color(0XFF353040)
22 | val tertiary_light = Color(0XFF282438)
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/theme/ui/theme/Shapes.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.theme.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material3.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | large = RoundedCornerShape(30.dp),
9 | medium = RoundedCornerShape(20.dp),
10 | small = RoundedCornerShape(15.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/theme/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.theme.ui.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 |
9 | private val DarkColorScheme = darkColorScheme(
10 | primary = primary_dark,
11 | secondary = secondary_dark,
12 | onSecondary = onSecondary_dark,
13 | tertiary = tertiary_dark,
14 | surface = surface_dark,
15 | onSurface = onSurface_dark,
16 | background = background_dark,
17 | onBackground = onBackground_dark,
18 | )
19 |
20 | private val LightColorScheme = lightColorScheme(
21 | primary = primary_light,
22 | secondary = secondary_light,
23 | onSecondary = onSecondary_light,
24 | tertiary = tertiary_light,
25 | surface = surface_light,
26 | onSurface = onSurface_light,
27 | background = background_light,
28 | onBackground = onBackground_light,
29 | )
30 |
31 | @Composable
32 | fun NotesWithRESTAPITheme(
33 | darkTheme: Boolean = isSystemInDarkTheme(),
34 | content: @Composable () -> Unit
35 | ) {
36 | val colorScheme = when {
37 | darkTheme -> DarkColorScheme
38 | else -> LightColorScheme
39 | }
40 |
41 | MaterialTheme(
42 | colorScheme = colorScheme,
43 | typography = Typography,
44 | content = content,
45 | shapes = Shapes
46 | )
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/presentation/theme/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.presentation.theme.ui.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.noteswithrestapi.R
10 |
11 | // Set of Material typography styles to start with
12 | val ud_digi_family = FontFamily(listOf(Font(R.font.ud_digi_kyokasho_nk_b, weight = FontWeight.Bold)))
13 |
14 | val roboto_family = FontFamily(listOf(
15 | Font(R.font.roboto_regular, weight = FontWeight.Normal),
16 | Font(R.font.roboto_bold, weight = FontWeight.Bold),
17 | Font(R.font.roboto_medium, weight = FontWeight.Medium),
18 | Font(R.font.roboto_light, weight = FontWeight.Light)
19 | ))
20 |
21 | val Typography = Typography(
22 | headlineMedium = TextStyle(
23 | fontFamily = ud_digi_family,
24 | fontWeight = FontWeight.Bold,
25 | lineHeight = 28.sp,
26 | fontSize = 28.sp
27 | ),
28 | titleLarge = TextStyle(
29 | fontFamily = ud_digi_family,
30 | fontWeight = FontWeight.Bold,
31 | letterSpacing = 0.5.sp,
32 | lineHeight = 28.sp,
33 | fontSize = 22.sp
34 | ),
35 | titleMedium = TextStyle(
36 | fontFamily = ud_digi_family,
37 | fontWeight = FontWeight.Bold,
38 | letterSpacing = 0.5.sp,
39 | lineHeight = 28.sp,
40 | fontSize = 20.sp
41 | ),
42 | bodyLarge = TextStyle(
43 | fontFamily = roboto_family,
44 | fontWeight = FontWeight.Normal,
45 | fontSize = 18.sp,
46 | lineHeight = 28.sp,
47 | letterSpacing = 0.5.sp
48 | ),
49 | bodyMedium = TextStyle(
50 | fontFamily = roboto_family,
51 | fontWeight = FontWeight.Normal,
52 | fontSize = 16.sp,
53 | lineHeight = 28.sp,
54 | letterSpacing = 0.4.sp
55 | ),
56 | bodySmall = TextStyle(
57 | fontFamily = roboto_family,
58 | fontWeight = FontWeight.Normal,
59 | fontSize = 14.sp,
60 | lineHeight = 28.sp,
61 | )
62 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/utils/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.utils
2 |
3 | object Constants {
4 |
5 | const val UNKNOWN_ID = -1
6 |
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/core/utils/TimeUtil.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.core.utils
2 |
3 | import java.time.LocalDateTime
4 |
5 |
6 | fun formatStringDate(stringDate: String): String {
7 | val localDate = LocalDateTime.parse(stringDate)
8 | return localDate.toString()
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/data/mapper/NoteMapper.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.data.mapper
2 |
3 | import com.example.noteswithrestapi.note_feature.data.remote.dto.NoteDto
4 | import com.example.noteswithrestapi.note_feature.domain.model.Note
5 |
6 |
7 | fun NoteDto.toNote(): Note = Note(
8 | id = this.id,
9 | title = this.title,
10 | content = this.content,
11 | date_updated = this.date_updated,
12 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/data/remote/NoteApiService.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.data.remote
2 |
3 | import com.example.noteswithrestapi.note_feature.data.remote.dto.GetNotesResultDto
4 | import com.example.noteswithrestapi.note_feature.data.remote.dto.NoteDto
5 | import com.example.noteswithrestapi.note_feature.domain.model.Note
6 | import retrofit2.Response
7 | import retrofit2.http.Body
8 | import retrofit2.http.DELETE
9 | import retrofit2.http.GET
10 | import retrofit2.http.Header
11 | import retrofit2.http.POST
12 | import retrofit2.http.PUT
13 | import retrofit2.http.Path
14 | import retrofit2.http.Query
15 |
16 | interface NoteApiService {
17 |
18 | @GET("api/v1/list-note/")
19 | suspend fun getNotes(
20 | @Header("Authorization") token: String,
21 | @Query("search") query: String = "",
22 | @Query("page") page: Int,
23 | @Query("page_size") pageSize: Int = 20,
24 | ): Response
25 |
26 |
27 | @GET("api/v1/get-note/{pk}")
28 | suspend fun getNote(
29 | @Header("Authorization") token: String,
30 | @Path("pk") id: Int,
31 | ): Response
32 |
33 |
34 | @POST("api/v1/add-note/")
35 | suspend fun addNote(
36 | @Header("Authorization") token: String,
37 | @Body note: Note
38 | ): Response
39 |
40 |
41 | @PUT("api/v1/update-note/{pk}")
42 | suspend fun updateNote(
43 | @Header("Authorization") token: String,
44 | @Path("pk") id: Int,
45 | @Body note: Note
46 | ): Response
47 |
48 |
49 | @DELETE("api/v1/delete-note/{pk}")
50 | suspend fun deleteNote(
51 | @Header("Authorization") token: String,
52 | @Path("pk") id: Int,
53 | ): Response
54 |
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/data/remote/dto/GetNotesResultDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.data.remote.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class GetNotesResultDto(
6 | val count: Int,
7 | val next: String,
8 | val previous: Any,
9 | @SerializedName("results") val noteDtos: List
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/data/remote/dto/NoteDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.data.remote.dto
2 |
3 | data class NoteDto(
4 | val id: Int,
5 | val user_id: Int,
6 | val title: String,
7 | val content: String,
8 | val date_updated: String,
9 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/data/repository/NoteRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.data.repository
2 |
3 | import com.example.noteswithrestapi.R
4 | import com.example.noteswithrestapi.core.data.network.getAppErrorByHttpErrorCode
5 | import com.example.noteswithrestapi.core.data.token.TokenManager
6 | import com.example.noteswithrestapi.core.domain.model.AppError
7 | import com.example.noteswithrestapi.core.domain.model.AppResponse
8 | import com.example.noteswithrestapi.note_feature.data.mapper.toNote
9 | import com.example.noteswithrestapi.note_feature.data.remote.NoteApiService
10 | import com.example.noteswithrestapi.note_feature.domain.model.Note
11 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository
12 | import retrofit2.HttpException
13 | import java.io.IOException
14 | import javax.inject.Inject
15 |
16 | class NoteRepositoryImpl @Inject constructor(
17 | private val noteApiService: NoteApiService,
18 | private val tokenManager: TokenManager,
19 | ) : NoteRepository {
20 |
21 | override suspend fun addNote(note: Note): AppResponse {
22 | try {
23 | val token = tokenManager.authToken
24 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_get_notes))
25 |
26 | val result = noteApiService.addNote("Token $token", note)
27 |
28 | if (result.isSuccessful) {
29 | val noteResult = result.body()?.toNote()
30 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_get_notes))
31 | return AppResponse.success(noteResult)
32 | }
33 |
34 | val error = result.getAppErrorByHttpErrorCode()
35 | return AppResponse.failed(error)
36 |
37 | } catch (e: IOException) {
38 | return AppResponse.failed(AppError.PoorNetworkConnectionError)
39 | } catch (e: HttpException) {
40 | return AppResponse.failed(AppError.GeneralError(R.string.error_get_notes))
41 | }
42 | }
43 |
44 |
45 | override suspend fun deleteNote(noteId: Int): AppResponse {
46 | try {
47 |
48 | val token = tokenManager.authToken
49 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_delete_note))
50 |
51 | val result = noteApiService.deleteNote("Token $token", noteId)
52 |
53 | if (result.isSuccessful) {
54 | return AppResponse.success(Unit)
55 | }
56 |
57 | val error = result.getAppErrorByHttpErrorCode()
58 | return AppResponse.failed(error)
59 |
60 | } catch (e: IOException) {
61 | return AppResponse.failed(AppError.PoorNetworkConnectionError)
62 | } catch (e: HttpException) {
63 | return AppResponse.failed(AppError.GeneralError(R.string.error_delete_note))
64 | }
65 | }
66 |
67 |
68 | override suspend fun updateNote(note: Note): AppResponse {
69 | try {
70 |
71 | val token = tokenManager.authToken
72 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_delete_note))
73 |
74 | val result = noteApiService.updateNote("Token $token", note.id, note)
75 |
76 | if (result.isSuccessful) {
77 | val noteModel = result.body()?.toNote()
78 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_delete_note))
79 | return AppResponse.success(noteModel)
80 | }
81 |
82 | val error = result.getAppErrorByHttpErrorCode()
83 | return AppResponse.failed(error)
84 |
85 | } catch (e: IOException) {
86 | return AppResponse.failed(AppError.PoorNetworkConnectionError)
87 | } catch (e: HttpException) {
88 | return AppResponse.failed(AppError.GeneralError(R.string.error_delete_note))
89 | }
90 | }
91 |
92 |
93 | override suspend fun getNotes(page: Int, query: String): AppResponse
> {
94 | try {
95 |
96 | val token = tokenManager.authToken
97 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_get_notes))
98 |
99 | val result = noteApiService.getNotes(
100 | token = "Token $token",
101 | page = page,
102 | query = query,
103 | )
104 |
105 | if (result.isSuccessful) {
106 | val noteDtos = result.body()?.noteDtos ?: return AppResponse.success(emptyList())
107 | val notes = noteDtos.map { dto -> dto.toNote() }
108 | return AppResponse.success(notes)
109 | }
110 |
111 | val error = result.getAppErrorByHttpErrorCode()
112 | return AppResponse.failed(error)
113 |
114 | } catch (e: IOException) {
115 | return AppResponse.failed(AppError.PoorNetworkConnectionError)
116 | } catch (e: HttpException) {
117 | return AppResponse.failed(AppError.GeneralError(R.string.error_get_notes))
118 | }
119 | }
120 |
121 |
122 | override suspend fun getNote(id: Int): AppResponse {
123 | try {
124 |
125 | val token = tokenManager.authToken
126 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_get_notes))
127 |
128 | val result = noteApiService.getNote(token = "Token $token", id = id)
129 |
130 | if (result.isSuccessful) {
131 | val note = result.body()?.toNote()
132 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_get_note))
133 | return AppResponse.success(note)
134 | }
135 |
136 | val error = result.getAppErrorByHttpErrorCode()
137 | return AppResponse.failed(error)
138 |
139 | } catch (e: IOException) {
140 | return AppResponse.failed(AppError.PoorNetworkConnectionError)
141 | } catch (e: HttpException) {
142 | return AppResponse.failed(AppError.GeneralError(R.string.error_get_note))
143 | }
144 | }
145 |
146 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/di/NoteModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.di
2 |
3 | import com.example.noteswithrestapi.note_feature.data.remote.NoteApiService
4 | import com.example.noteswithrestapi.note_feature.data.repository.NoteRepositoryImpl
5 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository
6 | import dagger.Binds
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import retrofit2.Retrofit
12 | import javax.inject.Singleton
13 |
14 | @Module
15 | @InstallIn(SingletonComponent::class)
16 | interface NoteModule {
17 |
18 | @Binds
19 | @Singleton
20 | fun bindNoteRepository(
21 | noteRepository: NoteRepositoryImpl,
22 | ): NoteRepository
23 |
24 |
25 | companion object {
26 |
27 | @Provides
28 | @Singleton
29 | fun provideNoteApiService(
30 | retrofit: Retrofit,
31 | ): NoteApiService {
32 | return retrofit.create(NoteApiService::class.java)
33 | }
34 |
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/domain/error/NoteError.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.domain.error
2 |
3 | import com.example.noteswithrestapi.core.domain.model.AppError
4 |
5 | open class NoteError: AppError() {
6 |
7 | object EmptyTitleError: NoteError()
8 |
9 | object EmptyContentError: NoteError()
10 |
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/domain/model/Note.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.domain.model
2 |
3 | import com.example.noteswithrestapi.core.utils.Constants
4 |
5 | data class Note(
6 | val id: Int = Constants.UNKNOWN_ID,
7 | val title: String,
8 | val content: String,
9 | val date_updated: String = "",
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/domain/repository/NoteRepository.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.domain.repository
2 |
3 | import com.example.noteswithrestapi.core.domain.model.AppResponse
4 | import com.example.noteswithrestapi.note_feature.domain.model.Note
5 |
6 | interface NoteRepository {
7 |
8 | suspend fun addNote(note: Note): AppResponse
9 |
10 | suspend fun deleteNote(noteId: Int): AppResponse
11 |
12 | suspend fun updateNote(note: Note): AppResponse
13 |
14 | suspend fun getNotes(page: Int, query: String): AppResponse>
15 |
16 | suspend fun getNote(id: Int): AppResponse
17 |
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/domain/usecase/AddNoteUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.core.domain.model.AppResponse
4 | import com.example.noteswithrestapi.note_feature.domain.error.NoteError
5 | import com.example.noteswithrestapi.note_feature.domain.model.Note
6 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository
7 | import javax.inject.Inject
8 |
9 | class AddNoteUseCase @Inject constructor(
10 | private val noteRepository: NoteRepository
11 | ) {
12 |
13 | suspend operator fun invoke(title: String, content: String): AppResponse {
14 | if (title.isEmpty()) {
15 | return AppResponse.failed(NoteError.EmptyTitleError)
16 | }
17 | if (content.isEmpty()) {
18 | return AppResponse.failed(NoteError.EmptyContentError)
19 | }
20 |
21 | val note = Note(
22 | title = title,
23 | content = content
24 | )
25 |
26 | return noteRepository.addNote(note)
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/domain/usecase/DeleteNoteUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.core.domain.model.AppResponse
4 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository
5 | import javax.inject.Inject
6 |
7 | class DeleteNoteUseCase @Inject constructor(
8 | private val noteRepository: NoteRepository
9 | ) {
10 |
11 | suspend operator fun invoke(id: Int): AppResponse {
12 | return noteRepository.deleteNote(id)
13 | }
14 |
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/domain/usecase/EditNoteUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.core.domain.model.AppResponse
4 | import com.example.noteswithrestapi.note_feature.domain.error.NoteError
5 | import com.example.noteswithrestapi.note_feature.domain.model.Note
6 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository
7 | import javax.inject.Inject
8 |
9 | class EditNoteUseCase @Inject constructor(
10 | private val noteRepository: NoteRepository
11 | ) {
12 |
13 | suspend operator fun invoke(title: String, content: String, id: Int): AppResponse {
14 | if (title.isEmpty()) {
15 | return AppResponse.failed(NoteError.EmptyTitleError)
16 | }
17 | if (content.isEmpty()) {
18 | return AppResponse.failed(NoteError.EmptyContentError)
19 | }
20 |
21 | val note = Note(
22 | id = id,
23 | title = title,
24 | content = content
25 | )
26 |
27 | return noteRepository.updateNote(note)
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/domain/usecase/GetNoteUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.core.domain.model.AppResponse
4 | import com.example.noteswithrestapi.note_feature.domain.model.Note
5 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository
6 | import javax.inject.Inject
7 |
8 | class GetNoteUseCase @Inject constructor(
9 | private val noteRepository: NoteRepository
10 | ) {
11 |
12 | suspend operator fun invoke(noteId: Int): AppResponse {
13 | return noteRepository.getNote(noteId)
14 | }
15 |
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/domain/usecase/GetNotesUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.core.domain.model.AppResponse
4 | import com.example.noteswithrestapi.note_feature.domain.model.Note
5 | import com.example.noteswithrestapi.note_feature.domain.repository.NoteRepository
6 | import javax.inject.Inject
7 |
8 | class GetNotesUseCase @Inject constructor(
9 | private val noteRepository: NoteRepository
10 | ) {
11 |
12 | suspend operator fun invoke(page: Int, query: String = ""): AppResponse> {
13 | return noteRepository.getNotes(page, query)
14 | }
15 |
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/add_note/AddNoteEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.add_note
2 |
3 | sealed class AddNoteEvent {
4 |
5 | data class OnTitleInputValueChange(val newValue: String): AddNoteEvent()
6 |
7 | data class OnContentInputValueChange(val newValue: String): AddNoteEvent()
8 |
9 | object OnSaveClick: AddNoteEvent()
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/add_note/AddNoteState.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.add_note
2 |
3 | data class AddNoteState(
4 | val titleInputValue: String = "",
5 |
6 | val contentInputValue: String = "",
7 |
8 | val isLoading: Boolean = false,
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/add_note/AddNoteUiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.add_note
2 |
3 | sealed class AddNoteUiEvent {
4 |
5 | object OnNoteSaved: AddNoteUiEvent()
6 |
7 | data class OnShowSnackbar(val message: String): AddNoteUiEvent()
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/add_note/AddNoteViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.add_note
2 |
3 | import android.app.Application
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import com.example.noteswithrestapi.R
10 | import com.example.noteswithrestapi.core.domain.model.AppError
11 | import com.example.noteswithrestapi.core.domain.model.AppResponse
12 | import com.example.noteswithrestapi.note_feature.domain.error.NoteError
13 | import com.example.noteswithrestapi.note_feature.domain.usecase.AddNoteUseCase
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.channels.Channel
16 | import kotlinx.coroutines.flow.receiveAsFlow
17 | import kotlinx.coroutines.launch
18 | import javax.inject.Inject
19 |
20 | @HiltViewModel
21 | class AddNoteViewModel @Inject constructor(
22 | private val addNoteUseCase: AddNoteUseCase,
23 | private val application: Application,
24 | ) : ViewModel() {
25 |
26 |
27 | private val _uiEvent = Channel()
28 | val uiEvent = _uiEvent.receiveAsFlow()
29 |
30 | var state by mutableStateOf(AddNoteState())
31 | private set
32 |
33 |
34 | fun onEvent(event: AddNoteEvent) {
35 | when (event) {
36 | is AddNoteEvent.OnContentInputValueChange -> {
37 | state = state.copy(contentInputValue = event.newValue)
38 | }
39 |
40 | is AddNoteEvent.OnTitleInputValueChange -> {
41 | state = state.copy(titleInputValue = event.newValue)
42 | }
43 |
44 | is AddNoteEvent.OnSaveClick -> {
45 | saveNote(state.titleInputValue, state.contentInputValue)
46 | }
47 |
48 | }
49 | }
50 |
51 | private fun saveNote(
52 | title: String,
53 | content: String,
54 | ) {
55 | viewModelScope.launch {
56 | state = state.copy(isLoading = true)
57 | when (val result = addNoteUseCase(title, content)) {
58 | is AppResponse.Success -> {
59 | state = state.copy(isLoading = false)
60 | _uiEvent.send(AddNoteUiEvent.OnNoteSaved)
61 | }
62 |
63 | is AppResponse.Failed -> {
64 | state = state.copy(isLoading = false)
65 | handleAppError(result.error)
66 | }
67 | }
68 | }
69 | }
70 |
71 |
72 | private fun handleAppError(error: AppError) {
73 | viewModelScope.launch {
74 | when (error) {
75 | is AppError.GeneralError -> {
76 | _uiEvent.send(AddNoteUiEvent.OnShowSnackbar(application.getString(error.messageResId)))
77 | }
78 |
79 | is AppError.PoorNetworkConnectionError -> {
80 | _uiEvent.send(AddNoteUiEvent.OnShowSnackbar(application.getString(R.string.error_poor_network_connection)))
81 | }
82 |
83 | is AppError.ServerError -> {
84 | _uiEvent.send(AddNoteUiEvent.OnShowSnackbar(application.getString(R.string.error_server)))
85 | }
86 |
87 | is NoteError -> {
88 | when (error) {
89 | is NoteError.EmptyTitleError -> {
90 | _uiEvent.send(AddNoteUiEvent.OnShowSnackbar(application.getString(R.string.error_empty_title)))
91 | }
92 |
93 | is NoteError.EmptyContentError -> {
94 | _uiEvent.send(AddNoteUiEvent.OnShowSnackbar(application.getString(R.string.error_empty_content)))
95 | }
96 | }
97 | }
98 |
99 | else -> {
100 | _uiEvent.send(AddNoteUiEvent.OnShowSnackbar(application.getString(R.string.error_unknown)))
101 | }
102 | }
103 | }
104 |
105 | }
106 |
107 |
108 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/components/NoteComponent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Spacer
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.material3.Card
9 | import androidx.compose.material3.CardDefaults
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.graphics.Shape
18 | import androidx.compose.ui.text.font.FontWeight
19 | import androidx.compose.ui.text.style.TextOverflow
20 | import androidx.compose.ui.tooling.preview.Preview
21 | import androidx.compose.ui.unit.dp
22 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme
23 | import com.example.noteswithrestapi.note_feature.domain.model.Note
24 |
25 |
26 | @OptIn(ExperimentalMaterial3Api::class)
27 | @Composable
28 | fun NoteComponent(
29 | note: Note,
30 | onClick: () -> Unit,
31 | modifier: Modifier = Modifier,
32 | shape: Shape = MaterialTheme.shapes.large,
33 | backgroundColor: Color = MaterialTheme.colorScheme.surface,
34 | contentColor: Color = MaterialTheme.colorScheme.onSurface,
35 | ) {
36 |
37 | Card(
38 | onClick = onClick,
39 | modifier = modifier,
40 | shape = shape,
41 | colors = CardDefaults.cardColors(
42 | contentColor = contentColor,
43 | containerColor = backgroundColor,
44 | )
45 | ) {
46 | Column(
47 | modifier = Modifier
48 | .padding(start = 16.dp, end = 16.dp, top = 18.dp, bottom = 12.dp)
49 | .fillMaxWidth(),
50 | horizontalAlignment = Alignment.CenterHorizontally
51 | ) {
52 | Text(
53 | text = note.title,
54 | style = MaterialTheme.typography.titleMedium,
55 | maxLines = 1,
56 | overflow = TextOverflow.Ellipsis,
57 | modifier = Modifier.align(Alignment.CenterHorizontally)
58 | )
59 | Spacer(modifier = Modifier.height(14.dp))
60 | Text(
61 | text = note.content,
62 | style = MaterialTheme.typography.bodyLarge,
63 | modifier = Modifier.align(Alignment.Start),
64 | maxLines = 4,
65 | overflow = TextOverflow.Ellipsis
66 | )
67 | Spacer(modifier = Modifier.height(8.dp))
68 | Text(
69 | text = note.date_updated,
70 | style = MaterialTheme.typography.bodySmall,
71 | fontWeight = FontWeight.Light,
72 | modifier = Modifier.align(Alignment.End)
73 | )
74 | Spacer(modifier = Modifier.height(5.dp))
75 | }
76 | }
77 |
78 | }
79 |
80 | @Preview
81 | @Composable
82 | fun NoteComponentPreview() {
83 | NotesWithRESTAPITheme {
84 | NoteComponent(
85 | note = Note(
86 | 0,
87 | "Note Title",
88 | "Hi there! Just wanted to leave a quick note to say that I hope you're having a great day. Remember to take breaks when you need them and stay focused. That's all i wanted to tell you",
89 | date_updated = "April 30, 2023"
90 | ),
91 | onClick = {},
92 | modifier = Modifier
93 | .padding(start = 16.dp, end = 16.dp)
94 | .fillMaxWidth()
95 | )
96 | }
97 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/edit_note/EditNoteEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.edit_note
2 |
3 | sealed class EditNoteEvent {
4 |
5 | data class OnTitleInputValueChange(val newValue: String): EditNoteEvent()
6 |
7 | data class OnContentInputValueChange(val newValue: String): EditNoteEvent()
8 |
9 | object OnSaveClick: EditNoteEvent()
10 |
11 | object OnDeleteClick: EditNoteEvent()
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/edit_note/EditNoteState.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.edit_note
2 |
3 | data class EditNoteState(
4 | val titleInputValue: String = "",
5 |
6 | val contentInputValue: String = "",
7 |
8 | val isLoading: Boolean = false,
9 | val getNoteErrorMessage: String? = null,
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/edit_note/EditNoteUiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.edit_note
2 |
3 | sealed class EditNoteUiEvent {
4 |
5 | object OnNoteSaved: EditNoteUiEvent()
6 |
7 | object OnNoteDeleted: EditNoteUiEvent()
8 |
9 | data class OnShowSnackbar(val message: String): EditNoteUiEvent()
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/notes/NotesEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.notes
2 |
3 | sealed class NotesEvent {
4 |
5 | object OnLoadNextItems: NotesEvent()
6 |
7 | object OnRefresh: NotesEvent()
8 |
9 | object OnRetry: NotesEvent()
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/notes/NotesState.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.notes
2 |
3 | import com.example.noteswithrestapi.note_feature.domain.model.Note
4 |
5 | data class NotesState(
6 | val notes: List = emptyList(),
7 | val page: Int = 1,
8 | val isLoading: Boolean = false,
9 | val endReached: Boolean = false,
10 | val errorMessage: String? = null,
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/notes/NotesViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.notes
2 |
3 | import android.app.Application
4 | import android.util.Log
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import com.example.noteswithrestapi.R
11 | import com.example.noteswithrestapi.core.domain.model.AppError
12 | import com.example.noteswithrestapi.core.domain.paginator.DefaultPaginator
13 | import com.example.noteswithrestapi.note_feature.domain.usecase.GetNotesUseCase
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.launch
16 | import javax.inject.Inject
17 |
18 | @HiltViewModel
19 | class NotesViewModel @Inject constructor(
20 | private val getNotesUseCase: GetNotesUseCase,
21 | private val application: Application,
22 | ) : ViewModel() {
23 |
24 |
25 | var state by mutableStateOf(NotesState())
26 | private set
27 |
28 |
29 | private val paginator = DefaultPaginator(
30 | initialKey = state.page,
31 | onLoadUpdated = { state = state.copy(isLoading = it) },
32 | onRequest = { nextPage -> getNotesUseCase.invoke(nextPage) },
33 | getNextKey = { state.page + 1 },
34 | onError = { appError -> handleAppError(appError) },
35 | onSuccess = { items, newKey ->
36 | state = state.copy(
37 | notes = state.notes + items,
38 | page = newKey,
39 | endReached = items.isEmpty()
40 | )
41 | }
42 | )
43 |
44 | init {
45 | loadNextItems()
46 | }
47 |
48 |
49 | fun onEvent(event: NotesEvent) {
50 | when (event) {
51 | is NotesEvent.OnLoadNextItems -> {
52 | loadNextItems()
53 | }
54 |
55 | is NotesEvent.OnRefresh -> {
56 | refresh()
57 | }
58 |
59 | is NotesEvent.OnRetry -> {
60 | onRetryLoadNextItems()
61 | }
62 | }
63 | }
64 |
65 | private fun onRetryLoadNextItems() {
66 | viewModelScope.launch {
67 | state = state.copy(errorMessage = null)
68 | paginator.loadNextPage()
69 | }
70 | }
71 |
72 | private fun loadNextItems() {
73 | viewModelScope.launch {
74 | paginator.loadNextPage()
75 | }
76 | }
77 |
78 | private fun refresh() {
79 | viewModelScope.launch {
80 | state = state.copy(notes = emptyList(), page = 1, endReached = false, errorMessage = null)
81 | paginator.reset()
82 | paginator.loadNextPage()
83 | }
84 | }
85 |
86 |
87 | private fun handleAppError(error: AppError) {
88 |
89 | when (error) {
90 | is AppError.GeneralError -> {
91 | state = state.copy(errorMessage = application.getString(error.messageResId))
92 | }
93 |
94 | is AppError.PoorNetworkConnectionError -> {
95 | state = state.copy(errorMessage = application.getString(R.string.error_poor_network_connection))
96 | }
97 |
98 | is AppError.ServerError -> {
99 | state = state.copy(errorMessage = application.getString(R.string.error_unknown))
100 | }
101 |
102 | is AppError.NotFoundError -> {
103 | state = state.copy(endReached = true)
104 | }
105 |
106 | else -> {
107 | state = state.copy(
108 | errorMessage = application.getString(R.string.error_unknown),
109 | )
110 | Log.e("UNEXPECTED_ERROR", error.toString())
111 | }
112 | }
113 | }
114 |
115 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/search_note/SearchNoteEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.search_note
2 |
3 |
4 | sealed class SearchNoteEvent() {
5 |
6 | data class OnSearchQueryValueChange(val newValue: String) : SearchNoteEvent()
7 |
8 | object OnClearQuery : SearchNoteEvent()
9 |
10 | object OnLoadNextItems: SearchNoteEvent()
11 |
12 | object OnRetryLoadNextItems: SearchNoteEvent()
13 |
14 | object OnRefresh: SearchNoteEvent()
15 |
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/search_note/SearchNoteState.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.search_note
2 |
3 | import com.example.noteswithrestapi.note_feature.domain.model.Note
4 |
5 | data class SearchNoteState(
6 | val searchQueryInputValue: String = "",
7 |
8 | val searchItems: List = emptyList(),
9 |
10 | val isLoadingNextItems: Boolean = false,
11 | val endReached: Boolean = false,
12 | val page: Int = 1,
13 |
14 | val errorMessage: String? = null,
15 | ) {
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/note_feature/presentation/search_note/SearchNoteViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.note_feature.presentation.search_note
2 |
3 | import android.app.Application
4 | import android.util.Log
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import com.example.noteswithrestapi.R
11 | import com.example.noteswithrestapi.core.domain.model.AppError
12 | import com.example.noteswithrestapi.core.domain.paginator.DefaultPaginator
13 | import com.example.noteswithrestapi.note_feature.domain.usecase.GetNotesUseCase
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.Job
16 | import kotlinx.coroutines.delay
17 | import kotlinx.coroutines.launch
18 | import javax.inject.Inject
19 |
20 | @HiltViewModel
21 | class SearchNoteViewModel @Inject constructor(
22 | private val getNotesUseCase: GetNotesUseCase,
23 | private val application: Application,
24 | ) : ViewModel() {
25 |
26 |
27 | var state by mutableStateOf(SearchNoteState())
28 | private set
29 |
30 |
31 | private val paginator = DefaultPaginator(
32 | initialKey = state.page,
33 | onLoadUpdated = { state = state.copy(isLoadingNextItems = it) },
34 | onRequest = { nextPage -> getNotesUseCase.invoke(nextPage, query) },
35 | getNextKey = { state.page + 1 },
36 | onError = { appError -> handleAppError(appError) },
37 | onSuccess = { items, newKey ->
38 | state = state.copy(
39 | searchItems = state.searchItems + items,
40 | page = newKey,
41 | endReached = items.isEmpty()
42 | )
43 | }
44 | )
45 |
46 | private var query: String = ""
47 |
48 | private var searchQueryJob: Job? = null
49 |
50 |
51 | fun onEvent(event: SearchNoteEvent) {
52 | when (event) {
53 | is SearchNoteEvent.OnClearQuery -> {
54 | state = state.copy(searchQueryInputValue = "")
55 | query = ""
56 | refresh()
57 | }
58 |
59 | is SearchNoteEvent.OnLoadNextItems -> {
60 | loadNextItems()
61 | }
62 |
63 | is SearchNoteEvent.OnRetryLoadNextItems -> {
64 | retryLoadNextItems()
65 | }
66 |
67 | is SearchNoteEvent.OnSearchQueryValueChange -> {
68 | state = state.copy(searchQueryInputValue = event.newValue)
69 | searchQueryJob?.cancel()
70 | searchQueryJob = viewModelScope.launch {
71 | delay(1000L)
72 | query = state.searchQueryInputValue
73 | refresh()
74 | }
75 | }
76 |
77 | is SearchNoteEvent.OnRefresh -> {
78 | refresh()
79 | }
80 | }
81 | }
82 |
83 | private fun retryLoadNextItems() {
84 | viewModelScope.launch {
85 | state = state.copy(errorMessage = null)
86 | paginator.loadNextPage()
87 | }
88 | }
89 |
90 | private fun loadNextItems() {
91 | viewModelScope.launch {
92 | paginator.loadNextPage()
93 | }
94 | }
95 |
96 |
97 | private fun refresh() {
98 | viewModelScope.launch {
99 | state = state.copy(searchItems = emptyList(), endReached = false, page = 1, errorMessage = null)
100 | paginator.reset()
101 | paginator.loadNextPage()
102 | }
103 | }
104 |
105 |
106 | private fun handleAppError(error: AppError) {
107 |
108 | when (error) {
109 | is AppError.GeneralError -> {
110 | state = state.copy(errorMessage = application.getString(error.messageResId))
111 | }
112 |
113 | is AppError.PoorNetworkConnectionError -> {
114 | state =
115 | state.copy(errorMessage = application.getString(R.string.error_poor_network_connection))
116 | }
117 |
118 | is AppError.ServerError -> {
119 | state = state.copy(errorMessage = application.getString(R.string.error_unknown))
120 | }
121 |
122 | is AppError.NotFoundError -> {
123 | state = state.copy(endReached = true)
124 | }
125 |
126 | else -> {
127 | state = state.copy(
128 | errorMessage = application.getString(R.string.error_unknown),
129 | )
130 | Log.e("UNEXPECTED_ERROR", error.toString())
131 | }
132 | }
133 | }
134 |
135 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/data/mapper/UserMapper.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.data.mapper
2 |
3 | import com.example.noteswithrestapi.profile_feature.data.remote.dto.GetUserResultDto
4 | import com.example.noteswithrestapi.profile_feature.domain.model.user.User
5 |
6 |
7 | fun GetUserResultDto.toUser(): User = User(this.userData.email, this.userData.date_created)
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/data/remote/ProfileApiService.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.data.remote
2 |
3 | import com.example.noteswithrestapi.profile_feature.data.remote.dto.GetUserResultDto
4 | import com.example.noteswithrestapi.profile_feature.data.remote.dto.LogoutResultDto
5 | import retrofit2.Response
6 | import retrofit2.http.GET
7 | import retrofit2.http.Header
8 | import retrofit2.http.POST
9 |
10 | interface ProfileApiService {
11 |
12 | @GET("accounts/login/")
13 | suspend fun getUser(
14 | @Header("Authorization") token: String,
15 | ): Response
16 |
17 |
18 | @POST("accounts/logout/")
19 | suspend fun logout(
20 | @Header("Authorization") token: String,
21 | ): Response
22 |
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/data/remote/dto/GetUserResultDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.data.remote.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class GetUserResultDto(
6 | @SerializedName("data") val userData: UserData,
7 | val message: String?
8 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/data/remote/dto/LogoutResultDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.data.remote.dto
2 |
3 | data class LogoutResultDto(
4 | val message: String?
5 | )
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/data/remote/dto/UserData.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.data.remote.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class UserData(
6 | @SerializedName("email") val email: String,
7 | @SerializedName("date_created") val date_created: String,
8 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/data/repository/ProfileRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.data.repository
2 |
3 | import com.example.noteswithrestapi.R
4 | import com.example.noteswithrestapi.core.data.network.getAppErrorByHttpErrorCode
5 | import com.example.noteswithrestapi.core.data.token.TokenManager
6 | import com.example.noteswithrestapi.core.domain.model.AppError
7 | import com.example.noteswithrestapi.core.domain.model.AppResponse
8 | import com.example.noteswithrestapi.profile_feature.data.mapper.toUser
9 | import com.example.noteswithrestapi.profile_feature.data.remote.ProfileApiService
10 | import com.example.noteswithrestapi.profile_feature.domain.model.user.User
11 | import com.example.noteswithrestapi.profile_feature.domain.repository.ProfileRepository
12 | import retrofit2.HttpException
13 | import java.io.IOException
14 | import javax.inject.Inject
15 |
16 | class ProfileRepositoryImpl @Inject constructor(
17 | private val tokenManager: TokenManager,
18 | private val profileApiService: ProfileApiService,
19 | ) : ProfileRepository {
20 |
21 |
22 | //Independently on result remove token and navigate to authentication screen
23 | override suspend fun logout(): AppResponse {
24 | try {
25 | val token = tokenManager.authToken ?: return AppResponse.Success(Unit)
26 | tokenManager.removeToken()
27 | val result = profileApiService.logout("Token $token")
28 |
29 | if (result.isSuccessful) {
30 | return AppResponse.success(Unit)
31 | }
32 |
33 | val error = result.getAppErrorByHttpErrorCode()
34 | return AppResponse.failed(error)
35 |
36 | } catch (e: IOException) {
37 | return AppResponse.failed(AppError.PoorNetworkConnectionError)
38 | } catch (e: HttpException) {
39 | return AppResponse.failed(AppError.GeneralError(R.string.error_logout_failed))
40 | }
41 | }
42 |
43 | override suspend fun getCurrentUser(): AppResponse {
44 | try {
45 | val token = tokenManager.authToken
46 | ?: return AppResponse.failed(AppError.GeneralError(R.string.error_logout_failed))
47 |
48 | val result = profileApiService.getUser("Token $token")
49 |
50 | if (result.isSuccessful) {
51 | val user = result.body()?.toUser() ?: return AppResponse.failed(AppError.GeneralError(R.string.error_logout_failed))
52 | return AppResponse.success(user)
53 | }
54 |
55 | val error = result.getAppErrorByHttpErrorCode()
56 | return AppResponse.failed(error)
57 |
58 | } catch (e: IOException) {
59 | return AppResponse.failed(AppError.PoorNetworkConnectionError)
60 | } catch (e: HttpException) {
61 | return AppResponse.failed(AppError.GeneralError(R.string.error_logout_failed))
62 | }
63 | }
64 |
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/di/ProfileModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.di
2 |
3 | import com.example.noteswithrestapi.profile_feature.data.remote.ProfileApiService
4 | import com.example.noteswithrestapi.profile_feature.data.repository.ProfileRepositoryImpl
5 | import com.example.noteswithrestapi.profile_feature.domain.repository.ProfileRepository
6 | import dagger.Binds
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import retrofit2.Retrofit
12 | import javax.inject.Singleton
13 |
14 | @Module
15 | @InstallIn(SingletonComponent::class)
16 | interface ProfileModule {
17 |
18 | @Binds
19 | @Singleton
20 | fun bindProfileRepository(
21 | profileRepositoryImpl: ProfileRepositoryImpl
22 | ): ProfileRepository
23 |
24 |
25 | companion object {
26 |
27 | @Provides
28 | @Singleton
29 | fun provideProfileApiService(
30 | retrofit: Retrofit,
31 | ): ProfileApiService {
32 | return retrofit.create(ProfileApiService::class.java)
33 | }
34 |
35 | }
36 |
37 |
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/domain/model/user/User.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.domain.model.user
2 |
3 | data class User(
4 | val email: String,
5 | val date_created: String,
6 | )
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/domain/repository/ProfileRepository.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.domain.repository
2 |
3 | import com.example.noteswithrestapi.core.domain.model.AppResponse
4 | import com.example.noteswithrestapi.profile_feature.domain.model.user.User
5 |
6 | interface ProfileRepository {
7 |
8 | suspend fun logout(): AppResponse
9 |
10 | suspend fun getCurrentUser(): AppResponse
11 |
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/domain/usecase/GetUserUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.core.domain.model.AppResponse
4 | import com.example.noteswithrestapi.profile_feature.domain.model.user.User
5 | import com.example.noteswithrestapi.profile_feature.domain.repository.ProfileRepository
6 | import javax.inject.Inject
7 |
8 | class GetUserUseCase @Inject constructor(
9 | private val profileRepository: ProfileRepository
10 | ) {
11 |
12 | suspend operator fun invoke(): AppResponse = profileRepository.getCurrentUser()
13 |
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/domain/usecase/LogoutUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.domain.usecase
2 |
3 | import com.example.noteswithrestapi.core.domain.model.AppResponse
4 | import com.example.noteswithrestapi.profile_feature.domain.repository.ProfileRepository
5 | import javax.inject.Inject
6 |
7 | class LogoutUseCase @Inject constructor(
8 | private val profileRepository: ProfileRepository
9 | ) {
10 |
11 | suspend operator fun invoke(): AppResponse {
12 | return profileRepository.logout()
13 | }
14 |
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/presentation/ProfileGraph.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.presentation
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.hilt.navigation.compose.hiltViewModel
5 | import androidx.navigation.NavController
6 | import androidx.navigation.NavGraph.Companion.findStartDestination
7 | import androidx.navigation.NavGraphBuilder
8 | import com.example.noteswithrestapi.core.presentation.navigation.Graph
9 | import com.example.noteswithrestapi.core.presentation.navigation.Screen
10 | import com.example.noteswithrestapi.profile_feature.presentation.profile.ProfileScreen
11 | import com.example.noteswithrestapi.profile_feature.presentation.profile.ProfileViewModel
12 | import com.google.accompanist.navigation.animation.composable
13 | import com.google.accompanist.navigation.animation.navigation
14 |
15 | @OptIn(ExperimentalAnimationApi::class)
16 | fun NavGraphBuilder.profileGraph(
17 | navController: NavController,
18 | onShowNavigationDrawer: () -> Unit,
19 | ) {
20 | navigation(
21 | route = Graph.PROFILE_GRAPH,
22 | startDestination = Screen.Profile.fullRoute
23 | ) {
24 | composable(Screen.Profile.fullRoute) {
25 | val viewModel: ProfileViewModel = hiltViewModel()
26 | ProfileScreen(
27 | onNavigateToAuthentication = {
28 | navController.navigate(
29 | Graph.AUTH_GRAPH,
30 | ) {
31 | popUpTo(navController.graph.findStartDestination().id) {
32 | inclusive = true
33 | }
34 | }
35 | navController.graph.setStartDestination(Graph.AUTH_GRAPH)
36 | },
37 | onShowNavigationDrawer = onShowNavigationDrawer,
38 | state = viewModel.state,
39 | uiEvent = viewModel.uiEvent,
40 | onEvent = viewModel::onEvent
41 | )
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/presentation/profile/ProfileEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.presentation.profile
2 |
3 | sealed class ProfileEvent {
4 |
5 | object OnLogout: ProfileEvent()
6 |
7 | object OnRetryLoadProfile: ProfileEvent()
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/presentation/profile/ProfileScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.presentation.profile
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.rounded.Logout
12 | import androidx.compose.material.icons.rounded.Menu
13 | import androidx.compose.material3.ExperimentalMaterial3Api
14 | import androidx.compose.material3.Icon
15 | import androidx.compose.material3.IconButton
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.Scaffold
18 | import androidx.compose.material3.Text
19 | import androidx.compose.material3.TopAppBar
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.res.stringResource
25 | import androidx.compose.ui.tooling.preview.Preview
26 | import androidx.compose.ui.unit.dp
27 | import com.example.noteswithrestapi.R
28 | import com.example.noteswithrestapi.core.presentation.components.ButtonComponent
29 | import com.example.noteswithrestapi.core.presentation.components.LoadingFailedComponent
30 | import com.example.noteswithrestapi.core.presentation.components.ProgressIndicatorComponent
31 | import com.example.noteswithrestapi.core.presentation.theme.ui.theme.NotesWithRESTAPITheme
32 | import com.example.noteswithrestapi.profile_feature.domain.model.user.User
33 | import kotlinx.coroutines.flow.Flow
34 | import kotlinx.coroutines.flow.flow
35 |
36 |
37 | @OptIn(ExperimentalMaterial3Api::class)
38 | @Composable
39 | fun ProfileScreen(
40 | onNavigateToAuthentication: () -> Unit,
41 | onShowNavigationDrawer: () -> Unit,
42 | state: ProfileState,
43 | uiEvent: Flow,
44 | onEvent: (ProfileEvent) -> Unit,
45 | ) {
46 |
47 | LaunchedEffect(key1 = Unit) {
48 | uiEvent.collect { event ->
49 | when (event) {
50 | ProfileUiEvent.OnLogoutComplete -> onNavigateToAuthentication()
51 | }
52 | }
53 | }
54 |
55 |
56 | Scaffold(
57 | topBar = {
58 | TopAppBar(
59 | title = {
60 | Text(
61 | text = stringResource(id = R.string.title_profile),
62 | style = MaterialTheme.typography.titleLarge,
63 | color = MaterialTheme.colorScheme.onSurface
64 | )
65 | },
66 | navigationIcon = {
67 | IconButton(
68 | onClick = onShowNavigationDrawer
69 | ) {
70 | Icon(
71 | imageVector = Icons.Rounded.Menu,
72 | contentDescription = stringResource(id = R.string.desc_menu_icon),
73 | modifier = Modifier.size(30.dp)
74 | )
75 | }
76 | }
77 | )
78 | }
79 | ) {
80 | Box(
81 | modifier = Modifier
82 | .padding(it)
83 | .fillMaxSize(),
84 | contentAlignment = Alignment.Center
85 | ) {
86 | if (state.errorMessage == null) {
87 | if (state.user != null) {
88 | Column(
89 | modifier = Modifier.fillMaxSize(),
90 | horizontalAlignment = Alignment.CenterHorizontally,
91 | ) {
92 | Spacer(modifier = Modifier.height(50.dp))
93 | Text(
94 | text = state.user.email,
95 | style = MaterialTheme.typography.titleMedium,
96 | color = MaterialTheme.colorScheme.primary
97 | )
98 | Spacer(modifier = Modifier.height(10.dp))
99 | Text(
100 | text = stringResource(R.string.text_joined_at_with_param, state.user.date_created),
101 | style = MaterialTheme.typography.bodyLarge,
102 | color = MaterialTheme.colorScheme.onBackground
103 | )
104 | Spacer(modifier = Modifier.height(30.dp))
105 | ButtonComponent(
106 | text = stringResource(R.string.title_logout),
107 | enabled = { !state.isLoading },
108 | leadingIcon = Icons.Rounded.Logout,
109 | onButtonClick = {
110 | onEvent(ProfileEvent.OnLogout)
111 | }
112 | )
113 | }
114 | }
115 | ProgressIndicatorComponent(isLoading = { state.isLoading })
116 | } else {
117 | LoadingFailedComponent(
118 | errorMessage = state.errorMessage,
119 | onRetryClick = { onEvent(ProfileEvent.OnRetryLoadProfile) }
120 | )
121 | }
122 | }
123 | }
124 |
125 | }
126 |
127 |
128 | @Preview
129 | @Composable
130 | fun ProfileScreenPreview() {
131 | NotesWithRESTAPITheme {
132 | ProfileScreen(
133 | onNavigateToAuthentication = {},
134 | onShowNavigationDrawer = {},
135 | state = ProfileState(User("itamiomw@gmail.com", "May 18, 2023")),
136 | uiEvent = flow {},
137 | onEvent = {}
138 | )
139 | }
140 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/presentation/profile/ProfileState.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.presentation.profile
2 |
3 | import com.example.noteswithrestapi.profile_feature.domain.model.user.User
4 |
5 | data class ProfileState(
6 | val user: User? = null,
7 |
8 | val isLoading: Boolean = false,
9 | val errorMessage: String? = null
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/presentation/profile/ProfileUiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.presentation.profile
2 |
3 | sealed class ProfileUiEvent {
4 |
5 | object OnLogoutComplete: ProfileUiEvent()
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/noteswithrestapi/profile_feature/presentation/profile/ProfileViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi.profile_feature.presentation.profile
2 |
3 | import android.app.Application
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import com.example.noteswithrestapi.R
10 | import com.example.noteswithrestapi.core.domain.model.AppError
11 | import com.example.noteswithrestapi.core.domain.model.AppResponse
12 | import com.example.noteswithrestapi.profile_feature.domain.usecase.GetUserUseCase
13 | import com.example.noteswithrestapi.profile_feature.domain.usecase.LogoutUseCase
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.channels.Channel
16 | import kotlinx.coroutines.flow.receiveAsFlow
17 | import kotlinx.coroutines.launch
18 | import javax.inject.Inject
19 |
20 | @HiltViewModel
21 | class ProfileViewModel @Inject constructor(
22 | private val logoutUseCase: LogoutUseCase,
23 | private val getUserUseCase: GetUserUseCase,
24 | private val application: Application,
25 | ) : ViewModel() {
26 |
27 |
28 | private val _uiEvent = Channel()
29 | val uiEvent = _uiEvent.receiveAsFlow()
30 |
31 | var state by mutableStateOf(ProfileState())
32 | private set
33 |
34 |
35 | init {
36 | getUser()
37 | }
38 |
39 |
40 | fun onEvent(event: ProfileEvent) {
41 | when (event) {
42 | is ProfileEvent.OnLogout -> {
43 | logout()
44 | }
45 |
46 | is ProfileEvent.OnRetryLoadProfile -> {
47 | getUser()
48 | }
49 | }
50 | }
51 |
52 |
53 | private fun logout() {
54 | viewModelScope.launch {
55 | state = state.copy(isLoading = true)
56 | logoutUseCase.invoke()
57 | state = state.copy(isLoading = false)
58 | _uiEvent.send(ProfileUiEvent.OnLogoutComplete)
59 | }
60 | }
61 |
62 | private fun getUser() {
63 | viewModelScope.launch {
64 | state = state.copy(isLoading = true, errorMessage = null)
65 | when (val result = getUserUseCase()) {
66 | is AppResponse.Success -> {
67 | state = state.copy(isLoading = false, user = result.data)
68 | }
69 |
70 | is AppResponse.Failed -> {
71 | state = state.copy(isLoading = false)
72 | handleAppError(result.error)
73 | }
74 | }
75 | }
76 | }
77 |
78 |
79 | private fun handleAppError(error: AppError) {
80 | when (error) {
81 | is AppError.GeneralError -> {
82 | state = state.copy(errorMessage = application.getString(error.messageResId))
83 | }
84 |
85 | is AppError.ServerError -> {
86 | state = state.copy(errorMessage = application.getString(R.string.error_server))
87 | }
88 |
89 | is AppError.PoorNetworkConnectionError -> {
90 | state = state.copy(errorMessage = application.getString(R.string.error_poor_network_connection))
91 | }
92 |
93 | else -> {
94 | state = state.copy(errorMessage = application.getString(R.string.error_unknown))
95 | }
96 | }
97 | }
98 |
99 |
100 | }
--------------------------------------------------------------------------------
/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/amico_confirm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/drawable/amico_confirm.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/amico_notes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/drawable/amico_notes.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/amico_reset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/drawable/amico_reset.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/amico_signin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/drawable/amico_signin.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/amico_signup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/drawable/amico_signup.png
--------------------------------------------------------------------------------
/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/font/roboto_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/font/roboto_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/font/roboto_light.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/font/roboto_medium.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/font/roboto_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/ud_digi_kyokasho_nk_b.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/font/ud_digi_kyokasho_nk_b.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/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/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 | Notes With REST API
3 | Leading Icon
4 | Failed to login
5 | Failed to verify email
6 | Failed to resend verification code
7 | Failed to send password reset code
8 | Failed to reset password
9 | Failed to logout
10 | Failed to get user
11 | Failed to register
12 | Unknown error occurred
13 | Sign in illustration
14 | Sign up illustration
15 | Password reset illustration
16 | Login
17 | Confirm Email
18 | Register
19 | Reset Password
20 | Reset Password
21 | Please fill the inputs below
22 | Please enter 6 digit code from your email
23 | Please enter your email
24 | EMAIL
25 | PASSWORD
26 | NEW PASSWORD
27 | CONFIRM PASSWORD
28 | Forgot Password?
29 | Already have an account?
30 | Don\'t have an account?
31 | Sign In
32 | Sign Up
33 | Confirm
34 | Resend code
35 | Poor network connection
36 | Invalid email or password
37 | Empty password
38 | Empty email
39 | Invalid email
40 | Short password
41 | Password do not match
42 | User with such an email already exists
43 | User with such an email doesn\'t exist
44 | Invalid verification code
45 | Go back
46 | Code re-sent successfully
47 | Server error, try again later
48 | Failed to get notes
49 | Failed to delete note
50 | Failed to update note
51 | Failed to get note
52 | Failed to add note
53 | Notes
54 | Note
55 | Profile
56 | Menu icon
57 | Add note
58 | Search icon
59 | Notes screen
60 | Profile screen
61 | Try again
62 | Retry icon
63 | Note title
64 | Type something…
65 | Save
66 | Cancel
67 | Empty title
68 | Empty content
69 | Not found
70 | Delete icon
71 | Search notes
72 | Clear query icon
73 | Logout
74 | Joined at %s
75 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/noteswithrestapi/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.noteswithrestapi
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 | }
--------------------------------------------------------------------------------
/art/dark/add_note_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/dark/add_note_dark.png
--------------------------------------------------------------------------------
/art/dark/confirm_email.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/dark/confirm_email.png
--------------------------------------------------------------------------------
/art/dark/edit_note_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/dark/edit_note_dark.png
--------------------------------------------------------------------------------
/art/dark/login_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/dark/login_dark.png
--------------------------------------------------------------------------------
/art/dark/nav_drawer_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/dark/nav_drawer_dark.png
--------------------------------------------------------------------------------
/art/dark/notes_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/dark/notes_dark.png
--------------------------------------------------------------------------------
/art/dark/profile_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/dark/profile_dark.png
--------------------------------------------------------------------------------
/art/dark/register_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/dark/register_dark.png
--------------------------------------------------------------------------------
/art/dark/reset_password_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/dark/reset_password_dark.png
--------------------------------------------------------------------------------
/art/dark/reset_pswrd_confirm_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/dark/reset_pswrd_confirm_dark.png
--------------------------------------------------------------------------------
/art/dark/search_notes_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/dark/search_notes_dark.png
--------------------------------------------------------------------------------
/art/light/add_note_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/light/add_note_light.png
--------------------------------------------------------------------------------
/art/light/confirm_email_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/light/confirm_email_light.png
--------------------------------------------------------------------------------
/art/light/edit_note_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/light/edit_note_light.png
--------------------------------------------------------------------------------
/art/light/login_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/light/login_light.png
--------------------------------------------------------------------------------
/art/light/nav_drawer_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/light/nav_drawer_light.png
--------------------------------------------------------------------------------
/art/light/notes_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/light/notes_light.png
--------------------------------------------------------------------------------
/art/light/proifle_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/light/proifle_light.png
--------------------------------------------------------------------------------
/art/light/register_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/light/register_light.png
--------------------------------------------------------------------------------
/art/light/reset_password.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/light/reset_password.png
--------------------------------------------------------------------------------
/art/light/reset_password_confirm_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/light/reset_password_confirm_light.png
--------------------------------------------------------------------------------
/art/light/search_notes_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/light/search_notes_light.png
--------------------------------------------------------------------------------
/art/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/art/thumbnail.png
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext {
4 | core_ktx_version = '1.10.0'
5 | lifecycle_runtime_ktx_version = '2.6.1'
6 | activity_compose_version = '1.7.1'
7 | compose_lifecycle_viewmodel_version = '2.6.1'
8 | compose_material_icons_version = '1.5.0-alpha04'
9 | dagger_hilt_version = '2.46'
10 | hilt_lifecycle_viewmodel_version = '1.0.0-alpha03'
11 | hilt_compiler_version = '1.0.0'
12 | hilt_nav_compose_version = '1.0.0'
13 | splash_screen_api_version = '1.0.1'
14 | compose_nav_version = '2.6.0-rc01'
15 | retrofit_version = '2.9.0'
16 | okhttp_version = '5.0.0-alpha.3'
17 | accompanist_animation_version = '0.31.1-alpha'
18 | accompanist_systemui_controller_version = '0.27.0'
19 | crypto_version = '1.1.0-alpha06'
20 | }
21 | }
22 | plugins {
23 | id 'com.android.application' version '8.0.0' apply false
24 | id 'com.android.library' version '8.0.0' apply false
25 | id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
26 | id("com.google.dagger.hilt.android") version "2.46" apply false
27 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItamiOMW/NotesWithRESTAPI/3f6f802647eb25ebbd2dc8d38a88252f7e28feeb/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Apr 30 08:05:03 MSK 2023
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 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Notes With REST API"
16 | include ':app'
17 |
--------------------------------------------------------------------------------