├── app ├── .gitignore ├── release │ ├── app-release.apk │ └── output-metadata.json ├── src │ └── main │ │ ├── ic_launcher-playstore.png │ │ ├── java │ │ └── ru │ │ │ └── tech │ │ │ └── cookhelper │ │ │ ├── domain │ │ │ ├── utils │ │ │ │ ├── Domain.kt │ │ │ │ └── text │ │ │ │ │ ├── TextValidator.kt │ │ │ │ │ ├── ChainTextValidator.kt │ │ │ │ │ ├── validators │ │ │ │ │ ├── NonEmptyTextValidator.kt │ │ │ │ │ ├── EmailTextValidator.kt │ │ │ │ │ ├── HasNumberTextValidator.kt │ │ │ │ │ └── LengthTextValidator.kt │ │ │ │ │ └── ValidatorResult.kt │ │ │ ├── model │ │ │ │ ├── FormMessage.kt │ │ │ │ ├── Setting.kt │ │ │ │ ├── MatchedRecipe.kt │ │ │ │ ├── Product.kt │ │ │ │ ├── Message.kt │ │ │ │ ├── FileData.kt │ │ │ │ ├── Chat.kt │ │ │ │ ├── Post.kt │ │ │ │ ├── Topic.kt │ │ │ │ ├── Reply.kt │ │ │ │ ├── User.kt │ │ │ │ ├── ForumFilters.kt │ │ │ │ └── Recipe.kt │ │ │ ├── use_case │ │ │ │ ├── get_user │ │ │ │ │ └── GetUserUseCase.kt │ │ │ │ ├── log_out │ │ │ │ │ └── LogOutUseCase.kt │ │ │ │ ├── get_feed │ │ │ │ │ └── GetFeedUseCase.kt │ │ │ │ ├── get_chat_list │ │ │ │ │ └── GetChatListUseCase.kt │ │ │ │ ├── load_user │ │ │ │ │ └── LoadUserByIdUseCase.kt │ │ │ │ ├── GetMatchedRecipesUseCase.kt │ │ │ │ ├── stop_awaiting_feed │ │ │ │ │ └── StopAwaitingFeedUseCase.kt │ │ │ │ ├── stop_awaiting_messages │ │ │ │ │ └── StopAwaitingMessagesUseCase.kt │ │ │ │ ├── log_in │ │ │ │ │ └── LoginUseCase.kt │ │ │ │ ├── check_code │ │ │ │ │ └── CheckCodeUseCase.kt │ │ │ │ ├── get_available_products │ │ │ │ │ └── GetAvailableProductsUseCase.kt │ │ │ │ ├── insert_setting │ │ │ │ │ └── InsertSettingUseCase.kt │ │ │ │ ├── cache_user │ │ │ │ │ └── CacheUserUseCase.kt │ │ │ │ ├── restore_password │ │ │ │ │ ├── SendRestoreCodeUseCase.kt │ │ │ │ │ └── ApplyPasswordByCodeUseCase.kt │ │ │ │ ├── send_message │ │ │ │ │ └── SendMessagesUseCase.kt │ │ │ │ ├── observe_user │ │ │ │ │ └── ObserveUserUseCase.kt │ │ │ │ ├── request_code │ │ │ │ │ └── RequestCodeUseCase.kt │ │ │ │ ├── check_email │ │ │ │ │ └── CheckEmailForAvailabilityUseCase.kt │ │ │ │ ├── check_login │ │ │ │ │ └── CheckLoginForAvailabilityUseCase.kt │ │ │ │ ├── add_products_to_fridge │ │ │ │ │ └── AddProductsToFridgeUseCase.kt │ │ │ │ ├── remove_products_from_fridge │ │ │ │ │ └── RemoveProductsFromFridgeUseCase.kt │ │ │ │ ├── get_all_messages │ │ │ │ │ └── GetAllMessagesUseCase.kt │ │ │ │ ├── create_topic │ │ │ │ │ └── CreateTopicUseCase.kt │ │ │ │ ├── await_new_messages │ │ │ │ │ └── AwaitNewMessagesUseCase.kt │ │ │ │ ├── create_post │ │ │ │ │ └── CreatePostUseCase.kt │ │ │ │ ├── get_settings_list │ │ │ │ │ └── GetSettingsListUseCaseFlow.kt │ │ │ │ ├── registration │ │ │ │ │ └── RegistrationUseCase.kt │ │ │ │ └── close_connection │ │ │ │ │ └── CloseConnectionsUseCase.kt │ │ │ └── repository │ │ │ │ ├── SettingsRepository.kt │ │ │ │ ├── FridgeRepository.kt │ │ │ │ ├── MessageRepository.kt │ │ │ │ └── UserRepository.kt │ │ │ ├── data │ │ │ ├── remote │ │ │ │ ├── utils │ │ │ │ │ ├── Response.kt │ │ │ │ │ └── Dto.kt │ │ │ │ ├── web_socket │ │ │ │ │ ├── Service.kt │ │ │ │ │ ├── feed │ │ │ │ │ │ ├── FeedService.kt │ │ │ │ │ │ └── FeedServiceImpl.kt │ │ │ │ │ ├── message │ │ │ │ │ │ ├── MessageService.kt │ │ │ │ │ │ └── MessageServiceImpl.kt │ │ │ │ │ ├── user │ │ │ │ │ │ ├── UserService.kt │ │ │ │ │ │ └── UserServiceImpl.kt │ │ │ │ │ └── WebSocketState.kt │ │ │ │ ├── dto │ │ │ │ │ ├── ProductDto.kt │ │ │ │ │ ├── MatchedRecipeDto.kt │ │ │ │ │ ├── MessageDto.kt │ │ │ │ │ ├── ChatDto.kt │ │ │ │ │ ├── PostDto.kt │ │ │ │ │ ├── TopicDto.kt │ │ │ │ │ ├── RecipeDto.kt │ │ │ │ │ └── UserDto.kt │ │ │ │ └── api │ │ │ │ │ ├── chat │ │ │ │ │ └── ChatApi.kt │ │ │ │ │ ├── ingredients │ │ │ │ │ └── FridgeApi.kt │ │ │ │ │ ├── user │ │ │ │ │ └── UserApi.kt │ │ │ │ │ └── auth │ │ │ │ │ └── AuthService.kt │ │ │ ├── local │ │ │ │ ├── utils │ │ │ │ │ └── DatabaseEntity.kt │ │ │ │ ├── entity │ │ │ │ │ ├── SettingsEntity.kt │ │ │ │ │ └── UserEntity.kt │ │ │ │ ├── dao │ │ │ │ │ ├── UserDao.kt │ │ │ │ │ └── SettingsDao.kt │ │ │ │ └── database │ │ │ │ │ ├── Database.kt │ │ │ │ │ └── TypeConverters.kt │ │ │ ├── utils │ │ │ │ ├── MoshiParser.kt │ │ │ │ └── JsonParser.kt │ │ │ └── repository │ │ │ │ ├── SettingsRepositoryImpl.kt │ │ │ │ └── MessageRepositoryImpl.kt │ │ │ ├── presentation │ │ │ ├── restore_password │ │ │ │ └── components │ │ │ │ │ ├── RestoreState.kt │ │ │ │ │ └── RestorePasswordState.kt │ │ │ ├── profile │ │ │ │ ├── components │ │ │ │ │ ├── RecipesTabContent.kt │ │ │ │ │ ├── SelectedTab.kt │ │ │ │ │ ├── LogoutDialog.kt │ │ │ │ │ ├── PostActionButton.kt │ │ │ │ │ ├── AuthorBubble.kt │ │ │ │ │ └── EditStatusDialog.kt │ │ │ │ └── viewModel │ │ │ │ │ └── ProfileViewModel.kt │ │ │ ├── confirm_email │ │ │ │ └── components │ │ │ │ │ └── CodeState.kt │ │ │ ├── app │ │ │ │ ├── components │ │ │ │ │ ├── UserState.kt │ │ │ │ │ ├── ExitDialog.kt │ │ │ │ │ ├── MainModalDrawerHeader.kt │ │ │ │ │ ├── BottomSheetHost.kt │ │ │ │ │ └── SimpleScaffold.kt │ │ │ │ └── MainActivity.kt │ │ │ ├── chat │ │ │ │ └── components │ │ │ │ │ ├── ChatState.kt │ │ │ │ │ └── MessageHeader.kt │ │ │ ├── login_screen │ │ │ │ └── components │ │ │ │ │ └── LoginState.kt │ │ │ ├── chat_list │ │ │ │ └── components │ │ │ │ │ ├── ChatListState.kt │ │ │ │ │ └── ChatPicture.kt │ │ │ ├── feed_screen │ │ │ │ └── components │ │ │ │ │ └── FeedState.kt │ │ │ ├── post_creation │ │ │ │ └── components │ │ │ │ │ └── PostCreationState.kt │ │ │ ├── topic_creation │ │ │ │ └── components │ │ │ │ │ └── TopicCreationState.kt │ │ │ ├── registration_screen │ │ │ │ └── components │ │ │ │ │ ├── RegistrationState.kt │ │ │ │ │ ├── CheckEmailState.kt │ │ │ │ │ └── CheckLoginState.kt │ │ │ ├── matched_recipes │ │ │ │ └── components │ │ │ │ │ └── MatchedRecipeState.kt │ │ │ ├── ui │ │ │ │ ├── utils │ │ │ │ │ ├── provider │ │ │ │ │ │ ├── LocalSettingsState.kt │ │ │ │ │ │ ├── LocalSnackbarHostState.kt │ │ │ │ │ │ ├── LocalToastHostState.kt │ │ │ │ │ │ ├── LocalBottomSheetController.kt │ │ │ │ │ │ ├── LocalWindowSizeClass.kt │ │ │ │ │ │ └── LocalScreenController.kt │ │ │ │ │ ├── event │ │ │ │ │ │ ├── ViewModelEvents.kt │ │ │ │ │ │ ├── ViewModelEventsImpl.kt │ │ │ │ │ │ ├── Event.kt │ │ │ │ │ │ └── EventUtils.kt │ │ │ │ │ ├── android │ │ │ │ │ │ ├── ContentUtils.kt │ │ │ │ │ │ ├── ShareUtils.kt │ │ │ │ │ │ ├── ConfigurationUtils.kt │ │ │ │ │ │ ├── Logger.kt │ │ │ │ │ │ ├── SystemBarUtils.kt │ │ │ │ │ │ └── exception │ │ │ │ │ │ │ └── GlobalExceptionHandler.kt │ │ │ │ │ ├── compose │ │ │ │ │ │ ├── ToastUtils.kt │ │ │ │ │ │ ├── SnackbarUtils.kt │ │ │ │ │ │ ├── UIText.kt │ │ │ │ │ │ ├── ResUtils.kt │ │ │ │ │ │ ├── ColorUtils.kt │ │ │ │ │ │ ├── ScrollUtils.kt │ │ │ │ │ │ ├── TopAppBarUtils.kt │ │ │ │ │ │ ├── PaddingUtils.kt │ │ │ │ │ │ ├── StateUtils.kt │ │ │ │ │ │ └── Modifiers.kt │ │ │ │ │ └── navigation │ │ │ │ │ │ └── BottomSheet.kt │ │ │ │ ├── widgets │ │ │ │ │ ├── KeepScreenOn.kt │ │ │ │ │ ├── zooomable │ │ │ │ │ │ └── ZoomParams.kt │ │ │ │ │ ├── Spacer.kt │ │ │ │ │ └── Placeholder.kt │ │ │ │ └── theme │ │ │ │ │ ├── Transitions.kt │ │ │ │ │ └── Typography.kt │ │ │ ├── fullscreen_image_pager │ │ │ │ └── FileDataSaver.kt │ │ │ ├── settings │ │ │ │ ├── viewModel │ │ │ │ │ └── SettingsViewModel.kt │ │ │ │ ├── components │ │ │ │ │ ├── RotationButton.kt │ │ │ │ │ └── PickLanguageDialog.kt │ │ │ │ └── SettingsScreen.kt │ │ │ ├── m3 │ │ │ │ └── M3Activity.kt │ │ │ ├── forum_discussion │ │ │ │ ├── components │ │ │ │ │ ├── TagGroup.kt │ │ │ │ │ ├── TagItem.kt │ │ │ │ │ └── RatingButton.kt │ │ │ │ └── viewModel │ │ │ │ │ └── ForumDiscussionViewModel.kt │ │ │ ├── recipe_post_creation │ │ │ │ ├── components │ │ │ │ │ ├── Separator.kt │ │ │ │ │ ├── FabSize.kt │ │ │ │ │ └── LeaveUnsavedDataDialog.kt │ │ │ │ └── viewModel │ │ │ │ │ └── RecipePostCreationViewModel.kt │ │ │ ├── authentication │ │ │ │ └── components │ │ │ │ │ └── LockScreenOrientaion.kt │ │ │ ├── fridge_screen │ │ │ │ └── components │ │ │ │ │ ├── ProductUtils.kt │ │ │ │ │ └── ProductItem.kt │ │ │ ├── recipe_details │ │ │ │ └── viewModel │ │ │ │ │ └── RecipeDetailsViewModel.kt │ │ │ ├── home_screen │ │ │ │ └── components │ │ │ │ │ └── BottomNavigationBar.kt │ │ │ ├── crash_screen │ │ │ │ └── viewModel │ │ │ │ │ └── CrashViewModel.kt │ │ │ ├── forum_screen │ │ │ │ └── components │ │ │ │ │ └── SearchBox.kt │ │ │ ├── all_images │ │ │ │ └── components │ │ │ │ │ └── AdaptiveVerticalGrid.kt │ │ │ └── edit_profile │ │ │ │ └── components │ │ │ │ └── EditProfileItem.kt │ │ │ └── core │ │ │ ├── application │ │ │ └── CookHelperApplication.kt │ │ │ ├── utils │ │ │ ├── ReflectionUtils.kt │ │ │ ├── kotlin │ │ │ │ └── KotlinUtils.kt │ │ │ ├── ConnectionUtils.kt │ │ │ └── RetrofitUtils.kt │ │ │ ├── constants │ │ │ ├── Constants.kt │ │ │ └── Status.kt │ │ │ ├── di │ │ │ ├── RoomModule.kt │ │ │ ├── RetrofitModule.kt │ │ │ ├── NetworkModule.kt │ │ │ └── RepositoryModule.kt │ │ │ └── Action.kt │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── values │ │ │ ├── colors.xml │ │ │ └── themes.xml │ │ ├── values-night │ │ │ └── colors.xml │ │ ├── xml │ │ │ ├── locales_config.xml │ │ │ ├── backup_rules.xml │ │ │ └── data_extraction_rules.xml │ │ └── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── AndroidManifest.xml └── proguard-rules.pro ├── dynamic_theme ├── .gitignore ├── consumer-rules.pro ├── src │ └── main │ │ └── AndroidManifest.xml ├── libs │ └── material-color-util.jar ├── proguard-rules.pro └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle.kts └── gradle.properties /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /dynamic_theme/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /dynamic_theme/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/app/release/app-release.apk -------------------------------------------------------------------------------- /dynamic_theme/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/utils/Domain.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.utils 2 | 3 | interface Domain -------------------------------------------------------------------------------- /dynamic_theme/libs/material-color-util.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/dynamic_theme/libs/material-color-util.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T8RIN/CookHelper/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #002B33 4 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/utils/Response.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.utils 2 | 3 | data class Response( 4 | val status: Int, 5 | val data: T? 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/restore_password/components/RestoreState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.restore_password.components 2 | 3 | enum class RestoreState { Login, Password } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/utils/Dto.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.utils 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | interface Dto { 6 | fun asDomain(): Domain 7 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/web_socket/Service.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.web_socket 2 | 3 | interface Service { 4 | fun sendMessage(data: String) 5 | fun closeService() 6 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/FormMessage.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | data class FormMessage( 4 | val text: String, 5 | val replyToId: Int, 6 | val attachments: List 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/local/utils/DatabaseEntity.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.local.utils 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | interface DatabaseEntity { 6 | fun asDomain(): Domain 7 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/Setting.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | data class Setting( 6 | val id: Int, 7 | val option: String 8 | ) : Domain 9 | -------------------------------------------------------------------------------- /app/src/main/res/xml/locales_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/profile/components/RecipesTabContent.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.profile.components 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | fun RecipesTabContent() { 7 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/confirm_email/components/CodeState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.confirm_email.components 2 | 3 | data class CodeState( 4 | val isLoading: Boolean = false, 5 | val error: Boolean = false 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/MatchedRecipe.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | data class MatchedRecipe( 6 | val recipe: Recipe, 7 | val percentString: String 8 | ) : Domain -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/utils/text/TextValidator.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.utils.text 2 | 3 | interface TextValidator { 4 | fun validate(stringToValidate: String): ValidatorResult 5 | var validatorResult: ValidatorResult 6 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/core/application/CookHelperApplication.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.core.application 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class CookHelperApplication : Application() -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/profile/components/SelectedTab.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.profile.components 2 | 3 | enum class SelectedTab { Posts, Recipes } 4 | 5 | fun Int.toTab(): SelectedTab = SelectedTab.values().first { it.ordinal == this } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Apr 15 18:54:45 MSK 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/app/components/UserState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.app.components 2 | 3 | import ru.tech.cookhelper.domain.model.User 4 | 5 | data class UserState( 6 | val user: User? = null, 7 | val token: String = user?.token ?: "" 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/Product.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | data class Product( 6 | val id: Int, 7 | val title: String, 8 | val category: Int, 9 | val mimetype: String 10 | ) : Domain 11 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/chat/components/ChatState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.chat.components 2 | 3 | data class ChatState( 4 | val isLoading: Boolean = false, 5 | val image: String? = null, 6 | val title: String = "", 7 | val newMessages: Int = 0 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/login_screen/components/LoginState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.login_screen.components 2 | 3 | import ru.tech.cookhelper.domain.model.User 4 | 5 | data class LoginState( 6 | val isLoading: Boolean = false, 7 | val user: User? = null 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/core/utils/ReflectionUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.core.utils 2 | 3 | import kotlin.reflect.KClass 4 | 5 | object ReflectionUtils { 6 | inline val KClass.name: String 7 | get() { 8 | return simpleName.toString() 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/chat_list/components/ChatListState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.chat_list.components 2 | 3 | import ru.tech.cookhelper.domain.model.Chat 4 | 5 | data class ChatListState( 6 | val isLoading: Boolean = false, 7 | val chatList: List = emptyList() 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/feed_screen/components/FeedState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.feed_screen.components 2 | 3 | import ru.tech.cookhelper.domain.model.Recipe 4 | 5 | data class FeedState( 6 | val data: List = emptyList(), 7 | val isLoading: Boolean = false, 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/post_creation/components/PostCreationState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.post_creation.components 2 | 3 | import ru.tech.cookhelper.domain.model.Post 4 | 5 | data class PostCreationState( 6 | val isLoading: Boolean = false, 7 | val post: Post? = null 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/topic_creation/components/TopicCreationState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.topic_creation.components 2 | 3 | import ru.tech.cookhelper.domain.model.Topic 4 | 5 | data class TopicCreationState( 6 | val isLoading: Boolean = false, 7 | val topic: Topic? = null 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/registration_screen/components/RegistrationState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.registration_screen.components 2 | 3 | import ru.tech.cookhelper.domain.model.User 4 | 5 | data class RegistrationState( 6 | val isLoading: Boolean = false, 7 | val user: User? = null 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/matched_recipes/components/MatchedRecipeState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.matched_recipes.components 2 | 3 | import ru.tech.cookhelper.domain.model.MatchedRecipe 4 | 5 | data class MatchedRecipeState( 6 | val isLoading: Boolean = false, 7 | val recipes: List = emptyList() 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/Message.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | data class Message( 6 | val id: Long, 7 | val text: String, 8 | val attachments: List, 9 | val replyToId: Long, 10 | val timestamp: Long, 11 | val author: User 12 | ) : Domain 13 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/provider/LocalSettingsState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.provider 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import ru.tech.cookhelper.presentation.settings.components.SettingsState 5 | 6 | val LocalSettingsProvider = compositionLocalOf { error("SettingsState not present") } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/registration_screen/components/CheckEmailState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.registration_screen.components 2 | 3 | import ru.tech.cookhelper.presentation.ui.utils.compose.UIText 4 | 5 | data class CheckEmailState( 6 | val isValid: Boolean = false, 7 | val error: UIText = UIText.Empty(), 8 | val isLoading: Boolean = false 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/registration_screen/components/CheckLoginState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.registration_screen.components 2 | 3 | import ru.tech.cookhelper.presentation.ui.utils.compose.UIText 4 | 5 | data class CheckLoginState( 6 | val isValid: Boolean = false, 7 | val error: UIText = UIText.Empty(), 8 | val isLoading: Boolean = false 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/get_user/GetUserUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.get_user 2 | 3 | import ru.tech.cookhelper.domain.repository.UserRepository 4 | import javax.inject.Inject 5 | 6 | class GetUserUseCase @Inject constructor( 7 | private val userRepository: UserRepository 8 | ) { 9 | operator fun invoke() = userRepository.getUser() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/restore_password/components/RestorePasswordState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.restore_password.components 2 | 3 | import ru.tech.cookhelper.domain.model.User 4 | 5 | data class RestorePasswordState( 6 | val isLoading: Boolean = false, 7 | val state: RestoreState = RestoreState.Login, 8 | val user: User? = null 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/FileData.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | data class FileData( 6 | val link: String, 7 | val id: String 8 | ) : Domain 9 | 10 | //data class FileData( 11 | // val id: Long, 12 | // val name: String, 13 | // val link: String, 14 | // val type: String 15 | //) 16 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/widgets/KeepScreenOn.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.widgets 2 | 3 | import android.view.View 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.viewinterop.AndroidView 6 | 7 | @Composable 8 | fun KeepScreenOn(flag: Boolean = true) { 9 | if (flag) AndroidView({ View(it).apply { keepScreenOn = true } }) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/log_out/LogOutUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.log_out 2 | 3 | import ru.tech.cookhelper.domain.repository.UserRepository 4 | import javax.inject.Inject 5 | 6 | class LogoutUseCase @Inject constructor( 7 | private val userRepository: UserRepository 8 | ) { 9 | suspend operator fun invoke() = userRepository.logOut() 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/get_feed/GetFeedUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.get_feed 2 | 3 | import ru.tech.cookhelper.domain.repository.UserRepository 4 | import javax.inject.Inject 5 | 6 | class GetFeedUseCase @Inject constructor( 7 | private val repository: UserRepository 8 | ) { 9 | operator fun invoke(token: String) = repository.getFeed(token) 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/widgets/zooomable/ZoomParams.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.widgets.zooomable 2 | 3 | import androidx.compose.ui.geometry.Offset 4 | 5 | data class ZoomParams( 6 | val zoomEnabled: Boolean = false, 7 | val hideBarsOnTap: Boolean = false, 8 | val minZoomScale: Float = 1f, 9 | val maxZoomScale: Float = 4f, 10 | val onTap: (Offset) -> Unit = {} 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/get_chat_list/GetChatListUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.get_chat_list 2 | 3 | import ru.tech.cookhelper.domain.repository.MessageRepository 4 | import javax.inject.Inject 5 | 6 | class GetChatListUseCase @Inject constructor( 7 | private val repository: MessageRepository 8 | ) { 9 | operator fun invoke(token: String) = repository.getChatList(token) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/load_user/LoadUserByIdUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.load_user 2 | 3 | import ru.tech.cookhelper.domain.repository.UserRepository 4 | import javax.inject.Inject 5 | 6 | class LoadUserByIdUseCase @Inject constructor( 7 | private val userRepository: UserRepository 8 | ) { 9 | suspend operator fun invoke(id: String) = userRepository.loadUserById(id) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/repository/SettingsRepository.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import ru.tech.cookhelper.domain.model.Setting 5 | 6 | interface SettingsRepository { 7 | 8 | fun getSettingsFlow(): Flow> 9 | 10 | suspend fun getSettings(): List 11 | 12 | suspend fun insertSetting(id: Int, option: String) 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/GetMatchedRecipesUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case 2 | 3 | import ru.tech.cookhelper.domain.repository.FridgeRepository 4 | import javax.inject.Inject 5 | 6 | class GetMatchedRecipesUseCase @Inject constructor( 7 | private val fridgeRepository: FridgeRepository 8 | ) { 9 | suspend operator fun invoke(token: String) = fridgeRepository.getMatchedRecipes(token) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/stop_awaiting_feed/StopAwaitingFeedUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.stop_awaiting_feed 2 | 3 | import ru.tech.cookhelper.domain.repository.UserRepository 4 | import javax.inject.Inject 5 | 6 | class StopAwaitingFeedUseCase @Inject constructor( 7 | private val repository: UserRepository 8 | ) { 9 | operator fun invoke() = repository.stopAwaitingFeed() 10 | } 11 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "CookHelper" 16 | include(":app") 17 | include(":dynamic_theme") 18 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/Chat.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | data class Chat( 6 | val id: Long, 7 | val images: List?, 8 | val title: String, 9 | val lastMessage: Message?, 10 | val newMessagesCount: Int, 11 | val members: List, 12 | val messages: List, 13 | val creationTimestamp: Long 14 | ) : Domain 15 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/stop_awaiting_messages/StopAwaitingMessagesUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.stop_awaiting_messages 2 | 3 | import ru.tech.cookhelper.domain.repository.MessageRepository 4 | import javax.inject.Inject 5 | 6 | class StopAwaitingMessagesUseCase @Inject constructor( 7 | private val repository: MessageRepository 8 | ) { 9 | operator fun invoke() = repository.stopAwaitingMessages() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/log_in/LoginUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.log_in 2 | 3 | import ru.tech.cookhelper.domain.repository.UserRepository 4 | import javax.inject.Inject 5 | 6 | class LoginUseCase @Inject constructor( 7 | private val userRepository: UserRepository 8 | ) { 9 | operator fun invoke( 10 | login: String, 11 | password: String 12 | ) = userRepository.loginWith(login, password) 13 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/check_code/CheckCodeUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.check_code 2 | 3 | import ru.tech.cookhelper.domain.repository.UserRepository 4 | import javax.inject.Inject 5 | 6 | class CheckCodeUseCase @Inject constructor( 7 | private val userRepository: UserRepository 8 | ) { 9 | operator fun invoke( 10 | code: String, 11 | token: String 12 | ) = userRepository.checkCode(code, token) 13 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/get_available_products/GetAvailableProductsUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.get_available_products 2 | 3 | import ru.tech.cookhelper.domain.repository.FridgeRepository 4 | import javax.inject.Inject 5 | 6 | class GetAvailableProductsUseCase @Inject constructor( 7 | private val fridgeRepository: FridgeRepository 8 | ) { 9 | suspend operator fun invoke() = fridgeRepository.getAvailableProducts() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/insert_setting/InsertSettingUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.insert_setting 2 | 3 | import ru.tech.cookhelper.domain.repository.SettingsRepository 4 | import javax.inject.Inject 5 | 6 | class InsertSettingUseCase @Inject constructor( 7 | private val repository: SettingsRepository 8 | ) { 9 | 10 | suspend operator fun invoke(id: Int, option: String) = repository.insertSetting(id, option) 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/cache_user/CacheUserUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.cache_user 2 | 3 | import ru.tech.cookhelper.domain.model.User 4 | import ru.tech.cookhelper.domain.repository.UserRepository 5 | import javax.inject.Inject 6 | 7 | class CacheUserUseCase @Inject constructor( 8 | private val userRepository: UserRepository 9 | ) { 10 | suspend operator fun invoke(user: User) = userRepository.cacheUser(user) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/web_socket/feed/FeedService.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.web_socket.feed 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import ru.tech.cookhelper.data.remote.dto.RecipeDto 5 | import ru.tech.cookhelper.data.remote.web_socket.Service 6 | import ru.tech.cookhelper.data.remote.web_socket.WebSocketState 7 | 8 | interface FeedService : Service { 9 | operator fun invoke(token: String): Flow>> 10 | } -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "ru.tech.cookhelper", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 6, 15 | "versionName": "0.1.23-alpha", 16 | "outputFile": "app-release.apk" 17 | } 18 | ], 19 | "elementType": "File" 20 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/restore_password/SendRestoreCodeUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.restore_password 2 | 3 | import ru.tech.cookhelper.domain.repository.UserRepository 4 | import javax.inject.Inject 5 | 6 | class SendRestoreCodeUseCase @Inject constructor( 7 | private val userRepository: UserRepository 8 | ) { 9 | suspend operator fun invoke( 10 | login: String 11 | ) = userRepository.requestPasswordRestoreCode(login) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/Post.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | data class Post( 6 | val id: Long, 7 | val author: User, 8 | val timestamp: Long, 9 | val label: String, 10 | val text: String, 11 | val likes: List, 12 | val comments: List, 13 | val reposts: List, 14 | val attachments: List, 15 | val images: List 16 | ) : Domain 17 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/send_message/SendMessagesUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.send_message 2 | 3 | import ru.tech.cookhelper.domain.model.FormMessage 4 | import ru.tech.cookhelper.domain.repository.MessageRepository 5 | import javax.inject.Inject 6 | 7 | class SendMessagesUseCase @Inject constructor( 8 | private val repository: MessageRepository 9 | ) { 10 | operator fun invoke(formMessage: FormMessage) = repository.sendMessage(formMessage) 11 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/dto/ProductDto.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.dto 2 | 3 | import ru.tech.cookhelper.data.remote.utils.Dto 4 | import ru.tech.cookhelper.domain.model.Product 5 | 6 | data class ProductDto( 7 | val id: Int, 8 | val title: String, 9 | val category: Int, 10 | val mimetype: String 11 | ) : Dto { 12 | override fun asDomain(): Product = Product( 13 | id = id, title = title, category = category, mimetype = mimetype 14 | ) 15 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/web_socket/message/MessageService.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.web_socket.message 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import ru.tech.cookhelper.data.remote.dto.MessageDto 5 | import ru.tech.cookhelper.data.remote.web_socket.Service 6 | import ru.tech.cookhelper.data.remote.web_socket.WebSocketState 7 | 8 | interface MessageService : Service { 9 | operator fun invoke(chatId: Long, token: String): Flow> 10 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/local/entity/SettingsEntity.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.local.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import ru.tech.cookhelper.data.local.utils.DatabaseEntity 6 | import ru.tech.cookhelper.domain.model.Setting 7 | 8 | @Entity 9 | data class SettingsEntity( 10 | @PrimaryKey val id: Int, 11 | val option: String 12 | ) : DatabaseEntity { 13 | override fun asDomain(): Setting = Setting(id = id, option = option) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/utils/MoshiParser.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.utils 2 | 3 | import com.squareup.moshi.Moshi 4 | import java.lang.reflect.Type 5 | 6 | class MoshiParser(private val moshi: Moshi) : JsonParser { 7 | 8 | override fun toJson(obj: T, type: Type): String? { 9 | return moshi.adapter(type).toJson(obj) 10 | } 11 | 12 | override fun fromJson(json: String, type: Type): T? { 13 | return moshi.adapter(type).fromJson(json) 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/observe_user/ObserveUserUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.observe_user 2 | 3 | import ru.tech.cookhelper.domain.repository.UserRepository 4 | import javax.inject.Inject 5 | 6 | class ObserveUserUseCase @Inject constructor( 7 | private val userRepository: UserRepository 8 | ) { 9 | operator fun invoke( 10 | id: Long, 11 | token: String 12 | ) = userRepository.observeUser( 13 | id = id, 14 | token = token 15 | ) 16 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/request_code/RequestCodeUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.request_code 2 | 3 | import ru.tech.cookhelper.domain.model.User 4 | import ru.tech.cookhelper.domain.repository.UserRepository 5 | import javax.inject.Inject 6 | 7 | class RequestCodeUseCase @Inject constructor( 8 | private val userRepository: UserRepository 9 | ) { 10 | suspend operator fun invoke( 11 | token: String 12 | ): Result = userRepository.requestCode(token) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/dto/MatchedRecipeDto.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.dto 2 | 3 | import ru.tech.cookhelper.data.remote.utils.Dto 4 | import ru.tech.cookhelper.domain.model.MatchedRecipe 5 | import ru.tech.cookhelper.domain.model.Recipe 6 | 7 | data class MatchedRecipeDto( 8 | val recipe: Recipe, 9 | val percentString: String 10 | ) : Dto { 11 | override fun asDomain(): MatchedRecipe = MatchedRecipe( 12 | recipe = recipe, 13 | percentString = percentString 14 | ) 15 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/Topic.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | data class Topic( 6 | val id: Long, 7 | val author: User, 8 | val title: String, 9 | val text: String, 10 | val replies: List, 11 | val attachments: List, 12 | val tags: List, 13 | val timestamp: Long, 14 | val closed: Boolean, 15 | val ratingPositive: List, 16 | val ratingNegative: List 17 | ) : Domain -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/event/ViewModelEvents.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.event 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface ViewModelEvents { 7 | val eventFlow: Flow 8 | fun sendEvent(event: T): Boolean 9 | } 10 | 11 | @Composable 12 | inline fun ViewModelEvents.collectEvents( 13 | noinline eventCollector: suspend (event: T) -> Unit 14 | ) = eventFlow.collectWithLifecycle(action = eventCollector) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/web_socket/user/UserService.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.web_socket.user 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import ru.tech.cookhelper.data.remote.dto.UserDto 5 | import ru.tech.cookhelper.data.remote.utils.Response 6 | import ru.tech.cookhelper.data.remote.web_socket.Service 7 | import ru.tech.cookhelper.data.remote.web_socket.WebSocketState 8 | 9 | interface UserService : Service { 10 | operator fun invoke(id: Long, token: String): Flow>> 11 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/android/ContentUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.android 2 | 3 | import androidx.activity.compose.ManagedActivityResultLauncher 4 | import androidx.activity.result.PickVisualMediaRequest 5 | import androidx.activity.result.contract.ActivityResultContracts 6 | 7 | object ContentUtils { 8 | 9 | fun ManagedActivityResultLauncher.pickImage() = 10 | launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/check_email/CheckEmailForAvailabilityUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.check_email 2 | 3 | import ru.tech.cookhelper.core.Action 4 | import ru.tech.cookhelper.domain.repository.UserRepository 5 | import javax.inject.Inject 6 | 7 | class CheckEmailForAvailabilityUseCase @Inject constructor( 8 | private val userRepository: UserRepository 9 | ) { 10 | suspend operator fun invoke( 11 | email: String 12 | ): Action = userRepository.checkEmailForAvailability(email) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/check_login/CheckLoginForAvailabilityUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.check_login 2 | 3 | import ru.tech.cookhelper.core.Action 4 | import ru.tech.cookhelper.domain.repository.UserRepository 5 | import javax.inject.Inject 6 | 7 | class CheckLoginForAvailabilityUseCase @Inject constructor( 8 | private val userRepository: UserRepository 9 | ) { 10 | suspend operator fun invoke( 11 | login: String 12 | ): Action = userRepository.checkLoginForAvailability(login) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/restore_password/ApplyPasswordByCodeUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.restore_password 2 | 3 | import ru.tech.cookhelper.domain.repository.UserRepository 4 | import javax.inject.Inject 5 | 6 | class ApplyPasswordByCodeUseCase @Inject constructor( 7 | private val userRepository: UserRepository 8 | ) { 9 | operator fun invoke( 10 | login: String, 11 | code: String, 12 | newPassword: String 13 | ) = userRepository.restorePasswordBy(login, code, newPassword) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/event/ViewModelEventsImpl.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.event 2 | 3 | import kotlinx.coroutines.channels.Channel 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.receiveAsFlow 6 | 7 | class ViewModelEventsImpl : ViewModelEvents { 8 | private val eventChannel: Channel = Channel(Channel.BUFFERED) 9 | override val eventFlow: Flow = eventChannel.receiveAsFlow() 10 | override fun sendEvent(event: T) = eventChannel.trySend(event).isSuccess 11 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/provider/LocalSnackbarHostState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.provider 2 | 3 | import androidx.compose.material3.SnackbarHostState 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.compositionLocalOf 6 | import androidx.compose.runtime.remember 7 | 8 | val LocalSnackbarHost = 9 | compositionLocalOf { error("SnackbarHostState not present") } 10 | 11 | @Composable 12 | fun rememberSnackbarHostState() = remember { SnackbarHostState() } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/provider/LocalToastHostState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.provider 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.compositionLocalOf 5 | import androidx.compose.runtime.remember 6 | import ru.tech.cookhelper.presentation.app.components.ToastHostState 7 | 8 | val LocalToastHostState = 9 | compositionLocalOf { error("ToastHostState not present") } 10 | 11 | @Composable 12 | fun rememberToastHostState() = remember { ToastHostState() } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/web_socket/WebSocketState.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.web_socket 2 | 3 | import okhttp3.Response 4 | 5 | sealed class WebSocketState { 6 | class Message(val obj: T?) : WebSocketState() 7 | class Error(val t: Throwable) : WebSocketState() 8 | class Opening : WebSocketState() 9 | class Opened(val response: Response) : WebSocketState() 10 | class Restarting : WebSocketState() 11 | class Closing : WebSocketState() 12 | class Closed : WebSocketState() 13 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/utils/JsonParser.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.utils 2 | 3 | import java.lang.reflect.Type 4 | 5 | interface JsonParser { 6 | 7 | /** 8 | * [type] is type of [obj]: [T], which is converted to json 9 | * 10 | * @return Json from given object 11 | */ 12 | fun toJson(obj: T, type: Type): String? 13 | 14 | /** 15 | * [type] is type of [T], which is will be parsed from json 16 | * 17 | * @return Object from given json 18 | */ 19 | fun fromJson(json: String, type: Type): T? 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/theme/Transitions.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.theme 2 | 3 | import androidx.compose.animation.* 4 | import androidx.compose.animation.core.tween 5 | import dev.olshevski.navigation.reimagined.NavTransitionSpec 6 | 7 | @OptIn(ExperimentalAnimationApi::class) 8 | val ScaleCrossfadeTransitionSpec = NavTransitionSpec { _, _, _ -> 9 | (fadeIn(tween(200)) + scaleIn(initialScale = 0.9f, animationSpec = tween(200))) 10 | .with(fadeOut(tween(200)) + scaleOut(targetScale = 0.9f, animationSpec = tween(200))) 11 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/add_products_to_fridge/AddProductsToFridgeUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.add_products_to_fridge 2 | 3 | import ru.tech.cookhelper.domain.model.Product 4 | import ru.tech.cookhelper.domain.repository.FridgeRepository 5 | import javax.inject.Inject 6 | 7 | class AddProductsToFridgeUseCase @Inject constructor( 8 | private val fridgeRepository: FridgeRepository 9 | ) { 10 | suspend operator fun invoke( 11 | token: String, 12 | fridge: List 13 | ) = fridgeRepository.addProductsToFridge(token, fridge) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/remove_products_from_fridge/RemoveProductsFromFridgeUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.remove_products_from_fridge 2 | 3 | import ru.tech.cookhelper.domain.model.Product 4 | import ru.tech.cookhelper.domain.repository.FridgeRepository 5 | import javax.inject.Inject 6 | 7 | class RemoveProductsFromFridgeUseCase @Inject constructor( 8 | private val fridgeRepository: FridgeRepository 9 | ) { 10 | suspend operator fun invoke( 11 | token: String, 12 | fridge: List 13 | ) = fridgeRepository.removeProductsFromFridge(token, fridge) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/api/chat/ChatApi.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.api.chat 2 | 3 | import retrofit2.http.GET 4 | import retrofit2.http.Query 5 | import ru.tech.cookhelper.data.remote.dto.ChatDto 6 | import ru.tech.cookhelper.data.remote.utils.Response 7 | 8 | interface ChatApi { 9 | 10 | @GET("api/chat/get/by-id/") 11 | suspend fun getChat( 12 | @Query("id") chatId: Long, 13 | @Query("token") token: String 14 | ): Response 15 | 16 | @GET("api/chat/get") 17 | suspend fun getChatList( 18 | @Query("token") token: String 19 | ): String 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/get_all_messages/GetAllMessagesUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.get_all_messages 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import ru.tech.cookhelper.core.Action 5 | import ru.tech.cookhelper.domain.model.Chat 6 | import ru.tech.cookhelper.domain.repository.MessageRepository 7 | import javax.inject.Inject 8 | 9 | class GetChatUseCase @Inject constructor( 10 | private val repository: MessageRepository 11 | ) { 12 | operator fun invoke( 13 | chatId: Long, 14 | token: String 15 | ): Flow> = repository.getChat(chatId, token) 16 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/create_topic/CreateTopicUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.create_topic 2 | 3 | import ru.tech.cookhelper.domain.repository.UserRepository 4 | import java.io.File 5 | import javax.inject.Inject 6 | 7 | class CreateTopicUseCase @Inject constructor( 8 | private val userRepository: UserRepository 9 | ) { 10 | operator fun invoke( 11 | token: String, 12 | title: String, 13 | text: String, 14 | attachments: List>, 15 | tags: List 16 | ) = userRepository.createTopic(token, title, text, attachments, tags) 17 | } -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/local/dao/UserDao.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.local.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import kotlinx.coroutines.flow.Flow 8 | import ru.tech.cookhelper.data.local.entity.UserEntity 9 | 10 | @Dao 11 | interface UserDao { 12 | 13 | @Query("SELECT * FROM userentity") 14 | fun getUser(): Flow 15 | 16 | @Insert(onConflict = OnConflictStrategy.REPLACE) 17 | suspend fun cacheUser(user: UserEntity) 18 | 19 | @Query("DELETE FROM userentity") 20 | suspend fun clearUser() 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/await_new_messages/AwaitNewMessagesUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.await_new_messages 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import ru.tech.cookhelper.core.Action 5 | import ru.tech.cookhelper.domain.model.Message 6 | import ru.tech.cookhelper.domain.repository.MessageRepository 7 | import javax.inject.Inject 8 | 9 | class AwaitNewMessagesUseCase @Inject constructor( 10 | private val repository: MessageRepository 11 | ) { 12 | operator fun invoke( 13 | chatId: Long, 14 | token: String 15 | ): Flow> = repository.awaitNewMessages(chatId, token) 16 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/utils/text/ChainTextValidator.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.utils.text 2 | 3 | class ChainTextValidator( 4 | private vararg val validators: TextValidator 5 | ) : TextValidator { 6 | override var validatorResult: ValidatorResult = ValidatorResult.NoResult() 7 | override fun validate(stringToValidate: String): ValidatorResult { 8 | validators.forEach { validator -> 9 | validatorResult = validator.validate(stringToValidate) 10 | if (validatorResult is ValidatorResult.Failure) return validatorResult 11 | } 12 | return validatorResult 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/widgets/Spacer.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.widgets 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | 7 | @Composable 8 | fun NavigationBarsSpacer() { 9 | Spacer(Modifier.padding(WindowInsets.navigationBars.asPaddingValues())) 10 | } 11 | 12 | @Composable 13 | fun StatusBarsSpacer() { 14 | Spacer(Modifier.padding(WindowInsets.statusBars.asPaddingValues())) 15 | } 16 | 17 | @Composable 18 | fun SystemBarsSpacer() { 19 | Spacer(Modifier.padding(WindowInsets.systemBars.asPaddingValues())) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/repository/FridgeRepository.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.repository 2 | 3 | import ru.tech.cookhelper.core.Action 4 | import ru.tech.cookhelper.domain.model.MatchedRecipe 5 | import ru.tech.cookhelper.domain.model.Product 6 | import ru.tech.cookhelper.domain.model.User 7 | 8 | interface FridgeRepository { 9 | suspend fun getAvailableProducts(): Action> 10 | suspend fun addProductsToFridge(token: String, fridge: List): Action 11 | suspend fun getMatchedRecipes(token: String): Action> 12 | suspend fun removeProductsFromFridge(token: String, fridge: List): Action 13 | } -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/android/ShareUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.android 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import ru.tech.cookhelper.R 6 | 7 | object ShareUtils { 8 | 9 | fun Context.shareWith(value: String?) { 10 | val intent = Intent().apply { 11 | action = Intent.ACTION_SEND 12 | putExtra(Intent.EXTRA_TEXT, value) 13 | type = "text/plain" 14 | } 15 | startActivity( 16 | Intent.createChooser( 17 | intent, 18 | getString(R.string.share) 19 | ) 20 | ) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/core/utils/kotlin/KotlinUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.core.utils.kotlin 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | 6 | inline fun T.applyCatching( 7 | block: T.() -> Unit 8 | ): T = apply { 9 | runCatching { block() } 10 | } 11 | 12 | inline fun Result.getOrExceptionAndNull(action: (Throwable) -> Unit): T? = try { 13 | getOrThrow() 14 | } catch (t: Throwable) { 15 | action(t) 16 | null 17 | } 18 | 19 | suspend fun runIo( 20 | function: suspend () -> T 21 | ): T = withContext(Dispatchers.IO) { function() } 22 | 23 | fun String.cptlize(): String = replaceFirstChar { it.titlecase() } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/repository/MessageRepository.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import ru.tech.cookhelper.core.Action 5 | import ru.tech.cookhelper.domain.model.Chat 6 | import ru.tech.cookhelper.domain.model.FormMessage 7 | import ru.tech.cookhelper.domain.model.Message 8 | 9 | interface MessageRepository { 10 | 11 | fun getChat(chatId: Long, token: String): Flow> 12 | 13 | fun awaitNewMessages(chatId: Long, token: String): Flow> 14 | 15 | fun sendMessage(message: FormMessage) 16 | 17 | fun stopAwaitingMessages() 18 | 19 | fun getChatList(token: String): Flow>> 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/local/dao/SettingsDao.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.local.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import kotlinx.coroutines.flow.Flow 8 | import ru.tech.cookhelper.data.local.entity.SettingsEntity 9 | 10 | @Dao 11 | interface SettingsDao { 12 | 13 | @Query("SELECT * FROM settingsentity") 14 | fun getSettingsFlow(): Flow> 15 | 16 | @Query("SELECT * FROM settingsentity") 17 | suspend fun getSettings(): List 18 | 19 | @Insert(onConflict = OnConflictStrategy.REPLACE) 20 | suspend fun insertSetting(setting: SettingsEntity) 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/create_post/CreatePostUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.create_post 2 | 3 | import ru.tech.cookhelper.domain.repository.UserRepository 4 | import java.io.File 5 | import javax.inject.Inject 6 | 7 | class CreatePostUseCase @Inject constructor( 8 | private val userRepository: UserRepository 9 | ) { 10 | operator fun invoke( 11 | token: String, 12 | label: String, 13 | content: String, 14 | imageFile: File?, 15 | type: String 16 | ) = userRepository.createPost( 17 | token = token, 18 | label = label, 19 | content = content, 20 | imageFile = imageFile, 21 | type = type 22 | ) 23 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/compose/ToastUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.compose 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.launch 7 | import ru.tech.cookhelper.presentation.app.components.ToastDuration 8 | import ru.tech.cookhelper.presentation.app.components.ToastHostState 9 | 10 | fun ToastHostState.show( 11 | icon: ImageVector? = null, 12 | message: String, 13 | duration: ToastDuration = ToastDuration.Short, 14 | scope: CoroutineScope = CoroutineScope(Dispatchers.Main.immediate) 15 | ) = scope.launch { showToast(message, icon, duration) } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/utils/text/validators/NonEmptyTextValidator.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.utils.text.validators 2 | 3 | import ru.tech.cookhelper.domain.utils.text.TextValidator 4 | import ru.tech.cookhelper.domain.utils.text.ValidatorResult 5 | 6 | class NonEmptyTextValidator( 7 | private val message: S 8 | ) : TextValidator { 9 | override var validatorResult: ValidatorResult = ValidatorResult.NoResult() 10 | override fun validate(stringToValidate: String): ValidatorResult { 11 | validatorResult = if (stringToValidate.trim().isEmpty()) 12 | ValidatorResult.Failure(message) 13 | else ValidatorResult.Success() 14 | 15 | return validatorResult 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/get_settings_list/GetSettingsListUseCaseFlow.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.get_settings_list 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import ru.tech.cookhelper.domain.model.Setting 5 | import ru.tech.cookhelper.domain.repository.SettingsRepository 6 | import javax.inject.Inject 7 | 8 | class GetSettingsListUseCaseFlow @Inject constructor( 9 | private val repository: SettingsRepository 10 | ) { 11 | operator fun invoke(): Flow> = repository.getSettingsFlow() 12 | } 13 | 14 | class GetSettingsListUseCase @Inject constructor( 15 | private val repository: SettingsRepository 16 | ) { 17 | suspend operator fun invoke(): List = repository.getSettings() 18 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/registration/RegistrationUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.registration 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import ru.tech.cookhelper.core.Action 5 | import ru.tech.cookhelper.domain.model.User 6 | import ru.tech.cookhelper.domain.repository.UserRepository 7 | import javax.inject.Inject 8 | 9 | class RegistrationUseCase @Inject constructor( 10 | private val userRepository: UserRepository 11 | ) { 12 | operator fun invoke( 13 | name: String, 14 | surname: String, 15 | nickname: String, 16 | email: String, 17 | password: String 18 | ): Flow> = userRepository.registerWith(name, surname, nickname, email, password) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/fullscreen_image_pager/FileDataSaver.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.fullscreen_image_pager 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.saveable.Saver 6 | import ru.tech.cookhelper.domain.model.FileData 7 | 8 | val FileDataSaver: Saver>, String> = Saver( 9 | save = { 10 | it.value.joinToString("*_*_*_*") { "${it.id}!_*_*_!${it.link}" } 11 | }, 12 | restore = { 13 | mutableStateOf( 14 | it.split("*_*_*_*").map { 15 | val t = it.split("!_*_*_!") 16 | FileData(t[1], t[0]) 17 | } 18 | ) 19 | } 20 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/settings/viewModel/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.settings.viewModel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import kotlinx.coroutines.launch 7 | import ru.tech.cookhelper.domain.use_case.insert_setting.InsertSettingUseCase 8 | import javax.inject.Inject 9 | 10 | @HiltViewModel 11 | class SettingsViewModel @Inject constructor( 12 | private val insertSettingUseCase: InsertSettingUseCase 13 | ) : ViewModel() { 14 | fun insertSetting(id: Int, option: Any?) { 15 | viewModelScope.launch { 16 | insertSettingUseCase(id, option.toString()) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/m3/M3Activity.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.m3 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.core.view.WindowCompat 6 | import ru.tech.cookhelper.presentation.crash_screen.CrashActivity 7 | import ru.tech.cookhelper.presentation.ui.utils.android.exception.GlobalExceptionHandler 8 | 9 | abstract class M3Activity : AppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | actionBar?.hide() 14 | WindowCompat.setDecorFitsSystemWindows(window, false) 15 | GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java) 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/use_case/close_connection/CloseConnectionsUseCase.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.use_case.close_connection 2 | 3 | import ru.tech.cookhelper.data.remote.web_socket.feed.FeedService 4 | import ru.tech.cookhelper.data.remote.web_socket.message.MessageService 5 | import ru.tech.cookhelper.data.remote.web_socket.user.UserService 6 | import javax.inject.Inject 7 | 8 | class CloseConnectionsUseCase @Inject constructor( 9 | private val messageService: MessageService, 10 | private val userService: UserService, 11 | private val feedService: FeedService 12 | ) { 13 | operator fun invoke() { 14 | listOf( 15 | messageService, userService, feedService 16 | ).forEach { it.closeService() } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.app 2 | 3 | import android.os.Bundle 4 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 5 | import dagger.hilt.android.AndroidEntryPoint 6 | import ru.tech.cookhelper.presentation.app.components.CookHelperApp 7 | import ru.tech.cookhelper.presentation.m3.M3Activity 8 | import ru.tech.cookhelper.presentation.ui.utils.provider.setContentWithWindowSizeClass 9 | 10 | @AndroidEntryPoint 11 | class MainActivity : M3Activity() { 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | installSplashScreen() 15 | super.onCreate(savedInstanceState) 16 | setContentWithWindowSizeClass { CookHelperApp() } 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/utils/text/ValidatorResult.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.utils.text 2 | 3 | sealed class ValidatorResult { 4 | class Failure(val message: S) : ValidatorResult() 5 | class NoResult : ValidatorResult() 6 | class Success : ValidatorResult() 7 | } 8 | 9 | inline fun ValidatorResult.onSuccess( 10 | action: () -> Unit 11 | ) = apply { 12 | if (this is ValidatorResult.Success) action() 13 | } 14 | 15 | inline fun ValidatorResult.onFailure( 16 | action: (S) -> Unit 17 | ) = apply { 18 | if (this is ValidatorResult.Failure) action(this.message) 19 | } 20 | 21 | inline fun ValidatorResult.isSuccess( 22 | action: (Boolean) -> Unit 23 | ) = apply { 24 | action(this is ValidatorResult.Success) 25 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/dto/MessageDto.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.dto 2 | 3 | import ru.tech.cookhelper.data.remote.utils.Dto 4 | import ru.tech.cookhelper.domain.model.FileData 5 | import ru.tech.cookhelper.domain.model.Message 6 | import ru.tech.cookhelper.domain.model.User 7 | 8 | data class MessageDto( 9 | val id: Long, 10 | val text: String, 11 | val attachments: List, 12 | val replyToId: Long, 13 | val views: List, 14 | val timestamp: Long, 15 | val author: User 16 | ) : Dto { 17 | override fun asDomain(): Message = Message( 18 | id = id, 19 | text = text, 20 | attachments = attachments, 21 | replyToId = replyToId, 22 | timestamp = timestamp, 23 | author = author 24 | ) 25 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/forum_discussion/components/TagGroup.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.forum_discussion.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.unit.dp 6 | import com.google.accompanist.flowlayout.FlowRow 7 | 8 | @Composable 9 | fun TagGroup( 10 | modifier: Modifier = Modifier, 11 | chips: List, 12 | onChipClick: (String) -> Unit 13 | ) { 14 | FlowRow( 15 | mainAxisSpacing = 8.dp, 16 | crossAxisSpacing = 8.dp, 17 | modifier = modifier 18 | ) { 19 | chips.forEach { chip -> 20 | TagItem( 21 | text = chip, 22 | onClick = { onChipClick(chip) } 23 | ) 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/recipe_post_creation/components/Separator.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.recipe_post_creation.components 2 | 3 | import androidx.compose.material3.Divider 4 | import androidx.compose.material3.DividerDefaults 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.unit.Dp 10 | 11 | @Composable 12 | fun Separator( 13 | modifier: Modifier = Modifier, 14 | color: Color = MaterialTheme.colorScheme.outlineVariant, 15 | thickness: Dp = DividerDefaults.Thickness 16 | ) { 17 | Divider( 18 | modifier = modifier, 19 | color = color, 20 | thickness = thickness 21 | ) 22 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/Reply.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | data class Reply( 6 | val author: User, 7 | val timestamp: Long, 8 | val attachments: List, 9 | val text: String, 10 | val id: Long, 11 | val replyToId: Long, 12 | val ratingPositive: List, 13 | val ratingNegative: List, 14 | val replies: List 15 | ) : Domain 16 | 17 | fun Reply.userRate(author: User): Int = when { 18 | ratingNegative.contains(author.id) -> -1 19 | ratingPositive.contains(author.id) -> 1 20 | else -> 0 21 | } 22 | 23 | fun Reply.getRating(): String { 24 | val rating = ratingPositive.size - ratingNegative.size 25 | return if (rating > 0) "+$rating" 26 | else "$rating" 27 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/utils/text/validators/EmailTextValidator.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.utils.text.validators 2 | 3 | import ru.tech.cookhelper.domain.utils.text.TextValidator 4 | import ru.tech.cookhelper.domain.utils.text.ValidatorResult 5 | import java.util.regex.Pattern 6 | 7 | class EmailTextValidator( 8 | private val message: S, 9 | private val pattern: Pattern 10 | ) : TextValidator { 11 | override var validatorResult: ValidatorResult = ValidatorResult.NoResult() 12 | override fun validate(stringToValidate: String): ValidatorResult { 13 | validatorResult = when { 14 | pattern.matcher(stringToValidate).matches() -> ValidatorResult.Success() 15 | else -> ValidatorResult.Failure(message) 16 | } 17 | return validatorResult 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/local/database/Database.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.local.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import ru.tech.cookhelper.data.local.dao.SettingsDao 6 | import ru.tech.cookhelper.data.local.dao.UserDao 7 | import ru.tech.cookhelper.data.local.entity.SettingsEntity 8 | import ru.tech.cookhelper.data.local.entity.UserEntity 9 | import androidx.room.TypeConverters as RoomTypeConverters 10 | 11 | @Database( 12 | entities = [SettingsEntity::class, UserEntity::class], 13 | exportSchema = false, version = 1, 14 | // autoMigrations = [AutoMigration(from = 1, to = 2)] 15 | ) 16 | @RoomTypeConverters(TypeConverters::class) 17 | abstract class Database : RoomDatabase() { 18 | abstract val settingsDao: SettingsDao 19 | abstract val userDao: UserDao 20 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/utils/text/validators/HasNumberTextValidator.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.utils.text.validators 2 | 3 | import ru.tech.cookhelper.domain.utils.text.TextValidator 4 | import ru.tech.cookhelper.domain.utils.text.ValidatorResult 5 | 6 | class HasNumberTextValidator( 7 | private val message: S, 8 | private val countOfNumbers: Int = 1 9 | ) : TextValidator { 10 | override var validatorResult: ValidatorResult = ValidatorResult.NoResult() 11 | override fun validate(stringToValidate: String): ValidatorResult { 12 | val count = stringToValidate.count { it.isDigit() } 13 | validatorResult = if (count >= countOfNumbers) { 14 | ValidatorResult.Success() 15 | } else ValidatorResult.Failure(message) 16 | 17 | return validatorResult 18 | } 19 | } -------------------------------------------------------------------------------- /dynamic_theme/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/authentication/components/LockScreenOrientaion.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.authentication.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.ui.platform.LocalContext 6 | import ru.tech.cookhelper.presentation.ui.utils.android.ContextUtils.findActivity 7 | 8 | @Composable 9 | fun LockScreenOrientation(orientation: Int) { 10 | val context = LocalContext.current 11 | DisposableEffect(Unit) { 12 | val activity = context.findActivity() ?: return@DisposableEffect onDispose {} 13 | val originalOrientation = activity.requestedOrientation 14 | activity.requestedOrientation = orientation 15 | onDispose { 16 | activity.requestedOrientation = originalOrientation 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/event/Event.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.event 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | import ru.tech.cookhelper.presentation.ui.utils.compose.UIText 5 | import ru.tech.cookhelper.presentation.ui.utils.navigation.Screen 6 | 7 | sealed class Event { 8 | class ShowSnackbar(val text: UIText, val action: () -> Unit) : Event() 9 | class ShowToast(val text: UIText, val icon: ImageVector? = null) : Event() 10 | class NavigateTo(val screen: Screen) : Event() 11 | class NavigateIf(val predicate: (Screen?) -> Boolean, val screen: Screen) : Event() 12 | 13 | class SendData(vararg val data: Pair) : Event() { 14 | val mappedData = data.toMap() 15 | inline operator fun get(key: String): T? = mappedData[key] as? T 16 | fun count() = mappedData.count() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/navigation/BottomSheet.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.navigation 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.IgnoredOnParcel 5 | import kotlinx.parcelize.Parcelize 6 | import ru.tech.cookhelper.domain.model.ForumFilters 7 | 8 | sealed class BottomSheet( 9 | val gesturesEnabled: Boolean = true, 10 | val dismissOnTapOutside: Boolean = true, 11 | val nestedScrollEnabled: Boolean = true, 12 | @IgnoredOnParcel val onDismiss: (() -> Unit)? = null 13 | ) : Parcelable { 14 | @Parcelize 15 | class ForumFilter( 16 | @IgnoredOnParcel val filters: ForumFilters = ForumFilters.empty(), 17 | @IgnoredOnParcel val onFiltersChange: (ForumFilters) -> Unit = {} 18 | ) : BottomSheet( 19 | gesturesEnabled = false, 20 | dismissOnTapOutside = false, 21 | nestedScrollEnabled = false 22 | ) 23 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/dto/ChatDto.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.dto 2 | 3 | import ru.tech.cookhelper.data.remote.utils.Dto 4 | import ru.tech.cookhelper.domain.model.Chat 5 | import ru.tech.cookhelper.domain.model.FileData 6 | import ru.tech.cookhelper.domain.model.Message 7 | 8 | data class ChatDto( 9 | val id: Long, 10 | val images: List?, 11 | val title: String, 12 | val lastMessage: Message?, 13 | val newMessagesCount: Int, 14 | val members: List, 15 | val messages: List, 16 | val creationTimestamp: Long 17 | ) : Dto { 18 | override fun asDomain(): Chat = Chat( 19 | id = id, 20 | images = images, 21 | title = title, 22 | lastMessage = lastMessage, 23 | newMessagesCount = newMessagesCount, 24 | members = members, 25 | messages = messages, 26 | creationTimestamp = creationTimestamp 27 | ) 28 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/android/ConfigurationUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.android 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.ui.unit.Dp 5 | import androidx.compose.ui.unit.dp 6 | import kotlin.math.max 7 | import kotlin.math.min 8 | 9 | object ConfigurationUtils { 10 | 11 | val Configuration.isLandscape: Boolean 12 | get() { 13 | return orientation == Configuration.ORIENTATION_LANDSCAPE 14 | } 15 | 16 | val Configuration.isPortrait: Boolean 17 | get() { 18 | return orientation == Configuration.ORIENTATION_PORTRAIT 19 | } 20 | 21 | val Configuration.maxScreenDp: Dp 22 | get() { 23 | return max(screenHeightDp, screenWidthDp).dp 24 | } 25 | 26 | val Configuration.minScreenDp: Dp 27 | get() { 28 | return min(screenHeightDp, screenWidthDp).dp 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/User.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | data class User( 6 | val id: Long, 7 | val avatar: List = emptyList(), 8 | val bannedIngredients: List? = null, 9 | val bannedRecipes: List? = null, 10 | val email: String, 11 | val forums: List? = null, 12 | val fridge: List = emptyList(), 13 | val name: String, 14 | val nickname: String, 15 | val starredIngredients: List? = null, 16 | val userPosts: List? = null, 17 | val userRecipes: List? = null, 18 | val starredRecipes: List? = null, 19 | val status: String? = "", 20 | val verified: Boolean = false, 21 | val surname: String, 22 | val lastSeen: Long = 0, 23 | val token: String = "" 24 | ) : Domain 25 | 26 | fun User?.getLastAvatar(): String? = this?.avatar?.lastOrNull()?.link -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/provider/LocalBottomSheetController.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.provider 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import dev.olshevski.navigation.reimagined.NavController 5 | import dev.olshevski.navigation.reimagined.popAll 6 | import ru.tech.cookhelper.presentation.ui.utils.navigation.BottomSheet 7 | import ru.tech.cookhelper.presentation.ui.widgets.bottomsheet.BottomSheetState 8 | 9 | val LocalBottomSheetController = 10 | compositionLocalOf { error("BottomSheetController not present") } 11 | 12 | suspend fun BottomSheetController.show(sheet: BottomSheet) { 13 | controller.navigateAndPopAll(sheet) 14 | state.expand() 15 | } 16 | 17 | suspend fun BottomSheetController.close() { 18 | state.collapse() 19 | controller.popAll() 20 | } 21 | 22 | data class BottomSheetController( 23 | val controller: NavController, 24 | val state: BottomSheetState 25 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/dto/PostDto.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.dto 2 | 3 | import ru.tech.cookhelper.data.remote.utils.Dto 4 | import ru.tech.cookhelper.domain.model.FileData 5 | import ru.tech.cookhelper.domain.model.Post 6 | import ru.tech.cookhelper.domain.model.User 7 | 8 | data class PostDto( 9 | val id: Long, 10 | val author: User, 11 | val timestamp: Long, 12 | val label: String, 13 | val text: String, 14 | val likes: List, 15 | val comments: List, 16 | val reposts: List, 17 | val attachments: List, 18 | val images: List 19 | ) : Dto { 20 | override fun asDomain(): Post = Post( 21 | id = id, 22 | author = author, 23 | timestamp = timestamp, 24 | label = label, 25 | text = text, 26 | likes = likes, 27 | comments = comments, 28 | reposts = reposts, 29 | attachments = attachments, 30 | images = attachments 31 | ) 32 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/core/constants/Constants.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.core.constants 2 | 3 | object Constants { 4 | 5 | val DOTS = "." * 100 6 | 7 | const val DELIMITER = "*" 8 | 9 | const val HOST_URL = "192.168.43.51:8080" 10 | 11 | const val BASE_URL = "http://$HOST_URL/" 12 | 13 | const val WS_BASE_URL = "ws://$HOST_URL/" 14 | 15 | const val LOREM_IPSUM = 16 | "Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 17 | 18 | } 19 | 20 | private operator fun String.times(count: Int): String { 21 | var s = this 22 | repeat(count) { s += this } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/android/Logger.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.android 2 | 3 | import android.util.Log 4 | 5 | object Logger { 6 | 7 | fun makeLog( 8 | any: Any?, 9 | tag: String = this::class.java.simpleName, 10 | level: Level = Level.DEBUG 11 | ) { 12 | when (level) { 13 | Level.VERBOSE -> Log.v(tag, any.toString()) 14 | Level.DEBUG -> Log.d(tag, any.toString()) 15 | Level.INFO -> Log.i(tag, any.toString()) 16 | Level.WARN -> Log.w(tag, any.toString()) 17 | Level.ERROR -> Log.e(tag, any.toString()) 18 | } 19 | } 20 | 21 | fun Any?.log( 22 | tag: String = (this@Logger)::class.java.simpleName, 23 | level: Level = Level.DEBUG 24 | ) { 25 | makeLog( 26 | any = this, 27 | tag = tag, 28 | level = level 29 | ) 30 | } 31 | 32 | enum class Level { 33 | VERBOSE, DEBUG, INFO, WARN, ERROR 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/ForumFilters.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | import ru.tech.cookhelper.domain.utils.Domain 4 | 5 | data class ForumFilters( 6 | val queryString: String, 7 | val noRepliesFilter: Boolean, 8 | val imageFilter: Boolean, 9 | val ratingNeutralFilter: Boolean, 10 | val ratingPositiveFilter: Boolean, 11 | val ratingNegativeFilter: Boolean, 12 | val tagFilter: String, 13 | val ratingSort: Boolean, 14 | val recencySort: Boolean, 15 | val reverseSort: Boolean, 16 | ) : Domain { 17 | companion object { 18 | fun empty() = ForumFilters( 19 | queryString = "", 20 | noRepliesFilter = false, 21 | imageFilter = false, 22 | ratingNeutralFilter = false, 23 | ratingPositiveFilter = false, 24 | ratingNegativeFilter = false, 25 | tagFilter = "", 26 | ratingSort = false, 27 | recencySort = true, 28 | reverseSort = false 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/theme/Typography.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | val Typography = Typography( 10 | bodyLarge = TextStyle( 11 | fontFamily = FontFamily.Default, 12 | fontWeight = FontWeight.Normal, 13 | fontSize = 16.sp, 14 | lineHeight = 24.sp, 15 | letterSpacing = 0.5.sp 16 | ), 17 | titleLarge = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.Normal, 20 | fontSize = 22.sp, 21 | lineHeight = 28.sp, 22 | letterSpacing = 0.sp 23 | ), 24 | labelSmall = TextStyle( 25 | fontFamily = FontFamily.Default, 26 | fontWeight = FontWeight.Medium, 27 | fontSize = 11.sp, 28 | lineHeight = 16.sp, 29 | letterSpacing = 0.5.sp 30 | ) 31 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/utils/text/validators/LengthTextValidator.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.utils.text.validators 2 | 3 | import ru.tech.cookhelper.domain.utils.text.TextValidator 4 | import ru.tech.cookhelper.domain.utils.text.ValidatorResult 5 | 6 | class LengthTextValidator( 7 | private val minLength: Int? = null, 8 | private val maxLength: Int? = null, 9 | private val message: (minNotReached: Boolean, maxOverflowed: Boolean) -> S 10 | ) : TextValidator { 11 | override var validatorResult: ValidatorResult = ValidatorResult.NoResult() 12 | override fun validate(stringToValidate: String): ValidatorResult { 13 | validatorResult = when { 14 | minLength != null && stringToValidate.count() < minLength -> 15 | ValidatorResult.Failure(message(true, false)) 16 | maxLength != null && stringToValidate.count() > maxLength -> 17 | ValidatorResult.Failure(message(false, true)) 18 | else -> ValidatorResult.Success() 19 | } 20 | return validatorResult 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/web_socket/message/MessageServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.web_socket.message 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import ru.tech.cookhelper.core.constants.Constants 5 | import ru.tech.cookhelper.data.remote.dto.MessageDto 6 | import ru.tech.cookhelper.data.remote.web_socket.WebSocketClient 7 | import ru.tech.cookhelper.data.remote.web_socket.WebSocketState 8 | import ru.tech.cookhelper.data.utils.JsonParser 9 | import javax.inject.Inject 10 | 11 | class MessageServiceImpl @Inject constructor( 12 | jsonParser: JsonParser, 13 | ) : WebSocketClient(jsonParser = jsonParser), MessageService { 14 | 15 | override operator fun invoke( 16 | chatId: Long, token: String 17 | ): Flow> = setBaseUrl( 18 | newBaseUrl = "${Constants.WS_BASE_URL}websocket/chat/?token=$token&id=$chatId" 19 | ).setType(MessageDto::class.java).openWebSocket().receiveAsFlow() 20 | 21 | override fun sendMessage(data: String) = send(data) 22 | 23 | override fun closeService() = close() 24 | 25 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/repository/SettingsRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.map 5 | import ru.tech.cookhelper.data.local.dao.SettingsDao 6 | import ru.tech.cookhelper.data.local.entity.SettingsEntity 7 | import ru.tech.cookhelper.domain.model.Setting 8 | import ru.tech.cookhelper.domain.repository.SettingsRepository 9 | import javax.inject.Inject 10 | 11 | class SettingsRepositoryImpl @Inject constructor( 12 | private val settingsDao: SettingsDao 13 | ) : SettingsRepository { 14 | 15 | override fun getSettingsFlow(): Flow> = 16 | settingsDao.getSettingsFlow() 17 | .map { settingsList -> 18 | settingsList.map { entity -> entity.asDomain() } 19 | } 20 | 21 | override suspend fun getSettings(): List = 22 | settingsDao.getSettings().map { it.asDomain() } 23 | 24 | override suspend fun insertSetting(id: Int, option: String) { 25 | settingsDao.insertSetting(SettingsEntity(id, option)) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/dto/TopicDto.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.dto 2 | 3 | import ru.tech.cookhelper.data.remote.utils.Dto 4 | import ru.tech.cookhelper.domain.model.FileData 5 | import ru.tech.cookhelper.domain.model.Reply 6 | import ru.tech.cookhelper.domain.model.Topic 7 | import ru.tech.cookhelper.domain.model.User 8 | 9 | data class TopicDto( 10 | val id: Long, 11 | val author: User, 12 | val title: String, 13 | val text: String, 14 | val replies: List, 15 | val attachments: List, 16 | val tags: List, 17 | val timestamp: Long, 18 | val closed: Boolean, 19 | val ratingPositive: List, 20 | val ratingNegative: List 21 | ) : Dto { 22 | override fun asDomain(): Topic = Topic( 23 | id = id, 24 | author = author, 25 | title = title, 26 | text = text, 27 | replies = replies, 28 | attachments = attachments, 29 | tags = tags, 30 | timestamp = timestamp, 31 | closed = closed, 32 | ratingPositive = ratingPositive, 33 | ratingNegative = ratingNegative 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/core/utils/ConnectionUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.core.utils 2 | 3 | import android.content.Context 4 | import android.content.Context.CONNECTIVITY_SERVICE 5 | import android.net.ConnectivityManager 6 | import android.net.NetworkCapabilities 7 | import android.os.Build 8 | 9 | object ConnectionUtils { 10 | fun Context.isOnline(): Boolean { 11 | (getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager).apply { 12 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 13 | getNetworkCapabilities(activeNetwork) 14 | ?.run { 15 | listOf( 16 | hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR), 17 | hasTransport(NetworkCapabilities.TRANSPORT_WIFI), 18 | hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET), 19 | ).any { it } 20 | } ?: false 21 | } else @Suppress("DEPRECATION") { 22 | activeNetworkInfo != null && activeNetworkInfo?.isConnected == true 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/recipe_post_creation/components/FabSize.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.recipe_post_creation.components 2 | 3 | import androidx.compose.material3.FloatingActionButtonDefaults 4 | import androidx.compose.ui.graphics.Shape 5 | import androidx.compose.ui.unit.Dp 6 | import androidx.compose.ui.unit.dp 7 | import ru.tech.cookhelper.presentation.ui.theme.SquircleShape 8 | 9 | sealed class FabSize( 10 | val horizontalPadding: Dp, 11 | val spacerPadding: Dp, 12 | val iconSize: Dp, 13 | val shape: Shape 14 | ) { 15 | object Small : FabSize( 16 | iconSize = 24.dp, 17 | spacerPadding = 4.dp, 18 | horizontalPadding = 8.dp, 19 | shape = SquircleShape(12.dp) 20 | ) 21 | 22 | object Common : FabSize( 23 | iconSize = 24.dp, 24 | spacerPadding = 12.dp, 25 | horizontalPadding = 16.dp, 26 | shape = SquircleShape(16.dp) 27 | ) 28 | 29 | object Large : FabSize( 30 | iconSize = FloatingActionButtonDefaults.LargeIconSize, 31 | spacerPadding = 20.dp, 32 | horizontalPadding = 24.dp, 33 | shape = SquircleShape(28.dp) 34 | ) 35 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/fridge_screen/components/ProductUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.fridge_screen.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Egg 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import ru.tech.cookhelper.domain.model.Product 7 | import ru.tech.cookhelper.presentation.ui.theme.* 8 | 9 | fun Product.getIcon(): ImageVector = when (this.category) { 10 | 1 -> Icons.Filled.Steak 11 | 2 -> Icons.Filled.Fish 12 | 3 -> Icons.Filled.Milk 13 | 4 -> Icons.Filled.Egg 14 | 5 -> Icons.Filled.Carrot 15 | 6 -> Icons.Filled.Apple 16 | 7 -> Icons.Filled.Baguette 17 | 8 -> Icons.Filled.Barley 18 | 9 -> Icons.Filled.Shaker 19 | 10 -> Icons.Filled.Candy 20 | 11 -> Icons.Filled.Cup 21 | 12 -> Icons.Filled.Bean 22 | 13 -> Icons.Filled.Mushroom 23 | 14 -> Icons.Filled.Jellyfish 24 | 15 -> Icons.Filled.Flavour 25 | 16 -> Icons.Filled.Peanut 26 | 17 -> Icons.Filled.DriedGrape 27 | 18 -> Icons.Filled.Cheese 28 | 19 -> Icons.Filled.Cherry 29 | 20 -> Icons.Filled.Oil 30 | else -> Icons.Filled.BorderRadius 31 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/api/ingredients/FridgeApi.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.api.ingredients 2 | 3 | import retrofit2.http.* 4 | import ru.tech.cookhelper.data.remote.dto.MatchedRecipeDto 5 | import ru.tech.cookhelper.data.remote.dto.ProductDto 6 | import ru.tech.cookhelper.data.remote.dto.UserDto 7 | import ru.tech.cookhelper.data.remote.utils.Response 8 | 9 | interface FridgeApi { 10 | 11 | @GET("api/ingredient/get/all/") 12 | suspend fun getAvailableProducts(): Result>> 13 | 14 | @Multipart 15 | @POST("api/user/post/fridge/insert/") 16 | suspend fun addProductsToFridge( 17 | @Part("token") token: String, 18 | @Part("fridge") fridge: String 19 | ): Result> 20 | 21 | @Multipart 22 | @POST("api/user/post/fridge/remove/") 23 | suspend fun removeProductsFromFridge( 24 | @Part("token") token: String, 25 | @Part("fridge") fridge: String 26 | ): Result> 27 | 28 | @GET("api/user/get/fridge/recipe/") 29 | suspend fun getMatchedRecipes( 30 | @Query("token") token: String 31 | ): Result>> 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/settings/components/RotationButton.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.settings.components 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.rounded.KeyboardArrowDown 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.IconButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.rotate 13 | import androidx.compose.ui.unit.dp 14 | 15 | @Composable 16 | fun RotationButton( 17 | modifier: Modifier = Modifier, 18 | rotated: Boolean = false, 19 | onClick: () -> Unit, 20 | ) { 21 | val rotation: Float by animateFloatAsState(if (rotated) 180f else 0f) 22 | IconButton( 23 | onClick = onClick, 24 | modifier = modifier.rotate(rotation), 25 | ) { 26 | Icon( 27 | imageVector = Icons.Rounded.KeyboardArrowDown, 28 | contentDescription = null, 29 | modifier = Modifier.size(26.dp) 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/event/EventUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.event 2 | 3 | import androidx.compose.runtime.* 4 | import androidx.compose.ui.platform.LocalLifecycleOwner 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.lifecycle.LifecycleOwner 7 | import androidx.lifecycle.flowWithLifecycle 8 | import androidx.lifecycle.lifecycleScope 9 | import kotlinx.coroutines.Job 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.launch 12 | 13 | @Composable 14 | inline fun Flow.collectWithLifecycle( 15 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, 16 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED, 17 | noinline action: suspend (T) -> Unit 18 | ) { 19 | var job by remember { mutableStateOf(null) } 20 | LaunchedEffect(Unit) { 21 | job = lifecycleOwner.lifecycleScope.launch { 22 | flowWithLifecycle( 23 | lifecycle = lifecycleOwner.lifecycle, 24 | minActiveState = minActiveState 25 | ).collect(collector = action) 26 | } 27 | } 28 | DisposableEffect(Unit) { 29 | onDispose { job?.cancel() } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/model/Recipe.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.model 2 | 3 | import ru.tech.cookhelper.core.constants.Constants.BASE_URL 4 | import ru.tech.cookhelper.domain.utils.Domain 5 | 6 | data class Recipe( 7 | val id: Long = 0, 8 | val author: User, 9 | val title: String, 10 | val cookSteps: List, 11 | val time: Long, 12 | val category: String, 13 | val ingredients: List, 14 | val measures: List, 15 | val proteins: Double, 16 | val carbohydrates: Double, 17 | val fats: Double, 18 | val calories: Double, 19 | val image: FileData, 20 | val comments: List = listOf(), 21 | val reposts: List = listOf(), 22 | val likes: List = listOf(), 23 | val timestamp: Long 24 | ) : Domain { 25 | fun toShareValue(): String { 26 | val n = "\n\n" 27 | return "$title${n}Категория - $category${n}Время приготовления - $time мин${n}Б/Ж/У - $proteins/$fats/${carbohydrates}${n}Калории - ${calories}$n${ 28 | ingredients.joinToString( 29 | ", " 30 | ) { it.title } 31 | }${n}${ 32 | cookSteps.joinToString(n) 33 | }${BASE_URL}recipe/$id" 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/api/user/UserApi.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.api.user 2 | 3 | import okhttp3.MultipartBody 4 | import retrofit2.Call 5 | import retrofit2.http.* 6 | import ru.tech.cookhelper.data.remote.dto.PostDto 7 | import ru.tech.cookhelper.data.remote.dto.RecipeDto 8 | import ru.tech.cookhelper.data.remote.dto.TopicDto 9 | import ru.tech.cookhelper.data.remote.utils.Response 10 | 11 | interface UserApi { 12 | 13 | @GET("api/user/get/feed/") 14 | suspend fun getFeed( 15 | @Query("token") token: String 16 | ): Result>> 17 | 18 | @Multipart 19 | @POST("api/feed/post/create/") 20 | fun createPost( 21 | @Part("token") token: String, 22 | @Part("label") label: String, 23 | @Part("text") text: String, 24 | @Part image: MultipartBody.Part? 25 | ): Call> 26 | 27 | @Multipart 28 | @POST("api/forum/post/topic/create/") 29 | fun createTopic( 30 | @Part("token") token: String, 31 | @Part("title") title: String, 32 | @Part("text") text: String, 33 | @Part files: MultipartBody.Part?, 34 | @Part("tags") tags: List 35 | ): Call> 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/web_socket/user/UserServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.web_socket.user 2 | 3 | import com.squareup.moshi.Types 4 | import kotlinx.coroutines.flow.Flow 5 | import ru.tech.cookhelper.core.constants.Constants 6 | import ru.tech.cookhelper.data.remote.dto.UserDto 7 | import ru.tech.cookhelper.data.remote.utils.Response 8 | import ru.tech.cookhelper.data.remote.web_socket.WebSocketClient 9 | import ru.tech.cookhelper.data.remote.web_socket.WebSocketState 10 | import ru.tech.cookhelper.data.utils.JsonParser 11 | import javax.inject.Inject 12 | 13 | class UserServiceImpl @Inject constructor( 14 | jsonParser: JsonParser 15 | ) : WebSocketClient>(jsonParser = jsonParser), UserService { 16 | 17 | override operator fun invoke( 18 | id: Long, 19 | token: String 20 | ): Flow>> = setBaseUrl( 21 | newBaseUrl = "${Constants.WS_BASE_URL}websocket/user/?id=$id&token=$token" 22 | ).setType( 23 | Types.newParameterizedType(Response::class.java, UserDto::class.java) 24 | ).openWebSocket().receiveAsFlow() 25 | 26 | override fun sendMessage(data: String) = send(data) 27 | 28 | override fun closeService() = close() 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/provider/LocalWindowSizeClass.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.provider 2 | 3 | import android.app.Activity 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi 7 | import androidx.compose.material3.windowsizeclass.WindowSizeClass 8 | import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.CompositionLocalProvider 11 | import androidx.compose.runtime.compositionLocalOf 12 | 13 | val LocalWindowSizeClass = compositionLocalOf { error("SizeClass not present") } 14 | 15 | @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) 16 | @Composable 17 | fun Activity.provideWindowSizeClass(content: @Composable () -> Unit) { 18 | CompositionLocalProvider( 19 | LocalWindowSizeClass provides calculateWindowSizeClass(this), 20 | content = content 21 | ) 22 | } 23 | 24 | fun ComponentActivity.setContentWithWindowSizeClass( 25 | content: @Composable () -> Unit 26 | ) = setContent { 27 | provideWindowSizeClass(content = content) 28 | } 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/web_socket/feed/FeedServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.web_socket.feed 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.flow 5 | import ru.tech.cookhelper.data.remote.dto.RecipeDto 6 | import ru.tech.cookhelper.data.remote.web_socket.WebSocketClient 7 | import ru.tech.cookhelper.data.remote.web_socket.WebSocketState 8 | import ru.tech.cookhelper.data.utils.JsonParser 9 | import javax.inject.Inject 10 | 11 | class FeedServiceImpl @Inject constructor( 12 | jsonParser: JsonParser 13 | ) : WebSocketClient>(jsonParser = jsonParser), FeedService { 14 | 15 | override operator fun invoke( 16 | token: String 17 | ): Flow>> = flow { 18 | emit(WebSocketState.Opening()) 19 | } 20 | 21 | override fun sendMessage(data: String) = send(data) 22 | 23 | override fun closeService() = close() 24 | 25 | //updateBaseUrl( 26 | // newBaseUrl = "${Constants.WS_BASE_URL}ws/feed/?token=$token" 27 | // ).setType( 28 | // Types.newParameterizedType( 29 | // List::class.java, 30 | // RecipePostDto::class.java 31 | // ) 32 | // ).openWebSocket().receiveAsFlow() 33 | 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/core/di/RoomModule.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.core.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import dagger.hilt.components.SingletonComponent 10 | import ru.tech.cookhelper.data.local.database.Database 11 | import ru.tech.cookhelper.data.local.database.TypeConverters 12 | import ru.tech.cookhelper.data.utils.JsonParser 13 | import javax.inject.Singleton 14 | 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | object RoomModule { 19 | 20 | @Provides 21 | @Singleton 22 | fun provideDatabase( 23 | @ApplicationContext applicationContext: Context, 24 | typeConverters: TypeConverters 25 | ): Database = Room.databaseBuilder( 26 | context = applicationContext, 27 | klass = Database::class.java, 28 | name = "CookHelperDatabase" 29 | ).addTypeConverter(typeConverters) 30 | .fallbackToDestructiveMigration() 31 | .fallbackToDestructiveMigrationOnDowngrade() 32 | .build() 33 | 34 | @Singleton 35 | @Provides 36 | fun provideTypeConverters( 37 | jsonParser: JsonParser 38 | ): TypeConverters = TypeConverters(jsonParser) 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/recipe_details/viewModel/RecipeDetailsViewModel.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.recipe_details.viewModel 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.lifecycle.SavedStateHandle 7 | import androidx.lifecycle.ViewModel 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import ru.tech.cookhelper.domain.model.Recipe 10 | import ru.tech.cookhelper.domain.use_case.get_user.GetUserUseCase 11 | import ru.tech.cookhelper.presentation.ui.utils.compose.StateUtils.update 12 | import ru.tech.cookhelper.presentation.ui.utils.event.Event 13 | import ru.tech.cookhelper.presentation.ui.utils.event.ViewModelEvents 14 | import ru.tech.cookhelper.presentation.ui.utils.event.ViewModelEventsImpl 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class RecipeDetailsViewModel @Inject constructor( 19 | private val getUserUseCase: GetUserUseCase, 20 | savedStateHandle: SavedStateHandle 21 | ) : ViewModel(), ViewModelEvents by ViewModelEventsImpl() { 22 | 23 | private val _recipe: MutableState = mutableStateOf(null) 24 | val recipe: Recipe? by _recipe 25 | 26 | fun updateRecipe(recipe: Recipe?) { 27 | _recipe.update { recipe } 28 | } 29 | 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/compose/SnackbarUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.compose 2 | 3 | import androidx.compose.material3.SnackbarDuration 4 | import androidx.compose.material3.SnackbarHostState 5 | import androidx.compose.material3.SnackbarResult 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.launch 9 | 10 | object SnackbarUtils { 11 | 12 | fun SnackbarHostState.show( 13 | scope: CoroutineScope = CoroutineScope(Dispatchers.Main), 14 | message: String, 15 | actionLabel: String? = null, 16 | duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite, 17 | result: (SnackbarResult) -> Unit = {} 18 | ) = scope.launch { 19 | result( 20 | showSnackbar( 21 | message = message, 22 | actionLabel = actionLabel, 23 | duration = duration 24 | ) 25 | ) 26 | } 27 | 28 | 29 | inline val SnackbarResult.actionPerformed: Boolean 30 | get() { 31 | return this == SnackbarResult.ActionPerformed 32 | } 33 | 34 | inline val SnackbarResult.dismissed: Boolean 35 | get() { 36 | return this == SnackbarResult.Dismissed 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/app/components/ExitDialog.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.app.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.DoorBack 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.res.stringResource 8 | import androidx.compose.ui.text.style.TextAlign 9 | import ru.tech.cookhelper.R 10 | import ru.tech.cookhelper.presentation.ui.theme.DialogShape 11 | 12 | @Composable 13 | fun ExitDialog(onExit: () -> Unit, onDismissRequest: () -> Unit) { 14 | AlertDialog( 15 | title = { Text(stringResource(R.string.app_closing)) }, 16 | text = { 17 | Text( 18 | stringResource(R.string.app_closing_message), 19 | textAlign = TextAlign.Center 20 | ) 21 | }, 22 | shape = DialogShape, 23 | onDismissRequest = onDismissRequest, 24 | icon = { Icon(Icons.Outlined.DoorBack, null) }, 25 | confirmButton = { 26 | Button(onClick = onDismissRequest) { 27 | Text(stringResource(R.string.stay)) 28 | } 29 | }, 30 | dismissButton = { 31 | FilledTonalButton(onClick = onExit) { 32 | Text(stringResource(R.string.exit)) 33 | } 34 | } 35 | ) 36 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/widgets/Placeholder.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.widgets 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.vector.ImageVector 13 | import androidx.compose.ui.text.style.TextAlign 14 | import androidx.compose.ui.unit.dp 15 | import ru.tech.cookhelper.presentation.ui.utils.compose.navigationBarsLandscapePadding 16 | 17 | @Composable 18 | fun Placeholder( 19 | icon: ImageVector, 20 | text: String, 21 | modifier: Modifier = Modifier 22 | ) { 23 | Column( 24 | modifier = if (modifier == Modifier) Modifier 25 | .fillMaxSize() 26 | .padding(horizontal = 8.dp) 27 | .navigationBarsLandscapePadding() 28 | else modifier, 29 | verticalArrangement = Arrangement.Center, 30 | horizontalAlignment = Alignment.CenterHorizontally 31 | ) { 32 | Icon(icon, null, modifier = Modifier.fillMaxSize(0.3f)) 33 | Text(text, textAlign = TextAlign.Center) 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/profile/components/LogoutDialog.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.profile.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.Logout 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.res.stringResource 8 | import androidx.compose.ui.text.style.TextAlign 9 | import ru.tech.cookhelper.R 10 | import ru.tech.cookhelper.presentation.ui.theme.DialogShape 11 | 12 | @Composable 13 | fun LogoutDialog(onLogout: () -> Unit, onDismissRequest: () -> Unit) { 14 | AlertDialog( 15 | title = { Text(stringResource(R.string.account_log_out)) }, 16 | text = { 17 | Text( 18 | stringResource(R.string.log_out_message), 19 | textAlign = TextAlign.Center 20 | ) 21 | }, 22 | shape = DialogShape, 23 | onDismissRequest = { onDismissRequest() }, 24 | icon = { Icon(Icons.Outlined.Logout, null) }, 25 | confirmButton = { 26 | Button(onClick = { onDismissRequest() }) { 27 | Text(stringResource(R.string.stay)) 28 | } 29 | }, 30 | dismissButton = { 31 | FilledTonalButton(onClick = { 32 | onLogout() 33 | onDismissRequest() 34 | }) { 35 | Text(stringResource(R.string.exit)) 36 | } 37 | } 38 | ) 39 | } -------------------------------------------------------------------------------- /dynamic_theme/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | namespace = "com.cookhelper.dynamic.dynamictheme" 8 | compileSdk = 33 9 | 10 | defaultConfig { 11 | minSdk = 21 12 | targetSdk = 33 13 | 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles("consumer-rules.pro") 16 | } 17 | 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = false 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro" 24 | ) 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_1_8 29 | targetCompatibility = JavaVersion.VERSION_1_8 30 | } 31 | kotlinOptions { 32 | jvmTarget = "1.8" 33 | freeCompilerArgs += "-Xexplicit-api=strict" 34 | } 35 | buildFeatures { 36 | compose = true 37 | } 38 | composeOptions { 39 | kotlinCompilerExtensionVersion = "1.4.2" 40 | } 41 | } 42 | 43 | dependencies { 44 | implementation(platform("androidx.compose:compose-bom:2023.01.00")) 45 | implementation("androidx.core:core-ktx:1.9.0") 46 | implementation("androidx.compose.material3:material3") 47 | implementation(files("libs/material-color-util.jar")) 48 | implementation("androidx.palette:palette:1.0.0") 49 | } 50 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | org.gradle.caching=true 25 | org.gradle.parallel=true 26 | org.gradle.daemon=true -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/recipe_post_creation/components/LeaveUnsavedDataDialog.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.recipe_post_creation.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.Save 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.res.stringResource 8 | import androidx.compose.ui.text.style.TextAlign 9 | import ru.tech.cookhelper.R 10 | import ru.tech.cookhelper.presentation.ui.theme.DialogShape 11 | 12 | @Composable 13 | fun LeaveUnsavedDataDialog( 14 | title: Int, 15 | message: Int, 16 | onLeave: () -> Unit, 17 | onDismissRequest: () -> Unit 18 | ) { 19 | AlertDialog( 20 | shape = DialogShape, 21 | title = { Text(stringResource(title), textAlign = TextAlign.Center) }, 22 | text = { Text(stringResource(message), textAlign = TextAlign.Center) }, 23 | onDismissRequest = { onDismissRequest() }, 24 | icon = { Icon(Icons.Outlined.Save, null) }, 25 | confirmButton = { 26 | Button(onClick = { onDismissRequest() }) { 27 | Text(stringResource(R.string.cancel)) 28 | } 29 | }, 30 | dismissButton = { 31 | FilledTonalButton(onClick = { 32 | onDismissRequest() 33 | onLeave() 34 | }) { 35 | Text(stringResource(R.string.leave_without_saving)) 36 | } 37 | } 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/dto/RecipeDto.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.dto 2 | 3 | import ru.tech.cookhelper.data.remote.utils.Dto 4 | import ru.tech.cookhelper.domain.model.FileData 5 | import ru.tech.cookhelper.domain.model.Product 6 | import ru.tech.cookhelper.domain.model.Recipe 7 | import ru.tech.cookhelper.domain.model.User 8 | 9 | data class RecipeDto( 10 | val id: Long = 0, 11 | val author: User, 12 | val title: String, 13 | val cookSteps: List, 14 | val time: Long, 15 | val category: String, 16 | val ingredients: List, 17 | val measures: List, 18 | val proteins: Double, 19 | val carbohydrates: Double, 20 | val fats: Double, 21 | val calories: Double, 22 | val image: FileData, 23 | val comments: List = listOf(), 24 | val reposts: List = listOf(), 25 | val likes: List = listOf(), 26 | val timestamp: Long 27 | ) : Dto { 28 | override fun asDomain(): Recipe = Recipe( 29 | id = id, 30 | author = author, 31 | title = title, 32 | cookSteps = cookSteps, 33 | time = time, 34 | category = category, 35 | ingredients = ingredients, 36 | measures = measures, 37 | proteins = proteins, 38 | carbohydrates = carbohydrates, 39 | fats = fats, 40 | calories = calories, 41 | image = image, 42 | comments = comments, 43 | reposts = reposts, 44 | likes = likes, 45 | timestamp = timestamp 46 | ) 47 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/core/constants/Status.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.core.constants 2 | 3 | object Status { 4 | const val SUCCESS: Int = 100 5 | const val WRONG_DATA: Int = 99 6 | const val PERMISSION_DENIED: Int = 98 7 | const val PARAMETER_MISSED: Int = 97 8 | const val EXCEPTION: Int = 0 9 | const val NO_INTERNET = -1 10 | const val CONNECTION_TIMED_OUT = -2 11 | const val READ_TIMEOUT = -3 12 | 13 | const val USER_NOT_FOUND: Int = 101 14 | const val WRONG_CREDENTIALS: Int = 102 15 | const val USER_NOT_VERIFIED: Int = 103 16 | const val USER_TOKEN_INVALID: Int = 104 17 | const val USER_DELETED: Int = 105 18 | const val NICKNAME_REJECTED: Int = 106 19 | const val EMAIL_REJECTED: Int = 107 20 | const val PASSWORD_REJECTED: Int = 108 21 | const val TOKEN_EXPIRED: Int = 109 22 | const val USER_UPLOAD_FAILED: Int = 110 23 | 24 | const val RECIPE_NOT_FOUND: Int = 201 25 | const val RECIPE_DELETED: Int = 202 26 | const val RECIPE_NOT_CREATED: Int = 204 27 | 28 | const val CHAT_NOT_FOUND: Int = 301 29 | const val CHAT_DELETED: Int = 302 30 | const val CHAT_NOT_CREATED: Int = 304 31 | 32 | const val TOPIC_NOT_FOUND: Int = 401 33 | const val TOPIC_DELETED: Int = 402 34 | const val ANSWER_NOT_ADDED: Int = 403 35 | const val TOPIC_NOT_CREATED: Int = 404 36 | 37 | const val COMMENT_NOT_FOUND: Int = 801 38 | const val COMMENT_DELETED: Int = 802 39 | 40 | const val POST_NOT_FOUND: Int = 901 41 | const val POST_DELETED: Int = 902 42 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/core/di/RetrofitModule.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.core.di 2 | 3 | import com.skydoves.retrofit.adapters.result.ResultCallAdapterFactory 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import okhttp3.OkHttpClient 9 | import okhttp3.logging.HttpLoggingInterceptor 10 | import retrofit2.Retrofit 11 | import retrofit2.converter.moshi.MoshiConverterFactory 12 | import ru.tech.cookhelper.core.constants.Constants 13 | import ru.tech.cookhelper.core.utils.RetrofitUtils.setTimeout 14 | import java.util.concurrent.TimeUnit 15 | import javax.inject.Singleton 16 | 17 | 18 | @Module 19 | @InstallIn(SingletonComponent::class) 20 | object RetrofitModule { 21 | 22 | @Provides 23 | @Singleton 24 | fun provideRetrofit(): Retrofit = Retrofit.Builder() 25 | .baseUrl(Constants.BASE_URL) 26 | .addConverterFactory(MoshiConverterFactory.create()) 27 | .addCallAdapterFactory(ResultCallAdapterFactory.create()) 28 | .let { 29 | val httpClient = OkHttpClient.Builder() 30 | .setTimeout(60, TimeUnit.SECONDS) 31 | val logging = HttpLoggingInterceptor() 32 | logging.setLevel(HttpLoggingInterceptor.Level.BODY) 33 | httpClient 34 | .addInterceptor(logging) 35 | // .addInterceptor(RetrofitUtils.RetryInterceptor { !it.isSuccessful }) 36 | return@let it.client(httpClient.build()) 37 | } 38 | .build() 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/forum_discussion/viewModel/ForumDiscussionViewModel.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.forum_discussion.viewModel 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.runtime.MutableState 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.setValue 8 | import androidx.lifecycle.SavedStateHandle 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.viewModelScope 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import ru.tech.cookhelper.presentation.ui.utils.android.ImageUtils.AsyncBlur.blur 13 | import ru.tech.cookhelper.presentation.ui.utils.android.ImageUtils.signature 14 | import ru.tech.cookhelper.presentation.ui.utils.compose.StateUtils.update 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class ForumDiscussionViewModel @Inject constructor( 19 | savedStateHandle: SavedStateHandle 20 | ) : ViewModel() { 21 | 22 | private val _blurredBitmap: MutableState = mutableStateOf(null) 23 | var blurredBitmap: Bitmap? by _blurredBitmap 24 | 25 | private var bitmapSignature: String = "" 26 | 27 | fun blur(bitmap: Bitmap) { 28 | bitmap.blur(scope = viewModelScope) { 29 | val sign = it?.signature() ?: "" 30 | if (bitmapSignature != sign) { 31 | _blurredBitmap.update { it } 32 | bitmapSignature = it?.signature() ?: "" 33 | } else it?.recycle() 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/chat/components/MessageHeader.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.chat.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.shape.CircleShape 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Surface 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | import ru.tech.cookhelper.presentation.ui.utils.compose.ColorUtils.createInverseSecondaryColor 17 | 18 | @Composable 19 | fun MessageHeader(text: String) { 20 | Row( 21 | modifier = Modifier 22 | .fillMaxWidth() 23 | .padding(8.dp), 24 | horizontalArrangement = Arrangement.Center 25 | ) { 26 | Surface( 27 | shape = CircleShape, 28 | color = MaterialTheme.colorScheme.surfaceVariant.createInverseSecondaryColor(0.3f) 29 | .copy(alpha = 0.5f) 30 | ) { 31 | Text( 32 | text = text, 33 | modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), 34 | fontSize = 14.sp, 35 | fontWeight = FontWeight.SemiBold 36 | ) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/chat_list/components/ChatPicture.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.chat_list.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.shape.CircleShape 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.text.font.FontWeight 12 | import androidx.compose.ui.unit.sp 13 | import ru.tech.cookhelper.presentation.ui.utils.compose.widgets.Picture 14 | 15 | @Composable 16 | fun ChatPicture( 17 | modifier: Modifier, 18 | image: String?, 19 | title: String 20 | ) { 21 | if (image != null) { 22 | Picture( 23 | model = image, 24 | modifier = modifier 25 | ) 26 | } else { 27 | Box( 28 | modifier = Modifier 29 | .then(modifier) 30 | .background( 31 | color = MaterialTheme.colorScheme.tertiaryContainer, 32 | shape = CircleShape 33 | ), 34 | contentAlignment = Alignment.Center 35 | ) { 36 | Text( 37 | text = (title.getOrNull(0)?.toString() ?: "#"), 38 | fontWeight = FontWeight.Bold, 39 | fontSize = 24.sp, 40 | color = MaterialTheme.colorScheme.onTertiaryContainer 41 | ) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/home_screen/components/BottomNavigationBar.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.home_screen.components 2 | 3 | import androidx.compose.foundation.layout.WindowInsets 4 | import androidx.compose.material3.* 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.text.style.TextOverflow 7 | import ru.tech.cookhelper.presentation.ui.utils.compose.ResUtils.getIcon 8 | import ru.tech.cookhelper.presentation.ui.utils.navigation.Screen 9 | 10 | @Composable 11 | fun BottomNavigationBar( 12 | windowInsets: WindowInsets = NavigationBarDefaults.windowInsets, 13 | selectedItem: T, 14 | items: List, 15 | onClick: (screen: T) -> Unit 16 | ) { 17 | NavigationBar(windowInsets = windowInsets) { 18 | items.forEach { screen -> 19 | NavigationBarItem( 20 | icon = { 21 | Icon( 22 | screen.getIcon(selectedItem == screen), 23 | null 24 | ) 25 | }, 26 | alwaysShowLabel = false, 27 | label = { 28 | Text( 29 | screen.shortTitle.asString(), 30 | maxLines = 1, 31 | overflow = TextOverflow.Ellipsis 32 | ) 33 | }, 34 | selected = selectedItem == screen, 35 | onClick = { 36 | if (selectedItem != screen) onClick(screen) 37 | } 38 | ) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/crash_screen/viewModel/CrashViewModel.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.crash_screen.viewModel 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.launchIn 10 | import kotlinx.coroutines.flow.onEach 11 | import kotlinx.coroutines.runBlocking 12 | import ru.tech.cookhelper.domain.use_case.get_settings_list.GetSettingsListUseCase 13 | import ru.tech.cookhelper.domain.use_case.get_settings_list.GetSettingsListUseCaseFlow 14 | import ru.tech.cookhelper.presentation.settings.components.SettingsState 15 | import ru.tech.cookhelper.presentation.settings.components.mapToState 16 | import ru.tech.cookhelper.presentation.ui.utils.compose.StateUtils.update 17 | import javax.inject.Inject 18 | 19 | @HiltViewModel 20 | class CrashViewModel @Inject constructor( 21 | getSettingsListUseCase: GetSettingsListUseCase, 22 | getSettingsListUseCaseFlow: GetSettingsListUseCaseFlow 23 | ) : ViewModel() { 24 | 25 | private val _settingsState: MutableState = mutableStateOf( 26 | runBlocking { getSettingsListUseCase().mapToState() } 27 | ) 28 | val settingsState: SettingsState by _settingsState 29 | 30 | init { 31 | getSettingsListUseCaseFlow().onEach { 32 | _settingsState.update { it.mapToState() } 33 | }.launchIn(viewModelScope) 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/forum_discussion/components/TagItem.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.forum_discussion.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.text.style.TextAlign 15 | import androidx.compose.ui.unit.dp 16 | 17 | @Composable 18 | fun TagItem(modifier: Modifier = Modifier, text: String, onClick: () -> Unit) { 19 | Box( 20 | modifier = modifier 21 | .border( 22 | width = 1.dp, 23 | color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.3f), 24 | shape = RoundedCornerShape(8.dp) 25 | ) 26 | .clip(RoundedCornerShape(8.dp)) 27 | .background(MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.4f)) 28 | .clickable { onClick() } 29 | .padding(10.dp) 30 | ) { 31 | Text( 32 | text = text, 33 | color = MaterialTheme.colorScheme.onTertiaryContainer, 34 | textAlign = TextAlign.Center, 35 | style = MaterialTheme.typography.bodySmall, 36 | maxLines = 1 37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/provider/LocalScreenController.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.provider 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ReadOnlyComposable 5 | import androidx.compose.runtime.compositionLocalOf 6 | import dev.olshevski.navigation.reimagined.NavController 7 | import dev.olshevski.navigation.reimagined.pop 8 | import dev.olshevski.navigation.reimagined.popAll 9 | import ru.tech.cookhelper.core.utils.ReflectionUtils.name 10 | import ru.tech.cookhelper.presentation.ui.utils.navigation.Screen 11 | import dev.olshevski.navigation.reimagined.navigate as libNavigate 12 | 13 | val LocalScreenController = compositionLocalOf> { 14 | error("ScreenController not present") 15 | } 16 | 17 | inline val T.isCurrentDestination: Boolean 18 | @ReadOnlyComposable 19 | @Composable 20 | get() = LocalScreenController.current.currentDestination == this 21 | 22 | inline val NavController.currentDestination: T? get() = this.backstack.entries.lastOrNull()?.destination 23 | 24 | fun NavController.navigate(destination: T) = apply { 25 | if ((currentDestination ?: "")::class.name != destination::class.name) libNavigate(destination) 26 | } 27 | 28 | fun NavController.navigateAndPopAll(destination: T) = apply { 29 | if ((currentDestination ?: "")::class.name != destination::class.name) { 30 | popAll() 31 | libNavigate(destination) 32 | } 33 | } 34 | 35 | fun NavController.goBack(): Boolean { 36 | if (backstack.entries.size == 1) return false 37 | return pop() 38 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/compose/UIText.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.compose 2 | 3 | import android.content.Context 4 | import androidx.annotation.StringRes 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.res.stringResource 7 | 8 | sealed class UIText { 9 | data class DynamicString(val value: String) : UIText() 10 | class StringResource( 11 | @StringRes val resId: Int, vararg val args: Any 12 | ) : UIText() 13 | 14 | @Composable 15 | fun asString(): String { 16 | return when (this) { 17 | is DynamicString -> value 18 | is StringResource -> stringResource(resId, *args) 19 | } 20 | } 21 | 22 | fun asString(context: Context): String { 23 | return when (this) { 24 | is DynamicString -> value 25 | is StringResource -> context.getString(resId, *args) 26 | } 27 | } 28 | 29 | fun isEmpty(): Boolean { 30 | return when (this) { 31 | is DynamicString -> value.isEmpty() 32 | is StringResource -> false 33 | } 34 | } 35 | 36 | fun isNotEmpty(): Boolean = !isEmpty() 37 | 38 | @Suppress("FunctionName") 39 | companion object { 40 | fun Empty() = UIText.DynamicString("") 41 | fun String.asUIText() = UIText.DynamicString(this) 42 | fun Int.asUIText() = UIText.StringResource(this) 43 | fun UIText(value: String?) = UIText.DynamicString(value) 44 | fun UIText(@StringRes value: Int) = UIText.StringResource(value) 45 | fun DynamicString(value: String?) = UIText.DynamicString(value ?: "") 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/dto/UserDto.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.dto 2 | 3 | import ru.tech.cookhelper.data.remote.utils.Dto 4 | import ru.tech.cookhelper.domain.model.FileData 5 | import ru.tech.cookhelper.domain.model.Product 6 | import ru.tech.cookhelper.domain.model.User 7 | 8 | data class UserDto( 9 | val id: Long?, 10 | val avatar: List, 11 | val bannedIngredients: List?, 12 | val bannedRecipes: List?, 13 | val email: String?, 14 | val forums: List?, 15 | val fridge: List, 16 | val name: String?, 17 | val nickname: String?, 18 | val userPosts: List? = null, 19 | val userRecipes: List? = null, 20 | val starredIngredients: List?, 21 | val starredRecipes: List?, 22 | val status: String?, 23 | val verified: Boolean?, 24 | val surname: String?, 25 | val lastSeen: Long?, 26 | val token: String? 27 | ) : Dto { 28 | override fun asDomain(): User = User( 29 | id = id ?: 0, 30 | avatar = avatar, 31 | bannedIngredients = bannedIngredients, 32 | bannedRecipes = bannedRecipes, 33 | email = email ?: "", 34 | forums = forums, 35 | fridge = fridge, 36 | name = name ?: "", 37 | nickname = nickname ?: "", 38 | starredIngredients = userPosts, 39 | userPosts = userRecipes, 40 | userRecipes = starredIngredients, 41 | starredRecipes = starredRecipes, 42 | status = status, 43 | verified = verified ?: false, 44 | surname = surname ?: "", 45 | lastSeen = lastSeen ?: 0L, 46 | token = token ?: "" 47 | ) 48 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/forum_screen/components/SearchBox.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.forum_screen.components 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.compose.foundation.text.BasicTextField 5 | import androidx.compose.foundation.text.KeyboardActions 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.SolidColor 10 | import androidx.compose.ui.platform.LocalFocusManager 11 | import androidx.compose.ui.text.TextStyle 12 | import androidx.compose.ui.text.style.TextAlign 13 | import androidx.compose.ui.unit.sp 14 | 15 | @Composable 16 | fun SearchBox( 17 | modifier: Modifier = Modifier, 18 | value: String, 19 | textStyle: TextStyle = TextStyle( 20 | fontSize = 22.sp, 21 | color = MaterialTheme.colorScheme.onBackground, 22 | textAlign = TextAlign.Start, 23 | ), 24 | hint: @Composable () -> Unit, 25 | onValueChange: (String) -> Unit 26 | ) { 27 | val localFocusManager = LocalFocusManager.current 28 | BasicTextField( 29 | modifier = modifier, 30 | value = value, 31 | textStyle = textStyle, 32 | keyboardActions = KeyboardActions( 33 | onDone = { localFocusManager.clearFocus() } 34 | ), 35 | singleLine = true, 36 | cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground), 37 | onValueChange = onValueChange 38 | ) 39 | if (value.isEmpty()) { 40 | hint() 41 | } else { 42 | BackHandler { 43 | onValueChange("") 44 | localFocusManager.clearFocus() 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/profile/components/PostActionButton.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.profile.components 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.shape.CircleShape 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.vector.ImageVector 11 | import androidx.compose.ui.text.TextStyle 12 | import androidx.compose.ui.text.font.FontWeight 13 | import androidx.compose.ui.unit.dp 14 | import ru.tech.cookhelper.presentation.ui.theme.Gray 15 | 16 | @OptIn(ExperimentalMaterial3Api::class) 17 | @Composable 18 | fun PostActionButton( 19 | onClick: () -> Unit, 20 | icon: ImageVector, 21 | text: String = "", 22 | containerColor: Color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.25f), 23 | contentColor: Color = Gray 24 | ) { 25 | Surface( 26 | modifier = Modifier.defaultMinSize(32.dp, 32.dp), 27 | shape = CircleShape, 28 | color = containerColor, 29 | onClick = onClick, 30 | contentColor = contentColor 31 | ) { 32 | ProvideTextStyle(value = TextStyle(fontWeight = FontWeight.Bold)) { 33 | Row( 34 | Modifier.padding(horizontal = 12.dp), 35 | horizontalArrangement = Arrangement.Center, 36 | verticalAlignment = Alignment.CenterVertically, 37 | ) { 38 | Icon(icon, null) 39 | if (text != "") { 40 | Spacer(Modifier.size(8.dp)) 41 | Text(text) 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/compose/ResUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.compose 2 | 3 | import android.content.Context 4 | import androidx.annotation.PluralsRes 5 | import androidx.annotation.StringRes 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.ExperimentalComposeUiApi 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.res.stringResource 10 | import ru.tech.cookhelper.presentation.ui.utils.navigation.Screen 11 | 12 | object ResUtils { 13 | 14 | fun Int.asString(context: Context, vararg formatArgs: Any = emptyArray()): String { 15 | return context.getString(this, formatArgs) 16 | } 17 | 18 | fun Screen.getIcon(selected: Boolean): ImageVector = 19 | if (selected) this.selectedIcon else this.baseIcon 20 | 21 | @Composable 22 | fun stringResourceListOf( 23 | @StringRes vararg ids: Int 24 | ): List = ids.map { 25 | stringResource(it) 26 | } 27 | 28 | @OptIn(ExperimentalComposeUiApi::class) 29 | @Composable 30 | fun pluralStringResource( 31 | @PluralsRes id: Int, 32 | count: Int, 33 | onZero: @Composable () -> String, 34 | ): String = if (count == 0) { 35 | onZero() 36 | } else { 37 | androidx.compose.ui.res.pluralStringResource(id, count, count) 38 | } 39 | 40 | @OptIn(ExperimentalComposeUiApi::class) 41 | @Composable 42 | fun pluralStringResource( 43 | @PluralsRes id: Int, 44 | count: Int, 45 | onZero: @Composable () -> String, 46 | vararg formatArgs: Any 47 | ): String = if (count == 0) { 48 | onZero() 49 | } else { 50 | androidx.compose.ui.res.pluralStringResource(id, count, formatArgs) 51 | } 52 | 53 | 54 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/compose/ColorUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.compose 2 | 3 | import androidx.annotation.FloatRange 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.graphics.toArgb 8 | import ru.tech.cookhelper.presentation.ui.theme.isDarkMode 9 | import androidx.core.graphics.ColorUtils as AndroidColorUtils 10 | 11 | object ColorUtils { 12 | 13 | fun Color.blend( 14 | color: Color, 15 | @FloatRange(from = 0.0, to = 1.0) fraction: Float = 0.2f 16 | ): Color = AndroidColorUtils.blendARGB(this.toArgb(), color.toArgb(), fraction).toColor() 17 | 18 | fun Color.darken( 19 | @FloatRange(from = 0.0, to = 1.0) fraction: Float = 0.2f 20 | ): Color = blend(color = Color.Black, fraction = fraction) 21 | 22 | @Composable 23 | fun Color.createSecondaryColor( 24 | @FloatRange(from = 0.0, to = 1.0) fraction: Float = 0.2f 25 | ): Color = if (isDarkMode()) lighten(fraction) else darken(fraction) 26 | 27 | @Composable 28 | fun Color.createInverseSecondaryColor( 29 | @FloatRange(from = 0.0, to = 1.0) fraction: Float = 0.2f 30 | ): Color = if (!isDarkMode()) lighten(fraction) else darken(fraction) 31 | 32 | fun Color.lighten( 33 | @FloatRange(from = 0.0, to = 1.0) fraction: Float = 0.2f 34 | ): Color = blend(color = Color.White, fraction = fraction) 35 | 36 | fun Int.toColor(): Color = Color(color = this) 37 | 38 | @Composable 39 | fun Color.harmonizeWithPrimary( 40 | @FloatRange( 41 | from = 0.0, 42 | to = 1.0 43 | ) fraction: Float = 0.2f 44 | ): Color = blend(MaterialTheme.colorScheme.primary, fraction) 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | #### OkHttp, Retrofit and Moshi 24 | -dontwarn okhttp3.** 25 | -dontwarn retrofit2.PlatformJava8 26 | -dontwarn okio.** 27 | -dontwarn javax.annotation.** 28 | -keepclasseswithmembers class * { 29 | @retrofit2.http.* ; 30 | } 31 | -keepclasseswithmembers class * { 32 | @com.squareup.moshi.* ; 33 | } 34 | -keep @com.squareup.moshi.JsonQualifier interface * 35 | -dontwarn org.jetbrains.annotations.** 36 | -keep class kotlin.Metadata { *; } 37 | -keepclassmembers class kotlin.Metadata { 38 | public ; 39 | } 40 | 41 | -keepclassmembers class * { 42 | @com.squareup.moshi.FromJson ; 43 | @com.squareup.moshi.ToJson ; 44 | } 45 | 46 | -keepnames @kotlin.Metadata class ru.tech.cookhelper.data.** 47 | -keep class ru.tech.cookhelper.data.** { *; } 48 | -keepclassmembers class ru.tech.cookhelper.data.** { *; } 49 | 50 | -keepnames @kotlin.Metadata class ru.tech.cookhelper.domain.** 51 | -keep class ru.tech.cookhelper.domain.** { *; } 52 | -keepclassmembers class ru.tech.cookhelper.domain.** { *; } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/all_images/components/AdaptiveVerticalGrid.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.all_images.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.lazy.grid.GridCells 6 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 7 | import androidx.compose.foundation.lazy.grid.items 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.platform.LocalConfiguration 12 | import androidx.compose.ui.unit.dp 13 | import ru.tech.cookhelper.domain.model.FileData 14 | import ru.tech.cookhelper.presentation.ui.utils.compose.PaddingUtils.addPadding 15 | import ru.tech.cookhelper.presentation.ui.utils.compose.widgets.Picture 16 | 17 | @Composable 18 | fun AdaptiveVerticalGrid(images: List, onImageClick: (id: String) -> Unit) { 19 | val configuration = LocalConfiguration.current 20 | 21 | val portrait = configuration.screenWidthDp < configuration.screenHeightDp 22 | val count = if (portrait) 3 else 4 23 | 24 | LazyVerticalGrid( 25 | columns = GridCells.Fixed(count), 26 | contentPadding = WindowInsets.navigationBars.asPaddingValues().addPadding( 27 | start = 2.dp, end = 2.dp, top = 4.dp, bottom = 80.dp 28 | ), 29 | modifier = Modifier.fillMaxSize() 30 | ) { 31 | items(images) { item -> 32 | Picture( 33 | model = item.link, 34 | modifier = Modifier 35 | .size(configuration.screenWidthDp.dp / count) 36 | .padding(horizontal = 2.dp, vertical = 2.dp) 37 | .clickable { onImageClick(item.id) }, 38 | shape = RoundedCornerShape(4.dp), 39 | ) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/android/SystemBarUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.android 2 | 3 | import android.app.Activity 4 | import androidx.core.view.WindowInsetsCompat 5 | import androidx.core.view.WindowInsetsControllerCompat 6 | 7 | object SystemBarUtils { 8 | 9 | val Activity.isSystemBarsHidden: Boolean 10 | get() { 11 | return _isSystemBarsHidden 12 | } 13 | 14 | private var _isSystemBarsHidden = false 15 | 16 | val Activity.isNavigationBarsHidden: Boolean 17 | get() { 18 | return _isNavigationBarsHidden 19 | } 20 | 21 | private var _isNavigationBarsHidden = false 22 | 23 | fun Activity.hideSystemBars() = WindowInsetsControllerCompat( 24 | window, 25 | window.decorView 26 | ).let { controller -> 27 | controller.systemBarsBehavior = 28 | WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 29 | controller.hide(WindowInsetsCompat.Type.systemBars()) 30 | _isSystemBarsHidden = true 31 | } 32 | 33 | fun Activity.hideNavigationBars() = WindowInsetsControllerCompat( 34 | window, 35 | window.decorView 36 | ).let { controller -> 37 | controller.systemBarsBehavior = 38 | WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 39 | controller.hide(WindowInsetsCompat.Type.navigationBars()) 40 | _isNavigationBarsHidden = true 41 | } 42 | 43 | fun Activity.showNavigationBars() = WindowInsetsControllerCompat( 44 | window, 45 | window.decorView 46 | ).show(WindowInsetsCompat.Type.navigationBars()).also { 47 | _isNavigationBarsHidden = false 48 | } 49 | 50 | fun Activity.showSystemBars() = WindowInsetsControllerCompat( 51 | window, 52 | window.decorView 53 | ).show(WindowInsetsCompat.Type.systemBars()).also { 54 | _isSystemBarsHidden = false 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/app/components/MainModalDrawerHeader.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.app.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import ru.tech.cookhelper.domain.model.getLastAvatar 15 | import ru.tech.cookhelper.presentation.ui.utils.compose.widgets.Picture 16 | 17 | @Composable 18 | fun MainModalDrawerHeader(userState: UserState, onClick: () -> Unit) { 19 | Column( 20 | Modifier.clickable( 21 | interactionSource = MutableInteractionSource(), 22 | indication = null, 23 | onClick = onClick 24 | ) 25 | ) { 26 | Picture( 27 | model = userState.user?.getLastAvatar(), 28 | modifier = Modifier 29 | .padding(start = 15.dp, top = 15.dp) 30 | .size(64.dp) 31 | ) 32 | Spacer(Modifier.size(10.dp)) 33 | Column( 34 | Modifier 35 | .padding(horizontal = 15.dp) 36 | ) { 37 | Text( 38 | userState.user?.let { "${it.name} ${it.surname}" }.toString(), 39 | style = MaterialTheme.typography.headlineSmall 40 | ) 41 | Spacer(Modifier.weight(1f)) 42 | Text( 43 | "@${userState.user?.nickname}", 44 | style = MaterialTheme.typography.bodyMedium, 45 | color = MaterialTheme.colorScheme.onSurfaceVariant 46 | ) 47 | } 48 | Spacer(Modifier.size(30.dp)) 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/compose/ScrollUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.compose 2 | 3 | import androidx.compose.foundation.ScrollState 4 | import androidx.compose.foundation.lazy.LazyListState 5 | import androidx.compose.runtime.* 6 | 7 | object ScrollUtils { 8 | 9 | /** 10 | * Returns whether the scrolling object is currently scrolling up. 11 | */ 12 | @Composable 13 | fun ScrollState.isScrollingUp(): Boolean { 14 | var previousScrollOffset by remember(this) { mutableStateOf(value) } 15 | return remember(this) { 16 | derivedStateOf { 17 | (previousScrollOffset >= value).also { 18 | previousScrollOffset = value 19 | } 20 | } 21 | }.value 22 | } 23 | 24 | /** 25 | * Returns whether the lazy list is currently scrolling up. 26 | */ 27 | @Composable 28 | fun LazyListState.isScrollingUp(): Boolean { 29 | var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) } 30 | var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) } 31 | return remember(this) { 32 | derivedStateOf { 33 | if (previousIndex != firstVisibleItemIndex) { 34 | previousIndex > firstVisibleItemIndex 35 | } else { 36 | previousScrollOffset >= firstVisibleItemScrollOffset 37 | }.also { 38 | previousIndex = firstVisibleItemIndex 39 | previousScrollOffset = firstVisibleItemScrollOffset 40 | } 41 | } 42 | }.value 43 | } 44 | 45 | @Composable 46 | fun LazyListState.isLastItemVisible(): Boolean { 47 | return remember(this) { 48 | derivedStateOf { 49 | layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1 50 | } 51 | }.value 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/compose/TopAppBarUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.compose 2 | 3 | import androidx.compose.material3.* 4 | import androidx.compose.runtime.Composable 5 | 6 | object TopAppBarUtils { 7 | 8 | /** 9 | * Returns a [TopAppBarScrollBehavior]. A top app bar that is set up with this 10 | * 11 | * @param state the state object to be used to control or observe the top app bar's scroll 12 | * state. See [rememberTopAppBarState] for a state that is remembered across compositions. 13 | * @param canScroll a callback used to determine whether scroll events are to be 14 | * handled by this [EnterAlwaysScrollBehavior] 15 | */ 16 | @OptIn(ExperimentalMaterial3Api::class) 17 | @Composable 18 | fun topAppBarScrollBehavior( 19 | scrollBehavior: ScrollBehavior = ScrollBehavior.Pinned, 20 | canScroll: () -> Boolean = { true }, 21 | state: TopAppBarState = rememberTopAppBarState() 22 | ): TopAppBarScrollBehavior = when (scrollBehavior) { 23 | is ScrollBehavior.EnterAlways -> { 24 | TopAppBarDefaults.enterAlwaysScrollBehavior( 25 | state = state, 26 | canScroll = canScroll, 27 | snapAnimationSpec = scrollBehavior.snapAnimationSpec, 28 | flingAnimationSpec = scrollBehavior.flingAnimationSpec() 29 | ) 30 | } 31 | is ScrollBehavior.ExitUntilCollapsed -> { 32 | TopAppBarDefaults.exitUntilCollapsedScrollBehavior( 33 | state = state, 34 | canScroll = canScroll, 35 | snapAnimationSpec = scrollBehavior.snapAnimationSpec, 36 | flingAnimationSpec = scrollBehavior.flingAnimationSpec() 37 | ) 38 | } 39 | is ScrollBehavior.Pinned -> { 40 | TopAppBarDefaults.pinnedScrollBehavior( 41 | state = state, 42 | canScroll = canScroll 43 | ) 44 | } 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 41 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/remote/api/auth/AuthService.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.remote.api.auth 2 | 3 | import retrofit2.Call 4 | import retrofit2.http.* 5 | import ru.tech.cookhelper.data.remote.dto.UserDto 6 | import ru.tech.cookhelper.data.remote.utils.Response 7 | 8 | interface AuthService { 9 | 10 | @Multipart 11 | @POST("api/user/post/auth/") 12 | fun loginWith( 13 | @Part("login") login: String, 14 | @Part("password") password: String 15 | ): Call> 16 | 17 | @Multipart 18 | @POST("api/user/post/reg/") 19 | fun registerWith( 20 | @Part("name") name: String, 21 | @Part("surname") surname: String, 22 | @Part("nickname") nickname: String, 23 | @Part("email") email: String, 24 | @Part("password") password: String 25 | ): Call> 26 | 27 | @Multipart 28 | @POST("api/user/post/verify/") 29 | fun verifyEmail( 30 | @Part("code") code: String, 31 | @Part("token") token: String 32 | ): Call> 33 | 34 | @GET("api/user/get/verification/") 35 | suspend fun requestCode( 36 | @Query("token") token: String 37 | ): Result> 38 | 39 | @GET("api/user/get/recover-password/") 40 | suspend fun requestPasswordRestoreCode( 41 | @Query("login") login: String 42 | ): Result> 43 | 44 | @Multipart 45 | @POST("api/user/post/recover-password/") 46 | fun restorePasswordBy( 47 | @Part("login") login: String, 48 | @Part("code") code: String, 49 | @Part("password") password: String 50 | ): Call> 51 | 52 | @GET("api/user/get/nickname-availability/") 53 | suspend fun checkNicknameForAvailability( 54 | @Query("nickname") nickname: String 55 | ): Result> 56 | 57 | @GET("api/user/get/email-availability/") 58 | suspend fun checkEmailForAvailability( 59 | @Query("email") email: String 60 | ): Result> 61 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/fridge_screen/components/ProductItem.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.fridge_screen.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.CircleShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.rounded.DeleteOutline 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.IconButton 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | import ru.tech.cookhelper.domain.model.Product 17 | 18 | @Composable 19 | fun ProductItem( 20 | modifier: Modifier, 21 | product: Product, 22 | onDelete: () -> Unit 23 | ) { 24 | Row( 25 | modifier = modifier, 26 | verticalAlignment = Alignment.CenterVertically 27 | ) { 28 | Box( 29 | modifier = Modifier 30 | .size(36.dp) 31 | .background( 32 | color = MaterialTheme.colorScheme.secondaryContainer, 33 | shape = CircleShape 34 | ), 35 | contentAlignment = Alignment.Center 36 | ) { 37 | Icon( 38 | imageVector = product.getIcon(), 39 | contentDescription = null, 40 | tint = MaterialTheme.colorScheme.onSecondaryContainer 41 | ) 42 | } 43 | Spacer( 44 | Modifier 45 | .weight(1f) 46 | .padding(end = 8.dp) 47 | ) 48 | Text(text = product.title) 49 | Spacer( 50 | Modifier 51 | .weight(1f) 52 | .padding(end = 8.dp) 53 | ) 54 | IconButton(onClick = onDelete) { 55 | Icon(Icons.Rounded.DeleteOutline, null) 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/local/database/TypeConverters.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.local.database 2 | 3 | import androidx.room.ProvidedTypeConverter 4 | import androidx.room.TypeConverter 5 | import com.squareup.moshi.Types 6 | import ru.tech.cookhelper.data.utils.JsonParser 7 | import ru.tech.cookhelper.domain.model.FileData 8 | import ru.tech.cookhelper.domain.model.Product 9 | import java.lang.reflect.Type 10 | import javax.inject.Inject 11 | 12 | @ProvidedTypeConverter 13 | class TypeConverters @Inject constructor(private val jsonParser: JsonParser) { 14 | 15 | private fun getListType(type: Type): Type { 16 | return Types.newParameterizedType( 17 | List::class.java, type 18 | ) 19 | } 20 | 21 | @TypeConverter 22 | fun fromStringList(data: List): String { 23 | return jsonParser.toJson( 24 | data, getListType(String::class.java) 25 | ) ?: "" 26 | } 27 | 28 | @TypeConverter 29 | fun toStringList(data: String): List { 30 | return jsonParser.fromJson>( 31 | data, getListType(String::class.java) 32 | ) ?: emptyList() 33 | } 34 | 35 | @TypeConverter 36 | fun fromFileDataList(data: List): String { 37 | return jsonParser.toJson( 38 | data, getListType(FileData::class.java) 39 | ) ?: "" 40 | } 41 | 42 | @TypeConverter 43 | fun toFileDataList(data: String): List { 44 | return jsonParser.fromJson>( 45 | data, getListType(FileData::class.java) 46 | ) ?: emptyList() 47 | } 48 | 49 | @TypeConverter 50 | fun fromProductsList(data: List): String { 51 | return jsonParser.toJson( 52 | data, getListType(Product::class.java) 53 | ) ?: "" 54 | } 55 | 56 | @TypeConverter 57 | fun toProductsList(data: String): List { 58 | return jsonParser.fromJson>( 59 | data, getListType(Product::class.java) 60 | ) ?: emptyList() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/profile/components/AuthorBubble.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.profile.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.shape.CircleShape 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.text.font.FontWeight 15 | import androidx.compose.ui.unit.dp 16 | import androidx.compose.ui.unit.sp 17 | import ru.tech.cookhelper.domain.model.User 18 | import ru.tech.cookhelper.domain.model.getLastAvatar 19 | import ru.tech.cookhelper.presentation.ui.theme.Gray 20 | import ru.tech.cookhelper.presentation.ui.utils.compose.widgets.Picture 21 | 22 | @Composable 23 | fun AuthorBubble( 24 | modifier: Modifier = Modifier, 25 | pictureModifier: Modifier = Modifier.size(54.dp), 26 | author: User?, 27 | timestamp: String, 28 | onClick: () -> Unit 29 | ) { 30 | Row( 31 | modifier = modifier 32 | .clip(CircleShape) 33 | .clickable { onClick() }, 34 | verticalAlignment = Alignment.CenterVertically 35 | ) { 36 | Picture( 37 | model = author.getLastAvatar(), 38 | modifier = pictureModifier 39 | ) 40 | Spacer(Modifier.size(8.dp)) 41 | Column { 42 | Text( 43 | text = "${author?.name} ${author?.surname}", 44 | fontWeight = FontWeight.SemiBold, 45 | fontSize = 16.sp 46 | ) 47 | Spacer(Modifier.size(5.dp)) 48 | Text( 49 | text = timestamp, 50 | color = Gray, 51 | fontSize = 14.sp 52 | ) 53 | } 54 | Spacer(Modifier.size(8.dp)) 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/domain/repository/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.domain.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import ru.tech.cookhelper.core.Action 5 | import ru.tech.cookhelper.domain.model.Post 6 | import ru.tech.cookhelper.domain.model.Recipe 7 | import ru.tech.cookhelper.domain.model.Topic 8 | import ru.tech.cookhelper.domain.model.User 9 | import java.io.File 10 | 11 | interface UserRepository { 12 | 13 | fun loginWith(login: String, password: String): Flow> 14 | 15 | fun registerWith( 16 | name: String, 17 | surname: String, 18 | nickname: String, 19 | email: String, 20 | password: String 21 | ): Flow> 22 | 23 | suspend fun requestPasswordRestoreCode(login: String): Action 24 | 25 | fun restorePasswordBy( 26 | login: String, 27 | code: String, 28 | newPassword: String 29 | ): Flow> 30 | 31 | suspend fun requestCode(token: String): Result 32 | 33 | fun checkCode(code: String, token: String): Flow> 34 | 35 | suspend fun cacheUser(user: User) 36 | 37 | fun getUser(): Flow 38 | 39 | suspend fun checkLoginForAvailability(login: String): Action 40 | 41 | suspend fun checkEmailForAvailability(email: String): Action 42 | 43 | suspend fun logOut() 44 | 45 | suspend fun loadUserById(id: String): User? 46 | 47 | fun getFeed(token: String): Flow>> 48 | 49 | fun stopAwaitingFeed() 50 | 51 | fun createPost( 52 | token: String, 53 | label: String, 54 | content: String, 55 | imageFile: File?, 56 | type: String 57 | ): Flow> 58 | 59 | fun createTopic( 60 | token: String, 61 | title: String, 62 | text: String, 63 | attachments: List>, 64 | tags: List 65 | ): Flow> 66 | 67 | fun observeUser( 68 | id: Long, 69 | token: String 70 | ): Flow> 71 | 72 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/compose/PaddingUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.compose 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.calculateEndPadding 5 | import androidx.compose.foundation.layout.calculateStartPadding 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.platform.LocalLayoutDirection 8 | import androidx.compose.ui.unit.Dp 9 | import androidx.compose.ui.unit.dp 10 | import kotlin.math.abs 11 | 12 | object PaddingUtils { 13 | 14 | @Composable 15 | fun PaddingValues.addPadding( 16 | bottom: Dp = 0.dp, 17 | top: Dp = 0.dp, 18 | start: Dp = 0.dp, 19 | end: Dp = 0.dp 20 | ): PaddingValues { 21 | return PaddingValues( 22 | start = abs(calculateStartPadding(LocalLayoutDirection.current) + start), 23 | top = abs(calculateTopPadding() + top), 24 | end = abs(calculateEndPadding(LocalLayoutDirection.current) + end), 25 | bottom = abs(calculateBottomPadding() + bottom) 26 | ) 27 | } 28 | 29 | fun abs(dp: Dp): Dp = abs(dp.value).dp 30 | 31 | @Composable 32 | fun PaddingValues.removePadding( 33 | bottom: Dp = 0.dp, 34 | top: Dp = 0.dp, 35 | start: Dp = 0.dp, 36 | end: Dp = 0.dp 37 | ): PaddingValues { 38 | return addPadding( 39 | bottom = -bottom, 40 | top = -top, 41 | start = -start, 42 | end = -end 43 | ) 44 | } 45 | 46 | @Composable 47 | fun PaddingValues.setPadding( 48 | predicate: () -> Boolean = { true }, 49 | bottom: Dp = calculateBottomPadding(), 50 | top: Dp = calculateTopPadding(), 51 | start: Dp = calculateStartPadding(LocalLayoutDirection.current), 52 | end: Dp = calculateEndPadding(LocalLayoutDirection.current) 53 | ): PaddingValues { 54 | return if (predicate()) PaddingValues( 55 | bottom = bottom, 56 | top = top, 57 | start = start, 58 | end = end 59 | ) else this 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/core/Action.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.core 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.transform 5 | 6 | sealed class Action(val data: T? = null, val message: String? = null) { 7 | class Loading(data: T? = null) : Action(data) 8 | class Success(data: T?) : Action(data) 9 | class Error(message: String?) : Action(message = message) 10 | class Empty(val status: Int? = null) : Action() 11 | } 12 | 13 | inline fun Flow>.onSuccess( 14 | crossinline action: suspend T.() -> Unit 15 | ): Flow> = transform { value -> 16 | if (value is Action.Success) value.data?.action() 17 | return@transform emit(value) 18 | } 19 | 20 | inline fun Flow>.onLoading( 21 | crossinline action: suspend T?.() -> Unit 22 | ): Flow> = transform { value -> 23 | if (value is Action.Loading) value.data.action() 24 | return@transform emit(value) 25 | } 26 | 27 | inline fun Flow>.onError( 28 | crossinline action: suspend String.() -> Unit 29 | ): Flow> = transform { value -> 30 | if (value is Action.Error) action(value.message ?: "") 31 | return@transform emit(value) 32 | } 33 | 34 | inline fun Flow>.onEmpty( 35 | crossinline action: suspend Int?.() -> Unit 36 | ): Flow> = transform { value -> 37 | if (value is Action.Empty) action(value.status) 38 | return@transform emit(value) 39 | } 40 | 41 | inline fun Action.onSuccess( 42 | crossinline action: T.() -> Unit 43 | ): Action = apply { 44 | if (this is Action.Success) data?.action() 45 | } 46 | 47 | inline fun Action.onLoading( 48 | crossinline action: T?.() -> Unit 49 | ): Action = apply { 50 | if (this is Action.Loading) data.action() 51 | } 52 | 53 | inline fun Action.onError( 54 | crossinline action: String.() -> Unit 55 | ): Action = apply { 56 | if (this is Action.Error) (message ?: "").action() 57 | } 58 | 59 | inline fun Action.onEmpty( 60 | crossinline action: Int?.() -> Unit 61 | ): Action = apply { 62 | if (this is Action.Empty) status.action() 63 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/profile/viewModel/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.profile.viewModel 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateListOf 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.launchIn 11 | import kotlinx.coroutines.flow.onEach 12 | import kotlinx.coroutines.launch 13 | import ru.tech.cookhelper.domain.model.Post 14 | import ru.tech.cookhelper.domain.use_case.close_connection.CloseConnectionsUseCase 15 | import ru.tech.cookhelper.domain.use_case.get_user.GetUserUseCase 16 | import ru.tech.cookhelper.domain.use_case.log_out.LogoutUseCase 17 | import ru.tech.cookhelper.presentation.app.components.UserState 18 | import ru.tech.cookhelper.presentation.ui.utils.compose.StateUtils.update 19 | import javax.inject.Inject 20 | 21 | @HiltViewModel 22 | class ProfileViewModel @Inject constructor( 23 | getUserUseCase: GetUserUseCase, 24 | private val logoutUseCase: LogoutUseCase, 25 | private val closeConnectionsUseCase: CloseConnectionsUseCase 26 | ) : ViewModel() { 27 | 28 | /*TODO: Remove this shit*/ 29 | val posts = mutableStateListOf() 30 | 31 | private val _userState: MutableState = mutableStateOf(UserState()) 32 | val userState: UserState by _userState 33 | 34 | init { 35 | getUserUseCase().onEach { user -> 36 | user?.let { _userState.update { UserState(it, it.token) } } 37 | }.launchIn(viewModelScope) 38 | } 39 | 40 | fun logOut() { 41 | viewModelScope.launch { 42 | logoutUseCase() 43 | closeConnectionsUseCase() 44 | } 45 | } 46 | 47 | fun addImage(imageUri: String) { 48 | //TODO: Send picked image to server 49 | } 50 | 51 | fun updateStatus(newStatus: String) { 52 | /*TODO: UpdateStatus*/ 53 | } 54 | 55 | fun addAvatar(imageUri: String) { 56 | /*TODO: UpdateAvatar*/ 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/app/components/BottomSheetHost.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.app.components 2 | 3 | import androidx.compose.animation.animateContentSize 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import dev.olshevski.navigation.reimagined.NavHost 7 | import dev.olshevski.navigation.reimagined.popAll 8 | import ru.tech.cookhelper.presentation.forum_screen.components.ForumFilterBottomSheet 9 | import ru.tech.cookhelper.presentation.ui.utils.navigation.BottomSheet 10 | import ru.tech.cookhelper.presentation.ui.utils.provider.BottomSheetController 11 | import ru.tech.cookhelper.presentation.ui.utils.provider.LocalBottomSheetController 12 | import ru.tech.cookhelper.presentation.ui.utils.provider.currentDestination 13 | import ru.tech.cookhelper.presentation.ui.widgets.bottomsheet.ModalBottomSheetScaffold 14 | 15 | @Composable 16 | fun BottomSheetHost( 17 | bottomSheetController: BottomSheetController = LocalBottomSheetController.current, 18 | content: @Composable () -> Unit 19 | ) { 20 | val controller = bottomSheetController.controller 21 | val state = bottomSheetController.state 22 | 23 | ModalBottomSheetScaffold( 24 | modifier = Modifier.animateContentSize(), 25 | state = state, 26 | nestedScrollEnabled = controller.currentDestination?.nestedScrollEnabled == true, 27 | dismissOnTapOutside = controller.currentDestination?.dismissOnTapOutside == true, 28 | gesturesEnabled = controller.currentDestination?.gesturesEnabled == true, 29 | onDismiss = controller.currentDestination?.onDismiss ?: { controller.popAll() }, 30 | sheetContent = { 31 | NavHost( 32 | controller = controller, 33 | ) { sheet -> 34 | when (sheet) { 35 | is BottomSheet.ForumFilter -> { 36 | ForumFilterBottomSheet( 37 | filters = sheet.filters, 38 | onFiltersChange = sheet.onFiltersChange 39 | ) 40 | } 41 | } 42 | } 43 | }, 44 | content = content 45 | ) 46 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/android/exception/GlobalExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.android.exception 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | import kotlin.system.exitProcess 8 | 9 | class GlobalExceptionHandler private constructor( 10 | private val applicationContext: Context, 11 | private val defaultHandler: Thread.UncaughtExceptionHandler, 12 | private val activityToBeLaunched: Class 13 | ) : Thread.UncaughtExceptionHandler { 14 | 15 | override fun uncaughtException(p0: Thread, p1: Throwable) { 16 | kotlin.runCatching { 17 | Log.e(this.toString(), p1.stackTraceToString()) 18 | applicationContext.launchActivity(activityToBeLaunched, p1) 19 | exitProcess(0) 20 | }.getOrElse { 21 | defaultHandler.uncaughtException(p0, p1) 22 | } 23 | } 24 | 25 | private fun Context.launchActivity( 26 | activity: Class, 27 | exception: Throwable 28 | ) { 29 | val crashedIntent = Intent(applicationContext, activity).apply { 30 | putExtra(INTENT_DATA_NAME, "$exception\n${Log.getStackTraceString(exception)}") 31 | addFlags(defFlags) 32 | } 33 | applicationContext.startActivity(crashedIntent) 34 | } 35 | 36 | companion object { 37 | private const val INTENT_DATA_NAME = "GlobalExceptionHandler" 38 | private const val defFlags = Intent.FLAG_ACTIVITY_CLEAR_TOP or 39 | Intent.FLAG_ACTIVITY_NEW_TASK or 40 | Intent.FLAG_ACTIVITY_CLEAR_TASK 41 | 42 | fun initialize( 43 | applicationContext: Context, 44 | activityToBeLaunched: Class 45 | ) = Thread.setDefaultUncaughtExceptionHandler( 46 | GlobalExceptionHandler( 47 | applicationContext, 48 | Thread.getDefaultUncaughtExceptionHandler() as Thread.UncaughtExceptionHandler, 49 | activityToBeLaunched 50 | ) 51 | ) 52 | 53 | fun Activity.getExceptionString(): String = intent.getStringExtra(INTENT_DATA_NAME) ?: "" 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/core/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.core.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import retrofit2.Retrofit 8 | import ru.tech.cookhelper.data.remote.api.auth.AuthService 9 | import ru.tech.cookhelper.data.remote.api.chat.ChatApi 10 | import ru.tech.cookhelper.data.remote.api.ingredients.FridgeApi 11 | import ru.tech.cookhelper.data.remote.api.user.UserApi 12 | import ru.tech.cookhelper.data.remote.web_socket.feed.FeedService 13 | import ru.tech.cookhelper.data.remote.web_socket.feed.FeedServiceImpl 14 | import ru.tech.cookhelper.data.remote.web_socket.message.MessageService 15 | import ru.tech.cookhelper.data.remote.web_socket.message.MessageServiceImpl 16 | import ru.tech.cookhelper.data.remote.web_socket.user.UserService 17 | import ru.tech.cookhelper.data.remote.web_socket.user.UserServiceImpl 18 | import ru.tech.cookhelper.data.utils.JsonParser 19 | import javax.inject.Singleton 20 | 21 | @Module 22 | @InstallIn(SingletonComponent::class) 23 | object NetworkModule { 24 | 25 | @Provides 26 | @Singleton 27 | fun provideAuthService( 28 | retrofit: Retrofit 29 | ): AuthService = retrofit.create(AuthService::class.java) 30 | 31 | @Provides 32 | @Singleton 33 | fun provideChatApi( 34 | retrofit: Retrofit 35 | ): ChatApi = retrofit.create(ChatApi::class.java) 36 | 37 | @Provides 38 | @Singleton 39 | fun provideUserApi( 40 | retrofit: Retrofit 41 | ): UserApi = retrofit.create(UserApi::class.java) 42 | 43 | @Provides 44 | @Singleton 45 | fun provideFridgeApi( 46 | retrofit: Retrofit 47 | ): FridgeApi = retrofit.create(FridgeApi::class.java) 48 | 49 | @Provides 50 | @Singleton 51 | fun provideMessageService( 52 | jsonParser: JsonParser 53 | ): MessageService = MessageServiceImpl(jsonParser) 54 | 55 | @Provides 56 | @Singleton 57 | fun provideFeedService( 58 | jsonParser: JsonParser 59 | ): FeedService = FeedServiceImpl(jsonParser) 60 | 61 | @Provides 62 | @Singleton 63 | fun provideUserService( 64 | jsonParser: JsonParser 65 | ): UserService = UserServiceImpl(jsonParser) 66 | 67 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/local/entity/UserEntity.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.local.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import ru.tech.cookhelper.data.local.utils.DatabaseEntity 6 | import ru.tech.cookhelper.domain.model.FileData 7 | import ru.tech.cookhelper.domain.model.Product 8 | import ru.tech.cookhelper.domain.model.User 9 | 10 | @Entity 11 | data class UserEntity( 12 | @PrimaryKey val id: Long, 13 | val avatar: List, 14 | val bannedIngredients: List, 15 | val bannedRecipes: List, 16 | val email: String, 17 | val forums: List, 18 | val fridge: List, 19 | val name: String, 20 | val nickname: String, 21 | val userPosts: List? = null, 22 | val userRecipes: List? = null, 23 | val starredIngredients: List, 24 | val starredRecipes: List, 25 | val status: String?, 26 | val verified: Boolean, 27 | val surname: String, 28 | val lastSeen: Long, 29 | val token: String 30 | ) : DatabaseEntity { 31 | override fun asDomain(): User = User( 32 | id = id, 33 | avatar = avatar, 34 | bannedIngredients = bannedIngredients, 35 | bannedRecipes = bannedRecipes, 36 | email = email, 37 | forums = forums, 38 | fridge = fridge, 39 | name = name, 40 | nickname = nickname, 41 | starredIngredients = userPosts, 42 | userPosts = userRecipes, 43 | userRecipes = starredIngredients, 44 | starredRecipes = starredRecipes, 45 | status = status, 46 | verified = verified, 47 | surname = surname, 48 | lastSeen = lastSeen, 49 | token = token 50 | ) 51 | } 52 | 53 | fun User.asDatabaseEntity() = UserEntity( 54 | id = id, 55 | avatar = avatar, 56 | bannedIngredients = bannedIngredients ?: emptyList(), 57 | bannedRecipes = bannedRecipes ?: emptyList(), 58 | email = email, 59 | forums = forums ?: emptyList(), 60 | fridge = fridge ?: emptyList(), 61 | name = name, 62 | nickname = nickname, 63 | userPosts = userPosts ?: emptyList(), 64 | userRecipes = userRecipes ?: emptyList(), 65 | starredIngredients = starredIngredients ?: emptyList(), 66 | starredRecipes = starredRecipes ?: emptyList(), 67 | status = status, 68 | verified = verified, 69 | surname = surname, 70 | lastSeen = lastSeen, 71 | token = token 72 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/compose/StateUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.compose 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.MutableState 5 | import androidx.compose.runtime.mutableStateMapOf 6 | import androidx.compose.runtime.saveable.Saver 7 | import androidx.compose.runtime.saveable.listSaver 8 | import androidx.compose.runtime.saveable.rememberSaveable 9 | import androidx.compose.runtime.snapshots.SnapshotStateList 10 | import androidx.compose.runtime.snapshots.SnapshotStateMap 11 | import androidx.compose.runtime.toMutableStateList 12 | 13 | object StateUtils { 14 | inline fun MutableState.update( 15 | transform: T.() -> T 16 | ) = apply { setValue(value = transform(this.value)) } 17 | 18 | inline fun MutableState.updateIf( 19 | predicate: T.() -> Boolean, 20 | transform: T.() -> T 21 | ) = apply { if (predicate(this.value)) transform(this.value) } 22 | 23 | fun MutableState.setValue(value: T) { 24 | this.value = value 25 | } 26 | 27 | @Composable 28 | fun rememberMutableStateListOf(vararg elements: T): SnapshotStateList { 29 | return rememberSaveable( 30 | saver = listSaver( 31 | save = { stateList -> 32 | if (stateList.isNotEmpty()) { 33 | val first = stateList.first() 34 | if (!canBeSaved(first)) { 35 | throw IllegalStateException("${first::class} cannot be saved. By default only types which can be stored in the Bundle class can be saved.") 36 | } 37 | } 38 | stateList.toList() 39 | }, 40 | restore = { it.toMutableStateList() } 41 | ) 42 | ) { 43 | elements.toList().toMutableStateList() 44 | } 45 | } 46 | 47 | @Composable 48 | fun Saver, String>.rememberMutableStateListOf( 49 | vararg elements: T 50 | ): SnapshotStateList { 51 | return rememberSaveable(saver = this) { elements.toList().toMutableStateList() } 52 | } 53 | 54 | fun Map.toMutableStateMap(): SnapshotStateMap { 55 | val map = mutableStateMapOf() 56 | this.forEach { 57 | map[it.key] = it.value 58 | } 59 | return map 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/recipe_post_creation/viewModel/RecipePostCreationViewModel.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.recipe_post_creation.viewModel 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.launchIn 10 | import kotlinx.coroutines.flow.onEach 11 | import ru.tech.cookhelper.domain.model.Product 12 | import ru.tech.cookhelper.domain.model.User 13 | import ru.tech.cookhelper.domain.use_case.get_user.GetUserUseCase 14 | import ru.tech.cookhelper.presentation.ui.utils.compose.StateUtils.update 15 | import javax.inject.Inject 16 | import kotlin.random.Random 17 | 18 | @HiltViewModel 19 | class RecipePostCreationViewModel @Inject constructor( 20 | getUserUseCase: GetUserUseCase 21 | ) : ViewModel() { 22 | 23 | private val _allProducts: MutableState> = mutableStateOf(emptyList()) 24 | val allProducts: List by _allProducts 25 | 26 | private val _products: MutableState> = mutableStateOf(emptyMap()) 27 | val products: Map by _products 28 | 29 | private val _user: MutableState = mutableStateOf(null) 30 | val user: User? by _user 31 | 32 | private val _categories: MutableState> = mutableStateOf(emptyList()) 33 | val categories: List by _categories 34 | 35 | init { 36 | getUserUseCase() 37 | .onEach { _user.update { it } } 38 | .launchIn(viewModelScope) 39 | 40 | _categories.value = List(15) { "Категория блюда с порядковым номером $it" } 41 | _allProducts.value = List(30) { 42 | Product( 43 | it, 44 | "Продукт $it", 45 | category = Random.nextInt(1, 22), 46 | mimetype = "грамм" 47 | ) 48 | } 49 | } 50 | 51 | fun sendRecipePost( 52 | label: String, 53 | imageUri: String, 54 | time: String, 55 | calories: String, 56 | proteins: String, 57 | fats: String, 58 | carbohydrates: String, 59 | category: String, 60 | steps: String 61 | ) { 62 | //TODO: Send data to server 63 | } 64 | 65 | fun setProducts(newProducts: Map) { 66 | _products.update { newProducts } 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/app/components/SimpleScaffold.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.app.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.layout.onSizeChanged 10 | import androidx.compose.ui.platform.LocalDensity 11 | import androidx.compose.ui.unit.dp 12 | import androidx.compose.ui.zIndex 13 | 14 | @Composable 15 | fun SimpleScaffold( 16 | modifier: Modifier = Modifier, 17 | topAppBar: (@Composable () -> Unit)? = null, 18 | bottomAppBar: (@Composable () -> Unit)? = null, 19 | appBarsPaddingProvider: ((PaddingValues) -> Unit)? = null, 20 | content: @Composable () -> Unit 21 | ) { 22 | val density = LocalDensity.current 23 | var topAppBarHeight by remember { mutableStateOf(0.dp) } 24 | var bottomAppBarHeight by remember { mutableStateOf(0.dp) } 25 | 26 | LaunchedEffect(topAppBarHeight, bottomAppBarHeight) { 27 | appBarsPaddingProvider?.invoke( 28 | PaddingValues( 29 | top = topAppBarHeight, 30 | bottom = bottomAppBarHeight 31 | ) 32 | ) 33 | } 34 | 35 | if (appBarsPaddingProvider != null) { 36 | Box(modifier = modifier) { 37 | Box( 38 | modifier = Modifier 39 | .align(Alignment.TopCenter) 40 | .zIndex(1f) 41 | .onSizeChanged { 42 | with(density) { 43 | topAppBarHeight = it.height.toDp() 44 | } 45 | } 46 | ) { topAppBar?.invoke() } 47 | Box( 48 | modifier = Modifier.zIndex(0f), 49 | ) { content() } 50 | Box( 51 | modifier = Modifier 52 | .align(Alignment.BottomCenter) 53 | .zIndex(1f) 54 | .onSizeChanged { 55 | with(density) { 56 | bottomAppBarHeight = it.height.toDp() 57 | } 58 | } 59 | ) { bottomAppBar?.invoke() } 60 | } 61 | } else { 62 | Column(modifier = modifier) { 63 | topAppBar?.invoke() 64 | content() 65 | bottomAppBar?.invoke() 66 | } 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/settings/SettingsScreen.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.settings 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.rememberScrollState 6 | import androidx.compose.foundation.verticalScroll 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.outlined.HelpOutline 9 | import androidx.compose.material3.IconButton 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.saveable.rememberSaveable 14 | import androidx.compose.runtime.setValue 15 | import androidx.compose.ui.Modifier 16 | import dev.olshevski.navigation.reimagined.hilt.hiltViewModel 17 | import ru.tech.cookhelper.presentation.settings.components.* 18 | import ru.tech.cookhelper.presentation.settings.viewModel.SettingsViewModel 19 | import ru.tech.cookhelper.presentation.ui.theme.invoke 20 | import ru.tech.cookhelper.presentation.ui.utils.provider.LocalTopAppBarVisuals 21 | import ru.tech.cookhelper.presentation.ui.widgets.NavigationBarsSpacer 22 | 23 | @Composable 24 | fun SettingsScreen( 25 | viewModel: SettingsViewModel = hiltViewModel(), 26 | settingsState: SettingsState 27 | ) { 28 | var showDialog by rememberSaveable { mutableStateOf(false) } 29 | 30 | LocalTopAppBarVisuals.current.update { 31 | actions { 32 | IconButton( 33 | onClick = { showDialog = true }, 34 | content = { Icons.Outlined.HelpOutline() } 35 | ) 36 | } 37 | } 38 | 39 | Column( 40 | Modifier 41 | .fillMaxSize() 42 | .verticalScroll(rememberScrollState()) 43 | ) { 44 | with(settingsState) { 45 | ChangeLanguageOption(viewModel::insertSetting) 46 | ThemeOption(viewModel::insertSetting) 47 | PureBlackOption(viewModel::insertSetting) 48 | ColorSchemeOption(viewModel::insertSetting) 49 | ThemePreviewOption() 50 | DynamicColorsOption(viewModel::insertSetting) 51 | FontSizeOption(viewModel::insertSetting) 52 | CartConnectionOption(viewModel::insertSetting) 53 | KeepScreenOnOption(viewModel::insertSetting) 54 | AppInfoVersionOption() 55 | 56 | NavigationBarsSpacer() 57 | } 58 | } 59 | 60 | if (showDialog) AboutAppDialog(onDismissRequest = { showDialog = false }) 61 | 62 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/settings/components/PickLanguageDialog.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.settings.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.outlined.Translate 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.unit.dp 14 | import ru.tech.cookhelper.R 15 | import ru.tech.cookhelper.presentation.ui.theme.DialogShape 16 | 17 | @Composable 18 | fun PickLanguageDialog( 19 | entries: Map, 20 | selected: String, 21 | onSelect: (String) -> Unit, 22 | onDismiss: () -> Unit 23 | ) { 24 | AlertDialog(onDismissRequest = onDismiss, 25 | shape = DialogShape, 26 | icon = { Icon(imageVector = Icons.Outlined.Translate, contentDescription = null) }, 27 | title = { Text(stringResource(R.string.language)) }, 28 | text = { 29 | Spacer(modifier = Modifier.height(8.dp)) 30 | Column( 31 | horizontalAlignment = Alignment.CenterHorizontally 32 | ) { 33 | entries.forEach { locale -> 34 | Row( 35 | modifier = Modifier 36 | .fillMaxWidth() 37 | .clip(MaterialTheme.shapes.small) 38 | .clickable { 39 | onSelect(locale.key) 40 | onDismiss() 41 | } 42 | .padding(start = 12.dp, end = 12.dp), 43 | verticalAlignment = Alignment.CenterVertically 44 | ) { 45 | RadioButton( 46 | selected = selected == locale.value, 47 | onClick = { 48 | onSelect(locale.key) 49 | onDismiss() 50 | } 51 | ) 52 | Text(locale.value) 53 | } 54 | } 55 | } 56 | Spacer(modifier = Modifier.height(8.dp)) 57 | }, 58 | confirmButton = { 59 | Button(onClick = onDismiss) { 60 | Text(stringResource(R.string.cancel)) 61 | } 62 | } 63 | ) 64 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/core/utils/RetrofitUtils.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.core.utils 2 | 3 | import okhttp3.Interceptor 4 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 5 | import okhttp3.MultipartBody 6 | import okhttp3.OkHttpClient 7 | import okhttp3.RequestBody.Companion.asRequestBody 8 | import okhttp3.Response 9 | import okhttp3.logging.HttpLoggingInterceptor 10 | import retrofit2.Retrofit 11 | import java.io.File 12 | import java.util.concurrent.TimeUnit 13 | 14 | 15 | object RetrofitUtils { 16 | 17 | fun File?.toMultipartFormData( 18 | type: String, 19 | filename: String = this?.name ?: "" 20 | ): MultipartBody.Part? = 21 | this?.let { 22 | MultipartBody.Part.createFormData( 23 | name = "image", 24 | filename = filename, 25 | body = it.asRequestBody(type.toMediaTypeOrNull()) 26 | ) 27 | } 28 | 29 | fun Retrofit.Builder.addLogger( 30 | level: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY 31 | ): Retrofit.Builder { 32 | val httpClient = OkHttpClient.Builder() 33 | .readTimeout(60, TimeUnit.SECONDS) 34 | .connectTimeout(60, TimeUnit.SECONDS) 35 | val logging = HttpLoggingInterceptor() 36 | logging.setLevel(level) 37 | 38 | return client(httpClient.addInterceptor(logging).build()) 39 | } 40 | 41 | fun Retrofit.Builder.setTimeout( 42 | timeout: Long = 60, 43 | timeUnit: TimeUnit = TimeUnit.SECONDS 44 | ): Retrofit.Builder = client( 45 | OkHttpClient.Builder() 46 | .setTimeout(timeout, timeUnit) 47 | .build() 48 | ) 49 | 50 | fun OkHttpClient.Builder.setTimeout( 51 | timeout: Long = 60, 52 | timeUnit: TimeUnit = TimeUnit.SECONDS 53 | ): OkHttpClient.Builder = readTimeout(timeout, timeUnit) 54 | .connectTimeout(timeout, timeUnit) 55 | 56 | fun retrofit2.Response.bodyOrThrow(): T = 57 | body() ?: throw Throwable("${code()} ${message()}") 58 | 59 | class RetryInterceptor( 60 | private val tryCount: Int = 3, 61 | private val reason: (Response) -> Boolean 62 | ) : Interceptor { 63 | override fun intercept(chain: Interceptor.Chain): Response { 64 | val request = chain.request() 65 | var response = chain.proceed(request) 66 | 67 | var tryCount = 0 68 | while (reason(response) && tryCount < this.tryCount) { 69 | tryCount++ 70 | response.close() 71 | response = chain.proceed(request) 72 | } 73 | return response 74 | } 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/edit_profile/components/EditProfileItem.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.edit_profile.components 2 | 3 | import androidx.compose.animation.animateContentSize 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.interaction.MutableInteractionSource 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.rounded.KeyboardArrowDown 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.* 14 | import androidx.compose.runtime.saveable.rememberSaveable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.rotate 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | 22 | @Composable 23 | fun EditProfileItem(text: String, content: @Composable ColumnScope.() -> Unit) { 24 | var expanded by rememberSaveable { mutableStateOf(false) } 25 | 26 | Column( 27 | modifier = Modifier.animateContentSize(), 28 | horizontalAlignment = Alignment.CenterHorizontally 29 | ) { 30 | Spacer(Modifier.height(12.dp)) 31 | Row( 32 | modifier = Modifier 33 | .fillMaxWidth() 34 | .clickable( 35 | interactionSource = remember { MutableInteractionSource() }, 36 | indication = null 37 | ) { expanded = !expanded }, 38 | verticalAlignment = Alignment.CenterVertically 39 | ) { 40 | Spacer(Modifier.width(12.dp)) 41 | Text( 42 | text = text, 43 | modifier = Modifier.weight(1f), 44 | fontWeight = FontWeight.Medium, 45 | fontSize = 18.sp 46 | ) 47 | val rotation: Float by animateFloatAsState(if (expanded) 180f else 0f) 48 | IconButton( 49 | onClick = { expanded = !expanded }, 50 | modifier = Modifier.rotate(rotation) 51 | ) { 52 | Icon( 53 | imageVector = Icons.Rounded.KeyboardArrowDown, 54 | contentDescription = null, 55 | modifier = Modifier.size(26.dp) 56 | ) 57 | } 58 | } 59 | if (expanded) { 60 | Spacer(Modifier.height(4.dp)) 61 | this@Column.content() 62 | } 63 | Spacer(Modifier.height(12.dp)) 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/data/repository/MessageRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.data.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.catch 5 | import kotlinx.coroutines.flow.flow 6 | import ru.tech.cookhelper.core.Action 7 | import ru.tech.cookhelper.core.constants.Status.SUCCESS 8 | import ru.tech.cookhelper.core.utils.kotlin.runIo 9 | import ru.tech.cookhelper.data.remote.api.chat.ChatApi 10 | import ru.tech.cookhelper.data.remote.web_socket.WebSocketState 11 | import ru.tech.cookhelper.data.remote.web_socket.message.MessageService 12 | import ru.tech.cookhelper.data.utils.JsonParser 13 | import ru.tech.cookhelper.domain.model.Chat 14 | import ru.tech.cookhelper.domain.model.FormMessage 15 | import ru.tech.cookhelper.domain.model.Message 16 | import ru.tech.cookhelper.domain.repository.MessageRepository 17 | import ru.tech.cookhelper.presentation.ui.utils.toAction 18 | import javax.inject.Inject 19 | 20 | class MessageRepositoryImpl @Inject constructor( 21 | private val messageService: MessageService, 22 | private val chatApi: ChatApi, 23 | private val jsonParser: JsonParser 24 | ) : MessageRepository { 25 | 26 | override fun getChat(chatId: Long, token: String): Flow> = flow { 27 | emit(Action.Loading()) 28 | val response = runIo { chatApi.getChat(chatId, token) } 29 | if (response.status == SUCCESS) emit(Action.Success(data = response.data?.asDomain())) 30 | else emit(Action.Empty(status = response.status)) 31 | }.catch { emit(it.toAction()) } 32 | 33 | override fun awaitNewMessages(chatId: Long, token: String): Flow> = flow { 34 | messageService(chatId = chatId, token = token) 35 | .catch { it.toAction() } 36 | .collect { state -> 37 | when (state) { 38 | is WebSocketState.Error -> emit(state.t.toAction()) 39 | is WebSocketState.Message -> emit(Action.Success(data = state.obj?.asDomain())) 40 | is WebSocketState.Opening, 41 | is WebSocketState.Restarting, 42 | is WebSocketState.Closing -> emit(Action.Loading()) 43 | is WebSocketState.Closed, is WebSocketState.Opened -> emit(Action.Empty()) 44 | } 45 | } 46 | } 47 | 48 | override fun sendMessage(message: FormMessage) { 49 | messageService.sendMessage(jsonParser.toJson(message, FormMessage::class.java) ?: "") 50 | } 51 | 52 | override fun stopAwaitingMessages() = messageService.closeService() 53 | 54 | override fun getChatList(token: String): Flow>> = flow { 55 | val response = chatApi.getChatList(token) 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/profile/components/EditStatusDialog.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.profile.components 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.outlined.Edit 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.* 8 | import androidx.compose.runtime.saveable.rememberSaveable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.compose.ui.unit.dp 12 | import androidx.compose.ui.window.DialogProperties 13 | import ru.tech.cookhelper.R 14 | import ru.tech.cookhelper.presentation.ui.theme.DialogShape 15 | import ru.tech.cookhelper.presentation.ui.widgets.* 16 | 17 | @OptIn(ExperimentalMaterial3Api::class) 18 | @Composable 19 | fun EditStatusDialog( 20 | currentStatus: String, 21 | onDone: (newStatus: String) -> Unit, 22 | onDismissRequest: () -> Unit 23 | ) { 24 | var status by rememberSaveable { mutableStateOf(currentStatus) } 25 | val textStyle = LocalTextStyle.current 26 | 27 | AlertDialog( 28 | properties = DialogProperties(usePlatformDefaultWidth = false), 29 | modifier = Modifier 30 | .width(TextFieldDefaults.MinWidth + 40.dp) 31 | .padding(vertical = 16.dp), 32 | title = { Text(stringResource(R.string.edit_status)) }, 33 | text = { 34 | Column { 35 | Spacer(Modifier.height(4.dp)) 36 | CompositionLocalProvider(LocalTextStyle provides textStyle) { 37 | CozyTextField( 38 | value = status, 39 | appearance = TextFieldAppearance.Outlined, 40 | onValueChange = { 41 | status = it 42 | }, 43 | label = { 44 | Text(stringResource(R.string.status)) 45 | } 46 | ) 47 | } 48 | Spacer(Modifier.height(4.dp)) 49 | } 50 | }, 51 | shape = DialogShape, 52 | onDismissRequest = { onDismissRequest() }, 53 | icon = { Icon(Icons.Outlined.Edit, null) }, 54 | confirmButton = { 55 | Button( 56 | onClick = { 57 | onDone(status) 58 | onDismissRequest() 59 | } 60 | ) { 61 | Text(stringResource(R.string.done)) 62 | } 63 | }, 64 | dismissButton = { 65 | FilledTonalButton(onClick = { onDismissRequest() }) { 66 | Text(stringResource(R.string.cancel)) 67 | } 68 | } 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/forum_discussion/components/RatingButton.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.forum_discussion.components 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.shape.CircleShape 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.rounded.ArrowDownward 7 | import androidx.compose.material.icons.rounded.ArrowUpward 8 | import androidx.compose.material3.* 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.text.style.TextAlign 14 | import androidx.compose.ui.unit.dp 15 | import ru.tech.cookhelper.presentation.ui.theme.Gray 16 | import ru.tech.cookhelper.presentation.ui.theme.Green 17 | import ru.tech.cookhelper.presentation.ui.theme.Red 18 | 19 | @Composable 20 | fun RatingButton( 21 | modifier: Modifier = Modifier, 22 | userRate: Int, 23 | currentRating: String, 24 | onRateUp: () -> Unit, 25 | onRateDown: () -> Unit 26 | ) { 27 | Surface( 28 | modifier = modifier, 29 | shape = CircleShape, 30 | color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.45f), 31 | contentColor = Gray 32 | ) { 33 | Row( 34 | verticalAlignment = Alignment.CenterVertically, 35 | horizontalArrangement = Arrangement.SpaceBetween 36 | ) { 37 | FilledIconButton( 38 | onClick = onRateUp, 39 | modifier = Modifier.padding(vertical = 3.dp), 40 | colors = IconButtonDefaults.filledIconButtonColors( 41 | containerColor = if (userRate == 1) Green.copy(alpha = 0.15f) else Color.Transparent 42 | ) 43 | ) { 44 | Icon( 45 | Icons.Rounded.ArrowUpward, 46 | null, 47 | tint = if (userRate == 1) Green else Gray 48 | ) 49 | } 50 | Spacer(Modifier.width(4.dp)) 51 | Text(currentRating, textAlign = TextAlign.Center) 52 | Spacer(Modifier.width(4.dp)) 53 | FilledIconButton( 54 | onClick = onRateDown, 55 | modifier = Modifier.padding(vertical = 3.dp), 56 | colors = IconButtonDefaults.filledIconButtonColors( 57 | containerColor = if (userRate == -1) Red.copy(alpha = 0.15f) else Color.Transparent 58 | ) 59 | ) { 60 | Icon( 61 | Icons.Rounded.ArrowDownward, 62 | null, 63 | tint = if (userRate == -1) Red else Gray 64 | ) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/core/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.core.di 2 | 3 | import com.squareup.moshi.Moshi 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import ru.tech.cookhelper.data.local.database.Database 9 | import ru.tech.cookhelper.data.remote.api.auth.AuthService 10 | import ru.tech.cookhelper.data.remote.api.chat.ChatApi 11 | import ru.tech.cookhelper.data.remote.api.ingredients.FridgeApi 12 | import ru.tech.cookhelper.data.remote.api.user.UserApi 13 | import ru.tech.cookhelper.data.remote.web_socket.feed.FeedService 14 | import ru.tech.cookhelper.data.remote.web_socket.message.MessageService 15 | import ru.tech.cookhelper.data.remote.web_socket.user.UserService 16 | import ru.tech.cookhelper.data.repository.FridgeRepositoryImpl 17 | import ru.tech.cookhelper.data.repository.MessageRepositoryImpl 18 | import ru.tech.cookhelper.data.repository.SettingsRepositoryImpl 19 | import ru.tech.cookhelper.data.repository.UserRepositoryImpl 20 | import ru.tech.cookhelper.data.utils.JsonParser 21 | import ru.tech.cookhelper.data.utils.MoshiParser 22 | import ru.tech.cookhelper.domain.repository.FridgeRepository 23 | import ru.tech.cookhelper.domain.repository.MessageRepository 24 | import ru.tech.cookhelper.domain.repository.SettingsRepository 25 | import ru.tech.cookhelper.domain.repository.UserRepository 26 | import javax.inject.Singleton 27 | 28 | @Module 29 | @InstallIn(SingletonComponent::class) 30 | object RepositoryModule { 31 | 32 | @Provides 33 | @Singleton 34 | fun provideSettingsRepository( 35 | db: Database 36 | ): SettingsRepository = SettingsRepositoryImpl( 37 | settingsDao = db.settingsDao 38 | ) 39 | 40 | @Provides 41 | @Singleton 42 | fun provideUserRepository( 43 | authService: AuthService, 44 | userApi: UserApi, 45 | db: Database, 46 | feedService: FeedService, 47 | userService: UserService 48 | ): UserRepository = UserRepositoryImpl( 49 | authService = authService, 50 | userApi = userApi, 51 | userDao = db.userDao, 52 | feedService = feedService, 53 | userService = userService 54 | ) 55 | 56 | @Provides 57 | @Singleton 58 | fun provideMessageRepository( 59 | messageService: MessageService, 60 | chatApi: ChatApi, 61 | jsonParser: JsonParser 62 | ): MessageRepository = MessageRepositoryImpl( 63 | jsonParser = jsonParser, 64 | messageService = messageService, 65 | chatApi = chatApi 66 | ) 67 | 68 | @Provides 69 | @Singleton 70 | fun provideIngredientsRepository( 71 | fridgeApi: FridgeApi 72 | ): FridgeRepository = FridgeRepositoryImpl(fridgeApi) 73 | 74 | @Provides 75 | fun provideJsonParser(): JsonParser = MoshiParser(Moshi.Builder().build()) 76 | 77 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/tech/cookhelper/presentation/ui/utils/compose/Modifiers.kt: -------------------------------------------------------------------------------- 1 | package ru.tech.cookhelper.presentation.ui.utils.compose 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.animation.core.FastOutSlowInEasing 5 | import androidx.compose.animation.core.animateDpAsState 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.TabPosition 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.composed 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.platform.LocalConfiguration 17 | import androidx.compose.ui.platform.debugInspectorInfo 18 | import androidx.compose.ui.unit.Dp 19 | import androidx.compose.ui.unit.dp 20 | import com.google.accompanist.placeholder.PlaceholderHighlight 21 | import com.google.accompanist.placeholder.material.placeholder 22 | import com.google.accompanist.placeholder.material.shimmer 23 | 24 | fun Modifier.customTabIndicatorOffset( 25 | currentTabPosition: TabPosition, 26 | tabWidth: Dp 27 | ): Modifier = composed( 28 | inspectorInfo = debugInspectorInfo { 29 | name = "customTabIndicatorOffset" 30 | value = currentTabPosition 31 | } 32 | ) { 33 | val currentTabWidth by animateDpAsState( 34 | targetValue = tabWidth, 35 | animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing) 36 | ) 37 | val indicatorOffset by animateDpAsState( 38 | targetValue = ((currentTabPosition.left + currentTabPosition.right - tabWidth) / 2), 39 | animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing) 40 | ) 41 | fillMaxWidth() 42 | .wrapContentSize(Alignment.BottomStart) 43 | .offset(x = indicatorOffset) 44 | .width(currentTabWidth) 45 | } 46 | 47 | fun Modifier.squareSize(): Modifier = composed { 48 | val modifier: Modifier 49 | LocalConfiguration.current.apply { 50 | val minSize = kotlin.math.min(screenWidthDp, screenHeightDp).dp 51 | modifier = if (orientation == Configuration.ORIENTATION_PORTRAIT) { 52 | Modifier.size(minSize) 53 | } else Modifier.size(minSize) 54 | } 55 | modifier 56 | } 57 | 58 | @Composable 59 | fun Modifier.shimmer( 60 | visible: Boolean, 61 | color: Color = MaterialTheme.colorScheme.surfaceVariant 62 | ) = then( 63 | Modifier.placeholder( 64 | visible = visible, 65 | color = color, 66 | highlight = PlaceholderHighlight.shimmer() 67 | ) 68 | ) 69 | 70 | fun Modifier.navigationBarsLandscapePadding(): Modifier = composed { 71 | if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { 72 | navigationBarsPadding() 73 | } else this 74 | } --------------------------------------------------------------------------------