├── .circleci ├── autogenerated_header.yml ├── config.yml └── src │ ├── @common.yml │ ├── commands │ ├── check_circleci_config.yml │ ├── install_cocoapods.yml │ ├── install_fastlane.yml │ ├── install_flutter.yml │ ├── install_gradle_dependencies.yml │ ├── install_node_modules.yml │ ├── persist_environment_variables.yml │ ├── prepare_project.yml │ ├── prepare_workspace.yml │ └── restore_environment_variables.yml │ ├── jobs │ ├── build_android.yml │ ├── build_ios.yml │ ├── bump_version.yml │ ├── check.yml │ ├── deliver_android.yml │ ├── deliver_ios.yml │ ├── promote_android.yml │ └── promote_ios.yml │ └── workflows │ ├── beta_delivery.yml │ ├── commit.yml │ ├── commit_main.yml │ ├── production_delivery.yml │ └── promotion.yml ├── .env.dev ├── .env.prod ├── .env.staging ├── .fvmrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── task.md ├── PULL_REQUEST_TEMPLATE.md └── hooks │ └── pre-commit ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── gruene-app.iml ├── icon.png ├── misc.xml ├── modules.xml ├── runConfigurations │ ├── Update_translations.xml │ ├── development.xml │ ├── main_dart.xml │ └── production.xml └── vcs.xml ├── .metadata ├── .vscode └── settings.json ├── CODEOWNERS ├── LICENSE.txt ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── .idea │ └── gruene_app_android.iml ├── Gemfile ├── Gemfile.lock ├── app │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ ├── AndroidManifest.xml │ │ └── google-services.json │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── de │ │ │ │ └── gruene │ │ │ │ └── wkapp │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ ├── android12splash.png │ │ │ └── splash.png │ │ │ ├── drawable-mdpi │ │ │ ├── android12splash.png │ │ │ └── splash.png │ │ │ ├── drawable-night-hdpi │ │ │ └── android12splash.png │ │ │ ├── drawable-night-mdpi │ │ │ └── android12splash.png │ │ │ ├── drawable-night-xhdpi │ │ │ └── android12splash.png │ │ │ ├── drawable-night-xxhdpi │ │ │ └── android12splash.png │ │ │ ├── drawable-night-xxxhdpi │ │ │ └── android12splash.png │ │ │ ├── drawable-v21 │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ ├── android12splash.png │ │ │ └── splash.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── android12splash.png │ │ │ └── splash.png │ │ │ ├── drawable-xxxhdpi │ │ │ ├── android12splash.png │ │ │ └── splash.png │ │ │ ├── drawable │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-ldpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values-night-v31 │ │ │ └── styles.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ ├── values-v31 │ │ │ └── styles.xml │ │ │ └── values │ │ │ ├── ic_launcher_background.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── fastlane │ ├── Fastfile │ └── README.md ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── assets ├── app_icon │ └── app_profile.png ├── badges │ ├── badge_bronze.svg │ ├── badge_empty.svg │ ├── badge_gold.svg │ ├── badge_platinum.svg │ └── badge_silver.svg ├── fonts │ └── GrueneType.otf ├── graphics │ ├── login.png │ ├── mfa_ready.svg │ ├── mfa_verify.svg │ └── placeholders │ │ ├── placeholder_1.jpg │ │ ├── placeholder_2.jpg │ │ └── placeholder_3.jpg ├── icons │ ├── gruene.png │ ├── mfa.svg │ ├── pollion.png │ ├── sunflower.svg │ └── verdigado.png ├── maps │ └── gruene_map.json ├── splash │ ├── logo.png │ └── logo_android12.png └── symbols │ ├── add_marker.svg │ ├── doors │ └── door.png │ ├── flyer │ └── flyer.png │ └── posters │ ├── poster.png │ ├── poster_damaged.png │ ├── poster_missing.png │ ├── poster_removed.png │ ├── poster_to_be_moved.png │ └── svg_inverted │ ├── poster_damaged.svg │ ├── poster_missing.svg │ ├── poster_ok.svg │ ├── poster_removed.svg │ └── poster_tobemoved.svg ├── build.yaml ├── devtools_options.yaml ├── docs ├── cicd.md ├── conventions.md └── release-workflow.md ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Gemfile ├── Gemfile.lock ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-50x50@1x.png │ │ │ ├── Icon-App-50x50@2x.png │ │ │ ├── Icon-App-57x57@1x.png │ │ │ ├── Icon-App-57x57@2x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-72x72@1x.png │ │ │ ├── Icon-App-72x72@2x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ ├── LaunchBackground.imageset │ │ │ ├── Contents.json │ │ │ └── background.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── Runner-Bridging-Header.h │ ├── RunnerDebug.entitlements │ └── RunnerRelease.entitlements └── fastlane │ ├── Appfile │ ├── Deliverfile │ ├── Fastfile │ ├── Matchfile │ └── README.md ├── lib ├── app │ ├── auth │ │ ├── bloc │ │ │ └── auth_bloc.dart │ │ ├── repository │ │ │ └── auth_repository.dart │ │ └── utils │ │ │ └── auth_stream.dart │ ├── constants │ │ ├── bottom_navigation_items.dart │ │ ├── config.dart │ │ ├── routes.dart │ │ ├── secure_storage_keys.dart │ │ └── urls.dart │ ├── enums │ │ └── push_notification_topic_enum.dart │ ├── geocode │ │ └── nominatim.dart │ ├── models │ │ └── push_notification_settings_model.dart │ ├── router.dart │ ├── screens │ │ ├── error_screen.dart │ │ ├── future_loading_screen.dart │ │ └── tab_screen.dart │ ├── services │ │ ├── access_token_authenticator.dart │ │ ├── campaign_action_database.dart │ │ ├── converters.dart │ │ ├── converters │ │ │ ├── address_model_parsing.dart │ │ │ ├── campaign_action_parsing.dart │ │ │ ├── date_time_parsing.dart │ │ │ ├── door_create_model_parsing.dart │ │ │ ├── door_update_model_parsing.dart │ │ │ ├── flyer_create_model_parsing.dart │ │ │ ├── flyer_update_model_parsing.dart │ │ │ ├── focus_area_parsing.dart │ │ │ ├── lat_lng_parsing.dart │ │ │ ├── lat_lng_parsing_extended.dart │ │ │ ├── map_string_dynamic_converter.dart │ │ │ ├── place_parser.dart │ │ │ ├── poi_address_parsing.dart │ │ │ ├── poi_parsing.dart │ │ │ ├── poi_poster_status_parsing.dart │ │ │ ├── poi_service_type_parsing.dart │ │ │ ├── poi_type_parsing.dart │ │ │ ├── poster_create_model_parsing.dart │ │ │ ├── poster_detail_model_extension.dart │ │ │ ├── poster_status_parsing.dart │ │ │ ├── poster_update_model_parsing.dart │ │ │ ├── slider_range_parsing.dart │ │ │ └── string_extension.dart │ │ ├── enums.dart │ │ ├── gruene_api_campaigns_service.dart │ │ ├── gruene_api_campaigns_statistics_service.dart │ │ ├── gruene_api_core.dart │ │ ├── gruene_api_door_service.dart │ │ ├── gruene_api_flyer_service.dart │ │ ├── gruene_api_poster_service.dart │ │ ├── interceptors │ │ │ ├── auth_interceptor.dart │ │ │ ├── keep_alive_interceptor.dart │ │ │ └── user_agent_interceptor.dart │ │ ├── ip_service.dart │ │ ├── nominatim_service.dart │ │ ├── push_notification_listener.dart │ │ ├── push_notification_service.dart │ │ └── secure_storage_service.dart │ ├── theme │ │ └── theme.dart │ ├── utils │ │ ├── build_page_without_animation.dart │ │ ├── date.dart │ │ ├── debouncer.dart │ │ ├── divisions.dart │ │ ├── error_message.dart │ │ ├── format_date.dart │ │ ├── logger.dart │ │ ├── open_url.dart │ │ ├── show_snack_bar.dart │ │ └── utils.dart │ └── widgets │ │ ├── app_bar.dart │ │ ├── bottom_navigation.dart │ │ ├── bottom_sheet_handle.dart │ │ ├── clean_layout.dart │ │ ├── date_range_picker.dart │ │ ├── expansion_list_tile.dart │ │ ├── full_screen_dialog.dart │ │ ├── html.dart │ │ ├── icon.dart │ │ ├── persistent_bottom_sheet.dart │ │ ├── rounded_icon_button.dart │ │ ├── search_bar.dart │ │ ├── section_title.dart │ │ ├── selection_list_item.dart │ │ ├── selection_view.dart │ │ ├── tab_bar.dart │ │ ├── text_list_item.dart │ │ └── toggle_list_item.dart ├── features │ ├── campaigns │ │ ├── helper │ │ │ ├── app_settings.dart │ │ │ ├── campaign_action.dart │ │ │ ├── campaign_action_cache.dart │ │ │ ├── campaign_action_cache_timer.dart │ │ │ ├── campaign_constants.dart │ │ │ ├── campaign_session_settings.dart │ │ │ ├── enums.dart │ │ │ ├── file_cache_manager.dart │ │ │ ├── map_helper.dart │ │ │ ├── map_layer_manager.dart │ │ │ ├── marker_item_helper.dart │ │ │ ├── marker_item_manager.dart │ │ │ ├── media_helper.dart │ │ │ ├── picture_gallery_view.dart │ │ │ ├── poster_status.dart │ │ │ └── util.dart │ │ ├── location │ │ │ ├── determine_position.dart │ │ │ ├── dialogs.dart │ │ │ └── location_ffi.dart │ │ ├── models │ │ │ ├── bounding_box.dart │ │ │ ├── doors │ │ │ │ ├── door_create_model.dart │ │ │ │ ├── door_detail_model.dart │ │ │ │ └── door_update_model.dart │ │ │ ├── flyer │ │ │ │ ├── flyer_create_model.dart │ │ │ │ ├── flyer_detail_model.dart │ │ │ │ └── flyer_update_model.dart │ │ │ ├── map_layer_model.dart │ │ │ ├── marker_item_model.dart │ │ │ ├── posters │ │ │ │ ├── poster_create_model.dart │ │ │ │ ├── poster_detail_model.dart │ │ │ │ ├── poster_list_item_model.dart │ │ │ │ ├── poster_photo_model.dart │ │ │ │ └── poster_update_model.dart │ │ │ └── statistics │ │ │ │ ├── campaign_statistics_model.dart │ │ │ │ └── campaign_statistics_set.dart │ │ ├── screens │ │ │ ├── campaigns_screen.dart │ │ │ ├── door_edit.dart │ │ │ ├── doors_add_screen.dart │ │ │ ├── doors_detail.dart │ │ │ ├── doors_screen.dart │ │ │ ├── flyer_add_screen.dart │ │ │ ├── flyer_detail.dart │ │ │ ├── flyer_edit.dart │ │ │ ├── flyer_screen.dart │ │ │ ├── map_consumer.dart │ │ │ ├── mixins.dart │ │ │ ├── mixins │ │ │ │ ├── address_extension.dart │ │ │ │ ├── confirm_delete.dart │ │ │ │ ├── door_validator.dart │ │ │ │ ├── flyer_validator.dart │ │ │ │ ├── focus_area_info.dart │ │ │ │ └── search_mixin.dart │ │ │ ├── my_poster_list_screen.dart │ │ │ ├── poster_add_screen.dart │ │ │ ├── poster_detail.dart │ │ │ ├── poster_edit.dart │ │ │ ├── posters_screen.dart │ │ │ ├── statistics_screen.dart │ │ │ └── teams_screen.dart │ │ └── widgets │ │ │ ├── address_field_detail.dart │ │ │ ├── app_route.dart │ │ │ ├── attribution_dialog.dart │ │ │ ├── close_edit_widget.dart │ │ │ ├── close_save_widget.dart │ │ │ ├── content_page.dart │ │ │ ├── create_address_widget.dart │ │ │ ├── custom_app_bar.dart │ │ │ ├── custom_sliver_app_bar.dart │ │ │ ├── delete_and_save_widget.dart │ │ │ ├── enhanced_wheel_slider.dart │ │ │ ├── filter_chip_widget.dart │ │ │ ├── location_button.dart │ │ │ ├── map_container.dart │ │ │ ├── map_controller.dart │ │ │ ├── map_controller_simplified.dart │ │ │ ├── map_with_location.dart │ │ │ ├── multiline_text_input_field.dart │ │ │ ├── refresh_button.dart │ │ │ ├── save_cancel_on_create_widget.dart │ │ │ ├── sliver_content_page.dart │ │ │ ├── small_button_spinner.dart │ │ │ └── text_input_field.dart │ ├── login │ │ ├── screens │ │ │ └── login_screen.dart │ │ └── widgets │ │ │ ├── intro_slides.dart │ │ │ ├── support_button.dart │ │ │ └── welcome_view.dart │ ├── mfa │ │ ├── bloc │ │ │ ├── mfa_bloc.dart │ │ │ ├── mfa_event.dart │ │ │ └── mfa_state.dart │ │ ├── domain │ │ │ └── mfa_factory.dart │ │ ├── dtos │ │ │ └── login_attempt_dto.dart │ │ ├── screens │ │ │ ├── mfa_screen.dart │ │ │ ├── token_input_screen.dart │ │ │ └── token_scan_screen.dart │ │ ├── util │ │ │ └── setup_mfa.dart │ │ └── widgets │ │ │ ├── intro_view.dart │ │ │ ├── last_granted_login_attempt_widget.dart │ │ │ ├── no_login_attempt_widget.dart │ │ │ ├── ready_view.dart │ │ │ └── verify_view.dart │ ├── news │ │ ├── bloc │ │ │ ├── bookmark_bloc.dart │ │ │ ├── bookmark_event.dart │ │ │ └── bookmark_state.dart │ │ ├── domain │ │ │ ├── bookmark_service.dart │ │ │ └── news_api_service.dart │ │ ├── dtos │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── news_model.dart │ │ ├── repository │ │ │ └── news_repository.dart │ │ ├── screens │ │ │ ├── news_detail_screen.dart │ │ │ └── news_screen.dart │ │ ├── utils │ │ │ └── utils.dart │ │ └── widgets │ │ │ ├── bookmark_button.dart │ │ │ ├── news_card.dart │ │ │ ├── news_filter_dialog.dart │ │ │ ├── news_list.dart │ │ │ └── news_search_filter_bar.dart │ ├── profiles │ │ ├── domain │ │ │ └── profiles_api_service.dart │ │ ├── helper │ │ │ └── social_media_type_translation.dart │ │ ├── screens │ │ │ └── own_profile_screen.dart │ │ └── widgets │ │ │ ├── profile_base_data.dart │ │ │ ├── profile_box.dart │ │ │ ├── profile_box_item.dart │ │ │ ├── profile_card.dart │ │ │ └── profile_header.dart │ ├── settings │ │ ├── bloc │ │ │ └── push_notifications │ │ │ │ ├── push_notification_settings_bloc.dart │ │ │ │ ├── push_notification_settings_event.dart │ │ │ │ └── push_notification_settings_state.dart │ │ ├── screens │ │ │ ├── push_notifications_screen.dart │ │ │ ├── settings_screen.dart │ │ │ └── support_screen.dart │ │ └── widgets │ │ │ ├── settings_card.dart │ │ │ └── version_number.dart │ └── tools │ │ ├── domain │ │ └── tools_api_service.dart │ │ ├── screens │ │ └── tools_screen.dart │ │ └── widgets │ │ └── tools_list.dart ├── i18n │ ├── app_de.json │ └── app_en.json └── main.dart ├── package.json ├── packages └── keycloak_authenticator │ ├── .gitignore │ ├── .metadata │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── analysis_options.yaml │ ├── lib │ ├── api.dart │ └── src │ │ ├── authenticator.dart │ │ ├── authenticator_repository.dart │ │ ├── authenticator_service.dart │ │ ├── dtos │ │ ├── activation_token_dto.dart │ │ ├── authenticator_entry.dart │ │ ├── authenticator_info.dart │ │ └── challenge.dart │ │ ├── enums │ │ ├── device_os_enum.dart │ │ ├── enums.dart │ │ ├── key_algorithm_enum.dart │ │ └── signature_algorithm_enum.dart │ │ ├── exceptions │ │ └── keycloak_client_exception.dart │ │ ├── keycloak_authenticator.dart │ │ ├── keycloak_client.dart │ │ ├── storage │ │ ├── flutter_secure_storage_adapter.dart │ │ └── storage.dart │ │ └── utils │ │ ├── crypto_utils.dart │ │ └── device_utils.dart │ └── pubspec.yaml ├── pubspec.lock ├── pubspec.yaml ├── slang.yaml ├── swaggers └── gruene-api.yaml ├── tools ├── circleci-update-config ├── constants.ts ├── git-version.ts ├── github-authentication.ts ├── github-promote-release.ts ├── github-release-asset.ts ├── github-release.ts ├── next-version.ts ├── package-lock.json ├── package.json ├── trigger-pipeline.ts └── tsconfig.json └── version.yaml /.circleci/autogenerated_header.yml: -------------------------------------------------------------------------------- 1 | ### AUTO GENERATED. DO NOT MODIFY. ### 2 | # This file should be auto generated by the files in the src folder. 3 | # You can update it by running `./tools/circleci-update-config` from the project root. 4 | -------------------------------------------------------------------------------- /.circleci/src/@common.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | parameters: 4 | api_triggered: 5 | type: boolean 6 | description: Whether the pipeline was triggered through the CircleCi API (https://circleci.com/docs/api/v2/?shell#trigger-a-new-pipeline). 7 | default: false 8 | workflow_type: 9 | type: enum 10 | enum: 11 | [ 12 | beta_delivery, 13 | production_delivery, 14 | promotion, 15 | none, 16 | ] 17 | default: none 18 | -------------------------------------------------------------------------------- /.circleci/src/commands/check_circleci_config.yml: -------------------------------------------------------------------------------- 1 | description: This command builds the circle config from the files in src and validates that it is up-to-date and valid. 2 | steps: 3 | - run: 4 | name: Install CircleCI CLI 5 | command: curl -fLSs https://raw.githubusercontent.com/CircleCI-Public/circleci-cli/master/install.sh | sudo bash 6 | - run: 7 | name: Build circle config 8 | command: ./tools/circleci-update-config 9 | - run: # Taken from https://github.com/roopakv/orbs/blob/master/src/commands/fail_if_dirty.yml 10 | name: CircleCI config up to date 11 | # language=bash 12 | command: | 13 | FILES_MODIFIED="" 14 | setcommit () { 15 | FILES_MODIFIED=$(git status -s | grep -i -E '.*circleci/config.yml') 16 | } 17 | setcommit || true 18 | if [ -z "$FILES_MODIFIED" ] 19 | then 20 | echo "The CircleCI config is up to date." 21 | exit 0; 22 | else 23 | echo "The CircleCI config is not up to date. You can update it by running the `./tools/circleci-update-config` script." 24 | exit 1; 25 | fi 26 | - run: 27 | name: Validate circle config 28 | command: circleci config validate 29 | -------------------------------------------------------------------------------- /.circleci/src/commands/install_cocoapods.yml: -------------------------------------------------------------------------------- 1 | description: Restores and saves the cocoa pods cache. 2 | steps: 3 | - restore_cache: 4 | name: Restore CocoaPods Cache 5 | keys: 6 | - 1-cocoapods-{{ arch }}-{{ checksum "ios/Podfile.lock" }} 7 | - run: 8 | name: Install CocoaPods 9 | command: bundle exec pod install 10 | working_directory: ios 11 | - save_cache: 12 | name: Save CocoaPods Cache 13 | key: 1-cocoapods-{{ arch }}-{{ checksum "ios/Podfile.lock" }} 14 | paths: 15 | - ~/Library/Caches/CocoaPods/ 16 | -------------------------------------------------------------------------------- /.circleci/src/commands/install_fastlane.yml: -------------------------------------------------------------------------------- 1 | description: Restores and saves fastlane cache of the passed directory. 2 | parameters: 3 | directory: 4 | type: string 5 | steps: 6 | - restore_cache: 7 | name: Restore Ruby Cache 8 | keys: 9 | - v1-gems-{{ arch }}-{{ checksum "<< parameters.directory >>/Gemfile.lock" }} 10 | - v1-gems-{{ arch }}- 11 | - run: 12 | name: Configure Installation Directory 13 | command: bundle config set path 'vendor/bundle' 14 | working_directory: << parameters.directory >> 15 | - run: 16 | name: Bundle Install 17 | command: bundle check || bundle install 18 | working_directory: << parameters.directory >> 19 | - save_cache: 20 | name: Save Ruby Cache 21 | key: v1-gems-{{ arch }}-{{ checksum "<< parameters.directory >>/Gemfile.lock" }} 22 | paths: 23 | - << parameters.directory >>/vendor/bundle 24 | -------------------------------------------------------------------------------- /.circleci/src/commands/install_flutter.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | precache: 3 | type: enum 4 | default: none 5 | enum: [none, android, ios] 6 | steps: 7 | - run: 8 | name: Install FVM 9 | command: curl -fsSL https://raw.githubusercontent.com/leoafarias/fvm/refs/heads/main/scripts/install.sh | bash 10 | - run: 11 | name: FVM Install Script Checksum 12 | command: curl -sL https://raw.githubusercontent.com/leoafarias/fvm/refs/heads/main/scripts/install.sh | shasum -a 256 13 | - restore_cache: 14 | keys: 15 | - v1-fvm-{{ arch }}-{{ checksum ".fvmrc" }}-{{ checksum "pubspec.yaml" }}-{{ checksum "pubspec.lock" }} 16 | - v1-fvm-{{ arch }}- 17 | - run: 18 | name: Install Flutter 19 | command: fvm install 20 | - run: 21 | name: Configure Flutter 22 | command: fvm flutter config --no-analytics 23 | - run: 24 | name: Show Flutter version 25 | command: fvm flutter --version 26 | - run: 27 | name: Install Flutter Packages 28 | command: fvm flutter pub get --enforce-lockfile 29 | - unless: 30 | condition: 31 | equal: [<< parameters.precache >>, none] 32 | steps: 33 | - run: 34 | name: Precache Flutter Binary Artifacts 35 | command: fvm flutter precache --<< parameters.precache >> 36 | working_directory: << parameters.precache >> 37 | - save_cache: 38 | key: v1-fvm-{{ arch }}-{{ checksum ".fvmrc" }}-{{ checksum "pubspec.yaml" }}-{{ checksum "pubspec.lock" }} 39 | paths: 40 | - .fvm 41 | - ~/fvm/ 42 | -------------------------------------------------------------------------------- /.circleci/src/commands/install_gradle_dependencies.yml: -------------------------------------------------------------------------------- 1 | description: Restores and saves the gradle cache. 2 | steps: 3 | - restore_cache: 4 | keys: 5 | - v1-gradle-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}-{{ checksum "android/settings.gradle" }} 6 | - v1-gradle- 7 | - run: 8 | name: 'Download Gradle Dependencies' 9 | command: ./gradlew androidDependencies 10 | working_directory: android 11 | - save_cache: 12 | paths: 13 | - ~/.gradle 14 | key: v1-gradle-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}-{{ checksum "android/settings.gradle" }} 15 | -------------------------------------------------------------------------------- /.circleci/src/commands/install_node_modules.yml: -------------------------------------------------------------------------------- 1 | description: Restores and saves the node_modules directories of the tools directory. 2 | steps: 3 | - restore_cache: 4 | keys: 5 | - v1-node-modules-{{checksum "tools/package-lock.json" }} 6 | - v1-node-modules- 7 | - run: 8 | name: Install node dependencies for npm workspaces 9 | command: "[ ! -d node_modules ] && npm ci --ignore-scripts --loglevel warn --yes || echo package.json and package-lock.json unchanged. Using cache." 10 | working_directory: tools 11 | - save_cache: 12 | key: v1-node-modules-{{checksum "tools/package-lock.json" }} 13 | paths: 14 | - ~/tools/node_modules 15 | -------------------------------------------------------------------------------- /.circleci/src/commands/persist_environment_variables.yml: -------------------------------------------------------------------------------- 1 | description: Sets the environment variables specified in the file 'environment_variables'. Make sure the file is persisted and has been attached. 2 | steps: 3 | - run: 4 | name: List environment variables 5 | command: cat ${BASH_ENV} 6 | - run: 7 | name: Save environment variables to file 8 | command: cat ${BASH_ENV} >> environment_variables 9 | - persist_to_workspace: 10 | root: ./ 11 | paths: 12 | - environment_variables 13 | -------------------------------------------------------------------------------- /.circleci/src/commands/prepare_project.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - run: 3 | name: Generate Translations 4 | command: fvm dart run slang 5 | - run: 6 | name: Apply Production Environment 7 | command: cp .env.prod .env 8 | - run: 9 | name: Generate Swagger API 10 | command: fvm dart run build_runner build 11 | - run: 12 | name: Restore google-services.json 13 | command: | 14 | echo $GOOGLE_SERVICES_JSON | base64 -d > android/app/google-services.json 15 | - run: 16 | name: Restore GoogleService-Info.plist 17 | command: | 18 | echo $GOOGLE_SERVICE_INFO_PLIST | base64 -d > ios/Runner/GoogleService-Info.plist 19 | -------------------------------------------------------------------------------- /.circleci/src/commands/prepare_workspace.yml: -------------------------------------------------------------------------------- 1 | description: Attach the workspace at ~/attached_workspace and list its contents 2 | steps: 3 | - attach_workspace: 4 | at: ~/attached_workspace 5 | - run: 6 | name: Attached workspace contents 7 | command: ls -A ~/attached_workspace 8 | - run: 9 | name: Recursively list attached workspace contents 10 | command: ls -AR ~/attached_workspace 11 | -------------------------------------------------------------------------------- /.circleci/src/commands/restore_environment_variables.yml: -------------------------------------------------------------------------------- 1 | description: Sets the environment variables specified in the file 'environment_variables'. Make sure the file is persisted and has been attached. 2 | steps: 3 | - run: 4 | name: List environment variables 5 | command: cat ~/attached_workspace/environment_variables 6 | - run: 7 | name: Restore environment variables 8 | command: cat ~/attached_workspace/environment_variables >> ${BASH_ENV} 9 | -------------------------------------------------------------------------------- /.circleci/src/jobs/build_android.yml: -------------------------------------------------------------------------------- 1 | docker: 2 | - image: cimg/android:2024.01.1-node 3 | resource_class: large 4 | shell: /bin/bash -eo pipefail 5 | environment: 6 | GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m" -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2' 7 | FASTLANE_SKIP_UPDATE_CHECK: true 8 | steps: 9 | - checkout 10 | - prepare_workspace 11 | - restore_environment_variables 12 | - install_flutter: 13 | precache: android 14 | - prepare_project 15 | - install_fastlane: 16 | directory: android 17 | - install_gradle_dependencies 18 | - run: 19 | name: '[FL] Prepare Android Keystore' 20 | command: bundle exec fastlane android keystore 21 | working_directory: android 22 | - run: 23 | name: '[FL] Build' 24 | command: bundle exec fastlane android build version_name:${NEW_VERSION_NAME} version_code:${NEW_VERSION_CODE} 25 | working_directory: android 26 | - run: 27 | name: Move aab 28 | command: mv ~/project/build/app/outputs/bundle/release/app-release.aab app-release.aab 29 | - store_artifacts: 30 | path: app-release.aab 31 | - run: 32 | name: Move apk 33 | command: mv ~/project/build/app/outputs/apk/release/app-release.apk app-release.apk 34 | - store_artifacts: 35 | path: app-release.apk 36 | - persist_to_workspace: 37 | root: . 38 | paths: 39 | - app-release.aab 40 | - app-release.apk 41 | - persist_environment_variables 42 | -------------------------------------------------------------------------------- /.circleci/src/jobs/build_ios.yml: -------------------------------------------------------------------------------- 1 | macos: 2 | xcode: 16.1.0 3 | environment: 4 | FASTLANE_SKIP_UPDATE_CHECK: true 5 | steps: 6 | - checkout 7 | - prepare_workspace 8 | - run: 9 | name: Install rosetta 10 | command: softwareupdate --install-rosetta --agree-to-license 11 | - restore_environment_variables 12 | - install_flutter: 13 | precache: ios 14 | - prepare_project 15 | - install_fastlane: 16 | directory: ios 17 | - install_cocoapods 18 | - run: 19 | name: '[FL] Build' 20 | command: bundle exec fastlane ios build version_name:${NEW_VERSION_NAME} version_code:${NEW_VERSION_CODE} 21 | working_directory: ios 22 | - store_artifacts: 23 | path: ~/app-release.ipa 24 | destination: app-release.ipa 25 | - persist_to_workspace: 26 | root: ~/ 27 | paths: 28 | - app-release.ipa 29 | -------------------------------------------------------------------------------- /.circleci/src/jobs/bump_version.yml: -------------------------------------------------------------------------------- 1 | # First step of each workflow. Reads and bumps the current version code and name. For deliveries the bump is committed. 2 | parameters: 3 | prepare_delivery: 4 | description: Whether to prepare for a delivery. If true, the version bump is committed. 5 | type: boolean 6 | default: false 7 | docker: 8 | - image: cimg/node:20.13.1 9 | resource_class: small 10 | shell: /bin/bash -eo pipefail 11 | steps: 12 | - checkout 13 | - install_node_modules 14 | - run: 15 | name: Calculate next version name 16 | command: echo "export NEW_VERSION_NAME=$(yarn --silent next-version calc | jq .versionName)" >> ${BASH_ENV} 17 | working_directory: tools 18 | - run: 19 | name: Calculate next version code 20 | command: echo "export NEW_VERSION_CODE=$(yarn --silent next-version calc | jq .versionCode)" >> ${BASH_ENV} 21 | working_directory: tools 22 | - when: 23 | condition: << parameters.prepare_delivery >> 24 | steps: 25 | - run: 26 | name: Bump git version 27 | command: yarn git-version bump-to ${NEW_VERSION_NAME} ${NEW_VERSION_CODE} --github-private-key ${GITHUB_PRIVATE_KEY} --owner ${CIRCLE_PROJECT_USERNAME} --repo ${CIRCLE_PROJECT_REPONAME} --branch ${CIRCLE_BRANCH} 28 | working_directory: tools 29 | - persist_environment_variables 30 | -------------------------------------------------------------------------------- /.circleci/src/jobs/check.yml: -------------------------------------------------------------------------------- 1 | docker: 2 | - image: cimg/node:20.13.1 3 | resource_class: small 4 | shell: /bin/bash -eo pipefail 5 | steps: 6 | - checkout 7 | - check_circleci_config 8 | - install_flutter 9 | - run: 10 | name: Check Formatting 11 | command: fvm dart format -l 120 -o none --set-exit-if-changed . 12 | # Prepare project after formatting check to avoid errors due to autogenerated files 13 | - prepare_project 14 | - run: 15 | name: Check Analyzer and Linting 16 | command: fvm flutter analyze --fatal-infos --fatal-warnings 17 | # - run: 18 | # name: Tests 19 | # command: fvm flutter test 20 | -------------------------------------------------------------------------------- /.circleci/src/jobs/deliver_android.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | production_delivery: 3 | description: Whether to deliver the build to production. 4 | type: boolean 5 | docker: 6 | - image: cimg/android:2024.08.1-node 7 | resource_class: small 8 | shell: /bin/bash -eo pipefail 9 | environment: 10 | FASTLANE_SKIP_UPDATE_CHECK: true 11 | steps: 12 | - checkout 13 | - prepare_workspace 14 | - restore_environment_variables 15 | - install_node_modules 16 | - install_fastlane: 17 | directory: android 18 | - run: 19 | name: '[FL] Google PlayStore Upload' 20 | command: bundle exec fastlane android upload aab_path:attached_workspace/app-release.aab production_delivery:"<< parameters.production_delivery >>" version_name:${NEW_VERSION_NAME} version_code:${NEW_VERSION_CODE} 21 | working_directory: android 22 | - run: 23 | name: Create Github Release 24 | command: echo "export ANDROID_RELEASE_ID='$(yarn --silent github-release create android ${NEW_VERSION_NAME} ${NEW_VERSION_CODE} --production-delivery << parameters.production_delivery >> --github-private-key ${GITHUB_PRIVATE_KEY} --owner ${CIRCLE_PROJECT_USERNAME} --repo ${CIRCLE_PROJECT_REPONAME} --release-notes "Release v${NEW_VERSION_NAME}+${NEW_VERSION_CODE}")'" >> ${BASH_ENV} 25 | working_directory: tools 26 | - run: 27 | name: Add Builds to Github Release 28 | command: yarn github-release-asset upload android --releaseId ${ANDROID_RELEASE_ID} --files "$(ls ~/attached_workspace/*.{apk,aab})" --github-private-key ${GITHUB_PRIVATE_KEY} --owner ${CIRCLE_PROJECT_USERNAME} --repo ${CIRCLE_PROJECT_REPONAME} 29 | working_directory: tools 30 | -------------------------------------------------------------------------------- /.circleci/src/jobs/promote_android.yml: -------------------------------------------------------------------------------- 1 | docker: 2 | - image: cimg/android:2024.08.1-node 3 | resource_class: small 4 | shell: /bin/bash -eo pipefail 5 | environment: 6 | FASTLANE_SKIP_UPDATE_CHECK: true 7 | steps: 8 | - checkout 9 | - install_node_modules 10 | - install_fastlane: 11 | directory: android 12 | - run: 13 | name: '[FL] Google PlayStore Promotion' 14 | command: bundle exec fastlane android promote 15 | working_directory: android 16 | - run: 17 | name: Remove Beta Flag from Github Release 18 | command: yarn github-promote-release promote --platform android --github-private-key ${GITHUB_PRIVATE_KEY} --owner ${CIRCLE_PROJECT_USERNAME} --repo ${CIRCLE_PROJECT_REPONAME} 19 | working_directory: tools 20 | -------------------------------------------------------------------------------- /.circleci/src/jobs/promote_ios.yml: -------------------------------------------------------------------------------- 1 | macos: 2 | xcode: 16.1.0 3 | environment: 4 | FASTLANE_SKIP_UPDATE_CHECK: true 5 | steps: 6 | - checkout 7 | - install_node_modules 8 | - install_fastlane: 9 | directory: ios 10 | - run: 11 | name: '[FL] Apple AppStore Promotion' 12 | command: bundle exec fastlane ios promote 13 | working_directory: ios 14 | - run: 15 | name: Remove Beta Flag from Github Release 16 | command: yarn github-promote-release promote --platform ios --github-private-key ${GITHUB_PRIVATE_KEY} --owner ${CIRCLE_PROJECT_USERNAME} --repo ${CIRCLE_PROJECT_REPONAME} 17 | working_directory: tools 18 | -------------------------------------------------------------------------------- /.circleci/src/workflows/beta_delivery.yml: -------------------------------------------------------------------------------- 1 | when: 2 | and: 3 | - << pipeline.parameters.api_triggered >> 4 | - equal: [<< pipeline.parameters.workflow_type >>, beta_delivery] 5 | jobs: 6 | - bump_version: 7 | context: 8 | - github 9 | prepare_delivery: true 10 | 11 | - build_android: 12 | context: 13 | - app_signing_android 14 | requires: 15 | - bump_version 16 | - deliver_android: 17 | production_delivery: false 18 | context: 19 | - github 20 | - gruene_google_playstore 21 | requires: 22 | - build_android 23 | 24 | - build_ios: 25 | context: 26 | - app_signing_ios 27 | requires: 28 | - bump_version 29 | - deliver_ios: 30 | production_delivery: false 31 | context: 32 | - github 33 | - gruene_apple_appstore 34 | requires: 35 | - build_ios 36 | -------------------------------------------------------------------------------- /.circleci/src/workflows/commit.yml: -------------------------------------------------------------------------------- 1 | unless: 2 | or: 3 | - equal: [main, << pipeline.git.branch >>] 4 | - << pipeline.parameters.api_triggered >> 5 | jobs: 6 | - check 7 | - bump_version 8 | - build_android: 9 | context: 10 | - app_signing_android 11 | requires: 12 | - check 13 | - bump_version 14 | -------------------------------------------------------------------------------- /.circleci/src/workflows/commit_main.yml: -------------------------------------------------------------------------------- 1 | when: 2 | and: 3 | - equal: [main, << pipeline.git.branch >>] 4 | - not: << pipeline.parameters.api_triggered >> 5 | jobs: 6 | - bump_version 7 | - build_android: 8 | context: 9 | - app_signing_android 10 | requires: 11 | - bump_version 12 | -------------------------------------------------------------------------------- /.circleci/src/workflows/production_delivery.yml: -------------------------------------------------------------------------------- 1 | when: 2 | and: 3 | - << pipeline.parameters.api_triggered >> 4 | - equal: [<< pipeline.parameters.workflow_type >>, production_delivery] 5 | jobs: 6 | - bump_version: 7 | context: 8 | - github 9 | prepare_delivery: true 10 | 11 | - build_android: 12 | context: 13 | - app_signing_android 14 | requires: 15 | - bump_version 16 | - deliver_android: 17 | production_delivery: true 18 | context: 19 | - github 20 | - gruene_google_playstore 21 | requires: 22 | - build_android 23 | 24 | - build_ios: 25 | context: 26 | - app_signing_ios 27 | requires: 28 | - bump_version 29 | - deliver_ios: 30 | production_delivery: true 31 | context: 32 | - github 33 | - gruene_apple_appstore 34 | requires: 35 | - build_ios 36 | -------------------------------------------------------------------------------- /.circleci/src/workflows/promotion.yml: -------------------------------------------------------------------------------- 1 | when: 2 | and: 3 | - << pipeline.parameters.api_triggered >> 4 | - equal: [<< pipeline.parameters.workflow_type >>, promotion] 5 | jobs: 6 | - promote_android: 7 | context: 8 | - github 9 | - gruene_google_playstore 10 | 11 | - promote_ios: 12 | context: 13 | - github 14 | - gruene_apple_appstore 15 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | ENV=development 2 | GRUENE_API_URL=http://127.0.0.1:5000 3 | # GRUENE_API_ACCESS_TOKEN= 4 | 5 | OIDC_CLIENT_ID=gruene_app 6 | OIDC_ISSUER=http://127.0.0.1:8080/realms/dev 7 | 8 | MAP_MAPLIBRE_URL=assets/maps/gruene_map.json 9 | MAP_ADDRESSSEARCH_URL=https://maps.gruene.verdigado.net/nominatim 10 | 11 | IP_V4_SERVICE_URL=https://ipv4.api.gruene.verdigado.net/v1/client-info 12 | IP_V6_SERVICE_URL=https://ipv6.api.gruene.verdigado.net/v1/client-info 13 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | ENV=production 2 | GRUENE_API_URL=https://api.gruene.de 3 | 4 | OIDC_CLIENT_ID=gruene_app 5 | OIDC_ISSUER=https://saml.gruene.de/realms/gruenes-netz 6 | 7 | MAP_MAPLIBRE_URL=assets/maps/gruene_map.json 8 | MAP_ADDRESSSEARCH_URL=https://maps.gruene.verdigado.net/nominatim 9 | 10 | IP_V4_SERVICE_URL=https://ipv4.api.gruene.verdigado.net/v1/client-info 11 | IP_V6_SERVICE_URL=https://ipv6.api.gruene.verdigado.net/v1/client-info 12 | 13 | # as wished a hard cut off date until campaign layers are introduced 14 | POI_FILTER_DATE=24.02.2025 15 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | ENV=staging 2 | GRUENE_API_URL=https://api.gruene.de 3 | 4 | OIDC_CLIENT_ID=gruene_app 5 | OIDC_ISSUER=https://saml.gruene.de/realms/gruene-app-test 6 | 7 | MAP_MAPLIBRE_URL=https://maps.gruene.verdigado.net/styles/wkapp/style.json 8 | MAP_ADDRESSSEARCH_URL=https://nominatim.maps.tuerantuer.org/nominatim 9 | 10 | IP_V4_SERVICE_URL=https://ipv4.api.gruene.verdigado.net/v1/client-info 11 | IP_V6_SERVICE_URL=https://ipv6.api.gruene.verdigado.net/v1/client-info 12 | -------------------------------------------------------------------------------- /.fvmrc: -------------------------------------------------------------------------------- 1 | { 2 | "flutter": "3.24.3" 3 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | 11 | # specific for windows script files 12 | *.bat text eol=crlf 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug Report \U0001F41B" 3 | about: Report a bug to help us to fix and improve the app 4 | labels: Bug 5 | --- 6 | 7 | ### Describe the Bug 8 | 9 | 10 | 11 | ### Steps to Reproduce 12 | 13 | 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ### Expected Behavior 21 | 22 | 23 | 24 | ### Additional Information 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Task \U0001F527" 3 | about: Suggest an idea or enhancement for this project 4 | labels: Task 5 | --- 6 | 7 | ### Describe the Problem 8 | 9 | 10 | 11 | 12 | ### Describe Your Preferred Solution 13 | 14 | 15 | 16 | ### Describe Possible Alternatives 17 | 18 | 19 | 20 | ### Additional Information 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Short Description 2 | 3 | 4 | 5 | ### Proposed Changes 6 | 7 | 8 | 9 | - 10 | 11 | ### Side Effects 12 | 13 | 14 | 15 | - 16 | 17 | ### Testing 18 | 19 | 20 | 21 | ### Resolved Issues 22 | 23 | 24 | 25 | Fixes: # 26 | 27 | --- -------------------------------------------------------------------------------- /.github/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Install this pre-commit hook by running the following command in the project root: 4 | # ln -s ../../.github/hooks/pre-commit .git/hooks/pre-commit 5 | 6 | # Regenerate circleci config.yml if necessary 7 | CIRCLECI_CONFIG_FILES=$(git diff --cached --name-only .circleci/src) 8 | 9 | if [ ! -z "$CIRCLECI_CONFIG_FILES" ]; then 10 | # Abort if circleci command line tools are not installed 11 | if ! command -v circleci; then 12 | echo "You need to install circleci in order to commit and update the circleci config" 13 | exit 1 14 | fi 15 | 16 | echo "$CIRCLECI_CONFIG_FILES" | ./tools/circleci-update-config 17 | echo "$CIRCLECI_CONFIG_FILES" | xargs git add 18 | xargs git add .circleci/config.yml 19 | fi 20 | 21 | # Format Dart Files 22 | DART_FILES=$(git diff --cached --name-only --diff-filter=ACMR "*.dart" | sed 's| |\\ |g') 23 | 24 | if [ ! -z "$DART_FILES" ]; then 25 | if [ ! -x "$(command -v fvm)" ]; then 26 | echo "You need to install fvm in order to commit and format files" 27 | exit 1 28 | fi 29 | 30 | echo "$DART_FILES" | xargs fvm dart format -l 120 31 | echo "$DART_FILES" | xargs git add 32 | fi 33 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/gruene-app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/.idea/icon.png -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Update_translations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /.idea/runConfigurations/development.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/main_dart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/runConfigurations/production.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "2663184aa79047d0a33a14a3b607954f8fdd8730" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 17 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 18 | - platform: android 19 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 20 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 21 | - platform: ios 22 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 23 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "FSharp.suggestGitignore": false, 3 | "cSpell.words": [ 4 | "gruene", 5 | "housenumber" 6 | ], 7 | "files.eol": "\n", 8 | "circleci.filters.branchFilter": "allBranches", 9 | "circleci.persistedProjectSelection": [ 10 | "gh/verdigado/gruene-app" 11 | ], 12 | "cSpell.enabled": false, 13 | "dart.lineLength": 120, 14 | "[dart]": { 15 | "editor.formatOnSave": true, 16 | "editor.rulers": [120], 17 | } 18 | } -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | 3 | # These owners will be the default owners for everything in 4 | # the repo. Unless a later match takes precedence, 5 | # these owners will be requested for 6 | # review when someone opens a pull request. 7 | * @NikoHadouken @volkramweber @steffenkleinle @Stift 8 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - build/** 6 | - lib/swagger_generated_code/** 7 | - lib/**.g.dart 8 | errors: 9 | always_use_package_imports: error 10 | directives_ordering: error 11 | prefer_single_quotes: error 12 | require_trailing_commas: error 13 | language: 14 | strict-casts: true 15 | strict-raw-types: true 16 | strict-inference: true 17 | 18 | linter: 19 | rules: 20 | - always_use_package_imports 21 | - directives_ordering 22 | - prefer_single_quotes 23 | - require_trailing_commas 24 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # https://github.com/mogol/flutter_secure_storage/issues/748 2 | -dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue 3 | -dontwarn com.google.errorprone.annotations.CheckReturnValue 4 | -dontwarn com.google.errorprone.annotations.Immutable 5 | -dontwarn com.google.errorprone.annotations.RestrictedApi 6 | -dontwarn javax.annotation.Nullable 7 | -dontwarn javax.annotation.concurrent.GuardedBy 8 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/debug/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "938158619395", 4 | "project_id": "gruene-app-development", 5 | "storage_bucket": "gruene-app-development.firebasestorage.app" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:938158619395:android:f2cd74986c6b33f2f74736", 11 | "android_client_info": { 12 | "package_name": "de.gruene.wkapp.development" 13 | } 14 | }, 15 | "oauth_client": [], 16 | "api_key": [ 17 | { 18 | "current_key": "AIzaSyDZBMTb5J6o9-IdAJ3LjjaKkHy9n5_NrWs" 19 | } 20 | ], 21 | "services": { 22 | "appinvite_service": { 23 | "other_platform_oauth_client": [] 24 | } 25 | } 26 | } 27 | ], 28 | "configuration_version": "1" 29 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/de/gruene/wkapp/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package de.gruene.wkapp 2 | 3 | import io.flutter.embedding.android.FlutterFragmentActivity 4 | 5 | class MainActivity: FlutterFragmentActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-hdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-mdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-hdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-night-hdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-mdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-night-mdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-xhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-night-xhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-xxhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-v21/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-xhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-xxhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-xxxhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/drawable/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #04543b 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /android/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## Android 17 | 18 | ### android keystore 19 | 20 | ```sh 21 | [bundle exec] fastlane android keystore 22 | ``` 23 | 24 | Download and decrypt the JKS 25 | 26 | ### android build 27 | 28 | ```sh 29 | [bundle exec] fastlane android build 30 | ``` 31 | 32 | Create an android release build 33 | 34 | ### android upload 35 | 36 | ```sh 37 | [bundle exec] fastlane android upload 38 | ``` 39 | 40 | Deliver android app to beta or production 41 | 42 | ### android promote 43 | 44 | ```sh 45 | [bundle exec] fastlane android promote 46 | ``` 47 | 48 | Promote the android app from beta to production 49 | 50 | ---- 51 | 52 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 53 | 54 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 55 | 56 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 57 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.3.2" apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false 23 | id "com.google.gms.google-services" version "4.4.2" apply false 24 | } 25 | 26 | include ":app" 27 | -------------------------------------------------------------------------------- /assets/app_icon/app_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/app_icon/app_profile.png -------------------------------------------------------------------------------- /assets/badges/badge_empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/fonts/GrueneType.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/fonts/GrueneType.otf -------------------------------------------------------------------------------- /assets/graphics/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/graphics/login.png -------------------------------------------------------------------------------- /assets/graphics/placeholders/placeholder_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/graphics/placeholders/placeholder_1.jpg -------------------------------------------------------------------------------- /assets/graphics/placeholders/placeholder_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/graphics/placeholders/placeholder_2.jpg -------------------------------------------------------------------------------- /assets/graphics/placeholders/placeholder_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/graphics/placeholders/placeholder_3.jpg -------------------------------------------------------------------------------- /assets/icons/gruene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/icons/gruene.png -------------------------------------------------------------------------------- /assets/icons/pollion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/icons/pollion.png -------------------------------------------------------------------------------- /assets/icons/verdigado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/icons/verdigado.png -------------------------------------------------------------------------------- /assets/splash/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/splash/logo.png -------------------------------------------------------------------------------- /assets/splash/logo_android12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/splash/logo_android12.png -------------------------------------------------------------------------------- /assets/symbols/add_marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/symbols/doors/door.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/symbols/doors/door.png -------------------------------------------------------------------------------- /assets/symbols/flyer/flyer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/symbols/flyer/flyer.png -------------------------------------------------------------------------------- /assets/symbols/posters/poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/symbols/posters/poster.png -------------------------------------------------------------------------------- /assets/symbols/posters/poster_damaged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/symbols/posters/poster_damaged.png -------------------------------------------------------------------------------- /assets/symbols/posters/poster_missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/symbols/posters/poster_missing.png -------------------------------------------------------------------------------- /assets/symbols/posters/poster_removed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/symbols/posters/poster_removed.png -------------------------------------------------------------------------------- /assets/symbols/posters/poster_to_be_moved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/assets/symbols/posters/poster_to_be_moved.png -------------------------------------------------------------------------------- /assets/symbols/posters/svg_inverted/poster_damaged.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/symbols/posters/svg_inverted/poster_missing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/symbols/posters/svg_inverted/poster_ok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/symbols/posters/svg_inverted/poster_removed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/symbols/posters/svg_inverted/poster_tobemoved.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | sources: 4 | - swaggers/** 5 | - lib/$lib$ 6 | # - $package$ 7 | - lib/** 8 | - pubspec.yaml 9 | builders: 10 | chopper_generator: 11 | options: 12 | header: "// Generated code" 13 | swagger_dart_code_generator: 14 | options: 15 | input_folder: "swaggers/" 16 | output_folder: "lib/swagger_generated_code/" 17 | multipart_file_type: "MultipartFile" -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # https://github.com/fastlane/fastlane/issues/21794 4 | gem "rb-readline" 5 | gem "fastlane" 6 | gem "cocoapods" 7 | 8 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '13.0' 2 | 3 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 4 | 5 | project 'Runner', { 6 | 'Debug' => :debug, 7 | 'Release' => :release, 8 | } 9 | 10 | def flutter_root 11 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 12 | unless File.exist?(generated_xcode_build_settings_path) 13 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 14 | end 15 | 16 | File.foreach(generated_xcode_build_settings_path) do |line| 17 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 18 | return matches[1].strip if matches 19 | end 20 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 21 | end 22 | 23 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 24 | 25 | flutter_ios_podfile_setup 26 | 27 | target 'Runner' do 28 | use_frameworks! 29 | use_modular_headers! 30 | 31 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 32 | end 33 | 34 | post_install do |installer| 35 | installer.pods_project.targets.each do |target| 36 | flutter_additional_ios_build_settings(target) 37 | if target.name == "geolocator_apple" 38 | target.build_configurations.each do |config| 39 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)', 'BYPASS_PERMISSION_LOCATION_ALWAYS=1'] 40 | end 41 | end 42 | 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import FirebaseCore 4 | 5 | @main 6 | @objc class AppDelegate: FlutterAppDelegate { 7 | override func application( 8 | _ application: UIApplication, 9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 10 | ) -> Bool { 11 | FirebaseApp.configure() 12 | GeneratedPluginRegistrant.register(with: self) 13 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "LaunchImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "LaunchImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/Runner/RunnerDebug.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/RunnerRelease.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | apple_id "app@verdigado.com" 2 | team_id "BH3ML3K6G2" 3 | app_identifier "de.gruene.wkapp" 4 | -------------------------------------------------------------------------------- /ios/fastlane/Deliverfile: -------------------------------------------------------------------------------- 1 | languages(['de-DE']) 2 | 3 | release_notes({ 4 | 'default' => "Wir haben die App für Dich verbessert" 5 | }) 6 | -------------------------------------------------------------------------------- /ios/fastlane/Matchfile: -------------------------------------------------------------------------------- 1 | git_url "git@github.com:verdigado/app-signing.git" 2 | git_branch "main" 3 | storage_mode "git" 4 | 5 | team_id "BH3ML3K6G2" 6 | app_identifier "de.gruene.wkapp" 7 | username "app@verdigado.com" 8 | 9 | # The docs are available on https://docs.fastlane.tools/actions/match 10 | -------------------------------------------------------------------------------- /ios/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## iOS 17 | 18 | ### ios build 19 | 20 | ```sh 21 | [bundle exec] fastlane ios build 22 | ``` 23 | 24 | Create a release build 25 | 26 | ### ios upload_to_test_flight 27 | 28 | ```sh 29 | [bundle exec] fastlane ios upload_to_test_flight 30 | ``` 31 | 32 | Deliver iOS App to TestFlight 33 | 34 | ---- 35 | 36 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 37 | 38 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 39 | 40 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 41 | -------------------------------------------------------------------------------- /lib/app/auth/utils/auth_stream.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/auth/bloc/auth_bloc.dart'; 3 | 4 | class AuthStream extends ChangeNotifier { 5 | final AuthBloc authBloc; 6 | 7 | AuthStream(this.authBloc) { 8 | authBloc.authStateStream.listen((_) => notifyListeners()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/app/constants/bottom_navigation_items.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/constants/routes.dart'; 3 | import 'package:gruene_app/i18n/translations.g.dart'; 4 | 5 | class BottomNavigationItem { 6 | final String label; 7 | final String route; 8 | final IconData? icon; 9 | final String? assetIcon; 10 | 11 | BottomNavigationItem({ 12 | required this.label, 13 | required this.route, 14 | this.icon, 15 | this.assetIcon, 16 | }) : assert(icon != null || assetIcon != null); 17 | } 18 | 19 | final List bottomNavigationItems = [ 20 | BottomNavigationItem(label: t.news.label, route: Routes.news.path, icon: Icons.feed_outlined), 21 | BottomNavigationItem(label: t.campaigns.label, route: Routes.campaigns.path, icon: Icons.campaign_outlined), 22 | BottomNavigationItem(label: t.profiles.label, route: Routes.profiles.path, icon: Icons.group_outlined), 23 | BottomNavigationItem(label: t.mfa.label, route: Routes.mfa.path, assetIcon: 'assets/icons/mfa.svg'), 24 | BottomNavigationItem(label: t.tools.label, route: Routes.tools.path, icon: Icons.menu_outlined), 25 | ]; 26 | -------------------------------------------------------------------------------- /lib/app/constants/secure_storage_keys.dart: -------------------------------------------------------------------------------- 1 | class SecureStorageKeys { 2 | // auth 3 | static const String accessToken = 'access_token'; 4 | static const String idToken = 'id_token'; 5 | static const String refreshToken = 'refresh_token'; 6 | // push notifications 7 | static const String pushNotificationsEnabled = 'push_notifications.enabled'; 8 | static const String pushNotificationsTopicMap = 'push_notifications.topic_map'; 9 | static const String pushNotificationsFcmTopics = 'push_notifications.subscribed_fcm_topics'; 10 | 11 | static const String newsDivisionFilters = 'news.division_filters'; 12 | } 13 | -------------------------------------------------------------------------------- /lib/app/constants/urls.dart: -------------------------------------------------------------------------------- 1 | const legalNoticeUrl = 'https://www.gruene.de/service/impressum'; 2 | const dataProtectionStatementUrl = 'https://www.gruene.de/service/datenschutz'; 3 | const termsOfUseUrl = 'https://www.gruene.de/service/nutzungsbedingungen'; 4 | const supportUrl = 'https://www.gruene.de/unterstuetzen'; 5 | const mfaSettingsUrl = 'https://saml.gruene.de/realms/gruenes-netz/account/account-security/signing-in'; 6 | const mfaInformationUrl = 'https://gruenstreifen.netzbegruenung.de/anmelden-im-gruenen-netz/anleitung-2fa-via-app'; 7 | const grueneAppFeedbackUrl = 'https://gruenede.typeform.com/to/ADq4RxII'; 8 | const grueneAppArticleUrl = 9 | 'https://netz.gruene.de/de/wissenswerk/2025-01/b90die-gruenen-die-app-von-buendnis-90die-gruenen'; 10 | 11 | const grueneSupportMail = 'beteiligung@gruene.de'; 12 | const pollionSupportMail = 'support@pollion.com'; 13 | const verdigadoSupportMail = 'support@verdigado.com'; 14 | -------------------------------------------------------------------------------- /lib/app/enums/push_notification_topic_enum.dart: -------------------------------------------------------------------------------- 1 | enum PushNotificationTopic { 2 | newsBv('news.bv'), 3 | newsLv('news.lv'), 4 | newsKv('news.kv'); 5 | 6 | final String value; 7 | 8 | const PushNotificationTopic(this.value); 9 | } 10 | -------------------------------------------------------------------------------- /lib/app/models/push_notification_settings_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/enums/push_notification_topic_enum.dart'; 2 | 3 | class PushNotificationSettingsModel { 4 | /// Flag to toggle overall push notifications 5 | bool enabled; 6 | 7 | /// Map of topics and their enabled status 8 | Map topics; 9 | 10 | PushNotificationSettingsModel({ 11 | required this.enabled, 12 | required this.topics, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /lib/app/screens/error_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/theme/theme.dart'; 3 | import 'package:gruene_app/app/utils/error_message.dart'; 4 | import 'package:gruene_app/i18n/translations.g.dart'; 5 | 6 | class ErrorScreen extends StatelessWidget { 7 | final Object? error; 8 | final String? errorMessage; 9 | final T Function() retry; 10 | 11 | const ErrorScreen({super.key, required this.retry, this.error, this.errorMessage}) 12 | : assert((error == null) != (errorMessage == null) && error is! String); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Center( 17 | child: Column( 18 | mainAxisAlignment: MainAxisAlignment.center, 19 | children: [ 20 | Text( 21 | errorMessage ?? getErrorMessage(error!), 22 | textAlign: TextAlign.center, 23 | style: TextStyle(color: ThemeColors.textWarning), 24 | ), 25 | SizedBox(height: 16), 26 | ElevatedButton( 27 | onPressed: retry, 28 | child: Text(t.error.retry), 29 | ), 30 | ], 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/app/screens/tab_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/widgets/tab_bar.dart'; 3 | 4 | class TabScreen extends StatefulWidget { 5 | final PreferredSizeWidget Function(PreferredSizeWidget tabBar) appBarBuilder; 6 | final List tabs; 7 | final bool scrollableBody; 8 | 9 | const TabScreen({super.key, required this.tabs, required this.appBarBuilder, this.scrollableBody = true}); 10 | 11 | @override 12 | State createState() => _TabScreenState(); 13 | } 14 | 15 | class _TabScreenState extends State with SingleTickerProviderStateMixin { 16 | late TabController _tabController; 17 | 18 | @override 19 | void initState() { 20 | super.initState(); 21 | _tabController = TabController(length: widget.tabs.length, vsync: this); 22 | } 23 | 24 | @override 25 | void dispose() { 26 | super.dispose(); 27 | _tabController.dispose(); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Scaffold( 33 | appBar: widget.appBarBuilder( 34 | CustomTabBar( 35 | tabController: _tabController, 36 | tabs: widget.tabs, 37 | onTap: (index) => setState(() => _tabController.index = index), 38 | ), 39 | ), 40 | body: TabBarView( 41 | controller: _tabController, 42 | physics: widget.scrollableBody ? null : NeverScrollableScrollPhysics(), 43 | children: widget.tabs.map((tab) => tab.view).toList(), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/app/services/converters/address_model_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension AddressModelParsing on AddressModel { 4 | PoiAddress transformToPoiAddress() { 5 | final address = this; 6 | return PoiAddress( 7 | city: address.city.isEmpty ? null : address.city, 8 | zip: address.zipCode.isEmpty ? null : address.zipCode, 9 | street: address.street.isEmpty ? null : address.street, 10 | houseNumber: address.houseNumber.isEmpty ? null : address.houseNumber, 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/app/services/converters/date_time_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension DateTimeParsing on DateTime { 4 | String getAsLocalDateTimeString() { 5 | DateTime utcDateTime = this; 6 | DateTime localDateTime = utcDateTime.toLocal(); 7 | final dateString = DateFormat(t.campaigns.poster.date_format).format(localDateTime); 8 | final timeString = DateFormat(t.campaigns.poster.time_format).format(localDateTime); 9 | return t.campaigns.poster.datetime_display_template(date: dateString, time: timeString); 10 | } 11 | 12 | String getAsTimeStamp() { 13 | DateTime utcDateTime = this; 14 | DateTime localDateTime = utcDateTime.toLocal(); 15 | final timestampString = DateFormat(t.campaigns.poster.timestamp_format).format(localDateTime); 16 | return timestampString; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/app/services/converters/door_create_model_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension DoorCreateModelParsing on DoorCreateModel { 4 | DoorDetailModel transformToDoorDetailModel(String temporaryId) { 5 | return DoorDetailModel( 6 | id: temporaryId, 7 | address: address, 8 | closedDoors: closedDoors, 9 | openedDoors: openedDoors, 10 | location: location, 11 | createdAt: '${DateTime.now().getAsLocalDateTimeString()}*', // should mark this as preliminary 12 | isCached: true, 13 | ); 14 | } 15 | 16 | MarkerItemModel transformToVirtualMarkerItem(int temporaryId) { 17 | return MarkerItemModel.virtual( 18 | id: temporaryId, 19 | status: PoiServiceType.door.name, 20 | location: location, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/app/services/converters/door_update_model_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension DoorUpdateModelParsing on DoorUpdateModel { 4 | DoorDetailModel transformToDoorDetailModel() { 5 | var newDoorDetail = oldDoorDetail.copyWith( 6 | address: address, 7 | closedDoors: closedDoors, 8 | openedDoors: openedDoors, 9 | isCached: true, 10 | ); 11 | return newDoorDetail; 12 | } 13 | 14 | MarkerItemModel transformToVirtualMarkerItem() { 15 | return MarkerItemModel.virtual( 16 | id: int.parse(id), 17 | status: PoiServiceType.door.name, 18 | location: location, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/app/services/converters/flyer_create_model_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension FlyerCreateModelParsing on FlyerCreateModel { 4 | FlyerDetailModel transformToFlyerDetailModel(String temporaryId) { 5 | return FlyerDetailModel( 6 | id: temporaryId, 7 | address: address, 8 | flyerCount: flyerCount, 9 | location: location, 10 | createdAt: '${DateTime.now().getAsLocalDateTimeString()}*', // should mark this as preliminary 11 | isCached: true, 12 | ); 13 | } 14 | 15 | MarkerItemModel transformToVirtualMarkerItem(int temporaryId) { 16 | return MarkerItemModel.virtual( 17 | id: temporaryId, 18 | status: PoiServiceType.flyer.name, 19 | location: location, 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/app/services/converters/flyer_update_model_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension FlyerUpdateModelParsing on FlyerUpdateModel { 4 | FlyerDetailModel transformToFlyerDetailModel() { 5 | var newFlyerDetail = oldFlyerDetail.copyWith( 6 | address: address, 7 | flyerCount: flyerCount, 8 | isCached: true, 9 | ); 10 | return newFlyerDetail; 11 | } 12 | 13 | MarkerItemModel transformToVirtualMarkerItem() { 14 | return MarkerItemModel.virtual( 15 | id: int.parse(id), 16 | status: PoiServiceType.flyer.name, 17 | location: location, 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/app/services/converters/focus_area_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension FocusAreaParsing on FocusArea { 4 | MapLayerModel transformToMapLayer() { 5 | toPosition(List? point) => turf.Position( 6 | point![0]!, 7 | point[1]!, 8 | ); 9 | toPositionList(List?> points) => points.map(toPosition).toList(); 10 | 11 | var coordList = polygon.coordinates.map(toPositionList).toList(); 12 | return MapLayerModel(id: id, coords: coordList, score: score, description: description); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/app/services/converters/lat_lng_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension LatLngParsing on LatLng { 4 | String toLngLatString() { 5 | return transformToGeoJsonCoords().join(','); 6 | } 7 | 8 | List transformToGeoJsonCoords() { 9 | return [longitude, latitude]; 10 | } 11 | 12 | List transformToGeoJsonBBox(LatLng northEast) { 13 | final southWest = this; 14 | final coords = southWest.transformToGeoJsonCoords(); 15 | coords.addAll(northEast.transformToGeoJsonCoords()); 16 | return coords; 17 | } 18 | 19 | String transformToGeoJsonBBoxString(LatLng northEast) { 20 | return transformToGeoJsonBBox(northEast).join(','); 21 | } 22 | 23 | num getDistance(LatLng other) { 24 | return turf.distance( 25 | asPoint(), 26 | other.asPoint(), 27 | turf.Unit.kilometers, 28 | ); 29 | } 30 | 31 | turf.Point asPoint() { 32 | return turf.Point(coordinates: turf.Position(longitude, latitude)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/app/services/converters/lat_lng_parsing_extended.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension LatLngParsingExtended on List { 4 | LatLng transformToLatLng() { 5 | if (length != 2) throw ArgumentError('coordinates should contain 2 items'); 6 | return LatLng(this[1], this[0]); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/app/services/converters/map_string_dynamic_converter.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension MapStringDynamicConverter on Map { 4 | Map convertLatLongField({String fieldName = 'location'}) { 5 | if (containsKey(fieldName) && this[fieldName] is List) { 6 | this[fieldName] = (this[fieldName] as List).cast().toList(); 7 | } 8 | return this; 9 | } 10 | 11 | Map updateIdField(int id) { 12 | this['id'] = id.toString(); 13 | return this; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/app/services/converters/poi_address_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension PoiAddressParsing on PoiAddress { 4 | AddressModel transformToAddressModel() { 5 | final address = this; 6 | return AddressModel( 7 | street: address.street ?? '', 8 | houseNumber: address.houseNumber ?? '', 9 | zipCode: address.zip ?? '', 10 | city: address.city ?? '', 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/app/services/converters/poi_poster_status_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension PoiPosterStatusParsing on PoiPosterStatus { 4 | PosterStatus transformToModelPosterStatus() { 5 | return switch (this) { 6 | PoiPosterStatus.ok => PosterStatus.ok, 7 | PoiPosterStatus.damaged => PosterStatus.damaged, 8 | PoiPosterStatus.missing => PosterStatus.missing, 9 | PoiPosterStatus.removed => PosterStatus.removed, 10 | PoiPosterStatus.toBeMoved => PosterStatus.toBeMoved, 11 | PoiPosterStatus.swaggerGeneratedUnknown => throw UnimplementedError(), 12 | }; 13 | } 14 | 15 | String translatePosterStatus() { 16 | return switch (this) { 17 | PoiPosterStatus.ok => '', 18 | PoiPosterStatus.damaged => t.campaigns.poster.status.damaged.label, 19 | PoiPosterStatus.removed => t.campaigns.poster.status.removed.label, 20 | PoiPosterStatus.missing => t.campaigns.poster.status.missing.label, 21 | PoiPosterStatus.toBeMoved => t.campaigns.poster.status.to_be_moved.label, 22 | PoiPosterStatus.swaggerGeneratedUnknown => throw UnimplementedError(), 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/app/services/converters/poi_type_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension PoiTypeParsing on PoiType { 4 | PoiServiceType transformToPoiServiceType() { 5 | switch (this) { 6 | case PoiType.flyerSpot: 7 | return PoiServiceType.flyer; 8 | case PoiType.poster: 9 | return PoiServiceType.poster; 10 | case PoiType.house: 11 | return PoiServiceType.door; 12 | case PoiType.swaggerGeneratedUnknown: 13 | throw UnimplementedError(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/app/services/converters/poster_create_model_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension PosterCreateModelParsing on PosterCreateModel { 4 | MarkerItemModel transformToVirtualMarkerItem(int temporaryId) { 5 | return MarkerItemModel.virtual( 6 | id: temporaryId, 7 | status: PoiServiceType.poster.getAsMarkerItemStatus(PosterStatus.ok), 8 | location: location, 9 | ); 10 | } 11 | 12 | PosterDetailModel transformToPosterDetailModel(String temporaryId) { 13 | return PosterDetailModel( 14 | id: temporaryId, 15 | status: PosterStatus.ok, 16 | address: address, 17 | photos: imageFileLocation == null 18 | ? [] 19 | : [ 20 | PosterPhotoModel( 21 | id: DateTime.now().millisecondsSinceEpoch.toString(), 22 | imageUrl: imageFileLocation!, 23 | thumbnailUrl: imageFileLocation!, 24 | createdAt: DateTime.now(), 25 | ), 26 | ], 27 | location: location, 28 | comment: '', 29 | createdAt: '${DateTime.now().getAsLocalDateTimeString()}*', // should mark this as preliminary 30 | isCached: true, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/app/services/converters/poster_detail_model_extension.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension PosterDetailModelExtension on PosterDetailModel { 4 | PosterPhotoModel? latestPhoto() { 5 | if (photos.isEmpty) return null; 6 | photos.sortByIdDescending(); 7 | return photos.first; 8 | } 9 | 10 | PosterUpdateModel asPosterUpdate() { 11 | return PosterUpdateModel( 12 | id: id, 13 | address: address, 14 | status: status, 15 | comment: comment, 16 | location: location, 17 | oldPosterDetail: this, 18 | deletedPhotoIds: [], 19 | newPhotos: [], 20 | ); 21 | } 22 | } 23 | 24 | extension PosterPhotoModelListExtension on List { 25 | void sortByIdDescending() { 26 | sort((a, b) => int.parse(b.id).compareTo(int.parse(a.id))); // reverse sorting 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/app/services/converters/poster_status_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension PosterStatusParsing on PosterStatus { 4 | PoiPosterStatus transformToPoiPosterStatus() { 5 | return switch (this) { 6 | PosterStatus.ok => PoiPosterStatus.ok, 7 | PosterStatus.damaged => PoiPosterStatus.damaged, 8 | PosterStatus.missing => PoiPosterStatus.missing, 9 | PosterStatus.removed => PoiPosterStatus.removed, 10 | PosterStatus.toBeMoved => PoiPosterStatus.toBeMoved, 11 | }; 12 | } 13 | 14 | String translatePosterStatus() => transformToPoiPosterStatus().translatePosterStatus(); 15 | } 16 | -------------------------------------------------------------------------------- /lib/app/services/converters/slider_range_parsing.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension SliderRangeParsing on SliderInputRange { 4 | InputFieldType getInputFieldType() { 5 | return switch (this) { 6 | SliderInputRange.numbers0To99 => InputFieldType.numbers0To99, 7 | SliderInputRange.numbers0To999 => InputFieldType.numbers0To999, 8 | SliderInputRange.numbers1To999 => InputFieldType.numbers1To999, 9 | }; 10 | } 11 | 12 | int getMinValue() { 13 | return switch (this) { 14 | SliderInputRange.numbers0To99 => 0, 15 | SliderInputRange.numbers0To999 => 0, 16 | SliderInputRange.numbers1To999 => 1, 17 | }; 18 | } 19 | 20 | int getMaxValue() { 21 | return switch (this) { 22 | SliderInputRange.numbers0To99 => 99, 23 | SliderInputRange.numbers0To999 => 999, 24 | SliderInputRange.numbers1To999 => 999, 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/app/services/converters/string_extension.dart: -------------------------------------------------------------------------------- 1 | part of '../converters.dart'; 2 | 3 | extension StringExtension on String { 4 | String appendIfNotEmpty(String text) { 5 | if (text.trim().isEmpty) return this; 6 | return this + text; 7 | } 8 | 9 | String appendLineIfNotEmpty(String text) { 10 | if (text.trim().isEmpty) return this; 11 | return '$this\n$text'; 12 | } 13 | 14 | bool isNetworkImageUrl() => Uri.parse(this).hasScheme; 15 | } 16 | -------------------------------------------------------------------------------- /lib/app/services/enums.dart: -------------------------------------------------------------------------------- 1 | enum PoiServiceType { poster, door, flyer } 2 | -------------------------------------------------------------------------------- /lib/app/services/gruene_api_campaigns_statistics_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics_model.dart'; 3 | import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics_set.dart'; 4 | import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart'; 5 | 6 | class GrueneApiCampaignsStatisticsService { 7 | late GrueneApi grueneApi; 8 | 9 | GrueneApiCampaignsStatisticsService() { 10 | grueneApi = GetIt.I(); 11 | } 12 | 13 | Future getStatistics() async { 14 | var statResult = await grueneApi.v1CampaignsStatisticsGet(); 15 | return statResult.body!.asCampaignStatistics(); 16 | } 17 | } 18 | 19 | extension StatisticsParser on CampaignStatistics { 20 | CampaignStatisticsModel asCampaignStatistics() { 21 | return CampaignStatisticsModel( 22 | flyerStats: flyer.asStatisticsSet(), 23 | houseStats: house.asStatisticsSet(), 24 | posterStats: poster.asStatisticsSet(), 25 | ); 26 | } 27 | } 28 | 29 | extension PoiStatisticsParser on PoiStatistics { 30 | CampaignStatisticsSet asStatisticsSet() { 31 | return CampaignStatisticsSet( 32 | own: own, 33 | division: division, 34 | state: state, 35 | germany: germany, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/app/services/interceptors/auth_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:chopper/chopper.dart'; 5 | import 'package:gruene_app/app/auth/repository/auth_repository.dart'; 6 | 7 | const bearerPrefix = 'Bearer'; 8 | 9 | class AuthInterceptor implements Interceptor { 10 | final AuthRepository _authRepository = AuthRepository(); 11 | final String? _accessToken; 12 | 13 | AuthInterceptor(this._accessToken); 14 | 15 | @override 16 | FutureOr> intercept(Chain chain) async { 17 | final accessToken = _accessToken ?? await _authRepository.getAccessToken(); 18 | final updatedRequest = applyHeader( 19 | chain.request, 20 | HttpHeaders.authorizationHeader, 21 | '$bearerPrefix ${accessToken!}', 22 | override: false, 23 | ); 24 | 25 | return chain.proceed(updatedRequest); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/app/services/interceptors/keep_alive_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:chopper/chopper.dart'; 5 | 6 | class KeepAliveInterceptor implements Interceptor { 7 | @override 8 | FutureOr> intercept(Chain chain) { 9 | final updatedRequest = applyHeader( 10 | chain.request, 11 | HttpHeaders.connectionHeader, 12 | 'keep-alive', 13 | override: false, 14 | ); 15 | 16 | return chain.proceed(updatedRequest); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/app/services/interceptors/user_agent_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:chopper/chopper.dart' as chopper; 5 | import 'package:package_info_plus/package_info_plus.dart'; 6 | 7 | String _getSystemDescription() { 8 | if (Platform.isAndroid) { 9 | return 'Android'; 10 | } else if (Platform.isIOS) { 11 | return 'iOS'; 12 | } else { 13 | return 'unknown_OS'; 14 | } 15 | } 16 | 17 | Future _getUserAgentString() async { 18 | final packageInfo = await PackageInfo.fromPlatform(); 19 | return '${packageInfo.packageName} ${packageInfo.version}+${packageInfo.buildNumber} (${_getSystemDescription()}, ${packageInfo.installerStore})'; 20 | } 21 | 22 | class UserAgentInterceptor implements chopper.Interceptor { 23 | final String? userAgent; 24 | 25 | UserAgentInterceptor([this.userAgent]); 26 | 27 | @override 28 | FutureOr> intercept(chopper.Chain chain) async { 29 | final userAgentHeaderValue = userAgent ?? await _getUserAgentString(); 30 | final updatedRequest = chopper.applyHeader( 31 | chain.request, 32 | HttpHeaders.userAgentHeader, 33 | userAgentHeaderValue, 34 | override: true, 35 | ); 36 | 37 | return chain.proceed(updatedRequest); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/app/services/ip_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:gruene_app/app/constants/config.dart'; 4 | import 'package:http/http.dart' as http; 5 | 6 | class IpService { 7 | Future isOwnIp(String ip) async { 8 | final isInputIpV6 = isIpV6(ip); 9 | final publicIp = await getPublicIp(useIpV6: isInputIpV6); 10 | return ip == publicIp; 11 | } 12 | 13 | Future getPublicIp({bool useIpV6 = false}) async { 14 | try { 15 | final url = useIpV6 ? Config.ipV6ServiceUrl : Config.ipV4ServiceUrl; 16 | 17 | final response = await http.get( 18 | Uri.parse(url), 19 | headers: {'Content-Type': 'application/json'}, 20 | ); 21 | 22 | if (response.statusCode == 200) { 23 | final Map jsonResponse = json.decode(response.body) as Map; 24 | return jsonResponse['clientIp'] as String?; 25 | } 26 | 27 | return null; 28 | } catch (e) { 29 | return null; 30 | } 31 | } 32 | 33 | bool isIpV6(String ip) { 34 | try { 35 | final address = InternetAddress(ip); 36 | return address.type == InternetAddressType.IPv6; 37 | } catch (e) { 38 | return false; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/app/services/secure_storage_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 2 | import 'package:get_it/get_it.dart'; 3 | 4 | final getIt = GetIt.instance; 5 | 6 | void registerSecureStorage() { 7 | getIt.registerSingleton( 8 | const FlutterSecureStorage( 9 | aOptions: AndroidOptions( 10 | encryptedSharedPreferences: true, 11 | ), 12 | ), 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /lib/app/utils/build_page_without_animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | 4 | CustomTransitionPage buildPageWithoutAnimation({ 5 | required BuildContext context, 6 | required GoRouterState state, 7 | required Widget child, 8 | }) { 9 | return CustomTransitionPage( 10 | key: state.pageKey, 11 | child: child, 12 | transitionsBuilder: (context, animation, secondaryAnimation, child) => child, 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /lib/app/utils/date.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:intl/intl.dart'; 4 | 5 | final dateFormatter = DateFormat.yMd(Platform.localeName); 6 | -------------------------------------------------------------------------------- /lib/app/utils/debouncer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class Debouncer { 5 | final int milliseconds; 6 | Timer? _timer; 7 | 8 | Debouncer({this.milliseconds = 250}); 9 | 10 | void run(VoidCallback action) { 11 | if (_timer?.isActive ?? false) { 12 | _timer?.cancel(); 13 | } 14 | _timer = Timer(Duration(milliseconds: milliseconds), action); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/app/utils/divisions.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart'; 2 | 3 | extension DivisionExtension on Division { 4 | String shortDisplayName() => level == DivisionLevel.bv ? name2 : '${level.value} $name2'; 5 | } 6 | 7 | extension DivisionFilter on Iterable { 8 | List filterByLevel(DivisionLevel level) { 9 | final filtered = where((division) => division.level == level).toList(); 10 | filtered.sort((a, b) => a.name2.compareTo(b.name2)); 11 | return filtered; 12 | } 13 | 14 | Division bundesverband() => firstWhere((it) => it.divisionKey == '10000000'); 15 | } 16 | -------------------------------------------------------------------------------- /lib/app/utils/error_message.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/i18n/translations.g.dart'; 2 | 3 | String getErrorMessage(Object error, {String? defaultMessage}) { 4 | if (error.toString().contains('Failed host lookup')) { 5 | return t.error.offlineError; 6 | } 7 | return defaultMessage ?? t.error.unknownError; 8 | } 9 | -------------------------------------------------------------------------------- /lib/app/utils/format_date.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | const dateFormat = 'dd.MM.yyyy'; 4 | 5 | String formatDate(DateTime date) { 6 | return DateFormat(dateFormat).format(date); 7 | } 8 | -------------------------------------------------------------------------------- /lib/app/utils/logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | 3 | final logger = Logger(); 4 | -------------------------------------------------------------------------------- /lib/app/utils/open_url.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 3 | import 'package:gruene_app/app/utils/logger.dart'; 4 | import 'package:url_launcher/url_launcher.dart'; 5 | 6 | Future _openInAppBrowser(String url, BuildContext context) async { 7 | final browser = ChromeSafariBrowser(); 8 | await browser.open( 9 | url: WebUri(url), 10 | settings: ChromeSafariBrowserSettings(toolbarBackgroundColor: Theme.of(context).primaryColor), 11 | ); 12 | } 13 | 14 | Future openUrl(String url, BuildContext context) async { 15 | final parsedUrl = Uri.parse(url); 16 | if (!parsedUrl.hasScheme) { 17 | logger.w('Unable to open $url'); 18 | return; 19 | } 20 | 21 | if (['https', 'http'].contains(parsedUrl.scheme)) { 22 | await _openInAppBrowser(url, context); 23 | return; 24 | } 25 | 26 | final canOpenUrl = await canLaunchUrl(parsedUrl); 27 | if (canOpenUrl) { 28 | await launchUrl(parsedUrl); 29 | } else { 30 | logger.w('Unable to open $url'); 31 | } 32 | } 33 | 34 | Future openMail(String mail, BuildContext context) async { 35 | await openUrl('mailto:$mail', context); 36 | } 37 | -------------------------------------------------------------------------------- /lib/app/utils/show_snack_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void showSnackBar(BuildContext context, String text) { 4 | ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text))); 5 | } 6 | -------------------------------------------------------------------------------- /lib/app/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | 4 | extension IterableX on Iterable { 5 | T? firstWhereOrNull(bool Function(T) test) { 6 | for (var item in this) { 7 | if (test(item)) { 8 | return item; 9 | } 10 | } 11 | return null; 12 | } 13 | } 14 | 15 | extension IsBetween on DateTime { 16 | bool isBetween(DateTimeRange dateRange) { 17 | final safeEndDate = dateRange.end.copyWith(day: dateRange.end.day + 1); 18 | return !dateRange.start.isAfter(this) && safeEndDate.isAfter(this); 19 | } 20 | } 21 | 22 | extension ContainsAny on List { 23 | bool containsAny(List other) { 24 | return any((element) => other.contains(element)); 25 | } 26 | } 27 | 28 | extension WithDividers on Iterable { 29 | List withDividers([Widget? divider]) => expand((item) => [item, Divider()]).toList()..removeLast(); 30 | } 31 | 32 | extension PushNested on BuildContext { 33 | void pushNested(String nestedSlug, {Object? extra}) { 34 | final currentPath = GoRouterState.of(this).fullPath; 35 | push('$currentPath/$nestedSlug', extra: extra); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/app/widgets/bottom_sheet_handle.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/theme/theme.dart'; 3 | 4 | class BottomSheetHandle extends StatelessWidget { 5 | const BottomSheetHandle({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Center( 10 | child: Container( 11 | width: 48, 12 | height: 6, 13 | decoration: BoxDecoration( 14 | color: ThemeColors.textLight, 15 | borderRadius: BorderRadius.all(Radius.circular(18)), 16 | ), 17 | ), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/app/widgets/clean_layout.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CleanLayout extends StatelessWidget { 4 | final Widget? child; 5 | final bool showAppBar; 6 | 7 | const CleanLayout({super.key, this.child, this.showAppBar = true}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final theme = Theme.of(context); 12 | return Scaffold( 13 | appBar: AppBar(backgroundColor: theme.colorScheme.surface, toolbarHeight: showAppBar ? null : 0), 14 | body: SafeArea(child: Container(color: theme.colorScheme.surface, child: child)), 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/app/widgets/expansion_list_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/theme/theme.dart'; 3 | import 'package:gruene_app/app/utils/utils.dart'; 4 | 5 | class ExpansionListTile extends StatelessWidget { 6 | final List children; 7 | final String title; 8 | final EdgeInsetsGeometry titlePadding; 9 | final Color? backgroundColor; 10 | 11 | const ExpansionListTile({ 12 | super.key, 13 | required this.children, 14 | required this.title, 15 | this.titlePadding = const EdgeInsets.symmetric(horizontal: 24), 16 | this.backgroundColor, 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final theme = Theme.of(context); 22 | return Container( 23 | color: theme.colorScheme.surface, 24 | child: ExpansionTile( 25 | shape: const Border(), 26 | title: Text(title), 27 | tilePadding: titlePadding, 28 | backgroundColor: backgroundColor ?? theme.colorScheme.surface, 29 | collapsedBackgroundColor: backgroundColor ?? theme.colorScheme.surface, 30 | iconColor: ThemeColors.textDisabled, 31 | collapsedIconColor: ThemeColors.textDisabled, 32 | children: children.withDividers(), 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/app/widgets/full_screen_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FullScreenDialog extends StatelessWidget { 4 | final Widget? child; 5 | final List? appBarActions; 6 | 7 | const FullScreenDialog({super.key, this.child, this.appBarActions}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final theme = Theme.of(context); 12 | return Scaffold( 13 | backgroundColor: theme.colorScheme.surfaceDim, 14 | appBar: AppBar( 15 | backgroundColor: theme.colorScheme.surfaceDim, 16 | surfaceTintColor: theme.colorScheme.surfaceDim, 17 | leading: IconButton(icon: Icon(Icons.close), onPressed: Navigator.of(context).pop), 18 | actions: appBarActions, 19 | ), 20 | body: child, 21 | ); 22 | } 23 | } 24 | 25 | void showFullScreenDialog(BuildContext context, WidgetBuilder builder) { 26 | Navigator.of(context).push(MaterialPageRoute(fullscreenDialog: true, builder: builder)); 27 | } 28 | -------------------------------------------------------------------------------- /lib/app/widgets/icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_svg/flutter_svg.dart'; 3 | 4 | class CustomIcon extends StatelessWidget { 5 | final String path; 6 | final Color color; 7 | final double width; 8 | final double height; 9 | 10 | const CustomIcon({super.key, required this.path, required this.color, this.width = 24, this.height = 24}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return SvgPicture.asset( 15 | path, 16 | width: width, 17 | height: height, 18 | colorFilter: ColorFilter.mode(color, BlendMode.srcIn), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/app/widgets/rounded_icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RoundedIconButton extends StatelessWidget { 4 | final void Function() onPressed; 5 | final IconData icon; 6 | final Color iconColor; 7 | final Color backgroundColor; 8 | final bool selected; 9 | final double width; 10 | final double height; 11 | 12 | const RoundedIconButton({ 13 | super.key, 14 | required this.onPressed, 15 | required this.icon, 16 | required this.iconColor, 17 | required this.backgroundColor, 18 | this.selected = false, 19 | this.width = 48, 20 | this.height = 48, 21 | }); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Container( 26 | decoration: ShapeDecoration( 27 | shape: RoundedRectangleBorder( 28 | borderRadius: BorderRadius.circular(10), 29 | side: BorderSide(color: selected ? backgroundColor : iconColor, width: 1), 30 | ), 31 | ), 32 | child: Container( 33 | width: width, 34 | height: height, 35 | decoration: ShapeDecoration( 36 | color: selected ? iconColor : backgroundColor, 37 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), 38 | ), 39 | child: IconButton( 40 | icon: Icon(icon, color: selected ? backgroundColor : iconColor), 41 | onPressed: onPressed, 42 | style: IconButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), 43 | ), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/app/widgets/section_title.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SectionTitle extends StatelessWidget { 4 | final String title; 5 | 6 | const SectionTitle({super.key, required this.title}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | final theme = Theme.of(context); 11 | return Container( 12 | padding: const EdgeInsets.only(bottom: 6, left: 24, right: 24, top: 24), 13 | color: theme.colorScheme.surfaceDim, 14 | child: Text(title, style: theme.textTheme.titleMedium), 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/app/widgets/selection_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/theme/theme.dart'; 3 | import 'package:gruene_app/app/widgets/text_list_item.dart'; 4 | 5 | class SelectionListItem extends StatelessWidget { 6 | final void Function() toggleSelection; 7 | final String title; 8 | final bool selected; 9 | 10 | const SelectionListItem({super.key, required this.title, required this.toggleSelection, required this.selected}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final theme = Theme.of(context); 15 | return TextListItem( 16 | onPress: toggleSelection, 17 | title: title, 18 | trailing: Container( 19 | width: 24, 20 | height: 24, 21 | decoration: BoxDecoration( 22 | shape: BoxShape.circle, 23 | color: selected ? theme.colorScheme.secondary : theme.colorScheme.surface, 24 | border: Border.all(width: 2, color: selected ? theme.colorScheme.secondary : ThemeColors.textDisabled), 25 | ), 26 | child: Checkbox( 27 | value: selected, 28 | checkColor: theme.colorScheme.surface, 29 | activeColor: theme.colorScheme.secondary, 30 | onChanged: (_) => toggleSelection(), 31 | shape: CircleBorder(), 32 | side: BorderSide.none, 33 | ), 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/app/widgets/text_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/theme/theme.dart'; 3 | import 'package:gruene_app/i18n/translations.g.dart'; 4 | 5 | class TextListItem extends StatelessWidget { 6 | final void Function() onPress; 7 | final String title; 8 | final bool isExternal; 9 | final bool isImplemented; 10 | final Widget? trailing; 11 | 12 | const TextListItem({ 13 | super.key, 14 | required this.onPress, 15 | required this.title, 16 | this.isExternal = false, 17 | this.isImplemented = true, 18 | this.trailing, 19 | }); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final theme = Theme.of(context); 24 | return Container( 25 | margin: const EdgeInsets.only(bottom: 1), 26 | child: ListTile( 27 | enabled: isImplemented, 28 | onTap: onPress, 29 | title: Text( 30 | isImplemented ? title : '$title ${t.settings.notImplemented}', 31 | style: theme.textTheme.bodyLarge?.apply(color: isImplemented ? ThemeColors.text : ThemeColors.textDisabled), 32 | ), 33 | trailing: SizedBox( 34 | height: 24, 35 | width: 24, 36 | child: trailing ?? 37 | Icon( 38 | isExternal ? Icons.open_in_browser_outlined : Icons.chevron_right_outlined, 39 | color: theme.disabledColor, 40 | ), 41 | ), 42 | tileColor: theme.colorScheme.surface, 43 | contentPadding: const EdgeInsets.symmetric(horizontal: 24), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/app/widgets/toggle_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/theme/theme.dart'; 3 | 4 | class ToggleListItem extends StatelessWidget { 5 | final String title; 6 | final bool value; 7 | final ValueChanged? onChanged; 8 | 9 | const ToggleListItem({ 10 | super.key, 11 | required this.title, 12 | required this.value, 13 | required this.onChanged, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final theme = Theme.of(context); 19 | 20 | return Container( 21 | color: theme.colorScheme.surface, 22 | margin: const EdgeInsets.only(bottom: 1), 23 | child: ListTile( 24 | contentPadding: const EdgeInsets.symmetric(horizontal: 24), 25 | title: Text( 26 | title, 27 | style: theme.textTheme.bodyLarge?.apply(color: ThemeColors.text), 28 | ), 29 | trailing: Switch( 30 | value: value, 31 | onChanged: onChanged, 32 | activeColor: Colors.white, 33 | activeTrackColor: onChanged != null ? theme.colorScheme.primary : ThemeColors.textDisabled, 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/features/campaigns/helper/app_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/features/campaigns/helper/campaign_session_settings.dart'; 2 | 3 | class AppSettings { 4 | var campaign = CampaignSessionSettings(); 5 | } 6 | -------------------------------------------------------------------------------- /lib/features/campaigns/helper/campaign_action_cache_timer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:get_it/get_it.dart'; 3 | import 'package:gruene_app/app/auth/repository/auth_repository.dart'; 4 | import 'package:gruene_app/features/campaigns/helper/campaign_action_cache.dart'; 5 | 6 | class CampaignActionCacheTimer { 7 | late Timer timer; 8 | 9 | CampaignActionCacheTimer() { 10 | timer = Timer.periodic(Duration(minutes: 5), (timer) => _flushData()); 11 | 12 | // initial flush 13 | Future.delayed(Duration(seconds: 5), () => Timer.run(_flushData)); 14 | } 15 | 16 | void _flushData() async { 17 | var authRepo = AuthRepository(); 18 | if (await authRepo.getAccessToken() != null) { 19 | GetIt.I().flushCache(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/features/campaigns/helper/campaign_session_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/services/nominatim_service.dart'; 2 | import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics_model.dart'; 3 | import 'package:maplibre_gl/maplibre_gl.dart'; 4 | 5 | class CampaignSessionSettings { 6 | LatLng? lastPosition; 7 | double? lastZoomLevel; 8 | 9 | CampaignStatisticsModel? recentStatistics; 10 | DateTime? recentStatisticsFetchTimestamp; 11 | 12 | bool imageConsentConfirmed = false; 13 | 14 | String? searchString; 15 | List? searchResult = []; 16 | } 17 | -------------------------------------------------------------------------------- /lib/features/campaigns/helper/enums.dart: -------------------------------------------------------------------------------- 1 | enum ModalEditResult { cancel, save, delete } 2 | 3 | enum ModalDetailResult { close, edit } 4 | 5 | enum ImageType { jpeg, png } 6 | -------------------------------------------------------------------------------- /lib/features/campaigns/helper/map_layer_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/features/campaigns/models/map_layer_model.dart'; 2 | 3 | class MapLayerDataManager { 4 | final Map> loadedLayers = {}; 5 | 6 | void addLayerData(String sourceId, List layerData) { 7 | if (!loadedLayers.keys.contains(sourceId)) { 8 | loadedLayers.putIfAbsent(sourceId, () => []); 9 | } 10 | final currentLayerData = loadedLayers[sourceId]!; 11 | // Currently disabled until a proper manager is enabled 12 | // layerData.retainWhere( 13 | // (newLayerItem) => !currentLayerData.any((currentLayerItem) => currentLayerItem.id == newLayerItem.id), 14 | // ); 15 | currentLayerData.clear(); 16 | currentLayerData.addAll(layerData); 17 | } 18 | 19 | List getMapLayerData(String sourceId) { 20 | return loadedLayers[sourceId]!; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/features/campaigns/helper/marker_item_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/features/campaigns/models/marker_item_model.dart'; 2 | 3 | class MarkerItemManager { 4 | List loadedMarkers = []; 5 | final List virtualMarkers = []; 6 | 7 | void addMarkers(List poiList) { 8 | // get virtual marker items and add them to cache list 9 | var newVirtualMarkers = poiList.where((p) => p.isVirtual).toList(); 10 | virtualMarkers.retainWhere((oldMarker) => !newVirtualMarkers.any((newMarker) => newMarker.id == oldMarker.id)); 11 | virtualMarkers.addAll(newVirtualMarkers); 12 | 13 | // get marker items which are not in cache 14 | var newStoredMarkers = poiList 15 | .where((p) => !p.isVirtual) 16 | .where((p) => !virtualMarkers.any((virtualMarker) => virtualMarker.id == p.id)) 17 | .toList(); 18 | 19 | // remove previously loaded markers to update them 20 | loadedMarkers.retainWhere((oldMarker) => !newStoredMarkers.any((newMarker) => newMarker.id == oldMarker.id)); 21 | 22 | // remove loaded markers which are also in cache 23 | loadedMarkers.removeWhere((oldMarker) => virtualMarkers.any((cachedMarker) => cachedMarker.id == oldMarker.id)); 24 | 25 | loadedMarkers.addAll(newStoredMarkers); 26 | } 27 | 28 | List getMarkers() { 29 | return loadedMarkers + virtualMarkers; 30 | } 31 | 32 | void removeMarker(int markerItemId) { 33 | loadedMarkers.retainWhere((item) => item.id == null || item.id != markerItemId); 34 | } 35 | 36 | void resetAllMarkers() { 37 | loadedMarkers.clear(); 38 | virtualMarkers.clear(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/features/campaigns/helper/poster_status.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/features/campaigns/models/posters/poster_detail_model.dart'; 2 | import 'package:gruene_app/i18n/translations.g.dart'; 3 | 4 | class PosterStatusHelper { 5 | static List<(PosterStatus, String)> getPosterStatusList = <(PosterStatus status, String label)>[ 6 | ( 7 | PosterStatus.ok, 8 | t.campaigns.poster.status.ok.label, 9 | ), 10 | ( 11 | PosterStatus.damaged, 12 | t.campaigns.poster.status.damaged.label, 13 | ), 14 | ( 15 | PosterStatus.missing, 16 | t.campaigns.poster.status.missing.label, 17 | ), 18 | ( 19 | PosterStatus.toBeMoved, 20 | t.campaigns.poster.status.to_be_moved.label, 21 | ), 22 | ( 23 | PosterStatus.removed, 24 | t.campaigns.poster.status.removed.label, 25 | ), 26 | ]; 27 | } 28 | -------------------------------------------------------------------------------- /lib/features/campaigns/helper/util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:maplibre_gl/maplibre_gl.dart'; 3 | 4 | /// Adds an asset image to the currently displayed style 5 | Future addImageFromAsset(MapLibreMapController controller, String name, String assetName) async { 6 | final bytes = await rootBundle.load(assetName); 7 | final list = bytes.buffer.asUint8List(); 8 | return controller.addImage(name, list); 9 | } 10 | -------------------------------------------------------------------------------- /lib/features/campaigns/location/location_ffi.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:gruene_app/app/constants/config.dart'; 3 | 4 | final platform = MethodChannel('${Config.appId}/location'); 5 | 6 | // Checks whether location services are enabled 7 | // without using Google Play Services 8 | Future isNonGoogleLocationServiceEnabled() async { 9 | try { 10 | final bool result = await platform.invokeMethod('isLocationServiceEnabled') as bool; 11 | return result; 12 | } on PlatformException catch (e) { 13 | throw 'Failed to get state of location services: "${e.message}".'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/features/campaigns/models/bounding_box.dart: -------------------------------------------------------------------------------- 1 | import 'package:maplibre_gl/maplibre_gl.dart'; 2 | 3 | class BoundingBox { 4 | final LatLng southwest; 5 | 6 | final LatLng northeast; 7 | 8 | const BoundingBox({required this.southwest, required this.northeast}); 9 | } 10 | -------------------------------------------------------------------------------- /lib/features/campaigns/models/doors/door_create_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/services/nominatim_service.dart'; 2 | import 'package:gruene_app/features/campaigns/models/posters/poster_create_model.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | import 'package:maplibre_gl/maplibre_gl.dart'; 5 | 6 | part 'door_create_model.g.dart'; 7 | 8 | @JsonSerializable() 9 | class DoorCreateModel { 10 | @LatLongConverter() 11 | final LatLng location; 12 | final AddressModel address; 13 | final int openedDoors; 14 | final int closedDoors; 15 | 16 | DoorCreateModel({ 17 | required this.location, 18 | required this.address, 19 | required this.openedDoors, 20 | required this.closedDoors, 21 | }); 22 | 23 | factory DoorCreateModel.fromJson(Map json) => _$DoorCreateModelFromJson(json); 24 | 25 | Map toJson() => _$DoorCreateModelToJson(this); 26 | } 27 | -------------------------------------------------------------------------------- /lib/features/campaigns/models/doors/door_update_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/services/nominatim_service.dart'; 2 | import 'package:gruene_app/features/campaigns/models/doors/door_detail_model.dart'; 3 | import 'package:gruene_app/features/campaigns/models/posters/poster_create_model.dart'; 4 | import 'package:json_annotation/json_annotation.dart'; 5 | import 'package:maplibre_gl/maplibre_gl.dart'; 6 | 7 | part 'door_update_model.g.dart'; 8 | 9 | @JsonSerializable() 10 | class DoorUpdateModel { 11 | final int openedDoors; 12 | final int closedDoors; 13 | final String id; 14 | final AddressModel address; 15 | @LatLongConverter() 16 | final LatLng location; 17 | final DoorDetailModel oldDoorDetail; 18 | 19 | DoorUpdateModel({ 20 | required this.id, 21 | required this.address, 22 | required this.openedDoors, 23 | required this.closedDoors, 24 | required this.oldDoorDetail, 25 | required this.location, 26 | }); 27 | 28 | factory DoorUpdateModel.fromJson(Map json) => _$DoorUpdateModelFromJson(json); 29 | 30 | Map toJson() => _$DoorUpdateModelToJson(this); 31 | } 32 | -------------------------------------------------------------------------------- /lib/features/campaigns/models/flyer/flyer_create_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/services/nominatim_service.dart'; 2 | import 'package:gruene_app/features/campaigns/models/posters/poster_create_model.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | import 'package:maplibre_gl/maplibre_gl.dart'; 5 | 6 | part 'flyer_create_model.g.dart'; 7 | 8 | @JsonSerializable() 9 | class FlyerCreateModel { 10 | @LatLongConverter() 11 | final LatLng location; 12 | 13 | final AddressModel address; 14 | 15 | final int flyerCount; 16 | 17 | FlyerCreateModel({required this.location, required this.address, required this.flyerCount}); 18 | 19 | factory FlyerCreateModel.fromJson(Map json) => _$FlyerCreateModelFromJson(json); 20 | 21 | Map toJson() => _$FlyerCreateModelToJson(this); 22 | } 23 | -------------------------------------------------------------------------------- /lib/features/campaigns/models/flyer/flyer_update_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/services/nominatim_service.dart'; 2 | import 'package:gruene_app/features/campaigns/models/flyer/flyer_detail_model.dart'; 3 | import 'package:gruene_app/features/campaigns/models/posters/poster_create_model.dart'; 4 | import 'package:json_annotation/json_annotation.dart'; 5 | import 'package:maplibre_gl/maplibre_gl.dart'; 6 | 7 | part 'flyer_update_model.g.dart'; 8 | 9 | @JsonSerializable() 10 | class FlyerUpdateModel { 11 | final String id; 12 | final AddressModel address; 13 | final int flyerCount; 14 | @LatLongConverter() 15 | final LatLng location; 16 | final FlyerDetailModel oldFlyerDetail; 17 | 18 | FlyerUpdateModel({ 19 | required this.id, 20 | required this.address, 21 | required this.flyerCount, 22 | required this.location, 23 | required this.oldFlyerDetail, 24 | }); 25 | 26 | factory FlyerUpdateModel.fromJson(Map json) => _$FlyerUpdateModelFromJson(json); 27 | 28 | Map toJson() => _$FlyerUpdateModelToJson(this); 29 | } 30 | -------------------------------------------------------------------------------- /lib/features/campaigns/models/map_layer_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:turf/transform.dart'; 2 | 3 | class MapLayerModel { 4 | final String id; 5 | final List> coords; 6 | final double score; 7 | final String? description; 8 | 9 | const MapLayerModel({ 10 | required this.id, 11 | required this.coords, 12 | required this.score, 13 | required this.description, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /lib/features/campaigns/models/marker_item_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:maplibre_gl/maplibre_gl.dart'; 2 | 3 | class MarkerItemModel { 4 | final LatLng location; 5 | final int? id; 6 | final String? status; 7 | final bool isVirtual; 8 | 9 | const MarkerItemModel({ 10 | required this.id, 11 | required this.status, 12 | required this.location, 13 | }) : isVirtual = false; 14 | 15 | MarkerItemModel.virtual({ 16 | required this.id, 17 | this.status, 18 | required this.location, 19 | }) : isVirtual = true; 20 | } 21 | -------------------------------------------------------------------------------- /lib/features/campaigns/models/posters/poster_list_item_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/services/nominatim_service.dart'; 2 | 3 | class PosterListItemModel { 4 | final String id; 5 | final String? thumbnailUrl; 6 | final String? imageUrl; 7 | final AddressModel address; 8 | final String status; 9 | final String lastChangeStatus; 10 | final String lastChangeDateTime; 11 | final DateTime createdAt; 12 | final bool isCached; 13 | 14 | const PosterListItemModel({ 15 | required this.id, 16 | required this.thumbnailUrl, 17 | required this.imageUrl, 18 | required this.address, 19 | required this.status, 20 | required this.lastChangeStatus, 21 | required this.lastChangeDateTime, 22 | required this.createdAt, 23 | this.isCached = false, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /lib/features/campaigns/models/posters/poster_photo_model.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'poster_photo_model.g.dart'; 5 | 6 | @JsonSerializable() 7 | class PosterPhotoModel { 8 | String id; 9 | String imageUrl; 10 | String thumbnailUrl; 11 | DateTime createdAt; 12 | 13 | PosterPhotoModel({ 14 | required this.id, 15 | required this.imageUrl, 16 | required this.thumbnailUrl, 17 | required this.createdAt, 18 | }); 19 | 20 | factory PosterPhotoModel.fromJson(Map json) => _$PosterPhotoModelFromJson(json); 21 | 22 | Map toJson() => _$PosterPhotoModelToJson(this); 23 | 24 | PosterPhotoModel copyWith({ 25 | String? id, 26 | String? imageUrl, 27 | String? thumbnailUrl, 28 | DateTime? createdAt, 29 | }) { 30 | return PosterPhotoModel( 31 | id: id ?? this.id, 32 | imageUrl: imageUrl ?? this.imageUrl, 33 | thumbnailUrl: thumbnailUrl ?? this.thumbnailUrl, 34 | createdAt: createdAt ?? this.createdAt, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/features/campaigns/models/statistics/campaign_statistics_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/features/campaigns/models/statistics/campaign_statistics_set.dart'; 2 | 3 | class CampaignStatisticsModel { 4 | final CampaignStatisticsSet flyerStats, houseStats, posterStats; 5 | 6 | const CampaignStatisticsModel({ 7 | required this.flyerStats, 8 | required this.houseStats, 9 | required this.posterStats, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /lib/features/campaigns/models/statistics/campaign_statistics_set.dart: -------------------------------------------------------------------------------- 1 | class CampaignStatisticsSet { 2 | final double own, division, state, germany; 3 | 4 | const CampaignStatisticsSet({ 5 | required this.own, 6 | required this.division, 7 | required this.state, 8 | required this.germany, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /lib/features/campaigns/screens/campaigns_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/screens/tab_screen.dart'; 3 | import 'package:gruene_app/app/widgets/app_bar.dart'; 4 | import 'package:gruene_app/app/widgets/tab_bar.dart'; 5 | import 'package:gruene_app/features/campaigns/screens/doors_screen.dart'; 6 | import 'package:gruene_app/features/campaigns/screens/flyer_screen.dart'; 7 | import 'package:gruene_app/features/campaigns/screens/posters_screen.dart'; 8 | import 'package:gruene_app/features/campaigns/screens/statistics_screen.dart'; 9 | import 'package:gruene_app/features/campaigns/screens/teams_screen.dart'; 10 | import 'package:gruene_app/features/campaigns/widgets/refresh_button.dart'; 11 | import 'package:gruene_app/i18n/translations.g.dart'; 12 | 13 | class CampaignsScreen extends StatelessWidget { 14 | final List campaignTabs = [ 15 | TabModel(label: t.campaigns.door.label, view: DoorsScreen()), 16 | TabModel(label: t.campaigns.poster.label, view: PostersScreen()), 17 | TabModel(label: t.campaigns.flyer.label, view: FlyerScreen()), 18 | TabModel(label: t.campaigns.team.label, view: TeamsScreen(), disabled: true), 19 | TabModel(label: t.campaigns.statistic.label, view: StatisticsScreen()), 20 | ]; 21 | 22 | CampaignsScreen({super.key}); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return TabScreen( 27 | appBarBuilder: (PreferredSizeWidget tabBar) => MainAppBar( 28 | title: t.campaigns.campaigns, 29 | appBarAction: RefreshButton(), 30 | tabBar: tabBar, 31 | ), 32 | tabs: campaignTabs, 33 | scrollableBody: false, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/features/campaigns/screens/mixins.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get_it/get_it.dart'; 3 | import 'package:gruene_app/app/services/converters.dart'; 4 | import 'package:gruene_app/app/services/nominatim_service.dart'; 5 | import 'package:gruene_app/app/theme/theme.dart'; 6 | import 'package:gruene_app/features/campaigns/helper/app_settings.dart'; 7 | import 'package:gruene_app/features/campaigns/helper/campaign_constants.dart'; 8 | import 'package:gruene_app/i18n/translations.g.dart'; 9 | import 'package:maplibre_gl_platform_interface/maplibre_gl_platform_interface.dart'; 10 | import 'package:motion_toast/motion_toast.dart'; 11 | 12 | part 'mixins/address_extension.dart'; 13 | part 'mixins/confirm_delete.dart'; 14 | part 'mixins/door_validator.dart'; 15 | part 'mixins/flyer_validator.dart'; 16 | part 'mixins/focus_area_info.dart'; 17 | part 'mixins/search_mixin.dart'; 18 | -------------------------------------------------------------------------------- /lib/features/campaigns/screens/mixins/address_extension.dart: -------------------------------------------------------------------------------- 1 | part of '../mixins.dart'; 2 | 3 | mixin AddressExtension { 4 | abstract TextEditingController streetTextController; 5 | abstract TextEditingController houseNumberTextController; 6 | abstract TextEditingController zipCodeTextController; 7 | abstract TextEditingController cityTextController; 8 | 9 | void disposeAddressTextControllers() { 10 | streetTextController.dispose(); 11 | houseNumberTextController.dispose(); 12 | zipCodeTextController.dispose(); 13 | cityTextController.dispose(); 14 | } 15 | 16 | AddressModel getAddress() { 17 | return AddressModel( 18 | street: streetTextController.text, 19 | houseNumber: houseNumberTextController.text, 20 | zipCode: zipCodeTextController.text, 21 | city: cityTextController.text, 22 | ); 23 | } 24 | 25 | void setAddress(AddressModel address) { 26 | streetTextController.text = address.street; 27 | houseNumberTextController.text = address.houseNumber; 28 | zipCodeTextController.text = address.zipCode; 29 | cityTextController.text = address.city; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/features/campaigns/screens/mixins/door_validator.dart: -------------------------------------------------------------------------------- 1 | part of '../mixins.dart'; 2 | 3 | mixin DoorValidator { 4 | ({int closedDoors, int openedDoors})? validateDoors( 5 | String openedDoorsRawValue, 6 | String closedDoorsRawValue, 7 | BuildContext context, 8 | ) { 9 | final openedDoors = int.tryParse(openedDoorsRawValue) ?? 0; 10 | final closedDoors = int.tryParse(closedDoorsRawValue) ?? 0; 11 | if (openedDoors + closedDoors == 0) { 12 | final theme = Theme.of(context); 13 | MotionToast.error( 14 | description: Text( 15 | t.campaigns.door.noDoorsWarning, 16 | style: theme.textTheme.labelMedium!.apply( 17 | color: ThemeColors.background, 18 | ), 19 | ), 20 | ).show(context); 21 | return null; 22 | } 23 | return (closedDoors: closedDoors, openedDoors: openedDoors); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/features/campaigns/screens/mixins/flyer_validator.dart: -------------------------------------------------------------------------------- 1 | part of '../mixins.dart'; 2 | 3 | mixin FlyerValidator { 4 | ({int flyerCount})? validateFlyer( 5 | String flyerCountRawValue, 6 | BuildContext context, 7 | ) { 8 | final flyerCount = int.tryParse(flyerCountRawValue) ?? 0; 9 | if (flyerCount < 1) { 10 | final theme = Theme.of(context); 11 | MotionToast.error( 12 | description: Text( 13 | t.campaigns.flyer.noFlyerWarning, 14 | style: theme.textTheme.labelMedium!.apply( 15 | color: ThemeColors.background, 16 | ), 17 | ), 18 | ).show(context); 19 | return null; 20 | } 21 | return (flyerCount: flyerCount); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/features/campaigns/screens/teams_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/theme/theme.dart'; 3 | import 'package:gruene_app/i18n/translations.g.dart'; 4 | 5 | class TeamsScreen extends StatelessWidget { 6 | const TeamsScreen({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Placeholder( 11 | color: Colors.red, 12 | child: Center( 13 | child: Text( 14 | t.campaigns.team.label, 15 | style: TextStyle(fontSize: 20, color: ThemeColors.primary), 16 | ), 17 | ), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/features/campaigns/widgets/address_field_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/theme/theme.dart'; 3 | 4 | class AddressFieldDetail extends StatelessWidget { 5 | const AddressFieldDetail({ 6 | super.key, 7 | required this.street, 8 | required this.houseNumber, 9 | }); 10 | 11 | final String street; 12 | final String houseNumber; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final theme = Theme.of(context); 17 | final addressTextStyle = theme.textTheme.labelSmall?.apply(color: ThemeColors.secondary, fontWeightDelta: 3); 18 | return Container( 19 | padding: EdgeInsets.symmetric(vertical: 3), 20 | child: Row( 21 | mainAxisAlignment: MainAxisAlignment.end, 22 | children: [ 23 | SizedBox( 24 | width: 100, 25 | child: Text( 26 | street, 27 | textAlign: TextAlign.right, 28 | style: addressTextStyle, 29 | overflow: TextOverflow.ellipsis, 30 | ), 31 | ), 32 | SizedBox(width: 3), 33 | Text( 34 | houseNumber, 35 | style: addressTextStyle, 36 | ), 37 | ], 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/features/campaigns/widgets/app_route.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppRoute extends MaterialPageRoute { 4 | AppRoute({required WidgetBuilder builder, super.settings}) 5 | : super( 6 | builder: (BuildContext context) { 7 | return Material(child: builder(context)); 8 | }, 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /lib/features/campaigns/widgets/close_edit_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/i18n/translations.g.dart'; 3 | 4 | class CloseEditWidget extends StatelessWidget { 5 | final void Function()? onEdit; 6 | final void Function() onClose; 7 | 8 | const CloseEditWidget({ 9 | super.key, 10 | this.onEdit, 11 | required this.onClose, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final theme = Theme.of(context); 17 | return Row( 18 | children: [ 19 | GestureDetector( 20 | onTap: onClose, 21 | child: Icon(Icons.close), 22 | ), 23 | _getSaveAction(theme), 24 | ], 25 | ); 26 | } 27 | 28 | Widget _getSaveAction(ThemeData theme) { 29 | if (onEdit == null) return SizedBox.shrink(); 30 | return Expanded( 31 | child: GestureDetector( 32 | onTap: onEdit, 33 | child: Text( 34 | t.common.actions.edit, 35 | textAlign: TextAlign.right, 36 | style: theme.textTheme.bodyLarge, 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/features/campaigns/widgets/close_save_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/i18n/translations.g.dart'; 3 | 4 | class CloseSaveWidget extends StatelessWidget { 5 | final void Function()? onSave; 6 | final void Function() onClose; 7 | 8 | const CloseSaveWidget({ 9 | super.key, 10 | this.onSave, 11 | required this.onClose, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final theme = Theme.of(context); 17 | return Row( 18 | children: [ 19 | GestureDetector( 20 | onTap: onClose, 21 | child: Icon(Icons.close), 22 | ), 23 | _getSaveAction(theme), 24 | ], 25 | ); 26 | } 27 | 28 | Widget _getSaveAction(ThemeData theme) { 29 | if (onSave == null) return SizedBox.shrink(); 30 | return Expanded( 31 | child: GestureDetector( 32 | onTap: onSave, 33 | child: Text( 34 | t.common.actions.save, 35 | textAlign: TextAlign.right, 36 | style: theme.textTheme.titleMedium, 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/features/campaigns/widgets/content_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/features/campaigns/widgets/custom_app_bar.dart'; 3 | 4 | class ContentPage extends StatelessWidget { 5 | final String title; 6 | final Widget child; 7 | 8 | final Color? contentBackgroundColor; 9 | 10 | final Alignment alignment; 11 | 12 | const ContentPage({ 13 | super.key, 14 | required this.title, 15 | required this.child, 16 | this.contentBackgroundColor, 17 | this.alignment = Alignment.center, 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final theme = Theme.of(context); 23 | return Scaffold( 24 | backgroundColor: contentBackgroundColor ?? theme.colorScheme.secondary, 25 | body: Align( 26 | alignment: alignment, 27 | child: SingleChildScrollView(child: child), 28 | ), 29 | appBar: CustomAppBar( 30 | title: title, 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/features/campaigns/widgets/custom_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:gruene_app/app/constants/routes.dart'; 4 | 5 | class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { 6 | final String title; 7 | const CustomAppBar({super.key, required this.title}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final theme = Theme.of(context); 12 | return AppBar( 13 | title: Text( 14 | title, 15 | style: theme.textTheme.displayMedium?.apply(color: theme.colorScheme.surface), 16 | ), 17 | leading: BackButton(), 18 | foregroundColor: theme.colorScheme.surface, 19 | backgroundColor: theme.primaryColor, 20 | centerTitle: true, 21 | actions: [ 22 | IconButton( 23 | icon: Icon(Icons.settings_outlined, color: theme.colorScheme.surface), 24 | onPressed: () => context.push(Routes.settings.path), 25 | ), 26 | ], 27 | ); 28 | } 29 | 30 | @override 31 | Size get preferredSize => const Size.fromHeight(kToolbarHeight); 32 | } 33 | -------------------------------------------------------------------------------- /lib/features/campaigns/widgets/custom_sliver_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:gruene_app/app/constants/routes.dart'; 4 | 5 | class CustomSliverAppBar extends StatelessWidget { 6 | final String title; 7 | 8 | const CustomSliverAppBar({super.key, required this.title}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final theme = Theme.of(context); 13 | final foregroundColor = theme.primaryColor; 14 | return SliverAppBar( 15 | backgroundColor: foregroundColor, 16 | leading: BackButton(color: foregroundColor), 17 | iconTheme: IconThemeData(color: foregroundColor), 18 | title: Text(title, style: theme.textTheme.titleMedium?.apply(color: theme.appBarTheme.foregroundColor)), 19 | centerTitle: true, 20 | actions: [ 21 | IconButton( 22 | icon: Icon(Icons.settings_outlined, color: theme.colorScheme.surface), 23 | onPressed: () => context.push(Routes.settings.path), 24 | ), 25 | ], 26 | pinned: true, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/features/campaigns/widgets/map_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:gruene_app/features/campaigns/models/bounding_box.dart'; 5 | import 'package:gruene_app/features/campaigns/models/map_layer_model.dart'; 6 | import 'package:gruene_app/features/campaigns/models/marker_item_model.dart'; 7 | import 'package:gruene_app/features/campaigns/widgets/map_container.dart'; 8 | import 'package:maplibre_gl/maplibre_gl.dart'; 9 | 10 | abstract class MapController { 11 | void setMarkerSource(List poiList); 12 | void addMarkerItem(MarkerItemModel markerItem); 13 | void removeMarkerItem(int markerItemId); 14 | 15 | Future?> getScreenPointFromLatLng(LatLng coord); 16 | 17 | Future> getFeaturesInScreen( 18 | Point point, 19 | List layers, 20 | ); 21 | 22 | Future getClosestFeaturesInScreen( 23 | Point point, 24 | List layers, 25 | ); 26 | 27 | void showMapPopover( 28 | LatLng coord, 29 | Widget widget, 30 | OnEditItemClickedCallback? onEditItemClicked, 31 | Size desiredSize, 32 | ); 33 | 34 | void setLayerSource(String sourceId, List layerData); 35 | 36 | void removeLayerSource(String focusAreadId); 37 | 38 | Future getCurrentBoundingBox(); 39 | double getCurrentZoomLevel(); 40 | 41 | double get minimumMarkerZoomLevel; 42 | 43 | void toggleInfoForMissingMapFeatures(bool enable); 44 | 45 | void navigateMapTo(LatLng location); 46 | 47 | Future setFocusToMarkerItem(Map feature); 48 | Future unsetFocusToMarkerItem(); 49 | } 50 | -------------------------------------------------------------------------------- /lib/features/campaigns/widgets/map_controller_simplified.dart: -------------------------------------------------------------------------------- 1 | abstract class MapControllerSimplified { 2 | void resetMarkerItems(); 3 | } 4 | -------------------------------------------------------------------------------- /lib/features/campaigns/widgets/sliver_content_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/features/campaigns/widgets/custom_sliver_app_bar.dart'; 3 | 4 | class SliverContentPage extends StatelessWidget { 5 | final String title; 6 | final List children; 7 | 8 | const SliverContentPage({super.key, required this.title, required this.children}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final theme = Theme.of(context); 13 | 14 | return DecoratedBox( 15 | decoration: BoxDecoration(color: theme.colorScheme.secondary), 16 | child: CustomScrollView( 17 | slivers: [ 18 | CustomSliverAppBar(title: title), 19 | SliverPadding( 20 | padding: const EdgeInsets.all(10), 21 | sliver: SliverList( 22 | delegate: SliverChildListDelegate(children), 23 | ), 24 | ), 25 | ], 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/features/campaigns/widgets/small_button_spinner.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SmallButtonSpinner extends StatelessWidget { 4 | const SmallButtonSpinner({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return const Padding( 9 | padding: EdgeInsets.all(4), 10 | child: SizedBox(height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2)), 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/features/login/screens/login_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/widgets/clean_layout.dart'; 3 | import 'package:gruene_app/features/login/widgets/support_button.dart'; 4 | import 'package:gruene_app/features/login/widgets/welcome_view.dart'; 5 | 6 | class LoginScreen extends StatelessWidget { 7 | const LoginScreen({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return CleanLayout( 12 | showAppBar: false, 13 | child: Stack( 14 | children: [ 15 | LayoutBuilder( 16 | builder: (context, constraints) { 17 | // #457: disable intro slides for now 18 | // return Container( 19 | // padding: EdgeInsets.only(bottom: defaultBottomSheetSize * constraints.maxHeight), 20 | // child: WelcomeView(), 21 | // ); 22 | return WelcomeView(); 23 | }, 24 | ), 25 | SupportButton(), 26 | // #457: disable intro slides for now 27 | // PersistentBottomSheet( 28 | // child: IntroSlides(), 29 | // ), 30 | ], 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/features/login/widgets/intro_slides.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/i18n/translations.g.dart'; 3 | 4 | class IntroSlides extends StatelessWidget { 5 | const IntroSlides({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | final theme = Theme.of(context); 10 | return Column( 11 | crossAxisAlignment: CrossAxisAlignment.start, 12 | children: [ 13 | Text( 14 | t.login.discover, 15 | style: theme.textTheme.titleLarge, 16 | ), 17 | Text(t.login.discoverDescription, style: theme.textTheme.bodyMedium), 18 | ], 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/features/login/widgets/support_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/constants/urls.dart'; 3 | import 'package:gruene_app/app/utils/open_url.dart'; 4 | import 'package:gruene_app/i18n/translations.g.dart'; 5 | 6 | class SupportButton extends StatelessWidget { 7 | const SupportButton({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final theme = Theme.of(context); 12 | return Positioned( 13 | right: 24, 14 | top: 32, 15 | height: 48, 16 | child: FilledButton.icon( 17 | onPressed: () => openUrl(supportUrl, context), 18 | icon: Icon(Icons.favorite, color: theme.colorScheme.surface), 19 | label: Text( 20 | t.login.support, 21 | style: theme.textTheme.titleMedium?.apply(color: theme.colorScheme.surface), 22 | ), 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/features/mfa/bloc/mfa_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class MfaEvent extends Equatable { 4 | const MfaEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class InitMfa extends MfaEvent {} 11 | 12 | class SetupMfa extends MfaEvent { 13 | final String activationToken; 14 | 15 | const SetupMfa(this.activationToken); 16 | 17 | @override 18 | List get props => [activationToken]; 19 | } 20 | 21 | class DeleteMfa extends MfaEvent {} 22 | 23 | class RefreshMfa extends MfaEvent { 24 | final bool raiseError; 25 | 26 | const RefreshMfa({this.raiseError = true}); 27 | } 28 | 29 | class SendReply extends MfaEvent { 30 | final bool granted; 31 | 32 | const SendReply(this.granted); 33 | 34 | @override 35 | List get props => [granted]; 36 | } 37 | 38 | class IdleTimeout extends MfaEvent {} 39 | -------------------------------------------------------------------------------- /lib/features/mfa/bloc/mfa_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:gruene_app/features/mfa/dtos/login_attempt_dto.dart'; 3 | 4 | enum MfaStatus { init, setup, ready, verify } 5 | 6 | class MfaState extends Equatable { 7 | final MfaStatus status; 8 | final Object? error; 9 | final bool isLoading; 10 | final LoginAttemptDto? loginAttempt; 11 | final DateTime? lastRefresh; 12 | final LoginAttemptDto? lastGrantedLoginAttempt; 13 | 14 | const MfaState({ 15 | this.status = MfaStatus.init, 16 | this.error, 17 | this.isLoading = false, 18 | this.loginAttempt, 19 | this.lastRefresh, 20 | this.lastGrantedLoginAttempt, 21 | }); 22 | 23 | MfaState copyWith({ 24 | MfaStatus? status, 25 | Object? error, 26 | bool? isLoading, 27 | LoginAttemptDto? loginAttempt, 28 | DateTime? lastRefresh, 29 | LoginAttemptDto? lastGrantedLoginAttempt, 30 | }) { 31 | return MfaState( 32 | status: status ?? this.status, 33 | error: error, 34 | isLoading: isLoading ?? this.isLoading, 35 | loginAttempt: loginAttempt ?? this.loginAttempt, 36 | lastRefresh: lastRefresh ?? this.lastRefresh, 37 | lastGrantedLoginAttempt: lastGrantedLoginAttempt ?? this.lastGrantedLoginAttempt, 38 | ); 39 | } 40 | 41 | @override 42 | List get props => [status, error, isLoading, loginAttempt, lastRefresh]; 43 | } 44 | -------------------------------------------------------------------------------- /lib/features/mfa/domain/mfa_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 2 | import 'package:get_it/get_it.dart'; 3 | import 'package:keycloak_authenticator/api.dart'; 4 | 5 | class MfaFactory { 6 | static AuthenticatorService create() { 7 | final secureStorage = GetIt.instance(); 8 | return AuthenticatorService( 9 | storage: FlutterSecureStorageAdapter( 10 | secureStorage, 11 | ), 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/features/mfa/dtos/login_attempt_dto.dart: -------------------------------------------------------------------------------- 1 | import 'package:keycloak_authenticator/api.dart'; 2 | 3 | class LoginAttemptDto { 4 | final DateTime loggedInAt; 5 | final String ipAddress; 6 | final String browser; 7 | final String os; 8 | final Challenge challenge; 9 | final int expiresIn; 10 | final String clientName; 11 | 12 | LoginAttemptDto({ 13 | required this.loggedInAt, 14 | required this.ipAddress, 15 | required this.browser, 16 | required this.os, 17 | required this.challenge, 18 | required this.expiresIn, 19 | required this.clientName, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /lib/features/news/bloc/bookmark_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class BookmarkEvent extends Equatable { 4 | @override 5 | List get props => []; 6 | } 7 | 8 | class LoadBookmarks extends BookmarkEvent {} 9 | 10 | class AddBookmark extends BookmarkEvent { 11 | final String newsId; 12 | 13 | AddBookmark(this.newsId); 14 | 15 | @override 16 | List get props => [newsId]; 17 | } 18 | 19 | class RemoveBookmark extends BookmarkEvent { 20 | final String bookmarkId; 21 | final String newsId; 22 | 23 | RemoveBookmark(this.bookmarkId, this.newsId); 24 | 25 | @override 26 | List get props => [bookmarkId, newsId]; 27 | } 28 | -------------------------------------------------------------------------------- /lib/features/news/bloc/bookmark_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart'; 3 | 4 | class BookmarkState extends Equatable { 5 | final bool isLoading; 6 | final List bookmarks; 7 | final Set bookmarkedNewsIds; 8 | final String? error; 9 | 10 | const BookmarkState({ 11 | this.isLoading = false, 12 | this.bookmarks = const [], 13 | this.bookmarkedNewsIds = const {}, 14 | this.error, 15 | }); 16 | 17 | BookmarkState copyWith({ 18 | bool? isLoading, 19 | List? bookmarks, 20 | Set? bookmarkedNewsIds, 21 | String? error, 22 | }) { 23 | return BookmarkState( 24 | isLoading: isLoading ?? this.isLoading, 25 | bookmarks: bookmarks ?? this.bookmarks, 26 | bookmarkedNewsIds: bookmarkedNewsIds ?? this.bookmarkedNewsIds, 27 | error: error, 28 | ); 29 | } 30 | 31 | @override 32 | List get props => [isLoading, bookmarks, bookmarkedNewsIds, error]; 33 | } 34 | -------------------------------------------------------------------------------- /lib/features/news/domain/bookmark_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/services/gruene_api_core.dart'; 2 | import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart'; 3 | 4 | Future> fetchBookmarks() async => getFromApi( 5 | request: (api) => api.v1BookmarksGet(), 6 | map: (data) => data.data, 7 | ); 8 | 9 | Future createBookmark(String newsId) async => postToApi( 10 | request: (api) => api.v1BookmarksPost( 11 | body: CreateBookmark( 12 | type: createBookmarkTypeFromJson(BookmarkType.news.value), 13 | itemId: newsId, 14 | ), 15 | ), 16 | ); 17 | 18 | Future deleteBookmark(String bookmarkId) async => deleteFromApi( 19 | request: (api) => api.v1BookmarksBookmarkIdDelete(bookmarkId: bookmarkId), 20 | ); 21 | -------------------------------------------------------------------------------- /lib/features/news/domain/news_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/services/gruene_api_core.dart'; 2 | import 'package:gruene_app/features/news/models/news_model.dart'; 3 | import 'package:gruene_app/swagger_generated_code/gruene_api.enums.swagger.dart'; 4 | 5 | Future fetchNewsById(String newsId) async => getFromApi( 6 | request: (api) => api.v1NewsNewsIdGet(newsId: newsId), 7 | map: NewsModel.fromApi, 8 | ); 9 | 10 | Future> fetchNews({String? query, bool bookmarked = false}) async => getFromApi( 11 | request: (api) => 12 | api.v1NewsGet(search: query, limit: 100, bookmarked: bookmarked ? V1NewsGetBookmarked.$true : null), 13 | map: (data) => data.data.map(NewsModel.fromApi).toList(), 14 | ); 15 | -------------------------------------------------------------------------------- /lib/features/news/dtos/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verdigado/gruene-app/2a391c820f6df53415d267c2a84728e40fbac9e3/lib/features/news/dtos/.gitkeep -------------------------------------------------------------------------------- /lib/features/news/repository/news_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 5 | import 'package:get_it/get_it.dart'; 6 | import 'package:gruene_app/app/constants/secure_storage_keys.dart'; 7 | import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart'; 8 | 9 | Future?> readDivisionFilterKeys() async { 10 | final secureStorage = GetIt.instance(); 11 | final json = await secureStorage.read(key: SecureStorageKeys.newsDivisionFilters); 12 | if (json == null) { 13 | return null; 14 | } 15 | return (jsonDecode(json) as List).map((item) => item as String).toList(); 16 | } 17 | 18 | Future writeDivisionFilterKeys(List divisions) async { 19 | final secureStorage = GetIt.instance(); 20 | final divisionKeys = divisions.map((it) => it.divisionKey).toList(); 21 | final json = jsonEncode(divisionKeys); 22 | await secureStorage.write(key: SecureStorageKeys.newsDivisionFilters, value: json); 23 | } 24 | -------------------------------------------------------------------------------- /lib/features/news/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' hide Image; 2 | import 'package:gruene_app/app/utils/utils.dart'; 3 | import 'package:gruene_app/features/news/models/news_model.dart'; 4 | import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart'; 5 | 6 | extension NewsListExtension on List { 7 | List divisions() => map((it) => it.division).nonNulls.toSet().toList(); 8 | 9 | List categories() { 10 | final categories = map((it) => it.categories).expand((it) => it).nonNulls.toSet().toList(); 11 | categories.sort((a, b) => a.label.compareTo(b.label)); 12 | return categories; 13 | } 14 | } 15 | 16 | bool isCustomFilterSelected( 17 | List selectedDivisions, 18 | List selectedCategories, 19 | DateTimeRange? dateRange, 20 | ) => 21 | !(selectedDivisions.length == 1 && selectedDivisions[0].level == DivisionLevel.bv) || 22 | selectedCategories.isNotEmpty || 23 | dateRange != null; 24 | 25 | String getPlaceholderImage(String id) { 26 | return 'assets/graphics/placeholders/placeholder_${int.parse(id) % 3 + 1}.jpg'; 27 | } 28 | 29 | extension ImageVariant on ImageSrcSet { 30 | Image variant(String type) => srcset.firstWhereOrNull((image) => image.type == type) ?? original; 31 | } 32 | -------------------------------------------------------------------------------- /lib/features/profiles/domain/profiles_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/services/gruene_api_core.dart'; 2 | import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart'; 3 | 4 | Future fetchOwnProfile() async => getFromApi(request: (api) => api.v1ProfilesSelfGet(), map: (data) => data); 5 | -------------------------------------------------------------------------------- /lib/features/profiles/helper/social_media_type_translation.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/i18n/translations.g.dart'; 2 | import 'package:gruene_app/swagger_generated_code/gruene_api.enums.swagger.dart'; 3 | 4 | String getSocialMediaTypeTranslation(SocialMediaEntryType type) { 5 | switch (type) { 6 | case SocialMediaEntryType.facebook: 7 | return t.profiles.socialMediaType.facebook; 8 | case SocialMediaEntryType.instagram: 9 | return t.profiles.socialMediaType.instagram; 10 | case SocialMediaEntryType.mastodon: 11 | return t.profiles.socialMediaType.mastodon; 12 | case SocialMediaEntryType.twitter: 13 | return t.profiles.socialMediaType.twitter; 14 | case SocialMediaEntryType.chatbegruenung: 15 | return t.profiles.socialMediaType.chatbegruenung; 16 | default: 17 | return type.value ?? ''; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/features/profiles/widgets/profile_box.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/utils/utils.dart'; 3 | import 'package:gruene_app/features/profiles/widgets/profile_box_item.dart'; 4 | import 'package:gruene_app/features/profiles/widgets/profile_card.dart'; 5 | 6 | class ProfileBox extends StatelessWidget { 7 | final String title; 8 | final Iterable items; 9 | 10 | const ProfileBox({ 11 | super.key, 12 | required this.title, 13 | required this.items, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Column( 19 | children: [ 20 | ProfileCard( 21 | child: Column( 22 | crossAxisAlignment: CrossAxisAlignment.stretch, 23 | children: [ 24 | SizedBox(height: 8), 25 | Padding( 26 | padding: const EdgeInsets.symmetric(horizontal: 16), 27 | child: Text( 28 | title, 29 | style: Theme.of(context).textTheme.titleMedium, 30 | ), 31 | ), 32 | SizedBox(height: 8), 33 | ...items.withDividers(Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: Divider())), 34 | ], 35 | ), 36 | ), 37 | ], 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/features/profiles/widgets/profile_box_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ProfileBoxItem extends StatelessWidget { 4 | final String title; 5 | final void Function()? onPress; 6 | 7 | const ProfileBoxItem({ 8 | super.key, 9 | required this.title, 10 | this.onPress, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final theme = Theme.of(context); 16 | return ListTile( 17 | contentPadding: EdgeInsets.symmetric(horizontal: 16), 18 | title: Text( 19 | title, 20 | style: theme.textTheme.bodyLarge, 21 | ), 22 | onTap: onPress, 23 | trailing: onPress != null ? Icon(Icons.open_in_browser_outlined, color: theme.primaryColor) : null, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/features/profiles/widgets/profile_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ProfileCard extends StatelessWidget { 4 | final Widget child; 5 | 6 | const ProfileCard({super.key, required this.child}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | final theme = Theme.of(context); 11 | return Padding( 12 | padding: const EdgeInsets.symmetric(horizontal: 16), 13 | child: Container( 14 | decoration: BoxDecoration( 15 | boxShadow: [ 16 | BoxShadow( 17 | color: Color.fromRGBO(0, 0, 0, 0.04), 18 | offset: Offset(0, 1), 19 | blurRadius: 12, 20 | ), 21 | ], 22 | ), 23 | child: Card( 24 | color: theme.colorScheme.surface, 25 | elevation: 0, 26 | shape: RoundedRectangleBorder( 27 | borderRadius: BorderRadius.circular(10), 28 | ), 29 | child: Padding(padding: EdgeInsets.symmetric(vertical: 4), child: child), 30 | ), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/features/profiles/widgets/profile_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/theme/theme.dart'; 3 | import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart'; 4 | 5 | class ProfileHeader extends StatelessWidget { 6 | final Profile profile; 7 | 8 | const ProfileHeader({super.key, required this.profile}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final theme = Theme.of(context); 13 | return Column( 14 | crossAxisAlignment: CrossAxisAlignment.center, 15 | children: [ 16 | SizedBox( 17 | height: 90, 18 | child: CircleAvatar( 19 | radius: 45, 20 | backgroundColor: ThemeColors.textDisabled, 21 | backgroundImage: profile.image?.thumbnail.url != null ? NetworkImage(profile.image!.thumbnail.url) : null, 22 | child: profile.image?.thumbnail.url == null 23 | ? Icon(Icons.person, size: 90, color: theme.colorScheme.surface) 24 | : null, 25 | ), 26 | ), 27 | SizedBox(height: 6), 28 | Text( 29 | '${profile.firstName} ${profile.lastName}', 30 | style: theme.textTheme.titleLarge, 31 | ), 32 | ], 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/features/settings/bloc/push_notifications/push_notification_settings_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/enums/push_notification_topic_enum.dart'; 2 | 3 | abstract class PushNotificationSettingsEvent {} 4 | 5 | class LoadSettings extends PushNotificationSettingsEvent {} 6 | 7 | class ToggleTopic extends PushNotificationSettingsEvent { 8 | final PushNotificationTopic topic; 9 | final bool value; 10 | 11 | ToggleTopic(this.topic, this.value); 12 | } 13 | 14 | class ToggleEnabled extends PushNotificationSettingsEvent {} 15 | -------------------------------------------------------------------------------- /lib/features/settings/bloc/push_notifications/push_notification_settings_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/enums/push_notification_topic_enum.dart'; 2 | 3 | class PushNotificationTopicGroup { 4 | final String name; 5 | final Map topics; 6 | 7 | const PushNotificationTopicGroup({ 8 | required this.name, 9 | required this.topics, 10 | }); 11 | } 12 | 13 | class PushNotificationSettingsState { 14 | // flag to toggle overall push notifications 15 | final bool enabled; 16 | final Map topics; 17 | 18 | const PushNotificationSettingsState({ 19 | this.enabled = true, 20 | this.topics = const {}, 21 | }); 22 | 23 | PushNotificationSettingsState copyWith({ 24 | bool? enabled, 25 | final Map? topics, 26 | }) { 27 | return PushNotificationSettingsState( 28 | enabled: enabled ?? this.enabled, 29 | topics: topics ?? this.topics, 30 | ); 31 | } 32 | 33 | List getTopicGroups() { 34 | final List topicGroups = []; 35 | 36 | // add news group 37 | topicGroups.add( 38 | PushNotificationTopicGroup( 39 | name: 'News', 40 | topics: Map.fromEntries( 41 | topics.entries.where( 42 | (entry) => [ 43 | PushNotificationTopic.newsBv, 44 | PushNotificationTopic.newsLv, 45 | PushNotificationTopic.newsKv, 46 | ].contains(entry.key), 47 | ), 48 | ), 49 | ), 50 | ); 51 | 52 | return topicGroups; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/features/settings/widgets/version_number.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/theme/theme.dart'; 3 | import 'package:gruene_app/i18n/translations.g.dart'; 4 | import 'package:package_info_plus/package_info_plus.dart'; 5 | 6 | class VersionNumber extends StatelessWidget { 7 | const VersionNumber({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final theme = Theme.of(context); 12 | return FutureBuilder( 13 | future: PackageInfo.fromPlatform(), 14 | builder: (context, snapshot) { 15 | if (snapshot.hasData) { 16 | return Container( 17 | padding: EdgeInsets.all(8), 18 | alignment: Alignment.center, 19 | child: Text( 20 | t.settings.version(version: '${snapshot.data!.version} (${snapshot.data!.buildNumber})'), 21 | style: theme.textTheme.bodyMedium!.apply(color: ThemeColors.textDisabled), 22 | ), 23 | ); 24 | } 25 | return Container(); 26 | }, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/features/tools/domain/tools_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:gruene_app/app/services/gruene_api_core.dart'; 2 | import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart'; 3 | 4 | Future> fetchTools() async => getFromApi( 5 | request: (api) => api.v1GnetzApplicationsGet(), 6 | map: (data) => data.data, 7 | ); 8 | 9 | List getToolCategories(List tools) { 10 | final categories = tools.map((tool) => tool.categories).expand((it) => it).toSet().toList(); 11 | categories.sort((a, b) => a.order.compareTo(b.order)); 12 | return categories; 13 | } 14 | -------------------------------------------------------------------------------- /lib/features/tools/screens/tools_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gruene_app/app/screens/future_loading_screen.dart'; 3 | import 'package:gruene_app/app/screens/tab_screen.dart'; 4 | import 'package:gruene_app/app/widgets/app_bar.dart'; 5 | import 'package:gruene_app/app/widgets/tab_bar.dart'; 6 | import 'package:gruene_app/features/tools/domain/tools_api_service.dart'; 7 | import 'package:gruene_app/features/tools/widgets/tools_list.dart'; 8 | import 'package:gruene_app/i18n/translations.g.dart'; 9 | import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart'; 10 | 11 | class ToolsScreen extends StatelessWidget { 12 | const ToolsScreen({super.key}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return FutureLoadingScreen( 17 | load: fetchTools, 18 | loadingLayoutBuilder: (Widget child) => Scaffold(appBar: MainAppBar(title: t.tools.tools)), 19 | buildChild: (List data) { 20 | final tabs = getToolCategories(data) 21 | .map( 22 | (category) => TabModel( 23 | label: category.title, 24 | view: ToolsList(tools: data.where((tool) => tool.categories.contains(category)).toList()), 25 | ), 26 | ) 27 | .toList(); 28 | 29 | return TabScreen( 30 | appBarBuilder: (PreferredSizeWidget tabBar) => MainAppBar(title: t.tools.tools, tabBar: tabBar), 31 | tabs: tabs, 32 | ); 33 | }, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/features/tools/widgets/tools_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_svg/flutter_svg.dart'; 3 | import 'package:gruene_app/app/constants/config.dart'; 4 | import 'package:gruene_app/app/utils/open_url.dart'; 5 | import 'package:gruene_app/app/widgets/icon.dart'; 6 | import 'package:gruene_app/features/settings/widgets/settings_card.dart'; 7 | import 'package:gruene_app/swagger_generated_code/gruene_api.swagger.dart'; 8 | 9 | class ToolsList extends StatelessWidget { 10 | final List tools; 11 | 12 | const ToolsList({super.key, required this.tools}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final theme = Theme.of(context); 17 | return ListView( 18 | padding: const EdgeInsets.all(16), 19 | children: tools.map( 20 | (tool) { 21 | final icon = tool.icon; 22 | final url = tool.url; 23 | return SettingsCard( 24 | title: tool.title, 25 | subtitle: tool.shortDescription[Config.defaultLocale] as String? ?? '', 26 | onPress: url != null ? () => openUrl(url, context) : null, 27 | isExternal: true, 28 | icon: icon == null 29 | ? CustomIcon(path: 'assets/icons/sunflower.svg', color: theme.colorScheme.primary) 30 | : SvgPicture.string(icon, colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.srcIn)), 31 | ); 32 | }, 33 | ).toList(), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/i18n/app_en.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "appName": "Green App" 4 | } 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "format": "fvm dart format -l 120 .", 4 | "analyze": "fvm dart analyze", 5 | "fix": "fvm dart fix --apply", 6 | "fix:dry": "fvm dart fix --dry-run" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "367f9ea16bfae1ca451b9cc27c1366870b187ae2" 8 | channel: "stable" 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/README.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | TODO: Put a short description of the package here that helps potential users 15 | know whether this package might be useful for them. 16 | 17 | ## Features 18 | 19 | TODO: List what your package can do. Maybe include images, gifs, or videos. 20 | 21 | ## Getting started 22 | 23 | TODO: List prerequisites and provide or point to information on how to 24 | start using the package. 25 | 26 | ## Usage 27 | 28 | TODO: Include short and useful examples for package users. Add longer examples 29 | to `/example` folder. 30 | 31 | ```dart 32 | const like = 'sample'; 33 | ``` 34 | 35 | ## Additional information 36 | 37 | TODO: Tell users more about the package: where to find more information, how to 38 | contribute to the package, how to file issues, what response they can expect 39 | from the package authors, and more. 40 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/api.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | export 'src/authenticator_service.dart'; 4 | export 'src/authenticator.dart'; 5 | export 'src/dtos/authenticator_info.dart'; 6 | export 'src/dtos/challenge.dart'; 7 | export 'src/exceptions/keycloak_client_exception.dart'; 8 | export 'src/keycloak_authenticator.dart'; 9 | export 'src/keycloak_client.dart'; 10 | export 'src/storage/flutter_secure_storage_adapter.dart'; 11 | export 'src/storage/storage.dart'; 12 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/src/authenticator.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | import 'package:keycloak_authenticator/src/dtos/challenge.dart'; 4 | 5 | abstract class Authenticator { 6 | String getId(); 7 | String? getLabel(); 8 | Future fetchChallenge(); 9 | Future reply({required Challenge challenge, required bool granted}); 10 | // AuthenticatorInfo getInfo(); 11 | } 12 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/src/dtos/activation_token_dto.dart: -------------------------------------------------------------------------------- 1 | class ActivationTokenDto { 2 | final String baseUrl; 3 | final String realm; 4 | final String key; 5 | final String tabId; 6 | final String clientId; 7 | 8 | ActivationTokenDto({ 9 | required this.baseUrl, 10 | required this.realm, 11 | required this.key, 12 | required this.tabId, 13 | required this.clientId, 14 | }); 15 | 16 | factory ActivationTokenDto.fromUrl(String url) { 17 | final uri = Uri.parse(url); 18 | 19 | var clientId = uri.queryParameters['client_id']; 20 | if (clientId == null) { 21 | throw const FormatException('missing client_id'); 22 | } 23 | 24 | var tabId = uri.queryParameters['tab_id']; 25 | if (tabId == null) { 26 | throw const FormatException('missing tab_id'); 27 | } 28 | 29 | var key = uri.queryParameters['key']; 30 | if (key == null) { 31 | throw const FormatException('missing key'); 32 | } 33 | 34 | RegExp exp = RegExp(r'(.*\/realms\/([^\/]+))\/'); 35 | RegExpMatch? match = exp.firstMatch(uri.path); 36 | var basePath = match?[1]; 37 | var realm = match?[2]; 38 | 39 | if (realm == null) { 40 | throw const FormatException('missing realm in path'); 41 | } 42 | var baseUrl = "${uri.origin}$basePath"; 43 | return ActivationTokenDto( 44 | baseUrl: baseUrl, 45 | realm: realm, 46 | clientId: clientId, 47 | tabId: tabId, 48 | key: key, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/src/dtos/authenticator_entry.dart: -------------------------------------------------------------------------------- 1 | class AuthenticatorEntry { 2 | final String id; 3 | final String? label; 4 | 5 | AuthenticatorEntry({required this.id, this.label}); 6 | 7 | factory AuthenticatorEntry.fromJson(Map json) { 8 | return AuthenticatorEntry( 9 | id: json['id'], 10 | label: json['label'], 11 | ); 12 | } 13 | 14 | Map toJson() { 15 | return { 16 | 'id': id, 17 | 'label': label, 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/src/dtos/authenticator_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:keycloak_authenticator/src/enums/key_algorithm_enum.dart'; 2 | import 'package:keycloak_authenticator/src/enums/signature_algorithm_enum.dart'; 3 | 4 | class AuthenticatorInfo { 5 | final SignatureAlgorithm signatureAlgorithm; 6 | final KeyAlgorithm keyAlgorithm; 7 | final DateTime registeredAt; 8 | 9 | AuthenticatorInfo({ 10 | required this.signatureAlgorithm, 11 | required this.keyAlgorithm, 12 | required this.registeredAt, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/src/enums/device_os_enum.dart: -------------------------------------------------------------------------------- 1 | enum DeviceOs { 2 | android, 3 | ios, 4 | } 5 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/src/enums/enums.dart: -------------------------------------------------------------------------------- 1 | export 'device_os_enum.dart'; 2 | export 'key_algorithm_enum.dart'; 3 | export 'signature_algorithm_enum.dart'; 4 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/src/enums/key_algorithm_enum.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | enum KeyAlgorithm { 4 | RSA, 5 | EC, 6 | } 7 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/src/enums/signature_algorithm_enum.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | enum SignatureAlgorithm { 4 | SHA256withRSA, 5 | SHA512withRSA, 6 | SHA512withECDSA, 7 | } 8 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/src/exceptions/keycloak_client_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | class KeycloakClientException implements Exception { 4 | String message; 5 | KeycloakExceptionType type; 6 | DioException dioException; 7 | KeycloakClientException( 8 | this.message, { 9 | required this.dioException, 10 | this.type = KeycloakExceptionType.badRequest, 11 | }); 12 | } 13 | 14 | enum KeycloakExceptionType { 15 | networkError, 16 | unknown, 17 | notRegistered, 18 | serverError, 19 | badRequest, 20 | } 21 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/src/storage/flutter_secure_storage_adapter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 2 | import 'storage.dart'; 3 | 4 | class FlutterSecureStorageAdapter implements Storage { 5 | final FlutterSecureStorage _storage; 6 | 7 | FlutterSecureStorageAdapter(FlutterSecureStorage storage) : _storage = storage; 8 | 9 | @override 10 | Future delete({required String key}) { 11 | return _storage.delete(key: key); 12 | } 13 | 14 | @override 15 | Future read({required String key}) { 16 | return _storage.read(key: key); 17 | } 18 | 19 | @override 20 | Future write({required String key, required String? value}) { 21 | return _storage.write(key: key, value: value); 22 | } 23 | 24 | @override 25 | Future containsKey({required String key}) { 26 | return _storage.containsKey( 27 | key: key, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/src/storage/storage.dart: -------------------------------------------------------------------------------- 1 | abstract class Storage { 2 | Future write({ 3 | required String key, 4 | required String? value, 5 | }); 6 | 7 | Future read({ 8 | required String key, 9 | }); 10 | 11 | Future delete({ 12 | required String key, 13 | }); 14 | 15 | Future containsKey({required String key}); 16 | } 17 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/lib/src/utils/device_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:keycloak_authenticator/src/enums/device_os_enum.dart'; 3 | 4 | class DeviceUtils { 5 | DeviceUtils._(); 6 | 7 | static DeviceOs getDeviceOs() { 8 | if (Platform.isIOS) { 9 | return DeviceOs.ios; 10 | } else if (Platform.isAndroid) { 11 | return DeviceOs.android; 12 | } 13 | throw Exception("${Platform.operatingSystem} is not supported"); 14 | } 15 | 16 | static Future getDevicePushId() async { 17 | return null; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/keycloak_authenticator/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: keycloak_authenticator 2 | description: A new Flutter package project. 3 | version: 0.0.1 4 | # homepage: 5 | 6 | environment: 7 | sdk: '>=3.1.2 <4.0.0' 8 | flutter: ">=1.17.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | flutter_secure_storage: ^9.0.0 14 | dio: ^5.3.3 15 | pointycastle: ^3.7.3 16 | dart_jsonwebtoken: ^2.12.1 17 | uuid: ^4.2.1 18 | 19 | dev_dependencies: 20 | flutter_lints: ^2.0.0 21 | 22 | # For information on the generic Dart part of this file, see the 23 | # following page: https://dart.dev/tools/pub/pubspec 24 | -------------------------------------------------------------------------------- /slang.yaml: -------------------------------------------------------------------------------- 1 | base_locale: de 2 | fallback_strategy: base_locale 3 | input_file_pattern: .json 4 | input_directory: lib/i18n 5 | output_file_name: translations.g.dart 6 | -------------------------------------------------------------------------------- /tools/circleci-update-config: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | { cat .circleci/autogenerated_header.yml; circleci config pack .circleci/src; } > .circleci/config.yml && circleci config validate -------------------------------------------------------------------------------- /tools/constants.ts: -------------------------------------------------------------------------------- 1 | const VERSION_FILE = 'version.yaml' 2 | const MAIN_BRANCH = 'main' 3 | 4 | const PLATFORM_ANDROID: 'android' = 'android' 5 | const PLATFORM_IOS: 'ios' = 'ios' 6 | 7 | const PLATFORMS = [PLATFORM_IOS, PLATFORM_ANDROID] 8 | type Platform = typeof PLATFORMS[number] 9 | 10 | type ReleaseInformation = { 11 | platform: Platform 12 | versionName: string 13 | } 14 | const tagId = ({ platform, versionName }: ReleaseInformation): string => `${versionName}-${platform}` 15 | export { 16 | VERSION_FILE, 17 | MAIN_BRANCH, 18 | PLATFORM_ANDROID, 19 | PLATFORM_IOS, 20 | PLATFORMS, 21 | Platform, 22 | tagId 23 | } 24 | -------------------------------------------------------------------------------- /tools/github-authentication.ts: -------------------------------------------------------------------------------- 1 | import { createAppAuth } from '@octokit/auth-app' 2 | import { Octokit } from '@octokit/rest' 3 | 4 | const authenticate = async ({ 5 | githubPrivateKey, 6 | owner, 7 | repo, 8 | }: { 9 | githubPrivateKey: string 10 | owner: string 11 | repo: string 12 | }): Promise => { 13 | const appId = 1059509 // https://github.com/apps/VerdigadoReleaseBot 14 | const privateKey = Buffer.from(githubPrivateKey, 'base64').toString('ascii') 15 | 16 | const octokit = new Octokit({ authStrategy: createAppAuth, auth: { appId, privateKey } }) 17 | const { 18 | data: { id: installationId }, 19 | } = await octokit.apps.getRepoInstallation({ owner, repo }) 20 | const { 21 | data: { token }, 22 | } = await octokit.apps.createInstallationAccessToken({ installation_id: installationId }) 23 | 24 | return new Octokit({ auth: token }) 25 | } 26 | 27 | export default authenticate 28 | -------------------------------------------------------------------------------- /tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "name": "tools", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "manage-metadata": "tsx manage-metadata.ts", 8 | "git-version": "tsx git-version.ts", 9 | "github-release": "tsx github-release.ts", 10 | "github-promote-release": "tsx github-promote-release.ts", 11 | "github-release-asset": "tsx github-release-asset.ts", 12 | "move-release-notes": "tsx move-release-notes.ts", 13 | "next-version": "tsx next-version.ts", 14 | "trigger-pipeline": "tsx trigger-pipeline.ts" 15 | }, 16 | "devDependencies": { 17 | "@octokit/auth-app": "^7.1.2", 18 | "@octokit/rest": "^21.0.2", 19 | "@types/js-yaml": "^4.0.9", 20 | "@types/node": "^22.9.0", 21 | "commander": "^12.1.0", 22 | "js-yaml": "^4.1.0", 23 | "node-fetch": "^3.3.2", 24 | "tsx": "^4.19.2", 25 | "typescript": "^5.6.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "noFallthroughCasesInSwitch": true, 8 | "noImplicitAny": true, 9 | "noUncheckedIndexedAccess": true, 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "outDir": "dist" 14 | }, 15 | "include": ["*"] 16 | } 17 | -------------------------------------------------------------------------------- /version.yaml: -------------------------------------------------------------------------------- 1 | versionName: 2025.4.4 2 | versionCode: 20527 3 | --------------------------------------------------------------------------------