From d710525123097a47c1de88a30526d30e081a818c Mon Sep 17 00:00:00 2001 From: MaximOksiuta <63787095+MaximOksiuta@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:19:14 +0300 Subject: [PATCH] Initial with template --- .gitignore | 15 + .idea/.gitignore | 3 + .idea/AndroidProjectSystem.xml | 6 + .idea/appInsightsSettings.xml | 40 ++ .idea/compiler.xml | 6 + .idea/deploymentTargetSelector.xml | 10 + .idea/gradle.xml | 19 + .idea/inspectionProfiles/Project_Default.xml | 61 +++ .idea/migrations.xml | 10 + .idea/misc.xml | 9 + .idea/runConfigurations.xml | 17 + app/.gitignore | 1 + app/build.gradle.kts | 201 +++++++++ app/google-services.json | 29 ++ app/kotzilla.json | 11 + app/proguard-rules.pro | 21 + .../1.json | 62 +++ .../moscow2025/ExampleInstrumentedTest.kt | 24 ++ app/src/main/AndroidManifest.xml | 50 +++ app/src/main/assets/kotzilla.key | 1 + .../moscow2025/FirebaseMessagingService.kt | 81 ++++ .../com/prodhack/moscow2025/common/App.kt | 68 +++ .../prodhack/moscow2025/common/Constants.kt | 5 + .../moscow2025/common/di/ScanModules.kt | 16 + .../moscow2025/data/base/BaseEntity.kt | 6 + .../moscow2025/data/base/BasePaginationDAO.kt | 10 + .../data/base/BaseRemoteMediator.kt | 62 +++ .../moscow2025/data/base/BaseRepository.kt | 175 ++++++++ .../moscow2025/data/base/DBMappableDTO.kt | 5 + .../moscow2025/data/base/DomainMappableDTO.kt | 5 + .../data_providers/GalleryPagingSource.kt | 60 +++ .../data/data_providers/api/ApiKtorClient.kt | 79 ++++ .../api/utils/TimestampFormatter.kt | 12 + .../localInfo/AuthorizationDataStore.kt | 55 +++ .../data_providers/local_db/AppDatabase.kt | 18 + .../local_db/DatabaseProvider.kt | 19 + .../data_providers/local_db/dao/CleanUpDao.kt | 17 + .../data_providers/local_db/dao/UserDao.kt | 24 ++ .../local_db/entities/UserEntity.kt | 44 ++ .../prodhack/moscow2025/data/dto/AuthDtos.kt | 85 ++++ .../repImplementations/AuthRepositoryImpl.kt | 60 +++ .../GalleryRepositoryImpl.kt | 23 + .../repImplementations/UserRepositoryImpl.kt | 75 ++++ .../domain/interfaces/AuthRepository.kt | 15 + .../domain/interfaces/GalleryRepository.kt | 8 + .../domain/interfaces/UserRepository.kt | 15 + .../prodhack/moscow2025/domain/models/Auth.kt | 11 + .../prodhack/moscow2025/domain/models/User.kt | 20 + .../usecase/auth/CheckSessionUseCase.kt | 15 + .../domain/usecase/auth/GetUserUseCase.kt | 12 + .../domain/usecase/auth/LogOutUseCase.kt | 16 + .../domain/usecase/auth/LoginUserUseCase.kt | 14 + .../usecase/auth/RegisterUserUseCase.kt | 14 + .../domain/usecase/auth/UpdateUserUseCase.kt | 15 + .../usecase/auth/ValidateAuthFieldsUseCase.kt | 102 +++++ .../moscow2025/domain/utils/NetworkError.kt | 38 ++ .../moscow2025/domain/utils/TypeAliases.kt | 21 + .../moscow2025/presentation/MainActivity.kt | 133 ++++++ .../components/BottomNavigation.kt | 138 ++++++ .../components/standart/TTBigButton.kt | 84 ++++ .../components/standart/TTCheckBox.kt | 38 ++ .../standart/TTFloatingActionButton.kt | 53 +++ .../components/standart/TTNamedTextField.kt | 47 ++ .../components/standart/TTPasswordField.kt | 127 ++++++ .../components/standart/TTTextField.kt | 402 ++++++++++++++++++ .../components/standart/TTTopLogo.kt | 44 ++ .../presentation/navigation/AppDestination.kt | 18 + .../presentation/navigation/TTasksApp.kt | 99 +++++ .../presentation/navigation/TTasksAppState.kt | 40 ++ .../presentation/navigation/TTasksNavHost.kt | 80 ++++ .../screens/fillProfile/FillProfileScreen.kt | 9 + .../fillProfile/FillProfileViewModel.kt | 185 ++++++++ .../presentation/screens/login/LoginScreen.kt | 206 +++++++++ .../screens/login/LoginViewModel.kt | 64 +++ .../presentation/screens/main/MainScreen.kt | 282 ++++++++++++ .../screens/main/MainScreenViewModel.kt | 143 +++++++ .../screens/register/RegisterScreen.kt | 182 ++++++++ .../screens/register/RegisterViewModel.kt | 110 +++++ .../moscow2025/presentation/theme/Color.kt | 103 +++++ .../moscow2025/presentation/theme/Dim.kt | 12 + .../moscow2025/presentation/theme/Shapes.kt | 11 + .../moscow2025/presentation/theme/Theme.kt | 154 +++++++ .../moscow2025/presentation/theme/Type.kt | 40 ++ .../moscow2025/presentation/utils/SetUtils.kt | 9 + .../presentation/utils/StringUtils.kt | 3 + .../presentation/utils/TimeUtils.kt | 55 +++ .../moscow2025/presentation/utils/UIState.kt | 226 ++++++++++ .../presentation/utils/base/BaseViewModel.kt | 78 ++++ .../presentation/utils/imageToByteArray.kt | 14 + .../presentation/utils/ui/CalendarModal.kt | 88 ++++ .../presentation/utils/ui/ColoredClickable.kt | 42 ++ .../utils/ui/placeholders/ErrorPlaceHolder.kt | 41 ++ .../ui/placeholders/LoadingPlaceholder.kt | 32 ++ .../main/res/drawable/add_square_outline.xml | 13 + app/src/main/res/drawable/ic_arr_dropdown.xml | 10 + app/src/main/res/drawable/ic_calendar.xml | 13 + app/src/main/res/drawable/ic_chart.xml | 10 + app/src/main/res/drawable/ic_checkmark.xml | 13 + app/src/main/res/drawable/ic_documents.xml | 10 + app/src/main/res/drawable/ic_flag_filled.xml | 9 + .../main/res/drawable/ic_flag_unfilled.xml | 12 + app/src/main/res/drawable/ic_group.xml | 20 + app/src/main/res/drawable/ic_home.xml | 13 + .../res/drawable/ic_launcher_background.xml | 170 ++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++ app/src/main/res/drawable/ic_magnifer.xml | 10 + app/src/main/res/drawable/ic_minus.xml | 13 + app/src/main/res/drawable/ic_notification.xml | 5 + app/src/main/res/drawable/ic_plus.xml | 13 + app/src/main/res/drawable/ic_profile.xml | 10 + app/src/main/res/drawable/ic_trips.xml | 13 + app/src/main/res/drawable/logout_icon.xml | 12 + app/src/main/res/drawable/lottie.png | Bin 0 -> 8824 bytes app/src/main/res/font/tinkoff_sans_bold.ttf | Bin 0 -> 70888 bytes app/src/main/res/font/tinkoff_sans_medium.ttf | Bin 0 -> 71272 bytes .../main/res/font/tinkoff_sans_regular.ttf | Bin 0 -> 70240 bytes .../main/res/mipmap-anydpi/ic_launcher.xml | 6 + .../res/mipmap-anydpi/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes app/src/main/res/values/colors.xml | 10 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/themes.xml | 5 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../prodhack/moscow2025/ExampleUnitTest.kt | 17 + build.gradle.kts | 20 + gradle.properties | 23 + gradle/libs.versions.toml | 133 ++++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45457 bytes gradle/wrapper/gradle-wrapper.properties | 8 + gradlew | 251 +++++++++++ gradlew.bat | 94 ++++ settings.gradle.kts | 28 ++ 142 files changed, 6343 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/appInsightsSettings.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/google-services.json create mode 100644 app/kotzilla.json create mode 100644 app/proguard-rules.pro create mode 100644 app/schemas/com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase/1.json create mode 100644 app/src/androidTest/java/com/prodhack/moscow2025/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/assets/kotzilla.key create mode 100644 app/src/main/java/com/prodhack/moscow2025/FirebaseMessagingService.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/common/App.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/common/Constants.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/common/di/ScanModules.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/base/BaseEntity.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/base/BasePaginationDAO.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/base/BaseRemoteMediator.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/base/BaseRepository.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/base/DBMappableDTO.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/base/DomainMappableDTO.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/data_providers/GalleryPagingSource.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/ApiKtorClient.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/utils/TimestampFormatter.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/data_providers/localInfo/AuthorizationDataStore.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/AppDatabase.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/DatabaseProvider.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/CleanUpDao.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/UserDao.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/entities/UserEntity.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/dto/AuthDtos.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/repImplementations/AuthRepositoryImpl.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/repImplementations/GalleryRepositoryImpl.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/data/repImplementations/UserRepositoryImpl.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/interfaces/AuthRepository.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/interfaces/GalleryRepository.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/interfaces/UserRepository.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/models/Auth.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/models/User.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/CheckSessionUseCase.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/GetUserUseCase.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LogOutUseCase.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LoginUserUseCase.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/RegisterUserUseCase.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/UpdateUserUseCase.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateAuthFieldsUseCase.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/utils/NetworkError.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/utils/TypeAliases.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/MainActivity.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/components/BottomNavigation.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTBigButton.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTCheckBox.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTFloatingActionButton.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTNamedTextField.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTPasswordField.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTopLogo.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksApp.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksAppState.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileViewModel.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginViewModel.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterViewModel.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/theme/Color.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/theme/Dim.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/theme/Shapes.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/theme/Theme.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/theme/Type.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/utils/SetUtils.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/utils/StringUtils.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/utils/TimeUtils.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/utils/UIState.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/utils/base/BaseViewModel.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/utils/imageToByteArray.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/CalendarModal.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/ColoredClickable.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/ErrorPlaceHolder.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/LoadingPlaceholder.kt create mode 100644 app/src/main/res/drawable/add_square_outline.xml create mode 100644 app/src/main/res/drawable/ic_arr_dropdown.xml create mode 100644 app/src/main/res/drawable/ic_calendar.xml create mode 100644 app/src/main/res/drawable/ic_chart.xml create mode 100644 app/src/main/res/drawable/ic_checkmark.xml create mode 100644 app/src/main/res/drawable/ic_documents.xml create mode 100644 app/src/main/res/drawable/ic_flag_filled.xml create mode 100644 app/src/main/res/drawable/ic_flag_unfilled.xml create mode 100644 app/src/main/res/drawable/ic_group.xml create mode 100644 app/src/main/res/drawable/ic_home.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_magnifer.xml create mode 100644 app/src/main/res/drawable/ic_minus.xml create mode 100644 app/src/main/res/drawable/ic_notification.xml create mode 100644 app/src/main/res/drawable/ic_plus.xml create mode 100644 app/src/main/res/drawable/ic_profile.xml create mode 100644 app/src/main/res/drawable/ic_trips.xml create mode 100644 app/src/main/res/drawable/logout_icon.xml create mode 100644 app/src/main/res/drawable/lottie.png create mode 100644 app/src/main/res/font/tinkoff_sans_bold.ttf create mode 100644 app/src/main/res/font/tinkoff_sans_medium.ttf create mode 100644 app/src/main/res/font/tinkoff_sans_regular.ttf create mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/prodhack/moscow2025/ExampleUnitTest.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..a0d2490 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,40 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..91bc919 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,201 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.jetbrainsKotlinAndroid) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.googleKsp) + alias(libs.plugins.room) + alias(libs.plugins.serialization) + alias(libs.plugins.secrets.gradle.plugin) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.google.services.gmc) + alias(libs.plugins.firebase.crashlytics) + alias(libs.plugins.kotzilla) +} + +android { + namespace = "com.prodhack.moscow2025" + compileSdk { + version = release(36) + } + + defaultConfig { + applicationId = "com.prodhack.moscow2025" + minSdk = 29 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + room { + schemaDirectory("$projectDir/schemas") + } + + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + buildFeatures { + compose = true + buildConfig = true + viewBinding = true + } + + ksp { + arg("KOIN_CONFIG_CHECK", "true") + arg("KOIN_DEFAULT_MODULE", "false") + arg("KOIN_USE_COMPOSE_VIEWMODEL", "true") + } + composeOptions.kotlinCompilerExtensionVersion = "1.5.6" + packagingOptions.resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}") + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode") + } + } +} + +dependencies { + implementation(libs.kotzilla.sdk) + // Base libraries ------------------------------------------------------------------------------ + + implementation(libs.core.ktx) + implementation(platform(libs.kotlin.bom)) + implementation(libs.lifecycle.runtime.ktx) + implementation(libs.activity.compose) + implementation(platform(libs.compose.bom)) + implementation(libs.ui) + implementation(libs.ui.graphics) + implementation(libs.ui.tooling.preview) + implementation(libs.compose.animation.graphics) + implementation(libs.material.icons.extended) + implementation(libs.androidx.foundation) + androidTestImplementation(platform(libs.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.ui.tooling) + debugImplementation(libs.ui.test.manifest) + + // Material + implementation(libs.material3) + + // Navigation + implementation(libs.navigation.compose) + + // Data store + implementation(libs.androidx.datastore.preferences) + + // TESTING ------------------------------------------------------------------------------------- + + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) + + // Koin testing + testImplementation(libs.koin.test) + + // IMAGES -------------------------------------------------------------------------------------- + + // Coil + implementation(libs.coil.compose) + implementation(libs.coil.svg) + implementation(libs.coil.gif) + + // VIEW ELEMENTS ------------------------------------------------------------------------------- + + // Lottie animation for start + implementation(libs.lottie.compose) + + // Fonts + implementation(libs.ui.text.google.fonts) + + // Constraint layout + implementation(libs.constraintlayout.compose) + + // NETWORK ------------------------------------------------------------------------------------- + + // gson + implementation(libs.gson) + + + // Ktor + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.client.core) + implementation(libs.io.ktor.ktor.client.cio) + implementation(libs.ktor.client.websockets) + implementation(libs.ktor.client.negotiation) + implementation(libs.ktor.client.auth) + + // Paging + implementation(libs.androidx.paging.runtime.ktx) + implementation(libs.androidx.paging.compose) + + // GOOGLE AND FACEBOOK SERVICES ---------------------------------------------------------------- + + // Cloud messaging + implementation(libs.play.services.gcm) + implementation(libs.google.firebase.analytics) + implementation(libs.firebase.messaging) + + // Import the BoM for the Firebase platform + implementation(platform(libs.firebase.bom)) + + // Guava + implementation(libs.guava) + + // OTHER SERVICES ------------------------------------------------------------------------------ + + // CameraX + implementation(libs.camera.core) + implementation(libs.camera.camera2) + implementation(libs.androidx.camera.lifecycle) + implementation(libs.androidx.camera.video) + implementation(libs.androidx.camera.view) + implementation(libs.androidx.camera.extensions) + + // Koin + implementation(libs.koin.core) + implementation(libs.koin.android) + implementation(libs.koin.annotations) + implementation(libs.koin.androidx.compose) + implementation(libs.koin.androidx.compose.navigation) + ksp(libs.koin.ksp.compiler) + ksp(libs.koin.ksp.bom) + + // Viewmodel + implementation(libs.androidx.savedstate.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.service) + implementation(libs.lifecycle.runtime.ktx) + + // Room + implementation(libs.androidx.room.runtime) + ksp(libs.androidx.room.compiler) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.room.paging) + + // Permission manager + implementation(libs.accompanist.permissions) + + // System UI controller + implementation(libs.accompanist.systemuicontroller) + + implementation(libs.androidx.core.splashscreen) + + // END ------------------------------------------------------------------------------------- +} \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..182ebf8 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "846499996834", + "project_id": "prodmoscow2025", + "storage_bucket": "prodmoscow2025.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:846499996834:android:3f1b450bd2c804dff523fb", + "android_client_info": { + "package_name": "com.prodhack.moscow2025" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDFkGaR9ME9drbAw2pAeSDd2QX8vY5H8d8" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/kotzilla.json b/app/kotzilla.json new file mode 100644 index 0000000..7273d9b --- /dev/null +++ b/app/kotzilla.json @@ -0,0 +1,11 @@ +{ + "sdkVersion": "1.3.1", + "keys": [ + { + "appId": "603ad697-3a85-4f69-89db-d0322cbb6059", + "applicationPackageName": "com.prodhack.moscow2025", + "keyId": "019aa5e2-2c8b-76f5-aa95-104934100116", + "apiKey": "ktz-sdk-rwsABf6YmCK4P70l7drtjOaWEj8s9WRu17VulWyrfcM" + } + ] +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/schemas/com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase/1.json b/app/schemas/com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase/1.json new file mode 100644 index 0000000..4877ef0 --- /dev/null +++ b/app/schemas/com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase/1.json @@ -0,0 +1,62 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "bf664fe902e116c42af432814d63d6a7", + "entities": [ + { + "tableName": "users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `email` TEXT NOT NULL, `first_name` TEXT, `last_name` TEXT, `display_name` TEXT, `avatar_url` TEXT, `phone` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lastName", + "columnName": "last_name", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatar_url", + "affinity": "TEXT" + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bf664fe902e116c42af432814d63d6a7')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/prodhack/moscow2025/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/prodhack/moscow2025/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..f5450d7 --- /dev/null +++ b/app/src/androidTest/java/com/prodhack/moscow2025/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.prodhack.moscow2025 + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.prodhack.moscow2025", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b9f1966 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/kotzilla.key b/app/src/main/assets/kotzilla.key new file mode 100644 index 0000000..53027ae --- /dev/null +++ b/app/src/main/assets/kotzilla.key @@ -0,0 +1 @@ +Omt0ei1zZGstcndzQUJmNlltQ0s0UDcwbDdkcnRqT2FXRWo4czlXUnUxN1Z1bFd5cmZjTQ== \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/FirebaseMessagingService.kt b/app/src/main/java/com/prodhack/moscow2025/FirebaseMessagingService.kt new file mode 100644 index 0000000..d3672b0 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/FirebaseMessagingService.kt @@ -0,0 +1,81 @@ +package com.prodhack.moscow2025 + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Intent +import android.util.Log +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.core.app.NotificationCompat +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import com.prodhack.moscow2025.presentation.MainActivity + +class FirebaseMessagingService : FirebaseMessagingService() { + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + } + + override fun onMessageReceived(message: RemoteMessage) { + + val title = message.data["title"] + val text = message.data["body"] + Log.e( + "fcm", + "title=$title" + + "\ntext=$text" + ) + + title?.let { it1 -> text?.let { it2 -> sendNotification(it1, it2) } } + } + + override fun onNewToken(token: String) { + super.onNewToken(token) + Log.e("fcm", "token=$token") + } + + private fun createNotificationChannel() { + val channel = NotificationChannel( + "default_channel_id", + "MainAppNotifications", + NotificationManager.IMPORTANCE_HIGH + ).apply { + description = "Channel for main motifications" + enableLights(true) + lightColor = Color.Red.toArgb() + enableVibration(true) + vibrationPattern = longArrayOf(0, 500, 200, 500) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + } + + val notificationManager = + getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + + private fun sendNotification(title: String?, messageBody: String?) { + val intent = Intent(this, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + + val pendingIntent = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) + + val notificationBuilder = NotificationCompat.Builder(this, "default_channel_id") + .setSmallIcon(R.drawable.ic_launcher_background) // замените на свою иконку + .setContentTitle(title ?: "Уведомление") + .setContentText(messageBody) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + + val notificationManager = + getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(0, notificationBuilder.build()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/common/App.kt b/app/src/main/java/com/prodhack/moscow2025/common/App.kt new file mode 100644 index 0000000..b8c58b6 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/common/App.kt @@ -0,0 +1,68 @@ +package com.prodhack.moscow2025.common + +import android.app.Application +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import com.google.firebase.FirebaseApp +import com.prodhack.moscow2025.common.di.AppModule +import com.prodhack.moscow2025.common.di.DataModule +import com.prodhack.moscow2025.common.di.DomainModule +import com.prodhack.moscow2025.data.data_providers.local_db.DatabaseProvider +import io.kotzilla.sdk.analytics.koin.analytics +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin +import org.koin.ksp.generated.module + + +class App : Application() { + + companion object { + lateinit var instance: Application + lateinit var version: String + } + + override fun onCreate() { + super.onCreate() + instance = this + version = getAppVersion() + startKoin { + androidContext(this@App) + analytics() + modules( + listOf( + AppModule().module, + DataModule().module, + DomainModule().module, + DatabaseProvider().module + ) + ) + } + FirebaseApp.initializeApp(this@App) + } + + private fun getAppVersion(): String { + var pInfo: PackageInfo? = null + try { + val pm = packageManager + if (pm != null) { + pInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)) + } else { + pm.getPackageInfo(packageName, 0) + } + } + } catch (e: Exception) { + Log.d("App", "method: getAppVersion - error: $e") + } + if (pInfo == null) { + pInfo = PackageInfo() + pInfo.versionName = "0.0.0" + pInfo.longVersionCode = 0 + } + var version = pInfo.versionName + "." + version += pInfo.longVersionCode + return version + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/common/Constants.kt b/app/src/main/java/com/prodhack/moscow2025/common/Constants.kt new file mode 100644 index 0000000..d66fabc --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/common/Constants.kt @@ -0,0 +1,5 @@ +package com.prodhack.moscow2025.common + +object Constants { + const val BASE_API_URL = "https://hackaton.paas.itqdev.xyz/" +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/common/di/ScanModules.kt b/app/src/main/java/com/prodhack/moscow2025/common/di/ScanModules.kt new file mode 100644 index 0000000..2c55559 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/common/di/ScanModules.kt @@ -0,0 +1,16 @@ +package com.prodhack.moscow2025.common.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("com.prodhack.moscow2025.presentation") +class AppModule + +@Module +@ComponentScan("com.prodhack.moscow2025.domain") +class DomainModule + +@Module +@ComponentScan("com.prodhack.moscow2025.data") +class DataModule diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/BaseEntity.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseEntity.kt new file mode 100644 index 0000000..17dcbd9 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseEntity.kt @@ -0,0 +1,6 @@ +package com.prodhack.moscow2025.data.base + + +interface BaseEntity { + val id: Number +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/BasePaginationDAO.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/BasePaginationDAO.kt new file mode 100644 index 0000000..30ab752 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/base/BasePaginationDAO.kt @@ -0,0 +1,10 @@ +package com.prodhack.moscow2025.data.base + +import androidx.paging.PagingSource + +interface BasePaginationDAO { + suspend fun clearAll() + suspend fun upsertAll(data: List) + + fun getPaginatedData(): PagingSource +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRemoteMediator.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRemoteMediator.kt new file mode 100644 index 0000000..ddf62da --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRemoteMediator.kt @@ -0,0 +1,62 @@ +package com.prodhack.moscow2025.data.base + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.RoomDatabase +import androidx.room.withTransaction + +@OptIn(ExperimentalPagingApi::class) +class BaseRemoteMediator( + private val db: RoomDatabase, + private val dao: BasePaginationDAO, + private val makeRequest: suspend (page: Long, pageCount: Int) -> Result> +) : RemoteMediator() { + + override suspend fun load( + loadType: LoadType, + state: PagingState + ): MediatorResult { + return try { + val loadKey = when (loadType) { + LoadType.REFRESH -> 1 + LoadType.PREPEND -> return MediatorResult.Success( + endOfPaginationReached = true + ) + + LoadType.APPEND -> { + val lastItem = state.lastItemOrNull() + if (lastItem == null) { + 1 + } else { + (lastItem.id.toLong() / state.config.pageSize) + 1 + } + } + } + + val result = makeRequest( + loadKey, + state.config.pageSize + ) + + if (result.isSuccess) { + val data = result.getOrNull()!! + db.withTransaction { + if (loadType == LoadType.REFRESH) { + dao.clearAll() + } + val beerEntities = data + dao.upsertAll(beerEntities) + } + MediatorResult.Success( + endOfPaginationReached = data.size < state.config.pageSize + ) + } else { + MediatorResult.Error(result.exceptionOrNull()!!) + } + } catch (e: Exception) { + MediatorResult.Error(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRepository.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRepository.kt new file mode 100644 index 0000000..21f0771 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/base/BaseRepository.kt @@ -0,0 +1,175 @@ +package com.prodhack.moscow2025.data.base + +import android.util.Log +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.room.RoomDatabase +import com.prodhack.moscow2025.data.dto.ErrorNetworkDTO +import com.prodhack.moscow2025.domain.utils.NetworkError +import com.prodhack.moscow2025.domain.utils.convertToNetworkError +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.request +import io.ktor.http.isSuccess +import kotlinx.coroutines.flow.Flow +import kotlin.time.Duration + +abstract class BaseRepository { + + // Caching module ============================================================================== + private val internalCacheStorage = mutableMapOf>() + + private data class CacheEntry( + val value: T, + val expirationTime: Long + ) + + fun putCache(cacheConfiguration: Pair, value: T) { + internalCacheStorage[cacheConfiguration.first] = + CacheEntry(value, cacheConfiguration.second.inWholeSeconds) + } + + @Suppress("UNCHECKED_CAST") + fun getCache(key: String): T? { + val entry = internalCacheStorage[key] ?: return null + if (entry.expirationTime < System.currentTimeMillis()) { + internalCacheStorage.remove(key) + return null + } + return entry.value as T + } + + // Base data sources =========================================================================== + + protected open val defaultKtorClient: HttpClient? = null + protected open val db: RoomDatabase? = null + + companion object { + private const val TAG = "BaseRepository" + } + + // Internal methods ============================================================================ + + private fun assertKtorClientSpecify() { + if (defaultKtorClient == null) { + Log.e(TAG, "You must specify ktor client for make network requests") + throw IllegalStateException("You must specify ktor client for make network requests") + } + } + + private fun assertDBSpecify() { + if (db == null) { + throw IllegalStateException("You must specify db for use pagination/cashing") + } + } + + // And methods for use :) ====================================================================== + + /** + * Makes a network request using the provided Ktor client and request builder block. + * + * This function handles the common boilerplate for making a network request, + * including error handling and converting exceptions to a domain-specific `NetworkError`. + * + * @param T The expected successful response type. This type must be deserializable by Ktor. + * @param ktorClient The [HttpClient] to use for the request. Defaults to `this.defaultKtorClient`. + * An [IllegalStateException] will be thrown if no client is provided and `defaultKtorClient` is null. + * @param block A lambda function that configures the [HttpRequestBuilder] for the request. + * @return A [Result] object containing either the successful response of type [T] or a [NetworkError] if the request fails. + * @throws IllegalStateException if `ktorClient` is null and `defaultKtorClient` is also null. + */ + internal suspend inline fun networkRequest( + ktorClient: HttpClient? = this.defaultKtorClient, + cacheConfiguration: Pair? = null, + block: HttpRequestBuilder.() -> Unit + ): Result { + Log.d(TAG, "Network request! Asserting ktor client specify") + assertKtorClientSpecify() + Log.d(TAG, "ktor client is specified - continue network request") + return try { + Log.d(TAG, "Start request!") + val response = ktorClient!!.request(block = block) + Log.d(TAG, "Request was made without exceptions") + + if (response.status.isSuccess()) { + Result.success( + value = response + ).map { + it.body() + } + } else { + val firstCodeNum = response.status.value / 100 + val detail = (response.body() as? ErrorNetworkDTO)?.detail ?: "Unknown" + Result.failure( + when (firstCodeNum) { + 4 -> NetworkError.InputError(detail) + else -> NetworkError.Unexpected(detail) + } + ) + } + } catch (e: Exception) { + Log.e(TAG, "Exception in request process! $e") + Result.failure( + exception = e.convertToNetworkError() + ) + }.onSuccess { + Log.v(TAG, "Network request was successful") + if (cacheConfiguration != null) { + putCache(cacheConfiguration, it) + } + }.onFailure { + Log.e(TAG, "Network request has error! $it") + } + } + + + internal suspend inline fun internalCachedRequest( + ktorClient: HttpClient? = this.defaultKtorClient, + cacheConfiguration: Pair, + block: HttpRequestBuilder.() -> Unit + ): Result { + val cachedResult = getCache(cacheConfiguration.first) + + return if (cachedResult != null) { + Result.success(cachedResult) + } else { + networkRequest(ktorClient, cacheConfiguration, block) + } + } + + @OptIn(ExperimentalPagingApi::class) + protected fun paginatedRequest( + pageSize: Int = 10, + prefetchDistance: Int = pageSize, + enablePlaceholders: Boolean = true, + initialLoadSize: Int = pageSize * 3, + maxSize: Int = Int.MAX_VALUE, + jumpThreshold: Int = Int.MIN_VALUE, + dbDao: BasePaginationDAO, + makeRequest: suspend (page: Long, pageSize: Int) -> Result> + ): Flow> { + assertDBSpecify() + + return Pager( + config = PagingConfig( + pageSize, + prefetchDistance, + enablePlaceholders, + initialLoadSize, + maxSize, + jumpThreshold + ), + remoteMediator = BaseRemoteMediator( + db = db!!, + dao = dbDao, + makeRequest = makeRequest + ), + pagingSourceFactory = { + dbDao.getPaginatedData() + } + ).flow + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/DBMappableDTO.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/DBMappableDTO.kt new file mode 100644 index 0000000..60c8262 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/base/DBMappableDTO.kt @@ -0,0 +1,5 @@ +package com.prodhack.moscow2025.data.base + +interface DBMappableDTO { + fun mapToDB(): T +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/base/DomainMappableDTO.kt b/app/src/main/java/com/prodhack/moscow2025/data/base/DomainMappableDTO.kt new file mode 100644 index 0000000..204b7d3 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/base/DomainMappableDTO.kt @@ -0,0 +1,5 @@ +package com.prodhack.moscow2025.data.base + +interface DomainMappableDTO { + fun mapToDomain(): T +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/GalleryPagingSource.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/GalleryPagingSource.kt new file mode 100644 index 0000000..bcbbb4d --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/GalleryPagingSource.kt @@ -0,0 +1,60 @@ +package com.prodhack.moscow2025.data.data_providers + +import android.content.ContentResolver +import android.provider.MediaStore +import androidx.paging.PagingSource +import androidx.paging.PagingState + +class GalleryPagingSource( + private val contentResolver: ContentResolver +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val page = params.key ?: 0 + val pageSize = params.loadSize + + val projection = arrayOf( + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DATE_ADDED + ) + + val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC" + + val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + val cursor = contentResolver.query( + uri, + projection, + null, + null, + "$sortOrder LIMIT $pageSize OFFSET ${page * pageSize}" + ) + + val images = mutableListOf() + cursor?.use { + val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID) + while (it.moveToNext()) { + images.add(it.getLong(idColumn)) + } + } + + val nextKey = if (images.size < pageSize) null else page + 1 + + LoadResult.Page( + data = images, + prevKey = if (page == 0) null else page - 1, + nextKey = nextKey + ) + + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/ApiKtorClient.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/ApiKtorClient.kt new file mode 100644 index 0000000..3071978 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/ApiKtorClient.kt @@ -0,0 +1,79 @@ +package com.prodhack.moscow2025.data.data_providers.api + +import com.prodhack.moscow2025.common.Constants +import com.prodhack.moscow2025.data.data_providers.localInfo.AuthorizationDataStore +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.HttpRequestRetry +import io.ktor.client.plugins.auth.Auth +import io.ktor.client.plugins.auth.providers.BearerTokens +import io.ktor.client.plugins.auth.providers.bearer +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.ANDROID +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import org.koin.core.annotation.Single + + +// Configuration Ktor client for request to API +@Single +class ApiKtorClient(authorizationDataStore: AuthorizationDataStore) { + + val client = HttpClient(OkHttp) { + install(Logging) { + logger = Logger.ANDROID + level = LogLevel.ALL + } + install(HttpRequestRetry) { + retryOnServerErrors(maxRetries = 3) + exponentialDelay() + } + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }) + } + defaultRequest { + url(Constants.BASE_API_URL) + } + install(Auth) { + bearer { + sendWithoutRequest { request -> + val segments = request.url.pathSegments + + val endpointsWithoutAuth = listOf( + "sign_in", + "sign_up" + ) + + endpointsWithoutAuth.any { segments.contains(it) }.not() + } + loadTokens { + return@loadTokens authorizationDataStore.token.first() + .toBearerTokens() + } + refreshTokens { + CoroutineScope(Dispatchers.IO).launch { + authorizationDataStore.clearToken() + } + + return@refreshTokens null + } + } + } + } + + private fun String.toBearerTokens(): BearerTokens { + return BearerTokens(this, null) + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/utils/TimestampFormatter.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/utils/TimestampFormatter.kt new file mode 100644 index 0000000..b2aa9d3 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/utils/TimestampFormatter.kt @@ -0,0 +1,12 @@ +package com.prodhack.moscow2025.data.data_providers.api.utils + +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +internal fun String.parseToTimestamp(): Long? { + val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) + dateFormatter.timeZone = TimeZone.getTimeZone("UTC") + + return dateFormatter.parse(this)?.time +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/localInfo/AuthorizationDataStore.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/localInfo/AuthorizationDataStore.kt new file mode 100644 index 0000000..0994d90 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/localInfo/AuthorizationDataStore.kt @@ -0,0 +1,55 @@ +package com.prodhack.moscow2025.data.data_providers.localInfo + +import android.content.Context +import android.util.Log +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.io.IOException +import org.koin.core.annotation.Single + + +@Single +class AuthorizationDataStore( + context: Context +) { + private val Context.dataStore by preferencesDataStore( + name = "authTokens" + ) + + private val dataStore = context.dataStore + + private companion object { + const val TAG = "AuthorizationDataStore" + val ACCESS_TOKEN = stringPreferencesKey("accessToken") + } + + suspend fun saveToken(accessToken: String) { + dataStore.edit { preferences -> + preferences[ACCESS_TOKEN] = accessToken + } + } + + suspend fun clearToken() { + dataStore.edit { preferences -> + preferences[ACCESS_TOKEN] = "" + } + } + + val token + get() = dataStore.data + .catch { + Log.e(TAG, "Error reading preferences.", it) + if (it is IOException) { + Log.e(TAG, "return empty prefs") + emit(emptyPreferences()) + } else { + throw it + } + }.map { preferences -> + preferences[ACCESS_TOKEN] ?: "" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/AppDatabase.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/AppDatabase.kt new file mode 100644 index 0000000..db2fb77 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/AppDatabase.kt @@ -0,0 +1,18 @@ +package com.prodhack.moscow2025.data.data_providers.local_db + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.prodhack.moscow2025.data.data_providers.local_db.dao.CleanUpDao +import com.prodhack.moscow2025.data.data_providers.local_db.dao.UserDao +import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity + +@Database( + entities = [UserEntity::class], + version = 1, + exportSchema = true +) +abstract class AppDatabase : RoomDatabase() { + abstract fun userDao(): UserDao + + abstract fun cleanUpDao(): CleanUpDao +} diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/DatabaseProvider.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/DatabaseProvider.kt new file mode 100644 index 0000000..6b1976b --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/DatabaseProvider.kt @@ -0,0 +1,19 @@ +package com.prodhack.moscow2025.data.data_providers.local_db + +import android.content.Context +import androidx.room.Room +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +@Module +class DatabaseProvider { + + @Single + fun provideDatabase(context: Context): AppDatabase = + Room.databaseBuilder( + context, + AppDatabase::class.java, + "t_tasks.db" + ).fallbackToDestructiveMigration() + .build() +} diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/CleanUpDao.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/CleanUpDao.kt new file mode 100644 index 0000000..efa0e44 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/CleanUpDao.kt @@ -0,0 +1,17 @@ +package com.prodhack.moscow2025.data.data_providers.local_db.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction + +@Dao +interface CleanUpDao { + @Query("DELETE FROM users") + suspend fun cleanUpUsers() + + @Transaction + suspend fun cleanUp() { + cleanUpUsers() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/UserDao.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/UserDao.kt new file mode 100644 index 0000000..8c2c6b0 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/dao/UserDao.kt @@ -0,0 +1,24 @@ +package com.prodhack.moscow2025.data.data_providers.local_db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface UserDao { + + @Query("SELECT * FROM users LIMIT 1") + fun observeUser(): Flow + + @Query("SELECT * FROM users LIMIT 1") + suspend fun getUser(): UserEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(user: UserEntity) + + @Query("DELETE FROM users") + suspend fun clear() +} diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/entities/UserEntity.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/entities/UserEntity.kt new file mode 100644 index 0000000..3e149cd --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/local_db/entities/UserEntity.kt @@ -0,0 +1,44 @@ +package com.prodhack.moscow2025.data.data_providers.local_db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.prodhack.moscow2025.domain.models.User + +@Entity(tableName = "users") +data class UserEntity( + @PrimaryKey(autoGenerate = false) + val id: String, + val email: String, + @ColumnInfo(name = "first_name") + val firstName: String?, + @ColumnInfo(name = "last_name") + val lastName: String?, + @ColumnInfo(name = "display_name") + val displayName: String?, + @ColumnInfo(name = "avatar_url") + val avatarUrl: String?, + val phone: String? +) { + fun mapToDomain(): User { + return User( + id = id, + firstName = firstName, + lastName = lastName, + displayName = displayName, + email = email, + avatarUrl = avatarUrl, + phone = phone + ) + } +} + +fun User.mapToDB(): UserEntity = UserEntity( + id = id, + firstName = firstName, + lastName = lastName, + displayName = displayName, + phone = phone, + email = email, + avatarUrl = avatarUrl +) \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/dto/AuthDtos.kt b/app/src/main/java/com/prodhack/moscow2025/data/dto/AuthDtos.kt new file mode 100644 index 0000000..1870dce --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/dto/AuthDtos.kt @@ -0,0 +1,85 @@ +package com.prodhack.moscow2025.data.dto + +import com.prodhack.moscow2025.domain.models.LoginData +import com.prodhack.moscow2025.domain.models.RegisterData +import com.prodhack.moscow2025.domain.models.UpdateUserData +import com.prodhack.moscow2025.domain.models.User +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ErrorNetworkDTO( + val detail: String +) + +@Serializable +data class UserPatchRequest( + val email: String?, + @SerialName("display_name") + val displayName: String? = null, + @SerialName("first_name") + val firstName: String? = null, + @SerialName("last_name") + val lastName: String? = null, + @SerialName("avatar_url") + val avatarUrl: String? = null, + val phone: String? = null, +) + +fun UpdateUserData.mapToData(): UserPatchRequest = UserPatchRequest( + email = email, + displayName = displayName, + firstName = firstName, + lastName = lastName, + avatarUrl = avatarUrl, + phone = phone +) + +@Serializable +data class UserLoginRequest( + val email: String, + val password: String +) + +fun LoginData.mapToData(): UserLoginRequest = UserLoginRequest(email, password) + + +@Serializable +data class UserRegisterRequest( + val email: String, + val password: String +) + +fun RegisterData.mapToData(): UserRegisterRequest = UserRegisterRequest(email, password) + + +@Serializable +data class TokenResponse( + @SerialName("access_token") + val token: String +) + +@Serializable +data class UserResponse( + val id: String, + val email: String, + @SerialName("display_name") + val displayName: String? = null, + @SerialName("first_name") + val firstName: String? = null, + @SerialName("last_name") + val lastName: String? = null, + @SerialName("avatar_url") + val avatarUrl: String? = null, + val phone: String? = null, +) { + fun mapToDomain(): User = User( + id = id, + email = email, + displayName = displayName, + firstName = firstName, + lastName = lastName, + avatarUrl = avatarUrl, + phone = phone + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/AuthRepositoryImpl.kt b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/AuthRepositoryImpl.kt new file mode 100644 index 0000000..a3cabe7 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/AuthRepositoryImpl.kt @@ -0,0 +1,60 @@ +package com.prodhack.moscow2025.data.repImplementations + +import com.prodhack.moscow2025.data.base.BaseRepository +import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient +import com.prodhack.moscow2025.data.data_providers.localInfo.AuthorizationDataStore +import com.prodhack.moscow2025.data.dto.TokenResponse +import com.prodhack.moscow2025.data.dto.mapToData +import com.prodhack.moscow2025.domain.interfaces.AuthRepository +import com.prodhack.moscow2025.domain.models.LoginData +import com.prodhack.moscow2025.domain.models.RegisterData +import io.ktor.client.request.setBody +import io.ktor.client.request.url +import io.ktor.http.ContentType +import io.ktor.http.HttpMethod +import io.ktor.http.contentType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.koin.core.annotation.Single + +@Single +class AuthRepositoryImpl( + ktorClient: ApiKtorClient, + private val authorizationDataStore: AuthorizationDataStore +) : AuthRepository, BaseRepository() { + + override val defaultKtorClient = ktorClient.client + + override fun fetchLoginState(): Flow = + authorizationDataStore.token.map { it.isNotBlank() } + + override suspend fun signUpRequest(request: RegisterData): Result = + networkRequest { + url { + method = HttpMethod.Post + url("/auth/sign_up/email") + setBody(request.mapToData()) + contentType(ContentType.Application.Json) + } + }.map { + authorizationDataStore.saveToken(it.token) + "Success" + } + + override suspend fun signInRequest(request: LoginData): Result = + networkRequest { + url { + method = HttpMethod.Post + url("/auth/sign_up/email") + setBody(request.mapToData()) + contentType(ContentType.Application.Json) + } + }.map { + authorizationDataStore.saveToken(it.token) + "Success" + } + + override suspend fun clearLoginData() { + authorizationDataStore.clearToken() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/GalleryRepositoryImpl.kt b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/GalleryRepositoryImpl.kt new file mode 100644 index 0000000..27c7df8 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/GalleryRepositoryImpl.kt @@ -0,0 +1,23 @@ +package com.prodhack.moscow2025.data.repImplementations + +import android.app.Application +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.prodhack.moscow2025.data.data_providers.GalleryPagingSource +import com.prodhack.moscow2025.domain.interfaces.GalleryRepository +import kotlinx.coroutines.flow.Flow +import org.koin.core.annotation.Single + +@Single +class GalleryRepositoryImpl(private val application: Application) : GalleryRepository { + override fun getImagesIds(): Flow> = Pager( + config = PagingConfig( + pageSize = 50, + enablePlaceholders = false + ), + pagingSourceFactory = { + GalleryPagingSource(application.contentResolver) + } + ).flow +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/UserRepositoryImpl.kt b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/UserRepositoryImpl.kt new file mode 100644 index 0000000..f1d274f --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/UserRepositoryImpl.kt @@ -0,0 +1,75 @@ +package com.prodhack.moscow2025.data.repImplementations + +import com.prodhack.moscow2025.data.base.BaseRepository +import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient +import com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase +import com.prodhack.moscow2025.data.data_providers.local_db.entities.mapToDB +import com.prodhack.moscow2025.data.dto.UserResponse +import com.prodhack.moscow2025.data.dto.mapToData +import com.prodhack.moscow2025.domain.interfaces.UserRepository +import com.prodhack.moscow2025.domain.models.UpdateUserData +import com.prodhack.moscow2025.domain.models.User +import io.ktor.client.request.setBody +import io.ktor.client.request.url +import io.ktor.http.ContentType +import io.ktor.http.HttpMethod +import io.ktor.http.contentType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import org.koin.core.annotation.Single + +@Single +class UserRepositoryImpl( + ktorClient: ApiKtorClient, + override val db: AppDatabase +) : UserRepository, BaseRepository() { + + override val defaultKtorClient = ktorClient.client + private val userDao = db.userDao() + + override fun observeUser(): Flow { + CoroutineScope(Dispatchers.IO).launch { + fetchProfile() + } + return userDao.observeUser().map { + it?.mapToDomain() + } + } + + private suspend fun writeProfileToDB(data: User) { + userDao.upsert(data.mapToDB()) + } + + override suspend fun fetchProfile(): Result = networkRequest { + url { + method = HttpMethod.Get + url("/profile") + } + }.map { + it.mapToDomain().also { + writeProfileToDB(it) + } + } + + override suspend fun updateProfile(request: UpdateUserData): Result { + return networkRequest { + url { + method = HttpMethod.Patch + url("/profile") + setBody(request.mapToData()) + contentType(ContentType.Application.Json) + } + }.map { + it.mapToDomain().also { + writeProfileToDB(it) + } + } + } + + override suspend fun clearLocalUserData() { + userDao.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/AuthRepository.kt b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/AuthRepository.kt new file mode 100644 index 0000000..6f377a2 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/AuthRepository.kt @@ -0,0 +1,15 @@ +package com.prodhack.moscow2025.domain.interfaces + +import com.prodhack.moscow2025.domain.models.LoginData +import com.prodhack.moscow2025.domain.models.RegisterData +import kotlinx.coroutines.flow.Flow + +interface AuthRepository { + fun fetchLoginState(): Flow + + suspend fun signUpRequest(request: RegisterData): Result + + suspend fun signInRequest(request: LoginData): Result + + suspend fun clearLoginData() +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/GalleryRepository.kt b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/GalleryRepository.kt new file mode 100644 index 0000000..fa17126 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/GalleryRepository.kt @@ -0,0 +1,8 @@ +package com.prodhack.moscow2025.domain.interfaces + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow + +interface GalleryRepository { + fun getImagesIds(): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/UserRepository.kt b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/UserRepository.kt new file mode 100644 index 0000000..a01ff12 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/UserRepository.kt @@ -0,0 +1,15 @@ +package com.prodhack.moscow2025.domain.interfaces + +import com.prodhack.moscow2025.domain.models.UpdateUserData +import com.prodhack.moscow2025.domain.models.User +import kotlinx.coroutines.flow.Flow + +interface UserRepository { + fun observeUser(): Flow + + suspend fun fetchProfile(): Result + + suspend fun updateProfile(request: UpdateUserData): Result + + suspend fun clearLocalUserData() +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/models/Auth.kt b/app/src/main/java/com/prodhack/moscow2025/domain/models/Auth.kt new file mode 100644 index 0000000..0eef4f8 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/models/Auth.kt @@ -0,0 +1,11 @@ +package com.prodhack.moscow2025.domain.models + +data class RegisterData( + val email: String, + val password: String +) + +data class LoginData( + val email: String, + val password: String +) \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/models/User.kt b/app/src/main/java/com/prodhack/moscow2025/domain/models/User.kt new file mode 100644 index 0000000..7a95845 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/models/User.kt @@ -0,0 +1,20 @@ +package com.prodhack.moscow2025.domain.models + +data class User( + val id: String, + val email: String, + val displayName: String?, + val firstName: String?, + val lastName: String?, + val avatarUrl: String?, + val phone: String? +) + +data class UpdateUserData( + val email: String? = null, + val displayName: String? = null, + val firstName: String? = null, + val lastName: String? = null, + val avatarUrl: String? = null, + val phone: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/CheckSessionUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/CheckSessionUseCase.kt new file mode 100644 index 0000000..371becd --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/CheckSessionUseCase.kt @@ -0,0 +1,15 @@ +package com.prodhack.moscow2025.domain.usecase.auth + +import com.prodhack.moscow2025.domain.interfaces.AuthRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import org.koin.core.annotation.Single + +@Single +class CheckSessionUseCase( + private val authRepository: AuthRepository +) { + operator suspend fun invoke(): Boolean { + return authRepository.fetchLoginState().firstOrNull() == true + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/GetUserUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/GetUserUseCase.kt new file mode 100644 index 0000000..f5d98ee --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/GetUserUseCase.kt @@ -0,0 +1,12 @@ +package com.prodhack.moscow2025.domain.usecase.auth + +import com.prodhack.moscow2025.domain.models.User +import com.prodhack.moscow2025.domain.interfaces.UserRepository +import org.koin.core.annotation.Single + +@Single +class GetUserUseCase( + private val userRepository: UserRepository +) { + suspend operator fun invoke(): Result = userRepository.fetchProfile() +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LogOutUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LogOutUseCase.kt new file mode 100644 index 0000000..4371f82 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LogOutUseCase.kt @@ -0,0 +1,16 @@ +package com.prodhack.moscow2025.domain.usecase.auth + +import com.prodhack.moscow2025.domain.interfaces.AuthRepository +import com.prodhack.moscow2025.domain.interfaces.UserRepository +import org.koin.core.annotation.Single + +@Single +class LogOutUseCase( + private val authRepository: AuthRepository, + private val userRepository: UserRepository +) { + suspend operator fun invoke() { + authRepository.clearLoginData() + userRepository.clearLocalUserData() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LoginUserUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LoginUserUseCase.kt new file mode 100644 index 0000000..34a97df --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/LoginUserUseCase.kt @@ -0,0 +1,14 @@ +package com.prodhack.moscow2025.domain.usecase.auth + +import com.prodhack.moscow2025.domain.models.LoginData +import com.prodhack.moscow2025.domain.interfaces.AuthRepository +import org.koin.core.annotation.Single + +@Single +class LoginUserUseCase( + private val authRepository: AuthRepository +) { + suspend operator fun invoke(data: LoginData): Result { + return authRepository.signInRequest(data) + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/RegisterUserUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/RegisterUserUseCase.kt new file mode 100644 index 0000000..3a0e301 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/RegisterUserUseCase.kt @@ -0,0 +1,14 @@ +package com.prodhack.moscow2025.domain.usecase.auth + +import com.prodhack.moscow2025.domain.models.RegisterData +import com.prodhack.moscow2025.domain.interfaces.AuthRepository +import org.koin.core.annotation.Single + +@Single +class RegisterUserUseCase( + private val authRepository: AuthRepository +) { + suspend operator fun invoke(data: RegisterData): Result { + return authRepository.signUpRequest(data) + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/UpdateUserUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/UpdateUserUseCase.kt new file mode 100644 index 0000000..a1b9d1c --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/UpdateUserUseCase.kt @@ -0,0 +1,15 @@ +package com.prodhack.moscow2025.domain.usecase.auth + +import com.prodhack.moscow2025.domain.models.UpdateUserData +import com.prodhack.moscow2025.domain.models.User +import com.prodhack.moscow2025.domain.interfaces.UserRepository +import org.koin.core.annotation.Single + +@Single +class UpdateUserUseCase( + private val userRepository: UserRepository +) { + suspend operator fun invoke(data: UpdateUserData): Result { + return userRepository.updateProfile(data) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateAuthFieldsUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateAuthFieldsUseCase.kt new file mode 100644 index 0000000..d176516 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/auth/ValidateAuthFieldsUseCase.kt @@ -0,0 +1,102 @@ +package com.prodhack.moscow2025.domain.usecase.auth + +import android.util.Patterns +import org.koin.core.annotation.Single + +enum class AuthField { + FirstName, + SecondName, + Email, + Password, + ConfirmPassword, + Phone +} + + +data class ValidationResult( + val errors: Map = emptyMap() +) { + val isValid: Boolean + get() = errors.isEmpty() +} + +@Single +class ValidateAuthFieldsUseCase { + + fun validateFillProfile( + displayName: String, + firstName: String, + lastName: String, + phone: String + ): ValidationResult { + val errors = buildMap { + if (displayName.isBlank()) put(AuthField.FirstName, "Введите никнейм") + if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя") + if (lastName.isBlank()) put(AuthField.SecondName, "Введите фамилию") + if (!isPhoneValid(phone)) put(AuthField.Phone, "Некорректный номер телефона") + } + return ValidationResult(errors) + } + + fun validateSignUp( + email: String, + password: String, + confirmPassword: String + ): ValidationResult { + val errors = buildMap { + if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email") + validatePassword(password)?.let { put(AuthField.Password, it) } + if (confirmPassword.isBlank()) put(AuthField.ConfirmPassword, "Повторите пароль") + + if (password != confirmPassword) { + put(AuthField.ConfirmPassword, "Пароли не совпадают") + } + } + return ValidationResult(errors) + } + + fun validatePassword(password: String): String? { + if (password.length < 8) { + return "Пароль должен быть не менее 8 символов" + } + if (!password.any { it.isUpperCase() }) { + return "Пароль должен содержать хотя бы одну заглавную букву" + } + if (!password.any { it.isDigit() }) { + return "Пароль должен содержать хотя бы одну цифру" + } + if (!password.any { !it.isLetterOrDigit() }) { + return "Пароль должен содержать хотя бы один специальный символ" + } + return null + } + + + fun validateLogin( + email: String, + password: String + ): ValidationResult { + val errors = buildMap { + if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email") + validatePassword(password)?.let { put(AuthField.Password, it) } + } + return ValidationResult(errors) + } + + fun validateProfile( + firstName: String, + secondName: String, + ): ValidationResult { + val errors = buildMap { + if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя") + if (secondName.isBlank()) put(AuthField.SecondName, "Введите фамилию") + } + return ValidationResult(errors) + } + + private fun isEmailValid(email: String): Boolean = + email.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(email).matches() + + private fun isPhoneValid(phone: String): Boolean = + phone.isNotBlank() && Patterns.PHONE.matcher(phone).matches() +} diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/utils/NetworkError.kt b/app/src/main/java/com/prodhack/moscow2025/domain/utils/NetworkError.kt new file mode 100644 index 0000000..7bf5034 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/utils/NetworkError.kt @@ -0,0 +1,38 @@ +package com.prodhack.moscow2025.domain.utils + +import com.prodhack.moscow2025.domain.utils.NetworkError.Connection +import com.prodhack.moscow2025.domain.utils.NetworkError.InputError +import com.prodhack.moscow2025.domain.utils.NetworkError.Unexpected +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.plugins.RedirectResponseException +import io.ktor.client.plugins.ServerResponseException + +/** + * Network error wrapper class + */ +sealed class NetworkError : Throwable() { + + /** + * Network connection error + */ + class Connection() : NetworkError() + + /** + * Unexpected error for example HTTP code - 500 or exception when mapping data + */ + class Unexpected(val error: String) : NetworkError() + + /** + * User input error - 400 codes + */ + class InputError(val error: String) : NetworkError() +} + +fun Throwable.convertToNetworkError() = + when (this) { + is NetworkError -> this + is RedirectResponseException -> Unexpected(error = message) + is ClientRequestException -> InputError(error = message) + is ServerResponseException -> Unexpected(error = message) + else -> Connection() + } \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/utils/TypeAliases.kt b/app/src/main/java/com/prodhack/moscow2025/domain/utils/TypeAliases.kt new file mode 100644 index 0000000..a7da9b0 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/utils/TypeAliases.kt @@ -0,0 +1,21 @@ +package com.prodhack.moscow2025.domain.utils + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow + +/** + * Simple wrapper for convenience of network requests in repositories + * + * @see Flow + * @see Result + * @see NetworkError + */ +internal typealias RemoteWrapper = Flow> + +/** + * Simple wrapper for convenience of network paging requests in repositories + * + * @see Flow + * @see PagingData + */ +internal typealias RemotePagingWrapper = Flow> \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/MainActivity.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/MainActivity.kt new file mode 100644 index 0000000..65a1e1b --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/MainActivity.kt @@ -0,0 +1,133 @@ +package com.prodhack.moscow2025.presentation + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.Text +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.core.content.ContextCompat +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.WindowCompat +import com.google.firebase.messaging.FirebaseMessaging +import com.prodhack.moscow2025.domain.usecase.auth.CheckSessionUseCase +import com.prodhack.moscow2025.presentation.navigation.AppDestination +import com.prodhack.moscow2025.presentation.navigation.TTasksApp +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.runBlocking +import org.koin.android.ext.android.inject +import kotlin.getValue + +class MainActivity : ComponentActivity() { + + private val checkSessionUseCase: CheckSessionUseCase by inject() + + private val sessionDestinationState = MutableStateFlow(null) + + override fun onCreate(savedInstanceState: Bundle?) { + val splashScreen = installSplashScreen() + var stateLoaded = false + splashScreen.setKeepOnScreenCondition { + stateLoaded.not() + } + super.onCreate(savedInstanceState) + enableEdgeToEdge() + WindowCompat.setDecorFitsSystemWindows(window, false) + + runBlocking { + val isAuthorized = try { + checkSessionUseCase() + } catch (e: Exception) { + false + } + sessionDestinationState.value = + if (isAuthorized) AppDestination.Main else AppDestination.Login + + stateLoaded = true + } + + setContent { + val sessionDestination by sessionDestinationState.collectAsState() + TTasksApp(sessionDestination = sessionDestination, context = this) + LaunchedEffect(Unit) { + requestPermissions( + arrayOf(Manifest.permission.ACCESS_NOTIFICATION_POLICY), 123 + ) + FirebaseMessaging.getInstance().token + .addOnCompleteListener { task -> + if (task.isSuccessful) { + val token = task.result + Log.d("TOKEN", token) + } + } + + checkAndRequestNotificationPermission() + } + } + } + + private fun checkAndRequestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + when { + ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED -> { + // Разрешение уже есть, получаем токен + getFCMToken() + } + + else -> { + // Запрашиваем разрешение + requestPermissions( + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + 123 + ) + } + } + } else { + // Для версий ниже Android 13 разрешение не требуется + getFCMToken() + } + } + + private fun getFCMToken() { + FirebaseMessaging.getInstance().token + .addOnCompleteListener { task -> + if (task.isSuccessful) { + val token = task.result + Log.d("TOKEN", token) + } else { + Log.e("TOKEN", "Failed to get token", task.exception) + } + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray, + deviceId: Int + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == 123) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + getFCMToken() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/BottomNavigation.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/BottomNavigation.kt new file mode 100644 index 0000000..6c4a7b7 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/BottomNavigation.kt @@ -0,0 +1,138 @@ +package com.prodhack.moscow2025.presentation.components + +import android.util.Log +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.prodhack.moscow2025.R +import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme +import com.prodhack.moscow2025.presentation.theme.Paddings +import com.prodhack.moscow2025.presentation.theme.Shapes +import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable + +@Composable +fun TBottomNavigation(modifier: Modifier = Modifier, selectedPage: Int, onSelect: (Int) -> Unit) { + Box( + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(vertical = Paddings.small), + contentAlignment = Alignment.Center + ) { + + val firstIconPos = remember { mutableFloatStateOf(0f) } + val secondIconPos = remember { mutableFloatStateOf(0f) } + val thirdIconPos = remember { mutableFloatStateOf(0f) } + + val indicatorOffset = + with(LocalDensity.current) { + when (selectedPage) { + 0 -> firstIconPos.floatValue - secondIconPos.floatValue + 1 -> 0f + 2 -> thirdIconPos.floatValue - secondIconPos.floatValue + else -> null + }?.toDp() + } + AnimatedVisibility(indicatorOffset != null) { + indicatorOffset?.let { + Box( + modifier = Modifier + .size(85.dp, 45.dp) + .offset(x = animateDpAsState(it).value) + .background( + MaterialTheme.colorScheme.primary, + shape = Shapes.smallRoundedBox + ) + ) + } + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { + Icon( + modifier = Modifier + .size(30.dp) + .onGloballyPositioned { + it.parentCoordinates?.positionInParent()?.let { + firstIconPos.floatValue = it.x + } + } + .noRippleClickable { + onSelect(0) + }, + painter = painterResource(R.drawable.ic_trips), + tint = animateColorAsState(if (selectedPage == 0) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant).value, + contentDescription = "open trips list screen" + ) + + Icon( + modifier = Modifier + .size(30.dp) + .onGloballyPositioned { + it.parentCoordinates?.positionInParent()?.let { + secondIconPos.floatValue = it.x + } + } + .noRippleClickable { + onSelect(1) + }, + painter = painterResource(R.drawable.ic_home), + tint = animateColorAsState(if (selectedPage == 1) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant).value, + contentDescription = "open tasks screen" + ) + + Icon( + modifier = Modifier + .size(30.dp) + .onGloballyPositioned { + it.parentCoordinates?.positionInParent()?.let { + thirdIconPos.floatValue = it.x + } + } + .noRippleClickable { + onSelect(2) + }, + painter = painterResource(R.drawable.ic_profile), + tint = animateColorAsState(if (selectedPage == 2) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant).value, + contentDescription = "open tasks screen" + ) + } + + } +} + +@Preview +@Composable +fun TBottomNavigationPreview() { + MoscowHackatonTemplateTheme { + Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Bottom) { + val page = remember { mutableIntStateOf(0) } + TBottomNavigation(selectedPage = page.intValue) { + Log.d("click", it.toString()) + page.intValue = it + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTBigButton.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTBigButton.kt new file mode 100644 index 0000000..d2283c5 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTBigButton.kt @@ -0,0 +1,84 @@ +package com.prodhack.moscow2025.presentation.components.standart + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.prodhack.moscow2025.presentation.theme.Shapes + +@Composable +fun BigButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + buttonText: String, + isLoading: Boolean +) { + val colorScheme = MaterialTheme.colorScheme + val typography = MaterialTheme.typography + Button( + modifier = modifier + .fillMaxWidth() + .height(60.dp), + shape = Shapes.smallRoundedBox, + onClick = onClick, + enabled = !isLoading, + colors = ButtonColors( + containerColor = colorScheme.onPrimary, + contentColor = colorScheme.primary, + disabledContainerColor = colorScheme.onPrimary, + disabledContentColor = colorScheme.primary + ) + ){ + if (isLoading) { + CircularProgressIndicator() + } else { + Text( + text = buttonText, + style = typography.labelMedium, + fontSize = 24.sp, + ) + } + } +} + +@Composable +fun MediumButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + buttonText: String, + isLoading: Boolean +) { + val colorScheme = MaterialTheme.colorScheme + val typography = MaterialTheme.typography + Button( + modifier = modifier + .fillMaxWidth() + .height(40.dp), + shape = Shapes.smallRoundedBox, + onClick = onClick, + enabled = !isLoading, + colors = ButtonColors( + containerColor = colorScheme.primary, + contentColor = colorScheme.onPrimary, + disabledContainerColor = colorScheme.primary, + disabledContentColor = colorScheme.onPrimary + ) + ){ + if (isLoading) { + CircularProgressIndicator() + } else { + Text( + text = buttonText, + style = typography.labelMedium, + fontSize = 16.sp, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTCheckBox.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTCheckBox.kt new file mode 100644 index 0000000..4804db0 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTCheckBox.kt @@ -0,0 +1,38 @@ +package com.prodhack.moscow2025.presentation.components.standart + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.prodhack.moscow2025.R +import com.prodhack.moscow2025.presentation.theme.Shapes + +@Composable +fun TCheckBox(modifier: Modifier = Modifier, checked: Boolean, color: Color) { + Box( + modifier = modifier + .background(Color.Transparent) + .border(width = 1.dp, color = color, shape = Shapes.verySmallRoundedBox) + ) { + AnimatedContent(checked) { + if (it) { + Icon( + modifier = Modifier + .fillMaxSize() + .padding(2.dp), + painter = painterResource(R.drawable.ic_checkmark), + tint = color, + contentDescription = "checkmark" + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTFloatingActionButton.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTFloatingActionButton.kt new file mode 100644 index 0000000..1c26fa0 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTFloatingActionButton.kt @@ -0,0 +1,53 @@ +package com.prodhack.moscow2025.presentation.components.standart + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.prodhack.moscow2025.R + +@Composable +fun TTFloatingActionButton( + modifier: Modifier, + onClick: () -> Unit, + text: String +) { + val colorScheme = MaterialTheme.colorScheme + val typography = MaterialTheme.typography + + ExtendedFloatingActionButton( + modifier = modifier, + onClick = { + onClick() + }, + shape = RoundedCornerShape(10.dp), + containerColor = colorScheme.tertiaryContainer, + contentColor = colorScheme.onTertiaryContainer, + elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 5.dp) + ) { + Row { + Text( + text = text, + style = typography.titleMedium, + fontSize = 16.sp + ) + Spacer(Modifier.width(10.dp)) + Icon( + painter = painterResource(R.drawable.add_square_outline), + contentDescription = null, + modifier = Modifier.size(22.dp) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTNamedTextField.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTNamedTextField.kt new file mode 100644 index 0000000..4cf3bdd --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTNamedTextField.kt @@ -0,0 +1,47 @@ +package com.prodhack.moscow2025.presentation.components.standart + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun TTNamedTextField( + name: String, + value: String, + onValueChange: (String) -> Unit, + error: String? = null, + singleLine: Boolean = true, + keyboardOptions: KeyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + onDone: (() -> Unit)? = null +) { + Column { + Text( + text = name, + style = typography.labelLarge, + fontSize = 14.sp, + color = Color.White + ) + Spacer(Modifier.height(5.dp)) + TTTextField( + value = value, + onValueChange = onValueChange, + error = error, + singleLine = singleLine, + keyboardOptions = keyboardOptions, + onDone = onDone + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTPasswordField.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTPasswordField.kt new file mode 100644 index 0000000..49100f4 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTPasswordField.kt @@ -0,0 +1,127 @@ +package com.prodhack.moscow2025.presentation.components.standart + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun TTPasswordField( + value: String, + onValueChange: (String) -> Unit, + label: String, + error: String? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + onDone: (() -> Unit)? = null +) { + val colorScheme = MaterialTheme.colorScheme + val typography = MaterialTheme.typography + var isVisible by remember { mutableStateOf(false) } + + Box( + Modifier.height(70.dp) + ) { + Box( + Modifier + .fillMaxWidth().height(56.dp) + .offset(x = 5.dp) + .background( + color = Color.White, + shape = RoundedCornerShape(15.dp) + ) + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth().offset(y = 5.dp), + value = value, + onValueChange = onValueChange, + textStyle = typography.labelLarge, + placeholder = { + Text( + label, + style = typography.labelLarge, + fontSize = 14.sp + ) + }, + isError = error != null, + supportingText = { + if (error != null) { + Text( + text = error, + style = typography.labelLarge, + fontSize = 12.sp + ) + } + }, + keyboardOptions = keyboardOptions, + keyboardActions = KeyboardActions( + onDone = { + onDone?.invoke() + } + ), + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = colorScheme.primary, + unfocusedContainerColor = colorScheme.primary, + errorContainerColor = colorScheme.error, + + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + errorBorderColor = Color.Transparent, + + focusedPlaceholderColor = colorScheme.onPrimary, + unfocusedPlaceholderColor = colorScheme.onPrimary, + errorPlaceholderColor = colorScheme.onError, + + focusedTextColor = colorScheme.onPrimary, + unfocusedTextColor = colorScheme.onPrimary, + errorTextColor = colorScheme.onError, + + cursorColor = colorScheme.onPrimary, + errorCursorColor = colorScheme.onError + ), + shape = RoundedCornerShape(15.dp), + visualTransformation = if (isVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + val icon = if (isVisible) Icons.Filled.VisibilityOff else Icons.Filled.Visibility + IconButton(onClick = { isVisible = !isVisible }) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = colorScheme.onPrimary + ) + } + } + ) + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt new file mode 100644 index 0000000..fbacc62 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt @@ -0,0 +1,402 @@ +package com.prodhack.moscow2025.presentation.components.standart + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.prodhack.moscow2025.R +import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class) +@Composable +fun TTTextField( + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + value: String, + onValueChange: (String) -> Unit, + readOnly: Boolean = false, + label: String = "", + error: String? = null, + singleLine: Boolean = true, + maxLines: Int = 1, + keyboardOptions: KeyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + onDone: (() -> Unit)? = null, + trailingIcon: @Composable () -> Unit = {} +) { + val typography = MaterialTheme.typography + val colorScheme = MaterialTheme.colorScheme + + Box( + Modifier.height(70.dp), + ) { + Box( + Modifier + .fillMaxWidth() + .height(56.dp) + .offset(x = 5.dp) + .background( + color = Color.White, + shape = RoundedCornerShape(15.dp) + ) + ) + OutlinedTextField( + modifier = modifier + .fillMaxWidth() + .offset(y = 5.dp), + value = value, + readOnly = readOnly, + onValueChange = onValueChange, + textStyle = typography.labelLarge, + placeholder = { + Text( + label, + style = typography.labelLarge, + fontSize = 14.sp, + ) + }, + isError = error != null, + supportingText = { + if (error != null) { + Spacer(Modifier.height(5.dp)) + Text( + text = error, + style = typography.labelLarge, + fontSize = 12.sp + ) + } + }, + keyboardOptions = keyboardOptions, + keyboardActions = KeyboardActions( + onDone = { + onDone?.invoke() + } + ), + singleLine = singleLine, + maxLines = maxLines, + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = colorScheme.primary, + unfocusedContainerColor = colorScheme.primary, + errorContainerColor = colorScheme.error, + + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + errorBorderColor = Color.Transparent, + + focusedPlaceholderColor = colorScheme.onPrimary, + unfocusedPlaceholderColor = colorScheme.onPrimary, + errorPlaceholderColor = colorScheme.onError, + + focusedTextColor = colorScheme.onPrimary, + unfocusedTextColor = colorScheme.onPrimary, + errorTextColor = colorScheme.onError, + + cursorColor = colorScheme.onPrimary, + errorCursorColor = colorScheme.onError + ), + shape = RoundedCornerShape(15.dp), + trailingIcon = trailingIcon + ) + + if (readOnly && onClick != null) { + Box( + modifier = Modifier + .fillMaxSize() + .noRippleClickable(onClick) + ) + } + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TTTextFieldWithDropdown( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit = {}, + readOnly: Boolean = true, + label: String, + error: String? = null, + singleLine: Boolean = true, + maxLines: Int = 1, + keyboardOptions: KeyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + dropdownItems: List = emptyList(), + onDropdownItemSelected: (T) -> Unit = {}, + dropDownItem: @Composable (T) -> Unit, + trailingIcon: @Composable (Boolean) -> Unit = { + Icon( + modifier = Modifier + .size(24.dp) + .rotate(animateFloatAsState(if (it) 180f else 0f).value), + painter = painterResource(R.drawable.ic_arr_dropdown), + tint = MaterialTheme.colorScheme.onPrimary, + contentDescription = null + ) + } +) { + val typography = MaterialTheme.typography + val colorScheme = MaterialTheme.colorScheme + + var expanded by remember { mutableStateOf(false) } + + Box( + modifier.height(70.dp), + ) { + Box( + Modifier + .fillMaxWidth() + .height(56.dp) + .offset(x = 5.dp) + .background( + color = Color.White, + shape = RoundedCornerShape(15.dp) + ) + ) + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + modifier = Modifier.offset(y = 5.dp) + ) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable), + value = value, + readOnly = readOnly, + onValueChange = onValueChange, + textStyle = typography.labelLarge, + placeholder = { + Text( + label, + style = typography.labelLarge, + fontSize = 14.sp, + ) + }, + isError = error != null, + supportingText = { + if (error != null) { + Spacer(Modifier.height(5.dp)) + Text( + text = error, + style = typography.labelLarge, + fontSize = 12.sp + ) + } + }, + keyboardOptions = keyboardOptions, + singleLine = singleLine, + maxLines = maxLines, + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = colorScheme.primary, + unfocusedContainerColor = colorScheme.primary, + errorContainerColor = colorScheme.error, + + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + errorBorderColor = Color.Transparent, + + focusedPlaceholderColor = colorScheme.onPrimary, + unfocusedPlaceholderColor = colorScheme.onPrimary, + errorPlaceholderColor = colorScheme.onError, + + focusedTextColor = colorScheme.onPrimary, + unfocusedTextColor = colorScheme.onPrimary, + errorTextColor = colorScheme.onError, + + cursorColor = colorScheme.onPrimary, + errorCursorColor = colorScheme.onError + ), + shape = RoundedCornerShape(15.dp), + trailingIcon = { + trailingIcon(expanded) + } + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.exposedDropdownSize() + ) { + if (dropdownItems.isEmpty()) { + DropdownMenuItem( + text = { + Text("Здесь пока ничего нет", style = typography.titleMedium) + }, + onClick = { + expanded = false + } + ) + } + dropdownItems.forEach { item -> + DropdownMenuItem( + text = { + dropDownItem(item) + }, + onClick = { + onDropdownItemSelected(item) + expanded = false + } + ) + } + } + } + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TTTextFieldWithSearch( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit = {}, + readOnly: Boolean = true, + label: String, + error: String? = null, + singleLine: Boolean = true, + maxLines: Int = 1, + keyboardOptions: KeyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + dropdownItems: List = emptyList(), + onDropdownItemSelected: (T) -> Unit = {}, + dropDownItem: @Composable (T) -> Unit, + trailingIcon: @Composable (Boolean) -> Unit = {} +) { + val typography = MaterialTheme.typography + val colorScheme = MaterialTheme.colorScheme + + var expanded by remember { mutableStateOf(false) } + + Box( + modifier.height(70.dp), + ) { + Box( + Modifier + .fillMaxWidth() + .height(56.dp) + .offset(x = 5.dp) + .background( + color = Color.White, + shape = RoundedCornerShape(15.dp) + ) + ) + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + modifier = Modifier.offset(y = 5.dp) + ) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable), + value = value, + readOnly = readOnly, + onValueChange = onValueChange, + textStyle = typography.labelLarge, + placeholder = { + Text( + label, + style = typography.labelLarge, + fontSize = 14.sp, + ) + }, + isError = error != null, + supportingText = { + if (error != null) { + Spacer(Modifier.height(5.dp)) + Text( + text = error, + style = typography.labelLarge, + fontSize = 12.sp + ) + } + }, + keyboardOptions = keyboardOptions, + singleLine = singleLine, + maxLines = maxLines, + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = colorScheme.primary, + unfocusedContainerColor = colorScheme.primary, + errorContainerColor = colorScheme.error, + + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + errorBorderColor = Color.Transparent, + + focusedPlaceholderColor = colorScheme.onPrimary, + unfocusedPlaceholderColor = colorScheme.onPrimary, + errorPlaceholderColor = colorScheme.onError, + + focusedTextColor = colorScheme.onPrimary, + unfocusedTextColor = colorScheme.onPrimary, + errorTextColor = colorScheme.onError, + + cursorColor = colorScheme.onPrimary, + errorCursorColor = colorScheme.onError + ), + shape = RoundedCornerShape(15.dp), + trailingIcon = { + trailingIcon(expanded) + } + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.exposedDropdownSize() + ) { + dropdownItems.forEach { item -> + DropdownMenuItem( + text = { + dropDownItem(item) + }, + onClick = { + onDropdownItemSelected(item) + expanded = false + } + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTopLogo.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTopLogo.kt new file mode 100644 index 0000000..d1c6b29 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTopLogo.kt @@ -0,0 +1,44 @@ +package com.prodhack.moscow2025.presentation.components.standart + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.prodhack.moscow2025.R +import com.prodhack.moscow2025.presentation.theme.Paddings + +@Composable +fun TopLogo( + modifier: Modifier = Modifier +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + modifier = Modifier.size(100.dp), + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = "App logo" + ) + + Spacer(modifier = Modifier.width(Paddings.medium)) + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.titleLarge, + fontSize = 48.sp + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt new file mode 100644 index 0000000..cd12537 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt @@ -0,0 +1,18 @@ +package com.prodhack.moscow2025.presentation.navigation + +/** + * Centralized list of application destinations. + * + * Keeping the routes in one place helps to avoid + * string duplication and makes refactoring safer. + */ +sealed class AppDestination(val route: String) { + data object Login : AppDestination("app/login") + data object Register : AppDestination("app/register") + + data object Main : AppDestination("app/main") + + + data object Profile : AppDestination("app/profile") + +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksApp.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksApp.kt new file mode 100644 index 0000000..1bf1391 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksApp.kt @@ -0,0 +1,99 @@ +package com.prodhack.moscow2025.presentation.navigation + +import android.content.Context +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.navigation.compose.currentBackStackEntryAsState +import com.prodhack.moscow2025.presentation.components.TBottomNavigation +import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme + +@Composable +fun TTasksApp( + appState: TTasksAppState = rememberTTasksAppState(), + context: Context, + sessionDestination: AppDestination? = null +) { + MoscowHackatonTemplateTheme() { + val snackbarHostState = remember { SnackbarHostState() } + val bottomBarState = remember { mutableStateOf(null) } + + when (appState.navController.currentBackStackEntryAsState().value?.destination?.route) { + AppDestination.Login.route -> { + bottomBarState.value = null + } + + AppDestination.Register.route -> { + bottomBarState.value = null + } + + AppDestination.Main.route -> { + bottomBarState.value = 1 + } + + AppDestination.Profile.route -> { + bottomBarState.value = 2 + } + } + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost( + hostState = snackbarHostState, + snackbar = { data -> + Snackbar( + snackbarData = data, + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + shape = MaterialTheme.shapes.medium + ) + } + ) + }, + bottomBar = { + bottomBarState.value?.let { bbState -> + TBottomNavigation( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceContainer) + .windowInsetsPadding(WindowInsets.navigationBars), + selectedPage = bbState + ) { newPage -> + when (newPage) { + 0 -> { + TODO() + } + + 1 -> { + appState.navController.navigate(AppDestination.Main.route) + } + + 2 -> { + appState.navController.navigate(AppDestination.Profile.route) + } + } + } + } + }, + ) { padding -> + TTasksNavHost( + navController = appState.navController, + modifier = Modifier.padding(padding), + sessionDestination = sessionDestination, + snackbarHostState = snackbarHostState, + context = context + ) + } + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksAppState.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksAppState.kt new file mode 100644 index 0000000..2e62727 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksAppState.kt @@ -0,0 +1,40 @@ +package com.prodhack.moscow2025.presentation.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.navigation.NavDestination +import androidx.navigation.NavHostController +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.CoroutineScope + +@Stable +class TTasksAppState( + val navController: NavHostController, + val coroutineScope: CoroutineScope +) { + val currentDestination: NavDestination? + get() = navController.currentDestination + + fun navigateTo( + destination: AppDestination, + builder: NavOptionsBuilder.() -> Unit = {} + ) { + navController.navigate(destination.route, builder) + } + + fun navigateBack(): Boolean = navController.popBackStack() +} + +@Composable +fun rememberTTasksAppState( + navController: NavHostController = rememberNavController(), + coroutineScope: CoroutineScope = rememberCoroutineScope() +): TTasksAppState = remember(navController, coroutineScope) { + TTasksAppState( + navController = navController, + coroutineScope = coroutineScope + ) +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt new file mode 100644 index 0000000..9f75f4a --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt @@ -0,0 +1,80 @@ +package com.prodhack.moscow2025.presentation.navigation + +import android.content.Context +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.prodhack.moscow2025.presentation.screens.main.MainScreen +import com.prodhack.moscow2025.domain.utils.NetworkError +import com.prodhack.moscow2025.presentation.screens.login.LoginScreen +import com.prodhack.moscow2025.presentation.screens.register.RegisterScreen +import com.prodhack.moscow2025.presentation.utils.ErrorCallbacks +import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope +import org.koin.compose.viewmodel.koinActivityViewModel + +@Composable +fun TTasksNavHost( + navController: NavHostController, + modifier: Modifier = Modifier, + sessionDestination: AppDestination? = null, + context: Context, + snackbarHostState: SnackbarHostState +) { + val startDestination = sessionDestination?.route ?: AppDestination.Login.route + + ErrorCollectorScope(context, navController, object : ErrorCallbacks { + override fun processConnectionError(networkError: NetworkError.Connection) { + + } + + override fun processUnexpectedError(networkError: NetworkError.Unexpected) { + + } + + }) { + NavHost( + navController = navController, + startDestination = startDestination, + modifier = modifier + ) { + composable(AppDestination.Login.route) { + LoginScreen( + snackbarHostState = snackbarHostState, + onRegisterClick = { + navController.navigate(AppDestination.Register.route) + }, + onSuccess = { + navController.navigate(AppDestination.Main.route) { + popUpTo(AppDestination.Login.route) { + inclusive = true + } + } + } + ) + } + + composable(AppDestination.Register.route) { + RegisterScreen( + snackbarHostState = snackbarHostState, + onLoginClick = { + navController.popBackStack() + }, + onSuccess = { + navController.navigate(AppDestination.Main.route) { + popUpTo(AppDestination.Register.route) { + inclusive = true + } + } + } + ) + } + + composable(AppDestination.Main.route) { + MainScreen() + } + } + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt new file mode 100644 index 0000000..fee1d41 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileScreen.kt @@ -0,0 +1,9 @@ +package com.prodhack.moscow2025.presentation.screens.fillProfile + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable + +@Composable +fun FillProfileScreen() { + Text("Fill profile will be here soon :)") +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileViewModel.kt new file mode 100644 index 0000000..df97251 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/fillProfile/FillProfileViewModel.kt @@ -0,0 +1,185 @@ +package com.prodhack.moscow2025.presentation.screens.fillProfile + +import android.content.ContentUris +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.net.Uri +import android.provider.MediaStore +import androidx.lifecycle.viewModelScope +import androidx.paging.map +import coil.ImageLoader +import coil.request.ImageRequest +import com.prodhack.moscow2025.domain.interfaces.GalleryRepository +import com.prodhack.moscow2025.domain.models.UpdateUserData +import com.prodhack.moscow2025.domain.usecase.auth.AuthField +import com.prodhack.moscow2025.domain.usecase.auth.UpdateUserUseCase +import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase +import com.prodhack.moscow2025.presentation.utils.UIState +import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel +import com.prodhack.moscow2025.presentation.utils.toByteArray +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +data class FillProfileFormState( + val displayName: String = "", + val firstName: String = "", + val lastName: String = "", + val phone: String = "", + val avatar: ByteArray? = null, + val errors: Map = emptyMap() +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FillProfileFormState + + if (displayName != other.displayName) return false + if (firstName != other.firstName) return false + if (lastName != other.lastName) return false + if (phone != other.phone) return false + if (!avatar.contentEquals(other.avatar)) return false + if (errors != other.errors) return false + + return true + } + + override fun hashCode(): Int { + var result = displayName.hashCode() + result = 31 * result + firstName.hashCode() + result = 31 * result + lastName.hashCode() + result = 31 * result + phone.hashCode() + result = 31 * result + (avatar?.contentHashCode() ?: 0) + result = 31 * result + errors.hashCode() + return result + } +} + +class FillProfileViewModel( + private val updateUserUseCase: UpdateUserUseCase, + private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase, + private val galleryRepository: GalleryRepository +) : BaseViewModel() { + private val _formStateFillProfile = MutableStateFlow(FillProfileFormState()) + val formStateSignUp: StateFlow = _formStateFillProfile + + + private val _profileFillState = MutableUIStateFlow() + val profileFillState: StateFlow> = _profileFillState + + + fun onDisplayNameChange(value: String) { + _formStateFillProfile.update { + it.copy( + displayName = value, + errors = it.errors - AuthField.Email + ) + } + } + + fun onFirstNameChange(value: String) { + _formStateFillProfile.update { + it.copy( + firstName = value, + errors = it.errors - AuthField.Email + ) + } + } + + fun onLastNameChange(value: String) { + _formStateFillProfile.update { + it.copy( + lastName = value, + errors = it.errors - AuthField.Email + ) + } + } + + fun onPhoneChange(value: String) { + _formStateFillProfile.update { + it.copy( + phone = value, + errors = it.errors - AuthField.Email + ) + } + } + + + val galleryItems = galleryRepository.getImagesIds().map { + it.map { id -> + ContentUris.withAppendedId( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + id + ) + } + } + + fun post(context: Context) { + viewModelScope.launch { + post( + (ImageLoader(context).execute( + ImageRequest.Builder(context) + .data(currentPhoto).build() + ).drawable as BitmapDrawable).bitmap + ) + } + } + + fun post(bitmap: Bitmap) { + viewModelScope.launch { + _formStateFillProfile.update { + it.copy( + avatar = bitmap.toByteArray() + ) + } + } + } + + fun clearAvatar() { + viewModelScope.launch { + _formStateFillProfile.update { + it.copy( + avatar = null + ) + } + } + } + + var currentPhoto: Uri? = null + + fun selectImage(photo: Uri) { + currentPhoto = photo + } + + fun submit() { + viewModelScope.launch { + val validation = validateAuthFieldsUseCase.validateFillProfile( + displayName = _formStateFillProfile.value.displayName, + firstName = _formStateFillProfile.value.firstName, + lastName = _formStateFillProfile.value.lastName, + phone = _formStateFillProfile.value.phone + ) + + if (!validation.isValid) { + _formStateFillProfile.update { it.copy(errors = validation.errors) } + return@launch + } + + _profileFillState.emit(UIState.Loading()) + + val result = updateUserUseCase( + UpdateUserData( + displayName = _formStateFillProfile.value.displayName, + firstName = _formStateFillProfile.value.firstName, + lastName = _formStateFillProfile.value.lastName, + phone = _formStateFillProfile.value.phone + ) + ) + result.map { it.id }.collectRequest(_profileFillState) + } + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt new file mode 100644 index 0000000..e7f1dc1 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginScreen.kt @@ -0,0 +1,206 @@ +package com.prodhack.moscow2025.presentation.screens.login + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import com.prodhack.moscow2025.R +import com.prodhack.moscow2025.domain.usecase.auth.AuthField +import com.prodhack.moscow2025.presentation.components.standart.BigButton +import com.prodhack.moscow2025.presentation.components.standart.TTPasswordField +import com.prodhack.moscow2025.presentation.components.standart.TTTextField +import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope +import com.prodhack.moscow2025.presentation.utils.UIState +import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable +import org.koin.androidx.compose.koinViewModel + +@Composable +fun ErrorCollectorScope.LoginScreen( + modifier: Modifier = Modifier, + snackbarHostState: SnackbarHostState, + onRegisterClick: () -> Unit, + onSuccess: () -> Unit, + viewModel: LoginViewModel = koinViewModel() +) { + + val showDialog = remember { mutableStateOf(false) } + + val testCreds = listOf( + Pair("user1@mail.ru", "qQW!!!.rty3nqc18123"), + Pair("user2@mail.ru", "qQW!!!.rty3nqc18123"), + Pair("user3@mail.ru", "qQW!!!.rty3nqc18123"), + Pair("user4@mail.ru", "qQW!!!.rty3nqc18123"), + Pair("user5@mail.ru", "qQW!!!.rty3nqc18123") + ) + + val typography = MaterialTheme.typography + val colorScheme = MaterialTheme.colorScheme + + val formState by viewModel.formState.collectAsState() + + var errorText by remember { mutableStateOf("") } + + val authState by viewModel.authState.collectAsStateWithCallbacks( + onInputError = { + errorText = it.error + }, + onConnectionError = { + errorText = "Нет подключения к сети" + }, + onUnexpectedError = { + errorText = it.error + }, + onLoading = { + errorText = "" + }, + onSuccess = { + errorText = "" + } + ) + + LaunchedEffect(authState) { + if (authState is UIState.Success) { + onSuccess() + } + } + + LaunchedEffect(errorText) { + if (errorText.isNotEmpty()) { + snackbarHostState.showSnackbar( + message = "Ошибка: $errorText", + duration = SnackbarDuration.Short + ) + } + } + Box( + modifier = modifier + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + contentAlignment = Alignment.BottomStart + ) { + Image( + painter = painterResource(R.drawable.lottie), + contentDescription = null, + modifier = Modifier + .width(130.dp), + contentScale = ContentScale.Crop + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 30.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier + .size(250.dp) + .noRippleClickable { + showDialog.value = true + } + ) + Spacer(Modifier.height(10.dp)) + Text( + text = "Вход", + style = MaterialTheme.typography.titleLarge, + fontSize = 40.sp + ) + Spacer(modifier = Modifier.height(10.dp)) + TTTextField( + value = formState.email, + onValueChange = viewModel::onEmailChange, + label = "Ваш email", + error = formState.errors[AuthField.Email] + ) + Spacer(Modifier.height(12.dp)) + TTPasswordField( + value = formState.password, + onValueChange = viewModel::onPasswordChange, + label = "Пароль", + error = formState.errors[AuthField.Password], + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + onDone = viewModel::submit + ) + Spacer(modifier = Modifier.height(40.dp)) + BigButton( + onClick = viewModel::submit, + modifier = Modifier.fillMaxWidth(), + buttonText = "Войти", + isLoading = authState is UIState.Loading + ) + Spacer(modifier = Modifier.height(20.dp)) + TextButton( + onClick = onRegisterClick, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Зарегистрироваться", + style = typography.labelMedium, + color = colorScheme.onBackground, + fontSize = 24.sp + ) + } + Spacer(Modifier.height(80.dp)) + } + if (showDialog.value) { + Dialog( + onDismissRequest = { + showDialog.value = false + } + ) { + Column { + testCreds.forEach { + Button(onClick = { + viewModel.onEmailChange(it.first) + viewModel.onPasswordChange(it.second) + viewModel.submit() + }) { + Text(it.first) + } + } + } + } + } + } + +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginViewModel.kt new file mode 100644 index 0000000..a75cd2c --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/login/LoginViewModel.kt @@ -0,0 +1,64 @@ +package com.prodhack.moscow2025.presentation.screens.login + +import androidx.lifecycle.viewModelScope +import com.prodhack.moscow2025.domain.models.LoginData +import com.prodhack.moscow2025.domain.usecase.auth.AuthField +import com.prodhack.moscow2025.domain.usecase.auth.LoginUserUseCase +import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase +import com.prodhack.moscow2025.presentation.utils.UIState +import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel + +data class LoginFormState( + val email: String = "", + val password: String = "", + val errors: Map = emptyMap() +) + +@KoinViewModel +class LoginViewModel( + private val loginUserUseCase: LoginUserUseCase, + private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase +) : BaseViewModel() { + + private val _formState = MutableStateFlow(LoginFormState()) + val formState: StateFlow = _formState + + private val _authState = MutableUIStateFlow() + val authState: StateFlow> = _authState + + fun onEmailChange(value: String) { + _formState.update { it.copy(email = value, errors = it.errors - AuthField.Email) } + } + + fun onPasswordChange(value: String) { + _formState.update { it.copy(password = value, errors = it.errors - AuthField.Password) } + } + + fun submit() { + viewModelScope.launch { + val validation = validateAuthFieldsUseCase.validateLogin( + email = _formState.value.email, + password = _formState.value.password + ) + if (!validation.isValid) { + _formState.update { it.copy(errors = validation.errors) } + return@launch + } + + _authState.emit(UIState.Loading()) + + val result = loginUserUseCase( + LoginData( + email = _formState.value.email, + password = _formState.value.password + ) + ) + result.collectRequest(_authState) + } + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt new file mode 100644 index 0000000..e7f84d3 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt @@ -0,0 +1,282 @@ +package com.prodhack.moscow2025.presentation.screens.main + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope +import org.koin.androidx.compose.koinViewModel + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ErrorCollectorScope.MainScreen( + modifier: Modifier = Modifier, + viewModel: MainScreenViewModel = koinViewModel() +) { + Text("Main screen will be here soon") +// val openCalendarModal = remember { mutableStateOf(false) } +// val openTaskAddSheet = remember { mutableStateOf(false) } +// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) +// val tasks = viewModel.taskList.collectAsLazyPagingItems() +// +// val selectedTask = remember { mutableStateOf(null) } +// +// Box( +// modifier = modifier +// .fillMaxSize() +// .padding(horizontal = Paddings.large), +// contentAlignment = Alignment.BottomCenter +// ) { +// Column( +// modifier = Modifier.fillMaxSize(), +// horizontalAlignment = Alignment.CenterHorizontally +// ) { +// Spacer(modifier = Modifier.height(Paddings.large)) +// TopLogo() +// Spacer(modifier = Modifier.height(Paddings.large)) +// +// MainScreenFilters(viewModel = viewModel) { +// openCalendarModal.value = true +// } +// +// Spacer(modifier = Modifier.height(Paddings.large)) +// +// viewModel.topicList.FoldUIStateWithGlobalCallbacks { topics -> +// BubbledCategoryFilters( +// categories = topics, +// selectedItemId = viewModel.selectedTopicId.value ?: -1 +// ) { categoryId -> +// viewModel.selectTopic(categoryId) +// } +// } +// Spacer(modifier = Modifier.height(Paddings.large)) +// +// if (tasks.loadState.hasError) { +// Text( +// "Упс, кажется что-то пошло не так :(\nНо мы уже со всем разбираемся!", +// style = Typography.titleMedium, +// textAlign = TextAlign.Center, +// fontSize = 18.sp, +// color = MaterialTheme.colorScheme.error +// ) +// } else if (tasks.loadState.append.endOfPaginationReached && tasks.itemCount == 0) { +// Spacer(modifier = Modifier.weight(1f)) +// +// Text( +// "Сделал дело - гуляй смело\nСамое время запланировать новую поездку", +// style = Typography.titleMedium, +// textAlign = TextAlign.Center, +// fontSize = 18.sp, +// color = MaterialTheme.colorScheme.onBackground +// ) +// Spacer(modifier = Modifier.height(Paddings.large)) +// BigButton(buttonText = "Начать", onClick = { +// +// }, isLoading = false) +// +// Spacer(modifier = Modifier.weight(3f)) +// +// } else { +// LazyColumn( +// verticalArrangement = Arrangement.spacedBy(Paddings.small), +// horizontalAlignment = Alignment.CenterHorizontally +// ) { +// items(tasks.itemCount) { it -> +// val task = tasks[it] +// task?.let { +// TaskCard( +// onClick = { +// selectedTask.value = it +// }, +// taskInfo = it, +// isArchiveWaiting = it.id in viewModel.archiveWaitingTasksIds.value +// ) { +// viewModel.toggleTaskAsDone( +// tripId = it.tripId, +// taskId = it.id, +// currState = it.archived +// ) +// tasks.refresh() +// } +// } +// } +// +// item { +// if (!tasks.loadState.append.endOfPaginationReached) { +// CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) +// } +// } +// } +// } +// } +// +// TTFloatingActionButton( +// modifier = Modifier +// .align(Alignment.BottomCenter) +// .padding(bottom = Paddings.medium), +// onClick = { +// openTaskAddSheet.value = true +// }, +// text = "Добавить задачу" +// ) +// } +// +// +// AnimatedVisibility(openCalendarModal.value) { +// DateRangePickerModal({ +// Log.d("DatePicker", it.toString()) +// if (it.first != null && it.second != null) { +// viewModel.setDate(Pair(it.first!!, it.second!!)) +// openCalendarModal.value = false +// } +// }) { +// openCalendarModal.value = false +// } +// } +// +// if (openTaskAddSheet.value) { +// AddTaskBottomSheet( +// sheetState = sheetState, +// onDismiss = { +// openTaskAddSheet.value = false +// } +// ) +// } +// +// val cs = MaterialTheme.colorScheme +// +// val viewSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) +// +// if (selectedTask.value != null) { +// +// val openCalendarModal2 = remember { mutableStateOf(false) } +// +// ModalBottomSheet( +// onDismissRequest = { +// selectedTask.value = null +// }, +// sheetState = viewSheetState, +// dragHandle = {}, +// shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp) +// ) { +// Column( +// modifier = Modifier +// .padding(horizontal = 24.dp, vertical = 16.dp) +// .verticalScroll(rememberScrollState()), +// horizontalAlignment = Alignment.CenterHorizontally +// ) { +// Text( +// text = "Просмотр задачи", +// color = cs.onSurface, +// style = Typography.titleMedium, +// fontSize = 22.sp, +// textAlign = TextAlign.Center, +// modifier = Modifier +// .fillMaxWidth() +// .padding(bottom = 24.dp, top = 8.dp) +// ) +// +// Spacer(modifier = Modifier.height(Paddings.medium)) +// +// Text( +// text = selectedTask.value!!.name, +// color = cs.onSurface, +// style = Typography.titleMedium, +// fontSize = 20.sp, +// textAlign = TextAlign.Center, +// modifier = Modifier +// .fillMaxWidth() +// .padding(bottom = 24.dp, top = 8.dp) +// ) +// +// Spacer(modifier = Modifier.height(Paddings.medium)) +// +// +// Text( +// text = "Что нужно сделать", +// color = cs.onSurface, +// style = Typography.titleMedium, +// fontSize = 18.sp, +// modifier = Modifier +// .fillMaxWidth() +// .padding(bottom = 24.dp, top = 8.dp) +// ) +// +// Spacer(modifier = Modifier.height(Paddings.small)) +// +// Text( +// text = selectedTask.value!!.whatNeedToDo, +// color = cs.onSurface, +// style = Typography.labelLarge, +// fontSize = 16.sp, +// modifier = Modifier +// .fillMaxWidth() +// .padding(bottom = 24.dp, top = 8.dp) +// ) +// +// Spacer(modifier = Modifier.height(Paddings.medium)) +// +// Text( +// text = "Для чего", +// color = cs.onSurface, +// style = Typography.titleMedium, +// fontSize = 18.sp, +// modifier = Modifier +// .fillMaxWidth() +// .padding(bottom = 24.dp, top = 8.dp) +// ) +// +// Spacer(modifier = Modifier.height(Paddings.small)) +// +// Text( +// text = selectedTask.value!!.reason, +// color = cs.onSurface, +// style = Typography.labelLarge, +// fontSize = 16.sp, +// modifier = Modifier +// .fillMaxWidth() +// .padding(bottom = 24.dp, top = 8.dp) +// ) +// +// Spacer(modifier = Modifier.height(Paddings.large)) +// +// TTTextField( +// onClick = { +// openCalendarModal2.value = true +// }, +// value = timestampToDateWithYear(selectedTask.value!!.deadline), +// readOnly = true, +// onValueChange = {}, +// label = "Дедлайн", +// trailingIcon = { +// Icon( +// modifier = Modifier +// .size(24.dp), +// painter = painterResource( +// R.drawable.ic_calendar +// ), +// tint = MaterialTheme.colorScheme.onPrimary, +// contentDescription = null +// ) +// } +// ) +// } +// } +// +// AnimatedVisibility(openCalendarModal2.value) { +// DatePickerModal({ +// Log.d("DatePicker", it.toString()) +// it?.let { date -> +// viewModel.changeTaskDeadline(selectedTask.value, date) +// selectedTask.value = null +// openCalendarModal.value = false +// } +// }) { +// openCalendarModal.value = false +// } +// } +// } +} + + diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt new file mode 100644 index 0000000..adbc27e --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreenViewModel.kt @@ -0,0 +1,143 @@ +package com.prodhack.moscow2025.presentation.screens.main + +import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel +import org.koin.android.annotation.KoinViewModel + + +@KoinViewModel +class MainScreenViewModel( +// private val loadTasksUseCase: LoadTasksUseCase, +// private val loadTasksTopicsListUseCase: LoadTasksTopicListUseCase, +// private val setFinishedStateToTaskUseCase: SetFinishedStateToTaskUseCase, +// private val changeDeadlineUseCase: ChangeDeadlineUseCase +) : BaseViewModel() { + +// var userChanged = false +// +// // Date filter +// private val defaultDateFilterState = +// getStartOfTodayTimestamp().let { Pair(it, it + 86400000) } +// +// +// private val dateState = +// mutableStateOf(defaultDateFilterState) +// +// val dateString = derivedStateOf { +// Log.d( +// "MainScreenViewModel", +// "deriving state , defaultDateFilterState - $defaultDateFilterState" +// ) +// when (dateState.value.first) { +// defaultDateFilterState.first -> "Сегодня" +// defaultDateFilterState.second -> "Завтра" +// else -> timestampToDate(dateState.value.first) +// } + "-" + +// when (dateState.value.second) { +// defaultDateFilterState.first -> "Сегодня" +// defaultDateFilterState.second -> "Завтра" +// else -> timestampToDate(dateState.value.second) +// } +// } +// +// fun setDate(dates: Pair) { +// userChanged = true +// dateState.value = +// Pair( +// convertGMTToSystemTimezone(dates.first), +// convertGMTToSystemTimezone(dates.second) +// ) +// +// Log.d("MainScreenViewModel", "updated dates ${dateState.value}") +// } +// +// // Other +// val onlyMyTasksState = mutableStateOf(true) +// +// val showFinished = mutableStateOf(false) +// +// // Topic filters +// +// val selectedTopicId = mutableStateOf(null) +// +// val topicList = MutableUIStateFlow>() +// +// fun loadTopicList() { +// loadTasksTopicsListUseCase().map { it -> it.map { it -> it.map { it.mapToUI() } } } +// .collectRequest(topicList) +// } +// +// fun selectTopic(id: Int) { +// if (selectedTopicId.value == id) { +// selectedTopicId.value = null +// } else { +// selectedTopicId.value = id +// } +// } +// +// // Tasks +// @OptIn(ExperimentalCoroutinesApi::class) +// val taskList = snapshotFlow { +// val dates = dateState.value +// TaskFilters( +// dateStart = dates.first, +// dateEnd = dates.second, +// topicId = selectedTopicId.value, +// onlySelf = onlyMyTasksState.value, +// showArchived = showFinished.value +// ) +// }.flatMapLatest { +// loadTasksUseCase(it) +// }.map { it -> it.map { it.mapToUI() } } +// +// private val archiveWaitingTaskJobs = mutableStateMapOf() +// +// val archiveWaitingTasksIds = derivedStateOf { archiveWaitingTaskJobs.keys } +// +// fun toggleTaskAsDone(tripId: Long, taskId: Long, currState: Boolean) { +// if (currState) { +// viewModelScope.launch { +// setFinishedStateToTaskUseCase( +// tripId = tripId, +// taskId = taskId, +// finishedState = false +// ) +// } +// } else { +// if (taskId in archiveWaitingTasksIds.value) { +// archiveWaitingTaskJobs[taskId]?.let { job -> +// if (!job.isCompleted) { +// job.cancel() +// } +// } +// archiveWaitingTaskJobs.remove(taskId) +// } else { +// archiveWaitingTaskJobs[taskId] = viewModelScope.launch { +// delay(1000) +// setFinishedStateToTaskUseCase( +// tripId = tripId, +// taskId = taskId, +// finishedState = true +// ) +// }.also { +// it.start() +// } +// } +// } +// } +// +// fun update() { +// loadTopicList() +// } +// +// fun changeTaskDeadline(value: UITaskModel?, date: Long) { +// viewModelScope.launch { +// value?.let { +// changeDeadlineUseCase(value.tripId, value.id, date) +// } +// } +// } +// +// init { +// update() +// } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt new file mode 100644 index 0000000..1bf160a --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterScreen.kt @@ -0,0 +1,182 @@ +package com.prodhack.moscow2025.presentation.screens.register + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.prodhack.moscow2025.R +import com.prodhack.moscow2025.domain.usecase.auth.AuthField +import com.prodhack.moscow2025.domain.utils.NetworkError +import com.prodhack.moscow2025.presentation.components.standart.BigButton +import com.prodhack.moscow2025.presentation.components.standart.TTPasswordField +import com.prodhack.moscow2025.presentation.components.standart.TTTextField +import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope +import com.prodhack.moscow2025.presentation.utils.UIState +import org.koin.androidx.compose.koinViewModel + +@Composable +fun ErrorCollectorScope.RegisterScreen( + modifier: Modifier = Modifier, + snackbarHostState: SnackbarHostState, + onLoginClick: () -> Unit, + onSuccess: () -> Unit, + viewModel: RegisterViewModel = koinViewModel() +) { + val typography = MaterialTheme.typography + val colorScheme = MaterialTheme.colorScheme + + val formState by viewModel.formStateSignUp.collectAsState() + var errorText by remember { mutableStateOf("") } + val registerState by viewModel.registerState.collectAsStateWithCallbacks( + onInputError = { + errorText = it.error + }, + onConnectionError = { + errorText = "Нет подключения к сети" + }, + onUnexpectedError = { + errorText = it.error + }, + onLoading = { + errorText = "" + }, + onSuccess = { + errorText = "" + } + ) + + LaunchedEffect(registerState) { + if (registerState is UIState.Success) { + onSuccess() + } + } + + LaunchedEffect(errorText) { + if (errorText.isNotEmpty()) { + snackbarHostState.showSnackbar( + message = "Ошибка: $errorText", + duration = SnackbarDuration.Short + ) + } + } + + Box( + modifier = modifier + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + contentAlignment = Alignment.BottomStart + ) { + Image( + painter = painterResource(R.drawable.lottie), + contentDescription = null, + modifier = Modifier.width(130.dp), + contentScale = ContentScale.Crop + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(start = 30.dp, end = 30.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Давайте\nзнакомиться!", + style = typography.titleLarge, + fontSize = 31.sp + ) + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier.size(140.dp), + contentScale = ContentScale.Crop + ) + } + Spacer(Modifier.height(20.dp)) + TTTextField( + value = formState.email, + onValueChange = viewModel::onEmailChange, + label = "Ваш email", + error = formState.errors[AuthField.Email] + ) + Spacer(Modifier.height(12.dp)) + TTPasswordField( + value = formState.password, + onValueChange = viewModel::onPasswordChange, + label = "Пароль", + error = formState.errors[AuthField.Password] + ) + Spacer(Modifier.height(12.dp)) + TTPasswordField( + value = formState.confirmPassword, + onValueChange = viewModel::onConfirmPasswordChange, + label = "Повторите пароль", + error = formState.errors[AuthField.ConfirmPassword], + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + onDone = viewModel::submit + ) + Spacer(modifier = Modifier.height(20.dp)) + BigButton( + onClick = viewModel::submit, + modifier = Modifier.fillMaxWidth(), + buttonText = "Зарегистрироваться", + isLoading = registerState is UIState.Loading + ) + Spacer(modifier = Modifier.height(20.dp)) + TextButton( + onClick = onLoginClick, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Уже есть аккаунт?", + style = typography.labelMedium, + color = colorScheme.onBackground, + fontSize = 24.sp + ) + } + Spacer(Modifier.height(80.dp)) + } + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterViewModel.kt new file mode 100644 index 0000000..6f78027 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/register/RegisterViewModel.kt @@ -0,0 +1,110 @@ +package com.prodhack.moscow2025.presentation.screens.register + +import androidx.lifecycle.viewModelScope +import com.prodhack.moscow2025.domain.models.RegisterData +import com.prodhack.moscow2025.domain.usecase.auth.AuthField +import com.prodhack.moscow2025.domain.usecase.auth.RegisterUserUseCase +import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase +import com.prodhack.moscow2025.presentation.utils.UIState +import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel + +data class RegisterFormState( + val email: String = "", + val password: String = "", + val confirmPassword: String = "", + val errors: Map = emptyMap() +) + +@KoinViewModel +class RegisterViewModel( + private val registerUserUseCase: RegisterUserUseCase, + private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase +) : BaseViewModel() { + + private val _formStateSignUp = MutableStateFlow(RegisterFormState()) + val formStateSignUp: StateFlow = _formStateSignUp + + + private val _registerState = MutableUIStateFlow() + val registerState: StateFlow> = _registerState + + + fun onEmailChange(value: String) { + _formStateSignUp.update { it.copy(email = value, errors = it.errors - AuthField.Email) } + } + + fun onPasswordChange(value: String) { + _formStateSignUp.update { + it.copy( + password = value, + errors = it.errors - AuthField.Password + ) + } + } + + fun onConfirmPasswordChange(value: String) { + _formStateSignUp.update { + it.copy( + confirmPassword = value, + errors = it.errors - AuthField.ConfirmPassword + ) + } + } + + fun submit() { + viewModelScope.launch { + + val validation = validateAuthFieldsUseCase.validateSignUp( + email = _formStateSignUp.value.email, + password = _formStateSignUp.value.password, + confirmPassword = _formStateSignUp.value.confirmPassword + ) + + if (!validation.isValid) { + _formStateSignUp.update { it.copy(errors = validation.errors) } + return@launch + } + + _registerState.emit(UIState.Loading()) + + val result = registerUserUseCase( + RegisterData( + email = _formStateSignUp.value.email, + password = _formStateSignUp.value.password + ) + ) + result.collectRequest(_registerState) + +// val validation = validateAuthFieldsUseCase.validateRegister( +// firstName = _formStateSignUp.value.firstName, +// lastName = _formStateSignUp.value.lastName, +// email = _formStateSignUp.value.email, +// password = _formStateSignUp.value.password, +// confirmPassword = _formStateSignUp.value.confirmPassword, +// phone = _formStateSignUp.value.ph +// ) +// +// if (!validation.isValid) { +// _formStateSignUp.update { it.copy(errors = validation.errors) } +// return@launch +// } +// +// _registerState.emit(UIState.Loading()) +// +// val result = registerUserUseCase( +// RegisterData( +// firstName = _formStateSignUp.value.firstName, +// secondName = _formStateSignUp.value.lastName, +// email = _formStateSignUp.value.email, +// password = _formStateSignUp.value.password +// ) +// ) +// result.collectRequest(_registerState) + } + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Color.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Color.kt new file mode 100644 index 0000000..4dee16c --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Color.kt @@ -0,0 +1,103 @@ +package com.prodhack.moscow2025.presentation.theme + +import androidx.compose.ui.graphics.Color + +val WhitePrimary = Color(0xFF1b6b51) +val WhiteSurfaceTint = Color(0xFF1b6b51) +val WhiteOnPrimary = Color(0xFFFFFFFF) +val WhitePrimaryContainer = Color(0xFFa6f2d1) +val WhiteOnPrimaryContainer = Color(0xFF00513b) +val WhiteSecondary = Color(0xFF4c6359) +val WhiteOnSecondary = Color(0xFFFFFFFF) +val WhiteSecondaryContainer = Color(0xFFcee9db) +val WhiteOnSecondaryContainer = Color(0xFF354b41) +val WhiteTertiary = Color(0xFF3e6374) +val WhiteOnTertiary = Color(0xFFFFFFFF) +val WhiteTertiaryContainer = Color(0xFFc2e8fd) +val WhiteOnTertiaryContainer = Color(0xFF264b5c) +val WhiteError = Color(0xFFba1a1a) +val WhiteOnError = Color(0xFFFFFFFF) +val WhiteErrorContainer = Color(0xFFffdad6) +val WhiteOnErrorContainer = Color(0xFF93000a) +val WhiteBackground = Color(0xFFf5fbf5) +val WhiteOnBackground = Color(0xFF171d1a) +val WhiteSurface = Color(0xFFf5fbf5) +val WhiteOnSurface = Color(0xFF171d1a) +val WhiteSurfaceVariant = Color(0xFFdbe5de) +val WhiteOnSurfaceVariant = Color(0xFF404944) +val WhiteOutline = Color(0xFF707974) +val WhiteOutlineVariant = Color(0xFFbfc9c2) +val WhiteShadow = Color(0xFF000000) +val WhiteScrim = Color(0xFF000000) +val WhiteInverseSurface = Color(0xFF2c322e) +val WhiteInverseOnSurface = Color(0xFFecf2ed) +val WhiteInversePrimary = Color(0xFF8bd6b6) +val WhitePrimaryFixed = Color(0xFFa6f2d1) +val WhiteOnPrimaryFixed = Color(0xFF002116) +val WhitePrimaryFixedDim = Color(0xFF8bd6b6) +val WhiteOnPrimaryFixedVariant = Color(0xFF00513b) +val WhiteSecondaryFixed = Color(0xFFcee9db) +val WhiteOnSecondaryFixed = Color(0xFF092017) +val WhiteSecondaryFixedDim = Color(0xFFb3ccbf) +val WhiteOnSecondaryFixedVariant = Color(0xFF354b41) +val WhiteTertiaryFixed = Color(0xFFc2e8fd) +val WhiteOnTertiaryFixed = Color(0xFF001f2a) +val WhiteTertiaryFixedDim = Color(0xFFa6cce0) +val WhiteOnTertiaryFixedVariant = Color(0xFF264b5c) +val WhiteSurfaceDim = Color(0xFFd6dbd6) +val WhiteSurfaceBright = Color(0xFFf5fbf5) +val WhiteSurfaceContainerLowest = Color(0xFFFFFFFF) +val WhiteSurfaceContainerLow = Color(0xFFeff5f0) +val WhiteSurfaceContainer = Color(0xFFe9efea) +val WhiteSurfaceContainerHigh = Color(0xFFe4eae4) +val WhiteSurfaceContainerHighest = Color(0xFFdee4df) + +val DarkPrimary = Color(0xFF8bd6b6) +val DarkSurfaceTint = Color(0xFF8bd6b6) +val DarkOnPrimary = Color(0xFF003828) +val DarkPrimaryContainer = Color(0xFF00513b) +val DarkOnPrimaryContainer = Color(0xFFa6f2d1) +val DarkSecondary = Color(0xFFb3ccbf) +val DarkOnSecondary = Color(0xFF1e352b) +val DarkSecondaryContainer = Color(0xFF354b41) +val DarkOnSecondaryContainer = Color(0xFFcee9db) +val DarkTertiary = Color(0xFFa6cce0) +val DarkOnTertiary = Color(0xFF093544) +val DarkTertiaryContainer = Color(0xFF264b5c) +val DarkOnTertiaryContainer = Color(0xFFc2e8fd) +val DarkError = Color(0xFFffb4ab) +val DarkOnError = Color(0xFF690005) +val DarkErrorContainer = Color(0xFF93000a) +val DarkOnErrorContainer = Color(0xFFffdad6) +val DarkBackground = Color(0xFF0f1512) +val DarkOnBackground = Color(0xFFdee4df) +val DarkSurface = Color(0xFF0f1512) +val DarkOnSurface = Color(0xFFdee4df) +val DarkSurfaceVariant = Color(0xFF404944) +val DarkOnSurfaceVariant = Color(0xFFbfc9c2) +val DarkOutline = Color(0xFF89938d) +val DarkOutlineVariant = Color(0xFF404944) +val DarkShadow = Color(0xFF000000) +val DarkScrim = Color(0xFF000000) +val DarkInverseSurface = Color(0xFFdee4df) +val DarkInverseOnSurface = Color(0xFF2c322e) +val DarkInversePrimary = Color(0xFF1b6b51) +val DarkPrimaryFixed = Color(0xFFa6f2d1) +val DarkOnPrimaryFixed = Color(0xFF002116) +val DarkPrimaryFixedDim = Color(0xFF8bd6b6) +val DarkOnPrimaryFixedVariant = Color(0xFF00513b) +val DarkSecondaryFixed = Color(0xFFcee9db) +val DarkOnSecondaryFixed = Color(0xFF092017) +val DarkSecondaryFixedDim = Color(0xFFb3ccbf) +val DarkOnSecondaryFixedVariant = Color(0xFF354b41) +val DarkTertiaryFixed = Color(0xFFc2e8fd) +val DarkOnTertiaryFixed = Color(0xFF001f2a) +val DarkTertiaryFixedDim = Color(0xFFa6cce0) +val DarkOnTertiaryFixedVariant = Color(0xFF264b5c) +val DarkSurfaceDim = Color(0xFF0f1512) +val DarkSurfaceBright = Color(0xFF343b37) +val DarkSurfaceContainerLowest = Color(0xFF0a0f0d) +val DarkSurfaceContainerLow = Color(0xFF171d1a) +val DarkSurfaceContainer = Color(0xFF1b211e) +val DarkSurfaceContainerHigh = Color(0xFF252b28) +val DarkSurfaceContainerHighest = Color(0xFF303633) diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Dim.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Dim.kt new file mode 100644 index 0000000..dfebcc3 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Dim.kt @@ -0,0 +1,12 @@ +package com.prodhack.moscow2025.presentation.theme + +import androidx.compose.ui.unit.dp + +object Paddings { + val verySmall = 4.dp + + val small = 8.dp + + val medium = 12.dp + val large = 20.dp +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Shapes.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Shapes.kt new file mode 100644 index 0000000..6563358 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Shapes.kt @@ -0,0 +1,11 @@ +package com.prodhack.moscow2025.presentation.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.unit.dp + +object Shapes{ + val verySmallRoundedBox = RoundedCornerShape(Paddings.verySmall) + + val smallRoundedBox = RoundedCornerShape(10.dp) + +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Theme.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Theme.kt new file mode 100644 index 0000000..09fb7de --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Theme.kt @@ -0,0 +1,154 @@ +package com.prodhack.moscow2025.presentation.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +// Light color scheme +private val LightColorScheme = lightColorScheme( + primary = WhitePrimary, + onPrimary = WhiteOnPrimary, + primaryContainer = WhitePrimaryContainer, + onPrimaryContainer = WhiteOnPrimaryContainer, + inversePrimary = WhiteInversePrimary, + + secondary = WhiteSecondary, + onSecondary = WhiteOnSecondary, + secondaryContainer = WhiteSecondaryContainer, + onSecondaryContainer = WhiteOnSecondaryContainer, + + tertiary = WhiteTertiary, + onTertiary = WhiteOnTertiary, + tertiaryContainer = WhiteTertiaryContainer, + onTertiaryContainer = WhiteOnTertiaryContainer, + + error = WhiteError, + onError = WhiteOnError, + errorContainer = WhiteErrorContainer, + onErrorContainer = WhiteOnErrorContainer, + + background = WhiteBackground, + onBackground = WhiteOnBackground, + surface = WhiteSurface, + onSurface = WhiteOnSurface, + surfaceVariant = WhiteSurfaceVariant, + onSurfaceVariant = WhiteOnSurfaceVariant, + inverseSurface = WhiteInverseSurface, + inverseOnSurface = WhiteInverseOnSurface, + + outline = WhiteOutline, + outlineVariant = WhiteOutlineVariant, + + scrim = WhiteScrim, + surfaceTint = WhiteSurfaceTint, + + // Fixed colors + primaryFixed = WhitePrimaryFixed, + onPrimaryFixed = WhiteOnPrimaryFixed, + primaryFixedDim = WhitePrimaryFixedDim, + onPrimaryFixedVariant = WhiteOnPrimaryFixedVariant, + + secondaryFixed = WhiteSecondaryFixed, + onSecondaryFixed = WhiteOnSecondaryFixed, + secondaryFixedDim = WhiteSecondaryFixedDim, + onSecondaryFixedVariant = WhiteOnSecondaryFixedVariant, + + tertiaryFixed = WhiteTertiaryFixed, + onTertiaryFixed = WhiteOnTertiaryFixed, + tertiaryFixedDim = WhiteTertiaryFixedDim, + onTertiaryFixedVariant = WhiteOnTertiaryFixedVariant, + + surfaceDim = WhiteSurfaceDim, + surfaceBright = WhiteSurfaceBright, + surfaceContainerLowest = WhiteSurfaceContainerLowest, + surfaceContainerLow = WhiteSurfaceContainerLow, + surfaceContainer = WhiteSurfaceContainer, + surfaceContainerHigh = WhiteSurfaceContainerHigh, + surfaceContainerHighest = WhiteSurfaceContainerHighest +) + +// Dark color scheme +private val DarkColorScheme = darkColorScheme( + primary = DarkPrimary, + onPrimary = DarkOnPrimary, + primaryContainer = DarkPrimaryContainer, + onPrimaryContainer = DarkOnPrimaryContainer, + inversePrimary = DarkInversePrimary, + + secondary = DarkSecondary, + onSecondary = DarkOnSecondary, + secondaryContainer = DarkSecondaryContainer, + onSecondaryContainer = DarkOnSecondaryContainer, + + tertiary = DarkTertiary, + onTertiary = DarkOnTertiary, + tertiaryContainer = DarkTertiaryContainer, + onTertiaryContainer = DarkOnTertiaryContainer, + + error = DarkError, + onError = DarkOnError, + errorContainer = DarkErrorContainer, + onErrorContainer = DarkOnErrorContainer, + + background = DarkBackground, + onBackground = DarkOnBackground, + surface = DarkSurface, + onSurface = DarkOnSurface, + surfaceVariant = DarkSurfaceVariant, + onSurfaceVariant = DarkOnSurfaceVariant, + inverseSurface = DarkInverseSurface, + inverseOnSurface = DarkInverseOnSurface, + + outline = DarkOutline, + outlineVariant = DarkOutlineVariant, + + scrim = DarkScrim, + surfaceTint = DarkSurfaceTint, + + // Fixed colors + primaryFixed = DarkPrimaryFixed, + onPrimaryFixed = DarkOnPrimaryFixed, + primaryFixedDim = DarkPrimaryFixedDim, + onPrimaryFixedVariant = DarkOnPrimaryFixedVariant, + + secondaryFixed = DarkSecondaryFixed, + onSecondaryFixed = DarkOnSecondaryFixed, + secondaryFixedDim = DarkSecondaryFixedDim, + onSecondaryFixedVariant = DarkOnSecondaryFixedVariant, + + tertiaryFixed = DarkTertiaryFixed, + onTertiaryFixed = DarkOnTertiaryFixed, + tertiaryFixedDim = DarkTertiaryFixedDim, + onTertiaryFixedVariant = DarkOnTertiaryFixedVariant, + + surfaceDim = DarkSurfaceDim, + surfaceBright = DarkSurfaceBright, + surfaceContainerLowest = DarkSurfaceContainerLowest, + surfaceContainerLow = DarkSurfaceContainerLow, + surfaceContainer = DarkSurfaceContainer, + surfaceContainerHigh = DarkSurfaceContainerHigh, + surfaceContainerHighest = DarkSurfaceContainerHighest +) + +@Composable +fun MoscowHackatonTemplateTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = when { + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Type.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Type.kt new file mode 100644 index 0000000..719ffca --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/theme/Type.kt @@ -0,0 +1,40 @@ +package com.prodhack.moscow2025.presentation.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.prodhack.moscow2025.R + +val TinkoffSansFamily = FontFamily( + Font( + R.font.tinkoff_sans_bold, + FontWeight.Bold + ), + Font( + R.font.tinkoff_sans_regular, + FontWeight.Normal + ), + Font( + R.font.tinkoff_sans_medium, + FontWeight.Medium + ) +) + +val Typography = Typography( + titleLarge = TextStyle( + fontFamily = TinkoffSansFamily, + fontWeight = FontWeight.Bold + ), + titleMedium = TextStyle( + fontFamily = TinkoffSansFamily, + fontWeight = FontWeight.Medium + ), + labelLarge = TextStyle( + fontFamily = TinkoffSansFamily, + fontWeight = FontWeight.Normal + ) + +) \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/SetUtils.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/SetUtils.kt new file mode 100644 index 0000000..f4d6e9a --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/SetUtils.kt @@ -0,0 +1,9 @@ +package com.prodhack.moscow2025.presentation.utils + +fun MutableSet.toggleItem(item: T) { + if (item in this) { + remove(item) + } else { + add(item) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/StringUtils.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/StringUtils.kt new file mode 100644 index 0000000..741b9de --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/StringUtils.kt @@ -0,0 +1,3 @@ +package com.prodhack.moscow2025.presentation.utils + +fun String?.notNullOrBlank() = this != null && this.isNotBlank() \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/TimeUtils.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/TimeUtils.kt new file mode 100644 index 0000000..1e6806a --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/TimeUtils.kt @@ -0,0 +1,55 @@ +package com.prodhack.moscow2025.presentation.utils + +import java.text.SimpleDateFormat +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.time.temporal.ChronoUnit +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +fun daysUntilTimestampZoned(targetTimestamp: Long, zoneId: ZoneId = ZoneId.systemDefault()): Int { + val now = Instant.now().atZone(zoneId) + val targetTime = Instant.ofEpochMilli(targetTimestamp).atZone(zoneId) + + return ChronoUnit.DAYS.between(now, targetTime).toInt() +} + +fun getStartOfDayTimestamp(date: Date): Long { + val localDate = date.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate() + return localDate.atStartOfDay(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli() +} + +fun getStartOfTodayTimestamp(): Long { + val today = LocalDate.now() + return today.atStartOfDay(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli() +} + +fun timestampToDate(timestamp: Long, timeZone: TimeZone = TimeZone.getDefault()): String { + val date = Date(timestamp) + val formatter = SimpleDateFormat("dd.MM", Locale.getDefault()) + formatter.timeZone = timeZone + return formatter.format(date) +} + +fun timestampToDateWithYear(timestamp: Long, timeZone: TimeZone = TimeZone.getDefault()): String { + val date = Date(timestamp) + val formatter = SimpleDateFormat("dd.MM.YYYY", Locale.getDefault()) + formatter.timeZone = timeZone + return formatter.format(date) +} + +fun convertGMTToSystemTimezone(gmtTimestamp: Long): Long { + return getStartOfDayTimestamp(Date(gmtTimestamp)) +} + +fun timestampToIso(timestamp: Long): String { + return Instant.ofEpochMilli(timestamp).toString() +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/UIState.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/UIState.kt new file mode 100644 index 0000000..0f56b58 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/UIState.kt @@ -0,0 +1,226 @@ +package com.prodhack.moscow2025.presentation.utils + +import android.content.Context +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.prodhack.moscow2025.domain.utils.NetworkError +import com.prodhack.moscow2025.domain.utils.convertToNetworkError +import com.prodhack.moscow2025.presentation.utils.ui.placeholders.ErrorPlaceholder +import com.prodhack.moscow2025.presentation.utils.ui.placeholders.LoadingPlaceholder +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +sealed class UIState { + class Idle : UIState() + class Loading : UIState() + class Error(val error: NetworkError) : UIState() + class Success(val data: T) : UIState() + + fun map(mapper: (T) -> S): UIState { + return when (this) { + is Idle -> Idle() + is Loading -> Loading() + is Error -> Error(this.error) + is Success -> Success(mapper(this.data)) + } + } + + fun getOrNull(): T? = if (this is Success) { + data + } else { + null + } + + val isSuccess: Boolean + get() = this is Success +} + +interface ErrorCallbacks { + fun processConnectionError(networkError: NetworkError.Connection) + fun processUnexpectedError(networkError: NetworkError.Unexpected) +} + +open class ErrorCollectorScope( + private val context: Context, + val navController: NavController, + private val errorCallbacks: ErrorCallbacks +) { + companion object { + private const val TAG = "ErrorCollectorScope" + } + + @Composable + fun Flow>.collectAsStateWithCallbacks( + onInputError: ((NetworkError.InputError) -> Unit) = { + Toast.makeText(context, "Something went wrong", Toast.LENGTH_SHORT) + .show() + }, + onUnexpectedError: ((NetworkError.Unexpected) -> Unit) = {}, + onConnectionError: ((NetworkError.Connection) -> Unit) = {}, + onLoading: (() -> Unit) = {}, + onSuccess: (T) -> Unit = {} + ): State> = this.onEach { + when (it) { + is UIState.Loading -> { + onLoading() + } + + is UIState.Error -> { + Log.e(TAG, "collected error ${it.error}") + when (it.error) { + is NetworkError.Connection -> { + errorCallbacks.processConnectionError(it.error) + onConnectionError.invoke(it.error) + } + + is NetworkError.Unexpected -> { + errorCallbacks.processUnexpectedError(it.error) + onUnexpectedError.invoke(it.error) + } + is NetworkError.InputError -> onInputError.invoke(it.error) + } + } + + is UIState.Success -> { + onSuccess.invoke(it.data) + } + + else -> {} + } + }.collectAsState(UIState.Idle()) + + + @Composable + fun Flow>.collectAsValueStateWithCallbacks( + onInputError: ((NetworkError.InputError) -> Unit) = { + Toast.makeText(context, "Something went wrong", Toast.LENGTH_SHORT) + .show() + }, + onLoading: (() -> Unit) = {}, + onSuccess: (T) -> Unit = {} + ): State = this.map { + when (it) { + is UIState.Loading -> { + onLoading() + null + } + + is UIState.Error -> { + Log.e(TAG, "collected error ${it.error}") + when (it.error) { + is NetworkError.Connection -> errorCallbacks.processConnectionError(it.error) + is NetworkError.Unexpected -> errorCallbacks.processUnexpectedError(it.error) + is NetworkError.InputError -> onInputError.invoke(it.error) + } + null + } + + is UIState.Success -> { + onSuccess.invoke(it.data) + it.data + } + + else -> { + null + } + } + }.collectAsState(null) + + @Composable + fun Flow>.FoldUIStateWithGlobalCallbacks( + modifier: Modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp), + onIdle: @Composable () -> Unit = {}, + onError: @Composable (NetworkError) -> Unit = { ErrorPlaceholder(modifier = modifier) { navController?.popBackStack() } }, + onLoading: @Composable () -> Unit = { LoadingPlaceholder(modifier = modifier) }, + onSuccess: @Composable (T) -> Unit + ) { + val state = this.onEach { + if (it is UIState.Error) { + Log.e(TAG, "collected error ${it.error}") + when (it.error) { + is NetworkError.Connection -> errorCallbacks.processConnectionError(it.error) + is NetworkError.Unexpected -> errorCallbacks.processUnexpectedError(it.error) + else -> {} + } + } + }.collectAsState(initial = UIState.Idle()).value + + when (state) { + is UIState.Idle -> { + onIdle() + } + + is UIState.Error -> { + onError(state.error.convertToNetworkError()) + } + + is UIState.Loading -> { + onLoading() + } + + is UIState.Success -> { + onSuccess(state.data) + } + } + } + + @Composable + fun UIState.FoldUIStateWithGlobalCallbacks( + modifier: Modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp), + onIdle: @Composable () -> Unit = {}, + onError: @Composable (NetworkError) -> Unit = { ErrorPlaceholder(modifier = modifier) { navController?.popBackStack() } }, + onLoading: @Composable () -> Unit = { LoadingPlaceholder(modifier = modifier) }, + onSuccess: @Composable (T) -> Unit + ) { + if (this is UIState.Error) { + Log.e(TAG, "collected error ${this.error}") + when (error) { + is NetworkError.Connection -> errorCallbacks.processConnectionError(error) + is NetworkError.Unexpected -> errorCallbacks.processUnexpectedError(error) + else -> {} + } + } + + when (this) { + is UIState.Idle -> { + onIdle() + } + + is UIState.Error -> { + onError(error.convertToNetworkError()) + } + + is UIState.Loading -> { + onLoading() + } + + is UIState.Success -> { + onSuccess.invoke(data) + } + } + } +} + +@Composable +fun ErrorCollectorScope( + context: Context, + navController: NavController? = null, + errorCallbacks: ErrorCallbacks, + content: @Composable ErrorCollectorScope.() -> Unit +) { + ErrorCollectorScope(context, navController ?: rememberNavController(), errorCallbacks).content() +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/base/BaseViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/base/BaseViewModel.kt new file mode 100644 index 0000000..fdbf6d9 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/base/BaseViewModel.kt @@ -0,0 +1,78 @@ +package com.prodhack.moscow2025.presentation.utils.base + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.map +import com.prodhack.moscow2025.presentation.utils.UIState +import com.prodhack.moscow2025.domain.utils.convertToNetworkError +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** + * Base class for all [ViewModel]s + */ +abstract class BaseViewModel : ViewModel() { + + /** + * Creates [MutableStateFlow] with [UIState] and the given initial value [UIState.Idle] + */ + @Suppress("FunctionName") + protected fun MutableUIStateFlow(defaultValue: T? = null) = + MutableStateFlow>(defaultValue?.let { UIState.Success(it) } ?: UIState.Idle()) + + /** + * Reset [MutableUIStateFlow] to [UIState.Idle] + */ + protected fun MutableStateFlow>.reset() { + value = UIState.Idle() + } + + /** + * Collect network request + * + * @return [UIState] depending request result + */ + protected fun Flow>.collectRequest( + state: MutableStateFlow>, + ) { + viewModelScope.launch { + state.value = UIState.Loading() + this@collectRequest.collect { + state.value = if (it.isSuccess) { + UIState.Success(it.getOrNull()!!) + } else { + UIState.Error(it.exceptionOrNull()!!.convertToNetworkError()) + } + } + } + } + + /** + * Collect network request + * + * @return [UIState] depending request result + */ + protected fun Result.collectRequest( + state: MutableStateFlow> + ) { + state.value = UIState.Loading() + state.value = if (isSuccess) { + UIState.Success(getOrNull()!!) + } else { + UIState.Error(exceptionOrNull()!!.convertToNetworkError()) + } + } + + + /** + * Collect paging request + */ + protected fun Flow>.collectPagingRequest( + mappedData: suspend (T) -> S + ) = map { it.map { data -> mappedData(data) } }.cachedIn(viewModelScope) + +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/imageToByteArray.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/imageToByteArray.kt new file mode 100644 index 0000000..8652d62 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/imageToByteArray.kt @@ -0,0 +1,14 @@ +package com.prodhack.moscow2025.presentation.utils + +import android.graphics.Bitmap +import java.io.ByteArrayOutputStream + +fun Bitmap.toByteArray(): ByteArray { + val stream = ByteArrayOutputStream() // Create a ByteArrayOutputStream + compress( + Bitmap.CompressFormat.JPEG, + 100, + stream + ) // Compress Bitmap to PNG with 100% quality + return stream.toByteArray() // Convert stream to byte array +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/CalendarModal.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/CalendarModal.kt new file mode 100644 index 0000000..d76e76d --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/CalendarModal.kt @@ -0,0 +1,88 @@ +package com.prodhack.moscow2025.presentation.utils.ui + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.DateRangePicker +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberDateRangePickerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun DateRangePickerModal( + onDateRangeSelected: (Pair) -> Unit, + onDismiss: () -> Unit +) { + val dateRangePickerState = rememberDateRangePickerState() + + DatePickerDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton( + onClick = { + onDateRangeSelected( + Pair( + dateRangePickerState.selectedStartDateMillis, + dateRangePickerState.selectedEndDateMillis + ) + ) + onDismiss() + } + ) { + Text("OK") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) { + DateRangePicker( + state = dateRangePickerState, + title = { + Text( + text = "Select date range" + ) + }, + showModeToggle = false, + modifier = Modifier + .fillMaxWidth() + .height(500.dp) + .padding(16.dp) + ) + } +} + +@Composable +fun DatePickerModal( + onDateSelected: (Long?) -> Unit, + onDismiss: () -> Unit +) { + val datePickerState = rememberDatePickerState() + + DatePickerDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { + onDateSelected(datePickerState.selectedDateMillis) + onDismiss() + }) { + Text("OK") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) { + DatePicker(state = datePickerState) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/ColoredClickable.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/ColoredClickable.kt new file mode 100644 index 0000000..ef25098 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/ColoredClickable.kt @@ -0,0 +1,42 @@ +package com.prodhack.moscow2025.presentation.utils.ui + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.debugInspectorInfo + +fun Modifier.clickable( + rippleColor: Color? = null, + onClick: () -> Unit +) = composed( + inspectorInfo = debugInspectorInfo { + name = "clickable" + properties["rippleColor"] = rippleColor + properties["onClick"] = onClick + } +) { + this.clickable( + onClick = onClick, + indication = rippleColor?.let { + ripple( + color = it + ) + } ?: LocalIndication.current, + interactionSource = remember { MutableInteractionSource() } + ) +} + +@Composable +fun Modifier.noRippleClickable( + onClick: () -> Unit +) = this.clickable( + onClick = onClick, + interactionSource = remember { MutableInteractionSource() }, + indication = null +) \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/ErrorPlaceHolder.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/ErrorPlaceHolder.kt new file mode 100644 index 0000000..e3a7555 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/ErrorPlaceHolder.kt @@ -0,0 +1,41 @@ +package com.prodhack.moscow2025.presentation.utils.ui.placeholders + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme + + +data class ErrorTexts( + val title: String = "Error", + val mainText: String = "Oh mio dio! \n" + + "Sembra che qualcosa non va", + val description: String = "Lavoreremo per sistemare le cose, ti chiediamo tornare più tardi." +) + +@Composable +fun ErrorPlaceholder( + modifier: Modifier = Modifier, + showTop: Boolean = false, + small: Boolean = false, + showButton: Boolean = true, + errorTexts: ErrorTexts = ErrorTexts(), + actionText: String = "Ok", + onAction: () -> Unit +) { + Text("Error") +} + + +@Preview +@Composable +fun ErrorPlaceHolderPreview() { + MoscowHackatonTemplateTheme { + Scaffold { + ErrorPlaceholder(modifier = Modifier.padding(it), showTop = true) { } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/LoadingPlaceholder.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/LoadingPlaceholder.kt new file mode 100644 index 0000000..a253804 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/utils/ui/placeholders/LoadingPlaceholder.kt @@ -0,0 +1,32 @@ +package com.prodhack.moscow2025.presentation.utils.ui.placeholders + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.prodhack.moscow2025.presentation.theme.MoscowHackatonTemplateTheme + +@Composable +fun LoadingPlaceholder( + modifier: Modifier = Modifier, + text: String = "Già quasi scaricato, per favore aspetta un po" +) { + Text(modifier = modifier, text = text) +} + +@Preview +@Composable +private fun LoadingPlaceholderPreview() { + Scaffold { paddingValues -> + MoscowHackatonTemplateTheme() { + LoadingPlaceholder( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/add_square_outline.xml b/app/src/main/res/drawable/add_square_outline.xml new file mode 100644 index 0000000..0604309 --- /dev/null +++ b/app/src/main/res/drawable/add_square_outline.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_arr_dropdown.xml b/app/src/main/res/drawable/ic_arr_dropdown.xml new file mode 100644 index 0000000..0c01396 --- /dev/null +++ b/app/src/main/res/drawable/ic_arr_dropdown.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_calendar.xml b/app/src/main/res/drawable/ic_calendar.xml new file mode 100644 index 0000000..ec87b7a --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_chart.xml b/app/src/main/res/drawable/ic_chart.xml new file mode 100644 index 0000000..330aeae --- /dev/null +++ b/app/src/main/res/drawable/ic_chart.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_checkmark.xml b/app/src/main/res/drawable/ic_checkmark.xml new file mode 100644 index 0000000..e583e14 --- /dev/null +++ b/app/src/main/res/drawable/ic_checkmark.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_documents.xml b/app/src/main/res/drawable/ic_documents.xml new file mode 100644 index 0000000..a40950b --- /dev/null +++ b/app/src/main/res/drawable/ic_documents.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_flag_filled.xml b/app/src/main/res/drawable/ic_flag_filled.xml new file mode 100644 index 0000000..2489881 --- /dev/null +++ b/app/src/main/res/drawable/ic_flag_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_flag_unfilled.xml b/app/src/main/res/drawable/ic_flag_unfilled.xml new file mode 100644 index 0000000..daa4f77 --- /dev/null +++ b/app/src/main/res/drawable/ic_flag_unfilled.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_group.xml b/app/src/main/res/drawable/ic_group.xml new file mode 100644 index 0000000..bb9d073 --- /dev/null +++ b/app/src/main/res/drawable/ic_group.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml new file mode 100644 index 0000000..b8ae911 --- /dev/null +++ b/app/src/main/res/drawable/ic_home.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_magnifer.xml b/app/src/main/res/drawable/ic_magnifer.xml new file mode 100644 index 0000000..38a28a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_magnifer.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..c88b884 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 0000000..a71432d --- /dev/null +++ b/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..800e533 --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_profile.xml b/app/src/main/res/drawable/ic_profile.xml new file mode 100644 index 0000000..45abdfd --- /dev/null +++ b/app/src/main/res/drawable/ic_profile.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_trips.xml b/app/src/main/res/drawable/ic_trips.xml new file mode 100644 index 0000000..cadeea0 --- /dev/null +++ b/app/src/main/res/drawable/ic_trips.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/logout_icon.xml b/app/src/main/res/drawable/logout_icon.xml new file mode 100644 index 0000000..46590a0 --- /dev/null +++ b/app/src/main/res/drawable/logout_icon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/lottie.png b/app/src/main/res/drawable/lottie.png new file mode 100644 index 0000000000000000000000000000000000000000..ba1559fef95700663251d8cb3d68ee5ed3f0b023 GIT binary patch literal 8824 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 sb_Zss831T*LPyN(>f8XEtpIv6V_ngo9oX`2} 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/font/tinkoff_sans_regular.ttf b/app/src/main/res/font/tinkoff_sans_regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..13963b9d89c6f5addc015df6e88127ba3a5d48aa GIT binary patch literal 70240 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 RH$MIQ+q%v0SNDeJ{}+t)C$#_o literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..74d295c --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + MoscowHackatonTemplate + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..bf97c8c --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +