From f41114b2619987c4a25e5199a796bca03bc72e22 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Wed, 16 Oct 2024 20:29:57 +0200 Subject: [PATCH 01/23] Project update --- .gitignore | 17 + app/.gitignore | 1 + app/build.gradle.kts | 134 ++ app/proguard-rules.pro | 21 + .../snipmeapp/ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 26 + app/src/main/java/dev/snipme/snipmeapp/App.kt | 37 + .../java/dev/snipme/snipmeapp/MainActivity.kt | 34 + .../dev/snipme/snipmeapp/bridge/Bridge.java | 1873 +++++++++++++++++ .../snipme/snipmeapp/bridge/ModelPlugin.kt | 106 + .../snipmeapp/bridge/detail/DetailModel.kt | 164 ++ .../bridge/detail/DetailModelPlugin.kt | 90 + .../snipmeapp/bridge/error/ErrorParsable.kt | 5 + .../snipmeapp/bridge/login/LoginModel.kt | 133 ++ .../bridge/login/LoginModelPlugin.kt | 64 + .../snipme/snipmeapp/bridge/main/MainModel.kt | 178 ++ .../snipmeapp/bridge/main/MainModelPlugin.kt | 96 + .../snipmeapp/bridge/session/SessionModel.kt | 24 + .../dev/snipme/snipmeapp/di/KoinConfig.kt | 13 + .../snipme/snipmeapp/di/MapperFilterModule.kt | 10 + .../dev/snipme/snipmeapp/di/ModelModule.kt | 14 + .../dev/snipme/snipmeapp/di/NetworkModule.kt | 72 + .../snipme/snipmeapp/di/PreferenceModule.kt | 11 + .../snipme/snipmeapp/di/RepositoryModule.kt | 21 + .../dev/snipme/snipmeapp/di/ServiceModule.kt | 13 + .../dev/snipme/snipmeapp/di/UseCaseModule.kt | 63 + .../dev/snipme/snipmeapp/di/UtilModule.kt | 19 + .../domain/auth/AuthorizationUseCase.kt | 20 + .../domain/auth/IdentifyUserUseCase.kt | 12 + .../domain/auth/InitialLoginUseCase.kt | 16 + .../snipmeapp/domain/auth/LoginInteractor.kt | 13 + .../snipmeapp/domain/auth/LoginUseCase.kt | 17 + .../domain/auth/LogoutUserUseCase.kt | 7 + .../snipmeapp/domain/auth/RegisterUseCase.kt | 18 + .../domain/clipboard/AddToClipboardUseCase.kt | 11 + .../clipboard/GetFromClipboardUseCase.kt | 16 + .../domain/error/DebugErrorHandler.kt | 38 + .../snipmeapp/domain/error/ErrorHandler.kt | 7 + .../domain/error/SafeErrorHandler.kt | 35 + .../error/exception/ConnectionException.kt | 3 + .../exception/ContentNotFoundException.kt | 3 + .../exception/ForbiddenActionException.kt | 3 + .../exception/NetworkNotAvailableException.kt | 3 + .../error/exception/NotAuthorizedException.kt | 3 + .../domain/error/exception/RemoteException.kt | 3 + .../exception/SessionExpiredException.kt | 3 + .../domain/error/exception/SnipException.kt | 5 + .../filter/FilterSnippetsByLanguageUseCase.kt | 10 + .../filter/FilterSnippetsByScopeUseCase.kt | 11 + .../filter/GetLanguageFiltersUseCase.kt | 18 + .../UpdateSnippetFiltersLanguageUseCase.kt | 24 + .../language/AvailableLanguageFilter.kt | 12 + .../domain/language/GetLanguagesUseCase.kt | 17 + .../domain/language/SnippetLanguage.kt | 7 + .../snipmeapp/domain/message/ErrorMessages.kt | 29 + .../domain/message/RealValidationMessages.kt | 26 + .../domain/message/ValidationMessages.kt | 22 + .../network/CheckNetworkAvailableUseCase.kt | 18 + .../reaction/GetTargetUserReactionUseCase.kt | 11 + .../domain/reaction/SetUserReactionUseCase.kt | 20 + .../snipmeapp/domain/reaction/UserReaction.kt | 7 + .../domain/repository/auth/AuthRepository.kt | 21 + .../repository/auth/AuthRepositoryReal.kt | 48 + .../repository/language/LanguageRepository.kt | 9 + .../language/LanguageRepositoryReal.kt | 24 + .../networkstate/NetworkStateRepository.kt | 10 + .../NetworkStateRepositoryReal.kt | 13 + .../repository/snippet/SnippetRepository.kt | 39 + .../snippet/SnippetRepositoryReal.kt | 72 + .../snippet/SnippetRepositoryTest.kt | 166 ++ .../domain/repository/user/UserRepository.kt | 8 + .../repository/user/UserRepositoryReal.kt | 18 + .../domain/share/ShareSnippetCodeUseCase.kt | 24 + .../snipmeapp/domain/share/ShareUser.kt | 16 + .../domain/snippet/CreateSnippetUseCase.kt | 27 + .../domain/snippet/DeleteSnippetUseCase.kt | 13 + .../domain/snippet/EditInteractor.kt | 32 + .../domain/snippet/GetSingleSnippetUseCase.kt | 20 + .../snippet/ObserveSnippetUpdatesUseCase.kt | 9 + .../ObserveUpdatedSnippetPageUseCase.kt | 36 + .../domain/snippet/SaveSnippetUseCase.kt | 20 + .../domain/snippet/UpdateSnippetUseCase.kt | 27 + .../domain/snippets/GetSnippetsUseCase.kt | 20 + .../snippets/HasMoreSnippetPagesUseCase.kt | 31 + .../snipmeapp/domain/snippets/Snippet.kt | 41 + .../domain/snippets/SnippetFilters.kt | 8 + .../domain/snippets/SnippetLanguageMapper.kt | 68 + .../domain/snippets/SnippetLanguageType.kt | 49 + .../domain/snippets/SnippetResponseMapper.kt | 59 + .../snipmeapp/domain/snippets/SnippetScope.kt | 7 + .../domain/snippets/SnippetVisibility.kt | 5 + .../domain/user/GetSingleUserUseCase.kt | 18 + .../dev/snipme/snipmeapp/domain/user/User.kt | 18 + .../infrastructure/local/AuthPreferences.kt | 17 + .../model/SnippetPageResponse.kt | 12 + .../model/request/CreateSnippetRequest.kt | 11 + .../model/request/IdentifyUserRequest.kt | 6 + .../model/request/LoginUserRequest.kt | 6 + .../model/request/RateSnippetRequest.kt | 6 + .../model/request/RegisterUserRequest.kt | 6 + .../model/request/ShareSnippetRequest.kt | 10 + .../model/response/LanguageResponse.kt | 6 + .../model/response/PersonResponse.kt | 20 + .../model/response/RegisterUserResponse.kt | 20 + .../model/response/SnippetResponse.kt | 22 + .../model/response/TokenResponse.kt | 6 + .../infrastructure/remote/AuthService.kt | 21 + .../infrastructure/remote/LanguageService.kt | 12 + .../infrastructure/remote/ShareService.kt | 19 + .../infrastructure/remote/SnippetService.kt | 39 + .../infrastructure/remote/UserService.kt | 11 + .../snipme/snipmeapp/util/AuthInterceptor.kt | 24 + .../snipmeapp/util/CrashReportingTree.kt | 9 + .../snipme/snipmeapp/util/PreferencesUtil.kt | 39 + .../snipmeapp/util/SyntaxHighlighter.kt | 240 +++ .../snipme/snipmeapp/util/SyntaxPhrases.kt | 43 + .../util/extension/CollectionExtensions.kt | 8 + .../util/extension/NetworkExtensions.kt | 10 + .../util/extension/NumberExtensions.kt | 19 + .../snipmeapp/util/extension/RxExtensions.kt | 19 + .../util/extension/TextExtensions.kt | 37 + app/src/main/res/drawable/logo.png | Bin 0 -> 18091 bytes app/src/main/res/layout/activity_main.xml | 13 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1720 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 1926 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3605 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1162 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1176 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2298 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2346 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 2588 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5197 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3673 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 4524 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 8374 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 5186 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 6801 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 12294 bytes app/src/main/res/values-pl/strings.xml | 39 + .../res/values/ic_launcher_background.xml | 4 + app/src/main/res/values/strings.xml | 44 + .../main/res/xml/data_extraction_rules.xml | 36 + .../main/res/xml/network_security_config.xml | 7 + .../dev/snipme/snipmeapp/ExampleUnitTest.kt | 17 + build.gradle.kts | 8 + flutter_module/.gitignore | 49 + flutter_module/.metadata | 10 + flutter_module/README.md | 11 + flutter_module/analysis_options.yaml | 4 + .../assets/images/illustrations/app_logo.png | Bin 0 -> 2046 bytes flutter_module/bridge/main_model.dart | 219 ++ flutter_module/fonts/Kanit-Regular.ttf | Bin 0 -> 169744 bytes flutter_module/lib/generated/assets.dart | 10 + flutter_module/lib/main.dart | 60 + flutter_module/lib/model/main_model.dart | 1258 +++++++++++ .../navigation/details/details_navigator.dart | 17 + .../navigation/login/login_navigator.dart | 20 + .../navigation/screen_navigator.dart | 14 + .../presentation/screens/details_screen.dart | 177 ++ .../presentation/screens/login_screen.dart | 140 ++ .../lib/presentation/screens/main_screen.dart | 281 +++ .../presentation/screens/named_screen.dart | 14 + .../lib/presentation/styles/color_styles.dart | 14 + .../lib/presentation/styles/dimens.dart | 14 + .../presentation/styles/padding_styles.dart | 11 + .../presentation/styles/surface_styles.dart | 63 + .../lib/presentation/styles/text_styles.dart | 93 + .../presentation/widgets/code_text_view.dart | 77 + .../presentation/widgets/filter_dropdown.dart | 42 + .../widgets/filter_list_view.dart | 38 + .../widgets/login_input_card.dart | 90 + ...o_overscroll_single_child_scroll_view.dart | 27 + .../widgets/rounded_action_button.dart | 46 + .../widgets/snippet_action_bar.dart | 81 + .../widgets/snippet_details_bar.dart | 75 + .../widgets/snippet_list_item.dart | 64 + .../lib/presentation/widgets/state_icon.dart | 40 + .../widgets/text_input_field.dart | 87 + .../widgets/view_state_wrapper.dart | 49 + .../extensions/build_context_extensions.dart | 12 + .../extensions/collection_extensions.dart | 78 + .../utils/extensions/state_extensions.dart | 43 + .../lib/utils/extensions/text_extensions.dart | 49 + .../lib/utils/hooks/use_navigator.dart | 45 + .../hooks/use_observable_state_hook.dart | 33 + .../lib/utils/hooks/use_same_state.dart | 51 + flutter_module/lib/utils/mock/mock_page.dart | 26 + flutter_module/lib/utils/mock/mocks.dart | 104 + flutter_module/pubspec.lock | 658 ++++++ flutter_module/pubspec.yaml | 105 + flutter_module/test/widget_test.dart | 2 + flutter_settings.gradle | 5 + gradle.properties | 24 + gradlew | 185 ++ gradlew.bat | 89 + settings.gradle.kts | 37 + 198 files changed, 10324 insertions(+) create mode 100644 .gitignore create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/dev/snipme/snipmeapp/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/dev/snipme/snipmeapp/App.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/MainActivity.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/bridge/Bridge.java create mode 100644 app/src/main/java/dev/snipme/snipmeapp/bridge/ModelPlugin.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/bridge/detail/DetailModel.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/bridge/detail/DetailModelPlugin.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/bridge/error/ErrorParsable.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/bridge/login/LoginModel.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/bridge/login/LoginModelPlugin.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModel.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModelPlugin.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/bridge/session/SessionModel.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/KoinConfig.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/MapperFilterModule.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/ModelModule.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/NetworkModule.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/PreferenceModule.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/RepositoryModule.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/UtilModule.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/auth/AuthorizationUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/auth/IdentifyUserUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/auth/InitialLoginUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginInteractor.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/auth/LogoutUserUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/auth/RegisterUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/clipboard/AddToClipboardUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/clipboard/GetFromClipboardUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/error/DebugErrorHandler.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/error/ErrorHandler.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/error/SafeErrorHandler.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/ConnectionException.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/ContentNotFoundException.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/ForbiddenActionException.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/NetworkNotAvailableException.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/NotAuthorizedException.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/RemoteException.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/SessionExpiredException.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/SnipException.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/filter/FilterSnippetsByLanguageUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/filter/FilterSnippetsByScopeUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/filter/GetLanguageFiltersUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/filter/UpdateSnippetFiltersLanguageUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/language/AvailableLanguageFilter.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/language/GetLanguagesUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/language/SnippetLanguage.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/message/ErrorMessages.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/message/RealValidationMessages.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/message/ValidationMessages.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/network/CheckNetworkAvailableUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/reaction/GetTargetUserReactionUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/reaction/UserReaction.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepository.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepositoryReal.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/repository/language/LanguageRepository.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/repository/language/LanguageRepositoryReal.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/repository/networkstate/NetworkStateRepository.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/repository/networkstate/NetworkStateRepositoryReal.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryTest.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepository.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepositoryReal.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetCodeUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareUser.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippet/DeleteSnippetUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippet/EditInteractor.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippet/GetSingleSnippetUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveSnippetUpdatesUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveUpdatedSnippetPageUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippet/SaveSnippetUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetSnippetsUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippets/HasMoreSnippetPagesUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippets/Snippet.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetFilters.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetLanguageMapper.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetLanguageType.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetScope.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetVisibility.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/user/GetSingleUserUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/user/User.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AuthPreferences.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/SnippetPageResponse.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/CreateSnippetRequest.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/IdentifyUserRequest.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/LoginUserRequest.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/RateSnippetRequest.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/RegisterUserRequest.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/ShareSnippetRequest.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/LanguageResponse.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/PersonResponse.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/RegisterUserResponse.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/SnippetResponse.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/TokenResponse.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/AuthService.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/LanguageService.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/ShareService.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/SnippetService.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/UserService.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/util/AuthInterceptor.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/util/CrashReportingTree.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/util/PreferencesUtil.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/util/SyntaxHighlighter.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/util/SyntaxPhrases.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/util/extension/CollectionExtensions.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/util/extension/NetworkExtensions.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/util/extension/NumberExtensions.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/util/extension/RxExtensions.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/util/extension/TextExtensions.kt create mode 100644 app/src/main/res/drawable/logo.png create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values-pl/strings.xml create mode 100644 app/src/main/res/values/ic_launcher_background.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/main/res/xml/network_security_config.xml create mode 100644 app/src/test/java/dev/snipme/snipmeapp/ExampleUnitTest.kt create mode 100644 build.gradle.kts create mode 100644 flutter_module/.gitignore create mode 100644 flutter_module/.metadata create mode 100644 flutter_module/README.md create mode 100644 flutter_module/analysis_options.yaml create mode 100644 flutter_module/assets/images/illustrations/app_logo.png create mode 100644 flutter_module/bridge/main_model.dart create mode 100644 flutter_module/fonts/Kanit-Regular.ttf create mode 100644 flutter_module/lib/generated/assets.dart create mode 100644 flutter_module/lib/main.dart create mode 100644 flutter_module/lib/model/main_model.dart create mode 100644 flutter_module/lib/presentation/navigation/details/details_navigator.dart create mode 100644 flutter_module/lib/presentation/navigation/login/login_navigator.dart create mode 100644 flutter_module/lib/presentation/navigation/screen_navigator.dart create mode 100644 flutter_module/lib/presentation/screens/details_screen.dart create mode 100644 flutter_module/lib/presentation/screens/login_screen.dart create mode 100644 flutter_module/lib/presentation/screens/main_screen.dart create mode 100644 flutter_module/lib/presentation/screens/named_screen.dart create mode 100644 flutter_module/lib/presentation/styles/color_styles.dart create mode 100644 flutter_module/lib/presentation/styles/dimens.dart create mode 100644 flutter_module/lib/presentation/styles/padding_styles.dart create mode 100644 flutter_module/lib/presentation/styles/surface_styles.dart create mode 100644 flutter_module/lib/presentation/styles/text_styles.dart create mode 100644 flutter_module/lib/presentation/widgets/code_text_view.dart create mode 100644 flutter_module/lib/presentation/widgets/filter_dropdown.dart create mode 100644 flutter_module/lib/presentation/widgets/filter_list_view.dart create mode 100644 flutter_module/lib/presentation/widgets/login_input_card.dart create mode 100644 flutter_module/lib/presentation/widgets/no_overscroll_single_child_scroll_view.dart create mode 100644 flutter_module/lib/presentation/widgets/rounded_action_button.dart create mode 100644 flutter_module/lib/presentation/widgets/snippet_action_bar.dart create mode 100644 flutter_module/lib/presentation/widgets/snippet_details_bar.dart create mode 100644 flutter_module/lib/presentation/widgets/snippet_list_item.dart create mode 100644 flutter_module/lib/presentation/widgets/state_icon.dart create mode 100644 flutter_module/lib/presentation/widgets/text_input_field.dart create mode 100644 flutter_module/lib/presentation/widgets/view_state_wrapper.dart create mode 100644 flutter_module/lib/utils/extensions/build_context_extensions.dart create mode 100644 flutter_module/lib/utils/extensions/collection_extensions.dart create mode 100644 flutter_module/lib/utils/extensions/state_extensions.dart create mode 100644 flutter_module/lib/utils/extensions/text_extensions.dart create mode 100644 flutter_module/lib/utils/hooks/use_navigator.dart create mode 100644 flutter_module/lib/utils/hooks/use_observable_state_hook.dart create mode 100644 flutter_module/lib/utils/hooks/use_same_state.dart create mode 100644 flutter_module/lib/utils/mock/mock_page.dart create mode 100644 flutter_module/lib/utils/mock/mocks.dart create mode 100644 flutter_module/pubspec.lock create mode 100644 flutter_module/pubspec.yaml create mode 100644 flutter_module/test/widget_test.dart create mode 100644 flutter_settings.gradle create mode 100644 gradle.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01a0e4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +/.idea/ +/gradle/ \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..ed89cf4 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,134 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + id("com.google.devtools.ksp") +} + +android { + namespace = "dev.snipme.snipmeapp" + compileSdk = 34 + + defaultConfig { + applicationId = "dev.snipme.snipmeapp" + minSdk = 31 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + ndk { + // Filter for architectures supported by Flutter + abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64") + } + } + + buildFeatures{ + android.buildFeatures.buildConfig=true + } + + buildTypes { + val BASE_URL = "BASE_URL" + val API_BASE_URL = "\"http://91.195.93.3:8000/snip-api/\"" // Must end with '/' + val STRING = "String" + + debug { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + + buildConfigField(STRING, BASE_URL, API_BASE_URL) + } + + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + buildConfigField(STRING, BASE_URL, API_BASE_URL) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + compose = true + } +} + +dependencies { + implementation(project(":flutter")) + // Kotlin + implementation(libs.kotlin.stdlib) + // AndroidX Libraries + implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.reactivestreams.ktx) + implementation(libs.androidx.navigation.runtime.ktx) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.firebase.analytics.ktx) + implementation(platform(libs.firebase.bom)) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.recyclerview) + implementation(libs.androidx.annotation) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + + // RX Libraries + implementation(libs.rxkotlin) + implementation(libs.rxandroid) + + // Dependency Injection + implementation(platform(libs.koin.bom)) + implementation(libs.koin.core) + implementation(libs.koin.android) + + // Network Libraries + implementation(libs.retrofit) + ksp(libs.moshi.kotlin.codegen) + implementation(libs.moshi.kotlin) + implementation(libs.adapter.rxjava2) + implementation(libs.converter.moshi) + implementation(libs.logging.interceptor) + implementation(libs.reactivenetwork.rx2) + + // Utility Libraries + implementation(libs.timber) + implementation(libs.glide) + ksp(libs.compiler) +// implementation(libs.codeview) + implementation(libs.kodeview) + + // Testing Libraries + testImplementation(libs.mockk) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + + // Debugging Tools + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/dev/snipme/snipmeapp/ExampleInstrumentedTest.kt b/app/src/androidTest/java/dev/snipme/snipmeapp/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..491986e --- /dev/null +++ b/app/src/androidTest/java/dev/snipme/snipmeapp/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package dev.snipme.snipmeapp + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("dev.snipme.snipmeapp", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ca695b1 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/App.kt b/app/src/main/java/dev/snipme/snipmeapp/App.kt new file mode 100644 index 0000000..0021273 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/App.kt @@ -0,0 +1,37 @@ +package dev.snipme.snipmeapp + +import android.app.Application +import androidx.multidex.BuildConfig +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin +import org.koin.dsl.koinApplication +import dev.snipme.snipmeapp.di.koinModules +import dev.snipme.snipmeapp.util.CrashReportingTree +import timber.log.Timber + +class App : Application() { + + override fun onCreate() { + super.onCreate() + Thread.setDefaultUncaughtExceptionHandler { thread, ex -> + Timber.e(ex) + } + initLogs() + // train classifier on app start +// CodeProcessor.init(this) + startKoin { + androidLogger() + androidContext(this@App) + modules(koinModules) + } + } + + private fun initLogs() { + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } else { + Timber.plant(CrashReportingTree()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/MainActivity.kt b/app/src/main/java/dev/snipme/snipmeapp/MainActivity.kt new file mode 100644 index 0000000..92a818d --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/MainActivity.kt @@ -0,0 +1,34 @@ +package dev.snipme.snipmeapp + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.FlutterEngineCache +import io.flutter.embedding.engine.dart.DartExecutor +import dev.snipme.snipmeapp.bridge.detail.DetailModelPlugin +import dev.snipme.snipmeapp.bridge.login.LoginModelPlugin +import dev.snipme.snipmeapp.bridge.main.MainModelPlugin + +class MainActivity : AppCompatActivity() { + private lateinit var flutterEngine : FlutterEngine + + private val cachedEngineId = "ID_CACHED_FLUTTER_ENGINE" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + FlutterEngine(this).apply { + dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault()) + + plugins.add(LoginModelPlugin()) + plugins.add(MainModelPlugin()) + plugins.add(DetailModelPlugin()) + + FlutterEngineCache.getInstance().put(cachedEngineId, this) + + startActivity(FlutterActivity.withCachedEngine(cachedEngineId).build(baseContext)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/Bridge.java b/app/src/main/java/dev/snipme/snipmeapp/bridge/Bridge.java new file mode 100644 index 0000000..f92c17d --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/Bridge.java @@ -0,0 +1,1873 @@ +// Autogenerated from Pigeon (v4.2.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package dev.snipme.snipmeapp.bridge; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/**Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) +public class Bridge { + + public enum SnippetLanguageType { + C(0), + CPP(1), + OBJECTIVE_C(2), + C_SHARP(3), + JAVA(4), + BASH(5), + PYTHON(6), + PERL(7), + RUBY(8), + SWIFT(9), + JAVASCRIPT(10), + KOTLIN(11), + COFFEESCRIPT(12), + RUST(13), + BASIC(14), + CLOJURE(15), + CSS(16), + DART(17), + ERLANG(18), + GO(19), + HASKELL(20), + LISP(21), + LLVM(22), + LUA(23), + MATLAB(24), + ML(25), + MUMPS(26), + NEMERLE(27), + PASCAL(28), + R(29), + RD(30), + SCALA(31), + SQL(32), + TEX(33), + VB(34), + VHDL(35), + TCL(36), + XQUERY(37), + YAML(38), + MARKDOWN(39), + JSON(40), + XML(41), + PROTO(42), + REGEX(43), + UNKNOWN(44); + + private int index; + private SnippetLanguageType(final int index) { + this.index = index; + } + } + + public enum SnippetFilterType { + ALL(0), + MINE(1), + SHARED(2); + + private int index; + private SnippetFilterType(final int index) { + this.index = index; + } + } + + public enum UserReaction { + NONE(0), + LIKE(1), + DISLIKE(2); + + private int index; + private UserReaction(final int index) { + this.index = index; + } + } + + public enum ModelState { + LOADING(0), + LOADED(1), + ERROR(2); + + private int index; + private ModelState(final int index) { + this.index = index; + } + } + + public enum MainModelEvent { + NONE(0), + ALERT(1), + LOGOUT(2); + + private int index; + private MainModelEvent(final int index) { + this.index = index; + } + } + + public enum DetailModelEvent { + NONE(0), + SAVED(1), + DELETED(2); + + private int index; + private DetailModelEvent(final int index) { + this.index = index; + } + } + + public enum LoginModelEvent { + NONE(0), + LOGGED(1); + + private int index; + private LoginModelEvent(final int index) { + this.index = index; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class Snippet { + private @Nullable String uuid; + public @Nullable String getUuid() { return uuid; } + public void setUuid(@Nullable String setterArg) { + this.uuid = setterArg; + } + + private @Nullable String title; + public @Nullable String getTitle() { return title; } + public void setTitle(@Nullable String setterArg) { + this.title = setterArg; + } + + private @Nullable SnippetCode code; + public @Nullable SnippetCode getCode() { return code; } + public void setCode(@Nullable SnippetCode setterArg) { + this.code = setterArg; + } + + private @Nullable SnippetLanguage language; + public @Nullable SnippetLanguage getLanguage() { return language; } + public void setLanguage(@Nullable SnippetLanguage setterArg) { + this.language = setterArg; + } + + private @Nullable Owner owner; + public @Nullable Owner getOwner() { return owner; } + public void setOwner(@Nullable Owner setterArg) { + this.owner = setterArg; + } + + private @Nullable Boolean isOwner; + public @Nullable Boolean getIsOwner() { return isOwner; } + public void setIsOwner(@Nullable Boolean setterArg) { + this.isOwner = setterArg; + } + + private @Nullable String timeAgo; + public @Nullable String getTimeAgo() { return timeAgo; } + public void setTimeAgo(@Nullable String setterArg) { + this.timeAgo = setterArg; + } + + private @Nullable Long voteResult; + public @Nullable Long getVoteResult() { return voteResult; } + public void setVoteResult(@Nullable Long setterArg) { + this.voteResult = setterArg; + } + + private @Nullable UserReaction userReaction; + public @Nullable UserReaction getUserReaction() { return userReaction; } + public void setUserReaction(@Nullable UserReaction setterArg) { + this.userReaction = setterArg; + } + + private @Nullable Boolean isPrivate; + public @Nullable Boolean getIsPrivate() { return isPrivate; } + public void setIsPrivate(@Nullable Boolean setterArg) { + this.isPrivate = setterArg; + } + + private @Nullable Boolean isLiked; + public @Nullable Boolean getIsLiked() { return isLiked; } + public void setIsLiked(@Nullable Boolean setterArg) { + this.isLiked = setterArg; + } + + private @Nullable Boolean isDisliked; + public @Nullable Boolean getIsDisliked() { return isDisliked; } + public void setIsDisliked(@Nullable Boolean setterArg) { + this.isDisliked = setterArg; + } + + private @Nullable Boolean isSaved; + public @Nullable Boolean getIsSaved() { return isSaved; } + public void setIsSaved(@Nullable Boolean setterArg) { + this.isSaved = setterArg; + } + + private @Nullable Boolean isToDelete; + public @Nullable Boolean getIsToDelete() { return isToDelete; } + public void setIsToDelete(@Nullable Boolean setterArg) { + this.isToDelete = setterArg; + } + + public static final class Builder { + private @Nullable String uuid; + public @NonNull Builder setUuid(@Nullable String setterArg) { + this.uuid = setterArg; + return this; + } + private @Nullable String title; + public @NonNull Builder setTitle(@Nullable String setterArg) { + this.title = setterArg; + return this; + } + private @Nullable SnippetCode code; + public @NonNull Builder setCode(@Nullable SnippetCode setterArg) { + this.code = setterArg; + return this; + } + private @Nullable SnippetLanguage language; + public @NonNull Builder setLanguage(@Nullable SnippetLanguage setterArg) { + this.language = setterArg; + return this; + } + private @Nullable Owner owner; + public @NonNull Builder setOwner(@Nullable Owner setterArg) { + this.owner = setterArg; + return this; + } + private @Nullable Boolean isOwner; + public @NonNull Builder setIsOwner(@Nullable Boolean setterArg) { + this.isOwner = setterArg; + return this; + } + private @Nullable String timeAgo; + public @NonNull Builder setTimeAgo(@Nullable String setterArg) { + this.timeAgo = setterArg; + return this; + } + private @Nullable Long voteResult; + public @NonNull Builder setVoteResult(@Nullable Long setterArg) { + this.voteResult = setterArg; + return this; + } + private @Nullable UserReaction userReaction; + public @NonNull Builder setUserReaction(@Nullable UserReaction setterArg) { + this.userReaction = setterArg; + return this; + } + private @Nullable Boolean isPrivate; + public @NonNull Builder setIsPrivate(@Nullable Boolean setterArg) { + this.isPrivate = setterArg; + return this; + } + private @Nullable Boolean isLiked; + public @NonNull Builder setIsLiked(@Nullable Boolean setterArg) { + this.isLiked = setterArg; + return this; + } + private @Nullable Boolean isDisliked; + public @NonNull Builder setIsDisliked(@Nullable Boolean setterArg) { + this.isDisliked = setterArg; + return this; + } + private @Nullable Boolean isSaved; + public @NonNull Builder setIsSaved(@Nullable Boolean setterArg) { + this.isSaved = setterArg; + return this; + } + private @Nullable Boolean isToDelete; + public @NonNull Builder setIsToDelete(@Nullable Boolean setterArg) { + this.isToDelete = setterArg; + return this; + } + public @NonNull Snippet build() { + Snippet pigeonReturn = new Snippet(); + pigeonReturn.setUuid(uuid); + pigeonReturn.setTitle(title); + pigeonReturn.setCode(code); + pigeonReturn.setLanguage(language); + pigeonReturn.setOwner(owner); + pigeonReturn.setIsOwner(isOwner); + pigeonReturn.setTimeAgo(timeAgo); + pigeonReturn.setVoteResult(voteResult); + pigeonReturn.setUserReaction(userReaction); + pigeonReturn.setIsPrivate(isPrivate); + pigeonReturn.setIsLiked(isLiked); + pigeonReturn.setIsDisliked(isDisliked); + pigeonReturn.setIsSaved(isSaved); + pigeonReturn.setIsToDelete(isToDelete); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("uuid", uuid); + toMapResult.put("title", title); + toMapResult.put("code", (code == null) ? null : code.toMap()); + toMapResult.put("language", (language == null) ? null : language.toMap()); + toMapResult.put("owner", (owner == null) ? null : owner.toMap()); + toMapResult.put("isOwner", isOwner); + toMapResult.put("timeAgo", timeAgo); + toMapResult.put("voteResult", voteResult); + toMapResult.put("userReaction", userReaction == null ? null : userReaction.index); + toMapResult.put("isPrivate", isPrivate); + toMapResult.put("isLiked", isLiked); + toMapResult.put("isDisliked", isDisliked); + toMapResult.put("isSaved", isSaved); + toMapResult.put("isToDelete", isToDelete); + return toMapResult; + } + static @NonNull Snippet fromMap(@NonNull Map map) { + Snippet pigeonResult = new Snippet(); + Object uuid = map.get("uuid"); + pigeonResult.setUuid((String)uuid); + Object title = map.get("title"); + pigeonResult.setTitle((String)title); + Object code = map.get("code"); + pigeonResult.setCode((code == null) ? null : SnippetCode.fromMap((Map)code)); + Object language = map.get("language"); + pigeonResult.setLanguage((language == null) ? null : SnippetLanguage.fromMap((Map)language)); + Object owner = map.get("owner"); + pigeonResult.setOwner((owner == null) ? null : Owner.fromMap((Map)owner)); + Object isOwner = map.get("isOwner"); + pigeonResult.setIsOwner((Boolean)isOwner); + Object timeAgo = map.get("timeAgo"); + pigeonResult.setTimeAgo((String)timeAgo); + Object voteResult = map.get("voteResult"); + pigeonResult.setVoteResult((voteResult == null) ? null : ((voteResult instanceof Integer) ? (Integer)voteResult : (Long)voteResult)); + Object userReaction = map.get("userReaction"); + pigeonResult.setUserReaction(userReaction == null ? null : UserReaction.values()[(int)userReaction]); + Object isPrivate = map.get("isPrivate"); + pigeonResult.setIsPrivate((Boolean)isPrivate); + Object isLiked = map.get("isLiked"); + pigeonResult.setIsLiked((Boolean)isLiked); + Object isDisliked = map.get("isDisliked"); + pigeonResult.setIsDisliked((Boolean)isDisliked); + Object isSaved = map.get("isSaved"); + pigeonResult.setIsSaved((Boolean)isSaved); + Object isToDelete = map.get("isToDelete"); + pigeonResult.setIsToDelete((Boolean)isToDelete); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class SnippetCode { + private @Nullable String raw; + public @Nullable String getRaw() { return raw; } + public void setRaw(@Nullable String setterArg) { + this.raw = setterArg; + } + + private @Nullable List tokens; + public @Nullable List getTokens() { return tokens; } + public void setTokens(@Nullable List setterArg) { + this.tokens = setterArg; + } + + public static final class Builder { + private @Nullable String raw; + public @NonNull Builder setRaw(@Nullable String setterArg) { + this.raw = setterArg; + return this; + } + private @Nullable List tokens; + public @NonNull Builder setTokens(@Nullable List setterArg) { + this.tokens = setterArg; + return this; + } + public @NonNull SnippetCode build() { + SnippetCode pigeonReturn = new SnippetCode(); + pigeonReturn.setRaw(raw); + pigeonReturn.setTokens(tokens); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("raw", raw); + toMapResult.put("tokens", tokens); + return toMapResult; + } + static @NonNull SnippetCode fromMap(@NonNull Map map) { + SnippetCode pigeonResult = new SnippetCode(); + Object raw = map.get("raw"); + pigeonResult.setRaw((String)raw); + Object tokens = map.get("tokens"); + pigeonResult.setTokens((List)tokens); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class SyntaxToken { + private @Nullable Long start; + public @Nullable Long getStart() { return start; } + public void setStart(@Nullable Long setterArg) { + this.start = setterArg; + } + + private @Nullable Long end; + public @Nullable Long getEnd() { return end; } + public void setEnd(@Nullable Long setterArg) { + this.end = setterArg; + } + + private @Nullable Long color; + public @Nullable Long getColor() { return color; } + public void setColor(@Nullable Long setterArg) { + this.color = setterArg; + } + + public static final class Builder { + private @Nullable Long start; + public @NonNull Builder setStart(@Nullable Long setterArg) { + this.start = setterArg; + return this; + } + private @Nullable Long end; + public @NonNull Builder setEnd(@Nullable Long setterArg) { + this.end = setterArg; + return this; + } + private @Nullable Long color; + public @NonNull Builder setColor(@Nullable Long setterArg) { + this.color = setterArg; + return this; + } + public @NonNull SyntaxToken build() { + SyntaxToken pigeonReturn = new SyntaxToken(); + pigeonReturn.setStart(start); + pigeonReturn.setEnd(end); + pigeonReturn.setColor(color); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("start", start); + toMapResult.put("end", end); + toMapResult.put("color", color); + return toMapResult; + } + static @NonNull SyntaxToken fromMap(@NonNull Map map) { + SyntaxToken pigeonResult = new SyntaxToken(); + Object start = map.get("start"); + pigeonResult.setStart((start == null) ? null : ((start instanceof Integer) ? (Integer)start : (Long)start)); + Object end = map.get("end"); + pigeonResult.setEnd((end == null) ? null : ((end instanceof Integer) ? (Integer)end : (Long)end)); + Object color = map.get("color"); + pigeonResult.setColor((color == null) ? null : ((color instanceof Integer) ? (Integer)color : (Long)color)); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class SnippetLanguage { + private @Nullable String raw; + public @Nullable String getRaw() { return raw; } + public void setRaw(@Nullable String setterArg) { + this.raw = setterArg; + } + + private @Nullable SnippetLanguageType type; + public @Nullable SnippetLanguageType getType() { return type; } + public void setType(@Nullable SnippetLanguageType setterArg) { + this.type = setterArg; + } + + public static final class Builder { + private @Nullable String raw; + public @NonNull Builder setRaw(@Nullable String setterArg) { + this.raw = setterArg; + return this; + } + private @Nullable SnippetLanguageType type; + public @NonNull Builder setType(@Nullable SnippetLanguageType setterArg) { + this.type = setterArg; + return this; + } + public @NonNull SnippetLanguage build() { + SnippetLanguage pigeonReturn = new SnippetLanguage(); + pigeonReturn.setRaw(raw); + pigeonReturn.setType(type); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("raw", raw); + toMapResult.put("type", type == null ? null : type.index); + return toMapResult; + } + static @NonNull SnippetLanguage fromMap(@NonNull Map map) { + SnippetLanguage pigeonResult = new SnippetLanguage(); + Object raw = map.get("raw"); + pigeonResult.setRaw((String)raw); + Object type = map.get("type"); + pigeonResult.setType(type == null ? null : SnippetLanguageType.values()[(int)type]); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class Owner { + private @Nullable Long id; + public @Nullable Long getId() { return id; } + public void setId(@Nullable Long setterArg) { + this.id = setterArg; + } + + private @Nullable String login; + public @Nullable String getLogin() { return login; } + public void setLogin(@Nullable String setterArg) { + this.login = setterArg; + } + + public static final class Builder { + private @Nullable Long id; + public @NonNull Builder setId(@Nullable Long setterArg) { + this.id = setterArg; + return this; + } + private @Nullable String login; + public @NonNull Builder setLogin(@Nullable String setterArg) { + this.login = setterArg; + return this; + } + public @NonNull Owner build() { + Owner pigeonReturn = new Owner(); + pigeonReturn.setId(id); + pigeonReturn.setLogin(login); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("id", id); + toMapResult.put("login", login); + return toMapResult; + } + static @NonNull Owner fromMap(@NonNull Map map) { + Owner pigeonResult = new Owner(); + Object id = map.get("id"); + pigeonResult.setId((id == null) ? null : ((id instanceof Integer) ? (Integer)id : (Long)id)); + Object login = map.get("login"); + pigeonResult.setLogin((String)login); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class SnippetFilter { + private @Nullable List languages; + public @Nullable List getLanguages() { return languages; } + public void setLanguages(@Nullable List setterArg) { + this.languages = setterArg; + } + + private @Nullable List selectedLanguages; + public @Nullable List getSelectedLanguages() { return selectedLanguages; } + public void setSelectedLanguages(@Nullable List setterArg) { + this.selectedLanguages = setterArg; + } + + private @Nullable List scopes; + public @Nullable List getScopes() { return scopes; } + public void setScopes(@Nullable List setterArg) { + this.scopes = setterArg; + } + + private @Nullable String selectedScope; + public @Nullable String getSelectedScope() { return selectedScope; } + public void setSelectedScope(@Nullable String setterArg) { + this.selectedScope = setterArg; + } + + public static final class Builder { + private @Nullable List languages; + public @NonNull Builder setLanguages(@Nullable List setterArg) { + this.languages = setterArg; + return this; + } + private @Nullable List selectedLanguages; + public @NonNull Builder setSelectedLanguages(@Nullable List setterArg) { + this.selectedLanguages = setterArg; + return this; + } + private @Nullable List scopes; + public @NonNull Builder setScopes(@Nullable List setterArg) { + this.scopes = setterArg; + return this; + } + private @Nullable String selectedScope; + public @NonNull Builder setSelectedScope(@Nullable String setterArg) { + this.selectedScope = setterArg; + return this; + } + public @NonNull SnippetFilter build() { + SnippetFilter pigeonReturn = new SnippetFilter(); + pigeonReturn.setLanguages(languages); + pigeonReturn.setSelectedLanguages(selectedLanguages); + pigeonReturn.setScopes(scopes); + pigeonReturn.setSelectedScope(selectedScope); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("languages", languages); + toMapResult.put("selectedLanguages", selectedLanguages); + toMapResult.put("scopes", scopes); + toMapResult.put("selectedScope", selectedScope); + return toMapResult; + } + static @NonNull SnippetFilter fromMap(@NonNull Map map) { + SnippetFilter pigeonResult = new SnippetFilter(); + Object languages = map.get("languages"); + pigeonResult.setLanguages((List)languages); + Object selectedLanguages = map.get("selectedLanguages"); + pigeonResult.setSelectedLanguages((List)selectedLanguages); + Object scopes = map.get("scopes"); + pigeonResult.setScopes((List)scopes); + Object selectedScope = map.get("selectedScope"); + pigeonResult.setSelectedScope((String)selectedScope); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class MainModelStateData { + private @Nullable ModelState state; + public @Nullable ModelState getState() { return state; } + public void setState(@Nullable ModelState setterArg) { + this.state = setterArg; + } + + private @Nullable Boolean is_loading; + public @Nullable Boolean getIs_loading() { return is_loading; } + public void setIs_loading(@Nullable Boolean setterArg) { + this.is_loading = setterArg; + } + + private @Nullable List data; + public @Nullable List getData() { return data; } + public void setData(@Nullable List setterArg) { + this.data = setterArg; + } + + private @Nullable SnippetFilter filter; + public @Nullable SnippetFilter getFilter() { return filter; } + public void setFilter(@Nullable SnippetFilter setterArg) { + this.filter = setterArg; + } + + private @Nullable String error; + public @Nullable String getError() { return error; } + public void setError(@Nullable String setterArg) { + this.error = setterArg; + } + + private @Nullable Long oldHash; + public @Nullable Long getOldHash() { return oldHash; } + public void setOldHash(@Nullable Long setterArg) { + this.oldHash = setterArg; + } + + private @Nullable Long newHash; + public @Nullable Long getNewHash() { return newHash; } + public void setNewHash(@Nullable Long setterArg) { + this.newHash = setterArg; + } + + public static final class Builder { + private @Nullable ModelState state; + public @NonNull Builder setState(@Nullable ModelState setterArg) { + this.state = setterArg; + return this; + } + private @Nullable Boolean is_loading; + public @NonNull Builder setIs_loading(@Nullable Boolean setterArg) { + this.is_loading = setterArg; + return this; + } + private @Nullable List data; + public @NonNull Builder setData(@Nullable List setterArg) { + this.data = setterArg; + return this; + } + private @Nullable SnippetFilter filter; + public @NonNull Builder setFilter(@Nullable SnippetFilter setterArg) { + this.filter = setterArg; + return this; + } + private @Nullable String error; + public @NonNull Builder setError(@Nullable String setterArg) { + this.error = setterArg; + return this; + } + private @Nullable Long oldHash; + public @NonNull Builder setOldHash(@Nullable Long setterArg) { + this.oldHash = setterArg; + return this; + } + private @Nullable Long newHash; + public @NonNull Builder setNewHash(@Nullable Long setterArg) { + this.newHash = setterArg; + return this; + } + public @NonNull MainModelStateData build() { + MainModelStateData pigeonReturn = new MainModelStateData(); + pigeonReturn.setState(state); + pigeonReturn.setIs_loading(is_loading); + pigeonReturn.setData(data); + pigeonReturn.setFilter(filter); + pigeonReturn.setError(error); + pigeonReturn.setOldHash(oldHash); + pigeonReturn.setNewHash(newHash); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("state", state == null ? null : state.index); + toMapResult.put("is_loading", is_loading); + toMapResult.put("data", data); + toMapResult.put("filter", (filter == null) ? null : filter.toMap()); + toMapResult.put("error", error); + toMapResult.put("oldHash", oldHash); + toMapResult.put("newHash", newHash); + return toMapResult; + } + static @NonNull MainModelStateData fromMap(@NonNull Map map) { + MainModelStateData pigeonResult = new MainModelStateData(); + Object state = map.get("state"); + pigeonResult.setState(state == null ? null : ModelState.values()[(int)state]); + Object is_loading = map.get("is_loading"); + pigeonResult.setIs_loading((Boolean)is_loading); + Object data = map.get("data"); + pigeonResult.setData((List)data); + Object filter = map.get("filter"); + pigeonResult.setFilter((filter == null) ? null : SnippetFilter.fromMap((Map)filter)); + Object error = map.get("error"); + pigeonResult.setError((String)error); + Object oldHash = map.get("oldHash"); + pigeonResult.setOldHash((oldHash == null) ? null : ((oldHash instanceof Integer) ? (Integer)oldHash : (Long)oldHash)); + Object newHash = map.get("newHash"); + pigeonResult.setNewHash((newHash == null) ? null : ((newHash instanceof Integer) ? (Integer)newHash : (Long)newHash)); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class MainModelEventData { + private @Nullable MainModelEvent event; + public @Nullable MainModelEvent getEvent() { return event; } + public void setEvent(@Nullable MainModelEvent setterArg) { + this.event = setterArg; + } + + private @Nullable String message; + public @Nullable String getMessage() { return message; } + public void setMessage(@Nullable String setterArg) { + this.message = setterArg; + } + + private @Nullable Long oldHash; + public @Nullable Long getOldHash() { return oldHash; } + public void setOldHash(@Nullable Long setterArg) { + this.oldHash = setterArg; + } + + private @Nullable Long newHash; + public @Nullable Long getNewHash() { return newHash; } + public void setNewHash(@Nullable Long setterArg) { + this.newHash = setterArg; + } + + public static final class Builder { + private @Nullable MainModelEvent event; + public @NonNull Builder setEvent(@Nullable MainModelEvent setterArg) { + this.event = setterArg; + return this; + } + private @Nullable String message; + public @NonNull Builder setMessage(@Nullable String setterArg) { + this.message = setterArg; + return this; + } + private @Nullable Long oldHash; + public @NonNull Builder setOldHash(@Nullable Long setterArg) { + this.oldHash = setterArg; + return this; + } + private @Nullable Long newHash; + public @NonNull Builder setNewHash(@Nullable Long setterArg) { + this.newHash = setterArg; + return this; + } + public @NonNull MainModelEventData build() { + MainModelEventData pigeonReturn = new MainModelEventData(); + pigeonReturn.setEvent(event); + pigeonReturn.setMessage(message); + pigeonReturn.setOldHash(oldHash); + pigeonReturn.setNewHash(newHash); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("event", event == null ? null : event.index); + toMapResult.put("message", message); + toMapResult.put("oldHash", oldHash); + toMapResult.put("newHash", newHash); + return toMapResult; + } + static @NonNull MainModelEventData fromMap(@NonNull Map map) { + MainModelEventData pigeonResult = new MainModelEventData(); + Object event = map.get("event"); + pigeonResult.setEvent(event == null ? null : MainModelEvent.values()[(int)event]); + Object message = map.get("message"); + pigeonResult.setMessage((String)message); + Object oldHash = map.get("oldHash"); + pigeonResult.setOldHash((oldHash == null) ? null : ((oldHash instanceof Integer) ? (Integer)oldHash : (Long)oldHash)); + Object newHash = map.get("newHash"); + pigeonResult.setNewHash((newHash == null) ? null : ((newHash instanceof Integer) ? (Integer)newHash : (Long)newHash)); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class DetailModelStateData { + private @Nullable ModelState state; + public @Nullable ModelState getState() { return state; } + public void setState(@Nullable ModelState setterArg) { + this.state = setterArg; + } + + private @Nullable Boolean is_loading; + public @Nullable Boolean getIs_loading() { return is_loading; } + public void setIs_loading(@Nullable Boolean setterArg) { + this.is_loading = setterArg; + } + + private @Nullable Snippet data; + public @Nullable Snippet getData() { return data; } + public void setData(@Nullable Snippet setterArg) { + this.data = setterArg; + } + + private @Nullable String error; + public @Nullable String getError() { return error; } + public void setError(@Nullable String setterArg) { + this.error = setterArg; + } + + private @Nullable Long oldHash; + public @Nullable Long getOldHash() { return oldHash; } + public void setOldHash(@Nullable Long setterArg) { + this.oldHash = setterArg; + } + + private @Nullable Long newHash; + public @Nullable Long getNewHash() { return newHash; } + public void setNewHash(@Nullable Long setterArg) { + this.newHash = setterArg; + } + + public static final class Builder { + private @Nullable ModelState state; + public @NonNull Builder setState(@Nullable ModelState setterArg) { + this.state = setterArg; + return this; + } + private @Nullable Boolean is_loading; + public @NonNull Builder setIs_loading(@Nullable Boolean setterArg) { + this.is_loading = setterArg; + return this; + } + private @Nullable Snippet data; + public @NonNull Builder setData(@Nullable Snippet setterArg) { + this.data = setterArg; + return this; + } + private @Nullable String error; + public @NonNull Builder setError(@Nullable String setterArg) { + this.error = setterArg; + return this; + } + private @Nullable Long oldHash; + public @NonNull Builder setOldHash(@Nullable Long setterArg) { + this.oldHash = setterArg; + return this; + } + private @Nullable Long newHash; + public @NonNull Builder setNewHash(@Nullable Long setterArg) { + this.newHash = setterArg; + return this; + } + public @NonNull DetailModelStateData build() { + DetailModelStateData pigeonReturn = new DetailModelStateData(); + pigeonReturn.setState(state); + pigeonReturn.setIs_loading(is_loading); + pigeonReturn.setData(data); + pigeonReturn.setError(error); + pigeonReturn.setOldHash(oldHash); + pigeonReturn.setNewHash(newHash); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("state", state == null ? null : state.index); + toMapResult.put("is_loading", is_loading); + toMapResult.put("data", (data == null) ? null : data.toMap()); + toMapResult.put("error", error); + toMapResult.put("oldHash", oldHash); + toMapResult.put("newHash", newHash); + return toMapResult; + } + static @NonNull DetailModelStateData fromMap(@NonNull Map map) { + DetailModelStateData pigeonResult = new DetailModelStateData(); + Object state = map.get("state"); + pigeonResult.setState(state == null ? null : ModelState.values()[(int)state]); + Object is_loading = map.get("is_loading"); + pigeonResult.setIs_loading((Boolean)is_loading); + Object data = map.get("data"); + pigeonResult.setData((data == null) ? null : Snippet.fromMap((Map)data)); + Object error = map.get("error"); + pigeonResult.setError((String)error); + Object oldHash = map.get("oldHash"); + pigeonResult.setOldHash((oldHash == null) ? null : ((oldHash instanceof Integer) ? (Integer)oldHash : (Long)oldHash)); + Object newHash = map.get("newHash"); + pigeonResult.setNewHash((newHash == null) ? null : ((newHash instanceof Integer) ? (Integer)newHash : (Long)newHash)); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class DetailModelEventData { + private @Nullable DetailModelEvent event; + public @Nullable DetailModelEvent getEvent() { return event; } + public void setEvent(@Nullable DetailModelEvent setterArg) { + this.event = setterArg; + } + + private @Nullable String value; + public @Nullable String getValue() { return value; } + public void setValue(@Nullable String setterArg) { + this.value = setterArg; + } + + private @Nullable Long oldHash; + public @Nullable Long getOldHash() { return oldHash; } + public void setOldHash(@Nullable Long setterArg) { + this.oldHash = setterArg; + } + + private @Nullable Long newHash; + public @Nullable Long getNewHash() { return newHash; } + public void setNewHash(@Nullable Long setterArg) { + this.newHash = setterArg; + } + + public static final class Builder { + private @Nullable DetailModelEvent event; + public @NonNull Builder setEvent(@Nullable DetailModelEvent setterArg) { + this.event = setterArg; + return this; + } + private @Nullable String value; + public @NonNull Builder setValue(@Nullable String setterArg) { + this.value = setterArg; + return this; + } + private @Nullable Long oldHash; + public @NonNull Builder setOldHash(@Nullable Long setterArg) { + this.oldHash = setterArg; + return this; + } + private @Nullable Long newHash; + public @NonNull Builder setNewHash(@Nullable Long setterArg) { + this.newHash = setterArg; + return this; + } + public @NonNull DetailModelEventData build() { + DetailModelEventData pigeonReturn = new DetailModelEventData(); + pigeonReturn.setEvent(event); + pigeonReturn.setValue(value); + pigeonReturn.setOldHash(oldHash); + pigeonReturn.setNewHash(newHash); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("event", event == null ? null : event.index); + toMapResult.put("value", value); + toMapResult.put("oldHash", oldHash); + toMapResult.put("newHash", newHash); + return toMapResult; + } + static @NonNull DetailModelEventData fromMap(@NonNull Map map) { + DetailModelEventData pigeonResult = new DetailModelEventData(); + Object event = map.get("event"); + pigeonResult.setEvent(event == null ? null : DetailModelEvent.values()[(int)event]); + Object value = map.get("value"); + pigeonResult.setValue((String)value); + Object oldHash = map.get("oldHash"); + pigeonResult.setOldHash((oldHash == null) ? null : ((oldHash instanceof Integer) ? (Integer)oldHash : (Long)oldHash)); + Object newHash = map.get("newHash"); + pigeonResult.setNewHash((newHash == null) ? null : ((newHash instanceof Integer) ? (Integer)newHash : (Long)newHash)); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class LoginModelStateData { + private @Nullable ModelState state; + public @Nullable ModelState getState() { return state; } + public void setState(@Nullable ModelState setterArg) { + this.state = setterArg; + } + + private @Nullable Boolean is_loading; + public @Nullable Boolean getIs_loading() { return is_loading; } + public void setIs_loading(@Nullable Boolean setterArg) { + this.is_loading = setterArg; + } + + private @Nullable Long oldHash; + public @Nullable Long getOldHash() { return oldHash; } + public void setOldHash(@Nullable Long setterArg) { + this.oldHash = setterArg; + } + + private @Nullable Long newHash; + public @Nullable Long getNewHash() { return newHash; } + public void setNewHash(@Nullable Long setterArg) { + this.newHash = setterArg; + } + + public static final class Builder { + private @Nullable ModelState state; + public @NonNull Builder setState(@Nullable ModelState setterArg) { + this.state = setterArg; + return this; + } + private @Nullable Boolean is_loading; + public @NonNull Builder setIs_loading(@Nullable Boolean setterArg) { + this.is_loading = setterArg; + return this; + } + private @Nullable Long oldHash; + public @NonNull Builder setOldHash(@Nullable Long setterArg) { + this.oldHash = setterArg; + return this; + } + private @Nullable Long newHash; + public @NonNull Builder setNewHash(@Nullable Long setterArg) { + this.newHash = setterArg; + return this; + } + public @NonNull LoginModelStateData build() { + LoginModelStateData pigeonReturn = new LoginModelStateData(); + pigeonReturn.setState(state); + pigeonReturn.setIs_loading(is_loading); + pigeonReturn.setOldHash(oldHash); + pigeonReturn.setNewHash(newHash); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("state", state == null ? null : state.index); + toMapResult.put("is_loading", is_loading); + toMapResult.put("oldHash", oldHash); + toMapResult.put("newHash", newHash); + return toMapResult; + } + static @NonNull LoginModelStateData fromMap(@NonNull Map map) { + LoginModelStateData pigeonResult = new LoginModelStateData(); + Object state = map.get("state"); + pigeonResult.setState(state == null ? null : ModelState.values()[(int)state]); + Object is_loading = map.get("is_loading"); + pigeonResult.setIs_loading((Boolean)is_loading); + Object oldHash = map.get("oldHash"); + pigeonResult.setOldHash((oldHash == null) ? null : ((oldHash instanceof Integer) ? (Integer)oldHash : (Long)oldHash)); + Object newHash = map.get("newHash"); + pigeonResult.setNewHash((newHash == null) ? null : ((newHash instanceof Integer) ? (Integer)newHash : (Long)newHash)); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class LoginModelEventData { + private @Nullable LoginModelEvent event; + public @Nullable LoginModelEvent getEvent() { return event; } + public void setEvent(@Nullable LoginModelEvent setterArg) { + this.event = setterArg; + } + + private @Nullable Long oldHash; + public @Nullable Long getOldHash() { return oldHash; } + public void setOldHash(@Nullable Long setterArg) { + this.oldHash = setterArg; + } + + private @Nullable Long newHash; + public @Nullable Long getNewHash() { return newHash; } + public void setNewHash(@Nullable Long setterArg) { + this.newHash = setterArg; + } + + public static final class Builder { + private @Nullable LoginModelEvent event; + public @NonNull Builder setEvent(@Nullable LoginModelEvent setterArg) { + this.event = setterArg; + return this; + } + private @Nullable Long oldHash; + public @NonNull Builder setOldHash(@Nullable Long setterArg) { + this.oldHash = setterArg; + return this; + } + private @Nullable Long newHash; + public @NonNull Builder setNewHash(@Nullable Long setterArg) { + this.newHash = setterArg; + return this; + } + public @NonNull LoginModelEventData build() { + LoginModelEventData pigeonReturn = new LoginModelEventData(); + pigeonReturn.setEvent(event); + pigeonReturn.setOldHash(oldHash); + pigeonReturn.setNewHash(newHash); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("event", event == null ? null : event.index); + toMapResult.put("oldHash", oldHash); + toMapResult.put("newHash", newHash); + return toMapResult; + } + static @NonNull LoginModelEventData fromMap(@NonNull Map map) { + LoginModelEventData pigeonResult = new LoginModelEventData(); + Object event = map.get("event"); + pigeonResult.setEvent(event == null ? null : LoginModelEvent.values()[(int)event]); + Object oldHash = map.get("oldHash"); + pigeonResult.setOldHash((oldHash == null) ? null : ((oldHash instanceof Integer) ? (Integer)oldHash : (Long)oldHash)); + Object newHash = map.get("newHash"); + pigeonResult.setNewHash((newHash == null) ? null : ((newHash instanceof Integer) ? (Integer)newHash : (Long)newHash)); + return pigeonResult; + } + } + private static class MainModelBridgeCodec extends StandardMessageCodec { + public static final MainModelBridgeCodec INSTANCE = new MainModelBridgeCodec(); + private MainModelBridgeCodec() {} + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte)128: + return MainModelEventData.fromMap((Map) readValue(buffer)); + + case (byte)129: + return MainModelStateData.fromMap((Map) readValue(buffer)); + + case (byte)130: + return Owner.fromMap((Map) readValue(buffer)); + + case (byte)131: + return Snippet.fromMap((Map) readValue(buffer)); + + case (byte)132: + return SnippetCode.fromMap((Map) readValue(buffer)); + + case (byte)133: + return SnippetFilter.fromMap((Map) readValue(buffer)); + + case (byte)134: + return SnippetLanguage.fromMap((Map) readValue(buffer)); + + case (byte)135: + return SyntaxToken.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + + } + } + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof MainModelEventData) { + stream.write(128); + writeValue(stream, ((MainModelEventData) value).toMap()); + } else + if (value instanceof MainModelStateData) { + stream.write(129); + writeValue(stream, ((MainModelStateData) value).toMap()); + } else + if (value instanceof Owner) { + stream.write(130); + writeValue(stream, ((Owner) value).toMap()); + } else + if (value instanceof Snippet) { + stream.write(131); + writeValue(stream, ((Snippet) value).toMap()); + } else + if (value instanceof SnippetCode) { + stream.write(132); + writeValue(stream, ((SnippetCode) value).toMap()); + } else + if (value instanceof SnippetFilter) { + stream.write(133); + writeValue(stream, ((SnippetFilter) value).toMap()); + } else + if (value instanceof SnippetLanguage) { + stream.write(134); + writeValue(stream, ((SnippetLanguage) value).toMap()); + } else + if (value instanceof SyntaxToken) { + stream.write(135); + writeValue(stream, ((SyntaxToken) value).toMap()); + } else +{ + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface MainModelBridge { + @NonNull MainModelStateData getState(); + @NonNull MainModelEventData getEvent(); + void resetEvent(); + void initState(); + void filterLanguage(@NonNull String language, @NonNull Boolean isSelected); + void filterScope(@NonNull String scope); + void logOut(); + + /** The codec used by MainModelBridge. */ + static MessageCodec getCodec() { + return MainModelBridgeCodec.INSTANCE; } + /**Sets up an instance of `MainModelBridge` to handle messages through the `binaryMessenger`. */ + static void setup(BinaryMessenger binaryMessenger, MainModelBridge api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.getState", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + MainModelStateData output = api.getState(); + wrapped.put("result", output); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.getEvent", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + MainModelEventData output = api.getEvent(); + wrapped.put("result", output); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.resetEvent", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.resetEvent(); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.initState", getCodec(), taskQueue); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.initState(); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.filterLanguage", getCodec(), taskQueue); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + assert args != null; + String languageArg = (String)args.get(0); + if (languageArg == null) { + throw new NullPointerException("languageArg unexpectedly null."); + } + Boolean isSelectedArg = (Boolean)args.get(1); + if (isSelectedArg == null) { + throw new NullPointerException("isSelectedArg unexpectedly null."); + } + api.filterLanguage(languageArg, isSelectedArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.filterScope", getCodec(), taskQueue); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + assert args != null; + String scopeArg = (String)args.get(0); + if (scopeArg == null) { + throw new NullPointerException("scopeArg unexpectedly null."); + } + api.filterScope(scopeArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.logOut", getCodec(), taskQueue); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.logOut(); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static class DetailModelBridgeCodec extends StandardMessageCodec { + public static final DetailModelBridgeCodec INSTANCE = new DetailModelBridgeCodec(); + private DetailModelBridgeCodec() {} + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte)128: + return DetailModelEventData.fromMap((Map) readValue(buffer)); + + case (byte)129: + return DetailModelStateData.fromMap((Map) readValue(buffer)); + + case (byte)130: + return Owner.fromMap((Map) readValue(buffer)); + + case (byte)131: + return Snippet.fromMap((Map) readValue(buffer)); + + case (byte)132: + return SnippetCode.fromMap((Map) readValue(buffer)); + + case (byte)133: + return SnippetLanguage.fromMap((Map) readValue(buffer)); + + case (byte)134: + return SyntaxToken.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + + } + } + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof DetailModelEventData) { + stream.write(128); + writeValue(stream, ((DetailModelEventData) value).toMap()); + } else + if (value instanceof DetailModelStateData) { + stream.write(129); + writeValue(stream, ((DetailModelStateData) value).toMap()); + } else + if (value instanceof Owner) { + stream.write(130); + writeValue(stream, ((Owner) value).toMap()); + } else + if (value instanceof Snippet) { + stream.write(131); + writeValue(stream, ((Snippet) value).toMap()); + } else + if (value instanceof SnippetCode) { + stream.write(132); + writeValue(stream, ((SnippetCode) value).toMap()); + } else + if (value instanceof SnippetLanguage) { + stream.write(133); + writeValue(stream, ((SnippetLanguage) value).toMap()); + } else + if (value instanceof SyntaxToken) { + stream.write(134); + writeValue(stream, ((SyntaxToken) value).toMap()); + } else +{ + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface DetailModelBridge { + @NonNull DetailModelStateData getState(); + @NonNull DetailModelEventData getEvent(); + void resetEvent(); + void load(@NonNull String uuid); + void like(); + void dislike(); + void save(); + void copyToClipboard(); + void share(); + void delete(); + + /** The codec used by DetailModelBridge. */ + static MessageCodec getCodec() { + return DetailModelBridgeCodec.INSTANCE; } + /**Sets up an instance of `DetailModelBridge` to handle messages through the `binaryMessenger`. */ + static void setup(BinaryMessenger binaryMessenger, DetailModelBridge api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.getState", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + DetailModelStateData output = api.getState(); + wrapped.put("result", output); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.getEvent", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + DetailModelEventData output = api.getEvent(); + wrapped.put("result", output); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.resetEvent", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.resetEvent(); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.load", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + assert args != null; + String uuidArg = (String)args.get(0); + if (uuidArg == null) { + throw new NullPointerException("uuidArg unexpectedly null."); + } + api.load(uuidArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.like", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.like(); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.dislike", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.dislike(); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.save", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.save(); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.copyToClipboard", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.copyToClipboard(); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.share", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.share(); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.delete", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.delete(); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static class LoginModelBridgeCodec extends StandardMessageCodec { + public static final LoginModelBridgeCodec INSTANCE = new LoginModelBridgeCodec(); + private LoginModelBridgeCodec() {} + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte)128: + return LoginModelEventData.fromMap((Map) readValue(buffer)); + + case (byte)129: + return LoginModelStateData.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + + } + } + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof LoginModelEventData) { + stream.write(128); + writeValue(stream, ((LoginModelEventData) value).toMap()); + } else + if (value instanceof LoginModelStateData) { + stream.write(129); + writeValue(stream, ((LoginModelStateData) value).toMap()); + } else +{ + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface LoginModelBridge { + @NonNull LoginModelStateData getState(); + @NonNull LoginModelEventData getEvent(); + void loginOrRegister(@NonNull String email, @NonNull String password); + void checkLoginState(); + void resetEvent(); + + /** The codec used by LoginModelBridge. */ + static MessageCodec getCodec() { + return LoginModelBridgeCodec.INSTANCE; } + /**Sets up an instance of `LoginModelBridge` to handle messages through the `binaryMessenger`. */ + static void setup(BinaryMessenger binaryMessenger, LoginModelBridge api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.LoginModelBridge.getState", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + LoginModelStateData output = api.getState(); + wrapped.put("result", output); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.LoginModelBridge.getEvent", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + LoginModelEventData output = api.getEvent(); + wrapped.put("result", output); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.LoginModelBridge.loginOrRegister", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + assert args != null; + String emailArg = (String)args.get(0); + if (emailArg == null) { + throw new NullPointerException("emailArg unexpectedly null."); + } + String passwordArg = (String)args.get(1); + if (passwordArg == null) { + throw new NullPointerException("passwordArg unexpectedly null."); + } + api.loginOrRegister(emailArg, passwordArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.LoginModelBridge.checkLoginState", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.checkLoginState(); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.LoginModelBridge.resetEvent", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.resetEvent(); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + @NonNull private static Map wrapError(@NonNull Throwable exception) { + Map errorMap = new HashMap<>(); + errorMap.put("message", exception.toString()); + errorMap.put("code", exception.getClass().getSimpleName()); + errorMap.put("details", "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + return errorMap; + } +} diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/ModelPlugin.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/ModelPlugin.kt new file mode 100644 index 0000000..965f687 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/ModelPlugin.kt @@ -0,0 +1,106 @@ +package dev.snipme.snipmeapp.bridge + +import android.text.Spanned +import android.text.format.DateUtils +import android.text.style.ForegroundColorSpan +import androidx.core.text.getSpans +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.BinaryMessenger +import org.koin.core.component.KoinComponent +import dev.snipme.snipmeapp.domain.reaction.UserReaction +import dev.snipme.snipmeapp.domain.snippets.* +import java.util.* + +/* + flutter pub run pigeon \ + --input bridge/main_model.dart \ + --dart_out lib/model/main_model.dart \ + --java_out ../app/src/main/java/pl/tkadziolka/snipmeandroid/bridge/Bridge.java \ + --java_package "dev.snipme.snipmeapp.bridge" + */ + +abstract class ModelPlugin : FlutterPlugin, KoinComponent { + + abstract fun onSetup(messenger: BinaryMessenger, bridge: T?) + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + onSetup(binding.binaryMessenger, this as T) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + onSetup(binding.binaryMessenger, null) + } +} + +fun Snippet.toModelData(): Bridge.Snippet { + val it = this + return Bridge.Snippet().apply { + uuid = it.uuid + title = it.title + code = it.code.toModelSnippetCode() + language = it.language.toModelSnippetLanguage() + owner = it.owner.toModelOwner() + isOwner = it.isOwner + voteResult = (it.numberOfLikes - it.numberOfDislikes).toLong() + userReaction = it.userReaction.toModelUserReaction() + isLiked = it.userReaction.toModelReactionState(UserReaction.LIKE) + isDisliked = it.userReaction.toModelReactionState(UserReaction.DISLIKE) + isPrivate = it.visibility == SnippetVisibility.PRIVATE + isSaved = calculateSavedState(it.isOwner, it.visibility) + isToDelete = it.isOwner + timeAgo = DateUtils.getRelativeTimeSpanString( + it.modifiedAt.time, + Date().time, + DateUtils.SECOND_IN_MILLIS + ).toString() + } +} + +private fun Owner.toModelOwner() = + Bridge.Owner().let { + it.id = id.toLong() + it.login = login + it + } + +private fun SnippetCode.toModelSnippetCode() = + Bridge.SnippetCode().let { + it.raw = raw + it.tokens = highlighted.getSpans().map { span -> + span.toSyntaxToken(highlighted) + } + it + } + +private fun SnippetLanguage.toModelSnippetLanguage() = + Bridge.SnippetLanguage().let { + it.raw = raw + it.type = Bridge.SnippetLanguageType.valueOf(type.name) + it + } + +private fun UserReaction.toModelUserReaction(): Bridge.UserReaction = + when (this) { + UserReaction.LIKE -> Bridge.UserReaction.LIKE + UserReaction.DISLIKE -> Bridge.UserReaction.DISLIKE + else -> Bridge.UserReaction.NONE + } + +private fun UserReaction.toModelReactionState(reaction: UserReaction) = + if (this == UserReaction.NONE) null else this == reaction + +private fun calculateSavedState( + isOwner: Boolean, + visibility: SnippetVisibility +): Boolean? { + if (isOwner.not()) return null + return visibility == SnippetVisibility.PRIVATE +} + +private fun ForegroundColorSpan.toSyntaxToken(spannable: Spanned) = + Bridge.SyntaxToken().let { + it.start = spannable.getSpanStart(this).toLong() + it.end = spannable.getSpanEnd(this).toLong() + it.color = foregroundColor.toLong() + it + } diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/detail/DetailModel.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/detail/DetailModel.kt new file mode 100644 index 0000000..1f479cc --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/detail/DetailModel.kt @@ -0,0 +1,164 @@ +package dev.snipme.snipmeapp.bridge.detail + +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.rxkotlin.subscribeBy +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.flow.MutableStateFlow +import dev.snipme.snipmeapp.bridge.session.SessionModel +import dev.snipme.snipmeapp.domain.clipboard.AddToClipboardUseCase +import dev.snipme.snipmeapp.domain.error.exception.* +import dev.snipme.snipmeapp.domain.message.ErrorMessages +import dev.snipme.snipmeapp.domain.reaction.GetTargetUserReactionUseCase +import dev.snipme.snipmeapp.domain.reaction.SetUserReactionUseCase +import dev.snipme.snipmeapp.domain.reaction.UserReaction +import dev.snipme.snipmeapp.domain.share.ShareSnippetCodeUseCase +import dev.snipme.snipmeapp.domain.snippet.DeleteSnippetUseCase +import dev.snipme.snipmeapp.domain.snippet.GetSingleSnippetUseCase +import dev.snipme.snipmeapp.domain.snippet.SaveSnippetUseCase +import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.bridge.error.ErrorParsable +import timber.log.Timber + +class DetailModel( + private val errorMessages: ErrorMessages, + private val getSnippet: GetSingleSnippetUseCase, + private val clipboard: AddToClipboardUseCase, + private val getTargetReaction: GetTargetUserReactionUseCase, + private val setUserReaction: SetUserReactionUseCase, + private val saveSnippet: SaveSnippetUseCase, + private val shareSnippet: ShareSnippetCodeUseCase, + private val deleteSnippet: DeleteSnippetUseCase, + private val session: SessionModel +) : ErrorParsable { + private val disposables = CompositeDisposable() + + private val mutableState = MutableStateFlow(Loading) + val state = mutableState + + private val mutableEvent = MutableStateFlow(Idle) + val event = mutableEvent + + override fun parseError(throwable: Throwable) { + when (throwable) { + is ConnectionException -> setState(Error(errorMessages.parse(throwable))) + is ContentNotFoundException -> setState(Error(errorMessages.parse(throwable))) + is ForbiddenActionException -> setState(Error(errorMessages.parse(throwable))) + is NetworkNotAvailableException -> setState(Error(errorMessages.parse(throwable))) + is NotAuthorizedException -> session.logOut { mutableEvent.value = Logout } + is RemoteException -> setState(Error(errorMessages.parse(throwable))) + is SessionExpiredException -> session.logOut { mutableEvent.value = Logout } + else -> setState(Error(errorMessages.parse(throwable))) + } + } + + fun load(uuid: String) { + setState(Loading) + getSnippet(uuid) + .subscribeOn(Schedulers.io()) + .subscribeBy( + onSuccess = { setState(Loaded(it)) }, + onError = { + Timber.e("Couldn't load snippets, error = $it") + parseError(it) + } + ).also { disposables += it } + } + + fun like() { + changeReaction(UserReaction.LIKE) + } + + fun dislike() { + changeReaction(UserReaction.DISLIKE) + } + + fun copyToClipboard() { + getSnippet()?.let { + clipboard(it.title, it.code.raw) + } + } + + fun save() { + getSnippet()?.let { + setState(Loading) + saveSnippet(it) + .subscribeOn(Schedulers.io()) + .subscribeBy( + onSuccess = { saved -> + setState(Loaded(it)) + mutableEvent.value = Saved(saved.uuid) + }, + onError = { error -> + Timber.e("Couldn't save snippet, error = $error") + parseError(error) + } + ).also { disposables += it } + } + } + + fun share() { + getSnippet()?.let { + shareSnippet(it) + } + } + + fun delete() { + getSnippet()?.let { + setState(Loading) + deleteSnippet(it.uuid) + .subscribeOn(Schedulers.io()) + .subscribeBy( + onComplete = { mutableEvent.value = Deleted }, + onError = { error -> + Timber.e("Couldn't delete snippet, error = $error") + parseError(error) + } + ).also { disposables += it } + } + } + + private fun changeReaction(newReaction: UserReaction) { + // Immediately show change to user + val previousState = getLoaded() ?: return + val targetReaction = getTargetReaction(previousState.snippet, newReaction) + setState(previousState.run { copy(snippet = snippet.copy(userReaction = targetReaction)) }) + + setUserReaction(previousState.snippet, newReaction) + .subscribeOn(Schedulers.io()) + .subscribeBy( + onSuccess = { snippet -> mutableState.value = Loaded(snippet) }, + onError = { + // Revert changes + Timber.e("Couldn't change user reaction, error = $it") + mutableEvent.value = Alert(errorMessages.generic) + setState(previousState) + } + ).also { disposables += it } + } + + private fun getSnippet(): Snippet? = getLoaded()?.snippet + + private fun getLoaded() = + if (state.value is Loaded) { + (state.value as Loaded) + } else { + null + } + + private fun setState(newState: DetailViewState?) { + newState?.let { mutableState.value = it } + } +} + +sealed class DetailViewState +object Loading : DetailViewState() +data class Loaded(val snippet: Snippet) : DetailViewState() +data class Error(val error: String?) : DetailViewState() + +sealed class DetailEvent +object Idle : DetailEvent() +object Deleted : DetailEvent() +data class Alert(val message: String) : DetailEvent() +data class Saved(val snippetId: String) : DetailEvent() +object Logout : DetailEvent() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/detail/DetailModelPlugin.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/detail/DetailModelPlugin.kt new file mode 100644 index 0000000..c7f04e0 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/detail/DetailModelPlugin.kt @@ -0,0 +1,90 @@ +package dev.snipme.snipmeapp.bridge.detail + +import io.flutter.plugin.common.BinaryMessenger +import org.koin.core.component.inject +import dev.snipme.snipmeapp.bridge.Bridge +import dev.snipme.snipmeapp.bridge.ModelPlugin +import dev.snipme.snipmeapp.bridge.toModelData + +class DetailModelPlugin : ModelPlugin(), Bridge.DetailModelBridge { + private val model: DetailModel by inject() + private var oldEvent: DetailEvent? = null + private var oldState: DetailViewState? = null + + override fun getState(): Bridge.DetailModelStateData = getData(model.state.value) + + override fun getEvent(): Bridge.DetailModelEventData = getEvent(model.event.value) + + override fun resetEvent() { + model.event.value = Idle + } + + override fun onSetup(messenger: BinaryMessenger, bridge: Bridge.DetailModelBridge?) { + Bridge.DetailModelBridge.setup(messenger, bridge) + } + + override fun load(uuid: String) { + model.load(uuid) + } + + override fun like() { + model.like() + } + + override fun dislike() { + model.dislike() + } + + override fun save() { + model.save() + } + + override fun copyToClipboard() { + model.copyToClipboard() + } + + override fun share() { + model.share() + } + + override fun delete() { + model.delete() + } + + private fun getData(viewState: DetailViewState): Bridge.DetailModelStateData { + return Bridge.DetailModelStateData().apply { + state = viewState.toModelState() + is_loading = viewState is Loading + data = (viewState as? Loaded)?.snippet?.toModelData() + oldHash = oldState?.hashCode()?.toLong() + newHash = viewState.hashCode().toLong() + }.also { + oldState = viewState + } + } + + private fun getEvent(detailEvent: DetailEvent): Bridge.DetailModelEventData { + return Bridge.DetailModelEventData().apply { + event = detailEvent.toModelEvent() + value = (detailEvent as? Saved)?.snippetId + oldHash = oldEvent?.hashCode()?.toLong() + newHash = detailEvent.hashCode().toLong() + }.also { + oldEvent = detailEvent + } + } + + private fun DetailViewState.toModelState() = + when (this) { + Loading -> Bridge.ModelState.LOADING + is Loaded -> Bridge.ModelState.LOADED + else -> Bridge.ModelState.ERROR + } + + private fun DetailEvent.toModelEvent() = + when (this) { + is Saved -> Bridge.DetailModelEvent.SAVED + is Deleted -> Bridge.DetailModelEvent.DELETED + else -> Bridge.DetailModelEvent.NONE + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/error/ErrorParsable.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/error/ErrorParsable.kt new file mode 100644 index 0000000..ee059b1 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/error/ErrorParsable.kt @@ -0,0 +1,5 @@ +package dev.snipme.snipmeapp.bridge.error + +interface ErrorParsable { + fun parseError(throwable: Throwable) +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/login/LoginModel.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/login/LoginModel.kt new file mode 100644 index 0000000..72e3d04 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/login/LoginModel.kt @@ -0,0 +1,133 @@ +package dev.snipme.snipmeapp.bridge.login + +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.rxkotlin.subscribeBy +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.flow.MutableStateFlow +import dev.snipme.snipmeapp.domain.auth.InitialLoginUseCase +import dev.snipme.snipmeapp.domain.auth.LoginInteractor +import dev.snipme.snipmeapp.domain.error.exception.* +import dev.snipme.snipmeapp.domain.message.ErrorMessages +import dev.snipme.snipmeapp.bridge.error.ErrorParsable +import dev.snipme.snipmeapp.util.extension.inProgress +import timber.log.Timber +import java.util.concurrent.TimeUnit + +class LoginModel( + private val errorMessages: ErrorMessages, + private val interactor: LoginInteractor, + private val initialLogin: InitialLoginUseCase, +) : ErrorParsable { + private val disposables = CompositeDisposable() + private var identifyDisposable: Disposable? = null + private var loginDisposable: Disposable? = null + private var registerDisposable: Disposable? = null + + private val mutableState = MutableStateFlow(Loading) + val state = mutableState + + private val mutableEvent = MutableStateFlow(Idle) + val event = mutableEvent + + override fun parseError(throwable: Throwable) { + when (throwable) { + is ConnectionException -> setEvent(Error(errorMessages.parse(throwable))) + is ContentNotFoundException -> setEvent(Error(errorMessages.parse(throwable))) + is ForbiddenActionException -> setEvent(Error(errorMessages.alreadyRegistered)) + is NetworkNotAvailableException -> setEvent(Error(errorMessages.parse(throwable))) + is NotAuthorizedException -> setEvent(Error(errorMessages.parse(throwable))) + is RemoteException -> setEvent(Error(errorMessages.parse(throwable))) + is SessionExpiredException -> setEvent(Error(errorMessages.parse(throwable))) + else -> setEvent(Error(errorMessages.parse(throwable))) + } + } + + fun init() { + initialLogin() + .delay(3, TimeUnit.SECONDS) + .subscribeOn(Schedulers.io()) + .doOnEvent { setState(Loaded) } + .subscribeBy( + onComplete = { setEvent(Logged) }, + onError = { + if (it !is NotAuthorizedException) { + Timber.e("Couldn't get token or user, error = $it") + } + } + ).also { disposables += it } + } + + fun loginOrRegister(email: String, password: String) { + if (identifyDisposable.inProgress()) return + + setState(Loading) + + identifyDisposable = interactor.identify(email) + .subscribeOn(Schedulers.io()) + .subscribeBy( + onSuccess = { identified -> publishIdentified(email, password, identified) }, + onError = { + Timber.d("Couldn't identify user = $email, error = $it") + parseError(it) + } + ).also { disposables += it } + } + + fun login(email: String, password: String) { + if (loginDisposable.inProgress()) return + + loginDisposable = interactor.login(email, password) + .subscribeOn(Schedulers.io()) + .doOnEvent { setState(Loaded) } + .subscribeBy( + onComplete = { setEvent(Logged) }, + onError = { + Timber.d("Couldn't login user = $email, error = $it") + parseError(it) + } + ).also { disposables += it } + } + + fun register(email: String, password: String) { + if (registerDisposable.inProgress()) return + + registerDisposable = interactor.register(email, password, email) + .subscribeOn(Schedulers.io()) + .doOnEvent { setState(Loaded) } + .subscribeBy( + onComplete = { setEvent(Logged) }, + onError = { + Timber.d("Couldn't register user = $email, error = $it") + parseError(it) + } + ).also { disposables += it } + } + + private fun publishIdentified(email: String, password: String, identified: Boolean) { + if (identified) { + login(email, password) + } else { + register(email, password) + } + } + + private fun setEvent(event: LoginEvent) { + mutableEvent.value = event + } + + private fun setState(state: LoginState) { + mutableState.value = state + } +} + + +sealed class LoginState +object Loading : LoginState() +object Loaded : LoginState() + +sealed class LoginEvent +object Idle : LoginEvent() +object Logged : LoginEvent() +data class Error(val message: String?) : LoginEvent() diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/login/LoginModelPlugin.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/login/LoginModelPlugin.kt new file mode 100644 index 0000000..040a39f --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/login/LoginModelPlugin.kt @@ -0,0 +1,64 @@ +package dev.snipme.snipmeapp.bridge.login + +import io.flutter.plugin.common.BinaryMessenger +import org.koin.core.component.inject +import dev.snipme.snipmeapp.bridge.Bridge +import dev.snipme.snipmeapp.bridge.ModelPlugin + +class LoginModelPlugin : ModelPlugin(), Bridge.LoginModelBridge { + private var oldEvent: LoginEvent? = null + private var oldState: LoginState? = null + private val model: LoginModel by inject() + + override fun getState(): Bridge.LoginModelStateData = getModelState(model.state.value) + + override fun getEvent(): Bridge.LoginModelEventData = getModelEvent(model.event.value) + + override fun resetEvent() { + model.event.value = Idle + } + + override fun onSetup(messenger: BinaryMessenger, bridge: Bridge.LoginModelBridge?) { + Bridge.LoginModelBridge.setup(messenger, bridge) + } + + override fun checkLoginState() { + model.init() + } + + override fun loginOrRegister(email: String, password: String) { + model.loginOrRegister(email, password) + } + + private fun getModelEvent(loginEvent: LoginEvent): Bridge.LoginModelEventData { + return Bridge.LoginModelEventData().apply { + event = loginEvent.toModelLoginEvent() + oldHash = oldEvent?.hashCode()?.toLong() ?: 0 + newHash = loginEvent.hashCode().toLong() + }.also { + oldEvent = loginEvent + } + } + + private fun getModelState(loginState: LoginState): Bridge.LoginModelStateData { + return Bridge.LoginModelStateData().apply { + state = loginState.toModelLoginState() + oldHash = oldState?.hashCode()?.toLong() ?: 0 + newHash = loginState.hashCode().toLong() + }.also { + oldState = loginState + } + } + + private fun LoginState.toModelLoginState() = + when (this) { + Loaded -> Bridge.ModelState.LOADED + else -> Bridge.ModelState.LOADING + } + + private fun LoginEvent.toModelLoginEvent() = + when (this) { + Logged -> Bridge.LoginModelEvent.LOGGED + else -> Bridge.LoginModelEvent.NONE + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModel.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModel.kt new file mode 100644 index 0000000..6be39e8 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModel.kt @@ -0,0 +1,178 @@ +package dev.snipme.snipmeapp.bridge.main + +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.rxkotlin.subscribeBy +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.flow.MutableStateFlow +import dev.snipme.snipmeapp.bridge.session.SessionModel +import dev.snipme.snipmeapp.domain.error.exception.* +import dev.snipme.snipmeapp.domain.filter.* +import dev.snipme.snipmeapp.domain.message.ErrorMessages +import dev.snipme.snipmeapp.domain.snippet.ObserveSnippetUpdatesUseCase +import dev.snipme.snipmeapp.domain.snippets.* +import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase +import dev.snipme.snipmeapp.domain.user.User +import dev.snipme.snipmeapp.bridge.error.ErrorParsable +import timber.log.Timber + +private const val ONE_PAGE = 1 + +class MainModel( + private val errorMessages: ErrorMessages, + private val getUser: GetSingleUserUseCase, + private val getSnippets: GetSnippetsUseCase, + private val observeUpdates: ObserveSnippetUpdatesUseCase, + private val hasMore: HasMoreSnippetPagesUseCase, + private val getLanguageFilters: GetLanguageFiltersUseCase, + private val filterSnippetsByLanguage: FilterSnippetsByLanguageUseCase, + private val filterSnippetsByScope: FilterSnippetsByScopeUseCase, + private val updateFilterLanguage: UpdateSnippetFiltersLanguageUseCase, + private val session: SessionModel +) : ErrorParsable { + private val disposables = CompositeDisposable() + + private val mutableEvent = MutableStateFlow(Startup) + val event = mutableEvent + + private val mutableState = MutableStateFlow(Loading) + val state = mutableState + + private var cachedSnippets = emptyList() + private var scopedSnippets = emptyList() + private lateinit var filterState: SnippetFilters + + override fun parseError(throwable: Throwable) { + when (throwable) { + is ConnectionException -> mutableState.value = Error(errorMessages.parse(throwable)) + is ContentNotFoundException -> mutableState.value = + Error(errorMessages.parse(throwable)) + is ForbiddenActionException -> mutableState.value = + Error(errorMessages.parse(throwable)) + is NetworkNotAvailableException -> mutableState.value = + Error(errorMessages.parse(throwable)) + is NotAuthorizedException -> session.logOut { mutableEvent.value = Logout } + is RemoteException -> mutableState.value = Error(errorMessages.parse(throwable)) + is SessionExpiredException -> session.logOut { mutableEvent.value = Logout } + else -> mutableState.value = Error(errorMessages.parse(throwable)) + } + } + + init { + observeUpdates() + .subscribeOn(Schedulers.io()) + .subscribeBy( + onNext = { initState() }, + onError = { Timber.e("Couldn't refresh snippet updates, error = $it") } + ).also { disposables += it } + } + + fun initState() { + mutableState.value = Loading + filterState = SnippetFilters( + languages = listOf(SNIPPET_FILTER_ALL), + selectedLanguages = listOf(SNIPPET_FILTER_ALL), + scopes = listOf("All", "Private", "Public"), + selectedScope = "All" + ) + getUser() + .subscribeOn(Schedulers.io()) + .subscribeBy( + onSuccess = { user -> loadSnippets(user) }, + onError = { + Timber.e("Couldn't load user, error = $it") + parseError(it) + } + ).also { disposables += it } + } + + fun filterLanguage(language: String, isSelected: Boolean) { + getLoadedState()?.let { + filterState = updateFilterLanguage(filterState, language, isSelected) + val filteredSnippets = + filterSnippetsByLanguage(scopedSnippets, filterState.selectedLanguages) + state.value = it.copy(snippets = filteredSnippets, filters = filterState) + } + } + + fun filterScope(scope: String) { + getLoadedState()?.let { + filterState = filterState.copy(selectedScope = scope) + scopedSnippets = filterSnippetsByScope(cachedSnippets, scope) + val updatedFilters = getLanguageFilters(scopedSnippets) + filterState = filterState.copy( + languages = updatedFilters, + selectedLanguages = listOf(SNIPPET_FILTER_ALL), + ) + state.value = it.copy(snippets = scopedSnippets, filters = filterState) + } + } + + fun logOut() { + session.logOut { mutableEvent.value = Logout } + } + + private fun loadNextPage() { + getLoadedState()?.let { state -> + hasMore(SnippetScope.ALL, state.pages) + .subscribeOn(Schedulers.io()) + .subscribeBy( + onSuccess = { hasMore -> + if (hasMore) { + loadSnippets(state.user, pages = state.pages + ONE_PAGE) + } + }, + onError = { + Timber.e("Couldn't check next page, error = $it") + mutableEvent.value = Alert(errorMessages.parse(it)) + }) + .also { disposables += it } + } + } + + private fun loadSnippets( + user: User, + pages: Int = 1, + scope: SnippetScope = SnippetScope.ALL + ) { + getSnippets(scope, pages) + .subscribeOn(Schedulers.io()) + .subscribeBy( + onSuccess = { + cachedSnippets = it + scopedSnippets = cachedSnippets + val updatedFilters = getLanguageFilters(cachedSnippets) + filterState = filterState.copy(languages = updatedFilters) + mutableState.value = Loaded( + user, + it, + pages, + filterState + ) + loadNextPage() + }, + onError = { + Timber.e("Couldn't load snippets, error = $it") + parseError(it) + } + ).also { disposables += it } + } + + private fun getLoadedState(): Loaded? = state.value as? Loaded +} + +sealed class MainViewState +object Loading : MainViewState() +data class Loaded( + val user: User, + val snippets: List, + val pages: Int, + val filters: SnippetFilters +) : MainViewState() + +data class Error(val message: String?) : MainViewState() + +sealed class MainEvent +object Startup : MainEvent() +data class Alert(val message: String) : MainEvent() +object Logout : MainEvent() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModelPlugin.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModelPlugin.kt new file mode 100644 index 0000000..aac58db --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModelPlugin.kt @@ -0,0 +1,96 @@ +package dev.snipme.snipmeapp.bridge.main + +import io.flutter.plugin.common.BinaryMessenger +import org.koin.core.component.inject +import dev.snipme.snipmeapp.bridge.Bridge +import dev.snipme.snipmeapp.bridge.ModelPlugin +import dev.snipme.snipmeapp.bridge.toModelData +import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.domain.snippets.SnippetFilters + +class MainModelPlugin : ModelPlugin(), Bridge.MainModelBridge { + private val model: MainModel by inject() + private var oldEvent: MainEvent? = null + private var oldState: MainViewState? = null + + override fun onSetup( + messenger: BinaryMessenger, + bridge: Bridge.MainModelBridge? + ) { + Bridge.MainModelBridge.setup(messenger, bridge) + } + + override fun getState(): Bridge.MainModelStateData = getState(model.state.value) + + override fun getEvent(): Bridge.MainModelEventData = getEvent(model.event.value) + + override fun resetEvent() { + model.event.value = Startup + } + + override fun initState() { + model.initState() + } + + override fun filterLanguage(language: String, isSelected: Boolean) { + model.filterLanguage(language, isSelected) + } + + override fun filterScope(scope: String) { + model.filterScope(scope) + } + + override fun logOut() { + model.logOut() + } + + private fun getState(viewState: MainViewState): Bridge.MainModelStateData { + return Bridge.MainModelStateData().apply { + state = viewState.toModelState() + is_loading = viewState is Loading + data = (viewState as? Loaded)?.snippets?.toModelData() + filter = (viewState as? Loaded)?.filters?.toModelFilter() + oldHash = oldState?.hashCode()?.toLong() + newHash = viewState.hashCode().toLong() + }.also { + oldState = viewState + } + } + + private fun getEvent(viewEvent: MainEvent): Bridge.MainModelEventData { + return Bridge.MainModelEventData().apply { + event = viewEvent.toModelEvent() + message = (viewEvent as? Alert)?.message + oldHash = oldEvent?.hashCode()?.toLong() + newHash = viewEvent.hashCode().toLong() + }.also { + oldEvent = viewEvent + } + } + + private fun MainEvent.toModelEvent() = + when (this) { + is Alert -> Bridge.MainModelEvent.ALERT + is Logout -> Bridge.MainModelEvent.LOGOUT + else -> Bridge.MainModelEvent.NONE + } + + private fun MainViewState.toModelState() = + when (this) { + Loading -> Bridge.ModelState.LOADING + is Loaded -> Bridge.ModelState.LOADED + is Error -> Bridge.ModelState.ERROR + } + + private fun List.toModelData() = map { it.toModelData() } + + private fun SnippetFilters.toModelFilter(): Bridge.SnippetFilter { + val it = this + return Bridge.SnippetFilter().apply { + languages = it.languages + selectedLanguages = it.selectedLanguages + scopes = it.scopes + selectedScope = it.selectedScope + } + } +} diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/session/SessionModel.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/session/SessionModel.kt new file mode 100644 index 0000000..14ae406 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/session/SessionModel.kt @@ -0,0 +1,24 @@ +package dev.snipme.snipmeapp.bridge.session + +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.schedulers.Schedulers +import dev.snipme.snipmeapp.domain.auth.LogoutUserUseCase +import dev.snipme.snipmeapp.util.extension.inProgress + +class SessionModel( + private val logout: LogoutUserUseCase +) { + private val disposables = CompositeDisposable() + private var logoutDisposable: Disposable? = null + + fun logOut(onComplete: () -> Unit) { + if (logoutDisposable.inProgress()) return + + logoutDisposable = logout() + .subscribeOn(Schedulers.io()) + .subscribe { onComplete() } + .also { disposables += it } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/KoinConfig.kt b/app/src/main/java/dev/snipme/snipmeapp/di/KoinConfig.kt new file mode 100644 index 0000000..ca08b3c --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/di/KoinConfig.kt @@ -0,0 +1,13 @@ +package dev.snipme.snipmeapp.di + +val koinModules = listOf( + mapperFilterModule, + preferenceModule, + networkModule, + serviceModule, + repositoryModule, + utilModule, + useCaseModule, + interactorModule, + modelModule +) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/MapperFilterModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/MapperFilterModule.kt new file mode 100644 index 0000000..e6fc98d --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/di/MapperFilterModule.kt @@ -0,0 +1,10 @@ +package dev.snipme.snipmeapp.di + +import org.koin.dsl.module +import dev.snipme.snipmeapp.domain.language.AvailableLanguageFilter +import dev.snipme.snipmeapp.domain.snippets.SnippetResponseMapper + +internal val mapperFilterModule = module { + factory { SnippetResponseMapper() } + factory { AvailableLanguageFilter() } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/ModelModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/ModelModule.kt new file mode 100644 index 0000000..a63b6ba --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/di/ModelModule.kt @@ -0,0 +1,14 @@ +package dev.snipme.snipmeapp.di + +import org.koin.dsl.module +import dev.snipme.snipmeapp.bridge.detail.DetailModel +import dev.snipme.snipmeapp.bridge.login.LoginModel +import dev.snipme.snipmeapp.bridge.main.MainModel +import dev.snipme.snipmeapp.bridge.session.SessionModel + +internal val modelModule = module { + single { SessionModel(get()) } + single { LoginModel(get(), get(), get()) } + single { MainModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } + single { DetailModel(get(), get(), get(), get(), get(), get(), get(), get(), get()) } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/NetworkModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/NetworkModule.kt new file mode 100644 index 0000000..a306688 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/di/NetworkModule.kt @@ -0,0 +1,72 @@ +package dev.snipme.snipmeapp.di + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import okhttp3.logging.HttpLoggingInterceptor.Level.BASIC +import okhttp3.logging.HttpLoggingInterceptor.Level.BODY +import org.koin.dsl.module +import dev.snipme.snipmeapp.BuildConfig +import dev.snipme.snipmeapp.util.AuthInterceptor +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.moshi.MoshiConverterFactory +import timber.log.Timber +import java.security.SecureRandom +import java.security.cert.X509Certificate +import javax.net.ssl.* + +private const val TAG_LOG_OKHTTP = "OkHttp" +private const val KEY_HEADER_AUTHORIZATION = "Authorization" +private const val KEY_HEADER_COOKIE = "Cookie" +private const val KEY_PROTOCOL_SSL = "SSL" + +internal val networkModule = module { + + single { + HttpLoggingInterceptor.Logger { message -> Timber.tag(TAG_LOG_OKHTTP).d(message) } + } + + single { + HttpLoggingInterceptor(get()).apply { + if (BuildConfig.DEBUG) setLevel(BODY) else setLevel(BASIC) + redactHeader(KEY_HEADER_AUTHORIZATION) + redactHeader(KEY_HEADER_COOKIE) + } + } + + single { AuthInterceptor(get()) } + + single { + object : X509TrustManager { + override fun checkClientTrusted(chain: Array?, authType: String?) = Unit + + override fun checkServerTrusted(chain: Array?, authType: String?) = Unit + + override fun getAcceptedIssuers(): Array = emptyArray() + } + } + single { get() } + + single { + SSLContext.getInstance(KEY_PROTOCOL_SSL).apply { + init(null, arrayOf(get()), SecureRandom()) + }.socketFactory + } + + single { + OkHttpClient.Builder().apply { + addInterceptor(get()) + addInterceptor(get()) + sslSocketFactory(get(), get()) + }.build() + } + + single { + Retrofit.Builder() + .client(get()) + .baseUrl(BuildConfig.BASE_URL) + .addConverterFactory(MoshiConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/PreferenceModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/PreferenceModule.kt new file mode 100644 index 0000000..5a63f08 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/di/PreferenceModule.kt @@ -0,0 +1,11 @@ +package dev.snipme.snipmeapp.di + +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module +import dev.snipme.snipmeapp.infrastructure.local.AuthPreferences +import dev.snipme.snipmeapp.util.PreferencesUtil + +val preferenceModule = module { + single { PreferencesUtil(androidContext()) } + single { AuthPreferences(get()) } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/RepositoryModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/RepositoryModule.kt new file mode 100644 index 0000000..5a8e567 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/di/RepositoryModule.kt @@ -0,0 +1,21 @@ +package dev.snipme.snipmeapp.di + +import org.koin.dsl.module +import dev.snipme.snipmeapp.domain.repository.auth.AuthRepository +import dev.snipme.snipmeapp.domain.repository.auth.AuthRepositoryReal +import dev.snipme.snipmeapp.domain.repository.language.LanguageRepository +import dev.snipme.snipmeapp.domain.repository.language.LanguageRepositoryReal +import dev.snipme.snipmeapp.domain.repository.networkstate.NetworkStateRepository +import dev.snipme.snipmeapp.domain.repository.networkstate.NetworkStateRepositoryReal +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepositoryReal +import dev.snipme.snipmeapp.domain.repository.user.UserRepository +import dev.snipme.snipmeapp.domain.repository.user.UserRepositoryReal + +internal val repositoryModule = module { + single { NetworkStateRepositoryReal() } + single { AuthRepositoryReal(get(), get(), get()) } + single { UserRepositoryReal(get(), get()) } + single { SnippetRepositoryReal(get(), get(), get()) } + single { LanguageRepositoryReal(get(), get(), get()) } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt new file mode 100644 index 0000000..b8002b7 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt @@ -0,0 +1,13 @@ +package dev.snipme.snipmeapp.di + +import org.koin.dsl.module +import dev.snipme.snipmeapp.infrastructure.remote.* +import retrofit2.Retrofit + +internal val serviceModule = module { + single { get().create(AuthService::class.java) } + single { get().create(UserService::class.java) } + single { get().create(SnippetService::class.java) } + single { get().create(LanguageService::class.java) } + single { get().create(ShareService::class.java) } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt new file mode 100644 index 0000000..837e586 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt @@ -0,0 +1,63 @@ +package dev.snipme.snipmeapp.di + +import org.koin.dsl.module +import dev.snipme.snipmeapp.domain.auth.* +import dev.snipme.snipmeapp.domain.clipboard.AddToClipboardUseCase +import dev.snipme.snipmeapp.domain.clipboard.GetFromClipboardUseCase +import dev.snipme.snipmeapp.domain.language.GetLanguagesUseCase +import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.reaction.GetTargetUserReactionUseCase +import dev.snipme.snipmeapp.domain.reaction.SetUserReactionUseCase +import dev.snipme.snipmeapp.domain.share.ShareSnippetCodeUseCase +import dev.snipme.snipmeapp.domain.snippet.* +import dev.snipme.snipmeapp.domain.filter.FilterSnippetsByLanguageUseCase +import dev.snipme.snipmeapp.domain.filter.FilterSnippetsByScopeUseCase +import dev.snipme.snipmeapp.domain.filter.GetLanguageFiltersUseCase +import dev.snipme.snipmeapp.domain.filter.UpdateSnippetFiltersLanguageUseCase +import dev.snipme.snipmeapp.domain.snippets.GetSnippetsUseCase +import dev.snipme.snipmeapp.domain.snippets.HasMoreSnippetPagesUseCase +import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase + +internal val useCaseModule = module { + // Base + factory { CheckNetworkAvailableUseCase(get()) } + // Auth + factory { IdentifyUserUseCase(get(), get()) } + factory { InitialLoginUseCase(get()) } + factory { LoginUseCase(get(), get()) } + factory { RegisterUseCase(get(), get()) } + factory { LogoutUserUseCase(get()) } + factory { AuthorizationUseCase(get()) } + // User + factory { GetSingleUserUseCase(get(), get(), get()) } + // Snippet + factory { GetSnippetsUseCase(get(), get(), get()) } + factory { GetSingleSnippetUseCase(get(), get(), get()) } + factory { HasMoreSnippetPagesUseCase(get(), get(), get()) } + factory { CreateSnippetUseCase(get(), get(), get()) } + factory { UpdateSnippetUseCase(get(), get(), get()) } + factory { ObserveUpdatedSnippetPageUseCase(get()) } + factory { ObserveSnippetUpdatesUseCase(get()) } + factory { GetTargetUserReactionUseCase() } + factory { SetUserReactionUseCase(get(), get(), get()) } + factory { DeleteSnippetUseCase(get()) } + // Language + factory { GetLanguagesUseCase(get(), get(), get()) } + // Share + factory { ShareSnippetCodeUseCase(get()) } + // Clipboard + single { AddToClipboardUseCase(get()) } + factory { GetFromClipboardUseCase(get()) } + // Save + factory { SaveSnippetUseCase(get()) } + // Filter + factory { GetLanguageFiltersUseCase() } + factory { FilterSnippetsByLanguageUseCase() } + factory { FilterSnippetsByScopeUseCase() } + factory { UpdateSnippetFiltersLanguageUseCase() } +} + +internal val interactorModule = module { + factory { LoginInteractor(get(), get(), get()) } + factory { EditInteractor(get(), get(), get(), get(), get()) } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/UtilModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/UtilModule.kt new file mode 100644 index 0000000..1ca58e7 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/di/UtilModule.kt @@ -0,0 +1,19 @@ +package dev.snipme.snipmeapp.di + +import android.content.ClipboardManager +import android.content.Context.CLIPBOARD_SERVICE +import org.koin.android.ext.koin.androidApplication +import org.koin.dsl.module +import dev.snipme.snipmeapp.BuildConfig +import dev.snipme.snipmeapp.domain.error.DebugErrorHandler +import dev.snipme.snipmeapp.domain.error.SafeErrorHandler +import dev.snipme.snipmeapp.domain.message.ErrorMessages +import dev.snipme.snipmeapp.domain.message.ValidationMessages +import dev.snipme.snipmeapp.domain.message.RealValidationMessages + +internal val utilModule = module { + factory { if (BuildConfig.DEBUG) DebugErrorHandler() else SafeErrorHandler() } + factory { ErrorMessages(get()) } + factory { RealValidationMessages(get()) } + single { androidApplication().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/AuthorizationUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/AuthorizationUseCase.kt new file mode 100644 index 0000000..32ad5ff --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/AuthorizationUseCase.kt @@ -0,0 +1,20 @@ +package dev.snipme.snipmeapp.domain.auth + +import io.reactivex.Completable +import dev.snipme.snipmeapp.domain.error.exception.SessionExpiredException +import dev.snipme.snipmeapp.domain.repository.auth.AuthRepository + +// In future this should check if token is correct and up to date (refresh) +class AuthorizationUseCase( + private val repository: AuthRepository +) { + operator fun invoke() = repository.getToken() + .defaultIfEmpty("") + .flatMapCompletable { token -> + if (token.isEmpty()) { + Completable.error(SessionExpiredException()) + } else { + Completable.complete() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/IdentifyUserUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/IdentifyUserUseCase.kt new file mode 100644 index 0000000..8b3cb9a --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/IdentifyUserUseCase.kt @@ -0,0 +1,12 @@ +package dev.snipme.snipmeapp.domain.auth + +import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.repository.auth.AuthRepository + +class IdentifyUserUseCase( + private val auth: AuthRepository, + private val checkNetwork: CheckNetworkAvailableUseCase +) { + operator fun invoke(login: String) = checkNetwork() + .andThen(auth.identify(login)) +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/InitialLoginUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/InitialLoginUseCase.kt new file mode 100644 index 0000000..0129e04 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/InitialLoginUseCase.kt @@ -0,0 +1,16 @@ +package dev.snipme.snipmeapp.domain.auth + +import io.reactivex.Completable +import dev.snipme.snipmeapp.domain.error.exception.NotAuthorizedException +import dev.snipme.snipmeapp.domain.repository.auth.AuthRepository + +class InitialLoginUseCase(private val auth: AuthRepository) { + operator fun invoke() = auth.getToken() + .isEmpty + .flatMapCompletable { isTokenEmpty -> + when { + isTokenEmpty -> Completable.error(NotAuthorizedException()) + else -> Completable.complete() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginInteractor.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginInteractor.kt new file mode 100644 index 0000000..f8e5ba1 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginInteractor.kt @@ -0,0 +1,13 @@ +package dev.snipme.snipmeapp.domain.auth + +class LoginInteractor( + private val identifyUserUseCase: IdentifyUserUseCase, + private val loginUseCase: LoginUseCase, + private val registerUseCase: RegisterUseCase +) { + fun identify(login: String) = identifyUserUseCase(login) + + fun login(login: String, password: String) = loginUseCase(login, password) + + fun register(login: String, password: String, email: String) = registerUseCase(login, password, email) +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginUseCase.kt new file mode 100644 index 0000000..c4d4809 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginUseCase.kt @@ -0,0 +1,17 @@ +package dev.snipme.snipmeapp.domain.auth + +import io.reactivex.Completable +import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.repository.auth.AuthRepository + +class LoginUseCase( + private val auth: AuthRepository, + private val checkNetwork: CheckNetworkAvailableUseCase +) { + + operator fun invoke(login: String, password: String): Completable = checkNetwork() + .andThen( + auth.login(login, password) + .flatMapCompletable { token -> auth.saveToken(token) } + ) +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LogoutUserUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LogoutUserUseCase.kt new file mode 100644 index 0000000..4ad59bc --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LogoutUserUseCase.kt @@ -0,0 +1,7 @@ +package dev.snipme.snipmeapp.domain.auth + +import dev.snipme.snipmeapp.domain.repository.auth.AuthRepository + +class LogoutUserUseCase(private val repository: AuthRepository) { + operator fun invoke() = repository.clearToken() +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/RegisterUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/RegisterUseCase.kt new file mode 100644 index 0000000..431a68d --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/RegisterUseCase.kt @@ -0,0 +1,18 @@ +package dev.snipme.snipmeapp.domain.auth + +import io.reactivex.Completable +import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.repository.auth.AuthRepository + +class RegisterUseCase( + private val auth: AuthRepository, + private val checkNetwork: CheckNetworkAvailableUseCase +) { + operator fun invoke(login: String, password: String, email: String): Completable = + checkNetwork().andThen(register(login, password, email)) + + private fun register(login: String, password: String, email: String) = + auth.register(login, password, email) + .flatMap { auth.login(login, password) } + .flatMapCompletable { token -> auth.saveToken(token) } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/clipboard/AddToClipboardUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/clipboard/AddToClipboardUseCase.kt new file mode 100644 index 0000000..b8bd405 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/clipboard/AddToClipboardUseCase.kt @@ -0,0 +1,11 @@ +package dev.snipme.snipmeapp.domain.clipboard + +import android.content.ClipData +import android.content.ClipboardManager + +class AddToClipboardUseCase(private val manager: ClipboardManager) { + operator fun invoke(label: String, text: String) { + val clip = ClipData.newPlainText(label, text) + manager.setPrimaryClip(clip) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/clipboard/GetFromClipboardUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/clipboard/GetFromClipboardUseCase.kt new file mode 100644 index 0000000..82a5b37 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/clipboard/GetFromClipboardUseCase.kt @@ -0,0 +1,16 @@ +package dev.snipme.snipmeapp.domain.clipboard + +import android.content.ClipboardManager +import timber.log.Timber + +private const val MAIN_CLIP = 0 + +class GetFromClipboardUseCase(private val manager: ClipboardManager) { + operator fun invoke(): String? = + try { + manager.primaryClip?.getItemAt(MAIN_CLIP)?.text.toString() + } catch (e: Exception) { + Timber.e("Couldn't get text from clipboard, error = $e") + null + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/error/DebugErrorHandler.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/error/DebugErrorHandler.kt new file mode 100644 index 0000000..05b1076 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/error/DebugErrorHandler.kt @@ -0,0 +1,38 @@ +package dev.snipme.snipmeapp.domain.error + +import dev.snipme.snipmeapp.domain.error.exception.* +import retrofit2.HttpException +import timber.log.Timber +import java.io.IOException +import java.net.HttpURLConnection +import java.net.UnknownHostException +import java.util.concurrent.TimeoutException + +class DebugErrorHandler: ErrorHandler { + + override fun handle(throwable: Throwable): SnipException { + Timber.e(throwable) + + if ( + throwable is UnknownHostException || + throwable is IOException || + throwable is TimeoutException + ) { + return ConnectionException(throwable) + } + + if (throwable is HttpException) { + return when (throwable.code()) { + HttpURLConnection.HTTP_INTERNAL_ERROR -> RemoteException(throwable) // 500 + HttpURLConnection.HTTP_UNAUTHORIZED -> NotAuthorizedException(throwable) // 401 + HttpURLConnection.HTTP_NOT_FOUND -> ContentNotFoundException(throwable) // 404 + HttpURLConnection.HTTP_FORBIDDEN -> ForbiddenActionException(throwable) // 403 + HttpURLConnection.HTTP_BAD_REQUEST -> ForbiddenActionException(throwable) // 400 + HttpURLConnection.HTTP_BAD_METHOD -> ForbiddenActionException(throwable) // 405 + else -> SnipException(throwable) + } + } + + return SnipException(throwable) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/error/ErrorHandler.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/error/ErrorHandler.kt new file mode 100644 index 0000000..37ef1bd --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/error/ErrorHandler.kt @@ -0,0 +1,7 @@ +package dev.snipme.snipmeapp.domain.error + +import dev.snipme.snipmeapp.domain.error.exception.SnipException + +interface ErrorHandler { + fun handle(throwable: Throwable): SnipException +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/error/SafeErrorHandler.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/error/SafeErrorHandler.kt new file mode 100644 index 0000000..d7ba903 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/error/SafeErrorHandler.kt @@ -0,0 +1,35 @@ +package dev.snipme.snipmeapp.domain.error + +import dev.snipme.snipmeapp.domain.error.exception.* +import retrofit2.HttpException +import java.io.IOException +import java.net.HttpURLConnection.* +import java.net.UnknownHostException +import java.util.concurrent.TimeoutException + +class SafeErrorHandler : ErrorHandler { + + override fun handle(throwable: Throwable): SnipException { + if ( + throwable is UnknownHostException || + throwable is IOException || + throwable is TimeoutException + ) { + return ConnectionException() + } + + if (throwable is HttpException) { + return when (throwable.code()) { + HTTP_INTERNAL_ERROR -> RemoteException() // 500 + HTTP_UNAUTHORIZED -> NotAuthorizedException() // 401 + HTTP_NOT_FOUND -> ContentNotFoundException() // 404 + HTTP_FORBIDDEN -> ForbiddenActionException() // 403 + HTTP_BAD_REQUEST -> ForbiddenActionException() // 400 + HTTP_BAD_METHOD -> ForbiddenActionException() // 405 + else -> SnipException() + } + } + + return SnipException() + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/ConnectionException.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/ConnectionException.kt new file mode 100644 index 0000000..269ac2a --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/ConnectionException.kt @@ -0,0 +1,3 @@ +package dev.snipme.snipmeapp.domain.error.exception + +class ConnectionException(override val cause: Throwable? = null): SnipException() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/ContentNotFoundException.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/ContentNotFoundException.kt new file mode 100644 index 0000000..8050ad1 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/ContentNotFoundException.kt @@ -0,0 +1,3 @@ +package dev.snipme.snipmeapp.domain.error.exception + +class ContentNotFoundException(override val cause: Throwable? = null): SnipException() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/ForbiddenActionException.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/ForbiddenActionException.kt new file mode 100644 index 0000000..a081fdc --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/ForbiddenActionException.kt @@ -0,0 +1,3 @@ +package dev.snipme.snipmeapp.domain.error.exception + +class ForbiddenActionException(override val cause: Throwable? = null): SnipException() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/NetworkNotAvailableException.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/NetworkNotAvailableException.kt new file mode 100644 index 0000000..01f50b0 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/NetworkNotAvailableException.kt @@ -0,0 +1,3 @@ +package dev.snipme.snipmeapp.domain.error.exception + +class NetworkNotAvailableException(override val cause: Throwable? = null): SnipException() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/NotAuthorizedException.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/NotAuthorizedException.kt new file mode 100644 index 0000000..a916c7d --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/NotAuthorizedException.kt @@ -0,0 +1,3 @@ +package dev.snipme.snipmeapp.domain.error.exception + +class NotAuthorizedException(override val cause: Throwable? = null): SnipException() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/RemoteException.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/RemoteException.kt new file mode 100644 index 0000000..172b4ca --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/RemoteException.kt @@ -0,0 +1,3 @@ +package dev.snipme.snipmeapp.domain.error.exception + +class RemoteException(override val cause: Throwable? = null): SnipException() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/SessionExpiredException.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/SessionExpiredException.kt new file mode 100644 index 0000000..e7316a5 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/SessionExpiredException.kt @@ -0,0 +1,3 @@ +package dev.snipme.snipmeapp.domain.error.exception + +class SessionExpiredException(override val cause: Throwable? = null): SnipException() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/SnipException.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/SnipException.kt new file mode 100644 index 0000000..0619fad --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/error/exception/SnipException.kt @@ -0,0 +1,5 @@ +package dev.snipme.snipmeapp.domain.error.exception + +import java.lang.Exception + +open class SnipException(override val cause: Throwable? = null): Exception() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/filter/FilterSnippetsByLanguageUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/filter/FilterSnippetsByLanguageUseCase.kt new file mode 100644 index 0000000..a77ee72 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/filter/FilterSnippetsByLanguageUseCase.kt @@ -0,0 +1,10 @@ +package dev.snipme.snipmeapp.domain.filter + +import dev.snipme.snipmeapp.domain.snippets.Snippet + +class FilterSnippetsByLanguageUseCase { + operator fun invoke(snippets: List, languages: List): List { + if (languages.contains(SNIPPET_FILTER_ALL)) return snippets + return snippets.filter { languages.contains(it.language.raw) } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/filter/FilterSnippetsByScopeUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/filter/FilterSnippetsByScopeUseCase.kt new file mode 100644 index 0000000..762110b --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/filter/FilterSnippetsByScopeUseCase.kt @@ -0,0 +1,11 @@ +package dev.snipme.snipmeapp.domain.filter + +import dev.snipme.snipmeapp.domain.snippets.Snippet + +class FilterSnippetsByScopeUseCase { + operator fun invoke(snippets: List, scope: String): List { + if (scope == SNIPPET_FILTER_ALL) return snippets + + return snippets.filter { it.isOwner && it.visibility.name.equals(scope, ignoreCase = true) } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/filter/GetLanguageFiltersUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/filter/GetLanguageFiltersUseCase.kt new file mode 100644 index 0000000..388444d --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/filter/GetLanguageFiltersUseCase.kt @@ -0,0 +1,18 @@ +package dev.snipme.snipmeapp.domain.filter + +import dev.snipme.snipmeapp.domain.snippets.Snippet + +const val SNIPPET_FILTER_ALL = "All" + +class GetLanguageFiltersUseCase { + + operator fun invoke(snippets: List): List { + return listOf(SNIPPET_FILTER_ALL) + + snippets.groupBy { it.language.raw } + .map { it.key to it.value.count() } + .sortedBy { it.second }.reversed() + .map { it.first } + .filter { it.isNotBlank() } + .distinct() + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/filter/UpdateSnippetFiltersLanguageUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/filter/UpdateSnippetFiltersLanguageUseCase.kt new file mode 100644 index 0000000..3a72c99 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/filter/UpdateSnippetFiltersLanguageUseCase.kt @@ -0,0 +1,24 @@ +package dev.snipme.snipmeapp.domain.filter + +import dev.snipme.snipmeapp.domain.snippets.SnippetFilters + +class UpdateSnippetFiltersLanguageUseCase { + + operator fun invoke( + filter: SnippetFilters, + language: String, + isSelected: Boolean + ): SnippetFilters = when { + language == SNIPPET_FILTER_ALL -> + filter.copy(selectedLanguages = listOf(SNIPPET_FILTER_ALL)) + isSelected.not() -> filter.copy(selectedLanguages = filter.selectedLanguages - language) + else -> + filter.copy( + selectedLanguages = ( + filter.selectedLanguages + - SNIPPET_FILTER_ALL + + language + ).distinct() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/language/AvailableLanguageFilter.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/language/AvailableLanguageFilter.kt new file mode 100644 index 0000000..24992ae --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/language/AvailableLanguageFilter.kt @@ -0,0 +1,12 @@ +package dev.snipme.snipmeapp.domain.language + +import dev.snipme.snipmeapp.domain.snippets.SnippetLanguageType +import dev.snipme.snipmeapp.domain.snippets.SnippetLanguageMapper + +class AvailableLanguageFilter { + + operator fun invoke(language: String): Boolean { + val type = SnippetLanguageMapper.fromString(language) + return type != SnippetLanguageType.UNKNOWN + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/language/GetLanguagesUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/language/GetLanguagesUseCase.kt new file mode 100644 index 0000000..673c3a0 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/language/GetLanguagesUseCase.kt @@ -0,0 +1,17 @@ +package dev.snipme.snipmeapp.domain.language + +import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase +import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.repository.language.LanguageRepository +import dev.snipme.snipmeapp.util.extension.mapItems + +class GetLanguagesUseCase( + private val auth: AuthorizationUseCase, + private val networkState: CheckNetworkAvailableUseCase, + private val language: LanguageRepository +) { + operator fun invoke() = + auth() + .andThen(networkState()) + .andThen(language.languages().mapItems { it.name }) +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/language/SnippetLanguage.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/language/SnippetLanguage.kt new file mode 100644 index 0000000..8668927 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/language/SnippetLanguage.kt @@ -0,0 +1,7 @@ +package dev.snipme.snipmeapp.domain.language + +import dev.snipme.snipmeapp.infrastructure.model.response.LanguageResponse + +data class SnippetLanguage(val name: String) + +fun LanguageResponse.toLanguage(): SnippetLanguage = SnippetLanguage(name) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/message/ErrorMessages.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/message/ErrorMessages.kt new file mode 100644 index 0000000..82acd29 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/message/ErrorMessages.kt @@ -0,0 +1,29 @@ +package dev.snipme.snipmeapp.domain.message + +import android.content.Context +import dev.snipme.snipmeapp.R +import dev.snipme.snipmeapp.domain.error.exception.* + +class ErrorMessages(context: Context) { + private val res = context.resources + + val generic = res.getString(R.string.error_message_generic) + val noConnection = res.getString(R.string.error_message_no_connection) + val sessionExpired = res.getString(R.string.error_message_session_expired) + val noAccess = res.getString(R.string.error_message_no_access) + val remoteError = res.getString(R.string.error_message_remote_error) + val unknownLanguage = res.getString(R.string.error_message_unknown_language) + val alreadyRegistered = res.getString(R.string.error_message_user_already_registered) + + fun parse(throwable: Throwable): String = + when (throwable) { + is ConnectionException -> noConnection + is ContentNotFoundException -> noAccess + is ForbiddenActionException -> noAccess + is NetworkNotAvailableException -> noConnection + is NotAuthorizedException -> sessionExpired + is RemoteException -> remoteError + is SessionExpiredException -> sessionExpired + else -> generic + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/message/RealValidationMessages.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/message/RealValidationMessages.kt new file mode 100644 index 0000000..5e67c01 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/message/RealValidationMessages.kt @@ -0,0 +1,26 @@ +package dev.snipme.snipmeapp.domain.message + +import android.content.Context +import dev.snipme.snipmeapp.R + +class RealValidationMessages(context: Context): ValidationMessages { + private val res = context.resources + + override val passwordWrong = res.getString(R.string.validation_password_wrong) + override val passwordTooShort = res.getString(R.string.validation_password_too_short) + override val passwordNoSpecialChar = res.getString(R.string.validation_password_special_char) + override val passwordNoDigit = res.getString(R.string.validation_password_digit) + override val passwordNoUpperLower = res.getString(R.string.validation_password_lower_upper) + override val passwordNotEqual: String = res.getString(R.string.validation_password_not_equal) + + override val emailTooShort: String = res.getString(R.string.validation_email_too_short) + override val emailWrongStructure: String = res.getString(R.string.validation_email_wrong_structure) + + override val phraseTooShort: String = res.getString(R.string.validation_phrase_too_short) + override val phraseTooLong: String = res.getString(R.string.validation_phrase_too_long) + override val phraseNoUpperLower: String = res.getString(R.string.validation_phrase_lower_upper) + override val phraseHasDiacritics: String = res.getString(R.string.validation_phrase_no_ascii) + + override val phrasesBlank = res.getString(R.string.validation_phrases_blank) + override val phrasesNotEqual: String = res.getString(R.string.validation_phrase_too_short) +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/message/ValidationMessages.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/message/ValidationMessages.kt new file mode 100644 index 0000000..0c2dbce --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/message/ValidationMessages.kt @@ -0,0 +1,22 @@ +package dev.snipme.snipmeapp.domain.message + +interface ValidationMessages { + + val passwordWrong: String + val passwordTooShort: String + val passwordNoSpecialChar: String + val passwordNoUpperLower: String + val passwordNoDigit: String + val passwordNotEqual: String + + val emailTooShort: String + val emailWrongStructure: String + + val phraseTooShort: String + val phraseTooLong: String + val phraseNoUpperLower: String + val phraseHasDiacritics: String + + val phrasesBlank: String + val phrasesNotEqual: String +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/network/CheckNetworkAvailableUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/network/CheckNetworkAvailableUseCase.kt new file mode 100644 index 0000000..208a91f --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/network/CheckNetworkAvailableUseCase.kt @@ -0,0 +1,18 @@ +package dev.snipme.snipmeapp.domain.network + +import io.reactivex.Completable +import dev.snipme.snipmeapp.domain.error.exception.NetworkNotAvailableException +import dev.snipme.snipmeapp.domain.repository.networkstate.NetworkStateRepository + +class CheckNetworkAvailableUseCase(private val networkStateRepository: NetworkStateRepository) { + + operator fun invoke(): Completable = + networkStateRepository.isAvailable() + .flatMapCompletable { available -> + if (available) { + Completable.complete() + } else { + Completable.error(NetworkNotAvailableException()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/GetTargetUserReactionUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/GetTargetUserReactionUseCase.kt new file mode 100644 index 0000000..9547c39 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/GetTargetUserReactionUseCase.kt @@ -0,0 +1,11 @@ +package dev.snipme.snipmeapp.domain.reaction + +import dev.snipme.snipmeapp.domain.snippets.Snippet + +class GetTargetUserReactionUseCase { + operator fun invoke(snippet: Snippet, reaction: UserReaction) = + when { + snippet.userReaction == reaction -> UserReaction.NONE + else -> reaction + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt new file mode 100644 index 0000000..057d569 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt @@ -0,0 +1,20 @@ +package dev.snipme.snipmeapp.domain.reaction + +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository +import dev.snipme.snipmeapp.domain.snippets.Snippet + +class SetUserReactionUseCase( + private val auth: AuthorizationUseCase, + private val repository: SnippetRepository, + private val getTargetReaction: GetTargetUserReactionUseCase +) { + operator fun invoke(snippet: Snippet, reaction: UserReaction): Single { + val targetReaction = getTargetReaction(snippet, reaction) + return auth() + .andThen(repository.reaction(snippet.uuid, targetReaction)) + .andThen(repository.snippet(snippet.uuid)) + .doOnSuccess { repository.updateListener.onNext(it) } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/UserReaction.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/UserReaction.kt new file mode 100644 index 0000000..0d7f461 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/UserReaction.kt @@ -0,0 +1,7 @@ +package dev.snipme.snipmeapp.domain.reaction + +enum class UserReaction { + NONE, + LIKE, + DISLIKE +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepository.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepository.kt new file mode 100644 index 0000000..7050dd9 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepository.kt @@ -0,0 +1,21 @@ +package dev.snipme.snipmeapp.domain.repository.auth + +import io.reactivex.Completable +import io.reactivex.Maybe +import io.reactivex.Single +import dev.snipme.snipmeapp.infrastructure.model.response.RegisterUserResponse + +interface AuthRepository { + + fun identify(login: String): Single + + fun login(login: String, password: String): Single + + fun register(login: String, password: String, email: String): Single + + fun saveToken(token: String): Completable + + fun getToken(): Maybe + + fun clearToken(): Completable +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepositoryReal.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepositoryReal.kt new file mode 100644 index 0000000..b048579 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepositoryReal.kt @@ -0,0 +1,48 @@ +package dev.snipme.snipmeapp.domain.repository.auth + +import io.reactivex.Completable +import io.reactivex.Maybe +import dev.snipme.snipmeapp.domain.error.ErrorHandler +import dev.snipme.snipmeapp.infrastructure.local.AuthPreferences +import dev.snipme.snipmeapp.infrastructure.model.request.IdentifyUserRequest +import dev.snipme.snipmeapp.infrastructure.model.request.LoginUserRequest +import dev.snipme.snipmeapp.infrastructure.model.request.RegisterUserRequest +import dev.snipme.snipmeapp.infrastructure.remote.AuthService +import dev.snipme.snipmeapp.util.extension.mapError + +class AuthRepositoryReal( + private val errorHandler: ErrorHandler, + private val service: AuthService, + private val prefs: AuthPreferences +) : AuthRepository { + + override fun identify(login: String) = + service.identify(IdentifyUserRequest(login)) + .mapError { errorHandler.handle(it) } + + override fun login(login: String, password: String) = + service.login(LoginUserRequest(login, password)) + .mapError { errorHandler.handle(it) } + .map { it.token } + + override fun register(login: String, password: String, email: String) = + service.register(RegisterUserRequest(login, password, email)) + .mapError { errorHandler.handle(it) } + + override fun saveToken(token: String) = Completable.fromCallable { + prefs.saveToken(token) + } + + override fun getToken(): Maybe { + val token = prefs.getToken() + return if (token.isNullOrBlank()) { + Maybe.empty() + } else { + Maybe.just(token) + } + } + + override fun clearToken() = Completable.fromCallable { + prefs.clearToken() + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/language/LanguageRepository.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/language/LanguageRepository.kt new file mode 100644 index 0000000..d236f8f --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/language/LanguageRepository.kt @@ -0,0 +1,9 @@ +package dev.snipme.snipmeapp.domain.repository.language + +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.language.SnippetLanguage + +interface LanguageRepository { + + fun languages(): Single> +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/language/LanguageRepositoryReal.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/language/LanguageRepositoryReal.kt new file mode 100644 index 0000000..bffde26 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/language/LanguageRepositoryReal.kt @@ -0,0 +1,24 @@ +package dev.snipme.snipmeapp.domain.repository.language + +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.error.ErrorHandler +import dev.snipme.snipmeapp.domain.language.AvailableLanguageFilter +import dev.snipme.snipmeapp.domain.language.SnippetLanguage +import dev.snipme.snipmeapp.domain.language.toLanguage +import dev.snipme.snipmeapp.infrastructure.remote.LanguageService +import dev.snipme.snipmeapp.util.extension.filterItems +import dev.snipme.snipmeapp.util.extension.mapError +import dev.snipme.snipmeapp.util.extension.mapItems + +class LanguageRepositoryReal( + private val errorHandler: ErrorHandler, + private val service: LanguageService, + private val filterAvailable: AvailableLanguageFilter +): LanguageRepository { + + override fun languages(): Single> = + service.languages() + .mapError { errorHandler.handle(it) } + .filterItems { filterAvailable(it.name) } + .mapItems { it.toLanguage() } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/networkstate/NetworkStateRepository.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/networkstate/NetworkStateRepository.kt new file mode 100644 index 0000000..5aac54a --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/networkstate/NetworkStateRepository.kt @@ -0,0 +1,10 @@ +package dev.snipme.snipmeapp.domain.repository.networkstate + +import io.reactivex.Observable +import io.reactivex.Single + +interface NetworkStateRepository { + fun isAvailable(): Single + + fun observe(): Observable +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/networkstate/NetworkStateRepositoryReal.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/networkstate/NetworkStateRepositoryReal.kt new file mode 100644 index 0000000..19600c6 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/networkstate/NetworkStateRepositoryReal.kt @@ -0,0 +1,13 @@ +package dev.snipme.snipmeapp.domain.repository.networkstate + +import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork +import io.reactivex.Observable +import io.reactivex.Single + +class NetworkStateRepositoryReal : NetworkStateRepository { + override fun isAvailable(): Single = + ReactiveNetwork.checkInternetConnectivity() + + override fun observe(): Observable = + ReactiveNetwork.observeInternetConnectivity() +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt new file mode 100644 index 0000000..18c06bf --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt @@ -0,0 +1,39 @@ +package dev.snipme.snipmeapp.domain.repository.snippet + +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.subjects.BehaviorSubject +import dev.snipme.snipmeapp.domain.reaction.UserReaction +import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.domain.snippets.SnippetScope +import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility + +interface SnippetRepository { + + val updateListener: BehaviorSubject + + fun snippets(scope: SnippetScope, page: Int): Single> + + fun snippet(id: String): Single + + fun create( + title: String, + code: String, + language: String, + visibility: SnippetVisibility + ): Single + + fun update( + uuid: String, + title: String, + code: String, + language: String, + visibility: SnippetVisibility + ): Single + + fun count(scope: SnippetScope): Single + + fun reaction(uuid: String, reaction: UserReaction): Completable + + fun delete(uuid: String): Completable +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt new file mode 100644 index 0000000..349818f --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt @@ -0,0 +1,72 @@ +package dev.snipme.snipmeapp.domain.repository.snippet + +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.subjects.BehaviorSubject +import dev.snipme.snipmeapp.domain.error.ErrorHandler +import dev.snipme.snipmeapp.domain.reaction.UserReaction +import dev.snipme.snipmeapp.domain.snippets.* +import dev.snipme.snipmeapp.infrastructure.model.request.CreateSnippetRequest +import dev.snipme.snipmeapp.infrastructure.model.request.RateSnippetRequest +import dev.snipme.snipmeapp.infrastructure.remote.SnippetService +import dev.snipme.snipmeapp.util.extension.mapError +import dev.snipme.snipmeapp.util.extension.mapItems + +const val PAGE_START = 0 +const val SNIPPET_PAGE_SIZE = 10 +const val ONE_SNIPPET = 1 + +class SnippetRepositoryReal( + private val errorHandler: ErrorHandler, + private val service: SnippetService, + private val mapper: SnippetResponseMapper +) : SnippetRepository { + private var count: Int? = null + + override val updateListener = BehaviorSubject.create() + + override fun snippets(scope: SnippetScope, page: Int): Single> = + service.snippets(scope.value(), PAGE_START, SNIPPET_PAGE_SIZE * page) + .mapError { errorHandler.handle(it) } + .map { count = it.count; it.results } + .mapItems { mapper(it) } + + override fun snippet(id: String): Single = + service.snippet(id).map { mapper(it) } + .mapError { errorHandler.handle(it) } + + override fun create( + title: String, + code: String, + language: String, + visibility: SnippetVisibility + ): Single = + service.create(CreateSnippetRequest(title, code, language, visibility.name)) + .mapError { errorHandler.handle(it) } + .map { mapper(it) } + + override fun update( + uuid: String, + title: String, + code: String, + language: String, + visibility: SnippetVisibility + ): Single = + service.update(uuid, CreateSnippetRequest(title, code, language, visibility.name)) + .mapError { errorHandler.handle(it) } + .map { mapper(it) } + + override fun delete(uuid: String): Completable = service.delete(uuid) + + override fun count(scope: SnippetScope) = + if (count != null) { + Single.just(count!!) + } else { + service.snippets(scope.value(), PAGE_START, ONE_SNIPPET).map { it.count } + .mapError { errorHandler.handle(it) } + } + + override fun reaction(uuid: String, reaction: UserReaction) = + service.rate(RateSnippetRequest(uuid, reaction.name)) + .mapError { errorHandler.handle(it) } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryTest.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryTest.kt new file mode 100644 index 0000000..aa46604 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryTest.kt @@ -0,0 +1,166 @@ +package dev.snipme.snipmeapp.domain.repository.snippet + +import android.text.SpannableString +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.subjects.BehaviorSubject +import dev.snipme.snipmeapp.domain.error.ErrorHandler +import dev.snipme.snipmeapp.domain.reaction.UserReaction +import dev.snipme.snipmeapp.domain.snippets.* +import dev.snipme.snipmeapp.util.SyntaxHighlighter.getHighlighted +import dev.snipme.snipmeapp.util.extension.lines +import dev.snipme.snipmeapp.util.extension.newLineChar +import java.util.* + +private const val PREVIEW_COUNT = 5 + +class SnippetRepositoryTest(private val errorHandler: ErrorHandler) : SnippetRepository { + + private val uuid + get() = UUID.randomUUID().toString() + + private val list by lazy { + List(25) { + Snippet( + uuid = uuid, + title = "Snippet $it", + code = getMockCode(code), + language = getMockLanguage(), + visibility = SnippetVisibility.PUBLIC, + isOwner = true, + owner = Owner(it, "User $it"), + modifiedAt = Date(), + numberOfLikes = 5, + numberOfDislikes = 3, + userReaction = UserReaction.LIKE + ) + } + } + override val updateListener: BehaviorSubject = BehaviorSubject.create() + + override fun snippets(scope: SnippetScope, page: Int): Single> { + return Single.just(list) + .map { it.take(SNIPPET_PAGE_SIZE * page) } + .onErrorResumeNext { throwable -> Single.error(errorHandler.handle(throwable)) } + } + + override fun snippet(id: String): Single = Single.just(list.find { it.uuid == id }) + + override fun create( + title: String, + code: String, + language: String, + visibility: SnippetVisibility + ): Single = Single.just( + Snippet( + uuid = uuid, + title = title, + code = getMockCode(code), + language = getMockLanguage(), + visibility = SnippetVisibility.PUBLIC, + isOwner = true, + owner = Owner(0, "login"), + modifiedAt = Date(), + numberOfLikes = 0, + numberOfDislikes = 0, + userReaction = UserReaction.NONE + ) + ) + + override fun update( + uuid: String, + title: String, + code: String, + language: String, + visibility: SnippetVisibility + ): Single = Single.just( + Snippet( + uuid = uuid, + title = title, + code = getMockCode(code), + language = getMockLanguage(), + visibility = SnippetVisibility.PUBLIC, + isOwner = true, + owner = Owner(0, "login"), + modifiedAt = Date(), + numberOfLikes = 5, + numberOfDislikes = 3, + userReaction = UserReaction.LIKE + ) + ) + + override fun count(scope: SnippetScope): Single = Single.just(list.size) + + override fun reaction(uuid: String, reaction: UserReaction): Completable = + Completable.complete() + + override fun delete(uuid: String): Completable = Completable.complete() + + private fun getPreview(code: String): SpannableString { + val preview = code.lines(PREVIEW_COUNT).joinToString(separator = newLineChar) + return getHighlighted(preview) + } + + private fun getMockCode(code: String) = SnippetCode(raw = code, highlighted = getPreview(code)) + + private fun getMockLanguage() = SnippetLanguage(raw = "Java", type = SnippetLanguageType.JAVA) + + private val code = + """ + /* Block comment */ + import java.util.Date; + import static AnInterface.CONSTANT; + import static java.util.Date.parse; + import static SomeClass.staticField; + /** + * Doc comment here for SomeClass + * @param T type parameter + * @see Math#sin(double) + */ + @Annotation (name=value) + public class SomeClass { // some comment + private T field = null; + private double unusedField = 12345.67890; + private UnknownType anotherString = "Another\nStrin\g"; + public static int staticField = 0; + public final int instanceFinalField = 0; + + /** + * Semantic highlighting: + * Generated spectrum to pick colors for local variables and parameters: + * Color#1 SC1.1 SC1.2 SC1.3 SC1.4 Color#2 SC2.1 SC2.2 SC2.3 SC2.4 Color#3 + * Color#3 SC3.1 SC3.2 SC3.3 SC3.4 Color#4 SC4.1 SC4.2 SC4.3 SC4.4 Color#5 + * @param param1 + * @param reassignedParam + * @param param2 + * @param param3 + */ + public SomeClass(AnInterface param1, int[] reassignedParam, + int param2 + int param3) { + int reassignedValue = this.staticField + param2 + param3; + long localVar1, localVar2, localVar3, localVar4; + int localVar = "IntelliJ"; // Error, incompatible types + System.out.println(anotherString + toString() + localVar); + long time = parse("1.2.3"); // Method is deprecated + new Thread().countStackFrames(); // Method is deprecated and marked for removal + reassignedValue ++; + field.run(); + new SomeClass() { + { + int a = localVar; + } + }; + reassignedParam = new ArrayList().toArray(new int[CONSTANT]); + } + } + enum AnEnum { CONST1, CONST2 } + interface AnInterface { + int CONSTANT = 2; + void method(); + } + abstract class SomeAbstractClass { + protected int instanceField = staticField; + } + """.trimIndent() +} diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepository.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepository.kt new file mode 100644 index 0000000..4ec5e5d --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepository.kt @@ -0,0 +1,8 @@ +package dev.snipme.snipmeapp.domain.repository.user + +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.user.User + +interface UserRepository { + fun user(): Single +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepositoryReal.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepositoryReal.kt new file mode 100644 index 0000000..281ca0e --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepositoryReal.kt @@ -0,0 +1,18 @@ +package dev.snipme.snipmeapp.domain.repository.user + +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.error.ErrorHandler +import dev.snipme.snipmeapp.domain.user.User +import dev.snipme.snipmeapp.domain.user.toUser +import dev.snipme.snipmeapp.infrastructure.remote.UserService +import dev.snipme.snipmeapp.util.extension.mapError + +class UserRepositoryReal( + private val errorHandler: ErrorHandler, + private val service: UserService +) : UserRepository { + + override fun user(): Single = service.user() + .mapError { errorHandler.handle(it) } + .map { it.toUser() } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetCodeUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetCodeUseCase.kt new file mode 100644 index 0000000..d7d1cf5 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetCodeUseCase.kt @@ -0,0 +1,24 @@ +package dev.snipme.snipmeapp.domain.share + +import android.content.Context +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import androidx.core.content.ContextCompat.startActivity +import dev.snipme.snipmeapp.domain.snippets.Snippet + +class ShareSnippetCodeUseCase( + private val context: Context +) { + + operator fun invoke(snippet: Snippet) { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, snippet.code.raw) + type = "text/plain" + } + + val shareIntent = Intent.createChooser(sendIntent, snippet.title) + shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) + startActivity(context, shareIntent, null) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareUser.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareUser.kt new file mode 100644 index 0000000..7931943 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareUser.kt @@ -0,0 +1,16 @@ +package dev.snipme.snipmeapp.domain.share + +import dev.snipme.snipmeapp.domain.user.User +import dev.snipme.snipmeapp.infrastructure.model.response.SharePersonResponse + +data class ShareUser(val user: User, val isShared: Boolean) + +fun SharePersonResponse.toShareUser() = ShareUser( + isShared = shared ?: throw IllegalArgumentException("Shared param must be defined!"), + user = User( + id = id ?: throw IllegalArgumentException("User must have an id!"), + login = username ?: throw IllegalArgumentException("User must have a login!"), + email = email ?: "", + photo = photo ?: "" + ) +) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt new file mode 100644 index 0000000..f14b94a --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt @@ -0,0 +1,27 @@ +package dev.snipme.snipmeapp.domain.snippet + +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase +import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository +import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility + +class CreateSnippetUseCase( + private val auth: AuthorizationUseCase, + private val networkAvailable: CheckNetworkAvailableUseCase, + private val snippetRepository: SnippetRepository +) { + operator fun invoke( + title: String, + code: String, + language: String, + visibility: SnippetVisibility = SnippetVisibility.PUBLIC + ): Single = auth() + .andThen(networkAvailable()) + .andThen(snippetRepository.create(title, code, language, visibility)) + .flatMap { + snippetRepository.updateListener.onNext(it) + Single.just(it) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/DeleteSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/DeleteSnippetUseCase.kt new file mode 100644 index 0000000..4814d9f --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/DeleteSnippetUseCase.kt @@ -0,0 +1,13 @@ +package dev.snipme.snipmeapp.domain.snippet + +import io.reactivex.Completable +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository +import dev.snipme.snipmeapp.domain.snippets.Snippet + +class DeleteSnippetUseCase(private val repository: SnippetRepository) { + + operator fun invoke(uuid: String): Completable = + repository + .delete(uuid) + .doOnComplete { repository.updateListener.onNext(Snippet.EMPTY.copy(uuid)) } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/EditInteractor.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/EditInteractor.kt new file mode 100644 index 0000000..844994d --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/EditInteractor.kt @@ -0,0 +1,32 @@ +package dev.snipme.snipmeapp.domain.snippet + +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.clipboard.GetFromClipboardUseCase +import dev.snipme.snipmeapp.domain.language.GetLanguagesUseCase +import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility + +class EditInteractor( + private val getLanguages: GetLanguagesUseCase, + private val getSnippet: GetSingleSnippetUseCase, + private val createSnippet: CreateSnippetUseCase, + private val updateSnippet: UpdateSnippetUseCase, + private val fromClipboard: GetFromClipboardUseCase +) { + fun languages() = getLanguages() + + fun snippet(uuid: String) = getSnippet(uuid) + + fun create(title: String, code: String, language: String): Single = + createSnippet(title, code, language) + + fun update( + uuid: String, + title: String, + code: String, + language: String, + visibility: SnippetVisibility, + ): Single = updateSnippet(uuid, title, code, language, visibility) + + fun getFromClipboard(): String? = fromClipboard() +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/GetSingleSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/GetSingleSnippetUseCase.kt new file mode 100644 index 0000000..ad5a47e --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/GetSingleSnippetUseCase.kt @@ -0,0 +1,20 @@ +package dev.snipme.snipmeapp.domain.snippet + +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase +import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository +import dev.snipme.snipmeapp.domain.snippets.Snippet + +class GetSingleSnippetUseCase( + private val auth: AuthorizationUseCase, + private val networkAvailable: CheckNetworkAvailableUseCase, + private val repository: SnippetRepository +) { + + operator fun invoke(uuid: String): Single = + auth() + .andThen(networkAvailable()) + .andThen(repository.snippet(uuid)) + +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveSnippetUpdatesUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveSnippetUpdatesUseCase.kt new file mode 100644 index 0000000..ac7ea4b --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveSnippetUpdatesUseCase.kt @@ -0,0 +1,9 @@ +package dev.snipme.snipmeapp.domain.snippet + +import io.reactivex.Observable +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository +import dev.snipme.snipmeapp.domain.snippets.Snippet + +class ObserveSnippetUpdatesUseCase(private val repository: SnippetRepository) { + operator fun invoke(): Observable = repository.updateListener.share() +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveUpdatedSnippetPageUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveUpdatedSnippetPageUseCase.kt new file mode 100644 index 0000000..e07cdf2 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveUpdatedSnippetPageUseCase.kt @@ -0,0 +1,36 @@ +package dev.snipme.snipmeapp.domain.snippet + +import io.reactivex.Observable +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository +import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.domain.snippets.SnippetScope + +private const val START_PAGE = 1 + +class ObserveUpdatedSnippetPageUseCase(private val repository: SnippetRepository) { + + operator fun invoke(scope: SnippetScope): Observable = + repository.updateListener + .skipWhile { it == Snippet.EMPTY } + .flatMapSingle { updated -> + getPageWithUpdated(scope, updated, START_PAGE) + } + + private fun getPageWithUpdated( + scope: SnippetScope, + updated: Snippet, + page: Int + ): Single = repository.snippets(scope, page) + .map { snippets -> snippets.contains(updated.uuid) } + .flatMap { contains -> + if (contains) { + Single.just(page) + } else { + // Be aware of recursion here + getPageWithUpdated(scope, updated, page + 1) + } + } + + private fun List.contains(uuid: String) = find { it.uuid == uuid } != null +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/SaveSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/SaveSnippetUseCase.kt new file mode 100644 index 0000000..de98bec --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/SaveSnippetUseCase.kt @@ -0,0 +1,20 @@ +package dev.snipme.snipmeapp.domain.snippet + +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility +import java.util.concurrent.TimeUnit + +class SaveSnippetUseCase( + private val createSnippet: CreateSnippetUseCase +) { + operator fun invoke(snippet: Snippet): Single { + if (snippet.isOwner) return Single.just(snippet) + return createSnippet( + snippet.title, + snippet.code.raw, + snippet.language.raw, + visibility = SnippetVisibility.PRIVATE, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt new file mode 100644 index 0000000..c11e020 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt @@ -0,0 +1,27 @@ +package dev.snipme.snipmeapp.domain.snippet + +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase +import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository +import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility + +class UpdateSnippetUseCase( + private val auth: AuthorizationUseCase, + private val networkAvailable: CheckNetworkAvailableUseCase, + private val repository: SnippetRepository +) { + operator fun invoke( + uuid: String, + title: String, + code: String, + language: String, + visibility: SnippetVisibility, + ) = auth() + .andThen(networkAvailable()) + .andThen(repository.update(uuid, title, code, language, visibility)) + .flatMap { + repository.updateListener.onNext(it) + Single.just(it) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetSnippetsUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetSnippetsUseCase.kt new file mode 100644 index 0000000..95480aa --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetSnippetsUseCase.kt @@ -0,0 +1,20 @@ +package dev.snipme.snipmeapp.domain.snippets + +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase +import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository + +class GetSnippetsUseCase( + private val auth: AuthorizationUseCase, + private val repository: SnippetRepository, + private val networkAvailable: CheckNetworkAvailableUseCase +) { + operator fun invoke(scope: SnippetScope, page: Int): Single> = + auth() + .andThen(networkAvailable()) + .andThen( + repository.snippets(scope, page) + .map { list -> list.sortedByDescending { it.modifiedAt.time } } + ) +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/HasMoreSnippetPagesUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/HasMoreSnippetPagesUseCase.kt new file mode 100644 index 0000000..6f2f580 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/HasMoreSnippetPagesUseCase.kt @@ -0,0 +1,31 @@ +package dev.snipme.snipmeapp.domain.snippets + +import io.reactivex.Single +import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase +import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.repository.snippet.SNIPPET_PAGE_SIZE +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository + +private const val NEXT_PAGE = 1 + +class HasMoreSnippetPagesUseCase( + private val auth: AuthorizationUseCase, + private val networkAvailable: CheckNetworkAvailableUseCase, + private val repository: SnippetRepository +) { + + operator fun invoke(scope: SnippetScope, page: Int): Single = + auth() + .andThen(networkAvailable()) + .andThen(repository.count(scope).map { count -> page < pageCountFromOverall(count) }) + + private fun pageCountFromOverall(count: Int): Int { + val nextPageOffset = count % SNIPPET_PAGE_SIZE + val fullPages = count / SNIPPET_PAGE_SIZE + return if (nextPageOffset > 0) { + fullPages + NEXT_PAGE + } else { + fullPages + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/Snippet.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/Snippet.kt new file mode 100644 index 0000000..c5ade79 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/Snippet.kt @@ -0,0 +1,41 @@ +package dev.snipme.snipmeapp.domain.snippets + +import android.text.SpannableString +import dev.snipme.snipmeapp.domain.reaction.UserReaction +import java.util.* + +data class Snippet( + val uuid: String, + val title: String, + val code: SnippetCode, + val language: SnippetLanguage, + val visibility: SnippetVisibility, + val isOwner: Boolean, + val owner: Owner, + val modifiedAt: Date, + val numberOfLikes: Int, + val numberOfDislikes: Int, + val userReaction: UserReaction +) { + companion object { + val EMPTY = Snippet( + uuid = "", + title = "", + code = SnippetCode("", SpannableString("")), + language = SnippetLanguage("", SnippetLanguageType.UNKNOWN), + visibility = SnippetVisibility.PRIVATE, + isOwner = false, + owner = Owner(0, ""), + modifiedAt = Date(), + numberOfLikes = 0, + numberOfDislikes = 0, + userReaction = UserReaction.NONE + ) + } +} + +data class Owner(val id: Int, val login: String) + +data class SnippetCode(val raw: String, val highlighted: SpannableString) + +data class SnippetLanguage(val raw: String, val type: SnippetLanguageType) diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetFilters.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetFilters.kt new file mode 100644 index 0000000..bc7847c --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetFilters.kt @@ -0,0 +1,8 @@ +package dev.snipme.snipmeapp.domain.snippets + +data class SnippetFilters( + val languages: List, + val selectedLanguages: List, + val scopes: List, + val selectedScope: String +) diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetLanguageMapper.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetLanguageMapper.kt new file mode 100644 index 0000000..1dd812b --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetLanguageMapper.kt @@ -0,0 +1,68 @@ +package dev.snipme.snipmeapp.domain.snippets + +import dev.snipme.snipmeapp.domain.snippets.SnippetLanguageType.* + +object SnippetLanguageMapper { + + fun toString(language: SnippetLanguageType): String = + languageMap + .filterValues { it == language } + .keys + .firstOrNull() + .orEmpty() + + fun fromString(language: String?): SnippetLanguageType = + if (language.isNullOrEmpty()) { + UNKNOWN + } else { + languageMap[language] ?: UNKNOWN + } + + val languageMap = mapOf( + "C" to C, + "C++" to CPP, + "Objective-C" to OBJECTIVE_C, + "C#" to C_SHARP, + "Java" to JAVA, + "Kotlin" to KOTLIN, + "Bash" to BASH, + "Python" to PYTHON, + "Perl" to PERL, + "Ruby" to RUBY, + "Swift (Apple programming language)" to SWIFT, + "JavaScript" to JAVASCRIPT, + "CoffeeScript" to COFFEESCRIPT, + "Rust" to RUST, + "BASIC" to BASIC, + "Clojure" to CLOJURE, + "CSS" to CSS, + "Dart" to DART, + "Erlang" to ERLANG, + "Go" to GO, + "Haskell" to HASKELL, + "Lisp" to LISP, + "LLVM" to LLVM, + "Lua" to LUA, + "MATLAB" to MATLAB, + "ML" to ML, + "MUMPS" to MUMPS, + "Nemerle" to NEMERLE, + "Pascal" to PASCAL, + "R" to R, + "RD" to RD, + "Scala" to SCALA, + "SQL" to SQL, + "TeX" to TEX, + "Visual Basic" to VB, + "VHDL" to VHDL, + "Tcl" to TCL, + "XQuery" to XQUERY, + "YAML" to YAML, + "Markdown" to MARKDOWN, + "JSON" to JSON, + "XML" to XML, + "Proto" to PROTO, + "Regex" to REGEX + ) +} + diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetLanguageType.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetLanguageType.kt new file mode 100644 index 0000000..77dd5ab --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetLanguageType.kt @@ -0,0 +1,49 @@ +package dev.snipme.snipmeapp.domain.snippets + +enum class SnippetLanguageType(val fileExtension: String) { + C("c"), + CPP("cpp"), + OBJECTIVE_C("m"), + C_SHARP("cs"), + JAVA("java"), + BASH("sh"), + PYTHON("py"), + PERL("pl"), + RUBY("rb"), + SWIFT("swift"), + JAVASCRIPT("js"), + KOTLIN("kt"), + COFFEESCRIPT("coffee"), + RUST("rs"), + BASIC("cbm"), + CLOJURE("clj"), + CSS("css"), + DART("dart"), + ERLANG("erl"), + GO("go"), + HASKELL("hs"), + LISP("lisp"), + LLVM("llvm"), + LUA("lua"), + MATLAB("matlab"), + ML("ml"), + MUMPS("mumps"), + NEMERLE("nemerle"), + PASCAL("pascal"), + R("r"), + RD("rd"), + SCALA("scala"), + SQL("sql"), + TEX("tex"), + VB("vbs"), + VHDL("vhd"), + TCL("tcl"), + XQUERY("xquery"), + YAML("yml"), + MARKDOWN("md"), + JSON("json"), + XML("xml"), + PROTO("proto"), + REGEX("regex"), + UNKNOWN(""); +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt new file mode 100644 index 0000000..1b69815 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt @@ -0,0 +1,59 @@ +package dev.snipme.snipmeapp.domain.snippets + +import android.text.SpannableString +import dev.snipme.snipmeapp.domain.reaction.UserReaction +import dev.snipme.snipmeapp.infrastructure.model.response.SnippetResponse +import dev.snipme.snipmeapp.util.SyntaxHighlighter.getHighlighted +import dev.snipme.snipmeapp.util.extension.lines +import dev.snipme.snipmeapp.util.extension.newLineChar +import dev.snipme.snipmeapp.util.extension.toDate +import dev.snipme.snipmeapp.util.extension.toSnippetLanguage +import java.util.* + +const val PREVIEW_COUNT = 5 + +class SnippetResponseMapper { + + operator fun invoke(response: SnippetResponse) = with(response) { + return@with Snippet( + uuid = id, + title = title.orEmpty(), + code = getCode(this), + language = getLanguage(language), + visibility = getVisibility(visibility), + isOwner = is_owner ?: false, + owner = Owner(owner?.id ?: 0, owner?.username ?: ""), + modifiedAt = modified_at?.toDate() ?: Date(), + numberOfLikes = number_of_likes ?: 0, + numberOfDislikes = number_of_dislikes ?: 0, + userReaction = getUserReaction(user_reaction) + ) + } + + private fun getUserReaction(value: String?) = + when { + value.equals("like", ignoreCase = true) -> UserReaction.LIKE + value.equals("dislike", ignoreCase = true) -> UserReaction.DISLIKE + else -> UserReaction.NONE + } + + private fun getCode(response: SnippetResponse) = SnippetCode( + raw = response.code.orEmpty(), + highlighted = getPreview(response.code.orEmpty()) + ) + + private fun getLanguage(language: String?) = SnippetLanguage( + raw = language.orEmpty(), + type = language.toSnippetLanguage() + ) + + private fun getPreview(code: String): SpannableString { + val preview = code.lines(PREVIEW_COUNT).joinToString(separator = newLineChar) + return getHighlighted(preview) + } + + private fun getVisibility(visibility: String?): SnippetVisibility { + if (visibility == null) return SnippetVisibility.PRIVATE + return SnippetVisibility.valueOf(visibility) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetScope.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetScope.kt new file mode 100644 index 0000000..be5c944 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetScope.kt @@ -0,0 +1,7 @@ +package dev.snipme.snipmeapp.domain.snippets + +enum class SnippetScope { + ALL, PUBLIC, OWNED, SHARED_FOR; +} + +fun SnippetScope.value() = this.name.toLowerCase() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetVisibility.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetVisibility.kt new file mode 100644 index 0000000..ff41c28 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetVisibility.kt @@ -0,0 +1,5 @@ +package dev.snipme.snipmeapp.domain.snippets + +enum class SnippetVisibility { + PUBLIC, PRIVATE +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/user/GetSingleUserUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/user/GetSingleUserUseCase.kt new file mode 100644 index 0000000..566912e --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/user/GetSingleUserUseCase.kt @@ -0,0 +1,18 @@ +package dev.snipme.snipmeapp.domain.user + +import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase +import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.repository.user.UserRepository + +class GetSingleUserUseCase( + private val auth: AuthorizationUseCase, + private val networkAvailable: CheckNetworkAvailableUseCase, + private val repository: UserRepository +) { + + operator fun invoke() = + auth() + .andThen(networkAvailable()) + .andThen(repository.user()) + +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/user/User.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/user/User.kt new file mode 100644 index 0000000..7ade0b0 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/user/User.kt @@ -0,0 +1,18 @@ +package dev.snipme.snipmeapp.domain.user + +import androidx.annotation.VisibleForTesting +import dev.snipme.snipmeapp.infrastructure.model.response.PersonResponse + +data class User(val id: Int, val login: String, val email: String, val photo: String) { + companion object { + @VisibleForTesting + fun mock() = User(0, "test login", "test email", "test photo") + } +} + +fun PersonResponse.toUser() = User( + id = id ?: throw IllegalArgumentException("User must have an id!"), + login = username ?: throw IllegalArgumentException("User must have a login!"), + email = email ?: "", + photo = photo ?: "" +) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AuthPreferences.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AuthPreferences.kt new file mode 100644 index 0000000..fae5b0c --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AuthPreferences.kt @@ -0,0 +1,17 @@ +package dev.snipme.snipmeapp.infrastructure.local + +import dev.snipme.snipmeapp.util.PreferencesUtil + +private const val TOKEN_KEY = "7e3a40ea-32d2-4248-bcfb-297f2f41246e" + +class AuthPreferences(private val prefs: PreferencesUtil) { + fun saveToken(token: String) { + prefs.save(TOKEN_KEY, token) + } + + fun getToken(): String? = prefs.get(TOKEN_KEY) + + fun clearToken() { + prefs.remove(TOKEN_KEY) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/SnippetPageResponse.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/SnippetPageResponse.kt new file mode 100644 index 0000000..8a669c4 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/SnippetPageResponse.kt @@ -0,0 +1,12 @@ +package dev.snipme.snipmeapp.infrastructure.model + +import com.squareup.moshi.JsonClass +import dev.snipme.snipmeapp.infrastructure.model.response.SnippetResponse + +@JsonClass(generateAdapter = true) +data class SnippetPageResponse( + val count: Int, + val next: String?, + val previous: String?, + val results: List +) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/CreateSnippetRequest.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/CreateSnippetRequest.kt new file mode 100644 index 0000000..a3412ab --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/CreateSnippetRequest.kt @@ -0,0 +1,11 @@ +package dev.snipme.snipmeapp.infrastructure.model.request + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class CreateSnippetRequest( + val title: String, + val code: String, + val language: String, + val visibility: String +) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/IdentifyUserRequest.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/IdentifyUserRequest.kt new file mode 100644 index 0000000..b7246ec --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/IdentifyUserRequest.kt @@ -0,0 +1,6 @@ +package dev.snipme.snipmeapp.infrastructure.model.request + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class IdentifyUserRequest(val username: String) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/LoginUserRequest.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/LoginUserRequest.kt new file mode 100644 index 0000000..4dc20da --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/LoginUserRequest.kt @@ -0,0 +1,6 @@ +package dev.snipme.snipmeapp.infrastructure.model.request + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class LoginUserRequest(val username: String, val password: String) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/RateSnippetRequest.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/RateSnippetRequest.kt new file mode 100644 index 0000000..dacbcab --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/RateSnippetRequest.kt @@ -0,0 +1,6 @@ +package dev.snipme.snipmeapp.infrastructure.model.request + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RateSnippetRequest(val id: String, val reaction: String) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/RegisterUserRequest.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/RegisterUserRequest.kt new file mode 100644 index 0000000..cda564d --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/RegisterUserRequest.kt @@ -0,0 +1,6 @@ +package dev.snipme.snipmeapp.infrastructure.model.request + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RegisterUserRequest(val username: String, val password: String, val email: String) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/ShareSnippetRequest.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/ShareSnippetRequest.kt new file mode 100644 index 0000000..5d011f6 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/request/ShareSnippetRequest.kt @@ -0,0 +1,10 @@ +package dev.snipme.snipmeapp.infrastructure.model.request + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ShareSnippetRequest( + val access_rights: String? = null, // In future use SnippetAccess + val snippet: String, + val allowed_user: Int +) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/LanguageResponse.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/LanguageResponse.kt new file mode 100644 index 0000000..b302c7c --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/LanguageResponse.kt @@ -0,0 +1,6 @@ +package dev.snipme.snipmeapp.infrastructure.model.response + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class LanguageResponse(val name: String) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/PersonResponse.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/PersonResponse.kt new file mode 100644 index 0000000..4d4aad1 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/PersonResponse.kt @@ -0,0 +1,20 @@ +package dev.snipme.snipmeapp.infrastructure.model.response + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class PersonResponse( + val id: Int?, + val username: String?, + val email: String?, + val photo: String? +) + +@JsonClass(generateAdapter = true) +data class SharePersonResponse( + val id: Int?, + val username: String?, + val email: String?, + val photo: String?, + val shared: Boolean? +) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/RegisterUserResponse.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/RegisterUserResponse.kt new file mode 100644 index 0000000..3e2f976 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/RegisterUserResponse.kt @@ -0,0 +1,20 @@ +package dev.snipme.snipmeapp.infrastructure.model.response + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RegisterUserResponse( + val id: Int?, + val username: String?, + val password: String?, + val email: String?, + val first_name: String?, + val last_name: String?, + val date_joined: String?, + val groups: List?, + val is_active: Boolean?, + val is_staff: Boolean?, + val is_superuser: Boolean?, + val last_login: Any?, + val user_permissions: List? +) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/SnippetResponse.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/SnippetResponse.kt new file mode 100644 index 0000000..6864079 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/SnippetResponse.kt @@ -0,0 +1,22 @@ +package dev.snipme.snipmeapp.infrastructure.model.response + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SnippetResponse( + val id: String, + val title: String?, + val code: String?, + val created_at: String?, + val modified_at: String?, + val visibility: String?, + val owner: OwnerResponse?, + val is_owner: Boolean?, + val language: String?, + val number_of_likes: Int?, + val number_of_dislikes: Int?, + val user_reaction: String? +) + +@JsonClass(generateAdapter = true) +data class OwnerResponse(val id: Int, val username: String) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/TokenResponse.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/TokenResponse.kt new file mode 100644 index 0000000..098ab33 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/model/response/TokenResponse.kt @@ -0,0 +1,6 @@ +package dev.snipme.snipmeapp.infrastructure.model.response + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TokenResponse(val token: String) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/AuthService.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/AuthService.kt new file mode 100644 index 0000000..a99525c --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/AuthService.kt @@ -0,0 +1,21 @@ +package dev.snipme.snipmeapp.infrastructure.remote + +import io.reactivex.Single +import dev.snipme.snipmeapp.infrastructure.model.request.IdentifyUserRequest +import dev.snipme.snipmeapp.infrastructure.model.request.LoginUserRequest +import dev.snipme.snipmeapp.infrastructure.model.request.RegisterUserRequest +import dev.snipme.snipmeapp.infrastructure.model.response.RegisterUserResponse +import dev.snipme.snipmeapp.infrastructure.model.response.TokenResponse +import retrofit2.http.* + +interface AuthService { + + @POST("check-if-user-exist/") + fun identify(@Body request: IdentifyUserRequest): Single + + @POST("auth-token/") + fun login(@Body request: LoginUserRequest): Single + + @POST("register/") + fun register(@Body request: RegisterUserRequest): Single +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/LanguageService.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/LanguageService.kt new file mode 100644 index 0000000..d7426c4 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/LanguageService.kt @@ -0,0 +1,12 @@ +package dev.snipme.snipmeapp.infrastructure.remote + +import io.reactivex.Single +import dev.snipme.snipmeapp.infrastructure.model.response.LanguageResponse +import retrofit2.http.GET + +interface LanguageService { + + @GET("language/") + fun languages(): Single> + +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/ShareService.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/ShareService.kt new file mode 100644 index 0000000..ea6ae7a --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/ShareService.kt @@ -0,0 +1,19 @@ +package dev.snipme.snipmeapp.infrastructure.remote + +import io.reactivex.Completable +import io.reactivex.Single +import dev.snipme.snipmeapp.infrastructure.model.request.ShareSnippetRequest +import dev.snipme.snipmeapp.infrastructure.model.response.SharePersonResponse +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +interface ShareService { + + @POST("share/") + fun share(@Body request: ShareSnippetRequest): Completable + + @GET("person-autocomplete/") + fun shareUsers(@Query("snippet_id") snippetUuid: String): Single> +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/SnippetService.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/SnippetService.kt new file mode 100644 index 0000000..8ce8dfa --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/SnippetService.kt @@ -0,0 +1,39 @@ +package dev.snipme.snipmeapp.infrastructure.remote + +import io.reactivex.Completable +import io.reactivex.Single +import dev.snipme.snipmeapp.infrastructure.model.SnippetPageResponse +import dev.snipme.snipmeapp.infrastructure.model.request.CreateSnippetRequest +import dev.snipme.snipmeapp.infrastructure.model.request.RateSnippetRequest +import dev.snipme.snipmeapp.infrastructure.model.response.SnippetResponse +import retrofit2.http.* + +private const val PATH_ID = "id" + +interface SnippetService { + + @GET("snippet/") + fun snippets( + @Query("scope") scope: String, + @Query("offset") offset: Int, + @Query("limit") limit: Int + ): Single + + @GET("snippet/{$PATH_ID}/") + fun snippet(@Path(PATH_ID) id: String): Single + + @POST("snippet/") + fun create(@Body request: CreateSnippetRequest): Single + + @PUT("snippet/{$PATH_ID}/") + fun update( + @Path(PATH_ID) id: String, + @Body request: CreateSnippetRequest + ): Single + + @POST("snippet-rate/") + fun rate(@Body request: RateSnippetRequest): Completable + + @DELETE("snippet/{$PATH_ID}/") + fun delete(@Path(PATH_ID) id: String): Completable +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/UserService.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/UserService.kt new file mode 100644 index 0000000..b856e69 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/remote/UserService.kt @@ -0,0 +1,11 @@ +package dev.snipme.snipmeapp.infrastructure.remote + +import io.reactivex.Single +import dev.snipme.snipmeapp.infrastructure.model.response.PersonResponse +import retrofit2.http.GET + +interface UserService { + + @GET("person/") + fun user(): Single +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/util/AuthInterceptor.kt b/app/src/main/java/dev/snipme/snipmeapp/util/AuthInterceptor.kt new file mode 100644 index 0000000..f0154b0 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/util/AuthInterceptor.kt @@ -0,0 +1,24 @@ +package dev.snipme.snipmeapp.util + +import okhttp3.Interceptor +import okhttp3.Response +import dev.snipme.snipmeapp.BuildConfig +import dev.snipme.snipmeapp.infrastructure.local.AuthPreferences +import timber.log.Timber + +private const val KEY_HEADER_AUTHORIZATION = "Authorization" + +class AuthInterceptor(private val authPreferences: AuthPreferences) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val token = authPreferences.getToken() ?: "" + Timber.i("Authorization token = $token") + val newRequest = chain + .request() + .newBuilder() + + if (token.isNotBlank()) + newRequest.addHeader(KEY_HEADER_AUTHORIZATION, "Token $token") + + return chain.proceed(newRequest.build()) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/util/CrashReportingTree.kt b/app/src/main/java/dev/snipme/snipmeapp/util/CrashReportingTree.kt new file mode 100644 index 0000000..21e2b06 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/util/CrashReportingTree.kt @@ -0,0 +1,9 @@ +package dev.snipme.snipmeapp.util + +import timber.log.Timber + +class CrashReportingTree : Timber.Tree() { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + // Don't show logs for user + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/util/PreferencesUtil.kt b/app/src/main/java/dev/snipme/snipmeapp/util/PreferencesUtil.kt new file mode 100644 index 0000000..6fabaa4 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/util/PreferencesUtil.kt @@ -0,0 +1,39 @@ +package dev.snipme.snipmeapp.util + +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.content.SharedPreferences + +private const val PREFS_FILENAME = "5ee122cc-6fc4-11eb-9439-0242ac130002" + +@Suppress("UNCHECKED_CAST") +class PreferencesUtil(context: Context) { + + private val pref: SharedPreferences = context.getSharedPreferences(PREFS_FILENAME, MODE_PRIVATE) + + fun save(key: String, value: T) { + val editor = pref.edit() + when(value) { + is Boolean -> editor.putBoolean(key, value) + is Int -> editor.putInt(key, value) + is String -> editor.putString(key, value) + else -> Unit + } + editor.apply() + } + + fun get(key: String): T? = try { + pref.all[key] as T? + } catch (e: Exception) { + null + } + + fun remove(key: String) { + pref.edit().remove(key).apply() + } + + fun clear() { + pref.edit().clear().apply() + } + +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/util/SyntaxHighlighter.kt b/app/src/main/java/dev/snipme/snipmeapp/util/SyntaxHighlighter.kt new file mode 100644 index 0000000..79046af --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/util/SyntaxHighlighter.kt @@ -0,0 +1,240 @@ +package dev.snipme.snipmeapp.util + +import android.graphics.Color +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import androidx.core.text.set +import dev.snipme.snipmeapp.BuildConfig +import dev.snipme.snipmeapp.util.SyntaxPhrases.commentTerminators +import dev.snipme.snipmeapp.util.SyntaxPhrases.keywords +import dev.snipme.snipmeapp.util.SyntaxPhrases.multilineCommentTerminators +import dev.snipme.snipmeapp.util.SyntaxPhrases.textTerminators +import dev.snipme.snipmeapp.util.SyntaxPhrases.wordTerminators +import dev.snipme.snipmeapp.util.extension.containsDefault +import dev.snipme.snipmeapp.util.extension.indicesOf +import dev.snipme.snipmeapp.util.extension.lengthToEOF +import dev.snipme.snipmeapp.util.extension.opaque +import timber.log.Timber +import java.util.* + +data class SyntaxWindowTheme( + val number: Int, + val note: Int, + val numberBackground: Int, + val contentBackground: Int +) + +data class SyntaxTheme( + val code: Int = 0x808080, + val keyword: Int = 0xCC7832, + val text: Int = 0x6A8759, + val literal: Int = 0x6897BB, // Number, bool, null etc + val comment: Int = 0x909090, + val metadata: Int = 0xBBB529, + val multilineComment: Int = 0x629755 +) + +private const val START_INDEX = 0 +private const val SINGLE_CHAR = 1 +private const val TWO_ELEMENTS = 2 + +// TODO +// 1. Handle annotations (metadata starts with @) +object SyntaxHighlighter { + + fun getHighlighted( + code: String, + theme: SyntaxTheme = SyntaxTheme(), + ignoreError: Boolean = false + ): SpannableString = + try { + SpannableString(code).apply { + Timber.d("--- START CODE HIGHLIGHTING ---") + highlightKeywords(code, theme) + highlightTexts(code, theme) + highlightNumbers(code, theme) + highlightComments(code, theme) + Timber.d("--- END CODE HIGHLIGHTING ---") + } + } catch (e: Exception) { + Timber.d("--- ERROR CODE HIGHLIGHTING ---") + Timber.e("Error: $e") + if (BuildConfig.DEBUG && ignoreError.not()) { + val errorPhrase = "ERROR = $e \nRAW CODE: \n" + SpannableString(errorPhrase + code).apply { + setSpan(ForegroundColorSpan(Color.RED), START_INDEX, errorPhrase.length, 0) + } + } else { + SpannableString(code) + } + } + + private fun Spannable.applyColor(start: Int, end: Int, color: Int) { + this[start..end] = ForegroundColorSpan(color) + } + + private fun Spannable.highlightKeywords(code: String, theme: SyntaxTheme) { + val keywordsMap = mutableMapOf>() + findKeywords(code).forEach { keyword -> + val indices = code + .indicesOf(keyword) + .filter { isIndexOnlyKeyword(code, it) } + + keywordsMap[keyword] = indices + } + + debugKeywords(keywordsMap) + + for ((word, indices) in keywordsMap) { + indices.forEach { index -> + applyColor(start = index, end = index + word.length, color = theme.keyword.opaque) + } + } + } + + private fun Spannable.highlightTexts(code: String, theme: SyntaxTheme) { + findTextSubstring(code) + .also { debugTexts(code, it) } + .forEach { substring -> + val (start, end) = substring + applyColor(start, end + SINGLE_CHAR, theme.text.opaque) + } + } + + private fun Spannable.highlightNumbers(code: String, theme: SyntaxTheme) { + findDigitIndices(code) + .also { debugNumbers(code, it) } + .forEach { index -> + applyColor(start = index, end = index + SINGLE_CHAR, color = theme.literal.opaque) + } + } + + private fun Spannable.highlightComments(code: String, theme: SyntaxTheme) { + val hasSingle = commentTerminators.any { code.containsDefault(it) } + val hasMultiline = multilineCommentTerminators.any { + code.containsDefault(it.first) && code.containsDefault(it.second) + } + + if (hasSingle) this.highlightSingleComment(code, theme) + if (hasMultiline) this.highlightMultiComment(code, theme) + } + + private fun Spannable.highlightSingleComment(code: String, theme: SyntaxTheme) { + val indices = mutableListOf() + commentTerminators.forEach { terminator -> + indices.addAll(code.indicesOf(terminator)) + } + + debugComments(code, indices) + + indices.forEach { startIndex -> + val end = startIndex + code.lengthToEOF(startIndex) + applyColor(start = startIndex, end = end, color = theme.comment.opaque) + } + } + + private fun Spannable.highlightMultiComment(code: String, theme: SyntaxTheme) { + findCommentSubstring(code) + .also { debugMultilineComment(code, it) } + .forEach { commentBlock -> + val (start, end) = commentBlock + applyColor(start = start, end = end + SINGLE_CHAR, color = theme.multilineComment.opaque) + } + } + + private fun findCommentSubstring(code: String): List> { + val comments = mutableListOf>() + val startIndices = mutableListOf() + val endIndices = mutableListOf() + + multilineCommentTerminators.forEach { commentBlock -> + val (prefix, postfix) = commentBlock + startIndices.addAll(code.indicesOf(prefix)) + endIndices.addAll(code.indicesOf(postfix).map { it + (postfix.lastIndex) }) + } + + val endIndex = minOf(startIndices.size, endIndices.size) - 1 + for (i in START_INDEX..endIndex) { + comments.add(Pair(startIndices[i], endIndices[i])) + } + + return comments + } + + private fun findDigitIndices(code: String): List { + val indices = mutableListOf() + code.forEach { char -> + if (char.isDigit()) indices.add(code.indexOf(char)) + } + return indices + } + + private fun findTextSubstring(code: String): List> { + val texts = mutableListOf>() + val textIndices = mutableListOf() + textTerminators.forEach { + textIndices += code.indicesOf(it) + } + + for (i in START_INDEX..textIndices.lastIndex step TWO_ELEMENTS) { + texts.add(Pair(textIndices[i], textIndices[i + 1])) + } + + return texts + } + + private fun findKeywords(code: String): List = + code.split(*wordTerminators, ignoreCase = true) // Split into words + .filter { it.isNotBlank() } // Remove empty + .map { it.trim() } // Remove whitespaces from phrase + .map { it.toLowerCase(Locale.getDefault()) } // Standardize + .filter { it in keywords } // Get supported + + // Sometimes keyword can be found in the middle of word. + // This returns information if index points only to the keyword + private fun isIndexOnlyKeyword(code: String, index: Int): Boolean { + if (index == START_INDEX) return true + if (index == code.lastIndex) return true + + val charBefore = code[index - 1].toString() + return charBefore in wordTerminators || charBefore.first().isLetter().not() + } + + // TODO Correct ranges and make debug print more safe! + + private fun debugKeywords(keywords: Map>) { + keywords.forEach { entry -> + Timber.d("KEYWORD = ${entry.key}, AT = ${entry.value}") + } + } + + private fun debugTexts(phrase: String, substrings: List>) { + substrings.forEach { range -> + val start = range.first + val end = range.second + Timber.d("TEXT = ${phrase.substring(start..end)}, AT = $start") + } + } + + private fun debugNumbers(phrase: String, indices: List) { + indices.forEach { index -> + Timber.d("NUMBER = ${phrase[index]}, AT = $index") + } + } + + private fun debugComments(phrase: String, indices: List) { + indices.forEach { start -> + val end = start + phrase.lengthToEOF(start) + Timber.d("COMMENT = ${phrase.substring(start until end)}, AT = $start") + } + } + + private fun debugMultilineComment(phrase: String, substrings: List>) { + substrings.forEach { range -> + val start = range.first + val end = range.second + Timber.d("COMMENT = ${phrase.substring(start..end)}, AT = $start") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/util/SyntaxPhrases.kt b/app/src/main/java/dev/snipme/snipmeapp/util/SyntaxPhrases.kt new file mode 100644 index 0000000..35e5bd2 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/util/SyntaxPhrases.kt @@ -0,0 +1,43 @@ +package dev.snipme.snipmeapp.util + +object SyntaxPhrases { + val wordTerminators = arrayOf(" ", ",", ".", ":", "(", ")", "=", "{", "}", "<", ">", "\r", "\n") + val textTerminators = arrayOf("\"", "\'", "\"\"\"") + val commentTerminators = arrayOf("//", "#") + val multilineCommentTerminators = arrayOf(Pair("/*", "*/"), Pair("\'\'\'", "\'\'\'")) + + val keywords: List = + """#available #column #define #defined #elif #else #else#elseif #endif #error #file #function + #if #ifdef #ifndef #include #line #pragma #selector #undef abstract add after alias + alignas alignof and and_eq andalso as ascending asm assert associatedtype associativity + async atomic_cancel atomic_commit atomic_noexcept auto await base become begin bitand + bitor bnot bor box break bsl bsr bxor case catch chan + bool boolean byte char char16_t char32_t decimal double enum float + checked class compl concept cond const const_cast constexpr continue convenience + covariant crate debugger decltype def default defer deferred defined? deinit + del delegate delete descending didset div do dynamic dynamic_cast dynamictype + elif else elseif elsif end ensure eval event except explicit export extends extension + extern external factory fallthrough false final finally fixed fn for foreach friend + from fun func function get global go goto group guard if impl implements implicit import + in indirect infix init inline inout instanceof int interface internal into is join lambda + lazy left let library local lock long loop macro map match mod module move mut mutable + mutating namespace native new next nil noexcept none nonlocal nonmutating not not_eq + null nullptr object of offsetof operator optional or or_eq orderby orelse out override + package params part partial pass postfix precedence prefix priv private proc protected + protocol pub public pure raise range readonly receive redo ref register reinterpret_cast + rem remove repeat required requires rescue rethrow rethrows retry return right sbyte + sealed select self set short signed sizeof stackalloc static static_assert static_cast + strictfp string struct subscript super switch sync synchronized template then this + thread_local throw throws trait transaction_safe transaction_safe_dynamic transient + true try type typealias typedef typeid typename typeof uint ulong unchecked undef + union unless unowned unsafe unsigned unsized until use ushort using value var virtual + void volatile wchar_t weak when where while willset with xor xor_eq xorauto yield + yieldabstract yieldarguments val list override get set as as? in !in !is is by + constructor delegate dynamic field file init param property receiver setparam data + data expect lateinit crossinline companion annotation actual noinline open reified + suspend tailrec vararg it constraint alter column table all any asc backup database + between check create index replace view procedure unique desc distinct drop exec + exists foreign key full outer having inner insert like limit order primary rownum + top truncate update values""" + .split(" ") +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/util/extension/CollectionExtensions.kt b/app/src/main/java/dev/snipme/snipmeapp/util/extension/CollectionExtensions.kt new file mode 100644 index 0000000..9165e40 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/util/extension/CollectionExtensions.kt @@ -0,0 +1,8 @@ +package dev.snipme.snipmeapp.util.extension + +import dev.snipme.snipmeapp.domain.snippets.SnippetLanguageType +import dev.snipme.snipmeapp.domain.snippets.SnippetLanguageMapper + +fun CharSequence.lines(count: Int) = lines().take(count) + +fun String?.toSnippetLanguage(): SnippetLanguageType = SnippetLanguageMapper.fromString(this) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/util/extension/NetworkExtensions.kt b/app/src/main/java/dev/snipme/snipmeapp/util/extension/NetworkExtensions.kt new file mode 100644 index 0000000..6e96ce0 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/util/extension/NetworkExtensions.kt @@ -0,0 +1,10 @@ +package dev.snipme.snipmeapp.util.extension + +import retrofit2.HttpException + +val HttpException.errorMessage: String? get() = + try { + response()?.errorBody()?.string() + } catch (e: Exception) { + null + } diff --git a/app/src/main/java/dev/snipme/snipmeapp/util/extension/NumberExtensions.kt b/app/src/main/java/dev/snipme/snipmeapp/util/extension/NumberExtensions.kt new file mode 100644 index 0000000..0db4018 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/util/extension/NumberExtensions.kt @@ -0,0 +1,19 @@ +package dev.snipme.snipmeapp.util.extension + +import android.graphics.Color + +val Int.maxAlpha get() = 255 + +val Int.red get() = Color.red(this) + +val Int.green get() = Color.green(this) + +val Int.blue get() = Color.blue(this) + +val Int.rgb get() = Color.rgb(red, green, blue) + +val Int.opaque: Int + get() = Color.argb(maxAlpha, red, green, blue) + +fun Int.transparent(alpha: Float) = Color.argb((maxAlpha * alpha).toInt(), red, green, blue) + diff --git a/app/src/main/java/dev/snipme/snipmeapp/util/extension/RxExtensions.kt b/app/src/main/java/dev/snipme/snipmeapp/util/extension/RxExtensions.kt new file mode 100644 index 0000000..85e64bf --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/util/extension/RxExtensions.kt @@ -0,0 +1,19 @@ +package dev.snipme.snipmeapp.util.extension + +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.disposables.Disposable + +fun Disposable?.inProgress() = (this != null && !isDisposed) + +fun Single.mapError(mapper: (Throwable) -> Throwable) = + this.onErrorResumeNext { Single.error(mapper(it)) } + +fun Completable.mapError(mapper: (Throwable) -> Throwable) = + this.onErrorResumeNext { Completable.error(mapper(it)) } + +fun Single>.mapItems(mapper: (T) -> E): Single> = + this.map { items -> items.map { mapper(it) } } + +fun Single>.filterItems(condition: (T) -> Boolean): Single> = + this.map { items -> items.filter { condition(it) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/util/extension/TextExtensions.kt b/app/src/main/java/dev/snipme/snipmeapp/util/extension/TextExtensions.kt new file mode 100644 index 0000000..1737799 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/util/extension/TextExtensions.kt @@ -0,0 +1,37 @@ +package dev.snipme.snipmeapp.util.extension + +import java.text.SimpleDateFormat +import java.util.* + +const val newLineChar = "\n" + +fun CharSequence.containsDefault(other: CharSequence) = + this.contains(other, ignoreCase = true) + +fun String.indicesOf(phrase: String, ignoreCase: Boolean = true): List { + val indices = mutableListOf() + var index: Int = indexOf(string = phrase, ignoreCase = ignoreCase) + while (index >= 0) { + indices += index + index = indexOf(string = phrase, startIndex = index + 1, ignoreCase = ignoreCase) + } + return indices +} + +fun Char.isNewLine(): Boolean { + val stringChar = this.toString() + return stringChar == "\n" || stringChar == "\r" || stringChar == "\r\n" +} + +fun String.lengthToEOF(start: Int = 0): Int { + if (all { it.isNewLine().not() }) return length - start + var endIndex = start + while (this.getOrNull(endIndex)?.isNewLine()?.not() == true) { endIndex++ } + return endIndex - start +} + +fun String.toDate(): Date { + val iso8061Pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + val sdf = SimpleDateFormat(iso8061Pattern, Locale.getDefault()) + return sdf.parse(this) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..10560bea54ae9b76938c9a5e0f25c674445d0fc3 GIT binary patch literal 18091 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 D9%35z literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..cd378c8 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..5f8292a016d3b747073e6b62a537e6ae33e18499 GIT binary patch literal 1720 zcmV;p21ogcP)PC%NreeEoG)HP>_~#YwIW-rls_@x?F$voOIZSwr8f(GiQ#ol0Rp9&OUo)eO-I) zwfC8sFd<gU{IWA-hy19?3|v|jYB=o`_dI1@fI zK0ADv@gN_=GUG-$rih9M1_l@+qPSPkm*PMmkP_;vW#f?XDIJxd!~XvMJ~s(I37`|= zq7ewzbCNVp#<9D*`(*=;{$Gf9iC=wPU0qyPS2ruvXG<1roYdLb`L$aCiTZkbd-=kJ z3!C9&0P622uJRHuTtocc3gRP$#N9pS zb~eiyfB>iLEb-fmiC;-mn!SMdz!Bo!UUPdJ6?;H{bE0AhPTDHs{NIT)*Af?mKp3`I_APgtK$XZAIW~PF(SR&?fleuk- z6&pY$WyG&7Bz`qr<(iK-5I0;gw{w8(2h=CSXYXO+SqoGIojFJB``6sgA+i?`a!b)^ z;yGDrSZ&%t+!NbbYBbpg2nqLix#XNh3eJ*j;>K&{b`Fz0fDk!=^F}ZsFIuZ+E4`fq zWe=c>e-XbGj2ytZe#_j(iUKnA*Iyz&c}5LYB>O54aq|tSy1UKoqab?#q3{Eo*Jb8v zzG=Qbk`R68FB5OvrgGQp43*$#rK=-mLClEhpD1|x{bQ6pfX-i1&cUg&YU|bUU{Wt7 zes}rM5(K3IY9|zzh)O`hd9-P|Zti1@vIh_nG=>BI%UF==*N4-kM&b`vDgf^YkRSIF zpQ|Un(L#KyjreAZ8oozMh*;Nxp!fxZb(gWxzkc(`MWil7;nnMwic4n#8eB?9 z)qPi%4*l9SIEM{?EMAQ*TWtL6eGDhuUs@%_r_7FpCr5?kOhDi7REy8-?})eOspn&d zkEvaOExBsT1aP3wbJWvVy^rApK;>uEy;wkFA+5Iwkuw2dmeQwmU09!O*rLh+{r=H} zrzgfPz`ozi*F)q?K&L8+x9%Zc^_AKIn7@p8o+xdlI)@_Bb3U@Cu8qyAPr#Y8SnUSr zZ9?QsKoEuk@&h3B^aRzb&P7^1_2KXk4-fjfB7f^iz zaYjf&Uh$-BLd{}`CJ=8@CkrwYilEqpw`yb9Jl|V zuDrZ_2YihPP-$uDT%$9Aq`yJj3clbcnSnrn^YZdC;A>S?)g+z6u*T{Kt`CWxYHDga zi%>*|(zz=HU*J=9b+t!)dVd4Jb;^G6LDk>_v z@3sE7)i&eNXYjZmpB+BH7x)C;SH?^nLwH((KK0(DbnfV@htcd^u>Bo4#Pm(4N6$T5|*&Iu>Sy!$wxLb1!0B& O0000K+C#N#Bf8_~O?h0BHs(r&ixir5RU1tT z+p?jI(k-$)=56vQugfrEqtLGIUvSUoa~_}1`FzhgpY!>C&bfg_{{qp_*HBVYf}A*x z^4#={ZwlVJX&XQ3!jzOir%s@boVnyXHF7ibZ~DC(d95KOmbkzBYe-wD)e{ zd~G%Rd$*W27yHnVq$+F*9F?J?dIWZ?!z7{;Nw6@}(E(^sAVhP=o!hvFCkmIA53Ur6 z7Hg#^+up}iykDn`0&f-tQ`_#2++C`ls*@(VWp1|rzmmo{lY36%hL2I+#B(HpBKm|) zGy$z%c8zrB%$~Cp()E#1Ktdt`Et65-@lM^s>DQ=Hq4GQ(oC#~KeGBcx(Ha}*AwN8d zvwP~WR(C71bneB9@`I;bFJU%W(JPb>{))~MtRF~z=0(R^R~xA+_j_joX>mSrPgEUm z#=}#~cL2pBTd@UEoLLri%?8@|C$U1}7awk!=rygdKi~Y(E3Vg`7%$OA1T3E%c+_n% z71m#`k-A_Mp@KHDMH2rj4dy{sQTkOq#C%qepb)A{qPc-@W{^ynjsXJd=x6&6j;YXE z8scB&t#Mb#Z~;}gNT|2U$?&Y+T})j&2yFxm47l`4tpK#O0Hg(Nl!<*USHHMUc0$GW zA2(-ciRNu4tsozAUBOP;&Lyw4)fh=w9(r=9*5y!aN59MJ%GuyA2ZYoKy* zFwD&o7vkOp#1FNT ziM?vd2Z~95L~h7Msk3%d+bbZ`&;ITcx&s06gy9vcexzv6@bn7x;a>Pvlwk~L6_eOD z8`c|^lZ8`|+WARcT!*q@FTW=R^B21A5e9!3dR(%Mpd1eNJfMWvnz=;=^`(R`Ib^}* zX}(l!f}#rxV-@~$c+Z+vb9jk5H;Hq)t;Yt__bi6DUJ=y)ihRm}in6FRZf5Ft{tLTYr@QbfFfq5h}^SQ4(LUOE6x(>EU_T#iI~BexQ!FFn0L- z9d`g>%nfSq*#hx4Xd#(A1)JIhWovio?5pVcgkK&hm>)(q23U>1Gh2}mDtBLH@#J|x zEU5!3Z_}l7p;r^dFy`B6hGJU|z0{f30>Zf?vP?js#Ti$7RXMwaS2w8%gA92YA>lB+ zrt4|xja{1fafDanWG{P0?*M0~YDr8{HJAankxBxA^-BY%t=B65p-i&(Oe>OG|Kta~ zekJ%Eey{I^%f!)L1LVK05KVpUDQcK%1#zU_GUu)y-N)kQQ|3(QHet9U2!eS$WV$Rn zm`KAK-&_>AGJhu}h?30qm!J{wvWoBK+v+XxBUQ6`I~|S7^7E$uDg0HFb8zRrPnla~ zK;l{A8FKu{d5`Sn{@fOdAAvi3Y1{~57EPcRs+3g~?y3p!Qt8(1 zZ&;XWSSMBaO&>iF9KrK=K4H_4J$n`}sA*MfkK@Z7MXFE!V0@_c*80c|UhtSZ5AaTz zb_iG0Ud=6R%bS;`{ji)~-9qPeRLv%EhA#0%c9&hHyezjblpy+8or8h0oJZMUbh=f^ zNtX6vVf3QTId1ldZ_#pmjjc8kGPQnCN+8j5bzxtDlFR%-zQA*1tj0nc3D=B}+4&hY z-{GqZqdl}E4_UIaG8C#gkE#rQHjOo_651N0jt0{&M*on?ziq!6S7GwWje86qR=g9{4%gxgTYgK#31uOelKluc)JG(}2qBmehL cDJcab70Tf0VXQ33D*PMj#4$9g0(m~=Ulw_$U;qFB literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..312d7ee2aa426e7a58e9573a55b062b5b5fcbca6 GIT binary patch literal 3605 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..32ab45222e05f281d8319cf318ca6eab3d104df6 GIT binary patch literal 1162 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 UVvE}StpET307*qoM6N<$f}QJrHvj+t literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..68b4fe344e52b46ebfbed495beb00ef028e88759 GIT binary patch literal 2346 zcmV+_3Dx$AP)$T6F+?im_D3utnkU8|G@ zM_iTTT){8!@bEAbks$)~5d6Vl@MgPaNr&ZTAQ1SxWY825esE}LC@jYz0}VyA7=$)r zz{$G0x*P1;B^B01!r|}+lzho7B$*AByn=ThW z0*7JuD481sOPjN2&mJ!=-0OLTN}oZq^2>s-5r z2}R`OE9&d(ZN=m|;}^PXrAC;v|3JJ%5JuUA!)o<7rX zwE-yI?=&dUVE`J~<9_0<^O;r8UkcY6Fw)gLMSRyo(Q0R7m1>anw3&=64X_4T_dXI^ z=N~@qOq=Pr&H(&9`Un@j9W=OJsPlDPXMjG!MakcHKs^IPX57bEDGULh`(0o?LPonTT94Y)4wrNCgXw$6FIbm1xkFoU^c5Am{lRQ>;tcbsYB z1ydNmSP?7is`aXxLA!GBeHr8(CVuN(;@4`3D_M|4cPQYTiq`P9Qda*ja%@f0p-uf_G}(3C2l$G&T7b$ z2E2JtEz;XfC|D_N!4~4We-U5kR%WBO}`llNToz02Cf@U=^SpGEH3rzhAg|>N@!E~CCail#JvDmzbdr?@ zSR6f}+5pP{Oj9Q_0#AgjHsI7pYDe||pMXu~37Al@_=39MG5}K;dYa1t?F<%v&<)mq zHx`~U;8Pi3UF?sq6nCDBoiQy~r8WcT>EZ?J4}W_@4fwFK7w?R!XAHm$>Z2Rgoc{LL zh#QU(cbro~pk!KW!L1)lZ>N@c#oE{g7(Jt8>VI@$@kJ3_3#0H|zW54)XAHm^fPLex z1-?0h(v8Lzi!%D}!Gn*bW*r#2_N+$z_WOy?v^&%2c*X#1XVHZn518|;l>E*VOh-V+ zoqMEBiI|L~(4&pcbUL0f0QLXNKGm1U=3NM&&jmpo)X}K>w`^CYVaJ4it;+=(VqVGq z1pY>BhqtkN_A{VCRJ;K4w-wY;vZH2V=lxfWaPw#}-Nq`YZ#`qe`?& z*!@Np&-^LzO=QM^I2h^e)W$Zrz?#T|0sZ~X^Wwo6Fb>cMdq?eE&AT5We)q7N|G_Sc zz9jy05MzJ_Cgk=XRI{b1^D)C2Gss$-+ReMh02`>sAvmg<)$56$sEDoe(a%q$t9dDm z0XDE%3tLpI2{k(_*Qx&`L_a54i~$;0GkET=(Vcbq!u=akU-B)C0i%E?0n3%8e;|J0 zW#ab#I@6>IW56V1447n$0h5d|V3IKgOfohDf+;m%@7}%NAY;I|*q?y%^78LNyObEP zW5o=y&pFjVG{{H?C%qvVT4YYwqe!stI z-n@Ca&`t_(;szipD=YDmvd=X(Hf|1w!+2Fw2>IxlXFH0zJG_dGO zE^(=%^L?q7@|Q1PzO102U}-jjdxr+1#Sfqfw25dmJ)&7k13jXv z^YZe(D3K?hZxX&H;C)%y3^ce2nm`-*eDxHiRayeBqEY^AZf@=uX3d)QrI|BletE`> z89CVq?j0IH3uqG2Cgml6v_K~4-=dyFrr8WMfEE)Xya^K~OqeiX!h|>Ce<1HhMPRFL QaR2}S07*qoM6N<$f?b+_egFUf literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..f96799ff7e5390a654e1d03985fc982826ab8473 GIT binary patch literal 2588 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..2f70d9760b2657850038f3b0ff23babb3852f2d1 GIT binary patch literal 5197 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 Hu0mjfap4)^ literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..3abcef65ae1f8bd831e0ba1b44fd35a10745aea3 GIT binary patch literal 3673 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 ZtMW-3IcB%=4_8JGOS4Ou8WY#p{{RgU&w~H} literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..dc568e5429b454b4958b53370472ddb9e5dbf55c GIT binary patch literal 4524 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 QF93k$HQ3ckGq;%k10!kMvH$=8 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..bde9fd4d7524459d20317b4e8046ea5043ebacbd GIT binary patch literal 8374 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 M07*qoM6N<$f|Pqx;{X5v literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..fce04a63b9414cdba18f3ad1955937ea3c96887b GIT binary patch literal 5186 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 SxCQF_QIzD=WUJqqhx|Ws5O;3? literal 0 HcmV?d00001 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000..6e78273 --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + Twoje hasło powinno mieć co najmniej 8 znaków, zawierać małe i duże litery oraz składać się z co najmniej jednej cyfry. + Minimalna długość hasła powinna składać się z 8 znaków. + Hasło powinno zawierać co najmniej jeden znak specjalny np. !@#$%. + Hasło powinno zawierać co najmniej jedną małą i dużą literę. + Hasło powinno zawierać co najmniej jedną cyfrę. + Wpisane hasła różnią się od siebie. + + Minimalna długość maila powinna składać się z 5 znaków. + Odpowiedni email powinien zawierać przykładową strukturę: abd@def.xyz + + Wpisana fraza jest zbyt krótka. + Wpisana fraza jest zbyt długa. + Fraza powinna zawierać przynajmniej jedną małą i dużą literę. + Fraza nie może zawierać liter takich jak: ąćĻłćńļĶł†ęŌ. + + Proszę wypełnić wszystkie pola formularza. + + + Coś poszło nie tak. Spróbuj jeszcze raz! + Sprawdź swoje połączenie z internetem i spróbuj ponownie. + Nieznany język programowania. Proszę wybrać język z listy. + Twoja sesja wygasła! Zaloguj się ponownie. + Nie masz dostępu do tego zasobu! + Mamy problemy po naszej stronie. Powiadom nas o problemie. + Użytkownik z tym adresem email jest już zarejestrowany. Użyj innego adresu. + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..c22830e --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,44 @@ + + + SnipMe + + + + + + + + + + + + + Your password should be at least 8 characters, has upper and lower case letters and at least one digit + Min length of the password is 8 characters. + Password should contain at least one special char. e.g. !@#$%. + Password should contain at least one lower and upper letter. + Password should contain at least one digit. + The passwords are not identical. + + Min length of the email is 5 characters. + Proper email should have abd@def.xyz structure. + + The phrase you have input is too short. + The phrase you have input is too long. + The phrase should have at least one lower and upper letter. + The phrase cannot have letters like ąćĻłćńļĶł†ęŌ. + + Please fill in all fields in the form. + + + Something went wrong. Try again! + Please check your connection to the internet and try again. + This programming language is unknown. Please choose one from list. + Your session has expired! Please log in again. + You have no access to this resource! + We have some issues. Please let us know about the problem. + User with this email is already registered. Please use another email or log in. + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..8b570f1 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..d97c26b --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,7 @@ + + + + clients3.google.com + 91.195.93.3 + + \ No newline at end of file diff --git a/app/src/test/java/dev/snipme/snipmeapp/ExampleUnitTest.kt b/app/src/test/java/dev/snipme/snipmeapp/ExampleUnitTest.kt new file mode 100644 index 0000000..61b692d --- /dev/null +++ b/app/src/test/java/dev/snipme/snipmeapp/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package dev.snipme.snipmeapp + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..dd78b7b --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,8 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false + id("com.google.devtools.ksp") version "2.0.21-1.0.25" apply false +} + diff --git a/flutter_module/.gitignore b/flutter_module/.gitignore new file mode 100644 index 0000000..525e05c --- /dev/null +++ b/flutter_module/.gitignore @@ -0,0 +1,49 @@ +.DS_Store +.dart_tool/ + +.pub/ + +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +migrate_working_dir/ + +*.swp +profile + +DerivedData/ + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +build/ +.android/ +.ios/ +.flutter-plugins +.flutter-plugins-dependencies + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/flutter_module/.metadata b/flutter_module/.metadata new file mode 100644 index 0000000..163e1c3 --- /dev/null +++ b/flutter_module/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49" + channel: "stable" + +project_type: module diff --git a/flutter_module/README.md b/flutter_module/README.md new file mode 100644 index 0000000..6ee3cc2 --- /dev/null +++ b/flutter_module/README.md @@ -0,0 +1,11 @@ +# flutter_module + +A new Flutter module project. + +## Getting Started + +For help getting started with Flutter development, view the online +[documentation](https://flutter.dev/). + +For instructions integrating Flutter modules to your existing applications, +see the [add-to-app documentation](https://flutter.dev/docs/development/add-to-app). diff --git a/flutter_module/analysis_options.yaml b/flutter_module/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/flutter_module/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/flutter_module/assets/images/illustrations/app_logo.png b/flutter_module/assets/images/illustrations/app_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8a0bce37b64b4397dffb2e45e57eb1341540c84d GIT binary patch literal 2046 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 literal 0 HcmV?d00001 diff --git a/flutter_module/bridge/main_model.dart b/flutter_module/bridge/main_model.dart new file mode 100644 index 0000000..0cc8750 --- /dev/null +++ b/flutter_module/bridge/main_model.dart @@ -0,0 +1,219 @@ +import 'package:pigeon/pigeon.dart'; + +// General + +class Snippet { + String? uuid; + String? title; + SnippetCode? code; + SnippetLanguage? language; + Owner? owner; + bool? isOwner; + String? timeAgo; + int? voteResult; + UserReaction? userReaction; + bool? isPrivate; + bool? isLiked; + bool? isDisliked; + bool? isSaved; + bool? isToDelete; +} + +class SnippetCode { + String? raw; + List? tokens; +} + +class SyntaxToken { + int? start; + int? end; + int? color; +} + +class SnippetLanguage { + String? raw; + SnippetLanguageType? type; +} + +class Owner { + int? id; + String? login; +} + +enum SnippetLanguageType { + c, + cpp, + objective_c, + c_sharp, + java, + bash, + python, + perl, + ruby, + swift, + javascript, + kotlin, + coffeescript, + rust, + basic, + clojure, + css, + dart, + erlang, + go, + haskell, + lisp, + llvm, + lua, + matlab, + ml, + mumps, + nemerle, + pascal, + r, + rd, + scala, + sql, + tex, + vb, + vhdl, + tcl, + xquery, + yaml, + markdown, + json, + xml, + proto, + regex, + unknown +} + +enum SnippetFilterType { all, mine, shared } + +class SnippetFilter { + List? languages; + List? selectedLanguages; + List? scopes; + String? selectedScope; +} + +enum UserReaction { + none, + like, + dislike +} + +// State + +enum ModelState { loading, loaded, error } + +enum MainModelEvent { none, alert, logout } + +enum DetailModelEvent { none, saved, deleted } + +enum LoginModelEvent { none, logged } + +class MainModelStateData { + ModelState? state; + bool? is_loading; + List? data; + SnippetFilter? filter; + String? error; + int? oldHash; + int? newHash; +} + +class MainModelEventData { + MainModelEvent? event; + String? message; + int? oldHash; + int? newHash; +} + +class DetailModelStateData { + ModelState? state; + bool? is_loading; + Snippet? data; + String? error; + int? oldHash; + int? newHash; +} + +class DetailModelEventData { + DetailModelEvent? event; + String? value; + int? oldHash; + int? newHash; +} + +class LoginModelStateData { + ModelState? state; + bool? is_loading; + int? oldHash; + int? newHash; +} + +class LoginModelEventData { + LoginModelEvent? event; + int? oldHash; + int? newHash; +} + +// Api + +@HostApi() +abstract class MainModelBridge { + MainModelStateData getState(); + + MainModelEventData getEvent(); + + void resetEvent(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + void initState(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + void filterLanguage(String language, bool isSelected); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + void filterScope(String scope); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + void logOut(); +} + +@HostApi() +abstract class DetailModelBridge { + DetailModelStateData getState(); + + DetailModelEventData getEvent(); + + void resetEvent(); + + void load(String uuid); + + void like(); + + void dislike(); + + void save(); + + void copyToClipboard(); + + void share(); + + void delete(); +} + +@HostApi() +abstract class LoginModelBridge { + LoginModelStateData getState(); + + LoginModelEventData getEvent(); + + void loginOrRegister(String email, String password); + + void checkLoginState(); + + void resetEvent(); +} \ No newline at end of file diff --git a/flutter_module/fonts/Kanit-Regular.ttf b/flutter_module/fonts/Kanit-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ef204c1b65c2bed16ab38e4bc68872ad15f7809b GIT binary patch literal 169744 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≥A=9^h*Bz1kvEwMBz! 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

#6?UC9ica$uYtTr69?2z3dx-v*Kob)6 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 literal 0 HcmV?d00001 diff --git a/flutter_module/lib/generated/assets.dart b/flutter_module/lib/generated/assets.dart new file mode 100644 index 0000000..f355e6e --- /dev/null +++ b/flutter_module/lib/generated/assets.dart @@ -0,0 +1,10 @@ +///This file is automatically generated. DO NOT EDIT, all your changes would be lost. +class Assets { + Assets._(); + + static const String appLogo = 'assets/images/illustrations/app_logo.png'; + static const String reactionDislike = 'assets/images/icons/reaction_dislike.png'; + static const String reactionLike = 'assets/images/icons/reaction_like.png'; + static const String reactionUndefined = 'assets/images/icons/reaction_undefined.png'; + +} diff --git a/flutter_module/lib/main.dart b/flutter_module/lib/main.dart new file mode 100644 index 0000000..5382aad --- /dev/null +++ b/flutter_module/lib/main.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_module/model/main_model.dart'; +import 'package:flutter_module/presentation/navigation/details/details_navigator.dart'; +import 'package:flutter_module/presentation/navigation/login/login_navigator.dart'; +import 'package:flutter_module/presentation/screens//main_screen.dart'; +import 'package:flutter_module/presentation/screens/details_screen.dart'; +import 'package:flutter_module/presentation/screens/login_screen.dart'; +import 'package:flutter_module/utils/extensions/text_extensions.dart'; +import 'package:go_router_plus/go_router_plus.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + MyApp({Key? key}) : super(key: key); + + final loginModel = LoginModelBridge(); + final mainModel = MainModelBridge(); + final detailModel = DetailModelBridge(); + + @override + Widget build(BuildContext context) { + final loginNavigator = LoginNavigator(); + final detailsNavigator = DetailsNavigator(); + final router = createGoRouter( + screens: [ + LoginScreen( + navigator: loginNavigator, + model: loginModel, + ), + MainScreen( + loginNavigator: loginNavigator, + detailsNavigator: detailsNavigator, + model: mainModel, + ), + DetailsScreen( + navigator: detailsNavigator, + model: detailModel, + ) + ], + redirectors: [ + ScreenRedirector(), + AuthRedirector( + state: loginNavigator, + guestRedirectPath: LoginScreen.name.route, + userRedirectPath: MainScreen.name.route, + ) + ], + refreshNotifiers: [loginNavigator], + ); + + return MaterialApp.router( + title: 'SnipMe', + theme: ThemeData(primarySwatch: Colors.blue), + // TODO Use theme tailor + routeInformationProvider: router.routeInformationProvider, + routeInformationParser: router.routeInformationParser, + routerDelegate: router.routerDelegate, + ); + } +} diff --git a/flutter_module/lib/model/main_model.dart b/flutter_module/lib/model/main_model.dart new file mode 100644 index 0000000..541c16d --- /dev/null +++ b/flutter_module/lib/model/main_model.dart @@ -0,0 +1,1258 @@ +// Autogenerated from Pigeon (v4.2.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +enum SnippetLanguageType { + c, + cpp, + objective_c, + c_sharp, + java, + bash, + python, + perl, + ruby, + swift, + javascript, + kotlin, + coffeescript, + rust, + basic, + clojure, + css, + dart, + erlang, + go, + haskell, + lisp, + llvm, + lua, + matlab, + ml, + mumps, + nemerle, + pascal, + r, + rd, + scala, + sql, + tex, + vb, + vhdl, + tcl, + xquery, + yaml, + markdown, + json, + xml, + proto, + regex, + unknown, +} + +enum SnippetFilterType { + all, + mine, + shared, +} + +enum UserReaction { + none, + like, + dislike, +} + +enum ModelState { + loading, + loaded, + error, +} + +enum MainModelEvent { + none, + alert, + logout, +} + +enum DetailModelEvent { + none, + saved, + deleted, +} + +enum LoginModelEvent { + none, + logged, +} + +class Snippet { + Snippet({ + this.uuid, + this.title, + this.code, + this.language, + this.owner, + this.isOwner, + this.timeAgo, + this.voteResult, + this.userReaction, + this.isPrivate, + this.isLiked, + this.isDisliked, + this.isSaved, + this.isToDelete, + }); + + String? uuid; + String? title; + SnippetCode? code; + SnippetLanguage? language; + Owner? owner; + bool? isOwner; + String? timeAgo; + int? voteResult; + UserReaction? userReaction; + bool? isPrivate; + bool? isLiked; + bool? isDisliked; + bool? isSaved; + bool? isToDelete; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['uuid'] = uuid; + pigeonMap['title'] = title; + pigeonMap['code'] = code?.encode(); + pigeonMap['language'] = language?.encode(); + pigeonMap['owner'] = owner?.encode(); + pigeonMap['isOwner'] = isOwner; + pigeonMap['timeAgo'] = timeAgo; + pigeonMap['voteResult'] = voteResult; + pigeonMap['userReaction'] = userReaction?.index; + pigeonMap['isPrivate'] = isPrivate; + pigeonMap['isLiked'] = isLiked; + pigeonMap['isDisliked'] = isDisliked; + pigeonMap['isSaved'] = isSaved; + pigeonMap['isToDelete'] = isToDelete; + return pigeonMap; + } + + static Snippet decode(Object message) { + final Map pigeonMap = message as Map; + return Snippet( + uuid: pigeonMap['uuid'] as String?, + title: pigeonMap['title'] as String?, + code: pigeonMap['code'] != null + ? SnippetCode.decode(pigeonMap['code']!) + : null, + language: pigeonMap['language'] != null + ? SnippetLanguage.decode(pigeonMap['language']!) + : null, + owner: pigeonMap['owner'] != null + ? Owner.decode(pigeonMap['owner']!) + : null, + isOwner: pigeonMap['isOwner'] as bool?, + timeAgo: pigeonMap['timeAgo'] as String?, + voteResult: pigeonMap['voteResult'] as int?, + userReaction: pigeonMap['userReaction'] != null + ? UserReaction.values[pigeonMap['userReaction']! as int] + : null, + isPrivate: pigeonMap['isPrivate'] as bool?, + isLiked: pigeonMap['isLiked'] as bool?, + isDisliked: pigeonMap['isDisliked'] as bool?, + isSaved: pigeonMap['isSaved'] as bool?, + isToDelete: pigeonMap['isToDelete'] as bool?, + ); + } +} + +class SnippetCode { + SnippetCode({ + this.raw, + this.tokens, + }); + + String? raw; + List? tokens; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['raw'] = raw; + pigeonMap['tokens'] = tokens; + return pigeonMap; + } + + static SnippetCode decode(Object message) { + final Map pigeonMap = message as Map; + return SnippetCode( + raw: pigeonMap['raw'] as String?, + tokens: (pigeonMap['tokens'] as List?)?.cast(), + ); + } +} + +class SyntaxToken { + SyntaxToken({ + this.start, + this.end, + this.color, + }); + + int? start; + int? end; + int? color; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['start'] = start; + pigeonMap['end'] = end; + pigeonMap['color'] = color; + return pigeonMap; + } + + static SyntaxToken decode(Object message) { + final Map pigeonMap = message as Map; + return SyntaxToken( + start: pigeonMap['start'] as int?, + end: pigeonMap['end'] as int?, + color: pigeonMap['color'] as int?, + ); + } +} + +class SnippetLanguage { + SnippetLanguage({ + this.raw, + this.type, + }); + + String? raw; + SnippetLanguageType? type; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['raw'] = raw; + pigeonMap['type'] = type?.index; + return pigeonMap; + } + + static SnippetLanguage decode(Object message) { + final Map pigeonMap = message as Map; + return SnippetLanguage( + raw: pigeonMap['raw'] as String?, + type: pigeonMap['type'] != null + ? SnippetLanguageType.values[pigeonMap['type']! as int] + : null, + ); + } +} + +class Owner { + Owner({ + this.id, + this.login, + }); + + int? id; + String? login; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['id'] = id; + pigeonMap['login'] = login; + return pigeonMap; + } + + static Owner decode(Object message) { + final Map pigeonMap = message as Map; + return Owner( + id: pigeonMap['id'] as int?, + login: pigeonMap['login'] as String?, + ); + } +} + +class SnippetFilter { + SnippetFilter({ + this.languages, + this.selectedLanguages, + this.scopes, + this.selectedScope, + }); + + List? languages; + List? selectedLanguages; + List? scopes; + String? selectedScope; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['languages'] = languages; + pigeonMap['selectedLanguages'] = selectedLanguages; + pigeonMap['scopes'] = scopes; + pigeonMap['selectedScope'] = selectedScope; + return pigeonMap; + } + + static SnippetFilter decode(Object message) { + final Map pigeonMap = message as Map; + return SnippetFilter( + languages: (pigeonMap['languages'] as List?)?.cast(), + selectedLanguages: (pigeonMap['selectedLanguages'] as List?)?.cast(), + scopes: (pigeonMap['scopes'] as List?)?.cast(), + selectedScope: pigeonMap['selectedScope'] as String?, + ); + } +} + +class MainModelStateData { + MainModelStateData({ + this.state, + this.is_loading, + this.data, + this.filter, + this.error, + this.oldHash, + this.newHash, + }); + + ModelState? state; + bool? is_loading; + List? data; + SnippetFilter? filter; + String? error; + int? oldHash; + int? newHash; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['state'] = state?.index; + pigeonMap['is_loading'] = is_loading; + pigeonMap['data'] = data; + pigeonMap['filter'] = filter?.encode(); + pigeonMap['error'] = error; + pigeonMap['oldHash'] = oldHash; + pigeonMap['newHash'] = newHash; + return pigeonMap; + } + + static MainModelStateData decode(Object message) { + final Map pigeonMap = message as Map; + return MainModelStateData( + state: pigeonMap['state'] != null + ? ModelState.values[pigeonMap['state']! as int] + : null, + is_loading: pigeonMap['is_loading'] as bool?, + data: (pigeonMap['data'] as List?)?.cast(), + filter: pigeonMap['filter'] != null + ? SnippetFilter.decode(pigeonMap['filter']!) + : null, + error: pigeonMap['error'] as String?, + oldHash: pigeonMap['oldHash'] as int?, + newHash: pigeonMap['newHash'] as int?, + ); + } +} + +class MainModelEventData { + MainModelEventData({ + this.event, + this.message, + this.oldHash, + this.newHash, + }); + + MainModelEvent? event; + String? message; + int? oldHash; + int? newHash; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['event'] = event?.index; + pigeonMap['message'] = message; + pigeonMap['oldHash'] = oldHash; + pigeonMap['newHash'] = newHash; + return pigeonMap; + } + + static MainModelEventData decode(Object message) { + final Map pigeonMap = message as Map; + return MainModelEventData( + event: pigeonMap['event'] != null + ? MainModelEvent.values[pigeonMap['event']! as int] + : null, + message: pigeonMap['message'] as String?, + oldHash: pigeonMap['oldHash'] as int?, + newHash: pigeonMap['newHash'] as int?, + ); + } +} + +class DetailModelStateData { + DetailModelStateData({ + this.state, + this.is_loading, + this.data, + this.error, + this.oldHash, + this.newHash, + }); + + ModelState? state; + bool? is_loading; + Snippet? data; + String? error; + int? oldHash; + int? newHash; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['state'] = state?.index; + pigeonMap['is_loading'] = is_loading; + pigeonMap['data'] = data?.encode(); + pigeonMap['error'] = error; + pigeonMap['oldHash'] = oldHash; + pigeonMap['newHash'] = newHash; + return pigeonMap; + } + + static DetailModelStateData decode(Object message) { + final Map pigeonMap = message as Map; + return DetailModelStateData( + state: pigeonMap['state'] != null + ? ModelState.values[pigeonMap['state']! as int] + : null, + is_loading: pigeonMap['is_loading'] as bool?, + data: pigeonMap['data'] != null + ? Snippet.decode(pigeonMap['data']!) + : null, + error: pigeonMap['error'] as String?, + oldHash: pigeonMap['oldHash'] as int?, + newHash: pigeonMap['newHash'] as int?, + ); + } +} + +class DetailModelEventData { + DetailModelEventData({ + this.event, + this.value, + this.oldHash, + this.newHash, + }); + + DetailModelEvent? event; + String? value; + int? oldHash; + int? newHash; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['event'] = event?.index; + pigeonMap['value'] = value; + pigeonMap['oldHash'] = oldHash; + pigeonMap['newHash'] = newHash; + return pigeonMap; + } + + static DetailModelEventData decode(Object message) { + final Map pigeonMap = message as Map; + return DetailModelEventData( + event: pigeonMap['event'] != null + ? DetailModelEvent.values[pigeonMap['event']! as int] + : null, + value: pigeonMap['value'] as String?, + oldHash: pigeonMap['oldHash'] as int?, + newHash: pigeonMap['newHash'] as int?, + ); + } +} + +class LoginModelStateData { + LoginModelStateData({ + this.state, + this.is_loading, + this.oldHash, + this.newHash, + }); + + ModelState? state; + bool? is_loading; + int? oldHash; + int? newHash; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['state'] = state?.index; + pigeonMap['is_loading'] = is_loading; + pigeonMap['oldHash'] = oldHash; + pigeonMap['newHash'] = newHash; + return pigeonMap; + } + + static LoginModelStateData decode(Object message) { + final Map pigeonMap = message as Map; + return LoginModelStateData( + state: pigeonMap['state'] != null + ? ModelState.values[pigeonMap['state']! as int] + : null, + is_loading: pigeonMap['is_loading'] as bool?, + oldHash: pigeonMap['oldHash'] as int?, + newHash: pigeonMap['newHash'] as int?, + ); + } +} + +class LoginModelEventData { + LoginModelEventData({ + this.event, + this.oldHash, + this.newHash, + }); + + LoginModelEvent? event; + int? oldHash; + int? newHash; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['event'] = event?.index; + pigeonMap['oldHash'] = oldHash; + pigeonMap['newHash'] = newHash; + return pigeonMap; + } + + static LoginModelEventData decode(Object message) { + final Map pigeonMap = message as Map; + return LoginModelEventData( + event: pigeonMap['event'] != null + ? LoginModelEvent.values[pigeonMap['event']! as int] + : null, + oldHash: pigeonMap['oldHash'] as int?, + newHash: pigeonMap['newHash'] as int?, + ); + } +} + +class _MainModelBridgeCodec extends StandardMessageCodec{ + const _MainModelBridgeCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is MainModelEventData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else + if (value is MainModelStateData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else + if (value is Owner) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else + if (value is Snippet) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else + if (value is SnippetCode) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else + if (value is SnippetFilter) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else + if (value is SnippetLanguage) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else + if (value is SyntaxToken) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else +{ + super.writeValue(buffer, value); + } + } + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return MainModelEventData.decode(readValue(buffer)!); + + case 129: + return MainModelStateData.decode(readValue(buffer)!); + + case 130: + return Owner.decode(readValue(buffer)!); + + case 131: + return Snippet.decode(readValue(buffer)!); + + case 132: + return SnippetCode.decode(readValue(buffer)!); + + case 133: + return SnippetFilter.decode(readValue(buffer)!); + + case 134: + return SnippetLanguage.decode(readValue(buffer)!); + + case 135: + return SyntaxToken.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + + } + } +} + +class MainModelBridge { + /// Constructor for [MainModelBridge]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + MainModelBridge({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _MainModelBridgeCodec(); + + Future getState() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MainModelBridge.getState', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as MainModelStateData?)!; + } + } + + Future getEvent() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MainModelBridge.getEvent', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as MainModelEventData?)!; + } + } + + Future resetEvent() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MainModelBridge.resetEvent', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future initState() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MainModelBridge.initState', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future filterLanguage(String arg_language, bool arg_isSelected) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MainModelBridge.filterLanguage', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_language, arg_isSelected]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future filterScope(String arg_scope) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MainModelBridge.filterScope', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_scope]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future logOut() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.MainModelBridge.logOut', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _DetailModelBridgeCodec extends StandardMessageCodec{ + const _DetailModelBridgeCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is DetailModelEventData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else + if (value is DetailModelStateData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else + if (value is Owner) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else + if (value is Snippet) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else + if (value is SnippetCode) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else + if (value is SnippetLanguage) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else + if (value is SyntaxToken) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else +{ + super.writeValue(buffer, value); + } + } + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return DetailModelEventData.decode(readValue(buffer)!); + + case 129: + return DetailModelStateData.decode(readValue(buffer)!); + + case 130: + return Owner.decode(readValue(buffer)!); + + case 131: + return Snippet.decode(readValue(buffer)!); + + case 132: + return SnippetCode.decode(readValue(buffer)!); + + case 133: + return SnippetLanguage.decode(readValue(buffer)!); + + case 134: + return SyntaxToken.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + + } + } +} + +class DetailModelBridge { + /// Constructor for [DetailModelBridge]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + DetailModelBridge({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _DetailModelBridgeCodec(); + + Future getState() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DetailModelBridge.getState', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as DetailModelStateData?)!; + } + } + + Future getEvent() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DetailModelBridge.getEvent', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as DetailModelEventData?)!; + } + } + + Future resetEvent() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DetailModelBridge.resetEvent', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future load(String arg_uuid) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DetailModelBridge.load', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_uuid]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future like() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DetailModelBridge.like', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future dislike() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DetailModelBridge.dislike', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future save() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DetailModelBridge.save', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future copyToClipboard() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DetailModelBridge.copyToClipboard', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future share() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DetailModelBridge.share', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future delete() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DetailModelBridge.delete', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _LoginModelBridgeCodec extends StandardMessageCodec{ + const _LoginModelBridgeCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is LoginModelEventData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else + if (value is LoginModelStateData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else +{ + super.writeValue(buffer, value); + } + } + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return LoginModelEventData.decode(readValue(buffer)!); + + case 129: + return LoginModelStateData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + + } + } +} + +class LoginModelBridge { + /// Constructor for [LoginModelBridge]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + LoginModelBridge({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _LoginModelBridgeCodec(); + + Future getState() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LoginModelBridge.getState', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as LoginModelStateData?)!; + } + } + + Future getEvent() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LoginModelBridge.getEvent', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as LoginModelEventData?)!; + } + } + + Future loginOrRegister(String arg_email, String arg_password) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LoginModelBridge.loginOrRegister', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_email, arg_password]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future checkLoginState() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LoginModelBridge.checkLoginState', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future resetEvent() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LoginModelBridge.resetEvent', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} diff --git a/flutter_module/lib/presentation/navigation/details/details_navigator.dart b/flutter_module/lib/presentation/navigation/details/details_navigator.dart new file mode 100644 index 0000000..f937a15 --- /dev/null +++ b/flutter_module/lib/presentation/navigation/details/details_navigator.dart @@ -0,0 +1,17 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_module/presentation/navigation/screen_navigator.dart'; +import 'package:flutter_module/presentation/screens/details_screen.dart'; +import 'package:flutter_module/utils/extensions/text_extensions.dart'; +import 'package:go_router/go_router.dart'; + +class DetailsNavigator extends ScreenNavigator { + String? _snippetId; + + String? get snippetId => _snippetId; + + void goToDetails(BuildContext context, String snippetId) { + _snippetId = snippetId; + router.push(DetailsScreen.name.route); + } +} diff --git a/flutter_module/lib/presentation/navigation/login/login_navigator.dart b/flutter_module/lib/presentation/navigation/login/login_navigator.dart new file mode 100644 index 0000000..8f90286 --- /dev/null +++ b/flutter_module/lib/presentation/navigation/login/login_navigator.dart @@ -0,0 +1,20 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_module/presentation/navigation/screen_navigator.dart'; +import 'package:go_router_plus/go_router_plus.dart'; + +class LoginNavigator extends ScreenNavigator with ChangeNotifier implements LoggedInState { + bool _loggedIn = false; + + @override + bool get loggedIn => _loggedIn; + + void login() { + _loggedIn = true; + notifyListeners(); + } + + void logout() { + _loggedIn = false; + notifyListeners(); + } +} diff --git a/flutter_module/lib/presentation/navigation/screen_navigator.dart b/flutter_module/lib/presentation/navigation/screen_navigator.dart new file mode 100644 index 0000000..d86fdf5 --- /dev/null +++ b/flutter_module/lib/presentation/navigation/screen_navigator.dart @@ -0,0 +1,14 @@ +import 'package:go_router/go_router.dart'; + +abstract class ScreenNavigator { + + late GoRouter router; + + void setRouter(GoRouter router) { + this.router = router; + } + + void back() { + router.pop(); + } +} diff --git a/flutter_module/lib/presentation/screens/details_screen.dart b/flutter_module/lib/presentation/screens/details_screen.dart new file mode 100644 index 0000000..52ee903 --- /dev/null +++ b/flutter_module/lib/presentation/screens/details_screen.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_module/model/main_model.dart'; +import 'package:flutter_module/presentation/navigation/details/details_navigator.dart'; +import 'package:flutter_module/presentation/screens/named_screen.dart'; +import 'package:flutter_module/presentation/styles/color_styles.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; +import 'package:flutter_module/presentation/styles/padding_styles.dart'; +import 'package:flutter_module/presentation/widgets/code_text_view.dart'; +import 'package:flutter_module/presentation/widgets/no_overscroll_single_child_scroll_view.dart'; +import 'package:flutter_module/presentation/widgets/snippet_action_bar.dart'; +import 'package:flutter_module/presentation/widgets/snippet_details_bar.dart'; +import 'package:flutter_module/presentation/widgets/view_state_wrapper.dart'; +import 'package:flutter_module/utils/extensions/state_extensions.dart'; +import 'package:flutter_module/utils/hooks/use_navigator.dart'; +import 'package:flutter_module/utils/hooks/use_observable_state_hook.dart'; +import 'package:go_router/go_router.dart'; + +class DetailsScreen extends NamedScreen { + DetailsScreen({ + required this.navigator, + required this.model, + }) : super(name); + + static String name = 'details'; + + final DetailsNavigator navigator; + final DetailModelBridge model; + + @override + Widget builder(BuildContext context, GoRouterState state) { + return _DetailsPage( + navigator: navigator, + model: model, + ); + } + + @override + build(BuildContext context, GoRouterState state) { + // TODO: implement build + throw UnimplementedError(); + } +} + +class _DetailsPage extends HookWidget { + const _DetailsPage({ + Key? key, + required this.navigator, + required this.model, + }) : super(key: key); + + final DetailsNavigator navigator; + final DetailModelBridge model; + + @override + Widget build(BuildContext context) { + useNavigator([navigator]); + + final state = useObservableState( + DetailModelStateData(), + () => model.getState(), + (current, newState) => (current as DetailModelStateData).equals(newState), + ).value; + + final event = useObservableState( + DetailModelEventData(), + () => model.getEvent(), + (current, newState) => (current as DetailModelEventData).equals(newState), + ).value; + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (event.event == DetailModelEvent.saved) { + final snippetId = event.value; + if (snippetId == null) { + _exit(); + return; + } + + _exit(); + WidgetsBinding.instance.addPostFrameCallback((_) { + navigator.goToDetails(context, snippetId); + }); + } + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (event.event == DetailModelEvent.deleted) { + _exit(); + } + }); + + useEffect(() { + model.load(navigator.snippetId ?? ''); + return null; + }, []); + + return Scaffold( + backgroundColor: ColorStyles.surfacePrimary(), + appBar: AppBar( + title: Text(state.data?.title ?? ''), + backgroundColor: ColorStyles.surfacePrimary(), + foregroundColor: Colors.black, + elevation: 0, + leading: BackButton( + onPressed: navigator.back, + color: Colors.black, + ), + actions: state.data?.isPrivate == true + ? [const PaddingStyles.regular(Icon(Icons.lock_outlined))] + : null, + ), + body: ViewStateWrapper( + isLoading: + state.state == ModelState.loading || state.is_loading == true, + error: state.error, + data: state.data, + builder: (_, snippet) => _DetailPageData( + model: model, + snippet: snippet, + ), + ), + ); + } + + void _exit() { + model.resetEvent(); + navigator.back(); + } +} + +class _DetailPageData extends StatelessWidget { + const _DetailPageData({ + Key? key, + required this.model, + required this.snippet, + }) : super(key: key); + + final DetailModelBridge model; + final Snippet? snippet; + + @override + Widget build(BuildContext context) { + if (snippet == null) return const SizedBox(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + PaddingStyles.regular(SnippetDetailsBar(snippet: snippet!)), + Expanded( + child: ColoredBox( + color: ColorStyles.codeBackground(), + child: NoOverscrollSingleChildScrollView( + padding: const EdgeInsets.all(Dimens.l), + child: CodeTextView( + code: snippet!.code!.raw!, + tokens: snippet!.code?.tokens, + ), + ), + ), + ), + PaddingStyles.regular( + Center( + child: SnippetActionBar( + snippet: snippet!, + onLikeTap: model.like, + onDislikeTap: model.dislike, + onSaveTap: model.save, + onCopyTap: model.copyToClipboard, + onShareTap: model.share, + onDeleteTap: model.delete, + ), + ), + ), + ], + ); + } +} diff --git a/flutter_module/lib/presentation/screens/login_screen.dart b/flutter_module/lib/presentation/screens/login_screen.dart new file mode 100644 index 0000000..2f3964a --- /dev/null +++ b/flutter_module/lib/presentation/screens/login_screen.dart @@ -0,0 +1,140 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_module/generated/assets.dart'; +import 'package:flutter_module/model/main_model.dart'; +import 'package:flutter_module/presentation/navigation/login/login_navigator.dart'; +import 'package:flutter_module/presentation/screens/named_screen.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; +import 'package:flutter_module/presentation/styles/padding_styles.dart'; +import 'package:flutter_module/presentation/styles/text_styles.dart'; +import 'package:flutter_module/presentation/widgets/login_input_card.dart'; +import 'package:flutter_module/presentation/widgets/no_overscroll_single_child_scroll_view.dart'; +import 'package:flutter_module/presentation/widgets/rounded_action_button.dart'; +import 'package:flutter_module/presentation/widgets/text_input_field.dart'; +import 'package:flutter_module/presentation/widgets/view_state_wrapper.dart'; +import 'package:flutter_module/utils/extensions/state_extensions.dart'; +import 'package:flutter_module/utils/hooks/use_navigator.dart'; +import 'package:flutter_module/utils/hooks/use_observable_state_hook.dart'; +import 'package:flutter_module/utils/hooks/use_same_state.dart'; +import 'package:go_router/go_router.dart'; +import 'package:go_router_plus/go_router_plus.dart'; + +class LoginScreen extends NamedScreen implements InitialScreen, GuestScreen { + LoginScreen({ + required this.navigator, + required this.model, + }) : super(name); + + static String name = 'login'; + final LoginNavigator navigator; + final LoginModelBridge model; + + @override + Widget builder(BuildContext context, GoRouterState state) { + return _MainPage( + navigator: navigator, + model: model, + ); + } + + @override + build(BuildContext context, GoRouterState state) { + // TODO: implement build + throw UnimplementedError(); + } +} + +class _MainPage extends HookWidget { + const _MainPage({ + Key? key, + required this.navigator, + required this.model, + }) : super(key: key); + + final LoginNavigator navigator; + final LoginModelBridge model; + + @override + Widget build(BuildContext context) { + useNavigator([navigator]); + + final email = useState(''); + final password = useState(''); + final validationCorrect = useState(false); + + final state = useObservableState( + LoginModelStateData(), + () => model.getState(), + (current, newState) => (current as LoginModelStateData).equals(newState), + ).value; + + final event = useObservableState( + LoginModelEventData(), + () => model.getEvent(), + (current, newState) => (current as LoginModelEventData).equals(newState), + ).value; + + useEffect(() { + model.checkLoginState(); + }, []); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (event.event == LoginModelEvent.logged) { + model.resetEvent(); + navigator.login(); + } + }); + + return Scaffold( + body: SafeArea( + child: ViewStateWrapper( + isLoading: state.state == ModelState.loading, + data: state.state, + builder: (BuildContext context, _) { + return NoOverscrollSingleChildScrollView( + child: Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: Dimens.xxl), + TextStyles.appLogo('SnipMe'), + const SizedBox(height: Dimens.xxl), + Image.asset(Assets.appLogo), + const SizedBox(height: Dimens.xxl), + const TextStyles.secondary('Snip your favorite code'), + PaddingStyles.regular( + LoginInputCard( + onEmailChanged: (emailValue) { + email.value = emailValue; + }, + onPasswordChanged: (passwordValue) { + password.value = passwordValue; + }, + onValidChanged: (isValid) { + validationCorrect.value = isValid; + }, + ), + ), + Center( + child: RoundedActionButton( + icon: Icons.check_circle, + title: 'Login', + enabled: validationCorrect.value, + onPressed: () { + model.loginOrRegister(email.value, password.value); + }, + ), + ), + const SizedBox(height: Dimens.xxl), + ], + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/flutter_module/lib/presentation/screens/main_screen.dart b/flutter_module/lib/presentation/screens/main_screen.dart new file mode 100644 index 0000000..2b4b8f7 --- /dev/null +++ b/flutter_module/lib/presentation/screens/main_screen.dart @@ -0,0 +1,281 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_module/generated/assets.dart'; +import 'package:flutter_module/model/main_model.dart'; +import 'package:flutter_module/presentation/navigation/details/details_navigator.dart'; +import 'package:flutter_module/presentation/navigation/login/login_navigator.dart'; +import 'package:flutter_module/presentation/screens/named_screen.dart'; +import 'package:flutter_module/presentation/styles/color_styles.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; +import 'package:flutter_module/presentation/styles/padding_styles.dart'; +import 'package:flutter_module/presentation/styles/text_styles.dart'; +import 'package:flutter_module/presentation/widgets/filter_dropdown.dart'; +import 'package:flutter_module/presentation/widgets/filter_list_view.dart'; +import 'package:flutter_module/presentation/widgets/snippet_list_item.dart'; +import 'package:flutter_module/presentation/widgets/view_state_wrapper.dart'; +import 'package:flutter_module/utils/extensions/state_extensions.dart'; +import 'package:flutter_module/utils/hooks/use_navigator.dart'; +import 'package:flutter_module/utils/hooks/use_observable_state_hook.dart'; +import 'package:go_router/go_router.dart'; +import 'package:go_router/src/state.dart'; +import 'package:go_router_plus/go_router_plus.dart'; + +class MainScreen extends NamedScreen implements UserScreen { + MainScreen({ + required this.loginNavigator, + required this.detailsNavigator, + required this.model, + }) : super(name); + + static String name = 'main'; + final LoginNavigator loginNavigator; + final DetailsNavigator detailsNavigator; + final MainModelBridge model; + + @override + Widget builder(BuildContext context, GoRouterState state) { + return _MainPage( + loginNavigator: loginNavigator, + detailsNavigator: detailsNavigator, + model: model, + ); + } + + @override + build(BuildContext context, GoRouterState state) { + // TODO: implement build + throw UnimplementedError(); + } +} + +class _MainPage extends HookWidget { + const _MainPage({ + Key? key, + required this.loginNavigator, + required this.detailsNavigator, + required this.model, + }) : super(key: key); + + final LoginNavigator loginNavigator; + final DetailsNavigator detailsNavigator; + final MainModelBridge model; + + @override + Widget build(BuildContext context) { + useNavigator([loginNavigator, detailsNavigator]); + + final state = useObservableState( + MainModelStateData(), + () => model.getState(), + (current, newState) => (current as MainModelStateData).equals(newState), + ).value; + + // Event + final event = useObservableState( + MainModelEventData(), + () => model.getEvent(), + (current, newState) => (current as MainModelEventData).equals(newState), + ).value; + + final expandedState = useState(true); + final controller = useScrollController(); + + useEffect(() { + model.initState(); + }, []); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (event.event == MainModelEvent.logout) { + model.resetEvent(); + loginNavigator.logout(); + } + }); + + return Scaffold( + backgroundColor: ColorStyles.pageBackground(), + body: ViewStateWrapper>( + isLoading: + state.state == ModelState.loading || state.is_loading == true, + error: state.error, + data: state.data?.cast(), + builder: (_, snippets) { + return _MainPageData( + navigator: detailsNavigator, + model: model, + snippets: snippets ?? List.empty(), + filter: state.filter ?? SnippetFilter(), + controller: controller, + expanded: expandedState.value, + onExpandChange: (expanded) => expandedState.value = expanded, + ); + }, + ), + floatingActionButton: FloatingActionButton.small( + onPressed: () { + controller.animateTo( + 0.0, + duration: const Duration(seconds: 1), + curve: Curves.easeIn, + ); + }, + tooltip: 'Scroll to top', + backgroundColor: ColorStyles.surfacePrimary(), + child: const Icon( + Icons.arrow_upward_outlined, + color: Colors.black, + ), + ), + ); + } +} + +typedef ExpandChangeListener = Function(bool); + +class _MainPageData extends HookWidget { + const _MainPageData( + {Key? key, + required this.navigator, + required this.model, + required this.snippets, + required this.filter, + required this.controller, + required this.expanded, + required this.onExpandChange}) + : super(key: key); + + final DetailsNavigator navigator; + final MainModelBridge model; + final List snippets; + final SnippetFilter filter; + final ScrollController controller; + final bool expanded; + final ExpandChangeListener onExpandChange; + + @override + Widget build(BuildContext context) { + return NestedScrollView( + controller: controller, + floatHeaderSlivers: true, + headerSliverBuilder: (_, __) { + return [ + SliverAppBar( + elevation: 0.0, + centerTitle: true, + title: Row(mainAxisSize: MainAxisSize.min, children: [ + Image.asset(Assets.appLogo, width: Dimens.logoSignetSize), + const SizedBox(width: Dimens.m), + TextStyles.appBarLogo('SnipMe'), + ]), + backgroundColor: ColorStyles.surfacePrimary(), + leading: IconButton( + icon: const Icon(Icons.logout), + color: Colors.black, + onPressed: model.logOut, + ), + actions: [ + IconButton( + icon: Icon( + expanded + ? Icons.close_fullscreen_outlined + : Icons.open_in_full_outlined, + ), + color: Colors.black, + onPressed: () => onExpandChange(!expanded), + ), + ], + ), + SliverAppBar( + floating: true, + forceElevated: true, + expandedHeight: Dimens.extendedAppBarHeight, + elevation: Dimens.s / 2, + backgroundColor: ColorStyles.surfacePrimary(), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(Dimens.l), + bottomRight: Radius.circular(Dimens.l), + ), + ), + flexibleSpace: FlexibleSpaceBar( + collapseMode: CollapseMode.parallax, + background: Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimens.m), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + PaddingStyles.small( + Row(children: [TextStyles.bold("Scope")]), + ), + PaddingStyles.small( + Row( + children: [ + Expanded( + child: SizedBox( + height: Dimens.filterDropdownHeight, + child: FilterDropdown( + filters: filter.scopes ?? List.empty(), + selected: filter.selectedScope ?? '', + onSelected: (scope) { + model.filterScope(scope); + }, + ), + ), + ), + ], + ), + ), + PaddingStyles.small( + Row(children: [TextStyles.bold("Language")]), + ), + SizedBox( + height: Dimens.filterListHeight, + child: FilterListView( + filters: filter.languages ?? List.empty(), + selected: filter.selectedLanguages ?? List.empty(), + onSelected: (language, isSelected) { + model.filterLanguage(language, isSelected); + }, + ), + ), + const SizedBox( + height: Dimens.m, + ) + ], + ), + ), + )) + ]; + }, + body: CustomScrollView( + scrollBehavior: const ScrollBehavior( + //TODO: change line below + // androidOverscrollIndicator: AndroidOverscrollIndicator.stretch, + ), + slivers: [ + SliverList( + delegate: SliverChildListDelegate([ + ...snippets.map( + (snippet) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: Dimens.s, + horizontal: Dimens.m, + ), + child: SnippetListTile( + isExpanded: expanded, + snippet: snippet, + onTap: () { + navigator.goToDetails(context, snippet.uuid!); + }, + ), + ); + }, + ).toList() + ]), + ), + ], + ), + ); + } +} diff --git a/flutter_module/lib/presentation/screens/named_screen.dart b/flutter_module/lib/presentation/screens/named_screen.dart new file mode 100644 index 0000000..ecb84df --- /dev/null +++ b/flutter_module/lib/presentation/screens/named_screen.dart @@ -0,0 +1,14 @@ +import 'package:flutter_module/utils/extensions/text_extensions.dart'; +import 'package:go_router_plus/go_router_plus.dart'; + +abstract class NamedScreen extends Screen { + NamedScreen(this._name); + + final String _name; + + @override + String get routeName => _name; + + @override + String get routePath => _name.route; +} \ No newline at end of file diff --git a/flutter_module/lib/presentation/styles/color_styles.dart b/flutter_module/lib/presentation/styles/color_styles.dart new file mode 100644 index 0000000..dc42a39 --- /dev/null +++ b/flutter_module/lib/presentation/styles/color_styles.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class ColorStyles extends Color { + + ColorStyles.accent(): super(0xFF5367FF); + + ColorStyles.surfacePrimary(): super(Colors.white.value); + + ColorStyles.pageBackground(): super(0xFFF5F5F5); + + ColorStyles.codeBackground(): super(0xFFF8F8F8); + + ColorStyles.filterBackgroundColor(): super(0xFF212121); +} \ No newline at end of file diff --git a/flutter_module/lib/presentation/styles/dimens.dart b/flutter_module/lib/presentation/styles/dimens.dart new file mode 100644 index 0000000..b3b3966 --- /dev/null +++ b/flutter_module/lib/presentation/styles/dimens.dart @@ -0,0 +1,14 @@ +class Dimens { + static const s = 4.0; + static const m = 8.0; + static const l = 16.0; + static const xl = 24.0; + static const xxl = 32.0; + + static const inputBorderWidth = 1.0; + static const filterDropdownHeight = 24.0; + static const filterListHeight = 48.0; + static const extendedAppBarHeight = 144.0; + + static const logoSignetSize = 18.0; +} \ No newline at end of file diff --git a/flutter_module/lib/presentation/styles/padding_styles.dart b/flutter_module/lib/presentation/styles/padding_styles.dart new file mode 100644 index 0000000..6822445 --- /dev/null +++ b/flutter_module/lib/presentation/styles/padding_styles.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; + +class PaddingStyles extends Padding { + + const PaddingStyles.small(Widget child, {Key? key}) + : super(key: key, padding: const EdgeInsets.all(Dimens.m), child: child); + + const PaddingStyles.regular(Widget child, {Key? key}) + : super(key: key, padding: const EdgeInsets.all(Dimens.l), child: child); +} diff --git a/flutter_module/lib/presentation/styles/surface_styles.dart b/flutter_module/lib/presentation/styles/surface_styles.dart new file mode 100644 index 0000000..50afe29 --- /dev/null +++ b/flutter_module/lib/presentation/styles/surface_styles.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_module/presentation/styles/color_styles.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; +import 'package:flutter_module/presentation/styles/padding_styles.dart'; + +class SurfaceStyles { + static actionCard({required Widget child}) { + return Card( + color: ColorStyles.surfacePrimary(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Dimens.xxl * 2, + ), + ), + child: PaddingStyles.regular(child), + ); + } + + static snippetCard({required Widget child, GestureTapCallback? onTap}) { + return Card( + color: ColorStyles.surfacePrimary(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(Dimens.m), + ), + child: InkWell( + borderRadius: BorderRadius.circular(Dimens.m), + onTap: onTap, + child: child, + ), + ); + } + + static rateBox(Widget child) { + return DecoratedBox( + decoration: BoxDecoration( + color: ColorStyles.codeBackground(), + borderRadius: BorderRadius.circular(Dimens.l), + ), + child: PaddingStyles.regular( + Center(child: child), + ), + ); + } + + static Widget roundedFloatingCard({ + required Widget child, + GestureTapCallback? onTap, + }) { + return Card( + color: ColorStyles.surfacePrimary(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Dimens.xxl * 2, + ), + ), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(Dimens.xxl * 2), + child: child, + ), + ); + } +} diff --git a/flutter_module/lib/presentation/styles/text_styles.dart b/flutter_module/lib/presentation/styles/text_styles.dart new file mode 100644 index 0000000..aaa035a --- /dev/null +++ b/flutter_module/lib/presentation/styles/text_styles.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_module/presentation/styles/color_styles.dart'; + +class TextStyles extends Text { + final String text; + + const TextStyles.title(this.text, {Key? key}) + : super( + text, + key: key, + style: const TextStyle(fontSize: 16), + ); + + const TextStyles.code(this.text, {Key? key}) + : super( + text, + key: key, + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + ), + ); + + TextStyles.regular(this.text, {Key? key, Color? color}) + : super( + text, + key: key, + style: TextStyle(color: color), + ); + + TextStyles.bold(this.text, {Key? key, Color? color}) + : super( + text, + key: key, + style: TextStyle( + color: color, + fontWeight: FontWeight.w600, + ), + ); + + const TextStyles.secondary(this.text, {Key? key}) + : super( + text, + key: key, + style: const TextStyle(color: Colors.grey), + ); + + const TextStyles.secondaryBold(this.text, {Key? key}) + : super( + text, + key: key, + style: const TextStyle( + color: Colors.grey, + fontWeight: FontWeight.bold, + ), + ); + + const TextStyles.label(this.text, {Key? key}) + : super( + text, + key: key, + style: const TextStyle(fontSize: 12, color: Colors.grey), + ); + + const TextStyles.helper(this.text, {Key? key}) + : super( + text, + key: key, + style: const TextStyle(fontSize: 10, color: Colors.grey), + ); + + TextStyles.appLogo(this.text, {Key? key}) + : super( + text, + key: key, + style: TextStyle( + fontFamily: 'Kanit', + fontSize: 24.0, + color: ColorStyles.accent(), + ), + ); + + TextStyles.appBarLogo(this.text, {Key? key}) + : super( + text, + key: key, + style: const TextStyle( + fontFamily: 'Kanit', + fontSize: 18.0, + color: Colors.black, + ), + ); +} diff --git a/flutter_module/lib/presentation/widgets/code_text_view.dart b/flutter_module/lib/presentation/widgets/code_text_view.dart new file mode 100644 index 0000000..d831259 --- /dev/null +++ b/flutter_module/lib/presentation/widgets/code_text_view.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_module/model/main_model.dart'; +import 'package:flutter_module/presentation/styles/text_styles.dart'; +import 'package:flutter_module/utils/extensions/collection_extensions.dart'; +import 'package:flutter_module/utils/extensions/text_extensions.dart'; + +class TextSelectionOptions { + ToolbarOptions toolbarOptions; + bool showCursor; + + TextSelectionOptions({ + required this.toolbarOptions, + required this.showCursor, + }); + + TextSelectionOptions.copyable() + : toolbarOptions = const ToolbarOptions( + copy: true, + cut: true, + selectAll: true, + ), + showCursor = true; +} + +class CodeTextView extends StatelessWidget { + const CodeTextView({ + Key? key, + required this.code, + this.maxLines, + this.tokens, + this.options, + this.onTap, + }) : super(key: key); + + final splitter = const LineSplitter(); + final String code; + final int? maxLines; + final List? tokens; + final TextSelectionOptions? options; + final GestureTapCallback? onTap; + + const CodeTextView.preview({ + super.key, + required this.code, + this.tokens, + this.options, + this.onTap, + }) : maxLines = 5; + + @override + Widget build(BuildContext context) { + final maxLinesOrAll = maxLines ?? splitter.convert(code).length; + + return FutureBuilder( + initialData: const [], + future: tokens.toSpans( + code.lines(maxLinesOrAll), + TextStyles.code(code).style!, + ), + builder: (_, value) { + return SelectableText.rich( + TextSpan(children: value.requireData), + minLines: 1, + maxLines: maxLinesOrAll, + onTap: () {}, + toolbarOptions: options?.toolbarOptions, + showCursor: options?.showCursor ?? false, + enableInteractiveSelection: false, + scrollPhysics: const NeverScrollableScrollPhysics(), + ); + }, + ); + } +} diff --git a/flutter_module/lib/presentation/widgets/filter_dropdown.dart b/flutter_module/lib/presentation/widgets/filter_dropdown.dart new file mode 100644 index 0000000..435cb65 --- /dev/null +++ b/flutter_module/lib/presentation/widgets/filter_dropdown.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_module/presentation/styles/text_styles.dart'; + +typedef FilterSelectedItemListener = Function(String); + +class FilterDropdown extends StatelessWidget { + const FilterDropdown({ + Key? key, + required this.filters, + required this.selected, + this.onSelected, + }) : super(key: key); + + final List filters; + final String selected; + final FilterSelectedItemListener? onSelected; + + @override + Widget build(BuildContext context) { + return DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + onChanged: (filter) => onSelected?.call(filter ?? ''), + value: selected, + items: [ + ...filters.map( + (filter) => DropdownMenuItem( + value: filter, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(width: 24,), + TextStyles.title(filter ?? ''), + ], + ), + ), + ) + ], + ), + ); + } +} diff --git a/flutter_module/lib/presentation/widgets/filter_list_view.dart b/flutter_module/lib/presentation/widgets/filter_list_view.dart new file mode 100644 index 0000000..11e82e8 --- /dev/null +++ b/flutter_module/lib/presentation/widgets/filter_list_view.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_module/presentation/styles/color_styles.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; + +typedef FilterSelectedListener = Function(String filter, bool selected); + +class FilterListView extends StatelessWidget { + const FilterListView({ + Key? key, + required this.filters, + required this.selected, + this.onSelected, + }) : super(key: key); + + final List filters; + final List selected; + final FilterSelectedListener? onSelected; + + @override + Widget build(BuildContext context) { + return ListView.separated( + physics: const BouncingScrollPhysics(), + itemCount: filters.length, + scrollDirection: Axis.horizontal, + separatorBuilder: (_, __) => const SizedBox(width: Dimens.m,), + itemBuilder: (BuildContext context, int index) { + final filter = filters[index]; + return ChoiceChip( + disabledColor: ColorStyles.filterBackgroundColor().withOpacity(0.08), + selectedColor: ColorStyles.filterBackgroundColor().withOpacity(0.25), + label: Text(filter ?? ''), + selected: selected.contains(filter), + onSelected: (isSelected) => onSelected?.call(filter ?? '', isSelected), + ); + }, + ); + } +} diff --git a/flutter_module/lib/presentation/widgets/login_input_card.dart b/flutter_module/lib/presentation/widgets/login_input_card.dart new file mode 100644 index 0000000..9ea3846 --- /dev/null +++ b/flutter_module/lib/presentation/widgets/login_input_card.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; +import 'package:flutter_module/presentation/styles/padding_styles.dart'; +import 'package:flutter_module/presentation/styles/surface_styles.dart'; +import 'package:flutter_module/presentation/widgets/text_input_field.dart'; +import 'package:flutter_module/utils/hooks/use_same_state.dart'; +import 'package:validators/validators.dart'; + +const _minPasswordLength = 8; + +class LoginInputCard extends HookWidget { + const LoginInputCard({ + Key? key, + required this.onEmailChanged, + required this.onPasswordChanged, + this.onValidChanged, + }) : super(key: key); + + final TextInputCallback onEmailChanged; + final TextInputCallback onPasswordChanged; + final Function(bool)? onValidChanged; + + @override + Widget build(BuildContext context) { + final emailErrorTextState = useSameState(''); + final passwordErrorTextState = useSameState(''); + + return SurfaceStyles.snippetCard( + child: PaddingStyles.regular( + Column( + children: [ + const SizedBox(height: Dimens.l), + TextInputField( + label: 'Email', + onChanged: onEmailChanged, + validator: (input) => _validate( + emailErrorTextState, + passwordErrorTextState, + email: input as String, + ), + ), + const SizedBox(height: Dimens.xl), + TextInputField( + label: 'Password', + onChanged: onPasswordChanged, + isPassword: true, + validator: (input) => _validate( + emailErrorTextState, + passwordErrorTextState, + password: input as String, + ), + ), + const SizedBox(height: Dimens.l), + ], + ), + ), + ); + } + + String? _validate( + SameValueNotifier emailErrorTextState, + SameValueNotifier passwordErrorTextState, { + String? email, + String? password, + }) { + String? fieldError; + + if (email != null) { + fieldError = isEmail(email) ? null : 'Provide valid email phrase'; + emailErrorTextState.value = fieldError; + } + + if (password != null) { + fieldError = password.length >= _minPasswordLength + ? null + : 'Password must have at least $_minPasswordLength characters'; + passwordErrorTextState.value = fieldError; + } + + final emailError = emailErrorTextState.value; + final passwordError = passwordErrorTextState.value; + + onValidChanged?.call(emailError == null && passwordError == null); + + return fieldError; + } +} diff --git a/flutter_module/lib/presentation/widgets/no_overscroll_single_child_scroll_view.dart b/flutter_module/lib/presentation/widgets/no_overscroll_single_child_scroll_view.dart new file mode 100644 index 0000000..db2fda6 --- /dev/null +++ b/flutter_module/lib/presentation/widgets/no_overscroll_single_child_scroll_view.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class NoOverscrollSingleChildScrollView extends StatelessWidget { + const NoOverscrollSingleChildScrollView({ + super.key, + required this.child, + this.padding, + }); + + final Widget child; + final EdgeInsets? padding; + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (overScroll) { + overScroll.disallowIndicator(); + return true; + }, + child: SingleChildScrollView( + padding: padding, + physics: const ClampingScrollPhysics(), + child: child, + ), + ); + } +} diff --git a/flutter_module/lib/presentation/widgets/rounded_action_button.dart b/flutter_module/lib/presentation/widgets/rounded_action_button.dart new file mode 100644 index 0000000..e0bcd19 --- /dev/null +++ b/flutter_module/lib/presentation/widgets/rounded_action_button.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_module/presentation/styles/color_styles.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; +import 'package:flutter_module/presentation/styles/padding_styles.dart'; +import 'package:flutter_module/presentation/styles/surface_styles.dart'; +import 'package:flutter_module/presentation/styles/text_styles.dart'; + +class RoundedActionButton extends StatelessWidget { + const RoundedActionButton({ + Key? key, + required this.icon, + required this.title, + this.enabled = true, + this.onPressed, + }) : super(key: key); + + final IconData icon; + final String title; + final bool enabled; + final GestureTapCallback? onPressed; + + @override + Widget build(BuildContext context) { + return SurfaceStyles.roundedFloatingCard( + onTap: enabled ? () => onPressed?.call() : null, + child: PaddingStyles.small( + Opacity( + opacity: enabled ? 1.0 : 0.3, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: Dimens.m), + Icon(icon, color: ColorStyles.accent()), + const SizedBox(width: Dimens.m), + TextStyles.regular( + title.toUpperCase(), + color: ColorStyles.accent(), + ), + const SizedBox(width: Dimens.m), + ], + ), + ), + ), + ); + } +} diff --git a/flutter_module/lib/presentation/widgets/snippet_action_bar.dart b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart new file mode 100644 index 0000000..9331919 --- /dev/null +++ b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_module/model/main_model.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; +import 'package:flutter_module/presentation/styles/surface_styles.dart'; +import 'package:flutter_module/presentation/widgets/state_icon.dart'; + +class SnippetActionBar extends StatelessWidget { + const SnippetActionBar({ + Key? key, + required this.snippet, + this.onLikeTap, + this.onDislikeTap, + this.onSaveTap, + this.onCopyTap, + this.onShareTap, + this.onDeleteTap, + }) : super(key: key); + + final Snippet snippet; + final GestureTapCallback? onLikeTap; + final GestureTapCallback? onDislikeTap; + final GestureTapCallback? onSaveTap; + final GestureTapCallback? onCopyTap; + final GestureTapCallback? onShareTap; + final GestureTapCallback? onDeleteTap; + + @override + Widget build(BuildContext context) { + return SurfaceStyles.actionCard( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + StateIcon( + icon: Icons.thumb_up_alt_outlined, + active: snippet.isLiked, + onTap: snippet.isLiked == false ? null : onLikeTap, + ), + const SizedBox(width: Dimens.l), + StateIcon( + icon: Icons.thumb_down_alt_outlined, + active: snippet.isDisliked, + onTap: snippet.isDisliked == false ? null : onDislikeTap, + ), + const SizedBox(width: Dimens.l), + StateIcon( + icon: Icons.save_alt_outlined, + active: snippet.isSaved, + onTap: getSaveCallback(snippet.isSaved, onSaveTap), + ), + const SizedBox(width: Dimens.l), + StateIcon( + icon: Icons.copy_all_outlined, + onTap: onCopyTap, + ), + const SizedBox(width: Dimens.l), + StateIcon( + icon: Icons.share, + onTap: onShareTap, + ), + const SizedBox(width: Dimens.l), + StateIcon( + onTap: snippet.isToDelete == true ? onDeleteTap : null, + active: snippet.isToDelete == true ? null : false, + activeColor: Colors.redAccent, + icon: Icons.delete_outline_outlined, + ), + ], + ), + ); + } + + GestureTapCallback? getSaveCallback( + bool? isSaved, + GestureTapCallback? onSaveTap, + ) { + if (isSaved == false) return null; + if (isSaved == true) return null; + return onSaveTap; + } +} diff --git a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart new file mode 100644 index 0000000..100cd9b --- /dev/null +++ b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_module/generated/assets.dart'; +import 'package:flutter_module/model/main_model.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; +import 'package:flutter_module/presentation/styles/surface_styles.dart'; +import 'package:flutter_module/presentation/styles/text_styles.dart'; + +class SnippetDetailsBar extends StatelessWidget { + const SnippetDetailsBar({ + Key? key, + required this.snippet, + }) : super(key: key); + + final Snippet snippet; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextStyles.regular(snippet.language?.raw ?? "Unknown language"), + const SizedBox(height: Dimens.m), + snippet.isOwner == true + ? TextStyles.secondaryBold(snippet.owner?.login ?? "") + : TextStyles.secondary(snippet.owner?.login ?? ""), + const SizedBox(height: Dimens.s), + TextStyles.helper(snippet.timeAgo ?? "") + ], + ), + ), + _UserReactionIndicator(reaction: snippet.userReaction), + const SizedBox(width: Dimens.l), + SurfaceStyles.rateBox( + TextStyles.title( + _getVoteCountText(snippet.voteResult), + ), + ) + ], + ); + } + + String _getVoteCountText(int? voteResult) { + const defaultValue = '+0'; + if (voteResult == null) return defaultValue; + if (voteResult == 0) return defaultValue; + if (voteResult > 0) return '+$voteResult'; + return '-$voteResult'; + } +} + +class _UserReactionIndicator extends StatelessWidget { + const _UserReactionIndicator({ + Key? key, + this.reaction, + }) : super(key: key); + + final UserReaction? reaction; + + @override + Widget build(BuildContext context) { + if (reaction == UserReaction.like) { + return Image.asset(Assets.reactionLike); + } + + if (reaction == UserReaction.dislike) { + return Image.asset(Assets.reactionDislike); + } + + return Image.asset(Assets.reactionUndefined); + } +} diff --git a/flutter_module/lib/presentation/widgets/snippet_list_item.dart b/flutter_module/lib/presentation/widgets/snippet_list_item.dart new file mode 100644 index 0000000..c45679e --- /dev/null +++ b/flutter_module/lib/presentation/widgets/snippet_list_item.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_module/model/main_model.dart'; +import 'package:flutter_module/presentation/styles/color_styles.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; +import 'package:flutter_module/presentation/styles/surface_styles.dart'; +import 'package:flutter_module/presentation/styles/text_styles.dart'; +import 'package:flutter_module/presentation/widgets/code_text_view.dart'; +import 'package:flutter_module/presentation/widgets/snippet_details_bar.dart'; + +class SnippetListTile extends HookWidget { + const SnippetListTile({ + Key? key, + required this.snippet, + required this.onTap, + this.isExpanded = true, + }) : super(key: key); + + final Snippet snippet; + final GestureTapCallback? onTap; + final bool isExpanded; + + @override + Widget build(BuildContext context) { + return SurfaceStyles.snippetCard( + onTap: onTap, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.only( + top: Dimens.l, + left: Dimens.l, + right: Dimens.l, + bottom: Dimens.m, + ), + child: TextStyles.title(snippet.title ?? ""), + ), + Ink( + color: ColorStyles.codeBackground(), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: Dimens.m, + horizontal: Dimens.l, + ), + child: CodeTextView.preview( + code: snippet.code?.raw ?? "", + tokens: snippet.code?.tokens, + ), + ), + ), + const SizedBox(height: Dimens.m), + if (isExpanded) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimens.l), + child: SnippetDetailsBar(snippet: snippet), + ), + const SizedBox(height: Dimens.m), + ], + ], + ), + ); + } +} diff --git a/flutter_module/lib/presentation/widgets/state_icon.dart b/flutter_module/lib/presentation/widgets/state_icon.dart new file mode 100644 index 0000000..9598709 --- /dev/null +++ b/flutter_module/lib/presentation/widgets/state_icon.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_module/presentation/styles/color_styles.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; + +class StateIcon extends StatelessWidget { + const StateIcon({ + Key? key, + required this.icon, + this.activeColor = Colors.black, + this.active, + this.onTap, + }) : super(key: key); + + final IconData icon; + final Color activeColor; + final bool? active; + final GestureTapCallback? onTap; + + @override + Widget build(BuildContext context) { + final color = getColorByState(active, activeColor); + return SizedBox( + width: 24, + height: 24, + child: IconButton( + padding: EdgeInsets.zero, + splashRadius: Dimens.xl, + icon: Icon(icon), + color: color, + disabledColor: color, + onPressed: onTap, + ), + ); + } + + Color getColorByState(bool? active, Color activeColor) { + if (active == null) return activeColor; + return active ? ColorStyles.accent() : Colors.grey; + } +} diff --git a/flutter_module/lib/presentation/widgets/text_input_field.dart b/flutter_module/lib/presentation/widgets/text_input_field.dart new file mode 100644 index 0000000..642037b --- /dev/null +++ b/flutter_module/lib/presentation/widgets/text_input_field.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_module/presentation/styles/color_styles.dart'; +import 'package:flutter_module/presentation/styles/dimens.dart'; +import 'package:flutter_module/utils/hooks/use_same_state.dart'; + +typedef TextInputCallback = Function(String value); + +class TextInputField extends HookWidget { + TextInputField({ + Key? key, + required this.label, + this.isPassword = false, + this.onChanged, + this.validator, + }) : super(key: key); + + final String label; + final bool isPassword; + final TextInputCallback? onChanged; + final FormFieldValidator? validator; + + @override + Widget build(BuildContext context) { + final controller = useTextEditingController(); + final shouldShow = useState(false); + final error = useState(null); + final passwordVisible = shouldShow.value; + + useEffect(() { + controller.addListener(() { + onChanged?.call(controller.text); + error.value = + controller.text.isNotEmpty ? validator?.call(controller.text) : null; + }); + + return () => controller.dispose(); + }, []); + + return TextFormField( + autovalidateMode: AutovalidateMode.onUserInteraction, + obscureText: isPassword && !shouldShow.value, + controller: controller, + cursorColor: ColorStyles.accent(), + decoration: InputDecoration( + errorText: error.value, + focusedErrorBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.red, + width: Dimens.inputBorderWidth, + ), + ), + errorBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.red, + width: Dimens.inputBorderWidth, + ), + ), + labelText: label, + floatingLabelStyle: TextStyle( + color: error.value == null ? ColorStyles.accent() : Colors.red, + ), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.grey, + width: Dimens.inputBorderWidth, + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: ColorStyles.accent(), + width: Dimens.inputBorderWidth, + ), + ), + suffixIcon: isPassword + ? InkWell( + radius: Dimens.xl, + onTap: () => shouldShow.value = !passwordVisible, + child: passwordVisible + ? const Icon(Icons.visibility_off, color: Colors.black) + : const Icon(Icons.visibility, color: Colors.black), + ) + : null, + ), + ); + } +} diff --git a/flutter_module/lib/presentation/widgets/view_state_wrapper.dart b/flutter_module/lib/presentation/widgets/view_state_wrapper.dart new file mode 100644 index 0000000..9d8a186 --- /dev/null +++ b/flutter_module/lib/presentation/widgets/view_state_wrapper.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +typedef DataWidgetBuilder = Widget Function(BuildContext context, T? data); + +class ViewStateWrapper extends StatelessWidget { + const ViewStateWrapper({ + Key? key, + required this.builder, + this.isLoading = false, + this.data, + this.error, + }) : super(key: key); + + final bool isLoading; + final T? data; + final String? error; + final DataWidgetBuilder builder; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // Data + Visibility( + visible: error == null && !isLoading, + child: builder.call(context, data), + ), + // Loading + Visibility( + visible: isLoading, + child: const Center(child: CircularProgressIndicator()), + ), + // Error + Visibility( + visible: error != null, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("Error"), + Text(error ?? ""), + ], + ), + ), + ) + ], + ); + } +} diff --git a/flutter_module/lib/utils/extensions/build_context_extensions.dart b/flutter_module/lib/utils/extensions/build_context_extensions.dart new file mode 100644 index 0000000..e7782bd --- /dev/null +++ b/flutter_module/lib/utils/extensions/build_context_extensions.dart @@ -0,0 +1,12 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +extension NavigatorPopExtension on BuildContext { + void popOrExit() { + if (Navigator.canPop(this)) { + Navigator.pop(this); + } else { + SystemNavigator.pop(); + } + } +} diff --git a/flutter_module/lib/utils/extensions/collection_extensions.dart b/flutter_module/lib/utils/extensions/collection_extensions.dart new file mode 100644 index 0000000..161f682 --- /dev/null +++ b/flutter_module/lib/utils/extensions/collection_extensions.dart @@ -0,0 +1,78 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter_module/model/main_model.dart'; +import 'package:flutter_module/presentation/styles/text_styles.dart'; +import 'package:flutter_module/utils/extensions/text_extensions.dart'; +import 'package:collection/collection.dart'; + +class TokenSpan with EquatableMixin { + String value; + Color color; + int start; + int end; + + TokenSpan({ + required this.value, + required this.color, + required this.start, + required this.end, + }); + + @override + List get props => [value, color, start, end]; +} + +extension SyntaxSpanExtension on List? { + Future> toSpans(String text, TextStyle baseStyle) { + print("toSpans"); + return Future.microtask(() { + if (this == null) return [TextSpan(text: text, style: baseStyle)]; + if (this!.isEmpty) return [TextSpan(text: text, style: baseStyle)]; + + final syntaxTokens = this!.whereType(); + + final tokens = syntaxTokens.map( + (token) => TokenSpan( + value: text.substring(token.start!, token.end!), + color: Color(token.color!), + start: token.start!, + end: token.end!, + ), + ); + + final uniqueTokens = tokens.where((tested) { + final isDuplicated = tokens.any( + (span) => + tested != span && + span.value.contains(tested.value) && + tested.start >= span.start && + tested.end <= span.end, + ); + + return !isDuplicated; + }); + + final tokenIndices = + uniqueTokens.expand((token) => [token.start, token.end]).toList(); + + final phrases = + tokenIndices.isNotEmpty ? text.splitByIndices(tokenIndices) : [text]; + + return phrases.map((phrase) { + TextStyle style = baseStyle; + + final foundToken = uniqueTokens.firstWhereOrNull( + (span) => span.value == phrase, + ); + + if (foundToken != null) { + style = + TextStyles.code(text).style!.copyWith(color: foundToken.color); + } + + return TextSpan(text: phrase, style: style); + }).toList(); + }); + } +} diff --git a/flutter_module/lib/utils/extensions/state_extensions.dart b/flutter_module/lib/utils/extensions/state_extensions.dart new file mode 100644 index 0000000..e4960f9 --- /dev/null +++ b/flutter_module/lib/utils/extensions/state_extensions.dart @@ -0,0 +1,43 @@ +import 'package:flutter_module/model/main_model.dart'; + +extension MainModelStateDataExtension on MainModelStateData { + bool equals(Object other) { + if (other is! MainModelStateData) return false; + return other.oldHash == other.newHash; + } +} + +extension MainModelEventDataExtension on MainModelEventData { + bool equals(Object other) { + if (other is! MainModelEventData) return false; + return other.oldHash == other.newHash; + } +} + +extension DetailModelStateDataExtension on DetailModelStateData { + bool equals(Object other) { + if (other is! DetailModelStateData) return false; + return other.oldHash == other.newHash; + } +} + +extension DetailModelEventDataExtension on DetailModelEventData { + bool equals(Object other) { + if (other is! DetailModelEventData) return false; + return other.oldHash == other.newHash; + } +} + +extension LoginModelStateDataExtension on LoginModelStateData { + bool equals(Object other) { + if (other is! LoginModelStateData) return false; + return other.oldHash == other.newHash; + } +} + +extension LoginModelEventDataExtension on LoginModelEventData { + bool equals(Object other) { + if (other is! LoginModelEventData) return false; + return other.oldHash == other.newHash; + } +} diff --git a/flutter_module/lib/utils/extensions/text_extensions.dart b/flutter_module/lib/utils/extensions/text_extensions.dart new file mode 100644 index 0000000..24df70e --- /dev/null +++ b/flutter_module/lib/utils/extensions/text_extensions.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; + +extension TextExtensions on String { + String get route => '/$this'; + + List splitByIndices(List splitters) { + List result = []; + + if (splitters.any((splitIndex) => splitIndex < 0)) { + throw Exception("Index for word must be at least 0"); + } + + if (splitters.any((splitIndex) => splitIndex < 0)) { + throw Exception("Index for word must at most ${length}"); + } + + if (splitters.isEmpty) { + throw Exception("There must be at least one splitter provided"); + } + + splitters.sort((a, b) => a.compareTo(b)); + + if (splitters.first > 0) { + splitters.insert(0, 0); + } + + if (splitters.last < length) { + splitters.add(length); + } + + for (int i = 0; i < splitters.length; i++) { + final splitter = splitters[i]; + + if (splitter == splitters.last) { + break; + } + + final nextSplitter = splitters[i + 1]; + result.add(substring(splitter, nextSplitter)); + } + + return result; + } + + String lines(int count) { + final split = const LineSplitter().convert(this).take(count); + return split.join('\n'); + } +} diff --git a/flutter_module/lib/utils/hooks/use_navigator.dart b/flutter_module/lib/utils/hooks/use_navigator.dart new file mode 100644 index 0000000..3141758 --- /dev/null +++ b/flutter_module/lib/utils/hooks/use_navigator.dart @@ -0,0 +1,45 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_module/presentation/navigation/screen_navigator.dart'; +import 'package:go_router/go_router.dart'; + +void useNavigator(List navigators) { + return use(_NavigatorHook(navigators)); +} + +class _NavigatorHook extends Hook { + const _NavigatorHook(this.navigators); + + final List navigators; + + @override + _NavigatorState createState() => _NavigatorState(navigators); +} + +class _NavigatorState extends HookState { + _NavigatorState(this.navigators); + + final List navigators; + bool isInitialized = false; + + @override + void build(BuildContext context) { + if (isInitialized) return; + + _setupNavigators(context); + } + + void _setupNavigators(BuildContext context) { + for (var navigator in navigators) { + navigator.setRouter(GoRouter.of(context)); + } + + isInitialized = true; + } + + @override + Object? get debugValue => navigators; + + @override + String get debugLabel => 'useNavigator([${navigators.join(', ')}])'; +} diff --git a/flutter_module/lib/utils/hooks/use_observable_state_hook.dart b/flutter_module/lib/utils/hooks/use_observable_state_hook.dart new file mode 100644 index 0000000..19c4054 --- /dev/null +++ b/flutter_module/lib/utils/hooks/use_observable_state_hook.dart @@ -0,0 +1,33 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_module/utils/hooks/use_same_state.dart'; + +typedef StateEqualsListener = bool Function(Object, Object); + +ValueNotifier useObservableState( + T initialData, + Future Function() getSource, + StateEqualsListener stateEquals, [ + List cancelKeys = const [], + List refreshKeys = const [], +]) { + final state = useSameState(initialData); + + final timer = useMemoized( + () => Timer.periodic(const Duration(milliseconds: 500), (_) async { + final T newData = await getSource(); + if (!stateEquals(state.value as Object, newData as Object)) { + state.value = newData; + } + }), + refreshKeys, + ); + + useEffect(() { + return () => timer.cancel(); + }, cancelKeys); + + return state; +} diff --git a/flutter_module/lib/utils/hooks/use_same_state.dart b/flutter_module/lib/utils/hooks/use_same_state.dart new file mode 100644 index 0000000..38c096e --- /dev/null +++ b/flutter_module/lib/utils/hooks/use_same_state.dart @@ -0,0 +1,51 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +class SameValueNotifier extends ValueNotifier { + SameValueNotifier(this._value) : super(_value); + + @override + T get value => _value; + T _value; + @override + set value(T newValue) { + _value = newValue; + notifyListeners(); + } +} + +SameValueNotifier useSameState(T initialData) { + return use(_SameStateHook(initialData: initialData)); +} + +class _SameStateHook extends Hook> { + const _SameStateHook({required this.initialData}); + + final T initialData; + + @override + _SameStateHookState createState() => _SameStateHookState(); +} + +class _SameStateHookState extends HookState, _SameStateHook> { + late final _state = SameValueNotifier(hook.initialData) + ..addListener(_listener); + + @override + void dispose() { + _state.dispose(); + } + + @override + SameValueNotifier build(BuildContext context) => _state; + + void _listener() { + setState(() {}); + } + + @override + Object? get debugValue => _state.value; + + @override + String get debugLabel => 'useState<$T>'; +} \ No newline at end of file diff --git a/flutter_module/lib/utils/mock/mock_page.dart b/flutter_module/lib/utils/mock/mock_page.dart new file mode 100644 index 0000000..1db3ea8 --- /dev/null +++ b/flutter_module/lib/utils/mock/mock_page.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_module/presentation/styles/padding_styles.dart'; + +class MockPage extends StatelessWidget { + const MockPage({ + Key? key, + required this.children, + }) : super(key: key); + + final List children; + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: Colors.grey, + child: PaddingStyles.regular( + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: children, + ), + ), + ), + ); + } +} diff --git a/flutter_module/lib/utils/mock/mocks.dart b/flutter_module/lib/utils/mock/mocks.dart new file mode 100644 index 0000000..7fe3982 --- /dev/null +++ b/flutter_module/lib/utils/mock/mocks.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_module/model/main_model.dart'; + +class Mocks { + static final snippet = Snippet( + uuid: '', + title: 'New snippet', + owner: Owner(id: 0, login: 'Snippet owner'), + timeAgo: '2 days ago', + voteResult: 32, + userReaction: UserReaction.like, + isLiked: true, + isDisliked: false, + isPrivate: true, + language: SnippetLanguage( + raw: 'Python', + type: SnippetLanguageType.python, + ), + code: SnippetCode( + raw: ''' + #!/usr/bin/env python +"""Test file for Python syntax highlighting in editors / IDEs. +Meant to cover a wide range of different types of statements and expressions. +Not necessarily sensical or comprehensive (assume that if one exception is +highlighted that all are, for instance). +Extraneous trailing whitespace can't be tested because of svn pre-commit hook +checks for such things. +""" +# Comment +# OPTIONAL: XXX catch your attention +# TODO(me): next big thing +# FIXME: this does not work + +# Statements +from __future__ import with_statement # Import +from sys import path as thing + +print(thing) + +assert True # keyword + + +def foo(): # function definition + return [] + + +class Bar(object): # Class definition + def __enter__(self): + pass + + def __exit__(self, *args): + pass + +foo() # UNCOLOURED: function call +while False: # 'while' + continue +for x in foo(): # 'for' + break +with Bar() as stuff: + pass +if False: + pass # 'if' +elif False: + pass +else: + pass + +# Constants +'single-quote', u'unicode' # Strings of all kinds; prefixes not highlighted +"double-quote" +"""triple double-quote""" +r'raw' +ur'unicode raw' +'escape\n' +'\04' # octal +'\xFF' # hex +'\u1111' # unicode character +1 # Integral +1L +1.0 # Float +.1 +1+2j # Complex + +# Expressions +1 and 2 or 3 # Boolean operators +2 < 3 # UNCOLOURED: comparison operators +spam = 42 # UNCOLOURED: assignment +2 + 3 # UNCOLOURED: number operators +[] # UNCOLOURED: list +{} # UNCOLOURED: dict +(1,) # UNCOLOURED: tuple +all # Built-in functions +GeneratorExit # Exceptions + ''', + tokens: [ + SyntaxToken( + start: 0, + end: 5, + color: Colors.amber.value, + ) + ], + ), + ); +} diff --git a/flutter_module/pubspec.lock b/flutter_module/pubspec.lock new file mode 100644 index 0000000..eff4110 --- /dev/null +++ b/flutter_module/pubspec.lock @@ -0,0 +1,658 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + url: "https://pub.dev" + source: hosted + version: "2.4.11" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe + url: "https://pub.dev" + source: hosted + version: "7.3.1" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + url: "https://pub.dev" + source: hosted + version: "1.9.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + url: "https://pub.dev" + source: hosted + version: "0.20.5" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: bd7e671d26fd39c78cba82070fa34ef1f830b0e7ed1aeebccabc6561302a7ee5 + url: "https://pub.dev" + source: hosted + version: "6.5.9" + go_router_plus: + dependency: "direct main" + description: + name: go_router_plus + sha256: "03dd0d8e43708c5745d354803d91ade5792e3fd2b97ea89a9afcdcb668761011" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + mime: + dependency: transitive + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + pigeon: + dependency: "direct dev" + description: + name: pigeon + sha256: "2f7af49f530b3208131489ce601f8d95d567b3fa3c71265a87f62b0b87d41e91" + url: "https://pub.dev" + source: hosted + version: "22.5.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct main" + description: + name: test + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + url: "https://pub.dev" + source: hosted + version: "1.25.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + validators: + dependency: "direct main" + description: + name: validators + sha256: "884515951f831a9c669a41ed6c4d3c61c2a0e8ec6bca761a4480b28e99cecf5d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" + source: hosted + version: "14.2.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/flutter_module/pubspec.yaml b/flutter_module/pubspec.yaml new file mode 100644 index 0000000..817d133 --- /dev/null +++ b/flutter_module/pubspec.yaml @@ -0,0 +1,105 @@ +name: flutter_module +description: A new flutter module project. + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# +# This version is used _only_ for the Runner app, which is used if you just do +# a `flutter run` or a `flutter make-host-app-editable`. It has no impact +# on any other native host app that you embed your Flutter project into. +version: 1.0.0+1 + +environment: + sdk: ">=2.18.2 <3.0.0" + +dependencies: + flutter: + sdk: flutter + equatable: ^2.0.5 + flutter_hooks: ^0.20.5 + test: ^1.21.4 + go_router: ^6.5.9 + go_router_plus: ^4.1.1 + validators: ^3.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.2.1 + flutter_lints: ^4.0.0 + pigeon: ^22.5.0 + +# For information on the generic Dart part of this file, see the +# following screens: https://dart.dev/tools/pub/pubspec + +flutter_assets_generator: + named_with_parent: false # Don't add folder to icon's name + +flutter: + plugin: + platforms: + android: + package: dev.snipme.snipmeapp + pluginClass: PigeonPlugin + ios: + pluginClass: PigeonPlugin + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + assets: + - assets/images/illustrations/app_logo.png +# - assets/images/icons/reaction_undefined.png +# - assets/images/icons/reaction_like.png +# - assets/images/icons/reaction_dislike.png + fonts: + - family: Kanit + fonts: + - asset: fonts/Kanit-Regular.ttf + + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add Flutter specific custom fonts to your application, add a fonts + # section here, in this "flutter" section. Each entry in this list should + # have a "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages + + + # This section identifies your Flutter project as a module meant for + # embedding in a native host app. These identifiers should _not_ ordinarily + # be changed after generation - they are used to ensure that the tooling can + # maintain consistency when adding or modifying assets and plugins. + # They also do not have any bearing on your native host application's + # identifiers, which may be completely independent or the same as these. + module: + androidX: true + androidPackage: dev.snipme.flutter_module + iosBundleIdentifier: dev.snipme.flutterModule diff --git a/flutter_module/test/widget_test.dart b/flutter_module/test/widget_test.dart new file mode 100644 index 0000000..43a57da --- /dev/null +++ b/flutter_module/test/widget_test.dart @@ -0,0 +1,2 @@ +void main() +{} diff --git a/flutter_settings.gradle b/flutter_settings.gradle new file mode 100644 index 0000000..bfcca4e --- /dev/null +++ b/flutter_settings.gradle @@ -0,0 +1,5 @@ +setBinding(new Binding([gradle: this, mainModuleName: 'app'])) +evaluate(new File( + settingsDir.parentFile, + 'SnipMeApp/flutter_module/.android/include_flutter.groovy' +)) \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..639da25 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +android.buildFeatures.buildConfig=true \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..3337975 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,37 @@ +import groovy.lang.Binding +import groovy.lang.GroovyShell +import org.gradle.internal.impldep.org.codehaus.plexus.interpolation.reflection.ReflectionValueExtractor.evaluate + +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode = RepositoriesMode.PREFER_SETTINGS + repositories { + google() + mavenCentral() + maven { + url = uri("https://jitpack.io") + } + maven { + url = uri("https://storage.googleapis.com/download.flutter.io") + } + } +} + +rootProject.name = "SnipMeApp" +include(":app") + +apply { from("flutter_settings.gradle") } + + \ No newline at end of file From 266c2167619708dfcf2df3a0c00d079057b2ad0e Mon Sep 17 00:00:00 2001 From: xJac0b Date: Fri, 18 Oct 2024 15:53:34 +0200 Subject: [PATCH 02/23] dart fix --- flutter_module/bridge/main_model.dart | 10 ++++---- flutter_module/lib/main.dart | 2 +- flutter_module/lib/model/main_model.dart | 4 ++-- .../navigation/details/details_navigator.dart | 2 -- .../presentation/screens/details_screen.dart | 13 +++------- .../presentation/screens/login_screen.dart | 16 +++---------- .../lib/presentation/screens/main_screen.dart | 24 ++++++------------- .../presentation/styles/padding_styles.dart | 8 +++---- .../lib/presentation/styles/text_styles.dart | 2 +- .../presentation/widgets/code_text_view.dart | 5 ++-- .../presentation/widgets/filter_dropdown.dart | 4 ++-- .../widgets/filter_list_view.dart | 4 ++-- .../widgets/login_input_card.dart | 5 ++-- .../widgets/rounded_action_button.dart | 4 ++-- .../widgets/snippet_action_bar.dart | 5 ++-- .../widgets/snippet_details_bar.dart | 7 +++--- .../widgets/snippet_list_item.dart | 4 ++-- .../lib/presentation/widgets/state_icon.dart | 4 ++-- .../widgets/text_input_field.dart | 7 +++--- .../widgets/view_state_wrapper.dart | 4 ++-- .../extensions/collection_extensions.dart | 1 - .../lib/utils/extensions/text_extensions.dart | 2 +- flutter_module/lib/utils/mock/mock_page.dart | 4 ++-- flutter_module/lib/utils/mock/mocks.dart | 2 +- flutter_module/pubspec.lock | 10 +++++++- flutter_module/pubspec.yaml | 2 ++ 26 files changed, 65 insertions(+), 90 deletions(-) diff --git a/flutter_module/bridge/main_model.dart b/flutter_module/bridge/main_model.dart index 0cc8750..94515c7 100644 --- a/flutter_module/bridge/main_model.dart +++ b/flutter_module/bridge/main_model.dart @@ -43,8 +43,8 @@ class Owner { enum SnippetLanguageType { c, cpp, - objective_c, - c_sharp, + objectiveC, + cSharp, java, bash, python, @@ -115,7 +115,7 @@ enum LoginModelEvent { none, logged } class MainModelStateData { ModelState? state; - bool? is_loading; + bool? isLoading; List? data; SnippetFilter? filter; String? error; @@ -132,7 +132,7 @@ class MainModelEventData { class DetailModelStateData { ModelState? state; - bool? is_loading; + bool? isLoading; Snippet? data; String? error; int? oldHash; @@ -148,7 +148,7 @@ class DetailModelEventData { class LoginModelStateData { ModelState? state; - bool? is_loading; + bool? isLoading; int? oldHash; int? newHash; } diff --git a/flutter_module/lib/main.dart b/flutter_module/lib/main.dart index 5382aad..2ccd9f3 100644 --- a/flutter_module/lib/main.dart +++ b/flutter_module/lib/main.dart @@ -11,7 +11,7 @@ import 'package:go_router_plus/go_router_plus.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { - MyApp({Key? key}) : super(key: key); + MyApp({super.key}); final loginModel = LoginModelBridge(); final mainModel = MainModelBridge(); diff --git a/flutter_module/lib/model/main_model.dart b/flutter_module/lib/model/main_model.dart index 541c16d..4324143 100644 --- a/flutter_module/lib/model/main_model.dart +++ b/flutter_module/lib/model/main_model.dart @@ -10,8 +10,8 @@ import 'package:flutter/services.dart'; enum SnippetLanguageType { c, cpp, - objective_c, - c_sharp, + objectiveC, + cSharp, java, bash, python, diff --git a/flutter_module/lib/presentation/navigation/details/details_navigator.dart b/flutter_module/lib/presentation/navigation/details/details_navigator.dart index f937a15..681db3b 100644 --- a/flutter_module/lib/presentation/navigation/details/details_navigator.dart +++ b/flutter_module/lib/presentation/navigation/details/details_navigator.dart @@ -1,9 +1,7 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_module/presentation/navigation/screen_navigator.dart'; import 'package:flutter_module/presentation/screens/details_screen.dart'; import 'package:flutter_module/utils/extensions/text_extensions.dart'; -import 'package:go_router/go_router.dart'; class DetailsNavigator extends ScreenNavigator { String? _snippetId; diff --git a/flutter_module/lib/presentation/screens/details_screen.dart b/flutter_module/lib/presentation/screens/details_screen.dart index 52ee903..cc3f3bc 100644 --- a/flutter_module/lib/presentation/screens/details_screen.dart +++ b/flutter_module/lib/presentation/screens/details_screen.dart @@ -28,26 +28,20 @@ class DetailsScreen extends NamedScreen { final DetailModelBridge model; @override - Widget builder(BuildContext context, GoRouterState state) { + Widget build(BuildContext context, GoRouterState state) { return _DetailsPage( navigator: navigator, model: model, ); } - @override - build(BuildContext context, GoRouterState state) { - // TODO: implement build - throw UnimplementedError(); - } } class _DetailsPage extends HookWidget { const _DetailsPage({ - Key? key, required this.navigator, required this.model, - }) : super(key: key); + }); final DetailsNavigator navigator; final DetailModelBridge model; @@ -130,10 +124,9 @@ class _DetailsPage extends HookWidget { class _DetailPageData extends StatelessWidget { const _DetailPageData({ - Key? key, required this.model, required this.snippet, - }) : super(key: key); + }); final DetailModelBridge model; final Snippet? snippet; diff --git a/flutter_module/lib/presentation/screens/login_screen.dart b/flutter_module/lib/presentation/screens/login_screen.dart index 2f3964a..873c5df 100644 --- a/flutter_module/lib/presentation/screens/login_screen.dart +++ b/flutter_module/lib/presentation/screens/login_screen.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -12,13 +11,10 @@ import 'package:flutter_module/presentation/styles/text_styles.dart'; import 'package:flutter_module/presentation/widgets/login_input_card.dart'; import 'package:flutter_module/presentation/widgets/no_overscroll_single_child_scroll_view.dart'; import 'package:flutter_module/presentation/widgets/rounded_action_button.dart'; -import 'package:flutter_module/presentation/widgets/text_input_field.dart'; import 'package:flutter_module/presentation/widgets/view_state_wrapper.dart'; import 'package:flutter_module/utils/extensions/state_extensions.dart'; import 'package:flutter_module/utils/hooks/use_navigator.dart'; import 'package:flutter_module/utils/hooks/use_observable_state_hook.dart'; -import 'package:flutter_module/utils/hooks/use_same_state.dart'; -import 'package:go_router/go_router.dart'; import 'package:go_router_plus/go_router_plus.dart'; class LoginScreen extends NamedScreen implements InitialScreen, GuestScreen { @@ -32,26 +28,19 @@ class LoginScreen extends NamedScreen implements InitialScreen, GuestScreen { final LoginModelBridge model; @override - Widget builder(BuildContext context, GoRouterState state) { + Widget build(BuildContext context, GoRouterState state) { return _MainPage( navigator: navigator, model: model, ); } - - @override - build(BuildContext context, GoRouterState state) { - // TODO: implement build - throw UnimplementedError(); - } } class _MainPage extends HookWidget { const _MainPage({ - Key? key, required this.navigator, required this.model, - }) : super(key: key); + }); final LoginNavigator navigator; final LoginModelBridge model; @@ -78,6 +67,7 @@ class _MainPage extends HookWidget { useEffect(() { model.checkLoginState(); + return null; }, []); WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/flutter_module/lib/presentation/screens/main_screen.dart b/flutter_module/lib/presentation/screens/main_screen.dart index 2b4b8f7..b531c79 100644 --- a/flutter_module/lib/presentation/screens/main_screen.dart +++ b/flutter_module/lib/presentation/screens/main_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_module/generated/assets.dart'; import 'package:flutter_module/model/main_model.dart'; @@ -17,8 +16,6 @@ import 'package:flutter_module/presentation/widgets/view_state_wrapper.dart'; import 'package:flutter_module/utils/extensions/state_extensions.dart'; import 'package:flutter_module/utils/hooks/use_navigator.dart'; import 'package:flutter_module/utils/hooks/use_observable_state_hook.dart'; -import 'package:go_router/go_router.dart'; -import 'package:go_router/src/state.dart'; import 'package:go_router_plus/go_router_plus.dart'; class MainScreen extends NamedScreen implements UserScreen { @@ -34,7 +31,7 @@ class MainScreen extends NamedScreen implements UserScreen { final MainModelBridge model; @override - Widget builder(BuildContext context, GoRouterState state) { + Widget build(BuildContext context, GoRouterState state) { return _MainPage( loginNavigator: loginNavigator, detailsNavigator: detailsNavigator, @@ -42,20 +39,14 @@ class MainScreen extends NamedScreen implements UserScreen { ); } - @override - build(BuildContext context, GoRouterState state) { - // TODO: implement build - throw UnimplementedError(); - } } class _MainPage extends HookWidget { const _MainPage({ - Key? key, required this.loginNavigator, required this.detailsNavigator, required this.model, - }) : super(key: key); + }); final LoginNavigator loginNavigator; final DetailsNavigator detailsNavigator; @@ -83,6 +74,7 @@ class _MainPage extends HookWidget { useEffect(() { model.initState(); + return null; }, []); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -134,15 +126,13 @@ typedef ExpandChangeListener = Function(bool); class _MainPageData extends HookWidget { const _MainPageData( - {Key? key, - required this.navigator, + {required this.navigator, required this.model, required this.snippets, required this.filter, required this.controller, required this.expanded, - required this.onExpandChange}) - : super(key: key); + required this.onExpandChange}); final DetailsNavigator navigator; final MainModelBridge model; @@ -165,7 +155,7 @@ class _MainPageData extends HookWidget { title: Row(mainAxisSize: MainAxisSize.min, children: [ Image.asset(Assets.appLogo, width: Dimens.logoSignetSize), const SizedBox(width: Dimens.m), - TextStyles.appBarLogo('SnipMe'), + const TextStyles.appBarLogo('SnipMe'), ]), backgroundColor: ColorStyles.surfacePrimary(), leading: IconButton( @@ -271,7 +261,7 @@ class _MainPageData extends HookWidget { ), ); }, - ).toList() + ) ]), ), ], diff --git a/flutter_module/lib/presentation/styles/padding_styles.dart b/flutter_module/lib/presentation/styles/padding_styles.dart index 6822445..255438f 100644 --- a/flutter_module/lib/presentation/styles/padding_styles.dart +++ b/flutter_module/lib/presentation/styles/padding_styles.dart @@ -3,9 +3,9 @@ import 'package:flutter_module/presentation/styles/dimens.dart'; class PaddingStyles extends Padding { - const PaddingStyles.small(Widget child, {Key? key}) - : super(key: key, padding: const EdgeInsets.all(Dimens.m), child: child); + const PaddingStyles.small(Widget child, {super.key}) + : super(padding: const EdgeInsets.all(Dimens.m), child: child); - const PaddingStyles.regular(Widget child, {Key? key}) - : super(key: key, padding: const EdgeInsets.all(Dimens.l), child: child); + const PaddingStyles.regular(Widget child, {super.key}) + : super(padding: const EdgeInsets.all(Dimens.l), child: child); } diff --git a/flutter_module/lib/presentation/styles/text_styles.dart b/flutter_module/lib/presentation/styles/text_styles.dart index aaa035a..3064042 100644 --- a/flutter_module/lib/presentation/styles/text_styles.dart +++ b/flutter_module/lib/presentation/styles/text_styles.dart @@ -80,7 +80,7 @@ class TextStyles extends Text { ), ); - TextStyles.appBarLogo(this.text, {Key? key}) + const TextStyles.appBarLogo(this.text, {Key? key}) : super( text, key: key, diff --git a/flutter_module/lib/presentation/widgets/code_text_view.dart b/flutter_module/lib/presentation/widgets/code_text_view.dart index d831259..bc514d5 100644 --- a/flutter_module/lib/presentation/widgets/code_text_view.dart +++ b/flutter_module/lib/presentation/widgets/code_text_view.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_module/model/main_model.dart'; import 'package:flutter_module/presentation/styles/text_styles.dart'; import 'package:flutter_module/utils/extensions/collection_extensions.dart'; @@ -27,13 +26,13 @@ class TextSelectionOptions { class CodeTextView extends StatelessWidget { const CodeTextView({ - Key? key, + super.key, required this.code, this.maxLines, this.tokens, this.options, this.onTap, - }) : super(key: key); + }); final splitter = const LineSplitter(); final String code; diff --git a/flutter_module/lib/presentation/widgets/filter_dropdown.dart b/flutter_module/lib/presentation/widgets/filter_dropdown.dart index 435cb65..03b2645 100644 --- a/flutter_module/lib/presentation/widgets/filter_dropdown.dart +++ b/flutter_module/lib/presentation/widgets/filter_dropdown.dart @@ -5,11 +5,11 @@ typedef FilterSelectedItemListener = Function(String); class FilterDropdown extends StatelessWidget { const FilterDropdown({ - Key? key, + super.key, required this.filters, required this.selected, this.onSelected, - }) : super(key: key); + }); final List filters; final String selected; diff --git a/flutter_module/lib/presentation/widgets/filter_list_view.dart b/flutter_module/lib/presentation/widgets/filter_list_view.dart index 11e82e8..ff58ad6 100644 --- a/flutter_module/lib/presentation/widgets/filter_list_view.dart +++ b/flutter_module/lib/presentation/widgets/filter_list_view.dart @@ -6,11 +6,11 @@ typedef FilterSelectedListener = Function(String filter, bool selected); class FilterListView extends StatelessWidget { const FilterListView({ - Key? key, + super.key, required this.filters, required this.selected, this.onSelected, - }) : super(key: key); + }); final List filters; final List selected; diff --git a/flutter_module/lib/presentation/widgets/login_input_card.dart b/flutter_module/lib/presentation/widgets/login_input_card.dart index 9ea3846..11d647e 100644 --- a/flutter_module/lib/presentation/widgets/login_input_card.dart +++ b/flutter_module/lib/presentation/widgets/login_input_card.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_module/presentation/styles/dimens.dart'; @@ -13,11 +12,11 @@ const _minPasswordLength = 8; class LoginInputCard extends HookWidget { const LoginInputCard({ - Key? key, + super.key, required this.onEmailChanged, required this.onPasswordChanged, this.onValidChanged, - }) : super(key: key); + }); final TextInputCallback onEmailChanged; final TextInputCallback onPasswordChanged; diff --git a/flutter_module/lib/presentation/widgets/rounded_action_button.dart b/flutter_module/lib/presentation/widgets/rounded_action_button.dart index e0bcd19..3e654e5 100644 --- a/flutter_module/lib/presentation/widgets/rounded_action_button.dart +++ b/flutter_module/lib/presentation/widgets/rounded_action_button.dart @@ -7,12 +7,12 @@ import 'package:flutter_module/presentation/styles/text_styles.dart'; class RoundedActionButton extends StatelessWidget { const RoundedActionButton({ - Key? key, + super.key, required this.icon, required this.title, this.enabled = true, this.onPressed, - }) : super(key: key); + }); final IconData icon; final String title; diff --git a/flutter_module/lib/presentation/widgets/snippet_action_bar.dart b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart index 9331919..7f4d0eb 100644 --- a/flutter_module/lib/presentation/widgets/snippet_action_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_module/model/main_model.dart'; import 'package:flutter_module/presentation/styles/dimens.dart'; import 'package:flutter_module/presentation/styles/surface_styles.dart'; @@ -7,7 +6,7 @@ import 'package:flutter_module/presentation/widgets/state_icon.dart'; class SnippetActionBar extends StatelessWidget { const SnippetActionBar({ - Key? key, + super.key, required this.snippet, this.onLikeTap, this.onDislikeTap, @@ -15,7 +14,7 @@ class SnippetActionBar extends StatelessWidget { this.onCopyTap, this.onShareTap, this.onDeleteTap, - }) : super(key: key); + }); final Snippet snippet; final GestureTapCallback? onLikeTap; diff --git a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart index 100cd9b..e928eaa 100644 --- a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart @@ -7,9 +7,9 @@ import 'package:flutter_module/presentation/styles/text_styles.dart'; class SnippetDetailsBar extends StatelessWidget { const SnippetDetailsBar({ - Key? key, + super.key, required this.snippet, - }) : super(key: key); + }); final Snippet snippet; @@ -54,9 +54,8 @@ class SnippetDetailsBar extends StatelessWidget { class _UserReactionIndicator extends StatelessWidget { const _UserReactionIndicator({ - Key? key, this.reaction, - }) : super(key: key); + }); final UserReaction? reaction; diff --git a/flutter_module/lib/presentation/widgets/snippet_list_item.dart b/flutter_module/lib/presentation/widgets/snippet_list_item.dart index c45679e..34a707d 100644 --- a/flutter_module/lib/presentation/widgets/snippet_list_item.dart +++ b/flutter_module/lib/presentation/widgets/snippet_list_item.dart @@ -10,11 +10,11 @@ import 'package:flutter_module/presentation/widgets/snippet_details_bar.dart'; class SnippetListTile extends HookWidget { const SnippetListTile({ - Key? key, + super.key, required this.snippet, required this.onTap, this.isExpanded = true, - }) : super(key: key); + }); final Snippet snippet; final GestureTapCallback? onTap; diff --git a/flutter_module/lib/presentation/widgets/state_icon.dart b/flutter_module/lib/presentation/widgets/state_icon.dart index 9598709..65f62ff 100644 --- a/flutter_module/lib/presentation/widgets/state_icon.dart +++ b/flutter_module/lib/presentation/widgets/state_icon.dart @@ -4,12 +4,12 @@ import 'package:flutter_module/presentation/styles/dimens.dart'; class StateIcon extends StatelessWidget { const StateIcon({ - Key? key, + super.key, required this.icon, this.activeColor = Colors.black, this.active, this.onTap, - }) : super(key: key); + }); final IconData icon; final Color activeColor; diff --git a/flutter_module/lib/presentation/widgets/text_input_field.dart b/flutter_module/lib/presentation/widgets/text_input_field.dart index 642037b..c538f3a 100644 --- a/flutter_module/lib/presentation/widgets/text_input_field.dart +++ b/flutter_module/lib/presentation/widgets/text_input_field.dart @@ -2,18 +2,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_module/presentation/styles/color_styles.dart'; import 'package:flutter_module/presentation/styles/dimens.dart'; -import 'package:flutter_module/utils/hooks/use_same_state.dart'; typedef TextInputCallback = Function(String value); class TextInputField extends HookWidget { - TextInputField({ - Key? key, + const TextInputField({ + super.key, required this.label, this.isPassword = false, this.onChanged, this.validator, - }) : super(key: key); + }); final String label; final bool isPassword; diff --git a/flutter_module/lib/presentation/widgets/view_state_wrapper.dart b/flutter_module/lib/presentation/widgets/view_state_wrapper.dart index 9d8a186..7cf7562 100644 --- a/flutter_module/lib/presentation/widgets/view_state_wrapper.dart +++ b/flutter_module/lib/presentation/widgets/view_state_wrapper.dart @@ -4,12 +4,12 @@ typedef DataWidgetBuilder = Widget Function(BuildContext context, T? data); class ViewStateWrapper extends StatelessWidget { const ViewStateWrapper({ - Key? key, + super.key, required this.builder, this.isLoading = false, this.data, this.error, - }) : super(key: key); + }); final bool isLoading; final T? data; diff --git a/flutter_module/lib/utils/extensions/collection_extensions.dart b/flutter_module/lib/utils/extensions/collection_extensions.dart index 161f682..f385eb8 100644 --- a/flutter_module/lib/utils/extensions/collection_extensions.dart +++ b/flutter_module/lib/utils/extensions/collection_extensions.dart @@ -25,7 +25,6 @@ class TokenSpan with EquatableMixin { extension SyntaxSpanExtension on List? { Future> toSpans(String text, TextStyle baseStyle) { - print("toSpans"); return Future.microtask(() { if (this == null) return [TextSpan(text: text, style: baseStyle)]; if (this!.isEmpty) return [TextSpan(text: text, style: baseStyle)]; diff --git a/flutter_module/lib/utils/extensions/text_extensions.dart b/flutter_module/lib/utils/extensions/text_extensions.dart index 24df70e..f26b8c8 100644 --- a/flutter_module/lib/utils/extensions/text_extensions.dart +++ b/flutter_module/lib/utils/extensions/text_extensions.dart @@ -11,7 +11,7 @@ extension TextExtensions on String { } if (splitters.any((splitIndex) => splitIndex < 0)) { - throw Exception("Index for word must at most ${length}"); + throw Exception("Index for word must at most $length"); } if (splitters.isEmpty) { diff --git a/flutter_module/lib/utils/mock/mock_page.dart b/flutter_module/lib/utils/mock/mock_page.dart index 1db3ea8..88d04ee 100644 --- a/flutter_module/lib/utils/mock/mock_page.dart +++ b/flutter_module/lib/utils/mock/mock_page.dart @@ -3,9 +3,9 @@ import 'package:flutter_module/presentation/styles/padding_styles.dart'; class MockPage extends StatelessWidget { const MockPage({ - Key? key, + super.key, required this.children, - }) : super(key: key); + }); final List children; diff --git a/flutter_module/lib/utils/mock/mocks.dart b/flutter_module/lib/utils/mock/mocks.dart index 7fe3982..4ea496c 100644 --- a/flutter_module/lib/utils/mock/mocks.dart +++ b/flutter_module/lib/utils/mock/mocks.dart @@ -72,7 +72,7 @@ else: r'raw' ur'unicode raw' 'escape\n' -'\04' # octal +'04' # octal '\xFF' # hex '\u1111' # unicode character 1 # Integral diff --git a/flutter_module/pubspec.lock b/flutter_module/pubspec.lock index eff4110..f9d8e05 100644 --- a/flutter_module/pubspec.lock +++ b/flutter_module/pubspec.lock @@ -138,7 +138,7 @@ packages: source: hosted version: "4.10.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a @@ -352,6 +352,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + logger: + dependency: "direct main" + description: + name: logger + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" + url: "https://pub.dev" + source: hosted + version: "2.4.0" logging: dependency: transitive description: diff --git a/flutter_module/pubspec.yaml b/flutter_module/pubspec.yaml index 817d133..601fdec 100644 --- a/flutter_module/pubspec.yaml +++ b/flutter_module/pubspec.yaml @@ -30,6 +30,8 @@ dependencies: go_router_plus: ^4.1.1 validators: ^3.0.0 + collection: any + logger: ^2.4.0 dev_dependencies: flutter_test: sdk: flutter From 96ca861c4384a9f0034a46375185e021a6191c8d Mon Sep 17 00:00:00 2001 From: xJac0b Date: Mon, 21 Oct 2024 22:46:17 +0200 Subject: [PATCH 03/23] chore: hardcode credentials for login form --- flutter_module/lib/presentation/screens/login_screen.dart | 8 +++++--- .../lib/presentation/widgets/login_input_card.dart | 6 ++++++ .../lib/presentation/widgets/text_input_field.dart | 4 +++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/flutter_module/lib/presentation/screens/login_screen.dart b/flutter_module/lib/presentation/screens/login_screen.dart index 873c5df..32f8ba8 100644 --- a/flutter_module/lib/presentation/screens/login_screen.dart +++ b/flutter_module/lib/presentation/screens/login_screen.dart @@ -49,9 +49,9 @@ class _MainPage extends HookWidget { Widget build(BuildContext context) { useNavigator([navigator]); - final email = useState(''); - final password = useState(''); - final validationCorrect = useState(false); + final email = useState('mail@o2.pl'); + final password = useState('12345678'); + final validationCorrect = useState(true); final state = useObservableState( LoginModelStateData(), @@ -96,6 +96,8 @@ class _MainPage extends HookWidget { const TextStyles.secondary('Snip your favorite code'), PaddingStyles.regular( LoginInputCard( + emailValue: email.value, + passwordValue: password.value, onEmailChanged: (emailValue) { email.value = emailValue; }, diff --git a/flutter_module/lib/presentation/widgets/login_input_card.dart b/flutter_module/lib/presentation/widgets/login_input_card.dart index 11d647e..c466fcb 100644 --- a/flutter_module/lib/presentation/widgets/login_input_card.dart +++ b/flutter_module/lib/presentation/widgets/login_input_card.dart @@ -16,11 +16,15 @@ class LoginInputCard extends HookWidget { required this.onEmailChanged, required this.onPasswordChanged, this.onValidChanged, + this.emailValue, + this.passwordValue, }); final TextInputCallback onEmailChanged; final TextInputCallback onPasswordChanged; final Function(bool)? onValidChanged; + final String? emailValue; + final String? passwordValue; @override Widget build(BuildContext context) { @@ -33,6 +37,7 @@ class LoginInputCard extends HookWidget { children: [ const SizedBox(height: Dimens.l), TextInputField( + initialValue: emailValue, label: 'Email', onChanged: onEmailChanged, validator: (input) => _validate( @@ -43,6 +48,7 @@ class LoginInputCard extends HookWidget { ), const SizedBox(height: Dimens.xl), TextInputField( + initialValue: passwordValue, label: 'Password', onChanged: onPasswordChanged, isPassword: true, diff --git a/flutter_module/lib/presentation/widgets/text_input_field.dart b/flutter_module/lib/presentation/widgets/text_input_field.dart index c538f3a..fad2549 100644 --- a/flutter_module/lib/presentation/widgets/text_input_field.dart +++ b/flutter_module/lib/presentation/widgets/text_input_field.dart @@ -12,16 +12,18 @@ class TextInputField extends HookWidget { this.isPassword = false, this.onChanged, this.validator, + this.initialValue }); final String label; + final String? initialValue; final bool isPassword; final TextInputCallback? onChanged; final FormFieldValidator? validator; @override Widget build(BuildContext context) { - final controller = useTextEditingController(); + final controller = useTextEditingController(text: initialValue); final shouldShow = useState(false); final error = useState(null); final passwordVisible = shouldShow.value; From f3522a19fd0baff3855ca8b2d3f8e412719854b3 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Mon, 21 Oct 2024 22:48:16 +0200 Subject: [PATCH 04/23] chore: replace remote login with sqlite mock authentication --- app/build.gradle.kts | 10 ++++++- app/src/main/AndroidManifest.xml | 1 + app/src/main/java/dev/snipme/snipmeapp/App.kt | 5 ++-- .../dev/snipme/snipmeapp/di/ServiceModule.kt | 15 +++++++++++ .../dev/snipme/snipmeapp/di/UseCaseModule.kt | 4 +-- .../domain/auth/IdentifyUserUseCase.kt | 5 ++-- .../snipmeapp/domain/auth/LoginUseCase.kt | 12 ++++----- .../snipmeapp/domain/auth/RegisterUseCase.kt | 11 ++++---- .../domain/repository/auth/AuthRepository.kt | 2 +- .../repository/auth/AuthRepositoryReal.kt | 20 +++++++------- .../infrastructure/local/AppDatabase.kt | 27 +++++++++++++++++++ .../snipmeapp/infrastructure/local/UserDao.kt | 20 ++++++++++++++ .../infrastructure/local/UserEntry.kt | 10 +++++++ .../main/res/xml/network_security_config.xml | 2 +- 14 files changed, 112 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserDao.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserEntry.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ed89cf4..753034d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -113,11 +113,19 @@ dependencies { implementation(libs.logging.interceptor) implementation(libs.reactivenetwork.rx2) + //Local Storage + implementation(libs.androidx.room.runtime) + annotationProcessor(libs.androidx.room.compiler) + ksp(libs.androidx.room.compiler) + implementation(libs.androidx.room.rxjava2) + + // Utility Libraries implementation(libs.timber) implementation(libs.glide) ksp(libs.compiler) -// implementation(libs.codeview) + + //implementation(libs.codeview) implementation(libs.kodeview) // Testing Libraries diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ca695b1..e321917 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" + android:networkSecurityConfig="@xml/network_security_config" > { get().create(SnippetService::class.java) } single { get().create(LanguageService::class.java) } single { get().create(ShareService::class.java) } + + + // Room database initialization + single { + Room.databaseBuilder( + get(), AppDatabase::class.java, "app_database" + ).build() + } + + // Providing UserDao from the database + single { get().userDao() } + } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt index 837e586..59c3ebf 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt @@ -22,9 +22,9 @@ internal val useCaseModule = module { // Base factory { CheckNetworkAvailableUseCase(get()) } // Auth - factory { IdentifyUserUseCase(get(), get()) } + factory { IdentifyUserUseCase(get()) } factory { InitialLoginUseCase(get()) } - factory { LoginUseCase(get(), get()) } + factory { LoginUseCase(get()) } factory { RegisterUseCase(get(), get()) } factory { LogoutUserUseCase(get()) } factory { AuthorizationUseCase(get()) } diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/IdentifyUserUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/IdentifyUserUseCase.kt index 8b3cb9a..86e01d7 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/IdentifyUserUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/IdentifyUserUseCase.kt @@ -5,8 +5,7 @@ import dev.snipme.snipmeapp.domain.repository.auth.AuthRepository class IdentifyUserUseCase( private val auth: AuthRepository, - private val checkNetwork: CheckNetworkAvailableUseCase ) { - operator fun invoke(login: String) = checkNetwork() - .andThen(auth.identify(login)) + operator fun invoke(login: String) = + auth.identify(login) } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginUseCase.kt index c4d4809..b96b028 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginUseCase.kt @@ -6,12 +6,10 @@ import dev.snipme.snipmeapp.domain.repository.auth.AuthRepository class LoginUseCase( private val auth: AuthRepository, - private val checkNetwork: CheckNetworkAvailableUseCase ) { - operator fun invoke(login: String, password: String): Completable = checkNetwork() - .andThen( - auth.login(login, password) - .flatMapCompletable { token -> auth.saveToken(token) } - ) -} \ No newline at end of file + operator fun invoke(login: String, password: String): Completable = + auth.login(login, password) + .flatMapCompletable { token -> auth.saveToken(token) } +} + diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/RegisterUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/RegisterUseCase.kt index 431a68d..7925204 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/RegisterUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/RegisterUseCase.kt @@ -6,13 +6,14 @@ import dev.snipme.snipmeapp.domain.repository.auth.AuthRepository class RegisterUseCase( private val auth: AuthRepository, - private val checkNetwork: CheckNetworkAvailableUseCase + private val loginUseCase: LoginUseCase, ) { operator fun invoke(login: String, password: String, email: String): Completable = - checkNetwork().andThen(register(login, password, email)) + register(login, password, email) private fun register(login: String, password: String, email: String) = - auth.register(login, password, email) - .flatMap { auth.login(login, password) } - .flatMapCompletable { token -> auth.saveToken(token) } + auth.register(login, password, email).andThen( + loginUseCase(login, password) + ) + } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepository.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepository.kt index 7050dd9..cd55769 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepository.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepository.kt @@ -11,7 +11,7 @@ interface AuthRepository { fun login(login: String, password: String): Single - fun register(login: String, password: String, email: String): Single + fun register(login: String, password: String, email: String): Completable fun saveToken(token: String): Completable diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepositoryReal.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepositoryReal.kt index b048579..8d45b3e 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepositoryReal.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepositoryReal.kt @@ -1,32 +1,34 @@ package dev.snipme.snipmeapp.domain.repository.auth +import android.util.Log import io.reactivex.Completable import io.reactivex.Maybe import dev.snipme.snipmeapp.domain.error.ErrorHandler import dev.snipme.snipmeapp.infrastructure.local.AuthPreferences -import dev.snipme.snipmeapp.infrastructure.model.request.IdentifyUserRequest -import dev.snipme.snipmeapp.infrastructure.model.request.LoginUserRequest -import dev.snipme.snipmeapp.infrastructure.model.request.RegisterUserRequest -import dev.snipme.snipmeapp.infrastructure.remote.AuthService +import dev.snipme.snipmeapp.infrastructure.local.UserEntry +import dev.snipme.snipmeapp.infrastructure.local.UserDao import dev.snipme.snipmeapp.util.extension.mapError +import io.reactivex.Single class AuthRepositoryReal( private val errorHandler: ErrorHandler, - private val service: AuthService, + private val service: UserDao, private val prefs: AuthPreferences ) : AuthRepository { override fun identify(login: String) = - service.identify(IdentifyUserRequest(login)) + service.identify(login) .mapError { errorHandler.handle(it) } + .map { it -> it > 0 } + override fun login(login: String, password: String) = - service.login(LoginUserRequest(login, password)) + service.login(login, password) .mapError { errorHandler.handle(it) } - .map { it.token } + .map { it } override fun register(login: String, password: String, email: String) = - service.register(RegisterUserRequest(login, password, email)) + service.register(UserEntry(email, password)) .mapError { errorHandler.handle(it) } override fun saveToken(token: String) = Completable.fromCallable { diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt new file mode 100644 index 0000000..484fae4 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt @@ -0,0 +1,27 @@ +package dev.snipme.snipmeapp.infrastructure.local +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import android.content.Context + +@Database(entities = [UserEntry::class], version = 1) +abstract class AppDatabase : RoomDatabase() { + abstract fun userDao(): UserDao + + companion object { + @Volatile + private var INSTANCE: AppDatabase? = null + + fun getDatabase(context: Context): AppDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + "app_database" + ).build() + INSTANCE = instance + instance + } + } + } +} diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserDao.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserDao.kt new file mode 100644 index 0000000..b0b2b2f --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserDao.kt @@ -0,0 +1,20 @@ +package dev.snipme.snipmeapp.infrastructure.local + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.reactivex.Completable +import io.reactivex.Single + +@Dao +interface UserDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun register(user: UserEntry): Completable + + @Query("SELECT password FROM users WHERE email = :email AND password = :password") + fun login(email: String, password: String): Single + + @Query("SELECT COUNT(*) FROM users WHERE email = :email") + fun identify(email: String): Single +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserEntry.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserEntry.kt new file mode 100644 index 0000000..700c055 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserEntry.kt @@ -0,0 +1,10 @@ +package dev.snipme.snipmeapp.infrastructure.local + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "users") +data class UserEntry( + @PrimaryKey val email: String, + val password: String +) \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml index d97c26b..a64b080 100644 --- a/app/src/main/res/xml/network_security_config.xml +++ b/app/src/main/res/xml/network_security_config.xml @@ -4,4 +4,4 @@ clients3.google.com 91.195.93.3 - \ No newline at end of file + From 7b706bbed69e492a6f3f6cc7f3b0fb4a2460da96 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Tue, 22 Oct 2024 23:17:44 +0200 Subject: [PATCH 05/23] fix: Debug logging --- app/src/main/java/dev/snipme/snipmeapp/App.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/dev/snipme/snipmeapp/App.kt b/app/src/main/java/dev/snipme/snipmeapp/App.kt index d2bf820..76e714a 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/App.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/App.kt @@ -1,7 +1,7 @@ package dev.snipme.snipmeapp import android.app.Application -import androidx.multidex.BuildConfig +import dev.snipme.snipmeapp.BuildConfig.* import dev.snipme.snipmeapp.di.koinModules import dev.snipme.snipmeapp.util.CrashReportingTree import org.koin.android.ext.koin.androidContext @@ -27,7 +27,7 @@ class App : Application() { } private fun initLogs() { - if (BuildConfig.DEBUG) { + if (DEBUG) { Timber.plant(Timber.DebugTree()) } else { Timber.plant(CrashReportingTree()) From b0497d1c773e34251a2a29962c5e2831c5d2b8f3 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Thu, 24 Oct 2024 14:39:08 +0200 Subject: [PATCH 06/23] chore: replace remote user doc with sqlite mock --- .../snipme/snipmeapp/bridge/main/MainModel.kt | 7 ++++--- .../dev/snipme/snipmeapp/di/UseCaseModule.kt | 2 +- .../snipmeapp/domain/auth/LoginUseCase.kt | 2 +- .../domain/repository/auth/AuthRepository.kt | 2 +- .../repository/auth/AuthRepositoryReal.kt | 7 ++++++- .../domain/repository/user/UserRepository.kt | 2 +- .../repository/user/UserRepositoryReal.kt | 8 ++++---- .../domain/user/GetSingleUserUseCase.kt | 18 ++++++++++++------ .../dev/snipme/snipmeapp/domain/user/User.kt | 8 ++++++++ .../snipmeapp/infrastructure/local/UserDao.kt | 8 ++++++-- .../infrastructure/local/UserEntry.kt | 7 +++++-- 11 files changed, 49 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModel.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModel.kt index 6be39e8..e397251 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModel.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModel.kt @@ -75,6 +75,7 @@ class MainModel( scopes = listOf("All", "Private", "Public"), selectedScope = "All" ) + getUser() .subscribeOn(Schedulers.io()) .subscribeBy( @@ -162,7 +163,7 @@ class MainModel( } sealed class MainViewState -object Loading : MainViewState() +data object Loading : MainViewState() data class Loaded( val user: User, val snippets: List, @@ -173,6 +174,6 @@ data class Loaded( data class Error(val message: String?) : MainViewState() sealed class MainEvent -object Startup : MainEvent() +data object Startup : MainEvent() data class Alert(val message: String) : MainEvent() -object Logout : MainEvent() \ No newline at end of file +data object Logout : MainEvent() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt index 59c3ebf..5c84484 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt @@ -29,7 +29,7 @@ internal val useCaseModule = module { factory { LogoutUserUseCase(get()) } factory { AuthorizationUseCase(get()) } // User - factory { GetSingleUserUseCase(get(), get(), get()) } + factory { GetSingleUserUseCase(get(), get(), get(), get()) } // Snippet factory { GetSnippetsUseCase(get(), get(), get()) } factory { GetSingleSnippetUseCase(get(), get(), get()) } diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginUseCase.kt index b96b028..6153f63 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/auth/LoginUseCase.kt @@ -10,6 +10,6 @@ class LoginUseCase( operator fun invoke(login: String, password: String): Completable = auth.login(login, password) - .flatMapCompletable { token -> auth.saveToken(token) } + .flatMapCompletable { token -> auth.saveToken(token.toString()) } } diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepository.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepository.kt index cd55769..ccc413d 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepository.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepository.kt @@ -9,7 +9,7 @@ interface AuthRepository { fun identify(login: String): Single - fun login(login: String, password: String): Single + fun login(login: String, password: String): Single fun register(login: String, password: String, email: String): Completable diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepositoryReal.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepositoryReal.kt index 8d45b3e..d17d49e 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepositoryReal.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/auth/AuthRepositoryReal.kt @@ -28,7 +28,12 @@ class AuthRepositoryReal( .map { it } override fun register(login: String, password: String, email: String) = - service.register(UserEntry(email, password)) + service.register(UserEntry( + email = email, + password = password, + login = email.substring(0, email.indexOf('@')), + photo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRrRHyzylz9-x3vO04G9qyWdOwOjtfsmtaubQ&s" + )) .mapError { errorHandler.handle(it) } override fun saveToken(token: String) = Completable.fromCallable { diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepository.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepository.kt index 4ec5e5d..f6d8032 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepository.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepository.kt @@ -4,5 +4,5 @@ import io.reactivex.Single import dev.snipme.snipmeapp.domain.user.User interface UserRepository { - fun user(): Single + fun user(id: Int): Single } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepositoryReal.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepositoryReal.kt index 281ca0e..2e70c3c 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepositoryReal.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/user/UserRepositoryReal.kt @@ -1,18 +1,18 @@ package dev.snipme.snipmeapp.domain.repository.user -import io.reactivex.Single import dev.snipme.snipmeapp.domain.error.ErrorHandler import dev.snipme.snipmeapp.domain.user.User import dev.snipme.snipmeapp.domain.user.toUser -import dev.snipme.snipmeapp.infrastructure.remote.UserService +import dev.snipme.snipmeapp.infrastructure.local.UserDao import dev.snipme.snipmeapp.util.extension.mapError +import io.reactivex.Single class UserRepositoryReal( private val errorHandler: ErrorHandler, - private val service: UserService + private val service: UserDao ) : UserRepository { - override fun user(): Single = service.user() + override fun user(id: Int): Single = service.user(id) .mapError { errorHandler.handle(it) } .map { it.toUser() } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/user/GetSingleUserUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/user/GetSingleUserUseCase.kt index 566912e..57854e2 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/user/GetSingleUserUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/user/GetSingleUserUseCase.kt @@ -1,18 +1,24 @@ package dev.snipme.snipmeapp.domain.user +import android.annotation.SuppressLint import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.repository.auth.AuthRepository import dev.snipme.snipmeapp.domain.repository.user.UserRepository +import io.reactivex.Single class GetSingleUserUseCase( private val auth: AuthorizationUseCase, private val networkAvailable: CheckNetworkAvailableUseCase, - private val repository: UserRepository + private val repository: UserRepository, + private val authRepository: AuthRepository, ) { - - operator fun invoke() = - auth() + @SuppressLint("CheckResult") + operator fun invoke(): Single { + var token: Int = 0 + authRepository.getToken().subscribe { value -> token = value.toInt() } + return auth() .andThen(networkAvailable()) - .andThen(repository.user()) - + .andThen(repository.user(token)) + } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/user/User.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/user/User.kt index 7ade0b0..88804ca 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/user/User.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/user/User.kt @@ -1,6 +1,7 @@ package dev.snipme.snipmeapp.domain.user import androidx.annotation.VisibleForTesting +import dev.snipme.snipmeapp.infrastructure.local.UserEntry import dev.snipme.snipmeapp.infrastructure.model.response.PersonResponse data class User(val id: Int, val login: String, val email: String, val photo: String) { @@ -15,4 +16,11 @@ fun PersonResponse.toUser() = User( login = username ?: throw IllegalArgumentException("User must have a login!"), email = email ?: "", photo = photo ?: "" +) + +fun UserEntry.toUser() = User( + id = id, + login = login, + email = email, + photo = photo ) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserDao.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserDao.kt index b0b2b2f..ad72231 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserDao.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserDao.kt @@ -12,9 +12,13 @@ interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun register(user: UserEntry): Completable - @Query("SELECT password FROM users WHERE email = :email AND password = :password") - fun login(email: String, password: String): Single + @Query("SELECT id FROM users WHERE email = :email AND password = :password") + fun login(email: String, password: String): Single @Query("SELECT COUNT(*) FROM users WHERE email = :email") fun identify(email: String): Single + + @Query("SELECT * FROM users WHERE id = :id") + fun user(id: Int) : Single + } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserEntry.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserEntry.kt index 700c055..7fee638 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserEntry.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/UserEntry.kt @@ -5,6 +5,9 @@ import androidx.room.PrimaryKey @Entity(tableName = "users") data class UserEntry( - @PrimaryKey val email: String, - val password: String + @PrimaryKey(true) val id: Int = 0, + val email: String, + val password: String, + val login: String, + val photo: String ) \ No newline at end of file From b9e25d901b0133cb0af47f790bda9539092285f1 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Sat, 2 Nov 2024 13:52:53 +0100 Subject: [PATCH 07/23] chore: replace remote snippets doc with sqlite mock --- .../dev/snipme/snipmeapp/bridge/Bridge.java | 2245 +++++++++++------ .../snipme/snipmeapp/bridge/ModelPlugin.kt | 2 +- .../bridge/detail/DetailModelPlugin.kt | 6 +- .../bridge/login/LoginModelPlugin.kt | 2 +- .../snipmeapp/bridge/main/MainModelPlugin.kt | 4 +- .../dev/snipme/snipmeapp/di/ServiceModule.kt | 4 +- .../dev/snipme/snipmeapp/di/UseCaseModule.kt | 2 +- .../domain/reaction/SetUserReactionUseCase.kt | 7 +- .../repository/snippet/SnippetRepository.kt | 12 +- .../snippet/SnippetRepositoryReal.kt | 88 +- .../snippet/SnippetRepositoryTest.kt | 333 +-- .../domain/snippet/CreateSnippetUseCase.kt | 22 +- .../domain/snippet/GetSingleSnippetUseCase.kt | 8 +- .../ObserveUpdatedSnippetPageUseCase.kt | 11 +- .../domain/snippet/UpdateSnippetUseCase.kt | 9 +- .../domain/snippets/GetSnippetsUseCase.kt | 12 +- .../snippets/HasMoreSnippetPagesUseCase.kt | 2 +- .../domain/snippets/SnippetResponseMapper.kt | 28 +- .../domain/user/GetSingleUserUseCase.kt | 2 +- .../infrastructure/local/AppDatabase.kt | 3 +- .../infrastructure/local/SnippetDao.kt | 50 + .../infrastructure/local/SnippetEntity.kt | 28 + flutter_module/lib/model/main_model.dart | 1336 +++++----- .../presentation/screens/details_screen.dart | 2 +- .../lib/presentation/screens/main_screen.dart | 2 +- gradle.properties | 3 +- 26 files changed, 2493 insertions(+), 1730 deletions(-) create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetEntity.kt diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/Bridge.java b/app/src/main/java/dev/snipme/snipmeapp/bridge/Bridge.java index f92c17d..b16744b 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/bridge/Bridge.java +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/Bridge.java @@ -1,8 +1,11 @@ -// Autogenerated from Pigeon (v4.2.3), do not edit directly. +// Autogenerated from Pigeon (v22.5.0), do not edit directly. // See also: https://pub.dev/packages/pigeon package dev.snipme.snipmeapp.bridge; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.CLASS; + import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -11,18 +14,59 @@ import io.flutter.plugin.common.MessageCodec; import io.flutter.plugin.common.StandardMessageCodec; import java.io.ByteArrayOutputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.nio.ByteBuffer; -import java.util.Arrays; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.HashMap; +import java.util.Objects; -/**Generated class from Pigeon. */ -@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) public class Bridge { + /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ + public static class FlutterError extends RuntimeException { + + /** The error code. */ + public final String code; + + /** The error details. Must be a datatype supported by the api codec. */ + public final Object details; + + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) + { + super(message); + this.code = code; + this.details = details; + } + } + + @NonNull + protected static ArrayList wrapError(@NonNull Throwable exception) { + ArrayList errorList = new ArrayList<>(3); + if (exception instanceof FlutterError) { + FlutterError error = (FlutterError) exception; + errorList.add(error.code); + errorList.add(error.getMessage()); + errorList.add(error.details); + } else { + errorList.add(exception.toString()); + errorList.add(exception.getClass().getSimpleName()); + errorList.add( + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + } + return errorList; + } + + @Target(METHOD) + @Retention(CLASS) + @interface CanIgnoreReturnValue {} + public enum SnippetLanguageType { C(0), CPP(1), @@ -70,8 +114,9 @@ public enum SnippetLanguageType { REGEX(43), UNKNOWN(44); - private int index; - private SnippetLanguageType(final int index) { + final int index; + + SnippetLanguageType(final int index) { this.index = index; } } @@ -81,8 +126,9 @@ public enum SnippetFilterType { MINE(1), SHARED(2); - private int index; - private SnippetFilterType(final int index) { + final int index; + + SnippetFilterType(final int index) { this.index = index; } } @@ -92,8 +138,9 @@ public enum UserReaction { LIKE(1), DISLIKE(2); - private int index; - private UserReaction(final int index) { + final int index; + + UserReaction(final int index) { this.index = index; } } @@ -103,8 +150,9 @@ public enum ModelState { LOADED(1), ERROR(2); - private int index; - private ModelState(final int index) { + final int index; + + ModelState(final int index) { this.index = index; } } @@ -114,8 +162,9 @@ public enum MainModelEvent { ALERT(1), LOGOUT(2); - private int index; - private MainModelEvent(final int index) { + final int index; + + MainModelEvent(final int index) { this.index = index; } } @@ -125,8 +174,9 @@ public enum DetailModelEvent { SAVED(1), DELETED(2); - private int index; - private DetailModelEvent(final int index) { + final int index; + + DetailModelEvent(final int index) { this.index = index; } } @@ -135,169 +185,282 @@ public enum LoginModelEvent { NONE(0), LOGGED(1); - private int index; - private LoginModelEvent(final int index) { + final int index; + + LoginModelEvent(final int index) { this.index = index; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class Snippet { + public static final class Snippet { private @Nullable String uuid; - public @Nullable String getUuid() { return uuid; } + + public @Nullable String getUuid() { + return uuid; + } + public void setUuid(@Nullable String setterArg) { this.uuid = setterArg; } private @Nullable String title; - public @Nullable String getTitle() { return title; } + + public @Nullable String getTitle() { + return title; + } + public void setTitle(@Nullable String setterArg) { this.title = setterArg; } private @Nullable SnippetCode code; - public @Nullable SnippetCode getCode() { return code; } + + public @Nullable SnippetCode getCode() { + return code; + } + public void setCode(@Nullable SnippetCode setterArg) { this.code = setterArg; } private @Nullable SnippetLanguage language; - public @Nullable SnippetLanguage getLanguage() { return language; } + + public @Nullable SnippetLanguage getLanguage() { + return language; + } + public void setLanguage(@Nullable SnippetLanguage setterArg) { this.language = setterArg; } private @Nullable Owner owner; - public @Nullable Owner getOwner() { return owner; } + + public @Nullable Owner getOwner() { + return owner; + } + public void setOwner(@Nullable Owner setterArg) { this.owner = setterArg; } private @Nullable Boolean isOwner; - public @Nullable Boolean getIsOwner() { return isOwner; } + + public @Nullable Boolean getIsOwner() { + return isOwner; + } + public void setIsOwner(@Nullable Boolean setterArg) { this.isOwner = setterArg; } private @Nullable String timeAgo; - public @Nullable String getTimeAgo() { return timeAgo; } + + public @Nullable String getTimeAgo() { + return timeAgo; + } + public void setTimeAgo(@Nullable String setterArg) { this.timeAgo = setterArg; } private @Nullable Long voteResult; - public @Nullable Long getVoteResult() { return voteResult; } + + public @Nullable Long getVoteResult() { + return voteResult; + } + public void setVoteResult(@Nullable Long setterArg) { this.voteResult = setterArg; } private @Nullable UserReaction userReaction; - public @Nullable UserReaction getUserReaction() { return userReaction; } + + public @Nullable UserReaction getUserReaction() { + return userReaction; + } + public void setUserReaction(@Nullable UserReaction setterArg) { this.userReaction = setterArg; } private @Nullable Boolean isPrivate; - public @Nullable Boolean getIsPrivate() { return isPrivate; } + + public @Nullable Boolean getIsPrivate() { + return isPrivate; + } + public void setIsPrivate(@Nullable Boolean setterArg) { this.isPrivate = setterArg; } private @Nullable Boolean isLiked; - public @Nullable Boolean getIsLiked() { return isLiked; } + + public @Nullable Boolean getIsLiked() { + return isLiked; + } + public void setIsLiked(@Nullable Boolean setterArg) { this.isLiked = setterArg; } private @Nullable Boolean isDisliked; - public @Nullable Boolean getIsDisliked() { return isDisliked; } + + public @Nullable Boolean getIsDisliked() { + return isDisliked; + } + public void setIsDisliked(@Nullable Boolean setterArg) { this.isDisliked = setterArg; } private @Nullable Boolean isSaved; - public @Nullable Boolean getIsSaved() { return isSaved; } + + public @Nullable Boolean getIsSaved() { + return isSaved; + } + public void setIsSaved(@Nullable Boolean setterArg) { this.isSaved = setterArg; } private @Nullable Boolean isToDelete; - public @Nullable Boolean getIsToDelete() { return isToDelete; } + + public @Nullable Boolean getIsToDelete() { + return isToDelete; + } + public void setIsToDelete(@Nullable Boolean setterArg) { this.isToDelete = setterArg; } + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + Snippet that = (Snippet) o; + return Objects.equals(uuid, that.uuid) && Objects.equals(title, that.title) && Objects.equals(code, that.code) && Objects.equals(language, that.language) && Objects.equals(owner, that.owner) && Objects.equals(isOwner, that.isOwner) && Objects.equals(timeAgo, that.timeAgo) && Objects.equals(voteResult, that.voteResult) && Objects.equals(userReaction, that.userReaction) && Objects.equals(isPrivate, that.isPrivate) && Objects.equals(isLiked, that.isLiked) && Objects.equals(isDisliked, that.isDisliked) && Objects.equals(isSaved, that.isSaved) && Objects.equals(isToDelete, that.isToDelete); + } + + @Override + public int hashCode() { + return Objects.hash(uuid, title, code, language, owner, isOwner, timeAgo, voteResult, userReaction, isPrivate, isLiked, isDisliked, isSaved, isToDelete); + } + public static final class Builder { + private @Nullable String uuid; + + @CanIgnoreReturnValue public @NonNull Builder setUuid(@Nullable String setterArg) { this.uuid = setterArg; return this; } + private @Nullable String title; + + @CanIgnoreReturnValue public @NonNull Builder setTitle(@Nullable String setterArg) { this.title = setterArg; return this; } + private @Nullable SnippetCode code; + + @CanIgnoreReturnValue public @NonNull Builder setCode(@Nullable SnippetCode setterArg) { this.code = setterArg; return this; } + private @Nullable SnippetLanguage language; + + @CanIgnoreReturnValue public @NonNull Builder setLanguage(@Nullable SnippetLanguage setterArg) { this.language = setterArg; return this; } + private @Nullable Owner owner; + + @CanIgnoreReturnValue public @NonNull Builder setOwner(@Nullable Owner setterArg) { this.owner = setterArg; return this; } + private @Nullable Boolean isOwner; + + @CanIgnoreReturnValue public @NonNull Builder setIsOwner(@Nullable Boolean setterArg) { this.isOwner = setterArg; return this; } + private @Nullable String timeAgo; + + @CanIgnoreReturnValue public @NonNull Builder setTimeAgo(@Nullable String setterArg) { this.timeAgo = setterArg; return this; } + private @Nullable Long voteResult; + + @CanIgnoreReturnValue public @NonNull Builder setVoteResult(@Nullable Long setterArg) { this.voteResult = setterArg; return this; } + private @Nullable UserReaction userReaction; + + @CanIgnoreReturnValue public @NonNull Builder setUserReaction(@Nullable UserReaction setterArg) { this.userReaction = setterArg; return this; } + private @Nullable Boolean isPrivate; + + @CanIgnoreReturnValue public @NonNull Builder setIsPrivate(@Nullable Boolean setterArg) { this.isPrivate = setterArg; return this; } + private @Nullable Boolean isLiked; + + @CanIgnoreReturnValue public @NonNull Builder setIsLiked(@Nullable Boolean setterArg) { this.isLiked = setterArg; return this; } + private @Nullable Boolean isDisliked; + + @CanIgnoreReturnValue public @NonNull Builder setIsDisliked(@Nullable Boolean setterArg) { this.isDisliked = setterArg; return this; } + private @Nullable Boolean isSaved; + + @CanIgnoreReturnValue public @NonNull Builder setIsSaved(@Nullable Boolean setterArg) { this.isSaved = setterArg; return this; } + private @Nullable Boolean isToDelete; + + @CanIgnoreReturnValue public @NonNull Builder setIsToDelete(@Nullable Boolean setterArg) { this.isToDelete = setterArg; return this; } + public @NonNull Snippet build() { Snippet pigeonReturn = new Snippet(); pigeonReturn.setUuid(uuid); @@ -317,83 +480,114 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("uuid", uuid); - toMapResult.put("title", title); - toMapResult.put("code", (code == null) ? null : code.toMap()); - toMapResult.put("language", (language == null) ? null : language.toMap()); - toMapResult.put("owner", (owner == null) ? null : owner.toMap()); - toMapResult.put("isOwner", isOwner); - toMapResult.put("timeAgo", timeAgo); - toMapResult.put("voteResult", voteResult); - toMapResult.put("userReaction", userReaction == null ? null : userReaction.index); - toMapResult.put("isPrivate", isPrivate); - toMapResult.put("isLiked", isLiked); - toMapResult.put("isDisliked", isDisliked); - toMapResult.put("isSaved", isSaved); - toMapResult.put("isToDelete", isToDelete); - return toMapResult; - } - static @NonNull Snippet fromMap(@NonNull Map map) { + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(14); + toListResult.add(uuid); + toListResult.add(title); + toListResult.add(code); + toListResult.add(language); + toListResult.add(owner); + toListResult.add(isOwner); + toListResult.add(timeAgo); + toListResult.add(voteResult); + toListResult.add(userReaction); + toListResult.add(isPrivate); + toListResult.add(isLiked); + toListResult.add(isDisliked); + toListResult.add(isSaved); + toListResult.add(isToDelete); + return toListResult; + } + + static @NonNull Snippet fromList(@NonNull ArrayList pigeonVar_list) { Snippet pigeonResult = new Snippet(); - Object uuid = map.get("uuid"); - pigeonResult.setUuid((String)uuid); - Object title = map.get("title"); - pigeonResult.setTitle((String)title); - Object code = map.get("code"); - pigeonResult.setCode((code == null) ? null : SnippetCode.fromMap((Map)code)); - Object language = map.get("language"); - pigeonResult.setLanguage((language == null) ? null : SnippetLanguage.fromMap((Map)language)); - Object owner = map.get("owner"); - pigeonResult.setOwner((owner == null) ? null : Owner.fromMap((Map)owner)); - Object isOwner = map.get("isOwner"); - pigeonResult.setIsOwner((Boolean)isOwner); - Object timeAgo = map.get("timeAgo"); - pigeonResult.setTimeAgo((String)timeAgo); - Object voteResult = map.get("voteResult"); - pigeonResult.setVoteResult((voteResult == null) ? null : ((voteResult instanceof Integer) ? (Integer)voteResult : (Long)voteResult)); - Object userReaction = map.get("userReaction"); - pigeonResult.setUserReaction(userReaction == null ? null : UserReaction.values()[(int)userReaction]); - Object isPrivate = map.get("isPrivate"); - pigeonResult.setIsPrivate((Boolean)isPrivate); - Object isLiked = map.get("isLiked"); - pigeonResult.setIsLiked((Boolean)isLiked); - Object isDisliked = map.get("isDisliked"); - pigeonResult.setIsDisliked((Boolean)isDisliked); - Object isSaved = map.get("isSaved"); - pigeonResult.setIsSaved((Boolean)isSaved); - Object isToDelete = map.get("isToDelete"); - pigeonResult.setIsToDelete((Boolean)isToDelete); + Object uuid = pigeonVar_list.get(0); + pigeonResult.setUuid((String) uuid); + Object title = pigeonVar_list.get(1); + pigeonResult.setTitle((String) title); + Object code = pigeonVar_list.get(2); + pigeonResult.setCode((SnippetCode) code); + Object language = pigeonVar_list.get(3); + pigeonResult.setLanguage((SnippetLanguage) language); + Object owner = pigeonVar_list.get(4); + pigeonResult.setOwner((Owner) owner); + Object isOwner = pigeonVar_list.get(5); + pigeonResult.setIsOwner((Boolean) isOwner); + Object timeAgo = pigeonVar_list.get(6); + pigeonResult.setTimeAgo((String) timeAgo); + Object voteResult = pigeonVar_list.get(7); + pigeonResult.setVoteResult((Long) voteResult); + Object userReaction = pigeonVar_list.get(8); + pigeonResult.setUserReaction((UserReaction) userReaction); + Object isPrivate = pigeonVar_list.get(9); + pigeonResult.setIsPrivate((Boolean) isPrivate); + Object isLiked = pigeonVar_list.get(10); + pigeonResult.setIsLiked((Boolean) isLiked); + Object isDisliked = pigeonVar_list.get(11); + pigeonResult.setIsDisliked((Boolean) isDisliked); + Object isSaved = pigeonVar_list.get(12); + pigeonResult.setIsSaved((Boolean) isSaved); + Object isToDelete = pigeonVar_list.get(13); + pigeonResult.setIsToDelete((Boolean) isToDelete); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class SnippetCode { + public static final class SnippetCode { private @Nullable String raw; - public @Nullable String getRaw() { return raw; } + + public @Nullable String getRaw() { + return raw; + } + public void setRaw(@Nullable String setterArg) { this.raw = setterArg; } private @Nullable List tokens; - public @Nullable List getTokens() { return tokens; } + + public @Nullable List getTokens() { + return tokens; + } + public void setTokens(@Nullable List setterArg) { this.tokens = setterArg; } + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + SnippetCode that = (SnippetCode) o; + return Objects.equals(raw, that.raw) && Objects.equals(tokens, that.tokens); + } + + @Override + public int hashCode() { + return Objects.hash(raw, tokens); + } + public static final class Builder { + private @Nullable String raw; + + @CanIgnoreReturnValue public @NonNull Builder setRaw(@Nullable String setterArg) { this.raw = setterArg; return this; } + private @Nullable List tokens; + + @CanIgnoreReturnValue public @NonNull Builder setTokens(@Nullable List setterArg) { this.tokens = setterArg; return this; } + public @NonNull SnippetCode build() { SnippetCode pigeonReturn = new SnippetCode(); pigeonReturn.setRaw(raw); @@ -401,58 +595,96 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("raw", raw); - toMapResult.put("tokens", tokens); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(2); + toListResult.add(raw); + toListResult.add(tokens); + return toListResult; } - static @NonNull SnippetCode fromMap(@NonNull Map map) { + + static @NonNull SnippetCode fromList(@NonNull ArrayList pigeonVar_list) { SnippetCode pigeonResult = new SnippetCode(); - Object raw = map.get("raw"); - pigeonResult.setRaw((String)raw); - Object tokens = map.get("tokens"); - pigeonResult.setTokens((List)tokens); + Object raw = pigeonVar_list.get(0); + pigeonResult.setRaw((String) raw); + Object tokens = pigeonVar_list.get(1); + pigeonResult.setTokens((List) tokens); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class SyntaxToken { + public static final class SyntaxToken { private @Nullable Long start; - public @Nullable Long getStart() { return start; } + + public @Nullable Long getStart() { + return start; + } + public void setStart(@Nullable Long setterArg) { this.start = setterArg; } private @Nullable Long end; - public @Nullable Long getEnd() { return end; } + + public @Nullable Long getEnd() { + return end; + } + public void setEnd(@Nullable Long setterArg) { this.end = setterArg; } private @Nullable Long color; - public @Nullable Long getColor() { return color; } + + public @Nullable Long getColor() { + return color; + } + public void setColor(@Nullable Long setterArg) { this.color = setterArg; } + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + SyntaxToken that = (SyntaxToken) o; + return Objects.equals(start, that.start) && Objects.equals(end, that.end) && Objects.equals(color, that.color); + } + + @Override + public int hashCode() { + return Objects.hash(start, end, color); + } + public static final class Builder { + private @Nullable Long start; + + @CanIgnoreReturnValue public @NonNull Builder setStart(@Nullable Long setterArg) { this.start = setterArg; return this; } + private @Nullable Long end; + + @CanIgnoreReturnValue public @NonNull Builder setEnd(@Nullable Long setterArg) { this.end = setterArg; return this; } + private @Nullable Long color; + + @CanIgnoreReturnValue public @NonNull Builder setColor(@Nullable Long setterArg) { this.color = setterArg; return this; } + public @NonNull SyntaxToken build() { SyntaxToken pigeonReturn = new SyntaxToken(); pigeonReturn.setStart(start); @@ -461,50 +693,81 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("start", start); - toMapResult.put("end", end); - toMapResult.put("color", color); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(3); + toListResult.add(start); + toListResult.add(end); + toListResult.add(color); + return toListResult; } - static @NonNull SyntaxToken fromMap(@NonNull Map map) { + + static @NonNull SyntaxToken fromList(@NonNull ArrayList pigeonVar_list) { SyntaxToken pigeonResult = new SyntaxToken(); - Object start = map.get("start"); - pigeonResult.setStart((start == null) ? null : ((start instanceof Integer) ? (Integer)start : (Long)start)); - Object end = map.get("end"); - pigeonResult.setEnd((end == null) ? null : ((end instanceof Integer) ? (Integer)end : (Long)end)); - Object color = map.get("color"); - pigeonResult.setColor((color == null) ? null : ((color instanceof Integer) ? (Integer)color : (Long)color)); + Object start = pigeonVar_list.get(0); + pigeonResult.setStart((Long) start); + Object end = pigeonVar_list.get(1); + pigeonResult.setEnd((Long) end); + Object color = pigeonVar_list.get(2); + pigeonResult.setColor((Long) color); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class SnippetLanguage { + public static final class SnippetLanguage { private @Nullable String raw; - public @Nullable String getRaw() { return raw; } + + public @Nullable String getRaw() { + return raw; + } + public void setRaw(@Nullable String setterArg) { this.raw = setterArg; } private @Nullable SnippetLanguageType type; - public @Nullable SnippetLanguageType getType() { return type; } + + public @Nullable SnippetLanguageType getType() { + return type; + } + public void setType(@Nullable SnippetLanguageType setterArg) { this.type = setterArg; } + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + SnippetLanguage that = (SnippetLanguage) o; + return Objects.equals(raw, that.raw) && Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + return Objects.hash(raw, type); + } + public static final class Builder { + private @Nullable String raw; + + @CanIgnoreReturnValue public @NonNull Builder setRaw(@Nullable String setterArg) { this.raw = setterArg; return this; } + private @Nullable SnippetLanguageType type; + + @CanIgnoreReturnValue public @NonNull Builder setType(@Nullable SnippetLanguageType setterArg) { this.type = setterArg; return this; } + public @NonNull SnippetLanguage build() { SnippetLanguage pigeonReturn = new SnippetLanguage(); pigeonReturn.setRaw(raw); @@ -512,47 +775,78 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("raw", raw); - toMapResult.put("type", type == null ? null : type.index); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(2); + toListResult.add(raw); + toListResult.add(type); + return toListResult; } - static @NonNull SnippetLanguage fromMap(@NonNull Map map) { + + static @NonNull SnippetLanguage fromList(@NonNull ArrayList pigeonVar_list) { SnippetLanguage pigeonResult = new SnippetLanguage(); - Object raw = map.get("raw"); - pigeonResult.setRaw((String)raw); - Object type = map.get("type"); - pigeonResult.setType(type == null ? null : SnippetLanguageType.values()[(int)type]); + Object raw = pigeonVar_list.get(0); + pigeonResult.setRaw((String) raw); + Object type = pigeonVar_list.get(1); + pigeonResult.setType((SnippetLanguageType) type); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class Owner { + public static final class Owner { private @Nullable Long id; - public @Nullable Long getId() { return id; } + + public @Nullable Long getId() { + return id; + } + public void setId(@Nullable Long setterArg) { this.id = setterArg; } private @Nullable String login; - public @Nullable String getLogin() { return login; } + + public @Nullable String getLogin() { + return login; + } + public void setLogin(@Nullable String setterArg) { this.login = setterArg; } + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + Owner that = (Owner) o; + return Objects.equals(id, that.id) && Objects.equals(login, that.login); + } + + @Override + public int hashCode() { + return Objects.hash(id, login); + } + public static final class Builder { + private @Nullable Long id; + + @CanIgnoreReturnValue public @NonNull Builder setId(@Nullable Long setterArg) { this.id = setterArg; return this; } + private @Nullable String login; + + @CanIgnoreReturnValue public @NonNull Builder setLogin(@Nullable String setterArg) { this.login = setterArg; return this; } + public @NonNull Owner build() { Owner pigeonReturn = new Owner(); pigeonReturn.setId(id); @@ -560,69 +854,114 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("id", id); - toMapResult.put("login", login); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(2); + toListResult.add(id); + toListResult.add(login); + return toListResult; } - static @NonNull Owner fromMap(@NonNull Map map) { + + static @NonNull Owner fromList(@NonNull ArrayList pigeonVar_list) { Owner pigeonResult = new Owner(); - Object id = map.get("id"); - pigeonResult.setId((id == null) ? null : ((id instanceof Integer) ? (Integer)id : (Long)id)); - Object login = map.get("login"); - pigeonResult.setLogin((String)login); + Object id = pigeonVar_list.get(0); + pigeonResult.setId((Long) id); + Object login = pigeonVar_list.get(1); + pigeonResult.setLogin((String) login); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class SnippetFilter { + public static final class SnippetFilter { private @Nullable List languages; - public @Nullable List getLanguages() { return languages; } + + public @Nullable List getLanguages() { + return languages; + } + public void setLanguages(@Nullable List setterArg) { this.languages = setterArg; } private @Nullable List selectedLanguages; - public @Nullable List getSelectedLanguages() { return selectedLanguages; } + + public @Nullable List getSelectedLanguages() { + return selectedLanguages; + } + public void setSelectedLanguages(@Nullable List setterArg) { this.selectedLanguages = setterArg; } private @Nullable List scopes; - public @Nullable List getScopes() { return scopes; } + + public @Nullable List getScopes() { + return scopes; + } + public void setScopes(@Nullable List setterArg) { this.scopes = setterArg; } private @Nullable String selectedScope; - public @Nullable String getSelectedScope() { return selectedScope; } + + public @Nullable String getSelectedScope() { + return selectedScope; + } + public void setSelectedScope(@Nullable String setterArg) { this.selectedScope = setterArg; } + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + SnippetFilter that = (SnippetFilter) o; + return Objects.equals(languages, that.languages) && Objects.equals(selectedLanguages, that.selectedLanguages) && Objects.equals(scopes, that.scopes) && Objects.equals(selectedScope, that.selectedScope); + } + + @Override + public int hashCode() { + return Objects.hash(languages, selectedLanguages, scopes, selectedScope); + } + public static final class Builder { + private @Nullable List languages; + + @CanIgnoreReturnValue public @NonNull Builder setLanguages(@Nullable List setterArg) { this.languages = setterArg; return this; } + private @Nullable List selectedLanguages; + + @CanIgnoreReturnValue public @NonNull Builder setSelectedLanguages(@Nullable List setterArg) { this.selectedLanguages = setterArg; return this; } + private @Nullable List scopes; + + @CanIgnoreReturnValue public @NonNull Builder setScopes(@Nullable List setterArg) { this.scopes = setterArg; return this; } + private @Nullable String selectedScope; + + @CanIgnoreReturnValue public @NonNull Builder setSelectedScope(@Nullable String setterArg) { this.selectedScope = setterArg; return this; } + public @NonNull SnippetFilter build() { SnippetFilter pigeonReturn = new SnippetFilter(); pigeonReturn.setLanguages(languages); @@ -632,112 +971,178 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("languages", languages); - toMapResult.put("selectedLanguages", selectedLanguages); - toMapResult.put("scopes", scopes); - toMapResult.put("selectedScope", selectedScope); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(4); + toListResult.add(languages); + toListResult.add(selectedLanguages); + toListResult.add(scopes); + toListResult.add(selectedScope); + return toListResult; } - static @NonNull SnippetFilter fromMap(@NonNull Map map) { + + static @NonNull SnippetFilter fromList(@NonNull ArrayList pigeonVar_list) { SnippetFilter pigeonResult = new SnippetFilter(); - Object languages = map.get("languages"); - pigeonResult.setLanguages((List)languages); - Object selectedLanguages = map.get("selectedLanguages"); - pigeonResult.setSelectedLanguages((List)selectedLanguages); - Object scopes = map.get("scopes"); - pigeonResult.setScopes((List)scopes); - Object selectedScope = map.get("selectedScope"); - pigeonResult.setSelectedScope((String)selectedScope); + Object languages = pigeonVar_list.get(0); + pigeonResult.setLanguages((List) languages); + Object selectedLanguages = pigeonVar_list.get(1); + pigeonResult.setSelectedLanguages((List) selectedLanguages); + Object scopes = pigeonVar_list.get(2); + pigeonResult.setScopes((List) scopes); + Object selectedScope = pigeonVar_list.get(3); + pigeonResult.setSelectedScope((String) selectedScope); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class MainModelStateData { + public static final class MainModelStateData { private @Nullable ModelState state; - public @Nullable ModelState getState() { return state; } + + public @Nullable ModelState getState() { + return state; + } + public void setState(@Nullable ModelState setterArg) { this.state = setterArg; } - private @Nullable Boolean is_loading; - public @Nullable Boolean getIs_loading() { return is_loading; } - public void setIs_loading(@Nullable Boolean setterArg) { - this.is_loading = setterArg; + private @Nullable Boolean isLoading; + + public @Nullable Boolean getIsLoading() { + return isLoading; + } + + public void setIsLoading(@Nullable Boolean setterArg) { + this.isLoading = setterArg; } private @Nullable List data; - public @Nullable List getData() { return data; } + + public @Nullable List getData() { + return data; + } + public void setData(@Nullable List setterArg) { this.data = setterArg; } private @Nullable SnippetFilter filter; - public @Nullable SnippetFilter getFilter() { return filter; } + + public @Nullable SnippetFilter getFilter() { + return filter; + } + public void setFilter(@Nullable SnippetFilter setterArg) { this.filter = setterArg; } private @Nullable String error; - public @Nullable String getError() { return error; } + + public @Nullable String getError() { + return error; + } + public void setError(@Nullable String setterArg) { this.error = setterArg; } private @Nullable Long oldHash; - public @Nullable Long getOldHash() { return oldHash; } + + public @Nullable Long getOldHash() { + return oldHash; + } + public void setOldHash(@Nullable Long setterArg) { this.oldHash = setterArg; } private @Nullable Long newHash; - public @Nullable Long getNewHash() { return newHash; } + + public @Nullable Long getNewHash() { + return newHash; + } + public void setNewHash(@Nullable Long setterArg) { this.newHash = setterArg; } + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + MainModelStateData that = (MainModelStateData) o; + return Objects.equals(state, that.state) && Objects.equals(isLoading, that.isLoading) && Objects.equals(data, that.data) && Objects.equals(filter, that.filter) && Objects.equals(error, that.error) && Objects.equals(oldHash, that.oldHash) && Objects.equals(newHash, that.newHash); + } + + @Override + public int hashCode() { + return Objects.hash(state, isLoading, data, filter, error, oldHash, newHash); + } + public static final class Builder { + private @Nullable ModelState state; + + @CanIgnoreReturnValue public @NonNull Builder setState(@Nullable ModelState setterArg) { this.state = setterArg; return this; } - private @Nullable Boolean is_loading; - public @NonNull Builder setIs_loading(@Nullable Boolean setterArg) { - this.is_loading = setterArg; + + private @Nullable Boolean isLoading; + + @CanIgnoreReturnValue + public @NonNull Builder setIsLoading(@Nullable Boolean setterArg) { + this.isLoading = setterArg; return this; } + private @Nullable List data; + + @CanIgnoreReturnValue public @NonNull Builder setData(@Nullable List setterArg) { this.data = setterArg; return this; } + private @Nullable SnippetFilter filter; + + @CanIgnoreReturnValue public @NonNull Builder setFilter(@Nullable SnippetFilter setterArg) { this.filter = setterArg; return this; } + private @Nullable String error; + + @CanIgnoreReturnValue public @NonNull Builder setError(@Nullable String setterArg) { this.error = setterArg; return this; } + private @Nullable Long oldHash; + + @CanIgnoreReturnValue public @NonNull Builder setOldHash(@Nullable Long setterArg) { this.oldHash = setterArg; return this; } + private @Nullable Long newHash; + + @CanIgnoreReturnValue public @NonNull Builder setNewHash(@Nullable Long setterArg) { this.newHash = setterArg; return this; } + public @NonNull MainModelStateData build() { MainModelStateData pigeonReturn = new MainModelStateData(); pigeonReturn.setState(state); - pigeonReturn.setIs_loading(is_loading); + pigeonReturn.setIsLoading(isLoading); pigeonReturn.setData(data); pigeonReturn.setFilter(filter); pigeonReturn.setError(error); @@ -746,84 +1151,129 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("state", state == null ? null : state.index); - toMapResult.put("is_loading", is_loading); - toMapResult.put("data", data); - toMapResult.put("filter", (filter == null) ? null : filter.toMap()); - toMapResult.put("error", error); - toMapResult.put("oldHash", oldHash); - toMapResult.put("newHash", newHash); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(7); + toListResult.add(state); + toListResult.add(isLoading); + toListResult.add(data); + toListResult.add(filter); + toListResult.add(error); + toListResult.add(oldHash); + toListResult.add(newHash); + return toListResult; } - static @NonNull MainModelStateData fromMap(@NonNull Map map) { + + static @NonNull MainModelStateData fromList(@NonNull ArrayList pigeonVar_list) { MainModelStateData pigeonResult = new MainModelStateData(); - Object state = map.get("state"); - pigeonResult.setState(state == null ? null : ModelState.values()[(int)state]); - Object is_loading = map.get("is_loading"); - pigeonResult.setIs_loading((Boolean)is_loading); - Object data = map.get("data"); - pigeonResult.setData((List)data); - Object filter = map.get("filter"); - pigeonResult.setFilter((filter == null) ? null : SnippetFilter.fromMap((Map)filter)); - Object error = map.get("error"); - pigeonResult.setError((String)error); - Object oldHash = map.get("oldHash"); - pigeonResult.setOldHash((oldHash == null) ? null : ((oldHash instanceof Integer) ? (Integer)oldHash : (Long)oldHash)); - Object newHash = map.get("newHash"); - pigeonResult.setNewHash((newHash == null) ? null : ((newHash instanceof Integer) ? (Integer)newHash : (Long)newHash)); + Object state = pigeonVar_list.get(0); + pigeonResult.setState((ModelState) state); + Object isLoading = pigeonVar_list.get(1); + pigeonResult.setIsLoading((Boolean) isLoading); + Object data = pigeonVar_list.get(2); + pigeonResult.setData((List) data); + Object filter = pigeonVar_list.get(3); + pigeonResult.setFilter((SnippetFilter) filter); + Object error = pigeonVar_list.get(4); + pigeonResult.setError((String) error); + Object oldHash = pigeonVar_list.get(5); + pigeonResult.setOldHash((Long) oldHash); + Object newHash = pigeonVar_list.get(6); + pigeonResult.setNewHash((Long) newHash); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class MainModelEventData { + public static final class MainModelEventData { private @Nullable MainModelEvent event; - public @Nullable MainModelEvent getEvent() { return event; } + + public @Nullable MainModelEvent getEvent() { + return event; + } + public void setEvent(@Nullable MainModelEvent setterArg) { this.event = setterArg; } private @Nullable String message; - public @Nullable String getMessage() { return message; } + + public @Nullable String getMessage() { + return message; + } + public void setMessage(@Nullable String setterArg) { this.message = setterArg; } private @Nullable Long oldHash; - public @Nullable Long getOldHash() { return oldHash; } + + public @Nullable Long getOldHash() { + return oldHash; + } + public void setOldHash(@Nullable Long setterArg) { this.oldHash = setterArg; } private @Nullable Long newHash; - public @Nullable Long getNewHash() { return newHash; } + + public @Nullable Long getNewHash() { + return newHash; + } + public void setNewHash(@Nullable Long setterArg) { this.newHash = setterArg; } + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + MainModelEventData that = (MainModelEventData) o; + return Objects.equals(event, that.event) && Objects.equals(message, that.message) && Objects.equals(oldHash, that.oldHash) && Objects.equals(newHash, that.newHash); + } + + @Override + public int hashCode() { + return Objects.hash(event, message, oldHash, newHash); + } + public static final class Builder { + private @Nullable MainModelEvent event; + + @CanIgnoreReturnValue public @NonNull Builder setEvent(@Nullable MainModelEvent setterArg) { this.event = setterArg; return this; } + private @Nullable String message; + + @CanIgnoreReturnValue public @NonNull Builder setMessage(@Nullable String setterArg) { this.message = setterArg; return this; } + private @Nullable Long oldHash; + + @CanIgnoreReturnValue public @NonNull Builder setOldHash(@Nullable Long setterArg) { this.oldHash = setterArg; return this; } + private @Nullable Long newHash; + + @CanIgnoreReturnValue public @NonNull Builder setNewHash(@Nullable Long setterArg) { this.newHash = setterArg; return this; } + public @NonNull MainModelEventData build() { MainModelEventData pigeonReturn = new MainModelEventData(); pigeonReturn.setEvent(event); @@ -833,101 +1283,160 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("event", event == null ? null : event.index); - toMapResult.put("message", message); - toMapResult.put("oldHash", oldHash); - toMapResult.put("newHash", newHash); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(4); + toListResult.add(event); + toListResult.add(message); + toListResult.add(oldHash); + toListResult.add(newHash); + return toListResult; } - static @NonNull MainModelEventData fromMap(@NonNull Map map) { + + static @NonNull MainModelEventData fromList(@NonNull ArrayList pigeonVar_list) { MainModelEventData pigeonResult = new MainModelEventData(); - Object event = map.get("event"); - pigeonResult.setEvent(event == null ? null : MainModelEvent.values()[(int)event]); - Object message = map.get("message"); - pigeonResult.setMessage((String)message); - Object oldHash = map.get("oldHash"); - pigeonResult.setOldHash((oldHash == null) ? null : ((oldHash instanceof Integer) ? (Integer)oldHash : (Long)oldHash)); - Object newHash = map.get("newHash"); - pigeonResult.setNewHash((newHash == null) ? null : ((newHash instanceof Integer) ? (Integer)newHash : (Long)newHash)); + Object event = pigeonVar_list.get(0); + pigeonResult.setEvent((MainModelEvent) event); + Object message = pigeonVar_list.get(1); + pigeonResult.setMessage((String) message); + Object oldHash = pigeonVar_list.get(2); + pigeonResult.setOldHash((Long) oldHash); + Object newHash = pigeonVar_list.get(3); + pigeonResult.setNewHash((Long) newHash); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class DetailModelStateData { + public static final class DetailModelStateData { private @Nullable ModelState state; - public @Nullable ModelState getState() { return state; } + + public @Nullable ModelState getState() { + return state; + } + public void setState(@Nullable ModelState setterArg) { this.state = setterArg; } - private @Nullable Boolean is_loading; - public @Nullable Boolean getIs_loading() { return is_loading; } - public void setIs_loading(@Nullable Boolean setterArg) { - this.is_loading = setterArg; + private @Nullable Boolean isLoading; + + public @Nullable Boolean getIsLoading() { + return isLoading; + } + + public void setIsLoading(@Nullable Boolean setterArg) { + this.isLoading = setterArg; } private @Nullable Snippet data; - public @Nullable Snippet getData() { return data; } + + public @Nullable Snippet getData() { + return data; + } + public void setData(@Nullable Snippet setterArg) { this.data = setterArg; } private @Nullable String error; - public @Nullable String getError() { return error; } + + public @Nullable String getError() { + return error; + } + public void setError(@Nullable String setterArg) { this.error = setterArg; } private @Nullable Long oldHash; - public @Nullable Long getOldHash() { return oldHash; } + + public @Nullable Long getOldHash() { + return oldHash; + } + public void setOldHash(@Nullable Long setterArg) { this.oldHash = setterArg; } private @Nullable Long newHash; - public @Nullable Long getNewHash() { return newHash; } + + public @Nullable Long getNewHash() { + return newHash; + } + public void setNewHash(@Nullable Long setterArg) { this.newHash = setterArg; } + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + DetailModelStateData that = (DetailModelStateData) o; + return Objects.equals(state, that.state) && Objects.equals(isLoading, that.isLoading) && Objects.equals(data, that.data) && Objects.equals(error, that.error) && Objects.equals(oldHash, that.oldHash) && Objects.equals(newHash, that.newHash); + } + + @Override + public int hashCode() { + return Objects.hash(state, isLoading, data, error, oldHash, newHash); + } + public static final class Builder { + private @Nullable ModelState state; + + @CanIgnoreReturnValue public @NonNull Builder setState(@Nullable ModelState setterArg) { this.state = setterArg; return this; } - private @Nullable Boolean is_loading; - public @NonNull Builder setIs_loading(@Nullable Boolean setterArg) { - this.is_loading = setterArg; + + private @Nullable Boolean isLoading; + + @CanIgnoreReturnValue + public @NonNull Builder setIsLoading(@Nullable Boolean setterArg) { + this.isLoading = setterArg; return this; } + private @Nullable Snippet data; + + @CanIgnoreReturnValue public @NonNull Builder setData(@Nullable Snippet setterArg) { this.data = setterArg; return this; } + private @Nullable String error; + + @CanIgnoreReturnValue public @NonNull Builder setError(@Nullable String setterArg) { this.error = setterArg; return this; } + private @Nullable Long oldHash; + + @CanIgnoreReturnValue public @NonNull Builder setOldHash(@Nullable Long setterArg) { this.oldHash = setterArg; return this; } + private @Nullable Long newHash; + + @CanIgnoreReturnValue public @NonNull Builder setNewHash(@Nullable Long setterArg) { this.newHash = setterArg; return this; } + public @NonNull DetailModelStateData build() { DetailModelStateData pigeonReturn = new DetailModelStateData(); pigeonReturn.setState(state); - pigeonReturn.setIs_loading(is_loading); + pigeonReturn.setIsLoading(isLoading); pigeonReturn.setData(data); pigeonReturn.setError(error); pigeonReturn.setOldHash(oldHash); @@ -935,81 +1444,126 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("state", state == null ? null : state.index); - toMapResult.put("is_loading", is_loading); - toMapResult.put("data", (data == null) ? null : data.toMap()); - toMapResult.put("error", error); - toMapResult.put("oldHash", oldHash); - toMapResult.put("newHash", newHash); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(6); + toListResult.add(state); + toListResult.add(isLoading); + toListResult.add(data); + toListResult.add(error); + toListResult.add(oldHash); + toListResult.add(newHash); + return toListResult; } - static @NonNull DetailModelStateData fromMap(@NonNull Map map) { + + static @NonNull DetailModelStateData fromList(@NonNull ArrayList pigeonVar_list) { DetailModelStateData pigeonResult = new DetailModelStateData(); - Object state = map.get("state"); - pigeonResult.setState(state == null ? null : ModelState.values()[(int)state]); - Object is_loading = map.get("is_loading"); - pigeonResult.setIs_loading((Boolean)is_loading); - Object data = map.get("data"); - pigeonResult.setData((data == null) ? null : Snippet.fromMap((Map)data)); - Object error = map.get("error"); - pigeonResult.setError((String)error); - Object oldHash = map.get("oldHash"); - pigeonResult.setOldHash((oldHash == null) ? null : ((oldHash instanceof Integer) ? (Integer)oldHash : (Long)oldHash)); - Object newHash = map.get("newHash"); - pigeonResult.setNewHash((newHash == null) ? null : ((newHash instanceof Integer) ? (Integer)newHash : (Long)newHash)); + Object state = pigeonVar_list.get(0); + pigeonResult.setState((ModelState) state); + Object isLoading = pigeonVar_list.get(1); + pigeonResult.setIsLoading((Boolean) isLoading); + Object data = pigeonVar_list.get(2); + pigeonResult.setData((Snippet) data); + Object error = pigeonVar_list.get(3); + pigeonResult.setError((String) error); + Object oldHash = pigeonVar_list.get(4); + pigeonResult.setOldHash((Long) oldHash); + Object newHash = pigeonVar_list.get(5); + pigeonResult.setNewHash((Long) newHash); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class DetailModelEventData { + public static final class DetailModelEventData { private @Nullable DetailModelEvent event; - public @Nullable DetailModelEvent getEvent() { return event; } + + public @Nullable DetailModelEvent getEvent() { + return event; + } + public void setEvent(@Nullable DetailModelEvent setterArg) { this.event = setterArg; } private @Nullable String value; - public @Nullable String getValue() { return value; } + + public @Nullable String getValue() { + return value; + } + public void setValue(@Nullable String setterArg) { this.value = setterArg; } private @Nullable Long oldHash; - public @Nullable Long getOldHash() { return oldHash; } + + public @Nullable Long getOldHash() { + return oldHash; + } + public void setOldHash(@Nullable Long setterArg) { this.oldHash = setterArg; } private @Nullable Long newHash; - public @Nullable Long getNewHash() { return newHash; } + + public @Nullable Long getNewHash() { + return newHash; + } + public void setNewHash(@Nullable Long setterArg) { this.newHash = setterArg; } + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + DetailModelEventData that = (DetailModelEventData) o; + return Objects.equals(event, that.event) && Objects.equals(value, that.value) && Objects.equals(oldHash, that.oldHash) && Objects.equals(newHash, that.newHash); + } + + @Override + public int hashCode() { + return Objects.hash(event, value, oldHash, newHash); + } + public static final class Builder { + private @Nullable DetailModelEvent event; + + @CanIgnoreReturnValue public @NonNull Builder setEvent(@Nullable DetailModelEvent setterArg) { this.event = setterArg; return this; } + private @Nullable String value; + + @CanIgnoreReturnValue public @NonNull Builder setValue(@Nullable String setterArg) { this.value = setterArg; return this; } + private @Nullable Long oldHash; + + @CanIgnoreReturnValue public @NonNull Builder setOldHash(@Nullable Long setterArg) { this.oldHash = setterArg; return this; } + private @Nullable Long newHash; + + @CanIgnoreReturnValue public @NonNull Builder setNewHash(@Nullable Long setterArg) { this.newHash = setterArg; return this; } + public @NonNull DetailModelEventData build() { DetailModelEventData pigeonReturn = new DetailModelEventData(); pigeonReturn.setEvent(event); @@ -1019,142 +1573,225 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("event", event == null ? null : event.index); - toMapResult.put("value", value); - toMapResult.put("oldHash", oldHash); - toMapResult.put("newHash", newHash); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(4); + toListResult.add(event); + toListResult.add(value); + toListResult.add(oldHash); + toListResult.add(newHash); + return toListResult; } - static @NonNull DetailModelEventData fromMap(@NonNull Map map) { + + static @NonNull DetailModelEventData fromList(@NonNull ArrayList pigeonVar_list) { DetailModelEventData pigeonResult = new DetailModelEventData(); - Object event = map.get("event"); - pigeonResult.setEvent(event == null ? null : DetailModelEvent.values()[(int)event]); - Object value = map.get("value"); - pigeonResult.setValue((String)value); - Object oldHash = map.get("oldHash"); - pigeonResult.setOldHash((oldHash == null) ? null : ((oldHash instanceof Integer) ? (Integer)oldHash : (Long)oldHash)); - Object newHash = map.get("newHash"); - pigeonResult.setNewHash((newHash == null) ? null : ((newHash instanceof Integer) ? (Integer)newHash : (Long)newHash)); + Object event = pigeonVar_list.get(0); + pigeonResult.setEvent((DetailModelEvent) event); + Object value = pigeonVar_list.get(1); + pigeonResult.setValue((String) value); + Object oldHash = pigeonVar_list.get(2); + pigeonResult.setOldHash((Long) oldHash); + Object newHash = pigeonVar_list.get(3); + pigeonResult.setNewHash((Long) newHash); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class LoginModelStateData { + public static final class LoginModelStateData { private @Nullable ModelState state; - public @Nullable ModelState getState() { return state; } + + public @Nullable ModelState getState() { + return state; + } + public void setState(@Nullable ModelState setterArg) { this.state = setterArg; } - private @Nullable Boolean is_loading; - public @Nullable Boolean getIs_loading() { return is_loading; } - public void setIs_loading(@Nullable Boolean setterArg) { - this.is_loading = setterArg; + private @Nullable Boolean isLoading; + + public @Nullable Boolean getIsLoading() { + return isLoading; + } + + public void setIsLoading(@Nullable Boolean setterArg) { + this.isLoading = setterArg; } private @Nullable Long oldHash; - public @Nullable Long getOldHash() { return oldHash; } + + public @Nullable Long getOldHash() { + return oldHash; + } + public void setOldHash(@Nullable Long setterArg) { this.oldHash = setterArg; } private @Nullable Long newHash; - public @Nullable Long getNewHash() { return newHash; } + + public @Nullable Long getNewHash() { + return newHash; + } + public void setNewHash(@Nullable Long setterArg) { this.newHash = setterArg; } + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + LoginModelStateData that = (LoginModelStateData) o; + return Objects.equals(state, that.state) && Objects.equals(isLoading, that.isLoading) && Objects.equals(oldHash, that.oldHash) && Objects.equals(newHash, that.newHash); + } + + @Override + public int hashCode() { + return Objects.hash(state, isLoading, oldHash, newHash); + } + public static final class Builder { + private @Nullable ModelState state; + + @CanIgnoreReturnValue public @NonNull Builder setState(@Nullable ModelState setterArg) { this.state = setterArg; return this; } - private @Nullable Boolean is_loading; - public @NonNull Builder setIs_loading(@Nullable Boolean setterArg) { - this.is_loading = setterArg; + + private @Nullable Boolean isLoading; + + @CanIgnoreReturnValue + public @NonNull Builder setIsLoading(@Nullable Boolean setterArg) { + this.isLoading = setterArg; return this; } + private @Nullable Long oldHash; + + @CanIgnoreReturnValue public @NonNull Builder setOldHash(@Nullable Long setterArg) { this.oldHash = setterArg; return this; } + private @Nullable Long newHash; + + @CanIgnoreReturnValue public @NonNull Builder setNewHash(@Nullable Long setterArg) { this.newHash = setterArg; return this; } + public @NonNull LoginModelStateData build() { LoginModelStateData pigeonReturn = new LoginModelStateData(); pigeonReturn.setState(state); - pigeonReturn.setIs_loading(is_loading); + pigeonReturn.setIsLoading(isLoading); pigeonReturn.setOldHash(oldHash); pigeonReturn.setNewHash(newHash); return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("state", state == null ? null : state.index); - toMapResult.put("is_loading", is_loading); - toMapResult.put("oldHash", oldHash); - toMapResult.put("newHash", newHash); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(4); + toListResult.add(state); + toListResult.add(isLoading); + toListResult.add(oldHash); + toListResult.add(newHash); + return toListResult; } - static @NonNull LoginModelStateData fromMap(@NonNull Map map) { + + static @NonNull LoginModelStateData fromList(@NonNull ArrayList pigeonVar_list) { LoginModelStateData pigeonResult = new LoginModelStateData(); - Object state = map.get("state"); - pigeonResult.setState(state == null ? null : ModelState.values()[(int)state]); - Object is_loading = map.get("is_loading"); - pigeonResult.setIs_loading((Boolean)is_loading); - Object oldHash = map.get("oldHash"); - pigeonResult.setOldHash((oldHash == null) ? null : ((oldHash instanceof Integer) ? (Integer)oldHash : (Long)oldHash)); - Object newHash = map.get("newHash"); - pigeonResult.setNewHash((newHash == null) ? null : ((newHash instanceof Integer) ? (Integer)newHash : (Long)newHash)); + Object state = pigeonVar_list.get(0); + pigeonResult.setState((ModelState) state); + Object isLoading = pigeonVar_list.get(1); + pigeonResult.setIsLoading((Boolean) isLoading); + Object oldHash = pigeonVar_list.get(2); + pigeonResult.setOldHash((Long) oldHash); + Object newHash = pigeonVar_list.get(3); + pigeonResult.setNewHash((Long) newHash); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class LoginModelEventData { + public static final class LoginModelEventData { private @Nullable LoginModelEvent event; - public @Nullable LoginModelEvent getEvent() { return event; } + + public @Nullable LoginModelEvent getEvent() { + return event; + } + public void setEvent(@Nullable LoginModelEvent setterArg) { this.event = setterArg; } private @Nullable Long oldHash; - public @Nullable Long getOldHash() { return oldHash; } + + public @Nullable Long getOldHash() { + return oldHash; + } + public void setOldHash(@Nullable Long setterArg) { this.oldHash = setterArg; } private @Nullable Long newHash; - public @Nullable Long getNewHash() { return newHash; } + + public @Nullable Long getNewHash() { + return newHash; + } + public void setNewHash(@Nullable Long setterArg) { this.newHash = setterArg; } + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + LoginModelEventData that = (LoginModelEventData) o; + return Objects.equals(event, that.event) && Objects.equals(oldHash, that.oldHash) && Objects.equals(newHash, that.newHash); + } + + @Override + public int hashCode() { + return Objects.hash(event, oldHash, newHash); + } + public static final class Builder { + private @Nullable LoginModelEvent event; + + @CanIgnoreReturnValue public @NonNull Builder setEvent(@Nullable LoginModelEvent setterArg) { this.event = setterArg; return this; } + private @Nullable Long oldHash; + + @CanIgnoreReturnValue public @NonNull Builder setOldHash(@Nullable Long setterArg) { this.oldHash = setterArg; return this; } + private @Nullable Long newHash; + + @CanIgnoreReturnValue public @NonNull Builder setNewHash(@Nullable Long setterArg) { this.newHash = setterArg; return this; } + public @NonNull LoginModelEventData build() { LoginModelEventData pigeonReturn = new LoginModelEventData(); pigeonReturn.setEvent(event); @@ -1163,94 +1800,153 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("event", event == null ? null : event.index); - toMapResult.put("oldHash", oldHash); - toMapResult.put("newHash", newHash); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(3); + toListResult.add(event); + toListResult.add(oldHash); + toListResult.add(newHash); + return toListResult; } - static @NonNull LoginModelEventData fromMap(@NonNull Map map) { + + static @NonNull LoginModelEventData fromList(@NonNull ArrayList pigeonVar_list) { LoginModelEventData pigeonResult = new LoginModelEventData(); - Object event = map.get("event"); - pigeonResult.setEvent(event == null ? null : LoginModelEvent.values()[(int)event]); - Object oldHash = map.get("oldHash"); - pigeonResult.setOldHash((oldHash == null) ? null : ((oldHash instanceof Integer) ? (Integer)oldHash : (Long)oldHash)); - Object newHash = map.get("newHash"); - pigeonResult.setNewHash((newHash == null) ? null : ((newHash instanceof Integer) ? (Integer)newHash : (Long)newHash)); + Object event = pigeonVar_list.get(0); + pigeonResult.setEvent((LoginModelEvent) event); + Object oldHash = pigeonVar_list.get(1); + pigeonResult.setOldHash((Long) oldHash); + Object newHash = pigeonVar_list.get(2); + pigeonResult.setNewHash((Long) newHash); return pigeonResult; } } - private static class MainModelBridgeCodec extends StandardMessageCodec { - public static final MainModelBridgeCodec INSTANCE = new MainModelBridgeCodec(); - private MainModelBridgeCodec() {} + + private static class PigeonCodec extends StandardMessageCodec { + public static final PigeonCodec INSTANCE = new PigeonCodec(); + + private PigeonCodec() {} + @Override protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return MainModelEventData.fromMap((Map) readValue(buffer)); - - case (byte)129: - return MainModelStateData.fromMap((Map) readValue(buffer)); - - case (byte)130: - return Owner.fromMap((Map) readValue(buffer)); - - case (byte)131: - return Snippet.fromMap((Map) readValue(buffer)); - - case (byte)132: - return SnippetCode.fromMap((Map) readValue(buffer)); - - case (byte)133: - return SnippetFilter.fromMap((Map) readValue(buffer)); - - case (byte)134: - return SnippetLanguage.fromMap((Map) readValue(buffer)); - - case (byte)135: - return SyntaxToken.fromMap((Map) readValue(buffer)); - - default: + case (byte) 129: { + Object value = readValue(buffer); + return value == null ? null : SnippetLanguageType.values()[((Long) value).intValue()]; + } + case (byte) 130: { + Object value = readValue(buffer); + return value == null ? null : SnippetFilterType.values()[((Long) value).intValue()]; + } + case (byte) 131: { + Object value = readValue(buffer); + return value == null ? null : UserReaction.values()[((Long) value).intValue()]; + } + case (byte) 132: { + Object value = readValue(buffer); + return value == null ? null : ModelState.values()[((Long) value).intValue()]; + } + case (byte) 133: { + Object value = readValue(buffer); + return value == null ? null : MainModelEvent.values()[((Long) value).intValue()]; + } + case (byte) 134: { + Object value = readValue(buffer); + return value == null ? null : DetailModelEvent.values()[((Long) value).intValue()]; + } + case (byte) 135: { + Object value = readValue(buffer); + return value == null ? null : LoginModelEvent.values()[((Long) value).intValue()]; + } + case (byte) 136: + return Snippet.fromList((ArrayList) readValue(buffer)); + case (byte) 137: + return SnippetCode.fromList((ArrayList) readValue(buffer)); + case (byte) 138: + return SyntaxToken.fromList((ArrayList) readValue(buffer)); + case (byte) 139: + return SnippetLanguage.fromList((ArrayList) readValue(buffer)); + case (byte) 140: + return Owner.fromList((ArrayList) readValue(buffer)); + case (byte) 141: + return SnippetFilter.fromList((ArrayList) readValue(buffer)); + case (byte) 142: + return MainModelStateData.fromList((ArrayList) readValue(buffer)); + case (byte) 143: + return MainModelEventData.fromList((ArrayList) readValue(buffer)); + case (byte) 144: + return DetailModelStateData.fromList((ArrayList) readValue(buffer)); + case (byte) 145: + return DetailModelEventData.fromList((ArrayList) readValue(buffer)); + case (byte) 146: + return LoginModelStateData.fromList((ArrayList) readValue(buffer)); + case (byte) 147: + return LoginModelEventData.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof MainModelEventData) { - stream.write(128); - writeValue(stream, ((MainModelEventData) value).toMap()); - } else - if (value instanceof MainModelStateData) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof SnippetLanguageType) { stream.write(129); - writeValue(stream, ((MainModelStateData) value).toMap()); - } else - if (value instanceof Owner) { + writeValue(stream, value == null ? null : ((SnippetLanguageType) value).index); + } else if (value instanceof SnippetFilterType) { stream.write(130); - writeValue(stream, ((Owner) value).toMap()); - } else - if (value instanceof Snippet) { + writeValue(stream, value == null ? null : ((SnippetFilterType) value).index); + } else if (value instanceof UserReaction) { stream.write(131); - writeValue(stream, ((Snippet) value).toMap()); - } else - if (value instanceof SnippetCode) { + writeValue(stream, value == null ? null : ((UserReaction) value).index); + } else if (value instanceof ModelState) { stream.write(132); - writeValue(stream, ((SnippetCode) value).toMap()); - } else - if (value instanceof SnippetFilter) { + writeValue(stream, value == null ? null : ((ModelState) value).index); + } else if (value instanceof MainModelEvent) { stream.write(133); - writeValue(stream, ((SnippetFilter) value).toMap()); - } else - if (value instanceof SnippetLanguage) { + writeValue(stream, value == null ? null : ((MainModelEvent) value).index); + } else if (value instanceof DetailModelEvent) { stream.write(134); - writeValue(stream, ((SnippetLanguage) value).toMap()); - } else - if (value instanceof SyntaxToken) { + writeValue(stream, value == null ? null : ((DetailModelEvent) value).index); + } else if (value instanceof LoginModelEvent) { stream.write(135); - writeValue(stream, ((SyntaxToken) value).toMap()); - } else -{ + writeValue(stream, value == null ? null : ((LoginModelEvent) value).index); + } else if (value instanceof Snippet) { + stream.write(136); + writeValue(stream, ((Snippet) value).toList()); + } else if (value instanceof SnippetCode) { + stream.write(137); + writeValue(stream, ((SnippetCode) value).toList()); + } else if (value instanceof SyntaxToken) { + stream.write(138); + writeValue(stream, ((SyntaxToken) value).toList()); + } else if (value instanceof SnippetLanguage) { + stream.write(139); + writeValue(stream, ((SnippetLanguage) value).toList()); + } else if (value instanceof Owner) { + stream.write(140); + writeValue(stream, ((Owner) value).toList()); + } else if (value instanceof SnippetFilter) { + stream.write(141); + writeValue(stream, ((SnippetFilter) value).toList()); + } else if (value instanceof MainModelStateData) { + stream.write(142); + writeValue(stream, ((MainModelStateData) value).toList()); + } else if (value instanceof MainModelEventData) { + stream.write(143); + writeValue(stream, ((MainModelEventData) value).toList()); + } else if (value instanceof DetailModelStateData) { + stream.write(144); + writeValue(stream, ((DetailModelStateData) value).toList()); + } else if (value instanceof DetailModelEventData) { + stream.write(145); + writeValue(stream, ((DetailModelEventData) value).toList()); + } else if (value instanceof LoginModelStateData) { + stream.write(146); + writeValue(stream, ((LoginModelStateData) value).toList()); + } else if (value instanceof LoginModelEventData) { + stream.write(147); + writeValue(stream, ((LoginModelEventData) value).toList()); + } else { super.writeValue(stream, value); } } @@ -1258,72 +1954,92 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface MainModelBridge { - @NonNull MainModelStateData getState(); - @NonNull MainModelEventData getEvent(); + + @NonNull + MainModelStateData getState(); + + @NonNull + MainModelEventData getEvent(); + void resetEvent(); + void initState(); + void filterLanguage(@NonNull String language, @NonNull Boolean isSelected); + void filterScope(@NonNull String scope); + void logOut(); /** The codec used by MainModelBridge. */ - static MessageCodec getCodec() { - return MainModelBridgeCodec.INSTANCE; } + static @NonNull MessageCodec getCodec() { + return PigeonCodec.INSTANCE; + } /**Sets up an instance of `MainModelBridge` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, MainModelBridge api) { + static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable MainModelBridge api) { + setUp(binaryMessenger, "", api); + } + static void setUp(@NonNull BinaryMessenger binaryMessenger, @NonNull String messageChannelSuffix, @Nullable MainModelBridge api) { + messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.getState", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.MainModelBridge.getState" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - MainModelStateData output = api.getState(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + MainModelStateData output = api.getState(); + wrapped.add(0, output); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.getEvent", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.MainModelBridge.getEvent" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - MainModelEventData output = api.getEvent(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + MainModelEventData output = api.getEvent(); + wrapped.add(0, output); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.resetEvent", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.MainModelBridge.resetEvent" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.resetEvent(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.resetEvent(); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } @@ -1331,19 +2047,21 @@ static void setup(BinaryMessenger binaryMessenger, MainModelBridge api) { { BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.initState", getCodec(), taskQueue); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.MainModelBridge.initState" + messageChannelSuffix, getCodec(), taskQueue); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.initState(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.initState(); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } @@ -1351,29 +2069,24 @@ static void setup(BinaryMessenger binaryMessenger, MainModelBridge api) { { BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.filterLanguage", getCodec(), taskQueue); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.MainModelBridge.filterLanguage" + messageChannelSuffix, getCodec(), taskQueue); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - assert args != null; - String languageArg = (String)args.get(0); - if (languageArg == null) { - throw new NullPointerException("languageArg unexpectedly null."); - } - Boolean isSelectedArg = (Boolean)args.get(1); - if (isSelectedArg == null) { - throw new NullPointerException("isSelectedArg unexpectedly null."); - } - api.filterLanguage(languageArg, isSelectedArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + ArrayList args = (ArrayList) message; + String languageArg = (String) args.get(0); + Boolean isSelectedArg = (Boolean) args.get(1); + try { + api.filterLanguage(languageArg, isSelectedArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } @@ -1381,25 +2094,23 @@ static void setup(BinaryMessenger binaryMessenger, MainModelBridge api) { { BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.filterScope", getCodec(), taskQueue); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.MainModelBridge.filterScope" + messageChannelSuffix, getCodec(), taskQueue); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - assert args != null; - String scopeArg = (String)args.get(0); - if (scopeArg == null) { - throw new NullPointerException("scopeArg unexpectedly null."); - } - api.filterScope(scopeArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + ArrayList args = (ArrayList) message; + String scopeArg = (String) args.get(0); + try { + api.filterScope(scopeArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } @@ -1407,467 +2118,409 @@ static void setup(BinaryMessenger binaryMessenger, MainModelBridge api) { { BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.MainModelBridge.logOut", getCodec(), taskQueue); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.MainModelBridge.logOut" + messageChannelSuffix, getCodec(), taskQueue); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.logOut(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.logOut(); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } - private static class DetailModelBridgeCodec extends StandardMessageCodec { - public static final DetailModelBridgeCodec INSTANCE = new DetailModelBridgeCodec(); - private DetailModelBridgeCodec() {} - @Override - protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { - switch (type) { - case (byte)128: - return DetailModelEventData.fromMap((Map) readValue(buffer)); - - case (byte)129: - return DetailModelStateData.fromMap((Map) readValue(buffer)); - - case (byte)130: - return Owner.fromMap((Map) readValue(buffer)); - - case (byte)131: - return Snippet.fromMap((Map) readValue(buffer)); - - case (byte)132: - return SnippetCode.fromMap((Map) readValue(buffer)); - - case (byte)133: - return SnippetLanguage.fromMap((Map) readValue(buffer)); - - case (byte)134: - return SyntaxToken.fromMap((Map) readValue(buffer)); - - default: - return super.readValueOfType(type, buffer); - - } - } - @Override - protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof DetailModelEventData) { - stream.write(128); - writeValue(stream, ((DetailModelEventData) value).toMap()); - } else - if (value instanceof DetailModelStateData) { - stream.write(129); - writeValue(stream, ((DetailModelStateData) value).toMap()); - } else - if (value instanceof Owner) { - stream.write(130); - writeValue(stream, ((Owner) value).toMap()); - } else - if (value instanceof Snippet) { - stream.write(131); - writeValue(stream, ((Snippet) value).toMap()); - } else - if (value instanceof SnippetCode) { - stream.write(132); - writeValue(stream, ((SnippetCode) value).toMap()); - } else - if (value instanceof SnippetLanguage) { - stream.write(133); - writeValue(stream, ((SnippetLanguage) value).toMap()); - } else - if (value instanceof SyntaxToken) { - stream.write(134); - writeValue(stream, ((SyntaxToken) value).toMap()); - } else -{ - super.writeValue(stream, value); - } - } - } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface DetailModelBridge { - @NonNull DetailModelStateData getState(); - @NonNull DetailModelEventData getEvent(); + + @NonNull + DetailModelStateData getState(); + + @NonNull + DetailModelEventData getEvent(); + void resetEvent(); + void load(@NonNull String uuid); + void like(); + void dislike(); + void save(); + void copyToClipboard(); + void share(); + void delete(); /** The codec used by DetailModelBridge. */ - static MessageCodec getCodec() { - return DetailModelBridgeCodec.INSTANCE; } + static @NonNull MessageCodec getCodec() { + return PigeonCodec.INSTANCE; + } /**Sets up an instance of `DetailModelBridge` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, DetailModelBridge api) { + static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable DetailModelBridge api) { + setUp(binaryMessenger, "", api); + } + static void setUp(@NonNull BinaryMessenger binaryMessenger, @NonNull String messageChannelSuffix, @Nullable DetailModelBridge api) { + messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.getState", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.DetailModelBridge.getState" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - DetailModelStateData output = api.getState(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + DetailModelStateData output = api.getState(); + wrapped.add(0, output); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.getEvent", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.DetailModelBridge.getEvent" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - DetailModelEventData output = api.getEvent(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + DetailModelEventData output = api.getEvent(); + wrapped.add(0, output); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.resetEvent", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.DetailModelBridge.resetEvent" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.resetEvent(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.resetEvent(); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.load", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.DetailModelBridge.load" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - assert args != null; - String uuidArg = (String)args.get(0); - if (uuidArg == null) { - throw new NullPointerException("uuidArg unexpectedly null."); - } - api.load(uuidArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + ArrayList args = (ArrayList) message; + String uuidArg = (String) args.get(0); + try { + api.load(uuidArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.like", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.DetailModelBridge.like" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.like(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.like(); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.dislike", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.DetailModelBridge.dislike" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.dislike(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.dislike(); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.save", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.DetailModelBridge.save" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.save(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.save(); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.copyToClipboard", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.DetailModelBridge.copyToClipboard" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.copyToClipboard(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.copyToClipboard(); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.share", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.DetailModelBridge.share" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.share(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.share(); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DetailModelBridge.delete", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.DetailModelBridge.delete" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.delete(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.delete(); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } - private static class LoginModelBridgeCodec extends StandardMessageCodec { - public static final LoginModelBridgeCodec INSTANCE = new LoginModelBridgeCodec(); - private LoginModelBridgeCodec() {} - @Override - protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { - switch (type) { - case (byte)128: - return LoginModelEventData.fromMap((Map) readValue(buffer)); - - case (byte)129: - return LoginModelStateData.fromMap((Map) readValue(buffer)); - - default: - return super.readValueOfType(type, buffer); - - } - } - @Override - protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof LoginModelEventData) { - stream.write(128); - writeValue(stream, ((LoginModelEventData) value).toMap()); - } else - if (value instanceof LoginModelStateData) { - stream.write(129); - writeValue(stream, ((LoginModelStateData) value).toMap()); - } else -{ - super.writeValue(stream, value); - } - } - } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface LoginModelBridge { - @NonNull LoginModelStateData getState(); - @NonNull LoginModelEventData getEvent(); + + @NonNull + LoginModelStateData getState(); + + @NonNull + LoginModelEventData getEvent(); + void loginOrRegister(@NonNull String email, @NonNull String password); + void checkLoginState(); + void resetEvent(); /** The codec used by LoginModelBridge. */ - static MessageCodec getCodec() { - return LoginModelBridgeCodec.INSTANCE; } + static @NonNull MessageCodec getCodec() { + return PigeonCodec.INSTANCE; + } /**Sets up an instance of `LoginModelBridge` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, LoginModelBridge api) { + static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable LoginModelBridge api) { + setUp(binaryMessenger, "", api); + } + static void setUp(@NonNull BinaryMessenger binaryMessenger, @NonNull String messageChannelSuffix, @Nullable LoginModelBridge api) { + messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.LoginModelBridge.getState", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.LoginModelBridge.getState" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - LoginModelStateData output = api.getState(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + LoginModelStateData output = api.getState(); + wrapped.add(0, output); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.LoginModelBridge.getEvent", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.LoginModelBridge.getEvent" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - LoginModelEventData output = api.getEvent(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + LoginModelEventData output = api.getEvent(); + wrapped.add(0, output); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.LoginModelBridge.loginOrRegister", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.LoginModelBridge.loginOrRegister" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - assert args != null; - String emailArg = (String)args.get(0); - if (emailArg == null) { - throw new NullPointerException("emailArg unexpectedly null."); - } - String passwordArg = (String)args.get(1); - if (passwordArg == null) { - throw new NullPointerException("passwordArg unexpectedly null."); - } - api.loginOrRegister(emailArg, passwordArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + ArrayList args = (ArrayList) message; + String emailArg = (String) args.get(0); + String passwordArg = (String) args.get(1); + try { + api.loginOrRegister(emailArg, passwordArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.LoginModelBridge.checkLoginState", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.LoginModelBridge.checkLoginState" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.checkLoginState(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.checkLoginState(); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.LoginModelBridge.resetEvent", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.flutter_module.LoginModelBridge.resetEvent" + messageChannelSuffix, getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.resetEvent(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.resetEvent(); + wrapped.add(0, null); + } + catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } - @NonNull private static Map wrapError(@NonNull Throwable exception) { - Map errorMap = new HashMap<>(); - errorMap.put("message", exception.toString()); - errorMap.put("code", exception.getClass().getSimpleName()); - errorMap.put("details", "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); - return errorMap; - } } diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/ModelPlugin.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/ModelPlugin.kt index 965f687..734cf9a 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/bridge/ModelPlugin.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/ModelPlugin.kt @@ -15,7 +15,7 @@ import java.util.* flutter pub run pigeon \ --input bridge/main_model.dart \ --dart_out lib/model/main_model.dart \ - --java_out ../app/src/main/java/pl/tkadziolka/snipmeandroid/bridge/Bridge.java \ + --java_out ../app/src/main/java/dev/snipme/snipmeapp/bridge/Bridge.java \ --java_package "dev.snipme.snipmeapp.bridge" */ diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/detail/DetailModelPlugin.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/detail/DetailModelPlugin.kt index c7f04e0..a43651c 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/bridge/detail/DetailModelPlugin.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/detail/DetailModelPlugin.kt @@ -20,7 +20,7 @@ class DetailModelPlugin : ModelPlugin(), Bridge.Detail } override fun onSetup(messenger: BinaryMessenger, bridge: Bridge.DetailModelBridge?) { - Bridge.DetailModelBridge.setup(messenger, bridge) + Bridge.DetailModelBridge.setUp(messenger, bridge) } override fun load(uuid: String) { @@ -54,7 +54,7 @@ class DetailModelPlugin : ModelPlugin(), Bridge.Detail private fun getData(viewState: DetailViewState): Bridge.DetailModelStateData { return Bridge.DetailModelStateData().apply { state = viewState.toModelState() - is_loading = viewState is Loading + isLoading = viewState is Loading data = (viewState as? Loaded)?.snippet?.toModelData() oldHash = oldState?.hashCode()?.toLong() newHash = viewState.hashCode().toLong() @@ -66,7 +66,7 @@ class DetailModelPlugin : ModelPlugin(), Bridge.Detail private fun getEvent(detailEvent: DetailEvent): Bridge.DetailModelEventData { return Bridge.DetailModelEventData().apply { event = detailEvent.toModelEvent() - value = (detailEvent as? Saved)?.snippetId + value = (detailEvent as? Saved)?.snippetId.toString() oldHash = oldEvent?.hashCode()?.toLong() newHash = detailEvent.hashCode().toLong() }.also { diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/login/LoginModelPlugin.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/login/LoginModelPlugin.kt index 040a39f..ef323ee 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/bridge/login/LoginModelPlugin.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/login/LoginModelPlugin.kt @@ -19,7 +19,7 @@ class LoginModelPlugin : ModelPlugin(), Bridge.LoginMod } override fun onSetup(messenger: BinaryMessenger, bridge: Bridge.LoginModelBridge?) { - Bridge.LoginModelBridge.setup(messenger, bridge) + Bridge.LoginModelBridge.setUp(messenger, bridge) } override fun checkLoginState() { diff --git a/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModelPlugin.kt b/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModelPlugin.kt index aac58db..efb3cd4 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModelPlugin.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/bridge/main/MainModelPlugin.kt @@ -17,7 +17,7 @@ class MainModelPlugin : ModelPlugin(), Bridge.MainModelB messenger: BinaryMessenger, bridge: Bridge.MainModelBridge? ) { - Bridge.MainModelBridge.setup(messenger, bridge) + Bridge.MainModelBridge.setUp(messenger, bridge) } override fun getState(): Bridge.MainModelStateData = getState(model.state.value) @@ -47,7 +47,7 @@ class MainModelPlugin : ModelPlugin(), Bridge.MainModelB private fun getState(viewState: MainViewState): Bridge.MainModelStateData { return Bridge.MainModelStateData().apply { state = viewState.toModelState() - is_loading = viewState is Loading + isLoading = viewState is Loading data = (viewState as? Loaded)?.snippets?.toModelData() filter = (viewState as? Loaded)?.filters?.toModelFilter() oldHash = oldState?.hashCode()?.toLong() diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt index b20efd4..9c53d71 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt @@ -2,6 +2,7 @@ package dev.snipme.snipmeapp.di import androidx.room.Room import dev.snipme.snipmeapp.infrastructure.local.AppDatabase +import dev.snipme.snipmeapp.infrastructure.local.SnippetDao import dev.snipme.snipmeapp.infrastructure.local.UserDao import org.koin.dsl.module import dev.snipme.snipmeapp.infrastructure.remote.* @@ -15,14 +16,13 @@ internal val serviceModule = module { single { get().create(ShareService::class.java) } - // Room database initialization single { Room.databaseBuilder( get(), AppDatabase::class.java, "app_database" ).build() } - // Providing UserDao from the database single { get().userDao() } + single { get().snippetDao() } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt index 5c84484..3d2f1b6 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt @@ -39,7 +39,7 @@ internal val useCaseModule = module { factory { ObserveUpdatedSnippetPageUseCase(get()) } factory { ObserveSnippetUpdatesUseCase(get()) } factory { GetTargetUserReactionUseCase() } - factory { SetUserReactionUseCase(get(), get(), get()) } + factory { SetUserReactionUseCase(get(), get(), get(), get()) } factory { DeleteSnippetUseCase(get()) } // Language factory { GetLanguagesUseCase(get(), get(), get()) } diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt index 057d569..f001e34 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt @@ -4,17 +4,20 @@ import io.reactivex.Single import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase class SetUserReactionUseCase( private val auth: AuthorizationUseCase, private val repository: SnippetRepository, - private val getTargetReaction: GetTargetUserReactionUseCase + private val getTargetReaction: GetTargetUserReactionUseCase, + private val getSingleUser: GetSingleUserUseCase ) { operator fun invoke(snippet: Snippet, reaction: UserReaction): Single { val targetReaction = getTargetReaction(snippet, reaction) return auth() .andThen(repository.reaction(snippet.uuid, targetReaction)) - .andThen(repository.snippet(snippet.uuid)) + .andThen(getSingleUser()) + .flatMap { repository.snippet(snippet.uuid, it.id) } .doOnSuccess { repository.updateListener.onNext(it) } } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt index 18c06bf..8257255 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt @@ -12,15 +12,16 @@ interface SnippetRepository { val updateListener: BehaviorSubject - fun snippets(scope: SnippetScope, page: Int): Single> + fun snippets(userId: Int): Single> - fun snippet(id: String): Single + fun snippet(uuid: String, userId: Int): Single fun create( title: String, code: String, language: String, - visibility: SnippetVisibility + visibility: SnippetVisibility, + userId: Int ): Single fun update( @@ -28,10 +29,11 @@ interface SnippetRepository { title: String, code: String, language: String, - visibility: SnippetVisibility + visibility: SnippetVisibility, + userId: Int ): Single - fun count(scope: SnippetScope): Single + fun count(): Single fun reaction(uuid: String, reaction: UserReaction): Completable diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt index 349818f..b651d6d 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt @@ -1,16 +1,18 @@ package dev.snipme.snipmeapp.domain.repository.snippet -import io.reactivex.Completable -import io.reactivex.Single -import io.reactivex.subjects.BehaviorSubject import dev.snipme.snipmeapp.domain.error.ErrorHandler import dev.snipme.snipmeapp.domain.reaction.UserReaction -import dev.snipme.snipmeapp.domain.snippets.* -import dev.snipme.snipmeapp.infrastructure.model.request.CreateSnippetRequest -import dev.snipme.snipmeapp.infrastructure.model.request.RateSnippetRequest -import dev.snipme.snipmeapp.infrastructure.remote.SnippetService +import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.domain.snippets.SnippetResponseMapper +import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility +import dev.snipme.snipmeapp.infrastructure.local.SnippetDao +import dev.snipme.snipmeapp.infrastructure.local.SnippetEntry import dev.snipme.snipmeapp.util.extension.mapError import dev.snipme.snipmeapp.util.extension.mapItems +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.subjects.BehaviorSubject +import java.util.Date const val PAGE_START = 0 const val SNIPPET_PAGE_SIZE = 10 @@ -18,55 +20,77 @@ const val ONE_SNIPPET = 1 class SnippetRepositoryReal( private val errorHandler: ErrorHandler, - private val service: SnippetService, + private val service: SnippetDao, private val mapper: SnippetResponseMapper ) : SnippetRepository { - private var count: Int? = null - override val updateListener = BehaviorSubject.create() - override fun snippets(scope: SnippetScope, page: Int): Single> = - service.snippets(scope.value(), PAGE_START, SNIPPET_PAGE_SIZE * page) + override fun snippets(userId: Int): Single> = + service.snippets(userId) .mapError { errorHandler.handle(it) } - .map { count = it.count; it.results } .mapItems { mapper(it) } - override fun snippet(id: String): Single = - service.snippet(id).map { mapper(it) } + override fun snippet(uuid: String, userId: Int): Single = + service.snippet(uuid.toInt(), userId).map { mapper(it) } .mapError { errorHandler.handle(it) } override fun create( title: String, code: String, language: String, - visibility: SnippetVisibility - ): Single = - service.create(CreateSnippetRequest(title, code, language, visibility.name)) + visibility: SnippetVisibility, + userId: Int + ): Single { + return service.create( + SnippetEntry( + title = title, + code = code, + createdAt = Date().toString(), + modifiedAt = Date().toString(), + visibility = visibility.name, + ownerId = userId, + language = language, + numberOfLikes = 0, + numberOfDislikes = 0, + userReaction = "" + ) + ) .mapError { errorHandler.handle(it) } - .map { mapper(it) } + .flatMap { newId -> + service.snippet(newId.toInt(), userId) + .mapError { errorHandler.handle(it) } + .map { mapper(it) } + } + } override fun update( uuid: String, title: String, code: String, language: String, - visibility: SnippetVisibility + visibility: SnippetVisibility, + userId: Int ): Single = - service.update(uuid, CreateSnippetRequest(title, code, language, visibility.name)) + service.update(uuid.toInt(), title, code, language, visibility.name) .mapError { errorHandler.handle(it) } - .map { mapper(it) } + .andThen( + service.snippet(uuid.toInt(), userId) + .mapError { errorHandler.handle(it) } + .map { mapper(it) } + ) - override fun delete(uuid: String): Completable = service.delete(uuid) + override fun delete(uuid: String): Completable = service.delete(uuid.toInt()) - override fun count(scope: SnippetScope) = - if (count != null) { - Single.just(count!!) - } else { - service.snippets(scope.value(), PAGE_START, ONE_SNIPPET).map { it.count } + override fun count() = + service.count() .mapError { errorHandler.handle(it) } - } + .map{it} - override fun reaction(uuid: String, reaction: UserReaction) = - service.rate(RateSnippetRequest(uuid, reaction.name)) - .mapError { errorHandler.handle(it) } + override fun reaction(uuid: String, reaction: UserReaction): Completable { + TODO("Not yet implemented") + } + +// override fun reaction(uuid: String, reaction: UserReaction) = throw NotImplementedError() +// service.rate(RateSnippetRequest(uuid, reaction.name)) +// .mapError { errorHandler.handle(it) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryTest.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryTest.kt index aa46604..8f6d7cd 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryTest.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryTest.kt @@ -1,166 +1,167 @@ -package dev.snipme.snipmeapp.domain.repository.snippet - -import android.text.SpannableString -import io.reactivex.Completable -import io.reactivex.Single -import io.reactivex.subjects.BehaviorSubject -import dev.snipme.snipmeapp.domain.error.ErrorHandler -import dev.snipme.snipmeapp.domain.reaction.UserReaction -import dev.snipme.snipmeapp.domain.snippets.* -import dev.snipme.snipmeapp.util.SyntaxHighlighter.getHighlighted -import dev.snipme.snipmeapp.util.extension.lines -import dev.snipme.snipmeapp.util.extension.newLineChar -import java.util.* - -private const val PREVIEW_COUNT = 5 - -class SnippetRepositoryTest(private val errorHandler: ErrorHandler) : SnippetRepository { - - private val uuid - get() = UUID.randomUUID().toString() - - private val list by lazy { - List(25) { - Snippet( - uuid = uuid, - title = "Snippet $it", - code = getMockCode(code), - language = getMockLanguage(), - visibility = SnippetVisibility.PUBLIC, - isOwner = true, - owner = Owner(it, "User $it"), - modifiedAt = Date(), - numberOfLikes = 5, - numberOfDislikes = 3, - userReaction = UserReaction.LIKE - ) - } - } - override val updateListener: BehaviorSubject = BehaviorSubject.create() - - override fun snippets(scope: SnippetScope, page: Int): Single> { - return Single.just(list) - .map { it.take(SNIPPET_PAGE_SIZE * page) } - .onErrorResumeNext { throwable -> Single.error(errorHandler.handle(throwable)) } - } - - override fun snippet(id: String): Single = Single.just(list.find { it.uuid == id }) - - override fun create( - title: String, - code: String, - language: String, - visibility: SnippetVisibility - ): Single = Single.just( - Snippet( - uuid = uuid, - title = title, - code = getMockCode(code), - language = getMockLanguage(), - visibility = SnippetVisibility.PUBLIC, - isOwner = true, - owner = Owner(0, "login"), - modifiedAt = Date(), - numberOfLikes = 0, - numberOfDislikes = 0, - userReaction = UserReaction.NONE - ) - ) - - override fun update( - uuid: String, - title: String, - code: String, - language: String, - visibility: SnippetVisibility - ): Single = Single.just( - Snippet( - uuid = uuid, - title = title, - code = getMockCode(code), - language = getMockLanguage(), - visibility = SnippetVisibility.PUBLIC, - isOwner = true, - owner = Owner(0, "login"), - modifiedAt = Date(), - numberOfLikes = 5, - numberOfDislikes = 3, - userReaction = UserReaction.LIKE - ) - ) - - override fun count(scope: SnippetScope): Single = Single.just(list.size) - - override fun reaction(uuid: String, reaction: UserReaction): Completable = - Completable.complete() - - override fun delete(uuid: String): Completable = Completable.complete() - - private fun getPreview(code: String): SpannableString { - val preview = code.lines(PREVIEW_COUNT).joinToString(separator = newLineChar) - return getHighlighted(preview) - } - - private fun getMockCode(code: String) = SnippetCode(raw = code, highlighted = getPreview(code)) - - private fun getMockLanguage() = SnippetLanguage(raw = "Java", type = SnippetLanguageType.JAVA) - - private val code = - """ - /* Block comment */ - import java.util.Date; - import static AnInterface.CONSTANT; - import static java.util.Date.parse; - import static SomeClass.staticField; - /** - * Doc comment here for SomeClass - * @param T type parameter - * @see Math#sin(double) - */ - @Annotation (name=value) - public class SomeClass { // some comment - private T field = null; - private double unusedField = 12345.67890; - private UnknownType anotherString = "Another\nStrin\g"; - public static int staticField = 0; - public final int instanceFinalField = 0; - - /** - * Semantic highlighting: - * Generated spectrum to pick colors for local variables and parameters: - * Color#1 SC1.1 SC1.2 SC1.3 SC1.4 Color#2 SC2.1 SC2.2 SC2.3 SC2.4 Color#3 - * Color#3 SC3.1 SC3.2 SC3.3 SC3.4 Color#4 SC4.1 SC4.2 SC4.3 SC4.4 Color#5 - * @param param1 - * @param reassignedParam - * @param param2 - * @param param3 - */ - public SomeClass(AnInterface param1, int[] reassignedParam, - int param2 - int param3) { - int reassignedValue = this.staticField + param2 + param3; - long localVar1, localVar2, localVar3, localVar4; - int localVar = "IntelliJ"; // Error, incompatible types - System.out.println(anotherString + toString() + localVar); - long time = parse("1.2.3"); // Method is deprecated - new Thread().countStackFrames(); // Method is deprecated and marked for removal - reassignedValue ++; - field.run(); - new SomeClass() { - { - int a = localVar; - } - }; - reassignedParam = new ArrayList().toArray(new int[CONSTANT]); - } - } - enum AnEnum { CONST1, CONST2 } - interface AnInterface { - int CONSTANT = 2; - void method(); - } - abstract class SomeAbstractClass { - protected int instanceField = staticField; - } - """.trimIndent() -} +//package dev.snipme.snipmeapp.domain.repository.snippet +// +//import android.text.SpannableString +//import io.reactivex.Completable +//import io.reactivex.Single +//import io.reactivex.subjects.BehaviorSubject +//import dev.snipme.snipmeapp.domain.error.ErrorHandler +//import dev.snipme.snipmeapp.domain.reaction.UserReaction +//import dev.snipme.snipmeapp.domain.snippets.* +//import dev.snipme.snipmeapp.util.SyntaxHighlighter.getHighlighted +//import dev.snipme.snipmeapp.util.extension.lines +//import dev.snipme.snipmeapp.util.extension.newLineChar +//import org.koin.core.definition.indexKey +//import java.util.* +// +//private const val PREVIEW_COUNT = 5 +// +//class SnippetRepositoryTest(private val errorHandler: ErrorHandler) : SnippetRepository { +// +// private val uuid +// get() = UUID.randomUUID().toString() +// +// private val list by lazy { +// List(25) { +// Snippet( +// id = it.toLong(), +// title = "Snippet $it", +// code = getMockCode(code), +// language = getMockLanguage(), +// visibility = SnippetVisibility.PUBLIC, +// isOwner = true, +// owner = Owner(it, "User $it"), +// modifiedAt = Date(), +// numberOfLikes = 5, +// numberOfDislikes = 3, +// userReaction = UserReaction.LIKE +// ) +// } +// } +// override val updateListener: BehaviorSubject = BehaviorSubject.create() +// +//// override fun snippets(scope: SnippetScope, page: Int): Single> { +//// return Single.just(list) +//// .map { it.take(SNIPPET_PAGE_SIZE * page) } +//// .onErrorResumeNext { throwable -> Single.error(errorHandler.handle(throwable)) } +//// } +//// +//// override fun snippet(id: String): Single = Single.just(list.find { it.id == id }) +//// +//// override fun create( +//// title: String, +//// code: String, +//// language: String, +//// visibility: SnippetVisibility +//// ): Single = Single.just( +//// Snippet( +//// id = uuid, +//// title = title, +//// code = getMockCode(code), +//// language = getMockLanguage(), +//// visibility = SnippetVisibility.PUBLIC, +//// isOwner = true, +//// owner = Owner(0, "login"), +//// modifiedAt = Date(), +//// numberOfLikes = 0, +//// numberOfDislikes = 0, +//// userReaction = UserReaction.NONE +//// ) +//// ) +//// +//// override fun update( +//// uuid: String, +//// title: String, +//// code: String, +//// language: String, +//// visibility: SnippetVisibility +//// ): Single = Single.just( +//// Snippet( +//// id = uuid, +//// title = title, +//// code = getMockCode(code), +//// language = getMockLanguage(), +//// visibility = SnippetVisibility.PUBLIC, +//// isOwner = true, +//// owner = Owner(0, "login"), +//// modifiedAt = Date(), +//// numberOfLikes = 5, +//// numberOfDislikes = 3, +//// userReaction = UserReaction.LIKE +//// ) +//// ) +//// +//// override fun count(scope: SnippetScope): Single = Single.just(list.size) +//// +//// override fun reaction(uuid: String, reaction: UserReaction): Completable = +//// Completable.complete() +//// +//// override fun delete(uuid: String): Completable = Completable.complete() +//// +//// private fun getPreview(code: String): SpannableString { +//// val preview = code.lines(PREVIEW_COUNT).joinToString(separator = newLineChar) +//// return getHighlighted(preview) +//// } +// +// private fun getMockCode(code: String) = SnippetCode(raw = code, highlighted = getPreview(code)) +// +// private fun getMockLanguage() = SnippetLanguage(raw = "Java", type = SnippetLanguageType.JAVA) +// +// private val code = +// """ +// /* Block comment */ +// import java.util.Date; +// import static AnInterface.CONSTANT; +// import static java.util.Date.parse; +// import static SomeClass.staticField; +// /** +// * Doc comment here for SomeClass +// * @param T type parameter +// * @see Math#sin(double) +// */ +// @Annotation (name=value) +// public class SomeClass { // some comment +// private T field = null; +// private double unusedField = 12345.67890; +// private UnknownType anotherString = "Another\nStrin\g"; +// public static int staticField = 0; +// public final int instanceFinalField = 0; +// +// /** +// * Semantic highlighting: +// * Generated spectrum to pick colors for local variables and parameters: +// * Color#1 SC1.1 SC1.2 SC1.3 SC1.4 Color#2 SC2.1 SC2.2 SC2.3 SC2.4 Color#3 +// * Color#3 SC3.1 SC3.2 SC3.3 SC3.4 Color#4 SC4.1 SC4.2 SC4.3 SC4.4 Color#5 +// * @param param1 +// * @param reassignedParam +// * @param param2 +// * @param param3 +// */ +// public SomeClass(AnInterface param1, int[] reassignedParam, +// int param2 +// int param3) { +// int reassignedValue = this.staticField + param2 + param3; +// long localVar1, localVar2, localVar3, localVar4; +// int localVar = "IntelliJ"; // Error, incompatible types +// System.out.println(anotherString + toString() + localVar); +// long time = parse("1.2.3"); // Method is deprecated +// new Thread().countStackFrames(); // Method is deprecated and marked for removal +// reassignedValue ++; +// field.run(); +// new SomeClass() { +// { +// int a = localVar; +// } +// }; +// reassignedParam = new ArrayList().toArray(new int[CONSTANT]); +// } +// } +// enum AnEnum { CONST1, CONST2 } +// interface AnInterface { +// int CONSTANT = 2; +// void method(); +// } +// abstract class SomeAbstractClass { +// protected int instanceField = staticField; +// } +// """.trimIndent() +//} diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt index f14b94a..ffc51d2 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt @@ -6,11 +6,12 @@ import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository import dev.snipme.snipmeapp.domain.snippets.Snippet import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility +import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase class CreateSnippetUseCase( private val auth: AuthorizationUseCase, - private val networkAvailable: CheckNetworkAvailableUseCase, - private val snippetRepository: SnippetRepository + private val snippetRepository: SnippetRepository, + private val getSingleUser: GetSingleUserUseCase ) { operator fun invoke( title: String, @@ -18,10 +19,17 @@ class CreateSnippetUseCase( language: String, visibility: SnippetVisibility = SnippetVisibility.PUBLIC ): Single = auth() - .andThen(networkAvailable()) - .andThen(snippetRepository.create(title, code, language, visibility)) - .flatMap { - snippetRepository.updateListener.onNext(it) - Single.just(it) + .andThen(getSingleUser()) + .flatMap { user -> + snippetRepository.create( + title = title, + code = code, + language = language, + visibility = visibility, + userId = user.id + ) + } + .doOnSuccess { snippet -> + snippetRepository.updateListener.onNext(snippet) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/GetSingleSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/GetSingleSnippetUseCase.kt index ad5a47e..1a6d0e7 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/GetSingleSnippetUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/GetSingleSnippetUseCase.kt @@ -5,16 +5,16 @@ import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase class GetSingleSnippetUseCase( private val auth: AuthorizationUseCase, - private val networkAvailable: CheckNetworkAvailableUseCase, + private val getSingleUser: GetSingleUserUseCase, private val repository: SnippetRepository ) { operator fun invoke(uuid: String): Single = auth() - .andThen(networkAvailable()) - .andThen(repository.snippet(uuid)) - + .andThen(getSingleUser()) + .flatMap { repository.snippet(uuid, it.id) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveUpdatedSnippetPageUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveUpdatedSnippetPageUseCase.kt index e07cdf2..4f4c0b9 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveUpdatedSnippetPageUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveUpdatedSnippetPageUseCase.kt @@ -10,25 +10,26 @@ private const val START_PAGE = 1 class ObserveUpdatedSnippetPageUseCase(private val repository: SnippetRepository) { - operator fun invoke(scope: SnippetScope): Observable = + operator fun invoke(scope: SnippetScope, userId: Int): Observable = repository.updateListener .skipWhile { it == Snippet.EMPTY } .flatMapSingle { updated -> - getPageWithUpdated(scope, updated, START_PAGE) + getPageWithUpdated(scope, updated, START_PAGE, userId) } private fun getPageWithUpdated( scope: SnippetScope, updated: Snippet, - page: Int - ): Single = repository.snippets(scope, page) + page: Int, + userId: Int + ): Single = repository.snippets(userId) .map { snippets -> snippets.contains(updated.uuid) } .flatMap { contains -> if (contains) { Single.just(page) } else { // Be aware of recursion here - getPageWithUpdated(scope, updated, page + 1) + getPageWithUpdated(scope, updated, page + 1, userId) } } diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt index c11e020..ed9c6af 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt @@ -5,10 +5,11 @@ import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility +import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase class UpdateSnippetUseCase( private val auth: AuthorizationUseCase, - private val networkAvailable: CheckNetworkAvailableUseCase, + private val getSingleUser: GetSingleUserUseCase, private val repository: SnippetRepository ) { operator fun invoke( @@ -18,9 +19,9 @@ class UpdateSnippetUseCase( language: String, visibility: SnippetVisibility, ) = auth() - .andThen(networkAvailable()) - .andThen(repository.update(uuid, title, code, language, visibility)) - .flatMap { + .andThen(getSingleUser()) + .flatMap{repository.update(uuid, title, code, language, visibility, it.id)} + .doOnSuccess() { repository.updateListener.onNext(it) Single.just(it) } diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetSnippetsUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetSnippetsUseCase.kt index 95480aa..20e0106 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetSnippetsUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetSnippetsUseCase.kt @@ -4,17 +4,19 @@ import io.reactivex.Single import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository +import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase class GetSnippetsUseCase( private val auth: AuthorizationUseCase, private val repository: SnippetRepository, - private val networkAvailable: CheckNetworkAvailableUseCase + private val getSingleUser: GetSingleUserUseCase ) { operator fun invoke(scope: SnippetScope, page: Int): Single> = auth() - .andThen(networkAvailable()) - .andThen( - repository.snippets(scope, page) + .andThen(getSingleUser()) + .flatMap { + user -> + repository.snippets(user.id) .map { list -> list.sortedByDescending { it.modifiedAt.time } } - ) + } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/HasMoreSnippetPagesUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/HasMoreSnippetPagesUseCase.kt index 6f2f580..8bd123c 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/HasMoreSnippetPagesUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/HasMoreSnippetPagesUseCase.kt @@ -17,7 +17,7 @@ class HasMoreSnippetPagesUseCase( operator fun invoke(scope: SnippetScope, page: Int): Single = auth() .andThen(networkAvailable()) - .andThen(repository.count(scope).map { count -> page < pageCountFromOverall(count) }) + .andThen(repository.count().map { count -> page < pageCountFromOverall(count) }) private fun pageCountFromOverall(count: Int): Int { val nextPageOffset = count % SNIPPET_PAGE_SIZE diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt index 1b69815..2bd0e65 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt @@ -2,7 +2,7 @@ package dev.snipme.snipmeapp.domain.snippets import android.text.SpannableString import dev.snipme.snipmeapp.domain.reaction.UserReaction -import dev.snipme.snipmeapp.infrastructure.model.response.SnippetResponse +import dev.snipme.snipmeapp.infrastructure.local.SnippetWithOwner import dev.snipme.snipmeapp.util.SyntaxHighlighter.getHighlighted import dev.snipme.snipmeapp.util.extension.lines import dev.snipme.snipmeapp.util.extension.newLineChar @@ -14,19 +14,19 @@ const val PREVIEW_COUNT = 5 class SnippetResponseMapper { - operator fun invoke(response: SnippetResponse) = with(response) { + operator fun invoke(response: SnippetWithOwner) = with(response.snippet) { return@with Snippet( - uuid = id, - title = title.orEmpty(), - code = getCode(this), + uuid = id.toString(), + title = title, + code = getCode(code), language = getLanguage(language), visibility = getVisibility(visibility), - isOwner = is_owner ?: false, - owner = Owner(owner?.id ?: 0, owner?.username ?: ""), - modifiedAt = modified_at?.toDate() ?: Date(), - numberOfLikes = number_of_likes ?: 0, - numberOfDislikes = number_of_dislikes ?: 0, - userReaction = getUserReaction(user_reaction) + isOwner = response.isOwner, + owner = Owner(ownerId , response.ownerName), + modifiedAt = modifiedAt.toDate(), + numberOfLikes = numberOfLikes, + numberOfDislikes = numberOfDislikes, + userReaction = getUserReaction(userReaction) ) } @@ -37,9 +37,9 @@ class SnippetResponseMapper { else -> UserReaction.NONE } - private fun getCode(response: SnippetResponse) = SnippetCode( - raw = response.code.orEmpty(), - highlighted = getPreview(response.code.orEmpty()) + private fun getCode(code: String) = SnippetCode( + raw = code.orEmpty(), + highlighted = getPreview(code) ) private fun getLanguage(language: String?) = SnippetLanguage( diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/user/GetSingleUserUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/user/GetSingleUserUseCase.kt index 57854e2..dc22e81 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/user/GetSingleUserUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/user/GetSingleUserUseCase.kt @@ -18,7 +18,7 @@ class GetSingleUserUseCase( var token: Int = 0 authRepository.getToken().subscribe { value -> token = value.toInt() } return auth() - .andThen(networkAvailable()) +// .andThen(networkAvailable()) .andThen(repository.user(token)) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt index 484fae4..fd24631 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt @@ -4,9 +4,10 @@ import androidx.room.Room import androidx.room.RoomDatabase import android.content.Context -@Database(entities = [UserEntry::class], version = 1) +@Database(entities = [UserEntry::class, SnippetEntry::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao + abstract fun snippetDao(): SnippetDao companion object { @Volatile diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt new file mode 100644 index 0000000..d3fe40b --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt @@ -0,0 +1,50 @@ +package dev.snipme.snipmeapp.infrastructure.local + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.reactivex.Completable +import io.reactivex.Single + +@Dao +interface SnippetDao { + @Query(""" + SELECT s.*, u.login as ownerName, + CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner + FROM snippets as s + INNER JOIN users as u ON s.ownerId = u.id + WHERE s.id = :uuid + """) + fun snippet(uuid: Int, userId: Int) : Single + + @Query("SELECT s.*, u.login as ownerName, CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner FROM snippets as s INNER JOIN users as u ON s.ownerId = u.id") + fun snippets(userId: Int) : Single> + + @Query("SELECT COUNT(*) FROM snippets") + fun count() : Single + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun create(snippet: SnippetEntry): Single + + @Query(""" + UPDATE snippets + SET title = :title, + code = :code, + modifiedAt = current_timestamp, + visibility = :visibility, + language = :language + WHERE id = :uuid + """) + fun update( + uuid: Int, + title: String, + code: String, + visibility: String, + language: String, + ) : Completable + + @Query("DELETE FROM snippets WHERE id = :uuid") + fun delete(uuid: Int): Completable + +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetEntity.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetEntity.kt new file mode 100644 index 0000000..24c1bd3 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetEntity.kt @@ -0,0 +1,28 @@ +package dev.snipme.snipmeapp.infrastructure.local + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey + + +@Entity(tableName = "snippets") +data class SnippetEntry( + @PrimaryKey(true) val id: Long = 0, + val title: String, + val code: String, + val createdAt: String, + val modifiedAt: String, + val visibility: String, + val ownerId: Int, + val language: String, + val numberOfLikes: Int, + val numberOfDislikes: Int, + val userReaction: String +) + + +data class SnippetWithOwner( + @Embedded val snippet: SnippetEntry, + val ownerName: String, + val isOwner: Boolean +) diff --git a/flutter_module/lib/model/main_model.dart b/flutter_module/lib/model/main_model.dart index 4324143..587929e 100644 --- a/flutter_module/lib/model/main_model.dart +++ b/flutter_module/lib/model/main_model.dart @@ -1,12 +1,20 @@ -// Autogenerated from Pigeon (v4.2.3), do not edit directly. +// Autogenerated from Pigeon (v22.5.0), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + enum SnippetLanguageType { c, cpp, @@ -109,64 +117,69 @@ class Snippet { }); String? uuid; + String? title; + SnippetCode? code; + SnippetLanguage? language; + Owner? owner; + bool? isOwner; + String? timeAgo; + int? voteResult; + UserReaction? userReaction; + bool? isPrivate; + bool? isLiked; + bool? isDisliked; + bool? isSaved; + bool? isToDelete; Object encode() { - final Map pigeonMap = {}; - pigeonMap['uuid'] = uuid; - pigeonMap['title'] = title; - pigeonMap['code'] = code?.encode(); - pigeonMap['language'] = language?.encode(); - pigeonMap['owner'] = owner?.encode(); - pigeonMap['isOwner'] = isOwner; - pigeonMap['timeAgo'] = timeAgo; - pigeonMap['voteResult'] = voteResult; - pigeonMap['userReaction'] = userReaction?.index; - pigeonMap['isPrivate'] = isPrivate; - pigeonMap['isLiked'] = isLiked; - pigeonMap['isDisliked'] = isDisliked; - pigeonMap['isSaved'] = isSaved; - pigeonMap['isToDelete'] = isToDelete; - return pigeonMap; + return [ + uuid, + title, + code, + language, + owner, + isOwner, + timeAgo, + voteResult, + userReaction, + isPrivate, + isLiked, + isDisliked, + isSaved, + isToDelete, + ]; } - static Snippet decode(Object message) { - final Map pigeonMap = message as Map; + static Snippet decode(Object result) { + result as List; return Snippet( - uuid: pigeonMap['uuid'] as String?, - title: pigeonMap['title'] as String?, - code: pigeonMap['code'] != null - ? SnippetCode.decode(pigeonMap['code']!) - : null, - language: pigeonMap['language'] != null - ? SnippetLanguage.decode(pigeonMap['language']!) - : null, - owner: pigeonMap['owner'] != null - ? Owner.decode(pigeonMap['owner']!) - : null, - isOwner: pigeonMap['isOwner'] as bool?, - timeAgo: pigeonMap['timeAgo'] as String?, - voteResult: pigeonMap['voteResult'] as int?, - userReaction: pigeonMap['userReaction'] != null - ? UserReaction.values[pigeonMap['userReaction']! as int] - : null, - isPrivate: pigeonMap['isPrivate'] as bool?, - isLiked: pigeonMap['isLiked'] as bool?, - isDisliked: pigeonMap['isDisliked'] as bool?, - isSaved: pigeonMap['isSaved'] as bool?, - isToDelete: pigeonMap['isToDelete'] as bool?, + uuid: result[0] as String?, + title: result[1] as String?, + code: result[2] as SnippetCode?, + language: result[3] as SnippetLanguage?, + owner: result[4] as Owner?, + isOwner: result[5] as bool?, + timeAgo: result[6] as String?, + voteResult: result[7] as int?, + userReaction: result[8] as UserReaction?, + isPrivate: result[9] as bool?, + isLiked: result[10] as bool?, + isDisliked: result[11] as bool?, + isSaved: result[12] as bool?, + isToDelete: result[13] as bool?, ); } } @@ -178,20 +191,21 @@ class SnippetCode { }); String? raw; + List? tokens; Object encode() { - final Map pigeonMap = {}; - pigeonMap['raw'] = raw; - pigeonMap['tokens'] = tokens; - return pigeonMap; + return [ + raw, + tokens, + ]; } - static SnippetCode decode(Object message) { - final Map pigeonMap = message as Map; + static SnippetCode decode(Object result) { + result as List; return SnippetCode( - raw: pigeonMap['raw'] as String?, - tokens: (pigeonMap['tokens'] as List?)?.cast(), + raw: result[0] as String?, + tokens: (result[1] as List?)?.cast(), ); } } @@ -204,23 +218,25 @@ class SyntaxToken { }); int? start; + int? end; + int? color; Object encode() { - final Map pigeonMap = {}; - pigeonMap['start'] = start; - pigeonMap['end'] = end; - pigeonMap['color'] = color; - return pigeonMap; + return [ + start, + end, + color, + ]; } - static SyntaxToken decode(Object message) { - final Map pigeonMap = message as Map; + static SyntaxToken decode(Object result) { + result as List; return SyntaxToken( - start: pigeonMap['start'] as int?, - end: pigeonMap['end'] as int?, - color: pigeonMap['color'] as int?, + start: result[0] as int?, + end: result[1] as int?, + color: result[2] as int?, ); } } @@ -232,22 +248,21 @@ class SnippetLanguage { }); String? raw; + SnippetLanguageType? type; Object encode() { - final Map pigeonMap = {}; - pigeonMap['raw'] = raw; - pigeonMap['type'] = type?.index; - return pigeonMap; + return [ + raw, + type, + ]; } - static SnippetLanguage decode(Object message) { - final Map pigeonMap = message as Map; + static SnippetLanguage decode(Object result) { + result as List; return SnippetLanguage( - raw: pigeonMap['raw'] as String?, - type: pigeonMap['type'] != null - ? SnippetLanguageType.values[pigeonMap['type']! as int] - : null, + raw: result[0] as String?, + type: result[1] as SnippetLanguageType?, ); } } @@ -259,20 +274,21 @@ class Owner { }); int? id; + String? login; Object encode() { - final Map pigeonMap = {}; - pigeonMap['id'] = id; - pigeonMap['login'] = login; - return pigeonMap; + return [ + id, + login, + ]; } - static Owner decode(Object message) { - final Map pigeonMap = message as Map; + static Owner decode(Object result) { + result as List; return Owner( - id: pigeonMap['id'] as int?, - login: pigeonMap['login'] as String?, + id: result[0] as int?, + login: result[1] as String?, ); } } @@ -286,26 +302,29 @@ class SnippetFilter { }); List? languages; + List? selectedLanguages; + List? scopes; + String? selectedScope; Object encode() { - final Map pigeonMap = {}; - pigeonMap['languages'] = languages; - pigeonMap['selectedLanguages'] = selectedLanguages; - pigeonMap['scopes'] = scopes; - pigeonMap['selectedScope'] = selectedScope; - return pigeonMap; + return [ + languages, + selectedLanguages, + scopes, + selectedScope, + ]; } - static SnippetFilter decode(Object message) { - final Map pigeonMap = message as Map; + static SnippetFilter decode(Object result) { + result as List; return SnippetFilter( - languages: (pigeonMap['languages'] as List?)?.cast(), - selectedLanguages: (pigeonMap['selectedLanguages'] as List?)?.cast(), - scopes: (pigeonMap['scopes'] as List?)?.cast(), - selectedScope: pigeonMap['selectedScope'] as String?, + languages: (result[0] as List?)?.cast(), + selectedLanguages: (result[1] as List?)?.cast(), + scopes: (result[2] as List?)?.cast(), + selectedScope: result[3] as String?, ); } } @@ -313,7 +332,7 @@ class SnippetFilter { class MainModelStateData { MainModelStateData({ this.state, - this.is_loading, + this.isLoading, this.data, this.filter, this.error, @@ -322,39 +341,41 @@ class MainModelStateData { }); ModelState? state; - bool? is_loading; + + bool? isLoading; + List? data; + SnippetFilter? filter; + String? error; + int? oldHash; + int? newHash; Object encode() { - final Map pigeonMap = {}; - pigeonMap['state'] = state?.index; - pigeonMap['is_loading'] = is_loading; - pigeonMap['data'] = data; - pigeonMap['filter'] = filter?.encode(); - pigeonMap['error'] = error; - pigeonMap['oldHash'] = oldHash; - pigeonMap['newHash'] = newHash; - return pigeonMap; + return [ + state, + isLoading, + data, + filter, + error, + oldHash, + newHash, + ]; } - static MainModelStateData decode(Object message) { - final Map pigeonMap = message as Map; + static MainModelStateData decode(Object result) { + result as List; return MainModelStateData( - state: pigeonMap['state'] != null - ? ModelState.values[pigeonMap['state']! as int] - : null, - is_loading: pigeonMap['is_loading'] as bool?, - data: (pigeonMap['data'] as List?)?.cast(), - filter: pigeonMap['filter'] != null - ? SnippetFilter.decode(pigeonMap['filter']!) - : null, - error: pigeonMap['error'] as String?, - oldHash: pigeonMap['oldHash'] as int?, - newHash: pigeonMap['newHash'] as int?, + state: result[0] as ModelState?, + isLoading: result[1] as bool?, + data: (result[2] as List?)?.cast(), + filter: result[3] as SnippetFilter?, + error: result[4] as String?, + oldHash: result[5] as int?, + newHash: result[6] as int?, ); } } @@ -368,28 +389,29 @@ class MainModelEventData { }); MainModelEvent? event; + String? message; + int? oldHash; + int? newHash; Object encode() { - final Map pigeonMap = {}; - pigeonMap['event'] = event?.index; - pigeonMap['message'] = message; - pigeonMap['oldHash'] = oldHash; - pigeonMap['newHash'] = newHash; - return pigeonMap; + return [ + event, + message, + oldHash, + newHash, + ]; } - static MainModelEventData decode(Object message) { - final Map pigeonMap = message as Map; + static MainModelEventData decode(Object result) { + result as List; return MainModelEventData( - event: pigeonMap['event'] != null - ? MainModelEvent.values[pigeonMap['event']! as int] - : null, - message: pigeonMap['message'] as String?, - oldHash: pigeonMap['oldHash'] as int?, - newHash: pigeonMap['newHash'] as int?, + event: result[0] as MainModelEvent?, + message: result[1] as String?, + oldHash: result[2] as int?, + newHash: result[3] as int?, ); } } @@ -397,7 +419,7 @@ class MainModelEventData { class DetailModelStateData { DetailModelStateData({ this.state, - this.is_loading, + this.isLoading, this.data, this.error, this.oldHash, @@ -405,36 +427,37 @@ class DetailModelStateData { }); ModelState? state; - bool? is_loading; + + bool? isLoading; + Snippet? data; + String? error; + int? oldHash; + int? newHash; Object encode() { - final Map pigeonMap = {}; - pigeonMap['state'] = state?.index; - pigeonMap['is_loading'] = is_loading; - pigeonMap['data'] = data?.encode(); - pigeonMap['error'] = error; - pigeonMap['oldHash'] = oldHash; - pigeonMap['newHash'] = newHash; - return pigeonMap; + return [ + state, + isLoading, + data, + error, + oldHash, + newHash, + ]; } - static DetailModelStateData decode(Object message) { - final Map pigeonMap = message as Map; + static DetailModelStateData decode(Object result) { + result as List; return DetailModelStateData( - state: pigeonMap['state'] != null - ? ModelState.values[pigeonMap['state']! as int] - : null, - is_loading: pigeonMap['is_loading'] as bool?, - data: pigeonMap['data'] != null - ? Snippet.decode(pigeonMap['data']!) - : null, - error: pigeonMap['error'] as String?, - oldHash: pigeonMap['oldHash'] as int?, - newHash: pigeonMap['newHash'] as int?, + state: result[0] as ModelState?, + isLoading: result[1] as bool?, + data: result[2] as Snippet?, + error: result[3] as String?, + oldHash: result[4] as int?, + newHash: result[5] as int?, ); } } @@ -448,28 +471,29 @@ class DetailModelEventData { }); DetailModelEvent? event; + String? value; + int? oldHash; + int? newHash; Object encode() { - final Map pigeonMap = {}; - pigeonMap['event'] = event?.index; - pigeonMap['value'] = value; - pigeonMap['oldHash'] = oldHash; - pigeonMap['newHash'] = newHash; - return pigeonMap; + return [ + event, + value, + oldHash, + newHash, + ]; } - static DetailModelEventData decode(Object message) { - final Map pigeonMap = message as Map; + static DetailModelEventData decode(Object result) { + result as List; return DetailModelEventData( - event: pigeonMap['event'] != null - ? DetailModelEvent.values[pigeonMap['event']! as int] - : null, - value: pigeonMap['value'] as String?, - oldHash: pigeonMap['oldHash'] as int?, - newHash: pigeonMap['newHash'] as int?, + event: result[0] as DetailModelEvent?, + value: result[1] as String?, + oldHash: result[2] as int?, + newHash: result[3] as int?, ); } } @@ -477,34 +501,35 @@ class DetailModelEventData { class LoginModelStateData { LoginModelStateData({ this.state, - this.is_loading, + this.isLoading, this.oldHash, this.newHash, }); ModelState? state; - bool? is_loading; + + bool? isLoading; + int? oldHash; + int? newHash; Object encode() { - final Map pigeonMap = {}; - pigeonMap['state'] = state?.index; - pigeonMap['is_loading'] = is_loading; - pigeonMap['oldHash'] = oldHash; - pigeonMap['newHash'] = newHash; - return pigeonMap; + return [ + state, + isLoading, + oldHash, + newHash, + ]; } - static LoginModelStateData decode(Object message) { - final Map pigeonMap = message as Map; + static LoginModelStateData decode(Object result) { + result as List; return LoginModelStateData( - state: pigeonMap['state'] != null - ? ModelState.values[pigeonMap['state']! as int] - : null, - is_loading: pigeonMap['is_loading'] as bool?, - oldHash: pigeonMap['oldHash'] as int?, - newHash: pigeonMap['newHash'] as int?, + state: result[0] as ModelState?, + isLoading: result[1] as bool?, + oldHash: result[2] as int?, + newHash: result[3] as int?, ); } } @@ -517,99 +542,149 @@ class LoginModelEventData { }); LoginModelEvent? event; + int? oldHash; + int? newHash; Object encode() { - final Map pigeonMap = {}; - pigeonMap['event'] = event?.index; - pigeonMap['oldHash'] = oldHash; - pigeonMap['newHash'] = newHash; - return pigeonMap; + return [ + event, + oldHash, + newHash, + ]; } - static LoginModelEventData decode(Object message) { - final Map pigeonMap = message as Map; + static LoginModelEventData decode(Object result) { + result as List; return LoginModelEventData( - event: pigeonMap['event'] != null - ? LoginModelEvent.values[pigeonMap['event']! as int] - : null, - oldHash: pigeonMap['oldHash'] as int?, - newHash: pigeonMap['newHash'] as int?, + event: result[0] as LoginModelEvent?, + oldHash: result[1] as int?, + newHash: result[2] as int?, ); } } -class _MainModelBridgeCodec extends StandardMessageCodec{ - const _MainModelBridgeCodec(); + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is MainModelEventData) { - buffer.putUint8(128); - writeValue(buffer, value.encode()); - } else - if (value is MainModelStateData) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is SnippetLanguageType) { buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else - if (value is Owner) { + writeValue(buffer, value.index); + } else if (value is SnippetFilterType) { buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else - if (value is Snippet) { + writeValue(buffer, value.index); + } else if (value is UserReaction) { buffer.putUint8(131); - writeValue(buffer, value.encode()); - } else - if (value is SnippetCode) { + writeValue(buffer, value.index); + } else if (value is ModelState) { buffer.putUint8(132); - writeValue(buffer, value.encode()); - } else - if (value is SnippetFilter) { + writeValue(buffer, value.index); + } else if (value is MainModelEvent) { buffer.putUint8(133); - writeValue(buffer, value.encode()); - } else - if (value is SnippetLanguage) { + writeValue(buffer, value.index); + } else if (value is DetailModelEvent) { buffer.putUint8(134); - writeValue(buffer, value.encode()); - } else - if (value is SyntaxToken) { + writeValue(buffer, value.index); + } else if (value is LoginModelEvent) { buffer.putUint8(135); + writeValue(buffer, value.index); + } else if (value is Snippet) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is SnippetCode) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is SyntaxToken) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); + } else if (value is SnippetLanguage) { + buffer.putUint8(139); + writeValue(buffer, value.encode()); + } else if (value is Owner) { + buffer.putUint8(140); + writeValue(buffer, value.encode()); + } else if (value is SnippetFilter) { + buffer.putUint8(141); + writeValue(buffer, value.encode()); + } else if (value is MainModelStateData) { + buffer.putUint8(142); + writeValue(buffer, value.encode()); + } else if (value is MainModelEventData) { + buffer.putUint8(143); + writeValue(buffer, value.encode()); + } else if (value is DetailModelStateData) { + buffer.putUint8(144); + writeValue(buffer, value.encode()); + } else if (value is DetailModelEventData) { + buffer.putUint8(145); + writeValue(buffer, value.encode()); + } else if (value is LoginModelStateData) { + buffer.putUint8(146); writeValue(buffer, value.encode()); - } else -{ + } else if (value is LoginModelEventData) { + buffer.putUint8(147); + writeValue(buffer, value.encode()); + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: - return MainModelEventData.decode(readValue(buffer)!); - - case 129: - return MainModelStateData.decode(readValue(buffer)!); - - case 130: - return Owner.decode(readValue(buffer)!); - - case 131: + case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : SnippetLanguageType.values[value]; + case 130: + final int? value = readValue(buffer) as int?; + return value == null ? null : SnippetFilterType.values[value]; + case 131: + final int? value = readValue(buffer) as int?; + return value == null ? null : UserReaction.values[value]; + case 132: + final int? value = readValue(buffer) as int?; + return value == null ? null : ModelState.values[value]; + case 133: + final int? value = readValue(buffer) as int?; + return value == null ? null : MainModelEvent.values[value]; + case 134: + final int? value = readValue(buffer) as int?; + return value == null ? null : DetailModelEvent.values[value]; + case 135: + final int? value = readValue(buffer) as int?; + return value == null ? null : LoginModelEvent.values[value]; + case 136: return Snippet.decode(readValue(buffer)!); - - case 132: + case 137: return SnippetCode.decode(readValue(buffer)!); - - case 133: - return SnippetFilter.decode(readValue(buffer)!); - - case 134: - return SnippetLanguage.decode(readValue(buffer)!); - - case 135: + case 138: return SyntaxToken.decode(readValue(buffer)!); - - default: + case 139: + return SnippetLanguage.decode(readValue(buffer)!); + case 140: + return Owner.decode(readValue(buffer)!); + case 141: + return SnippetFilter.decode(readValue(buffer)!); + case 142: + return MainModelStateData.decode(readValue(buffer)!); + case 143: + return MainModelEventData.decode(readValue(buffer)!); + case 144: + return DetailModelStateData.decode(readValue(buffer)!); + case 145: + return DetailModelEventData.decode(readValue(buffer)!); + case 146: + return LoginModelStateData.decode(readValue(buffer)!); + case 147: + return LoginModelEventData.decode(readValue(buffer)!); + default: return super.readValueOfType(type, buffer); - } } } @@ -618,81 +693,85 @@ class MainModelBridge { /// Constructor for [MainModelBridge]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - MainModelBridge({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; + MainModelBridge({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - static const MessageCodec codec = _MainModelBridgeCodec(); + final String pigeonVar_messageChannelSuffix; Future getState() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.MainModelBridge.getState', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.MainModelBridge.getState$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as MainModelStateData?)!; + return (pigeonVar_replyList[0] as MainModelStateData?)!; } } Future getEvent() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.MainModelBridge.getEvent', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.MainModelBridge.getEvent$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as MainModelEventData?)!; + return (pigeonVar_replyList[0] as MainModelEventData?)!; } } Future resetEvent() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.MainModelBridge.resetEvent', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.MainModelBridge.resetEvent$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; @@ -700,65 +779,65 @@ class MainModelBridge { } Future initState() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.MainModelBridge.initState', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.MainModelBridge.initState$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; } } - Future filterLanguage(String arg_language, bool arg_isSelected) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.MainModelBridge.filterLanguage', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_language, arg_isSelected]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + Future filterLanguage(String language, bool isSelected) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.MainModelBridge.filterLanguage$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([language, isSelected]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; } } - Future filterScope(String arg_scope) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.MainModelBridge.filterScope', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_scope]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + Future filterScope(String scope) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.MainModelBridge.filterScope$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([scope]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; @@ -766,21 +845,21 @@ class MainModelBridge { } Future logOut() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.MainModelBridge.logOut', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.MainModelBridge.logOut$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; @@ -788,174 +867,111 @@ class MainModelBridge { } } -class _DetailModelBridgeCodec extends StandardMessageCodec{ - const _DetailModelBridgeCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is DetailModelEventData) { - buffer.putUint8(128); - writeValue(buffer, value.encode()); - } else - if (value is DetailModelStateData) { - buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else - if (value is Owner) { - buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else - if (value is Snippet) { - buffer.putUint8(131); - writeValue(buffer, value.encode()); - } else - if (value is SnippetCode) { - buffer.putUint8(132); - writeValue(buffer, value.encode()); - } else - if (value is SnippetLanguage) { - buffer.putUint8(133); - writeValue(buffer, value.encode()); - } else - if (value is SyntaxToken) { - buffer.putUint8(134); - writeValue(buffer, value.encode()); - } else -{ - super.writeValue(buffer, value); - } - } - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 128: - return DetailModelEventData.decode(readValue(buffer)!); - - case 129: - return DetailModelStateData.decode(readValue(buffer)!); - - case 130: - return Owner.decode(readValue(buffer)!); - - case 131: - return Snippet.decode(readValue(buffer)!); - - case 132: - return SnippetCode.decode(readValue(buffer)!); - - case 133: - return SnippetLanguage.decode(readValue(buffer)!); - - case 134: - return SyntaxToken.decode(readValue(buffer)!); - - default: - return super.readValueOfType(type, buffer); - - } - } -} - class DetailModelBridge { /// Constructor for [DetailModelBridge]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - DetailModelBridge({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; + DetailModelBridge({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; - static const MessageCodec codec = _DetailModelBridgeCodec(); + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; Future getState() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DetailModelBridge.getState', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.DetailModelBridge.getState$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as DetailModelStateData?)!; + return (pigeonVar_replyList[0] as DetailModelStateData?)!; } } Future getEvent() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DetailModelBridge.getEvent', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.DetailModelBridge.getEvent$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as DetailModelEventData?)!; + return (pigeonVar_replyList[0] as DetailModelEventData?)!; } } Future resetEvent() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DetailModelBridge.resetEvent', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.DetailModelBridge.resetEvent$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; } } - Future load(String arg_uuid) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DetailModelBridge.load', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_uuid]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + Future load(String uuid) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.DetailModelBridge.load$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([uuid]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; @@ -963,21 +979,21 @@ class DetailModelBridge { } Future like() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DetailModelBridge.like', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.DetailModelBridge.like$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; @@ -985,21 +1001,21 @@ class DetailModelBridge { } Future dislike() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DetailModelBridge.dislike', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.DetailModelBridge.dislike$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; @@ -1007,21 +1023,21 @@ class DetailModelBridge { } Future save() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DetailModelBridge.save', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.DetailModelBridge.save$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; @@ -1029,21 +1045,21 @@ class DetailModelBridge { } Future copyToClipboard() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DetailModelBridge.copyToClipboard', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.DetailModelBridge.copyToClipboard$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; @@ -1051,21 +1067,21 @@ class DetailModelBridge { } Future share() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DetailModelBridge.share', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.DetailModelBridge.share$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; @@ -1073,21 +1089,21 @@ class DetailModelBridge { } Future delete() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DetailModelBridge.delete', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.DetailModelBridge.delete$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; @@ -1095,117 +1111,89 @@ class DetailModelBridge { } } -class _LoginModelBridgeCodec extends StandardMessageCodec{ - const _LoginModelBridgeCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is LoginModelEventData) { - buffer.putUint8(128); - writeValue(buffer, value.encode()); - } else - if (value is LoginModelStateData) { - buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else -{ - super.writeValue(buffer, value); - } - } - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 128: - return LoginModelEventData.decode(readValue(buffer)!); - - case 129: - return LoginModelStateData.decode(readValue(buffer)!); - - default: - return super.readValueOfType(type, buffer); - - } - } -} - class LoginModelBridge { /// Constructor for [LoginModelBridge]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - LoginModelBridge({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; + LoginModelBridge({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - static const MessageCodec codec = _LoginModelBridgeCodec(); + final String pigeonVar_messageChannelSuffix; Future getState() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.LoginModelBridge.getState', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.LoginModelBridge.getState$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as LoginModelStateData?)!; + return (pigeonVar_replyList[0] as LoginModelStateData?)!; } } Future getEvent() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.LoginModelBridge.getEvent', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.LoginModelBridge.getEvent$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as LoginModelEventData?)!; + return (pigeonVar_replyList[0] as LoginModelEventData?)!; } } - Future loginOrRegister(String arg_email, String arg_password) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.LoginModelBridge.loginOrRegister', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_email, arg_password]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + Future loginOrRegister(String email, String password) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.LoginModelBridge.loginOrRegister$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([email, password]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; @@ -1213,21 +1201,21 @@ class LoginModelBridge { } Future checkLoginState() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.LoginModelBridge.checkLoginState', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.LoginModelBridge.checkLoginState$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; @@ -1235,21 +1223,21 @@ class LoginModelBridge { } Future resetEvent() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.LoginModelBridge.resetEvent', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.LoginModelBridge.resetEvent$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; diff --git a/flutter_module/lib/presentation/screens/details_screen.dart b/flutter_module/lib/presentation/screens/details_screen.dart index cc3f3bc..50962c7 100644 --- a/flutter_module/lib/presentation/screens/details_screen.dart +++ b/flutter_module/lib/presentation/screens/details_screen.dart @@ -105,7 +105,7 @@ class _DetailsPage extends HookWidget { ), body: ViewStateWrapper( isLoading: - state.state == ModelState.loading || state.is_loading == true, + state.state == ModelState.loading || state.isLoading == true, error: state.error, data: state.data, builder: (_, snippet) => _DetailPageData( diff --git a/flutter_module/lib/presentation/screens/main_screen.dart b/flutter_module/lib/presentation/screens/main_screen.dart index b531c79..133a6a7 100644 --- a/flutter_module/lib/presentation/screens/main_screen.dart +++ b/flutter_module/lib/presentation/screens/main_screen.dart @@ -88,7 +88,7 @@ class _MainPage extends HookWidget { backgroundColor: ColorStyles.pageBackground(), body: ViewStateWrapper>( isLoading: - state.state == ModelState.loading || state.is_loading == true, + state.state == ModelState.loading || state.isLoading == true, error: state.error, data: state.data?.cast(), builder: (_, snippets) { diff --git a/gradle.properties b/gradle.properties index 639da25..0f6106e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,4 +21,5 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -android.buildFeatures.buildConfig=true \ No newline at end of file +android.buildFeatures.buildConfig=true +ksp.incremental=false \ No newline at end of file From fa98c07517549cca520f1d786bf5a54a4558914b Mon Sep 17 00:00:00 2001 From: xJac0b Date: Sun, 3 Nov 2024 16:09:09 +0100 Subject: [PATCH 08/23] fix: add assets for reactions --- flutter_module/lib/generated/assets.dart | 6 +++--- .../presentation/widgets/snippet_details_bar.dart | 13 ++++++++++--- flutter_module/pubspec.lock | 12 ++++++------ flutter_module/pubspec.yaml | 8 ++++---- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/flutter_module/lib/generated/assets.dart b/flutter_module/lib/generated/assets.dart index f355e6e..b0ec512 100644 --- a/flutter_module/lib/generated/assets.dart +++ b/flutter_module/lib/generated/assets.dart @@ -3,8 +3,8 @@ class Assets { Assets._(); static const String appLogo = 'assets/images/illustrations/app_logo.png'; - static const String reactionDislike = 'assets/images/icons/reaction_dislike.png'; - static const String reactionLike = 'assets/images/icons/reaction_like.png'; - static const String reactionUndefined = 'assets/images/icons/reaction_undefined.png'; + static const String reactionDislike = 'assets/images/icons/reaction_dislike.jpeg'; + static const String reactionLike = 'assets/images/icons/reaction_like.jpeg'; + static const String reactionUndefined = 'assets/images/icons/reaction_undefined.jpeg'; } diff --git a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart index e928eaa..d3ec723 100644 --- a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart @@ -58,17 +58,24 @@ class _UserReactionIndicator extends StatelessWidget { }); final UserReaction? reaction; + final _scale = 2.0; @override Widget build(BuildContext context) { if (reaction == UserReaction.like) { - return Image.asset(Assets.reactionLike); + return Image.asset( + Assets.reactionLike, + scale: _scale, + ); } if (reaction == UserReaction.dislike) { - return Image.asset(Assets.reactionDislike); + return Image.asset( + Assets.reactionDislike, + scale: _scale, + ); } - return Image.asset(Assets.reactionUndefined); + return Image.asset(Assets.reactionUndefined, scale: _scale); } } diff --git a/flutter_module/pubspec.lock b/flutter_module/pubspec.lock index f9d8e05..f896473 100644 --- a/flutter_module/pubspec.lock +++ b/flutter_module/pubspec.lock @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: coverage - sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + sha256: "88b0fddbe4c92910fefc09cc0248f5e7f0cd23e450ded4c28f16ab8ee8f83268" url: "https://pub.dev" source: hosted - version: "1.9.2" + version: "1.10.0" crypto: dependency: transitive description: @@ -364,10 +364,10 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: @@ -428,10 +428,10 @@ packages: dependency: "direct dev" description: name: pigeon - sha256: "2f7af49f530b3208131489ce601f8d95d567b3fa3c71265a87f62b0b87d41e91" + sha256: bb5505b81fc9c718911fe7188c88a67566c37497a66f4d4abcfe29fb08614ea3 url: "https://pub.dev" source: hosted - version: "22.5.0" + version: "22.6.0" pool: dependency: transitive description: diff --git a/flutter_module/pubspec.yaml b/flutter_module/pubspec.yaml index 601fdec..629edd0 100644 --- a/flutter_module/pubspec.yaml +++ b/flutter_module/pubspec.yaml @@ -43,7 +43,7 @@ dev_dependencies: # following screens: https://dart.dev/tools/pub/pubspec flutter_assets_generator: - named_with_parent: false # Don't add folder to icon's name + named_with_parent: false # Don't add folder to icons's name flutter: plugin: @@ -59,9 +59,9 @@ flutter: uses-material-design: true assets: - assets/images/illustrations/app_logo.png -# - assets/images/icons/reaction_undefined.png -# - assets/images/icons/reaction_like.png -# - assets/images/icons/reaction_dislike.png + - assets/images/icons/reaction_undefined.jpeg + - assets/images/icons/reaction_like.jpeg + - assets/images/icons/reaction_dislike.jpeg fonts: - family: Kanit fonts: From bad1da28c3eb932ab807b2f37c3da26d914b3127 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Sun, 3 Nov 2024 22:26:21 +0100 Subject: [PATCH 09/23] fix: assets files --- .../assets/images/icons/reaction_dislike.jpeg | Bin 0 -> 998 bytes .../assets/images/icons/reaction_like.jpeg | Bin 0 -> 932 bytes .../assets/images/icons/reaction_undefined.jpeg | Bin 0 -> 1050 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 flutter_module/assets/images/icons/reaction_dislike.jpeg create mode 100644 flutter_module/assets/images/icons/reaction_like.jpeg create mode 100644 flutter_module/assets/images/icons/reaction_undefined.jpeg diff --git a/flutter_module/assets/images/icons/reaction_dislike.jpeg b/flutter_module/assets/images/icons/reaction_dislike.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..dbc7ebaba3812ec89895f1229c3fb16911ffb99a GIT binary patch literal 998 zcmex=DK+5#AqGJX1`P%sW=16jCP7AK zLB{__7<3pIm>Ge90RdRqn3x&aQG~#f|8FtyFf%eR2`~#VFfi5j>=aqODyX1qPnWLr z$`cR$Eai^YdhjtQECYJ@2m=#LH3KtHv!Y>-$>b9-sgxlPe2NPFz`B%GlXHF*xh;nW&TV1Wsw@nw*Gx$u99T(!A)p z_>tld#T~!(0k@%+Jaicc@yD*0@_E49UF*X&RuvrP`iOzoq79Xx5qXfO5a zc#E!gaY3WWw<^&sk3P!9`zOw?SayZ|8FzNFa-yOh^G3gw7I`fyER70mprBF|0LDKk z^56l(DCo!_pkSD&7+ARQ!$(-;*((Zi{FM)pC`jYiw`;XcTt7)>X2t894u%!kH#{Qu zdX*s!eRJCx zde;>6tk_+#{Bh-h1q&3IdO>cH1GDK+5#AqGJX1`P%sW=16jCP7AK zLB{__7<7ORWMT$75&>A*7+KhvL9Ryy|8Fty097&xFbgm+Fzu95Q(p4W+e#($t-$1z z4KmjkXH2!`sBZQQFfxk? zh=`gBE4TsmaR9AmWM_Kt`JL?p3jvj|?ef#q&SWPZVGt9#e6n+a^49FhN&eA7RUZ$& zuD!hXkp1Exr*hBif4t&-LI0!^9nERymiTvAoIcUM_RDO+7B|n$Ii9bR{3Cm0FFJO| zV>nIVgTKt@b{mk>Lsj#SI(+8+$IqaEEx~neL^=CA%>b=dRl^Q}2hcNT<`*A(ul?91Qy(G^Pli1o~1z02m$2tZ*-Y60xA6K;j1lMaRI#g|G;+S6n`WyZwOZd_9+=yA1t# z6sBJY&~MOPKi^r3`D{oqM{VAPjc+bITJpf|(Zv4L^uOCTANaCp)-=X5uQ_VpT-b3u zF5;-G$i36zGo5_x!L|Y;9cV2qRRC=jWDrnPFmyBsOk4;HDMovNjTdy-rl?sl+)BD- z<`R56^M&~Xrh~y3KZ;)X&(Orp`I&!TvP)GakKl5vDK+5#AqGJX1_K5IW=16jCP7AK zLB{__7<3pIn3zE>MF3VNW;PZ^R56h3|62?^%!~|70?YzHO_#OzUpzSXA_LR3lR1ia z3MQ|;G;sst+>1;Vmo5N}Wr8VYU$E1(^M9KA+b~211baDmYgN<)*7M zc4Vy7tq2J#{?;xYbm_Ur^9Psgfng#q08AV}0E%%4U=(y@P*gA!2uv(o2#Z*I#TSfz z_YXu*Ty#5RMutVpuKx@v$=8?FINyB3S=tb|rYcRSC^#MrI;*1P}iiQe-i2{yrM+qn-+5OsAxUN=s@}v6GXRYpgSuBs1i@O|I zBdfER=~>011}Db&f2SA9E6#fM=QX#-ck}0wQ6Cc4|7S?O(A8JomBI3av0A@*$zndu ZYsaT=u?HCuaZn~d(AvrZWODugn*b!fP1gVb literal 0 HcmV?d00001 From b1fefce8da189b7d0ae090257a986933d5145a39 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Sun, 3 Nov 2024 22:27:34 +0100 Subject: [PATCH 10/23] feat: snippet reactions --- .../domain/reaction/SetUserReactionUseCase.kt | 8 ++-- .../snipmeapp/domain/reaction/UserReaction.kt | 4 +- .../repository/snippet/SnippetRepository.kt | 2 +- .../snippet/SnippetRepositoryReal.kt | 25 +++++------ .../domain/snippets/SnippetResponseMapper.kt | 10 ++--- .../infrastructure/local/AppDatabase.kt | 2 +- .../infrastructure/local/ReactionEntry.kt | 34 ++++++++++++++ .../infrastructure/local/SnippetDao.kt | 44 ++++++++++++++----- .../infrastructure/local/SnippetEntity.kt | 10 ++--- 9 files changed, 96 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/ReactionEntry.kt diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt index f001e34..855251e 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt @@ -15,9 +15,11 @@ class SetUserReactionUseCase( operator fun invoke(snippet: Snippet, reaction: UserReaction): Single { val targetReaction = getTargetReaction(snippet, reaction) return auth() - .andThen(repository.reaction(snippet.uuid, targetReaction)) .andThen(getSingleUser()) - .flatMap { repository.snippet(snippet.uuid, it.id) } - .doOnSuccess { repository.updateListener.onNext(it) } + .flatMap { user -> + repository.reaction(snippet.uuid, user.id, targetReaction) + .andThen(repository.snippet(snippet.uuid, user.id)) + .doOnSuccess { repository.updateListener.onNext(it) } + } } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/UserReaction.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/UserReaction.kt index 0d7f461..dc53fac 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/UserReaction.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/UserReaction.kt @@ -1,7 +1,7 @@ package dev.snipme.snipmeapp.domain.reaction enum class UserReaction { + DISLIKE, NONE, - LIKE, - DISLIKE + LIKE } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt index 8257255..7790765 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt @@ -35,7 +35,7 @@ interface SnippetRepository { fun count(): Single - fun reaction(uuid: String, reaction: UserReaction): Completable + fun reaction(uuid: String, userId: Int, reaction: UserReaction): Completable fun delete(uuid: String): Completable } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt index b651d6d..9d8e86c 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt @@ -5,6 +5,7 @@ import dev.snipme.snipmeapp.domain.reaction.UserReaction import dev.snipme.snipmeapp.domain.snippets.Snippet import dev.snipme.snipmeapp.domain.snippets.SnippetResponseMapper import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility +import dev.snipme.snipmeapp.infrastructure.local.ReactionEntry import dev.snipme.snipmeapp.infrastructure.local.SnippetDao import dev.snipme.snipmeapp.infrastructure.local.SnippetEntry import dev.snipme.snipmeapp.util.extension.mapError @@ -50,9 +51,6 @@ class SnippetRepositoryReal( visibility = visibility.name, ownerId = userId, language = language, - numberOfLikes = 0, - numberOfDislikes = 0, - userReaction = "" ) ) .mapError { errorHandler.handle(it) } @@ -71,7 +69,7 @@ class SnippetRepositoryReal( visibility: SnippetVisibility, userId: Int ): Single = - service.update(uuid.toInt(), title, code, language, visibility.name) + service.update(uuid.toInt(), title, code, language, visibility.name) .mapError { errorHandler.handle(it) } .andThen( service.snippet(uuid.toInt(), userId) @@ -79,18 +77,15 @@ class SnippetRepositoryReal( .map { mapper(it) } ) - override fun delete(uuid: String): Completable = service.delete(uuid.toInt()) + override fun delete(uuid: String): Completable = + service.delete(uuid.toInt()).mapError { errorHandler.handle(it) } override fun count() = - service.count() - .mapError { errorHandler.handle(it) } - .map{it} - - override fun reaction(uuid: String, reaction: UserReaction): Completable { - TODO("Not yet implemented") - } + service.count() + .mapError { errorHandler.handle(it) } + .map { it } -// override fun reaction(uuid: String, reaction: UserReaction) = throw NotImplementedError() -// service.rate(RateSnippetRequest(uuid, reaction.name)) -// .mapError { errorHandler.handle(it) } + override fun reaction(uuid: String, userId: Int, reaction: UserReaction): Completable = + service.reaction(ReactionEntry(snippetId = uuid.toInt(), userId = userId, reaction = reaction.ordinal.toShort())) + .mapError { errorHandler.handle(it) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt index 2bd0e65..f999162 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt @@ -2,7 +2,7 @@ package dev.snipme.snipmeapp.domain.snippets import android.text.SpannableString import dev.snipme.snipmeapp.domain.reaction.UserReaction -import dev.snipme.snipmeapp.infrastructure.local.SnippetWithOwner +import dev.snipme.snipmeapp.infrastructure.local.SnippetExtended import dev.snipme.snipmeapp.util.SyntaxHighlighter.getHighlighted import dev.snipme.snipmeapp.util.extension.lines import dev.snipme.snipmeapp.util.extension.newLineChar @@ -14,7 +14,7 @@ const val PREVIEW_COUNT = 5 class SnippetResponseMapper { - operator fun invoke(response: SnippetWithOwner) = with(response.snippet) { + operator fun invoke(response: SnippetExtended) = with(response.snippet) { return@with Snippet( uuid = id.toString(), title = title, @@ -24,9 +24,9 @@ class SnippetResponseMapper { isOwner = response.isOwner, owner = Owner(ownerId , response.ownerName), modifiedAt = modifiedAt.toDate(), - numberOfLikes = numberOfLikes, - numberOfDislikes = numberOfDislikes, - userReaction = getUserReaction(userReaction) + numberOfLikes = response.numberOfLikes, + numberOfDislikes = response.numberOfDislikes, + userReaction = getUserReaction(response.userReaction) ) } diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt index fd24631..e77bf49 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt @@ -4,7 +4,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import android.content.Context -@Database(entities = [UserEntry::class, SnippetEntry::class], version = 1) +@Database(entities = [UserEntry::class, SnippetEntry::class, ReactionEntry::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao abstract fun snippetDao(): SnippetDao diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/ReactionEntry.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/ReactionEntry.kt new file mode 100644 index 0000000..b32bad5 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/ReactionEntry.kt @@ -0,0 +1,34 @@ +package dev.snipme.snipmeapp.infrastructure.local + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import dev.snipme.snipmeapp.bridge.Bridge.Snippet + +@Entity( + tableName = "reactions", + foreignKeys = [ + ForeignKey( + entity = UserEntry::class, + parentColumns = arrayOf("id"), + childColumns = arrayOf("userId"), + onUpdate = ForeignKey.CASCADE, + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = SnippetEntry::class, + parentColumns = arrayOf("id"), + childColumns = arrayOf("snippetId"), + onUpdate = ForeignKey.CASCADE, + onDelete = ForeignKey.CASCADE + ), + ], + indices = [Index(value = ["userId", "snippetId"], unique = true)] +) +data class ReactionEntry( + @PrimaryKey(true) val id: Int = 0, + val userId: Int, + val snippetId: Int, + val reaction: Short // 0 dislike, 1 none, 2 like +) \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt index d3fe40b..ddbdb29 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt @@ -9,25 +9,43 @@ import io.reactivex.Single @Dao interface SnippetDao { - @Query(""" - SELECT s.*, u.login as ownerName, - CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner + @Query( + """ + SELECT s.*, u.login as ownerName, + CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner, + CASE WHEN r.reaction = 0 THEN 'DISLIKE' ELSE CASE WHEN r.reaction = 2 THEN 'LIKE' ELSE 'NONE' END END as userReaction, + (Select Count(*) FROM reactions as r where r.userId = :userId and r.snippetId = :uuid and reaction = 2) as numberOfLikes, + (Select Count(*) FROM reactions as r where r.userId = :userId and r.snippetId = :uuid and reaction = 0) as numberOfDislikes FROM snippets as s INNER JOIN users as u ON s.ownerId = u.id + LEFT JOIN reactions as r ON r.userId = :userId and r.snippetId = :uuid WHERE s.id = :uuid - """) - fun snippet(uuid: Int, userId: Int) : Single + """ + ) + fun snippet(uuid: Int, userId: Int): Single - @Query("SELECT s.*, u.login as ownerName, CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner FROM snippets as s INNER JOIN users as u ON s.ownerId = u.id") - fun snippets(userId: Int) : Single> + @Query( + """ + SELECT s.*, u.login as ownerName, + CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner, + CASE WHEN r.reaction = -1 THEN "DISLIKE" ELSE CASE WHEN r.reaction = 1 THEN "LIKE" ELSE "NONE" END END as userReaction, + (Select Count(*) FROM reactions as r where r.userId = :userId and r.snippetId = s.id and reaction = 1) as numberOfLikes, + (Select Count(*) FROM reactions as r where r.userId = :userId and r.snippetId = s.id and reaction = -1) as numberOfDislikes + FROM snippets as s + INNER JOIN users as u ON s.ownerId = u.id + LEFT JOIN reactions as r ON r.userId = :userId + """ + ) + fun snippets(userId: Int): Single> @Query("SELECT COUNT(*) FROM snippets") - fun count() : Single + fun count(): Single @Insert(onConflict = OnConflictStrategy.REPLACE) fun create(snippet: SnippetEntry): Single - @Query(""" + @Query( + """ UPDATE snippets SET title = :title, code = :code, @@ -35,16 +53,20 @@ interface SnippetDao { visibility = :visibility, language = :language WHERE id = :uuid - """) + """ + ) fun update( uuid: Int, title: String, code: String, visibility: String, language: String, - ) : Completable + ): Completable @Query("DELETE FROM snippets WHERE id = :uuid") fun delete(uuid: Int): Completable + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun reaction(reaction: ReactionEntry): Completable + } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetEntity.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetEntity.kt index 24c1bd3..425f7c2 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetEntity.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetEntity.kt @@ -15,14 +15,14 @@ data class SnippetEntry( val visibility: String, val ownerId: Int, val language: String, - val numberOfLikes: Int, - val numberOfDislikes: Int, - val userReaction: String ) -data class SnippetWithOwner( +data class SnippetExtended( @Embedded val snippet: SnippetEntry, val ownerName: String, - val isOwner: Boolean + val isOwner: Boolean, + val userReaction: String, + val numberOfLikes: Int, + val numberOfDislikes: Int, ) From 35f5edca31fdd863c21d71452fe654482c5331da Mon Sep 17 00:00:00 2001 From: xJac0b Date: Sun, 3 Nov 2024 22:30:01 +0100 Subject: [PATCH 11/23] fix: queries for likes and dislikes --- .../snipme/snipmeapp/infrastructure/local/SnippetDao.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt index ddbdb29..0c876ec 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt @@ -14,8 +14,8 @@ interface SnippetDao { SELECT s.*, u.login as ownerName, CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner, CASE WHEN r.reaction = 0 THEN 'DISLIKE' ELSE CASE WHEN r.reaction = 2 THEN 'LIKE' ELSE 'NONE' END END as userReaction, - (Select Count(*) FROM reactions as r where r.userId = :userId and r.snippetId = :uuid and reaction = 2) as numberOfLikes, - (Select Count(*) FROM reactions as r where r.userId = :userId and r.snippetId = :uuid and reaction = 0) as numberOfDislikes + (Select Count(*) FROM reactions as r where r.snippetId = :uuid and reaction = 2) as numberOfLikes, + (Select Count(*) FROM reactions as r where r.snippetId = :uuid and reaction = 0) as numberOfDislikes FROM snippets as s INNER JOIN users as u ON s.ownerId = u.id LEFT JOIN reactions as r ON r.userId = :userId and r.snippetId = :uuid @@ -29,8 +29,8 @@ interface SnippetDao { SELECT s.*, u.login as ownerName, CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner, CASE WHEN r.reaction = -1 THEN "DISLIKE" ELSE CASE WHEN r.reaction = 1 THEN "LIKE" ELSE "NONE" END END as userReaction, - (Select Count(*) FROM reactions as r where r.userId = :userId and r.snippetId = s.id and reaction = 1) as numberOfLikes, - (Select Count(*) FROM reactions as r where r.userId = :userId and r.snippetId = s.id and reaction = -1) as numberOfDislikes + (Select Count(*) FROM reactions as r where r.snippetId = s.id and reaction = 2) as numberOfLikes, + (Select Count(*) FROM reactions as r where r.snippetId = s.id and reaction = 0) as numberOfDislikes FROM snippets as s INNER JOIN users as u ON s.ownerId = u.id LEFT JOIN reactions as r ON r.userId = :userId From dc3c7913bd68b122fbafbc089cb594e51f62a317 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Sun, 3 Nov 2024 22:34:21 +0100 Subject: [PATCH 12/23] fix: query for userReaction --- .../dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt index 0c876ec..e6f0722 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt @@ -28,7 +28,7 @@ interface SnippetDao { """ SELECT s.*, u.login as ownerName, CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner, - CASE WHEN r.reaction = -1 THEN "DISLIKE" ELSE CASE WHEN r.reaction = 1 THEN "LIKE" ELSE "NONE" END END as userReaction, + CASE WHEN r.reaction = 0 THEN "DISLIKE" ELSE CASE WHEN r.reaction = 2 THEN "LIKE" ELSE "NONE" END END as userReaction, (Select Count(*) FROM reactions as r where r.snippetId = s.id and reaction = 2) as numberOfLikes, (Select Count(*) FROM reactions as r where r.snippetId = s.id and reaction = 0) as numberOfDislikes FROM snippets as s From 4b05f1ffe22a8df186861dd0d231b4c7555e5f9a Mon Sep 17 00:00:00 2001 From: xJac0b Date: Sun, 3 Nov 2024 23:08:57 +0100 Subject: [PATCH 13/23] feat: db createFromAsset --- app/src/main/assets/app_database.db | Bin 0 -> 36864 bytes .../dev/snipme/snipmeapp/di/ServiceModule.kt | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 app/src/main/assets/app_database.db diff --git a/app/src/main/assets/app_database.db b/app/src/main/assets/app_database.db new file mode 100644 index 0000000000000000000000000000000000000000..c233e45c36fdc355823a311efa8b68313d181f65 GIT binary patch literal 36864 zcmeI)-%i_B90zbaA*8g0Oq!~w>QD^RCbE)XI|-0%XidST=^&xOS<9p;c5DOQY@c8o zXnF@4udusa?P9O6SDE?(eSm4x?slBNB&3=;Y1PKQR-EA9$vMAsKF36|5})0tTc&St z`^2?XpUW`sF`*Fifa4g33DJFu?!9f2{xQ)#2;yva?}o;{gqXDZ<1{Ub-u#t`{WSA> z=KJZ>+mTy@9%6$41Rwwb2tWV=5P-lt5okrD@uem9Y|d93mf;d&*KO7F4Yxi%FtuKl zin7GX#fMuGcjf)uf~j+*vMg;%Rc@zR+Adc2xG$u=MN>B%-}GDcL)AOvWa+t_JbQmK z8sAQ^XPc&@8!y^Vb=Oce-z1J#KlTi_q}M&iY&H$Qr1wj1bh}V1m!8%nnr%aR&MALY zUEwNaPU$zIa0^N&gR;me{Ul2AoJ6AWM1s9YbpU$(>DP=72@JPr>dLs$bH$omDV1r( zZK*7CZ3|kDs7en_x>Q0Tvd7`Ri*N}wA)GR zDGLhiZ!&PH?Q8^-x?bE}FK$RdWJB7LE=3l(p+*ON8*aB7f2*dG=O0W&;y%}vih%TU^<-{o1*h(0b#jeSEe`d2I1L^t=%@nt8bXEscWp6gfZ zky9r)smD#4VyVu-v3fAtk>pNzGJgL)yVt=vq755$&p0|Z9L?~C4Y4QaHLTd;_xT1MvsDY%GlU&+7u z>79HgBMAf`009U<00Izz00bZa0SG_<0{@&q@ZCHc`<Yq z@u~Lm2f_7!WSai||A*K|Ge6Jl(Lb?400Izz00bZa0SG_<0uX?}^$}1aQ_Q{G+S=V4 zcjb~ySGCMFUDNRw<{uMFjkr%TmflrAmvLF^Rd?CxHmvh;#-W1=dJ=`j- z@3j0w;(R?pGYD&I_onW2GGqCRheHc2)ROzPiI77(EdoA zF0T0{U9&N${B4*vU!eo&HJ{mS(Iq_XG&%Ou?Xg%69Zfsg6ZJ%$Pk!3ZmY0RqLN-?r z`1HzZevoY>-Wg3$`6NV>?FGr^I%5$vx>_f(j5J-tua{-tB{mnldslPKrce308HE%t z$O2ywc}ke3-|BZWUX2ejcG^%`ns+70+npt$H%skt(PafKy+2K~IZ+m}!88fEH2wX+ zAk9ep@-*T8|LgNljgdeA0uX=z1Rwwb2tWV=5P$##Mg*dX2s6d96QP^5fQ|jZ&<8dM zKmY;|fB*y_009U<00Izz00gd^z~n@5{h#Q3|Nn}Ky}E7##CRY80SG_<0uX=z1Rwwb r2tWV={|kW+KM6D2%Q { Room.databaseBuilder( get(), AppDatabase::class.java, "app_database" - ).build() + ).createFromAsset("app_database.db").build() } single { get().userDao() } From 05abdaa52fa225a117e7dce2c86b9d71dc9bc383 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Sun, 3 Nov 2024 23:17:25 +0100 Subject: [PATCH 14/23] fix: snippets query, left join condition --- .../dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt index e6f0722..8f070fe 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt @@ -33,7 +33,7 @@ interface SnippetDao { (Select Count(*) FROM reactions as r where r.snippetId = s.id and reaction = 0) as numberOfDislikes FROM snippets as s INNER JOIN users as u ON s.ownerId = u.id - LEFT JOIN reactions as r ON r.userId = :userId + LEFT JOIN reactions as r ON r.userId = :userId and r.snippetId = s.id """ ) fun snippets(userId: Int): Single> From 17fe4d57765eecdb52a79fab6cb1149475b64456 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Tue, 5 Nov 2024 16:18:49 +0100 Subject: [PATCH 15/23] fix: changes to allow hot reload and restart --- .../presentation/screens/login_screen.dart | 71 +++++++++---------- .../lib/presentation/screens/main_screen.dart | 9 +-- .../widgets/text_input_field.dart | 23 +++--- 3 files changed, 47 insertions(+), 56 deletions(-) diff --git a/flutter_module/lib/presentation/screens/login_screen.dart b/flutter_module/lib/presentation/screens/login_screen.dart index 32f8ba8..d3a208c 100644 --- a/flutter_module/lib/presentation/screens/login_screen.dart +++ b/flutter_module/lib/presentation/screens/login_screen.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_module/generated/assets.dart'; @@ -84,44 +83,42 @@ class _MainPage extends HookWidget { data: state.state, builder: (BuildContext context, _) { return NoOverscrollSingleChildScrollView( - child: Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: Dimens.xxl), - TextStyles.appLogo('SnipMe'), - const SizedBox(height: Dimens.xxl), - Image.asset(Assets.appLogo), - const SizedBox(height: Dimens.xxl), - const TextStyles.secondary('Snip your favorite code'), - PaddingStyles.regular( - LoginInputCard( - emailValue: email.value, - passwordValue: password.value, - onEmailChanged: (emailValue) { - email.value = emailValue; - }, - onPasswordChanged: (passwordValue) { - password.value = passwordValue; - }, - onValidChanged: (isValid) { - validationCorrect.value = isValid; - }, - ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: Dimens.xxl), + TextStyles.appLogo('SnipMe'), + const SizedBox(height: Dimens.xxl), + Image.asset(Assets.appLogo), + const SizedBox(height: Dimens.xxl), + const TextStyles.secondary('Snip your favorite code'), + PaddingStyles.regular( + LoginInputCard( + emailValue: email.value, + passwordValue: password.value, + onEmailChanged: (emailValue) { + email.value = emailValue; + }, + onPasswordChanged: (passwordValue) { + password.value = passwordValue; + }, + onValidChanged: (isValid) { + validationCorrect.value = isValid; + }, ), - Center( - child: RoundedActionButton( - icon: Icons.check_circle, - title: 'Login', - enabled: validationCorrect.value, - onPressed: () { - model.loginOrRegister(email.value, password.value); - }, - ), + ), + Center( + child: RoundedActionButton( + icon: Icons.check_circle, + title: 'Login', + enabled: validationCorrect.value, + onPressed: () { + model.loginOrRegister(email.value, password.value); + }, ), - const SizedBox(height: Dimens.xxl), - ], - ), + ), + const SizedBox(height: Dimens.xxl), + ], ), ); }, diff --git a/flutter_module/lib/presentation/screens/main_screen.dart b/flutter_module/lib/presentation/screens/main_screen.dart index 133a6a7..2c59a13 100644 --- a/flutter_module/lib/presentation/screens/main_screen.dart +++ b/flutter_module/lib/presentation/screens/main_screen.dart @@ -38,7 +38,6 @@ class MainScreen extends NamedScreen implements UserScreen { model: model, ); } - } class _MainPage extends HookWidget { @@ -87,8 +86,7 @@ class _MainPage extends HookWidget { return Scaffold( backgroundColor: ColorStyles.pageBackground(), body: ViewStateWrapper>( - isLoading: - state.state == ModelState.loading || state.isLoading == true, + isLoading: state.state == ModelState.loading || state.isLoading == true, error: state.error, data: state.data?.cast(), builder: (_, snippets) { @@ -238,10 +236,7 @@ class _MainPageData extends HookWidget { ]; }, body: CustomScrollView( - scrollBehavior: const ScrollBehavior( - //TODO: change line below - // androidOverscrollIndicator: AndroidOverscrollIndicator.stretch, - ), + scrollBehavior: const ScrollBehavior(), slivers: [ SliverList( delegate: SliverChildListDelegate([ diff --git a/flutter_module/lib/presentation/widgets/text_input_field.dart b/flutter_module/lib/presentation/widgets/text_input_field.dart index fad2549..8202335 100644 --- a/flutter_module/lib/presentation/widgets/text_input_field.dart +++ b/flutter_module/lib/presentation/widgets/text_input_field.dart @@ -6,14 +6,13 @@ import 'package:flutter_module/presentation/styles/dimens.dart'; typedef TextInputCallback = Function(String value); class TextInputField extends HookWidget { - const TextInputField({ - super.key, - required this.label, - this.isPassword = false, - this.onChanged, - this.validator, - this.initialValue - }); + const TextInputField( + {super.key, + required this.label, + this.isPassword = false, + this.onChanged, + this.validator, + this.initialValue}); final String label; final String? initialValue; @@ -31,11 +30,11 @@ class TextInputField extends HookWidget { useEffect(() { controller.addListener(() { onChanged?.call(controller.text); - error.value = - controller.text.isNotEmpty ? validator?.call(controller.text) : null; + error.value = controller.text.isNotEmpty + ? validator?.call(controller.text) + : null; }); - - return () => controller.dispose(); + return null; }, []); return TextFormField( From 9337155f4662530f4330cf8ef3f28fada98680e6 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Tue, 5 Nov 2024 16:19:31 +0100 Subject: [PATCH 16/23] feat: change style of ChoiceChip widget --- .../widgets/filter_list_view.dart | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/flutter_module/lib/presentation/widgets/filter_list_view.dart b/flutter_module/lib/presentation/widgets/filter_list_view.dart index ff58ad6..f1c03a0 100644 --- a/flutter_module/lib/presentation/widgets/filter_list_view.dart +++ b/flutter_module/lib/presentation/widgets/filter_list_view.dart @@ -22,15 +22,26 @@ class FilterListView extends StatelessWidget { physics: const BouncingScrollPhysics(), itemCount: filters.length, scrollDirection: Axis.horizontal, - separatorBuilder: (_, __) => const SizedBox(width: Dimens.m,), + separatorBuilder: (_, __) => const SizedBox( + width: Dimens.m, + ), itemBuilder: (BuildContext context, int index) { final filter = filters[index]; - return ChoiceChip( - disabledColor: ColorStyles.filterBackgroundColor().withOpacity(0.08), - selectedColor: ColorStyles.filterBackgroundColor().withOpacity(0.25), - label: Text(filter ?? ''), - selected: selected.contains(filter), - onSelected: (isSelected) => onSelected?.call(filter ?? '', isSelected), + return ChipTheme( + data: ChipThemeData( + side: BorderSide.none, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + ), + child: ChoiceChip( + backgroundColor: + ColorStyles.filterBackgroundColor().withOpacity(0.13), + selectedColor: + ColorStyles.filterBackgroundColor().withOpacity(0.25), + label: Text(filter ?? ''), + selected: selected.contains(filter), + onSelected: (isSelected) => + onSelected?.call(filter ?? '', isSelected)), ); }, ); From d817b630b4101b6072e8593890b0090176d38865 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Tue, 5 Nov 2024 17:33:29 +0100 Subject: [PATCH 17/23] fix: remove gradle dir from gitignore --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 01a0e4b..bfbd8c8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,4 @@ .externalNativeBuild .cxx local.properties -/.idea/ -/gradle/ \ No newline at end of file +/.idea/ \ No newline at end of file From 76e2cd303685ac365ce26d2bc2f1039b2cdd07ee Mon Sep 17 00:00:00 2001 From: xJac0b Date: Tue, 5 Nov 2024 17:35:27 +0100 Subject: [PATCH 18/23] fix: gradle dir --- gradle/libs.versions.toml | 93 +++++++++++++++++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 ++ 3 files changed, 99 insertions(+) create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..da38f03 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,93 @@ +[versions] +adapterRxjava2 = "2.9.0" +agp = "8.6.1" +annotation = "1.8.2" +appcompat = "1.7.0" +codeview = "1.3.2" +compiler = "4.16.0" +constraintlayout = "2.1.4" +firebaseAnalyticsKtx = "22.1.2" +firebaseBom = "33.4.0" +fragmentKtx = "1.8.4" +glide = "4.12.0" +kodeview = "0.8.0" +kotlin = "2.0.21" +coreKtx = "1.13.1" +junit = "4.13.2" +junitVersion = "1.2.1" +espressoCore = "3.6.1" +lifecycleRuntimeKtx = "2.8.6" +activityCompose = "1.9.2" +composeBom = "2024.09.03" +loggingInterceptor = "4.9.0" +material = "1.12.0" +mockk = "1.10.6" +moshiKotlin = "2.9.0" +moshiKotlinCodegen = "1.13.0" +moshiKotlinVersion = "1.13.0" +navigationRuntimeKtx = "2.8.2" +reactivenetworkRx2 = "3.0.8" +recyclerview = "1.3.2" +retrofit = "2.9.0" +roomRuntime = "2.6.1" +rxandroid = "2.1.1" +rxkotlin = "2.4.0" +koinBom = "4.0.0" +timber = "4.7.1" + +[libraries] +adapter-rxjava2 = { module = "com.squareup.retrofit2:adapter-rxjava2", version.ref = "adapterRxjava2" } +androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" } +androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-reactivestreams-ktx = { module = "androidx.lifecycle:lifecycle-reactivestreams-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigationRuntimeKtx" } +androidx-navigation-runtime-ktx = { module = "androidx.navigation:navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" } +androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigationRuntimeKtx" } +androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } +androidx-room-rxjava2 = { module = "androidx.room:room-rxjava2", version.ref = "roomRuntime" } +codeview = { module = "com.github.kbiakov:CodeView-Android", version.ref = "codeview" } +compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "compiler" } +converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "moshiKotlin" } +firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "firebaseAnalyticsKtx" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } +glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +kodeview = { module = "dev.snipme:kodeview", version.ref = "kodeview" } +koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinBom" } +koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koinBom" } +koin-core = { module = "io.insert-koin:koin-core" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } +logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" } +material = { module = "com.google.android.material:material", version.ref = "material" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiKotlinVersion" } +moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshiKotlinCodegen" } +reactivenetwork-rx2 = { module = "com.github.pwittchen:reactivenetwork-rx2", version.ref = "reactivenetworkRx2" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +rxandroid = { module = "io.reactivex.rxjava2:rxandroid", version.ref = "rxandroid" } +rxkotlin = { module = "io.reactivex.rxjava2:rxkotlin", version.ref = "rxkotlin" } +timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..fdf72cd --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Oct 14 17:20:55 CEST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists From 3ac3655bb1a036ceb7e1a44b2dbc7b9e0340d227 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Tue, 5 Nov 2024 17:55:17 +0100 Subject: [PATCH 19/23] fix: flutter_module/.android --- flutter_module/.android/Flutter/build.gradle | 56 ++++++ .../Flutter/src/main/AndroidManifest.xml | 17 ++ .../plugins/GeneratedPluginRegistrant.java | 19 +++ flutter_module/.android/app/build.gradle | 44 +++++ .../.android/app/src/main/AndroidManifest.xml | 51 ++++++ .../flutter_module/host/MainActivity.java | 6 + .../main/res/drawable/launch_background.xml | 12 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../app/src/main/res/values/styles.xml | 8 + flutter_module/.android/build.gradle | 40 +++++ flutter_module/.android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + flutter_module/.android/gradlew | 160 ++++++++++++++++++ flutter_module/.android/gradlew.bat | 90 ++++++++++ .../.android/include_flutter.groovy | 16 ++ flutter_module/.android/settings.gradle | 6 + .../.android/src/main/AndroidManifest.xml | 3 + flutter_module/.gitignore | 1 - 19 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 flutter_module/.android/Flutter/build.gradle create mode 100644 flutter_module/.android/Flutter/src/main/AndroidManifest.xml create mode 100644 flutter_module/.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java create mode 100644 flutter_module/.android/app/build.gradle create mode 100644 flutter_module/.android/app/src/main/AndroidManifest.xml create mode 100644 flutter_module/.android/app/src/main/java/dev/snipme/flutter_module/host/MainActivity.java create mode 100644 flutter_module/.android/app/src/main/res/drawable/launch_background.xml create mode 100644 flutter_module/.android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 flutter_module/.android/app/src/main/res/values/styles.xml create mode 100644 flutter_module/.android/build.gradle create mode 100644 flutter_module/.android/gradle.properties create mode 100755 flutter_module/.android/gradle/wrapper/gradle-wrapper.jar create mode 100644 flutter_module/.android/gradle/wrapper/gradle-wrapper.properties create mode 100755 flutter_module/.android/gradlew create mode 100755 flutter_module/.android/gradlew.bat create mode 100644 flutter_module/.android/include_flutter.groovy create mode 100644 flutter_module/.android/settings.gradle create mode 100644 flutter_module/.android/src/main/AndroidManifest.xml diff --git a/flutter_module/.android/Flutter/build.gradle b/flutter_module/.android/Flutter/build.gradle new file mode 100644 index 0000000..becb6b9 --- /dev/null +++ b/flutter_module/.android/Flutter/build.gradle @@ -0,0 +1,56 @@ +// Generated file. Do not edit. + +def localProperties = new Properties() +def localPropertiesFile = new File(buildscript.sourceFile.parentFile.parentFile, "local.properties") +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader("UTF-8") { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty("flutter.sdk") +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") +if (flutterVersionCode == null) { + flutterVersionCode = "1" +} + +def flutterVersionName = localProperties.getProperty("flutter.versionName") +if (flutterVersionName == null) { + flutterVersionName = "1.0" +} + +apply plugin: "com.android.library" +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +group = "dev.snipme.flutter_module" +version = "1.0" + +android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace = "dev.snipme.flutter_module" + } + + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutterVersionCode.toInteger() + versionName = flutterVersionName + } +} + +flutter { + source = "../.." +} diff --git a/flutter_module/.android/Flutter/src/main/AndroidManifest.xml b/flutter_module/.android/Flutter/src/main/AndroidManifest.xml new file mode 100644 index 0000000..801919c --- /dev/null +++ b/flutter_module/.android/Flutter/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/flutter_module/.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/flutter_module/.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 0000000..539ab02 --- /dev/null +++ b/flutter_module/.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,19 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + } +} diff --git a/flutter_module/.android/app/build.gradle b/flutter_module/.android/app/build.gradle new file mode 100644 index 0000000..be23c63 --- /dev/null +++ b/flutter_module/.android/app/build.gradle @@ -0,0 +1,44 @@ +def flutterPluginVersion = "managed" + +apply plugin: "com.android.application" + +android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace = "dev.snipme.flutter_module.host" + } + + compileSdk = 34 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + defaultConfig { + applicationId = "dev.snipme.flutter_module.host" + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + } + + buildTypes { + profile { + initWith debug + } + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.debug + } + } + +} +buildDir = new File(rootProject.projectDir, "../build/host") +dependencies { + implementation(project(":flutter")) + implementation(fileTree(dir: "libs", include: ["*.jar"])) + implementation("androidx.appcompat:appcompat:1.0.2") + implementation("androidx.constraintlayout:constraintlayout:1.1.3") +} diff --git a/flutter_module/.android/app/src/main/AndroidManifest.xml b/flutter_module/.android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c30c61a --- /dev/null +++ b/flutter_module/.android/app/src/main/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter_module/.android/app/src/main/java/dev/snipme/flutter_module/host/MainActivity.java b/flutter_module/.android/app/src/main/java/dev/snipme/flutter_module/host/MainActivity.java new file mode 100644 index 0000000..cc518bb --- /dev/null +++ b/flutter_module/.android/app/src/main/java/dev/snipme/flutter_module/host/MainActivity.java @@ -0,0 +1,6 @@ +package dev.snipme.flutter_module.host; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity { +} diff --git a/flutter_module/.android/app/src/main/res/drawable/launch_background.xml b/flutter_module/.android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/flutter_module/.android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/flutter_module/.android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter_module/.android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/flutter_module/.android/app/src/main/res/values/styles.xml b/flutter_module/.android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..2676b0b --- /dev/null +++ b/flutter_module/.android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/flutter_module/.android/build.gradle b/flutter_module/.android/build.gradle new file mode 100644 index 0000000..ca43a8a --- /dev/null +++ b/flutter_module/.android/build.gradle @@ -0,0 +1,40 @@ +// Generated file. Do not edit. + +buildscript { + ext.kotlin_version = "1.7.10" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:7.3.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace = "dev.snipme.flutter_module" + } + + compileSdk = 34 + defaultConfig { + minSdk = 21 + } +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version") +} diff --git a/flutter_module/.android/gradle.properties b/flutter_module/.android/gradle.properties new file mode 100644 index 0000000..3b5b324 --- /dev/null +++ b/flutter_module/.android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/flutter_module/.android/gradle/wrapper/gradle-wrapper.jar b/flutter_module/.android/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/flutter_module/.android/gradle/wrapper/gradle-wrapper.properties b/flutter_module/.android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c472b9 --- /dev/null +++ b/flutter_module/.android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/flutter_module/.android/gradlew b/flutter_module/.android/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/flutter_module/.android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/flutter_module/.android/gradlew.bat b/flutter_module/.android/gradlew.bat new file mode 100755 index 0000000..8a0b282 --- /dev/null +++ b/flutter_module/.android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/flutter_module/.android/include_flutter.groovy b/flutter_module/.android/include_flutter.groovy new file mode 100644 index 0000000..6ceda7c --- /dev/null +++ b/flutter_module/.android/include_flutter.groovy @@ -0,0 +1,16 @@ +def scriptFile = getClass().protectionDomain.codeSource.location.toURI() +def flutterProjectRoot = new File(scriptFile).parentFile.parentFile + +gradle.include ":flutter" +gradle.project(":flutter").projectDir = new File(flutterProjectRoot, ".android/Flutter") + +def localPropertiesFile = new File(flutterProjectRoot, ".android/local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists(), "❗️The Flutter module doesn't have a `$localPropertiesFile` file." + + "\nYou must run `flutter pub get` in `$flutterProjectRoot`." +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +gradle.apply from: "$flutterSdkPath/packages/flutter_tools/gradle/module_plugin_loader.gradle" diff --git a/flutter_module/.android/settings.gradle b/flutter_module/.android/settings.gradle new file mode 100644 index 0000000..b40f9f7 --- /dev/null +++ b/flutter_module/.android/settings.gradle @@ -0,0 +1,6 @@ +// Generated file. Do not edit. +include ':app' + +rootProject.name = 'android_generated' +setBinding(new Binding([gradle: this])) +evaluate(new File(settingsDir, 'include_flutter.groovy')) diff --git a/flutter_module/.android/src/main/AndroidManifest.xml b/flutter_module/.android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f2940c7 --- /dev/null +++ b/flutter_module/.android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/flutter_module/.gitignore b/flutter_module/.gitignore index 525e05c..73d931f 100644 --- a/flutter_module/.gitignore +++ b/flutter_module/.gitignore @@ -37,7 +37,6 @@ Icon? .tags* build/ -.android/ .ios/ .flutter-plugins .flutter-plugins-dependencies From 88544de818ed6ca88b1bd459124e6cba1d79b2b5 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Tue, 5 Nov 2024 18:20:45 +0100 Subject: [PATCH 20/23] fix: gitignore android --- flutter_module/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter_module/.gitignore b/flutter_module/.gitignore index 73d931f..525e05c 100644 --- a/flutter_module/.gitignore +++ b/flutter_module/.gitignore @@ -37,6 +37,7 @@ Icon? .tags* build/ +.android/ .ios/ .flutter-plugins .flutter-plugins-dependencies From 1a7a70baba3740d8d706e07fc0c88a07ffebc848 Mon Sep 17 00:00:00 2001 From: xJac0b Date: Tue, 5 Nov 2024 18:23:30 +0100 Subject: [PATCH 21/23] fix: rm .android --- flutter_module/.android/Flutter/build.gradle | 56 ------ .../Flutter/src/main/AndroidManifest.xml | 17 -- .../plugins/GeneratedPluginRegistrant.java | 19 --- flutter_module/.android/app/build.gradle | 44 ----- .../.android/app/src/main/AndroidManifest.xml | 51 ------ .../flutter_module/host/MainActivity.java | 6 - .../main/res/drawable/launch_background.xml | 12 -- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 0 bytes .../app/src/main/res/values/styles.xml | 8 - flutter_module/.android/build.gradle | 40 ----- flutter_module/.android/gradle.properties | 3 - .../gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - flutter_module/.android/gradlew | 160 ------------------ flutter_module/.android/gradlew.bat | 90 ---------- .../.android/include_flutter.groovy | 16 -- flutter_module/.android/settings.gradle | 6 - .../.android/src/main/AndroidManifest.xml | 3 - 18 files changed, 536 deletions(-) delete mode 100644 flutter_module/.android/Flutter/build.gradle delete mode 100644 flutter_module/.android/Flutter/src/main/AndroidManifest.xml delete mode 100644 flutter_module/.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java delete mode 100644 flutter_module/.android/app/build.gradle delete mode 100644 flutter_module/.android/app/src/main/AndroidManifest.xml delete mode 100644 flutter_module/.android/app/src/main/java/dev/snipme/flutter_module/host/MainActivity.java delete mode 100644 flutter_module/.android/app/src/main/res/drawable/launch_background.xml delete mode 100644 flutter_module/.android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 flutter_module/.android/app/src/main/res/values/styles.xml delete mode 100644 flutter_module/.android/build.gradle delete mode 100644 flutter_module/.android/gradle.properties delete mode 100755 flutter_module/.android/gradle/wrapper/gradle-wrapper.jar delete mode 100644 flutter_module/.android/gradle/wrapper/gradle-wrapper.properties delete mode 100755 flutter_module/.android/gradlew delete mode 100755 flutter_module/.android/gradlew.bat delete mode 100644 flutter_module/.android/include_flutter.groovy delete mode 100644 flutter_module/.android/settings.gradle delete mode 100644 flutter_module/.android/src/main/AndroidManifest.xml diff --git a/flutter_module/.android/Flutter/build.gradle b/flutter_module/.android/Flutter/build.gradle deleted file mode 100644 index becb6b9..0000000 --- a/flutter_module/.android/Flutter/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -// Generated file. Do not edit. - -def localProperties = new Properties() -def localPropertiesFile = new File(buildscript.sourceFile.parentFile.parentFile, "local.properties") -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader("UTF-8") { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty("flutter.sdk") -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty("flutter.versionCode") -if (flutterVersionCode == null) { - flutterVersionCode = "1" -} - -def flutterVersionName = localProperties.getProperty("flutter.versionName") -if (flutterVersionName == null) { - flutterVersionName = "1.0" -} - -apply plugin: "com.android.library" -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -group = "dev.snipme.flutter_module" -version = "1.0" - -android { - // Conditional for compatibility with AGP <4.2. - if (project.android.hasProperty("namespace")) { - namespace = "dev.snipme.flutter_module" - } - - compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - defaultConfig { - minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion - versionCode = flutterVersionCode.toInteger() - versionName = flutterVersionName - } -} - -flutter { - source = "../.." -} diff --git a/flutter_module/.android/Flutter/src/main/AndroidManifest.xml b/flutter_module/.android/Flutter/src/main/AndroidManifest.xml deleted file mode 100644 index 801919c..0000000 --- a/flutter_module/.android/Flutter/src/main/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/flutter_module/.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/flutter_module/.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java deleted file mode 100644 index 539ab02..0000000 --- a/flutter_module/.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.flutter.plugins; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import io.flutter.Log; - -import io.flutter.embedding.engine.FlutterEngine; - -/** - * Generated file. Do not edit. - * This file is generated by the Flutter tool based on the - * plugins that support the Android platform. - */ -@Keep -public final class GeneratedPluginRegistrant { - private static final String TAG = "GeneratedPluginRegistrant"; - public static void registerWith(@NonNull FlutterEngine flutterEngine) { - } -} diff --git a/flutter_module/.android/app/build.gradle b/flutter_module/.android/app/build.gradle deleted file mode 100644 index be23c63..0000000 --- a/flutter_module/.android/app/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -def flutterPluginVersion = "managed" - -apply plugin: "com.android.application" - -android { - // Conditional for compatibility with AGP <4.2. - if (project.android.hasProperty("namespace")) { - namespace = "dev.snipme.flutter_module.host" - } - - compileSdk = 34 - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - defaultConfig { - applicationId = "dev.snipme.flutter_module.host" - minSdk = 21 - targetSdk = 34 - versionCode = 1 - versionName = "1.0" - } - - buildTypes { - profile { - initWith debug - } - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.debug - } - } - -} -buildDir = new File(rootProject.projectDir, "../build/host") -dependencies { - implementation(project(":flutter")) - implementation(fileTree(dir: "libs", include: ["*.jar"])) - implementation("androidx.appcompat:appcompat:1.0.2") - implementation("androidx.constraintlayout:constraintlayout:1.1.3") -} diff --git a/flutter_module/.android/app/src/main/AndroidManifest.xml b/flutter_module/.android/app/src/main/AndroidManifest.xml deleted file mode 100644 index c30c61a..0000000 --- a/flutter_module/.android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flutter_module/.android/app/src/main/java/dev/snipme/flutter_module/host/MainActivity.java b/flutter_module/.android/app/src/main/java/dev/snipme/flutter_module/host/MainActivity.java deleted file mode 100644 index cc518bb..0000000 --- a/flutter_module/.android/app/src/main/java/dev/snipme/flutter_module/host/MainActivity.java +++ /dev/null @@ -1,6 +0,0 @@ -package dev.snipme.flutter_module.host; - -import io.flutter.embedding.android.FlutterActivity; - -public class MainActivity extends FlutterActivity { -} diff --git a/flutter_module/.android/app/src/main/res/drawable/launch_background.xml b/flutter_module/.android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f..0000000 --- a/flutter_module/.android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/flutter_module/.android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter_module/.android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/flutter_module/.android/app/src/main/res/values/styles.xml b/flutter_module/.android/app/src/main/res/values/styles.xml deleted file mode 100644 index 2676b0b..0000000 --- a/flutter_module/.android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/flutter_module/.android/build.gradle b/flutter_module/.android/build.gradle deleted file mode 100644 index ca43a8a..0000000 --- a/flutter_module/.android/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -// Generated file. Do not edit. - -buildscript { - ext.kotlin_version = "1.7.10" - repositories { - google() - mavenCentral() - } - - dependencies { - classpath("com.android.tools.build:gradle:7.3.0") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: "com.android.library" -apply plugin: "kotlin-android" - -android { - // Conditional for compatibility with AGP <4.2. - if (project.android.hasProperty("namespace")) { - namespace = "dev.snipme.flutter_module" - } - - compileSdk = 34 - defaultConfig { - minSdk = 21 - } -} - -dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version") -} diff --git a/flutter_module/.android/gradle.properties b/flutter_module/.android/gradle.properties deleted file mode 100644 index 3b5b324..0000000 --- a/flutter_module/.android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true -android.enableJetifier=true diff --git a/flutter_module/.android/gradle/wrapper/gradle-wrapper.jar b/flutter_module/.android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100755 index 13372aef5e24af05341d49695ee84e5f9b594659..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53636 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ⅅ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 H-+%o-(+5aQ diff --git a/flutter_module/.android/gradle/wrapper/gradle-wrapper.properties b/flutter_module/.android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 3c472b9..0000000 --- a/flutter_module/.android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/flutter_module/.android/gradlew b/flutter_module/.android/gradlew deleted file mode 100755 index 9d82f78..0000000 --- a/flutter_module/.android/gradlew +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/flutter_module/.android/gradlew.bat b/flutter_module/.android/gradlew.bat deleted file mode 100755 index 8a0b282..0000000 --- a/flutter_module/.android/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/flutter_module/.android/include_flutter.groovy b/flutter_module/.android/include_flutter.groovy deleted file mode 100644 index 6ceda7c..0000000 --- a/flutter_module/.android/include_flutter.groovy +++ /dev/null @@ -1,16 +0,0 @@ -def scriptFile = getClass().protectionDomain.codeSource.location.toURI() -def flutterProjectRoot = new File(scriptFile).parentFile.parentFile - -gradle.include ":flutter" -gradle.project(":flutter").projectDir = new File(flutterProjectRoot, ".android/Flutter") - -def localPropertiesFile = new File(flutterProjectRoot, ".android/local.properties") -def properties = new Properties() - -assert localPropertiesFile.exists(), "❗️The Flutter module doesn't have a `$localPropertiesFile` file." + - "\nYou must run `flutter pub get` in `$flutterProjectRoot`." -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } - -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -gradle.apply from: "$flutterSdkPath/packages/flutter_tools/gradle/module_plugin_loader.gradle" diff --git a/flutter_module/.android/settings.gradle b/flutter_module/.android/settings.gradle deleted file mode 100644 index b40f9f7..0000000 --- a/flutter_module/.android/settings.gradle +++ /dev/null @@ -1,6 +0,0 @@ -// Generated file. Do not edit. -include ':app' - -rootProject.name = 'android_generated' -setBinding(new Binding([gradle: this])) -evaluate(new File(settingsDir, 'include_flutter.groovy')) diff --git a/flutter_module/.android/src/main/AndroidManifest.xml b/flutter_module/.android/src/main/AndroidManifest.xml deleted file mode 100644 index f2940c7..0000000 --- a/flutter_module/.android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - From b33401fb50f4a61576a68a8a466f260f1ed13ce6 Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Wed, 6 Nov 2024 21:23:06 +0100 Subject: [PATCH 22/23] Setup fvm --- flutter_module/.fvm/flutter_sdk | 1 + flutter_module/.fvm/fvm_config.json | 4 ++++ flutter_module/pubspec.lock | 32 ++++++++++++++--------------- 3 files changed, 21 insertions(+), 16 deletions(-) create mode 120000 flutter_module/.fvm/flutter_sdk create mode 100644 flutter_module/.fvm/fvm_config.json diff --git a/flutter_module/.fvm/flutter_sdk b/flutter_module/.fvm/flutter_sdk new file mode 120000 index 0000000..b478f77 --- /dev/null +++ b/flutter_module/.fvm/flutter_sdk @@ -0,0 +1 @@ +/Users/tkadziolka/fvm/versions/3.24.0 \ No newline at end of file diff --git a/flutter_module/.fvm/fvm_config.json b/flutter_module/.fvm/fvm_config.json new file mode 100644 index 0000000..8142fd7 --- /dev/null +++ b/flutter_module/.fvm/fvm_config.json @@ -0,0 +1,4 @@ +{ + "flutterSdkVersion": "3.24.0", + "flavors": {} +} \ No newline at end of file diff --git a/flutter_module/pubspec.lock b/flutter_module/pubspec.lock index f896473..4e8fcf8 100644 --- a/flutter_module/pubspec.lock +++ b/flutter_module/pubspec.lock @@ -324,18 +324,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -380,18 +380,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -561,26 +561,26 @@ packages: dependency: "direct main" description: name: test - sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" url: "https://pub.dev" source: hosted - version: "1.25.2" + version: "1.25.7" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" test_core: dependency: transitive description: name: test_core - sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.4" timing: dependency: transitive description: @@ -617,10 +617,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.4" watcher: dependency: transitive description: From 44855d4711ebff148a6b7bcfbd43c0859bb154d4 Mon Sep 17 00:00:00 2001 From: tkadziolka Date: Wed, 6 Nov 2024 21:27:15 +0100 Subject: [PATCH 23/23] Disabled material3 --- flutter_module/lib/main.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flutter_module/lib/main.dart b/flutter_module/lib/main.dart index 2ccd9f3..0a72b44 100644 --- a/flutter_module/lib/main.dart +++ b/flutter_module/lib/main.dart @@ -50,7 +50,10 @@ class MyApp extends StatelessWidget { return MaterialApp.router( title: 'SnipMe', - theme: ThemeData(primarySwatch: Colors.blue), + theme: ThemeData( + primarySwatch: Colors.blue, + useMaterial3: false, + ), // TODO Use theme tailor routeInformationProvider: router.routeInformationProvider, routeInformationParser: router.routeInformationParser,