├── .editorconfig ├── .firebaserc ├── .github ├── release-drafter.yml ├── scripts │ ├── compress-android-apk.sh │ ├── compress-android-appbundle.sh │ ├── configure-android-keystore.sh │ ├── configure-ios-certificate.sh │ ├── configure-ios-keychain.sh │ ├── configure-ios-profile.py │ ├── configure-ios-profile.sh │ ├── export-ios-ipa.sh │ ├── update-remote-config.py │ └── update-remote-config.sh └── workflows │ ├── deploy-beta.yml │ ├── deploy-release.yml │ ├── firebase-hosting-merge.yml │ ├── firebase-hosting-pull-request.yml │ └── sonarcloud-analyze.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── apps ├── .gitkeep ├── firebase │ ├── .firebaserc │ ├── .gitignore │ ├── README.md │ ├── firebase.json │ ├── functions │ │ ├── .eslintrc.js │ │ ├── .gitignore │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── src │ │ │ ├── http │ │ │ │ ├── connectivity │ │ │ │ │ └── status.f.ts │ │ │ │ └── pushNotifications │ │ │ │ │ ├── remove.f.ts │ │ │ │ │ └── save.f.ts │ │ │ ├── index.ts │ │ │ ├── locales │ │ │ │ └── en.json │ │ │ ├── models │ │ │ │ ├── base.ts │ │ │ │ ├── device.ts │ │ │ │ ├── fcm.ts │ │ │ │ ├── location.ts │ │ │ │ ├── message.ts │ │ │ │ ├── push_notification.ts │ │ │ │ └── units.ts │ │ │ ├── schedule │ │ │ │ └── send_forecast.f.ts │ │ │ └── utils │ │ │ │ ├── forecast_utils.ts │ │ │ │ ├── push_utils.ts │ │ │ │ ├── remote_config_utils.ts │ │ │ │ └── string_utils.ts │ │ ├── tsconfig.dev.json │ │ └── tsconfig.json │ └── rules │ │ └── firestore.rules ├── mobile_flutter │ ├── .gitignore │ ├── .metadata │ ├── Dockerfile │ ├── LICENSE │ ├── README.md │ ├── android │ │ ├── .gitignore │ │ ├── app │ │ │ ├── build.gradle │ │ │ ├── proguard-rules.pro │ │ │ └── src │ │ │ │ ├── androidTest │ │ │ │ └── java │ │ │ │ │ └── io │ │ │ │ │ └── flutter_weather │ │ │ │ │ └── MainActivityTest.java │ │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ │ ├── main │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── io │ │ │ │ │ │ └── flutter_weather │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── res │ │ │ │ │ ├── drawable-hdpi │ │ │ │ │ ├── app_icon.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-mdpi │ │ │ │ │ ├── app_icon.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-night-hdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-night-mdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-night-xhdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-night-xxhdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-night-xxxhdpi │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-night │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ ├── app_icon.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ ├── app_icon.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ ├── app_icon.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable │ │ │ │ │ ├── app_icon.png │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── raw │ │ │ │ │ └── keep.xml │ │ │ │ │ ├── values-night │ │ │ │ │ └── colors.xml │ │ │ │ │ └── values │ │ │ │ │ ├── colors.xml │ │ │ │ │ └── styles.xml │ │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── gradle.properties │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ └── gradle-wrapper.properties │ │ ├── settings.gradle │ │ └── settings_aar.gradle │ ├── assets │ │ └── images │ │ │ ├── github_dark.png │ │ │ ├── github_light.png │ │ │ ├── launcher.png │ │ │ ├── launcher_background.png │ │ │ ├── launcher_foreground.png │ │ │ ├── osi.png │ │ │ ├── splash.png │ │ │ ├── splash_invert.png │ │ │ └── splash_notext.png │ ├── dist │ │ └── whatsnew │ │ │ └── whatsnew-en-US │ ├── docker-compose.yml │ ├── integration_test │ │ ├── integration_test_utils.dart │ │ └── views │ │ │ └── lookup │ │ │ └── lookup_view_test.dart │ ├── ios │ │ ├── .gitignore │ │ ├── Flutter │ │ │ ├── .last_build_id │ │ │ ├── AppFrameworkInfo.plist │ │ │ ├── Debug.xcconfig │ │ │ └── Release.xcconfig │ │ ├── Podfile │ │ ├── Podfile.lock │ │ ├── Runner.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ ├── dev.xcscheme │ │ │ │ ├── prod.xcscheme │ │ │ │ └── tst.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-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ ├── LaunchBackground.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── background.png │ │ │ │ └── darkbackground.png │ │ │ └── LaunchImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── LaunchImageDark.png │ │ │ │ ├── LaunchImageDark@2x.png │ │ │ │ ├── LaunchImageDark@3x.png │ │ │ │ └── README.md │ │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ ├── Runner-Bridging-Header.h │ │ │ └── Runner.entitlements │ ├── lib │ │ ├── about │ │ │ ├── about.dart │ │ │ └── view │ │ │ │ ├── privacy_policy_view.dart │ │ │ │ └── view.dart │ │ ├── app │ │ │ ├── app_config.dart │ │ │ ├── app_keys.dart │ │ │ ├── app_localization.dart │ │ │ ├── app_prefs.dart │ │ │ ├── app_root.dart │ │ │ ├── app_theme.dart │ │ │ ├── bloc │ │ │ │ ├── app_bloc.dart │ │ │ │ ├── app_bloc_observer.dart │ │ │ │ ├── app_events.dart │ │ │ │ ├── app_state.dart │ │ │ │ └── bloc.dart │ │ │ ├── mocks │ │ │ │ ├── mock_storage.dart │ │ │ │ └── mocks.dart │ │ │ ├── utils │ │ │ │ ├── chart_utils.dart │ │ │ │ ├── color_utils.dart │ │ │ │ ├── common_utils.dart │ │ │ │ ├── connectivity_utils.dart │ │ │ │ ├── date_utils.dart │ │ │ │ ├── device_utils.dart │ │ │ │ ├── fab_utils.dart │ │ │ │ ├── geolocator_utils.dart │ │ │ │ ├── input_utils.dart │ │ │ │ ├── math_utils.dart │ │ │ │ ├── push_utils.dart │ │ │ │ ├── scroll_utils.dart │ │ │ │ ├── snackbar_utils.dart │ │ │ │ ├── utils.dart │ │ │ │ └── version_utils.dart │ │ │ └── widgets │ │ │ │ ├── app_checkbox_tile.dart │ │ │ │ ├── app_color_theme.dart │ │ │ │ ├── app_day_night_switch.dart │ │ │ │ ├── app_fab.dart │ │ │ │ ├── app_form_button.dart │ │ │ │ ├── app_label.dart │ │ │ │ ├── app_none_found.dart │ │ │ │ ├── app_option_button.dart │ │ │ │ ├── app_pageview_scroll_physics.dart │ │ │ │ ├── app_progress_indicator.dart │ │ │ │ ├── app_radio_tile.dart │ │ │ │ ├── app_section_header.dart │ │ │ │ ├── app_temperature_display.dart │ │ │ │ ├── app_transparent_route.dart │ │ │ │ ├── app_ui_overlay_style.dart │ │ │ │ ├── app_ui_safe_area.dart │ │ │ │ ├── app_update_dialog.dart │ │ │ │ └── widgets.dart │ │ ├── enums │ │ │ ├── chart_type.dart │ │ │ ├── crud_status.dart │ │ │ ├── distance_unit.dart │ │ │ ├── enums.dart │ │ │ ├── flavor.dart │ │ │ ├── hour_range.dart │ │ │ ├── lookup_status.dart │ │ │ ├── message_type.dart │ │ │ ├── pressure_unit.dart │ │ │ ├── push_notifications.dart │ │ │ ├── refresh_status.dart │ │ │ ├── temperature_unit.dart │ │ │ ├── theme_mode.dart │ │ │ ├── update_period.dart │ │ │ └── wind_speed_unit.dart │ │ ├── forecast │ │ │ ├── bloc │ │ │ │ ├── bloc.dart │ │ │ │ └── forecast_form_bloc.dart │ │ │ ├── forecast.dart │ │ │ ├── forecast_extension.dart │ │ │ ├── forecast_form.dart │ │ │ ├── forecast_utils.dart │ │ │ ├── view │ │ │ │ ├── forecast_alerts_view.dart │ │ │ │ ├── forecast_form_view.dart │ │ │ │ ├── forecast_view.dart │ │ │ │ └── view.dart │ │ │ └── widgets │ │ │ │ ├── forecast_alert_button.dart │ │ │ │ ├── forecast_alert_description.dart │ │ │ │ ├── forecast_condition.dart │ │ │ │ ├── forecast_country_picker.dart │ │ │ │ ├── forecast_current_temp.dart │ │ │ │ ├── forecast_day_charts.dart │ │ │ │ ├── forecast_day_scroller.dart │ │ │ │ ├── forecast_detail_display.dart │ │ │ │ ├── forecast_dew_point.dart │ │ │ │ ├── forecast_display.dart │ │ │ │ ├── forecast_divider.dart │ │ │ │ ├── forecast_edit_button.dart │ │ │ │ ├── forecast_hi_lo.dart │ │ │ │ ├── forecast_hour_display.dart │ │ │ │ ├── forecast_hour_tile.dart │ │ │ │ ├── forecast_hours.dart │ │ │ │ ├── forecast_humidity.dart │ │ │ │ ├── forecast_icon.dart │ │ │ │ ├── forecast_last_updated.dart │ │ │ │ ├── forecast_location.dart │ │ │ │ ├── forecast_meta.dart │ │ │ │ ├── forecast_meta_info.dart │ │ │ │ ├── forecast_options.dart │ │ │ │ ├── forecast_pressure.dart │ │ │ │ ├── forecast_refresh.dart │ │ │ │ ├── forecast_settings_button.dart │ │ │ │ ├── forecast_sliver_header.dart │ │ │ │ ├── forecast_uv_index.dart │ │ │ │ ├── forecast_visibility.dart │ │ │ │ ├── forecast_wind_direction.dart │ │ │ │ ├── forecast_wind_speed.dart │ │ │ │ └── widgets.dart │ │ ├── lookup │ │ │ ├── bloc │ │ │ │ ├── bloc.dart │ │ │ │ ├── lookup_bloc.dart │ │ │ │ ├── lookup_events.dart │ │ │ │ └── lookup_state.dart │ │ │ ├── lookup.dart │ │ │ ├── lookup_utils.dart │ │ │ └── view │ │ │ │ ├── lookup_view.dart │ │ │ │ └── view.dart │ │ ├── main.dart │ │ ├── main_dev.dart │ │ ├── main_test.dart │ │ ├── models │ │ │ ├── config.dart │ │ │ ├── forecast.dart │ │ │ ├── models.dart │ │ │ ├── notification.dart │ │ │ └── units.dart │ │ ├── services │ │ │ ├── connectivity_service.dart │ │ │ ├── firebase_remoteconfig_service.dart │ │ │ ├── forecast_service.dart │ │ │ ├── push_notification_service.dart │ │ │ └── services.dart │ │ └── settings │ │ │ ├── settings.dart │ │ │ ├── settings_utils.dart │ │ │ ├── view │ │ │ ├── settings_view.dart │ │ │ └── view.dart │ │ │ └── widgets │ │ │ ├── settings_auto_update.dart │ │ │ ├── settings_chart_type_picker.dart │ │ │ ├── settings_distance_units_picker.dart │ │ │ ├── settings_hour_range_picker.dart │ │ │ ├── settings_open_source_info.dart │ │ │ ├── settings_option.dart │ │ │ ├── settings_pressure_units_picker.dart │ │ │ ├── settings_push_notification_picker.dart │ │ │ ├── settings_temperature_units_picker.dart │ │ │ ├── settings_theme_mode_picker.dart │ │ │ ├── settings_update_period_picker.dart │ │ │ ├── settings_version_status_text.dart │ │ │ ├── settings_wind_speed_units_picker.dart │ │ │ └── widgets.dart │ ├── pubspec.lock │ ├── pubspec.yaml │ ├── test │ │ ├── app │ │ │ └── utils │ │ │ │ ├── common_utils_test.dart │ │ │ │ ├── connectivity_utils_test.dart │ │ │ │ ├── connectivity_utils_test.mocks.dart │ │ │ │ ├── date_utils_test.dart │ │ │ │ ├── forecast_utils_test.dart │ │ │ │ ├── math_utils_test.dart │ │ │ │ └── version_utils_test.dart │ │ ├── forecast │ │ │ ├── forecast_utils_test.dart │ │ │ └── view │ │ │ │ └── forecast_form_view_test.dart │ │ ├── lookup │ │ │ ├── lookup_utils_test.dart │ │ │ └── view │ │ │ │ └── lookup_view_test.dart │ │ ├── settings │ │ │ └── widgets │ │ │ │ └── settings_version_status_text_test.dart │ │ └── utils │ │ │ ├── pump_utils.dart │ │ │ └── test_utils.dart │ └── test_driver │ │ └── integration_test.dart ├── web_ng │ ├── .browserslistrc │ ├── README.md │ ├── jest.config.js │ ├── src │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ └── app.module.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── test-setup.ts │ ├── tsconfig.app.json │ ├── tsconfig.editor.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── tslint.json └── web_ng_e2e │ ├── cypress.json │ ├── src │ ├── fixtures │ │ └── example.json │ ├── integration │ │ └── app.spec.ts │ ├── plugins │ │ └── index.js │ └── support │ │ ├── app.po.ts │ │ ├── commands.ts │ │ └── index.ts │ ├── tsconfig.e2e.json │ ├── tsconfig.json │ └── tslint.json ├── docs └── images │ ├── app_store.png │ ├── logo.png │ ├── play_store.png │ ├── screen1.png │ ├── screen2.png │ └── screen3.png ├── firebase.json ├── git-version.yml ├── jest.config.js ├── jest.preset.js ├── libs └── .gitkeep ├── nx.json ├── package-lock.json ├── package.json ├── sonar-project.properties ├── tools ├── generators │ └── .gitkeep └── tsconfig.tools.json ├── tsconfig.base.json ├── tslint.json └── workspace.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "my-flutter-weather" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What's Changed 3 | 4 | $CHANGES -------------------------------------------------------------------------------- /.github/scripts/compress-android-apk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | cd "$FLUTTER_WEATHER_RELEASE_FOLDER" 6 | zip -r "$FLUTTER_WEATHER_RELEASE_ZIP" "$FLUTTER_WEATHER_RELEASE_APP" 7 | ls -l "$FLUTTER_WEATHER_RELEASE_ZIP" -------------------------------------------------------------------------------- /.github/scripts/compress-android-appbundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | cd "$FLUTTER_WEATHER_RELEASE_FOLDER" 6 | zip -r "$FLUTTER_WEATHER_RELEASE_ZIP" "$FLUTTER_WEATHER_RELEASE_APP" 7 | ls -l "$FLUTTER_WEATHER_RELEASE_ZIP" -------------------------------------------------------------------------------- /.github/scripts/configure-android-keystore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | echo "$FLUTTER_WEATHER_KEYSTORE" > "$FLUTTER_WEATHER_KEYSTORE_JKS_ASC_FILE_PATH" 6 | gpg -d --passphrase "$FLUTTER_WEATHER_KEYSTORE_PASS" --batch "$FLUTTER_WEATHER_KEYSTORE_JKS_ASC_FILE_PATH"> "$FLUTTER_WEATHER_KEYSTORE_JKS_FILE_PATH" -------------------------------------------------------------------------------- /.github/scripts/configure-ios-certificate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | security import "$FLUTTER_WEATHER_CERTS_FILE_PATH" -k "$FLUTTER_WEATHER_KEYCHAIN" -P "$FLUTTER_WEATHER_CERTS_P12_PASSWORD" -A 6 | security find-identity 7 | security set-key-partition-list -S apple-tool:,apple: -s -k "" "$FLUTTER_WEATHER_KEYCHAIN" -------------------------------------------------------------------------------- /.github/scripts/configure-ios-keychain.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | security create-keychain -p "" "$FLUTTER_WEATHER_KEYCHAIN" 6 | security list-keychains -s "$FLUTTER_WEATHER_KEYCHAIN" 7 | security default-keychain -s "$FLUTTER_WEATHER_KEYCHAIN" 8 | security unlock-keychain -p "" "$FLUTTER_WEATHER_KEYCHAIN" 9 | security set-keychain-settings 10 | security list-keychains 11 | -------------------------------------------------------------------------------- /.github/scripts/configure-ios-profile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # Create a virtual environment for python 6 | pip install virtualenv 7 | python -m virtualenv env 8 | source env/bin/activate 9 | 10 | # Install libs 11 | pip install requests Authlib 12 | 13 | # Configures the provisioning profile 14 | python .github/scripts/configure-ios-profile.py \ 15 | --homePath "$HOME" \ 16 | --keyId "$FLUTTER_WEATHER_APPSTORE_KEY_ID" \ 17 | --issuerId "$FLUTTER_WEATHER_APPSTORE_ISSUER_ID" \ 18 | --privateKey "$FLUTTER_WEATHER_APPSTORE_PRIVATE_KEY" \ 19 | --identifier "$FLUTTER_WEATHER_APPSTORE_IDENTIFIER" \ 20 | --certificateId "$FLUTTER_WEATHER_APPSTORE_CERTIFICATE_ID" \ 21 | --certificatePath "$FLUTTER_WEATHER_CERTS_FILE_PATH" \ 22 | --profileName "$FLUTTER_WEATHER_APPSTORE_PROFILE_NAME" \ 23 | --profileType "$FLUTTER_WEATHER_APPSTORE_PROFILE_TYPE" 24 | -------------------------------------------------------------------------------- /.github/scripts/export-ios-ipa.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | xcodebuild -exportArchive \ 6 | -archivePath "$FLUTTER_WEATHER_EXPORT_XCARCHIVE_FILE_PATH" \ 7 | -exportOptionsPlist "$FLUTTER_WEATHER_EXPORT_OPTIONS_PLIST_FILE_PATH" \ 8 | -exportPath "$FLUTTER_WEATHER_EXPORT_IPA_FILE_PATH" 9 | 10 | cd "$FLUTTER_WEATHER_EXPORT_IPA_FILE_PATH" 11 | mv "Flutter Weather.ipa" "$FLUTTER_WEATHER_EXPORT_IPA_FILE_NAME" 12 | -------------------------------------------------------------------------------- /.github/scripts/update-remote-config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import getopt, sys, json 4 | 5 | argumentList = sys.argv[1:] 6 | options = 'p:v:' 7 | long_options = ['platform', 'version'] 8 | platform = None 9 | version = None 10 | 11 | try: 12 | arguments, values = getopt.getopt(argumentList, options, long_options) 13 | for currentArgument, currentValue in arguments: 14 | if currentArgument in ('-p', '--platform'): 15 | platform = currentValue 16 | elif currentArgument in ('-v', '--version'): 17 | version = currentValue 18 | 19 | with open('firebase-remote-config.json', 'r+') as f: 20 | data = json.load(f) 21 | version_build_parts = version.split('+') 22 | data['parameters']['app_version']['defaultValue']['value'] = version_build_parts[0] 23 | data['parameters']['app_build']['defaultValue']['value'] = version_build_parts[1] 24 | 25 | # Example of using the device platform 26 | # data['parameters']['app_version']['conditionalValues'][platform]['value'] = version_build_parts[0] 27 | # data['parameters']['app_build']['conditionalValues'][platform]['value'] = version_build_parts[1] 28 | 29 | f.seek(0) 30 | json.dump(data, f) 31 | f.truncate() 32 | except getopt.error as err: 33 | print(str(err)) 34 | -------------------------------------------------------------------------------- /.github/scripts/update-remote-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | while getopts ":p:v:" opt; do 6 | case $opt in 7 | p) platform="$OPTARG" 8 | ;; 9 | v) version="$OPTARG" 10 | ;; 11 | \?) echo "Invalid option -$OPTARG" >&2 12 | ;; 13 | esac 14 | done 15 | 16 | # Update some values in the file 17 | # sudo python .github/scripts/update-remote-config.py -p "$platform" -v "$version" 18 | sudo python .github/scripts/update-remote-config.py -v "$version" -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-merge.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy to Firebase Hosting on merge" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | buildDeploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | uses: actions/checkout@v1 14 | - 15 | run: npm ci 16 | - 17 | name: "Nrwl Nx" 18 | uses: MansaGroup/nrwl-nx-action@v2.0.3 19 | with: 20 | targets: build 21 | projects: web_ng 22 | - 23 | uses: FirebaseExtended/action-hosting-deploy@v0 24 | with: 25 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 26 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_MY_FLUTTER_WEATHER }}" 27 | channelId: live 28 | projectId: "${{ secrets.FIREBASE_PROJECT_ID }}" 29 | env: 30 | FIREBASE_CLI_PREVIEWS: hostingchannels -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy to Firebase Hosting on PR" 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | buildPreview: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - 10 | uses: actions/checkout@v1 11 | - 12 | run: npm ci 13 | - 14 | name: "Nrwl Nx" 15 | uses: MansaGroup/nrwl-nx-action@v2.0.3 16 | with: 17 | targets: build 18 | projects: web_ng 19 | - 20 | uses: FirebaseExtended/action-hosting-deploy@v0 21 | with: 22 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 23 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_MY_FLUTTER_WEATHER }}" 24 | projectId: "${{ secrets.FIREBASE_PROJECT_ID }}" 25 | env: 26 | FIREBASE_CLI_PREVIEWS: hostingchannels -------------------------------------------------------------------------------- /.github/workflows/sonarcloud-analyze.yml: -------------------------------------------------------------------------------- 1 | name: "SonarCloud" 2 | 3 | on: 4 | push: 5 | branches: 6 | - release-** 7 | 8 | # pull_request: 9 | # types: [ opened, synchronize, reopened ] 10 | 11 | jobs: 12 | sonarcloud: 13 | name: "SonarCloud" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - 21 | name: "SonarCloud Scan" 22 | uses: SonarSource/sonarcloud-github-action@master 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | SONAR_TOKEN: ${{ secrets.FLUTTER_WEATHER_SONAR_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment 2 | environment_config.yaml 3 | .env 4 | 5 | # Miscellaneous 6 | *.class 7 | *.log 8 | *.pyc 9 | *.swp 10 | .DS_Store 11 | .atom/ 12 | .buildlog/ 13 | .history 14 | .svn/ 15 | 16 | # IntelliJ related 17 | *.iml 18 | *.ipr 19 | *.iws 20 | .idea/ 21 | 22 | # Visual Studio Code related 23 | .vscode/settings.json 24 | .vscode/extensions.json 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .packages 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Web related 37 | lib/generated_plugin_registrant.dart 38 | 39 | # Symbolication related 40 | app.*.symbols 41 | 42 | # Obfuscation related 43 | app.*.map.json 44 | 45 | # Exceptions to above rules. 46 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 47 | node_modules 48 | **/dist/* 49 | !**/dist/whatsnew -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Flutter_Weather_DEV", 6 | "type": "dart", 7 | "request": "launch", 8 | "program": "apps/mobile_flutter/lib/main_dev.dart", 9 | "args": [ 10 | "--flavor", 11 | "dev" 12 | ] 13 | }, 14 | { 15 | "name": "Flutter_Weather_PROD", 16 | "type": "dart", 17 | "request": "launch", 18 | "program": "apps/mobile_flutter/lib/main.dart", 19 | "args": [ 20 | "--flavor", 21 | "prod" 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Scott Carnett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/.gitkeep -------------------------------------------------------------------------------- /apps/firebase/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "my-flutter-weather" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /apps/firebase/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | -------------------------------------------------------------------------------- /apps/firebase/README.md: -------------------------------------------------------------------------------- 1 | ## Setup 2 | ```bash 3 | npm install -g firebase-tools 4 | firebase login 5 | cd functions/ 6 | npm i 7 | ``` 8 | 9 | ## Tests 10 | ```bash 11 | npm test 12 | ``` 13 | 14 | ## Firebase Configuration 15 | Some of these functions refer to the Firebase remote configuration but they need authorization to do so. You will need to generate a new Firebase Admin SDK private key and move it into the following folder: 16 | 17 | ```bash 18 | functions/src/keys/ 19 | ``` 20 | 21 | ### Firebase Environment Configuration 22 | ```bash 23 | firebase functions:config:set auth.key.filename="" # This is the private key that you generated above. Ex: flutter-weather-firebase-adminsdk.json 24 | firebase functions:config:set project.id="" 25 | firebase functions:config:set openweathermap.key="" 26 | ``` 27 | 28 | ### Inspect your Firebase Environment Configuration 29 | ```bash 30 | firebase functions:config:get 31 | ``` 32 | 33 | ## Deploy functions 34 | 35 | ```bash 36 | npm run deploy-functions 37 | ``` 38 | 39 | ## Deploy hosting 40 | 41 | ```bash 42 | npm run deploy-hosting 43 | ``` 44 | 45 | ## Deploy firestore rules 46 | 47 | ```bash 48 | npm run deploy-firestore-rules 49 | ``` 50 | 51 | ## Structure 52 | ``` 53 | functions/ 54 | firestore/ 55 | name/ 56 | onWrite.f.ts 57 | 58 | name2/ 59 | onCreate.f.ts 60 | 61 | name3/ 62 | onUpdate.f.ts 63 | 64 | name4/ 65 | onCreate.f.ts 66 | onUpdate.f.ts 67 | onDelete.f.ts 68 | 69 | http/ 70 | name/ 71 | endpointName.f.ts 72 | 73 | schedule/ 74 | jobName.f.ts 75 | 76 | index.ts 77 | ``` 78 | 79 | ## Notes 80 | When you deploy these functions a lib/ folder will be generated that contains the transpiled javascript files that get deployed to firebase. 81 | It is highly recommended to delete this folder if it exists prior to deploying the functions. This will ensure that a clean build is being deployed. 82 | -------------------------------------------------------------------------------- /apps/firebase/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "rules/firestore.rules" 4 | }, 5 | "functions": { 6 | "predeploy": [ 7 | "npm --prefix \"$RESOURCE_DIR\" run lint", 8 | "npm --prefix \"$RESOURCE_DIR\" run build" 9 | ] 10 | }, 11 | "hosting": { 12 | "public": "/", 13 | "ignore": [ 14 | "firebase.json", 15 | "**/.*", 16 | "**/node_modules/**" 17 | ], 18 | "rewrites": [ 19 | { 20 | "source": "http-push-notifications-save", 21 | "function": "httpPushNotificationsSave" 22 | }, 23 | { 24 | "source": "http-push-notifications-remove", 25 | "function": "httpPushNotificationsRemove" 26 | }, 27 | { 28 | "source": "http-connectivity-status", 29 | "function": "httpConnectivityStatus" 30 | }, 31 | { 32 | "source": "**", 33 | "destination": "/index.html" 34 | } 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/firebase/functions/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:import/errors', 10 | 'plugin:import/warnings', 11 | 'plugin:import/typescript', 12 | 'google', 13 | 'prettier', 14 | ], 15 | parser: '@typescript-eslint/parser', 16 | parserOptions: { 17 | project: ['tsconfig.json', 'tsconfig.dev.json'], 18 | sourceType: 'module', 19 | tsconfigRootDir: __dirname, 20 | }, 21 | ignorePatterns: [ 22 | '/lib/**/*', // Ignore built files. 23 | ], 24 | plugins: [ 25 | '@typescript-eslint', 26 | 'import', 27 | ], 28 | rules: { 29 | '@typescript-eslint/member-delimiter-style': [ 30 | 'error', 31 | { 32 | 'multiline': { 33 | 'delimiter': 'none', 34 | 'requireLast': true, 35 | }, 36 | 'singleline': { 37 | 'delimiter': 'semi', 38 | 'requireLast': false, 39 | }, 40 | }, 41 | ], 42 | '@typescript-eslint/semi': [ 43 | 'error', 44 | 'never', 45 | ], 46 | 'semi': [0, 'never'], 47 | 'max-len': ['error', {'code': 120}], 48 | 'object-curly-spacing': [0, 'never'], 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /apps/firebase/functions/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled JavaScript files 2 | lib/** 3 | 4 | # TypeScript v1 declaration files 5 | typings/ 6 | 7 | # Node.js dependency directory 8 | node_modules/ 9 | 10 | /keys/ 11 | flutter-weather-firebase-config.json 12 | src/assets/service-account.json -------------------------------------------------------------------------------- /apps/firebase/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "eslint --ext .js,.ts .", 5 | "build": "tsc", 6 | "serve": "npm run build && firebase emulators:start --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy-functions": "firebase deploy --only functions", 10 | "deploy-hosting": "firebase deploy --only hosting", 11 | "deploy-firestore-rules": "firebase deploy --only firestore:rules", 12 | "logs": "firebase functions:log", 13 | "test": "jest --watchAll" 14 | }, 15 | "engines": { 16 | "node": "12" 17 | }, 18 | "main": "lib/index.js", 19 | "dependencies": { 20 | "camelcase": "^6.2.0", 21 | "firebase-admin": "^9.8.0", 22 | "firebase-functions": "^3.14.1", 23 | "glob": "^7.1.7", 24 | "googleapis": "^73.0.0", 25 | "i18n": "^0.13.3", 26 | "luxon": "^1.27.0", 27 | "openweathermap-ts": "^1.2.7" 28 | }, 29 | "devDependencies": { 30 | "@types/i18n": "^0.13.0", 31 | "@types/luxon": "^1.26.5", 32 | "@typescript-eslint/eslint-plugin": "^4.24.0", 33 | "@typescript-eslint/parser": "^4.24.0", 34 | "eslint": "^7.26.0", 35 | "eslint-config-google": "^0.14.0", 36 | "eslint-config-prettier": "^8.3.0", 37 | "eslint-plugin-import": "^2.23.2", 38 | "firebase-functions-test": "^0.2.3", 39 | "typescript": "^4.2.4" 40 | }, 41 | "private": true 42 | } 43 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/http/connectivity/status.f.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions' 2 | 3 | exports = module.exports = functions.https 4 | .onRequest(async (req: functions.https.Request, res: functions.Response) => { 5 | res.status(200).send('ok') 6 | }) 7 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/http/pushNotifications/remove.f.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin' 2 | import * as functions from 'firebase-functions' 3 | 4 | exports = module.exports = functions.https 5 | .onRequest(async (req: functions.https.Request, res: functions.Response) => { 6 | const promises: Array> = [] 7 | const data: any = req.body 8 | if (data != null) { 9 | try { 10 | // Deletes the device from firestore 11 | promises.push(admin.firestore().doc(`devices/${data['device']}`).delete()) 12 | 13 | return Promise.all(promises) 14 | .then(() => { 15 | res.status(200).send('ok') 16 | }) 17 | .catch((error: any) => { 18 | functions.logger.error(error) 19 | res.status(500).send('error') 20 | }) 21 | } catch (error) { 22 | functions.logger.error(error) 23 | res.status(500).send('error') 24 | } 25 | } 26 | 27 | res.status(200).send('ok') 28 | }) 29 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/http/pushNotifications/save.f.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin' 2 | import * as functions from 'firebase-functions' 3 | import { DateTime } from 'luxon' 4 | 5 | exports = module.exports = functions.https 6 | .onRequest(async (req: functions.https.Request, res: functions.Response) => { 7 | const promises: Array> = [] 8 | const data: any = req.body 9 | if (data != null) { 10 | try { 11 | const now: admin.firestore.Timestamp = admin.firestore.Timestamp.now() 12 | const lastPushDate: DateTime = DateTime.fromJSDate(now.toDate()).startOf('hour') 13 | const pushNotificationExtras: string = data['pushNotificationExtras'] 14 | const units: string = data['units'] 15 | 16 | // Save the device to firestore 17 | promises.push(admin.firestore().doc(`devices/${data['device']}`).set({ 18 | 'period': data['period'], 19 | 'pushNotification': data['pushNotification'], 20 | 'pushNotificationExtras': (pushNotificationExtras == null) ? 21 | admin.firestore.FieldValue.delete() : 22 | JSON.parse(pushNotificationExtras), 23 | 'units': (units == null) ? 24 | admin.firestore.FieldValue.delete() : 25 | JSON.parse(units), 26 | 'fcm': { 27 | 'token': data['fcmToken'], 28 | }, 29 | 'lastUpdated': now.toDate(), 30 | 'lastPushDate': lastPushDate.toJSDate(), 31 | }, { 32 | 'merge': true, 33 | })) 34 | 35 | return Promise.all(promises) 36 | .then(() => { 37 | res.status(200).send('ok') 38 | }) 39 | .catch((error: any) => { 40 | functions.logger.error(error) 41 | res.status(500).send('error') 42 | }) 43 | } catch (error) { 44 | functions.logger.error(error) 45 | res.status(500).send('error') 46 | } 47 | } 48 | 49 | res.status(200).send('ok') 50 | }) 51 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import camelcase from 'camelcase' 2 | import * as admin from 'firebase-admin' 3 | import * as glob from 'glob' 4 | import * as i18n from 'i18n' 5 | import * as path from 'path' 6 | 7 | admin.initializeApp() 8 | 9 | const paths: string[] = [ 10 | './firestore/**/*.f.js', // Firestore 11 | './http/**/*.f.js', // HTTP 12 | './schedule/*.f.js', // Cron 13 | ] 14 | 15 | i18n.configure({ 16 | locales: ['en'], 17 | directory: path.join(__dirname, 'locales'), 18 | }) 19 | 20 | for (const filePath of paths) { 21 | const files: string[] = glob.sync(filePath, {cwd: __dirname, ignore: `./node_modules/**`}) 22 | for (const file of files) { 23 | processExport(file) 24 | } 25 | } 26 | 27 | /** 28 | * Processes the export 29 | * @param {string} file the function ts file 30 | */ 31 | function processExport(file: string) { 32 | const functionName: string = camelcase(file.slice(0, -5).split('/').join('_')) // Strip off '.f.ts' 33 | exports[functionName] = require(file) 34 | } 35 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "{{temp}}{{temperatureUnit}} in {{cityName}} - {{condition}}": "{{temp}}{{temperatureUnit}} in {{cityName}} - {{condition}}", 3 | "High {{highTemp}}{{temperatureUnit}} | Low {{lowTemp}}{{temperatureUnit}}": "High {{highTemp}}{{temperatureUnit}} | Low {{lowTemp}}{{temperatureUnit}}" 4 | } -------------------------------------------------------------------------------- /apps/firebase/functions/src/models/base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The base model 3 | */ 4 | export class BaseModel { 5 | /** 6 | * Converts a json string into an object 7 | * @param {any} json the json 8 | * @return {any} with the object 9 | */ 10 | static fromJSON(json: any): any { 11 | return JSON.parse(json) 12 | } 13 | 14 | /** 15 | * Converts an object into a json string 16 | * @return {string} with the json string 17 | */ 18 | toJSON(): string { 19 | return JSON.stringify(this) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/models/device.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin' 2 | import { BaseModel } from './base' 3 | import { FCM } from './fcm' 4 | import { PushNotificationExtras } from './push_notification' 5 | import { Units } from './units' 6 | 7 | /** 8 | * The device model 9 | */ 10 | export class Device extends BaseModel { 11 | fcm?: FCM 12 | period?: string 13 | pushNotification?: string 14 | pushNotificationExtras?: PushNotificationExtras 15 | units?: Units 16 | lastUpdated?: admin.firestore.Timestamp 17 | lastPushDate?: admin.firestore.Timestamp 18 | } 19 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/models/fcm.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel } from './base' 2 | 3 | /** 4 | * The fcm model 5 | */ 6 | export class FCM extends BaseModel { 7 | token?: string 8 | } 9 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/models/location.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel } from './base' 2 | 3 | /** 4 | * The location model 5 | */ 6 | export class Location extends BaseModel { 7 | id?: string 8 | latitude?: number 9 | longitude?: number 10 | name?: string 11 | } 12 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/models/message.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The push message model 3 | */ 4 | export class Message { 5 | title: any 6 | body: any 7 | image: any 8 | data: any = {} 9 | priorityAndroid: MessagePriorityAndroid = 'high' 10 | priorityIos: MessagePriorityIos = '10' 11 | sound: MessageSound = 'default' 12 | color: string = '#7D33B7' 13 | icon: string = 'app_icon' 14 | tag: string = 'flutter_weather' 15 | } 16 | 17 | type MessageSound = 'default' | 'disabled' 18 | type MessagePriorityAndroid = 'normal' | 'high' 19 | type MessagePriorityIos = '5' | '10' 20 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/models/push_notification.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel } from './base' 2 | import { Location } from './location' 3 | 4 | /** 5 | * The push notification extras model 6 | */ 7 | export class PushNotificationExtras extends BaseModel { 8 | location?: Location 9 | sound: boolean = true 10 | showUnits: boolean = false 11 | } 12 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/models/units.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel } from './base' 2 | 3 | /** 4 | * The units model 5 | */ 6 | export class Units extends BaseModel { 7 | temperature?: 'imperial' | 'metric' | 'standard' 8 | windSpeed?: 'mph' | 'kmh' 9 | } 10 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/utils/forecast_utils.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin' 2 | import { DateTime } from 'luxon' 3 | import * as deviceModel from '../models/device' 4 | 5 | /** 6 | * Checks to see if we can push a message to a device 7 | * @param {deviceModel.Device} device the device 8 | * @param {Date} minDate the min date to compare against 9 | * @return {boolean} with the status 10 | */ 11 | export function canPushForecast(device: deviceModel.Device, minDate: Date): boolean { 12 | const lastPushDateTs: admin.firestore.Timestamp | undefined = device.lastPushDate 13 | if (lastPushDateTs != null) { 14 | const lastPushDate: DateTime = DateTime.fromJSDate(lastPushDateTs.toDate()) 15 | const hours: number = getPeriodHours(device) 16 | if (lastPushDate.plus({ hours: hours }) <= DateTime.fromJSDate(minDate)) { 17 | return true 18 | } 19 | 20 | return false 21 | } 22 | 23 | return true 24 | } 25 | 26 | /** 27 | * Gets the hours for a period 28 | * @param {deviceModel.Device} device the device 29 | * @return {number} with the hours 30 | */ 31 | export function getPeriodHours(device: deviceModel.Device): number { 32 | switch (device.period) { 33 | case '1hr': 34 | return 1 35 | 36 | case '2hrs': 37 | return 2 38 | 39 | case '3hrs': 40 | return 3 41 | 42 | case '4hrs': 43 | return 4 44 | 45 | case '5hrs': 46 | return 5 47 | 48 | default: 49 | return 0 50 | } 51 | } 52 | 53 | /** 54 | * Gets the temperature unit 55 | * @param {any} temperatureUnit the temperature unit. can be one of standard, metric or imperial 56 | * @param {boolean} showUnits the show units status 57 | * @return {string} with the temperature unit 58 | */ 59 | export function getTemperatureUnit(temperatureUnit: any, showUnits: boolean): string { 60 | switch (temperatureUnit) { 61 | case 'metric': 62 | return `°${showUnits ? 'C' : ''}` 63 | 64 | case 'standard': 65 | return ` ${showUnits ? ' K' : ''}` 66 | 67 | case 'imperial': 68 | default: 69 | return `°${showUnits ? 'F' : ''}` 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/utils/remote_config_utils.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions' 2 | import { google } from 'googleapis' 3 | 4 | const rp: any = require('request-promise') 5 | const RC_SCOPES: string[] = [ 6 | 'https://www.googleapis.com/auth/firebase.remoteconfig', 7 | 'https://www.googleapis.com/auth/userinfo.email', 8 | 'https://www.googleapis.com/auth/firebase.database', 9 | 'https://www.googleapis.com/auth/androidpublisher', 10 | ] 11 | 12 | /** 13 | * Gets the remote configuration url 14 | * @return {string} with the remote configuration url 15 | */ 16 | export function remoteConfigUrl(): string { 17 | const host: string = 'firebaseremoteconfig.googleapis.com' 18 | return `https://${host}/v1/projects/${functions.config().project.id}/remoteConfig` 19 | } 20 | 21 | /** 22 | * Gets the remote configuration 23 | * @return {Promise} with the remote configuration 24 | */ 25 | export async function getRemoteConfig(): Promise { 26 | return getAccessToken() 27 | .then((accessToken: any) => { 28 | const options: any = { 29 | uri: remoteConfigUrl(), 30 | headers: { 31 | 'Authorization': `Bearer ${accessToken}`, 32 | }, 33 | json: true, 34 | } 35 | 36 | return rp(options) 37 | .then((config: any) => Promise.resolve(config)) 38 | .catch((err: any) => Promise.resolve(null)) 39 | }) 40 | .catch((error: any) => { 41 | functions.logger.error(error) 42 | return Promise.resolve(null) 43 | }) 44 | } 45 | 46 | /** 47 | * Gets an access token 48 | * @return {any} with the token 49 | */ 50 | export function getAccessToken(): any { 51 | return new Promise((resolve, reject) => { 52 | const key: any = require(`../keys/${functions.config().auth.key.filename}`) 53 | const jwtClient: any = new google.auth.JWT(key.client_email, undefined, key.private_key, RC_SCOPES, undefined) 54 | jwtClient.authorize((err: any, tokens: any) => { 55 | if (err) { 56 | reject(err) 57 | return 58 | } 59 | 60 | resolve(tokens.access_token) 61 | }) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /apps/firebase/functions/src/utils/string_utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Capitalizes a string 3 | * @param {string} str the string 4 | * @return {string} with the capitalized string 5 | */ 6 | export function capitalize(str: string): string { 7 | return str.charAt(0).toUpperCase() + str.slice(1) 8 | } 9 | -------------------------------------------------------------------------------- /apps/firebase/functions/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | ".eslintrc.js" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /apps/firebase/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "module": "commonjs", 5 | "noImplicitReturns": true, 6 | "noUnusedLocals": true, 7 | "outDir": "lib", 8 | "sourceMap": true, 9 | "strict": true, 10 | "target": "es2017", 11 | "resolveJsonModule": true 12 | }, 13 | "compileOnSave": true, 14 | "include": [ 15 | "**/*.ts", 16 | "*/keys/*.json", 17 | "*/locales/*.json" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /apps/firebase/rules/firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /{document=**} { 5 | allow read, write: if false; 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /apps/mobile_flutter/.gitignore: -------------------------------------------------------------------------------- 1 | # Environment 2 | environment_config.yaml 3 | .env 4 | 5 | # Miscellaneous 6 | *.class 7 | *.log 8 | *.pyc 9 | *.swp 10 | .DS_Store 11 | .atom/ 12 | .buildlog/ 13 | .history 14 | .svn/ 15 | 16 | # IntelliJ related 17 | *.iml 18 | *.ipr 19 | *.iws 20 | .idea/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .flutter-plugins-dependencies 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Web related 33 | lib/generated_plugin_registrant.dart 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Exceptions to above rules. 42 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 43 | -------------------------------------------------------------------------------- /apps/mobile_flutter/.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: 78910062997c3a836feee883712c241a5fd22983 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /apps/mobile_flutter/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.10 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | # Prerequisites 6 | RUN apt update && apt install -y curl git unzip xz-utils zip libglu1-mesa openjdk-8-jdk wget npm 7 | 8 | # Set up new user 9 | RUN useradd -ms /bin/bash developer 10 | USER developer 11 | WORKDIR /home/developer 12 | 13 | # Prepare Android directories and system variables 14 | RUN mkdir -p Android/sdk 15 | ENV ANDROID_SDK_ROOT /home/developer/Android/sdk 16 | RUN mkdir -p .android && touch .android/repositories.cfg 17 | 18 | # Set up Android SDK 19 | RUN wget -O sdk-tools.zip https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip 20 | RUN unzip sdk-tools.zip && rm sdk-tools.zip 21 | RUN mv tools Android/sdk/tools 22 | RUN cd Android/sdk/tools/bin && yes | ./sdkmanager --licenses 23 | RUN cd Android/sdk/tools/bin && ./sdkmanager "build-tools;29.0.2" "patcher;v4" "platform-tools" "platforms;android-29" "sources;android-29" 24 | ENV PATH "$PATH:/home/developer/Android/sdk/platform-tools" 25 | 26 | # Download Flutter SDK 27 | RUN git clone https://github.com/flutter/flutter.git 28 | ENV PATH "$PATH:/home/developer/flutter/bin" 29 | 30 | # Run basic check to download Dark SDK 31 | RUN flutter doctor 32 | 33 | # Switch to the stable channel 34 | RUN flutter channel stable 35 | 36 | # Upgrade flutter 37 | RUN flutter upgrade 38 | 39 | # Install and start the remote dev server 40 | # RUN npm i -g remotedev-server 41 | # RUN remotedev --port 8000 42 | 43 | WORKDIR /home/developer/workspace 44 | 45 | # Install packages 46 | # ENTRYPOINT ["/bin/bash", "-c", "flutter pub get"] -------------------------------------------------------------------------------- /apps/mobile_flutter/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Scott Carnett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/mobile_flutter/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/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | app/key.jks 13 | app/google-services.json -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | ## Gson rules 2 | # Gson uses generic type information stored in a class file when working with fields. Proguard 3 | # removes such information by default, so configure it to keep all of it. 4 | -keepattributes Signature 5 | 6 | # For using GSON @Expose annotation 7 | -keepattributes *Annotation* 8 | 9 | # Gson specific classes 10 | -dontwarn sun.misc.** 11 | #-keep class com.google.gson.stream.** { *; } 12 | 13 | # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, 14 | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) 15 | -keep class * extends com.google.gson.TypeAdapter 16 | -keep class * implements com.google.gson.TypeAdapterFactory 17 | -keep class * implements com.google.gson.JsonSerializer 18 | -keep class * implements com.google.gson.JsonDeserializer 19 | 20 | # Prevent R8 from leaving Data object members always null 21 | -keepclassmembers,allowobfuscation class * { 22 | @com.google.gson.annotations.SerializedName ; 23 | } -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/androidTest/java/io/flutter_weather/MainActivityTest.java: -------------------------------------------------------------------------------- 1 | package io.flutter_weather; 2 | 3 | import androidx.test.rule.ActivityTestRule; 4 | import dev.flutter.plugins.integration_test.FlutterTestRunner; 5 | import org.junit.Rule; 6 | import org.junit.runner.RunWith; 7 | 8 | @RunWith(FlutterTestRunner.class) 9 | public class MainActivityTest { 10 | @Rule 11 | public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class, true, false); 12 | } 13 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/kotlin/io/flutter_weather/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.flutter_weather 2 | 3 | import androidx.core.view.WindowCompat 4 | import io.flutter.embedding.android.FlutterActivity 5 | 6 | class MainActivity: FlutterActivity() { 7 | // @see https://github.com/goderbauer/scratchpad/commit/fb5c97d3ecadf6c48bff20b5a8d83f8b19226070 8 | override fun onPostResume() { 9 | super.onPostResume() 10 | WindowCompat.setDecorFitsSystemWindows(window, false) 11 | window.navigationBarColor = 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-hdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-hdpi/app_icon.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-mdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-mdpi/app_icon.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-mdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-night-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-night-hdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-night-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-night-mdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-night-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-night-xhdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-night-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-night-xxhdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-night-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-night-xxxhdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-night/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-xhdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-xhdpi/app_icon.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-xxhdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-xxhdpi/app_icon.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-xxxhdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-xxxhdpi/app_icon.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/drawable/app_icon.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/raw/keep.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #262626 4 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #8725BE 4 | #FFFFFF 5 | #7D33B7 6 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 12 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.4.10' 3 | ext { 4 | compileSdkVersion = 31 5 | targetSdkVersion = 31 6 | appCompatVersion = '1.1.0' 7 | } 8 | 9 | repositories { 10 | google() 11 | jcenter() 12 | maven { 13 | url 'http://download.flutter.io' 14 | } 15 | } 16 | 17 | dependencies { 18 | classpath 'com.android.tools.build:gradle:4.2.0' 19 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 20 | classpath 'com.google.gms:google-services:4.3.4' 21 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0' 22 | } 23 | } 24 | 25 | allprojects { 26 | repositories { 27 | google() 28 | jcenter() 29 | } 30 | } 31 | 32 | rootProject.buildDir = '../build' 33 | subprojects { 34 | project.buildDir = "${rootProject.buildDir}/${project.name}" 35 | } 36 | subprojects { 37 | project.evaluationDependsOn(':app') 38 | } 39 | 40 | task clean(type: Delete) { 41 | delete rootProject.buildDir 42 | } 43 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip -------------------------------------------------------------------------------- /apps/mobile_flutter/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /apps/mobile_flutter/android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /apps/mobile_flutter/assets/images/github_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/assets/images/github_dark.png -------------------------------------------------------------------------------- /apps/mobile_flutter/assets/images/github_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/assets/images/github_light.png -------------------------------------------------------------------------------- /apps/mobile_flutter/assets/images/launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/assets/images/launcher.png -------------------------------------------------------------------------------- /apps/mobile_flutter/assets/images/launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/assets/images/launcher_background.png -------------------------------------------------------------------------------- /apps/mobile_flutter/assets/images/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/assets/images/launcher_foreground.png -------------------------------------------------------------------------------- /apps/mobile_flutter/assets/images/osi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/assets/images/osi.png -------------------------------------------------------------------------------- /apps/mobile_flutter/assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/assets/images/splash.png -------------------------------------------------------------------------------- /apps/mobile_flutter/assets/images/splash_invert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/assets/images/splash_invert.png -------------------------------------------------------------------------------- /apps/mobile_flutter/assets/images/splash_notext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/assets/images/splash_notext.png -------------------------------------------------------------------------------- /apps/mobile_flutter/dist/whatsnew/whatsnew-en-US: -------------------------------------------------------------------------------- 1 | Dependency upgrades 2 | -------------------------------------------------------------------------------- /apps/mobile_flutter/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | flutter_weather_dev: 4 | container_name: flutter_weather_dev 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - '$PWD:/home/developer/workspace' 10 | - '/dev/bus/usb:/dev/bus/usb' 11 | privileged: true 12 | stdin_open: true 13 | tty: true 14 | command: flutter run -t lib/main_dev.dart --flavor dev 15 | 16 | flutter_weather_prod: 17 | container_name: flutter_weather_prod 18 | build: 19 | context: . 20 | dockerfile: Dockerfile 21 | volumes: 22 | - '$PWD:/home/developer/workspace' 23 | - '/dev/bus/usb:/dev/bus/usb' 24 | privileged: true 25 | stdin_open: true 26 | tty: true 27 | command: flutter run -t lib/main.dart --flavor prod --release 28 | -------------------------------------------------------------------------------- /apps/mobile_flutter/integration_test/integration_test_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | Future pumpForSeconds( 6 | WidgetTester tester, 7 | int seconds, 8 | ) async { 9 | bool timerDone = false; 10 | 11 | Timer(Duration(seconds: seconds), () => timerDone = true); 12 | 13 | while (timerDone != true) { 14 | await tester.pump(); 15 | } 16 | } 17 | 18 | Finder findText( 19 | String text, 20 | ) => 21 | find.byWidgetPredicate( 22 | (Widget widget) => (widget is Text) && widget.data!.startsWith(text), 23 | ); 24 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | Runner/GoogleService-Info.plist 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Flutter/.last_build_id: -------------------------------------------------------------------------------- 1 | 7298f7277f9d454a81c3d1c2d119edcb -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 3 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig" -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | import Firebase 4 | 5 | @UIApplicationMain 6 | @objc class AppDelegate: FlutterAppDelegate { 7 | override func application( 8 | _ application: UIApplication, 9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 10 | ) -> Bool { 11 | FirebaseApp.configure() 12 | 13 | GeneratedPluginRegistrant.register(with: self) 14 | 15 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/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 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "darkbackground.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "appearances" : [ 25 | { 26 | "appearance" : "luminosity", 27 | "value" : "dark" 28 | } 29 | ], 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "universal", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "dark" 42 | } 43 | ], 44 | "idiom" : "universal", 45 | "scale" : "3x" 46 | } 47 | ], 48 | "info" : { 49 | "author" : "xcode", 50 | "version" : 1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png -------------------------------------------------------------------------------- /apps/mobile_flutter/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 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "LaunchImageDark.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "LaunchImage@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "LaunchImageDark@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "LaunchImage@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "LaunchImageDark@3x.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/mobile_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png -------------------------------------------------------------------------------- /apps/mobile_flutter/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. -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /apps/mobile_flutter/ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/about/about.dart: -------------------------------------------------------------------------------- 1 | export 'view/view.dart'; 2 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/about/view/privacy_policy_view.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_weather/app/app_config.dart'; 5 | import 'package:flutter_weather/app/app_localization.dart'; 6 | import 'package:flutter_weather/app/widgets/widgets.dart'; 7 | import 'package:webview_flutter/webview_flutter.dart'; 8 | 9 | class PrivacyPolicyView extends StatefulWidget { 10 | static Route route() => 11 | MaterialPageRoute(builder: (_) => PrivacyPolicyView()); 12 | 13 | PrivacyPolicyView({ 14 | Key? key, 15 | }) : super(key: key); 16 | 17 | @override 18 | State createState() => _PrivacyPolicyPageViewState(); 19 | } 20 | 21 | class _PrivacyPolicyPageViewState extends State 22 | with TickerProviderStateMixin { 23 | Completer _controller = Completer(); 24 | 25 | @override 26 | Widget build( 27 | BuildContext context, 28 | ) => 29 | AppUiOverlayStyle( 30 | child: Scaffold( 31 | appBar: AppBar( 32 | title: Text(AppLocalizations.of(context)!.privacyPolicy), 33 | leading: IconButton( 34 | icon: const Icon(Icons.arrow_back), 35 | onPressed: () => Navigator.of(context).pop(), 36 | ), 37 | ), 38 | body: AppUiSafeArea( 39 | child: Expanded( 40 | child: Stack( 41 | children: [ 42 | WebView( 43 | initialUrl: AppConfig.instance.config.privacyPolicyUrl, 44 | onWebViewCreated: _onWebViewCreated, 45 | ), 46 | ], 47 | ), 48 | ), 49 | ), 50 | extendBody: true, 51 | extendBodyBehindAppBar: true, 52 | ), 53 | ); 54 | 55 | void _onWebViewCreated( 56 | WebViewController webViewController, 57 | ) => 58 | _controller.complete(webViewController); 59 | } 60 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/about/view/view.dart: -------------------------------------------------------------------------------- 1 | export 'privacy_policy_view.dart'; 2 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/app_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_weather/enums/flavor.dart'; 3 | import 'package:flutter_weather/models/models.dart'; 4 | 5 | class AppConfig extends InheritedWidget { 6 | final Flavor flavor; 7 | final Config config; 8 | 9 | static late AppConfig _instance; 10 | 11 | factory AppConfig({ 12 | required Flavor flavor, 13 | required Config config, 14 | required Widget child, 15 | }) { 16 | _instance = AppConfig._internal( 17 | flavor, 18 | config, 19 | child, 20 | ); 21 | 22 | return _instance; 23 | } 24 | 25 | AppConfig._internal( 26 | this.flavor, 27 | this.config, 28 | Widget child, 29 | ) : super(child: child); 30 | 31 | static AppConfig get instance => _instance; 32 | 33 | static AppConfig? of( 34 | BuildContext context, 35 | ) => 36 | context.dependOnInheritedWidgetOfExactType(aspect: AppConfig); 37 | 38 | static bool isDebug() { 39 | switch (instance.flavor) { 40 | case Flavor.dev: 41 | return true; 42 | 43 | case Flavor.tst: 44 | case Flavor.prod: 45 | default: 46 | return false; 47 | } 48 | } 49 | 50 | static bool isRelease() { 51 | switch (instance.flavor) { 52 | case Flavor.prod: 53 | return true; 54 | 55 | case Flavor.tst: 56 | case Flavor.dev: 57 | default: 58 | return false; 59 | } 60 | } 61 | 62 | @override 63 | bool updateShouldNotify( 64 | InheritedWidget oldWidget, 65 | ) => 66 | false; 67 | } 68 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/app_keys.dart: -------------------------------------------------------------------------------- 1 | class AppKeys { 2 | static String addLocationKey = 'addLocation'; 3 | static String locationCityKey = 'locationCity'; 4 | static String locationPostalCodeKey = 'locationPostalCode'; 5 | static String locationCountryKey = 'locationCountryKey'; 6 | static String locationCountryFilterKey = 'locationCountryFilterKey'; 7 | static String locationLookupButtonKey = 'locationLookupButton'; 8 | static String addThisForecastKey = 'addThisForecastButtonKey'; 9 | static String saveForecastButtonKey = 'saveForecastButtonKey'; 10 | static String deleteForecastButtonKey = 'deleteForecastButton'; 11 | } 12 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/bloc/app_bloc_observer.dart: -------------------------------------------------------------------------------- 1 | // import 'dart:developer' as developer; 2 | import 'package:bloc/bloc.dart'; 3 | 4 | class AppBlocObserver extends BlocObserver { 5 | @override 6 | void onEvent( 7 | Bloc bloc, 8 | Object? event, 9 | ) { 10 | // developer.log(event.toString(), name: 'AppBlocObserver'); 11 | super.onEvent(bloc, event); 12 | } 13 | 14 | @override 15 | void onChange( 16 | BlocBase base, 17 | Change change, 18 | ) { 19 | // developer.log(change.toString(), name: 'AppBlocObserver'); 20 | super.onChange(base, change); 21 | } 22 | 23 | @override 24 | void onTransition( 25 | Bloc bloc, 26 | Transition transition, 27 | ) { 28 | // developer.log(transition.toString(), name: 'AppBlocObserver'); 29 | super.onTransition(bloc, transition); 30 | } 31 | 32 | @override 33 | void onError( 34 | BlocBase base, 35 | Object error, 36 | StackTrace stackTrace, 37 | ) { 38 | /* 39 | developer.log( 40 | error.toString(), 41 | name: 'AppBlocObserver', 42 | error: error, 43 | stackTrace: stackTrace, 44 | ); 45 | */ 46 | 47 | super.onError(base, error, stackTrace); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/bloc/bloc.dart: -------------------------------------------------------------------------------- 1 | export 'app_bloc.dart'; 2 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/mocks/mock_storage.dart: -------------------------------------------------------------------------------- 1 | import 'package:hydrated_bloc/hydrated_bloc.dart'; 2 | 3 | class MockStorage implements Storage { 4 | @override 5 | dynamic read( 6 | String key, 7 | ) {} 8 | 9 | @override 10 | Future write( 11 | String key, 12 | dynamic value, 13 | ) async {} 14 | 15 | @override 16 | Future delete( 17 | String key, 18 | ) async {} 19 | 20 | @override 21 | Future clear() async {} 22 | } 23 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/mocks/mocks.dart: -------------------------------------------------------------------------------- 1 | export 'mock_storage.dart'; 2 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/utils/color_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension Extension on Color { 4 | Color darken([ 5 | double amount = 10, 6 | ]) { 7 | assert((1 <= amount) && (amount <= 100)); 8 | double calc = (1 - (amount / 100)); 9 | return Color.fromARGB( 10 | this.alpha, 11 | (this.red * calc).round(), 12 | (this.green * calc).round(), 13 | (this.blue * calc).round(), 14 | ); 15 | } 16 | 17 | Color brighten([ 18 | double amount = 10, 19 | ]) { 20 | assert((1 <= amount) && (amount <= 100)); 21 | double calc = (amount / 100); 22 | return Color.fromARGB( 23 | this.alpha, 24 | this.red + ((255 - this.red) * calc).round(), 25 | this.green + ((255 - this.green) * calc).round(), 26 | this.blue + ((255 - this.blue) * calc).round(), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/utils/connectivity_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:connectivity_plus/connectivity_plus.dart'; 2 | import 'package:flutter_weather/models/models.dart'; 3 | import 'package:flutter_weather/services/services.dart'; 4 | import 'package:http/http.dart' as http; 5 | 6 | Future hasConnectivity({ 7 | required http.Client client, 8 | required Config config, 9 | ConnectivityResult? result, 10 | }) async { 11 | if (result != null) { 12 | switch (result) { 13 | case ConnectivityResult.none: 14 | return false; 15 | 16 | case ConnectivityResult.mobile: 17 | case ConnectivityResult.wifi: 18 | default: 19 | break; 20 | } 21 | } 22 | 23 | http.Response connectivityResponse = await connectivityStatus( 24 | client: client, 25 | config: config, 26 | ); 27 | 28 | if ((connectivityResponse.statusCode == 200) && 29 | (connectivityResponse.body == 'ok')) { 30 | return true; 31 | } 32 | 33 | return false; 34 | } 35 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/utils/fab_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppFABLocation extends FloatingActionButtonLocation { 4 | FloatingActionButtonLocation location; 5 | double offsetX; 6 | double offsetY; 7 | 8 | AppFABLocation({ 9 | required this.location, 10 | required this.offsetX, 11 | required this.offsetY, 12 | }); 13 | 14 | @override 15 | Offset getOffset( 16 | ScaffoldPrelayoutGeometry scaffoldGeometry, 17 | ) { 18 | final Offset offset = location.getOffset(scaffoldGeometry); 19 | return Offset((offset.dx + offsetX), (offset.dy + offsetY)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/utils/geolocator_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:geolocator/geolocator.dart'; 2 | import 'package:permission_handler/permission_handler.dart'; 3 | 4 | Future requestLocationPermission() async { 5 | bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); 6 | if (!serviceEnabled) { 7 | return Future.value(false); 8 | } 9 | 10 | LocationPermission locationPermission = await Geolocator.checkPermission(); 11 | if (locationPermission == LocationPermission.denied) { 12 | Permission perm = Permission.location; 13 | PermissionStatus status = await perm.request(); 14 | return Future.value(status.isGranted); 15 | } 16 | 17 | return Future.value(true); 18 | } 19 | 20 | Future getPosition() async { 21 | if (!await requestLocationPermission()) { 22 | return Future.value(null); 23 | } 24 | 25 | return await Geolocator.getCurrentPosition(); 26 | } 27 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/utils/input_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/app/app_theme.dart'; 3 | 4 | getInputTextLabelStyle( 5 | FocusNode focusNode, 6 | ) => 7 | TextStyle( 8 | color: focusNode.hasFocus ? AppTheme.primaryColor : Colors.grey[400], 9 | ); 10 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/utils/math_utils.dart: -------------------------------------------------------------------------------- 1 | double round5({ 2 | required num number, 3 | num? offset, 4 | }) { 5 | if ((number % 5) == 0) { 6 | return (number + (offset?.toDouble() ?? 5.0)); 7 | } 8 | 9 | return (((number % 5.0) >= 2.5) 10 | ? (((number / 5.0).ceil() * 5.0)) 11 | : ((number / 5.0).ceil() * 5.0)) + 12 | (offset?.toDouble() ?? 0.0); 13 | } 14 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/utils/push_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_messaging/firebase_messaging.dart'; 2 | 3 | Future requestPushNotificationPermission() async { 4 | FirebaseMessaging messaging = FirebaseMessaging.instance; 5 | NotificationSettings settings = await messaging.requestPermission( 6 | alert: true, 7 | announcement: false, 8 | badge: true, 9 | carPlay: false, 10 | criticalAlert: false, 11 | provisional: false, 12 | sound: true, 13 | ); 14 | 15 | switch (settings.authorizationStatus) { 16 | case AuthorizationStatus.authorized: 17 | case AuthorizationStatus.provisional: 18 | return Future.value(true); 19 | 20 | default: 21 | return Future.value(false); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/utils/scroll_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/rendering.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | bool isScrolling( 5 | ScrollController controller, 6 | ) => 7 | ((controller.offset > controller.position.minScrollExtent) && 8 | (controller.offset < controller.position.maxScrollExtent)); 9 | 10 | bool isAtTop( 11 | ScrollController controller, 12 | ) => 13 | (controller.offset == controller.position.minScrollExtent); 14 | 15 | bool isAtBottom( 16 | ScrollController controller, 17 | ) => 18 | (controller.offset == controller.position.maxScrollExtent); 19 | 20 | bool isScrollingDown( 21 | ScrollController controller, 22 | ) => 23 | (controller.position.userScrollDirection == ScrollDirection.reverse); 24 | 25 | bool isScrollingUp( 26 | ScrollController controller, 27 | ) => 28 | (controller.position.userScrollDirection == ScrollDirection.forward); 29 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/utils/snackbar_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/enums/message_type.dart'; 3 | 4 | void showSnackbar( 5 | BuildContext context, 6 | String message, { 7 | MessageType messageType: MessageType.success, 8 | Color? textColor: Colors.white, 9 | Color? backgroundColor, 10 | double backgroundColorOpacity: 0.9, 11 | Duration duration: const Duration(milliseconds: 2000), 12 | }) => 13 | ScaffoldMessenger.of(context) 14 | ..hideCurrentSnackBar() 15 | ..showSnackBar( 16 | SnackBar( 17 | content: Text(message, style: TextStyle(color: textColor)), 18 | backgroundColor: (backgroundColor == null) 19 | ? messageType.color.withOpacity(backgroundColorOpacity) 20 | : backgroundColor.withOpacity(backgroundColorOpacity), 21 | duration: duration, 22 | ), 23 | ); 24 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/utils/utils.dart: -------------------------------------------------------------------------------- 1 | export 'chart_utils.dart'; 2 | export 'color_utils.dart'; 3 | export 'common_utils.dart'; 4 | export 'connectivity_utils.dart'; 5 | export 'date_utils.dart'; 6 | export 'device_utils.dart'; 7 | export 'fab_utils.dart'; 8 | export 'geolocator_utils.dart'; 9 | export 'input_utils.dart'; 10 | export 'math_utils.dart'; 11 | export 'push_utils.dart'; 12 | export 'scroll_utils.dart'; 13 | export 'snackbar_utils.dart'; 14 | export 'version_utils.dart'; 15 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/utils/version_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_weather/app/utils/utils.dart'; 2 | import 'package:version/version.dart'; 3 | 4 | String scrubVersion( 5 | String? version, 6 | ) { 7 | if (version.isNullOrEmpty()) { 8 | return ''; 9 | } 10 | 11 | String scrubbedVersion = version!.replaceAll(RegExp(r'[^0-9.+]'), ''); 12 | return scrubbedVersion; 13 | } 14 | 15 | Version? getAppVersion( 16 | String? appVersion, 17 | ) { 18 | if (appVersion.isNullOrEmpty()) { 19 | return null; 20 | } 21 | 22 | String scrubbedVersion = scrubVersion(appVersion); 23 | Version parsedVersion; 24 | 25 | if (scrubbedVersion.contains('+')) { 26 | parsedVersion = Version.parse(scrubbedVersion.split('+')[0]); 27 | } else { 28 | parsedVersion = Version.parse(scrubbedVersion); 29 | } 30 | 31 | return parsedVersion; 32 | } 33 | 34 | bool needsAppUpdate( 35 | String? appVersion, 36 | String? packageVersion, 37 | ) { 38 | if (appVersion.isNullOrEmpty() || packageVersion.isNullOrEmpty()) { 39 | return true; 40 | } 41 | 42 | Version parsedAppVersion = Version.parse(appVersion); 43 | Version parsedPackageVersion = Version.parse(packageVersion); 44 | return (parsedAppVersion > parsedPackageVersion); 45 | } 46 | 47 | bool isAppBeta( 48 | String appVersion, 49 | String packageVersion, 50 | ) { 51 | if (appVersion.isNullOrEmpty() || packageVersion.isNullOrEmpty()) { 52 | return false; 53 | } 54 | 55 | Version parsedAppVersion = Version.parse(appVersion); 56 | Version parsedPackageVersion = Version.parse(packageVersion); 57 | return (parsedPackageVersion > parsedAppVersion); 58 | } 59 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_checkbox_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_weather/app/app_theme.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | 6 | class AppCheckboxTile extends StatelessWidget { 7 | final String title; 8 | final bool checked; 9 | final Function(bool?)? onTap; 10 | 11 | const AppCheckboxTile({ 12 | Key? key, 13 | required this.title, 14 | required this.checked, 15 | this.onTap, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build( 20 | BuildContext context, 21 | ) => 22 | CheckboxListTile( 23 | title: Text( 24 | title, 25 | style: TextStyle( 26 | color: checked 27 | ? AppTheme.getRadioActiveColor( 28 | context.read().state.themeMode) 29 | : AppTheme.getRadioInactiveColor( 30 | context.read().state.themeMode), 31 | ), 32 | ), 33 | value: checked, 34 | onChanged: onTap, 35 | controlAffinity: ListTileControlAffinity.trailing, 36 | activeColor: AppTheme.primaryColor, 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_color_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_weather/app/app_localization.dart'; 4 | import 'package:flutter_weather/app/app_theme.dart'; 5 | import 'package:flutter_weather/app/bloc/bloc.dart'; 6 | import 'package:flutter_weather/forecast/forecast.dart'; 7 | 8 | class AppColorThemeToggle extends StatelessWidget { 9 | final Function callback; 10 | 11 | AppColorThemeToggle({ 12 | Key? key, 13 | required this.callback, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build( 18 | BuildContext context, 19 | ) { 20 | AppState state = context.read().state; 21 | return ((state.themeMode == ThemeMode.dark) || 22 | !hasForecasts(state.forecasts)) 23 | ? Container() 24 | : Container( 25 | padding: const EdgeInsets.symmetric(horizontal: 10.0), 26 | child: Tooltip( 27 | message: state.colorTheme 28 | ? AppLocalizations.of(context)!.colorThemeDisable 29 | : AppLocalizations.of(context)!.colorThemeEnable, 30 | child: Material( 31 | type: MaterialType.transparency, 32 | child: Container( 33 | height: 40.0, 34 | width: 40.0, 35 | child: InkWell( 36 | borderRadius: BorderRadius.circular(40.0), 37 | child: Icon( 38 | Icons.brightness_7, 39 | color: state.colorTheme 40 | ? Colors.white 41 | : AppTheme.getHintColor(state.themeMode), 42 | ), 43 | onTap: () => callback(), 44 | ), 45 | ), 46 | ), 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_day_night_switch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_switch/flutter_switch.dart'; 4 | import 'package:flutter_weather/app/app_theme.dart'; 5 | import 'package:flutter_weather/app/bloc/bloc.dart'; 6 | 7 | class AppDayNightSwitch extends StatelessWidget { 8 | final IconData activeIcon; 9 | final IconData inactiveIcon; 10 | final Function callback; 11 | 12 | AppDayNightSwitch({ 13 | Key? key, 14 | this.activeIcon: Icons.nightlight_round, 15 | this.inactiveIcon: Icons.wb_sunny, 16 | required this.callback, 17 | }) : super(key: key); 18 | 19 | @override 20 | Widget build( 21 | BuildContext context, 22 | ) { 23 | AppState state = context.read().state; 24 | return FlutterSwitch( 25 | width: 60.0, 26 | height: 28.0, 27 | toggleSize: 24.0, 28 | borderRadius: 24.0, 29 | padding: 1.0, 30 | value: (state.themeMode == ThemeMode.dark), 31 | activeToggleColor: AppTheme.primaryColor, 32 | inactiveToggleColor: 33 | state.colorTheme ? Colors.white : AppTheme.secondaryColor, 34 | activeSwitchBorder: Border.all( 35 | color: Colors.deepPurple[500]!, 36 | width: 2.0, 37 | ), 38 | inactiveSwitchBorder: Border.all( 39 | color: state.colorTheme ? Colors.white : Colors.grey[300]!, 40 | width: 2.0, 41 | ), 42 | activeColor: AppTheme.secondaryColor.withOpacity(0.5), 43 | inactiveColor: Colors.white.withOpacity(0.5), 44 | activeIcon: Icon( 45 | activeIcon, 46 | color: Colors.yellow[400], 47 | size: 16.0, 48 | ), 49 | inactiveIcon: Icon( 50 | inactiveIcon, 51 | color: Colors.amber[500], 52 | size: 16.0, 53 | ), 54 | onToggle: (bool val) => callback(), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_form_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/app/app_theme.dart'; 3 | 4 | class AppFormButton extends StatelessWidget { 5 | final Function()? onTap; 6 | final String? text; 7 | final Widget? icon; 8 | final Color? buttonColor; 9 | final Color? textColor; 10 | 11 | AppFormButton({ 12 | Key? key, 13 | this.onTap, 14 | this.text, 15 | this.icon, 16 | this.buttonColor, 17 | this.textColor, 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build( 22 | BuildContext context, 23 | ) => 24 | TextButton( 25 | style: TextButton.styleFrom( 26 | padding: const EdgeInsets.symmetric( 27 | horizontal: 10.0, 28 | vertical: 10.0, 29 | ), 30 | backgroundColor: 31 | (buttonColor == null) ? AppTheme.primaryColor : buttonColor, 32 | primary: Colors.white, 33 | onSurface: AppTheme.disabledTextColor, 34 | minimumSize: Size(100, 10), 35 | textStyle: TextStyle( 36 | color: Colors.white, 37 | ), 38 | ), 39 | child: (icon == null) 40 | ? (text == null) 41 | ? Container() 42 | : Text(text!, 43 | style: TextStyle( 44 | fontSize: 16.0, 45 | color: textColor, 46 | )) 47 | : Row( 48 | mainAxisSize: MainAxisSize.min, 49 | children: [ 50 | Padding( 51 | padding: EdgeInsets.only( 52 | right: (text == null) ? 0.0 : 5.0, 53 | ), 54 | child: icon), 55 | (text == null) 56 | ? Container() 57 | : Text(text!, 58 | style: TextStyle( 59 | fontSize: 16.0, 60 | color: textColor, 61 | )), 62 | ], 63 | ), 64 | onPressed: onTap, 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_label.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_weather/app/app_theme.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | 6 | class AppLabel extends StatelessWidget { 7 | final String text; 8 | final Color? colorThemeColor; 9 | 10 | const AppLabel({ 11 | Key? key, 12 | required this.text, 13 | this.colorThemeColor, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build( 18 | BuildContext context, 19 | ) => 20 | Container( 21 | decoration: BoxDecoration( 22 | color: Colors.black.withOpacity(0.1), 23 | borderRadius: BorderRadius.all(Radius.circular(10.0)), 24 | ), 25 | padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), 26 | child: Padding( 27 | padding: const EdgeInsets.symmetric( 28 | horizontal: 10.0, 29 | vertical: 2.0, 30 | ), 31 | child: Text( 32 | text, 33 | style: Theme.of(context).textTheme.subtitle2!.copyWith( 34 | color: context.read().state.colorTheme 35 | ? (colorThemeColor ?? AppTheme.primaryColor) 36 | : Colors.white, 37 | fontWeight: FontWeight.w400, 38 | ), 39 | ), 40 | ), 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_none_found.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/app/app_theme.dart'; 3 | 4 | class AppNoneFound extends StatelessWidget { 5 | final IconData icon; 6 | final double iconSize; 7 | final String? text; 8 | 9 | AppNoneFound({ 10 | this.icon: Icons.sentiment_dissatisfied, 11 | this.iconSize: 70.0, 12 | this.text, 13 | }); 14 | 15 | @override 16 | Widget build( 17 | BuildContext context, 18 | ) => 19 | Column( 20 | mainAxisSize: MainAxisSize.min, 21 | crossAxisAlignment: CrossAxisAlignment.center, 22 | children: [ 23 | Padding( 24 | padding: const EdgeInsets.only(bottom: 20.0), 25 | child: Container( 26 | width: iconSize, 27 | height: iconSize, 28 | decoration: BoxDecoration( 29 | color: Colors.white, 30 | shape: BoxShape.circle, 31 | ), 32 | child: Icon( 33 | icon, 34 | color: AppTheme.primaryColor, 35 | size: iconSize, 36 | ), 37 | ), 38 | ), 39 | (text == null) 40 | ? Container() 41 | : Text( 42 | text!, 43 | style: Theme.of(context).textTheme.headline3!.copyWith( 44 | fontSize: 30.0, 45 | ), 46 | textAlign: TextAlign.center, 47 | ), 48 | ], 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_pageview_scroll_physics.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/physics.dart'; 3 | 4 | class AppPageViewScrollPhysics extends ScrollPhysics { 5 | const AppPageViewScrollPhysics({ 6 | ScrollPhysics? parent, 7 | }) : super(parent: parent); 8 | 9 | @override 10 | AppPageViewScrollPhysics applyTo( 11 | ScrollPhysics? ancestor, 12 | ) => 13 | AppPageViewScrollPhysics( 14 | parent: buildParent(ancestor), 15 | ); 16 | 17 | @override 18 | SpringDescription get spring => const SpringDescription( 19 | mass: 80.0, 20 | stiffness: 100.0, 21 | damping: 1.0, 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_progress_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_form_bloc/flutter_form_bloc.dart'; 4 | import 'package:flutter_weather/app/app_theme.dart'; 5 | import 'package:flutter_weather/app/bloc/bloc.dart'; 6 | 7 | class AppProgressIndicator extends StatelessWidget { 8 | final Color? color; 9 | final double strokeWidth; 10 | 11 | const AppProgressIndicator({ 12 | Key? key, 13 | this.color, 14 | this.strokeWidth: 2.0, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build( 19 | BuildContext context, 20 | ) => 21 | CircularProgressIndicator( 22 | strokeWidth: strokeWidth, 23 | valueColor: 24 | AlwaysStoppedAnimation(color ?? getDefaultColor(context)), 25 | ); 26 | 27 | Color getDefaultColor( 28 | BuildContext context, 29 | ) => 30 | (context.read().state.themeMode == ThemeMode.dark) || 31 | context.read().state.colorTheme 32 | ? Colors.white 33 | : AppTheme.primaryColor; 34 | } 35 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_radio_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_form_bloc/flutter_form_bloc.dart'; 4 | import 'package:flutter_weather/app/app_theme.dart'; 5 | import 'package:flutter_weather/app/bloc/bloc.dart'; 6 | 7 | class AppRadioTile extends StatelessWidget { 8 | final String title; 9 | final T value; 10 | final T? groupValue; 11 | final Function(T?)? onTap; 12 | 13 | const AppRadioTile({ 14 | Key? key, 15 | required this.title, 16 | required this.value, 17 | this.groupValue, 18 | this.onTap, 19 | }) : assert(value != null), 20 | super(key: key); 21 | 22 | @override 23 | Widget build( 24 | BuildContext context, 25 | ) { 26 | ThemeMode themeMode = context.read().state.themeMode; 27 | 28 | return RadioListTile( 29 | title: Text( 30 | title, 31 | style: TextStyle( 32 | color: (value == groupValue) 33 | ? AppTheme.getRadioActiveColor(themeMode) 34 | : AppTheme.getRadioInactiveColor(themeMode), 35 | ), 36 | ), 37 | value: value, 38 | groupValue: groupValue, 39 | onChanged: onTap, 40 | controlAffinity: ListTileControlAffinity.trailing, 41 | activeColor: AppTheme.primaryColor, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_section_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_weather/app/app_theme.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | 6 | class AppSectionHeader extends StatelessWidget { 7 | final String text; 8 | final bool borderTop; 9 | final bool borderBottom; 10 | final List? options; 11 | final EdgeInsets padding; 12 | 13 | AppSectionHeader({ 14 | required this.text, 15 | this.borderTop: false, 16 | this.borderBottom: false, 17 | this.options, 18 | this.padding: const EdgeInsets.symmetric( 19 | horizontal: 16.0, 20 | vertical: 16.0, 21 | ), 22 | }); 23 | 24 | @override 25 | Widget build( 26 | BuildContext context, 27 | ) { 28 | AppState state = context.read().state; 29 | return Container( 30 | decoration: BoxDecoration( 31 | color: AppTheme.getSectionColor(state.themeMode), 32 | border: Border( 33 | bottom: BorderSide( 34 | color: AppTheme.getBorderColor( 35 | state.themeMode, 36 | colorTheme: state.colorTheme, 37 | ), 38 | width: borderBottom ? 1.0 : 0.0, 39 | ), 40 | top: BorderSide( 41 | color: AppTheme.getBorderColor( 42 | state.themeMode, 43 | colorTheme: state.colorTheme, 44 | ), 45 | width: borderTop ? 1.0 : 0.0, 46 | ), 47 | ), 48 | ), 49 | padding: padding, 50 | child: _buildContent(context), 51 | ); 52 | } 53 | 54 | Widget _buildContent( 55 | BuildContext context, 56 | ) => 57 | Row( 58 | mainAxisSize: MainAxisSize.max, 59 | children: [ 60 | Expanded( 61 | child: Text( 62 | text, 63 | style: Theme.of(context).textTheme.headline6, 64 | ), 65 | ), 66 | _buildOptions(), 67 | ], 68 | ); 69 | 70 | Widget _buildOptions() { 71 | if ((options == null) || options!.isEmpty) { 72 | return Container(); 73 | } 74 | 75 | return Container(child: Row(children: options!)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_temperature_display.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | import 'package:flutter_weather/forecast/forecast.dart'; 6 | 7 | class AppTemperatureDisplay extends StatelessWidget { 8 | final String temperature; 9 | final TextStyle? style; 10 | final num unitSizeFactor; 11 | 12 | AppTemperatureDisplay({ 13 | required this.temperature, 14 | this.style, 15 | this.unitSizeFactor: 3.5, 16 | }); 17 | 18 | @override 19 | Widget build( 20 | BuildContext context, 21 | ) => 22 | Row( 23 | mainAxisSize: MainAxisSize.max, 24 | mainAxisAlignment: MainAxisAlignment.start, 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | Text( 28 | temperature, 29 | style: style, 30 | ), 31 | Align( 32 | alignment: Alignment.topLeft, 33 | child: Text( 34 | getUnitSymbol(context.read().state.units.temperature), 35 | style: style!.copyWith( 36 | fontSize: (style!.fontSize! / unitSizeFactor), 37 | ), 38 | ), 39 | ), 40 | ], 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_transparent_route.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class TransparentRoute extends PageRoute { 4 | TransparentRoute({ 5 | required this.builder, 6 | RouteSettings? settings, 7 | }) : super(settings: settings, fullscreenDialog: false); 8 | 9 | final WidgetBuilder builder; 10 | 11 | @override 12 | bool get opaque => false; 13 | 14 | @override 15 | Color? get barrierColor => null; 16 | 17 | @override 18 | String? get barrierLabel => null; 19 | 20 | @override 21 | bool get maintainState => true; 22 | 23 | @override 24 | Duration get transitionDuration => Duration(milliseconds: 350); 25 | 26 | @override 27 | Widget buildPage( 28 | BuildContext context, 29 | Animation animation, 30 | Animation secondaryAnimation, 31 | ) => 32 | FadeTransition( 33 | opacity: Tween( 34 | begin: 0.0, 35 | end: 1.0, 36 | ).animate(animation), 37 | child: Semantics( 38 | scopesRoute: true, 39 | explicitChildNodes: true, 40 | child: builder(context), 41 | ), 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_ui_overlay_style.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | 6 | class AppUiOverlayStyle extends StatelessWidget { 7 | final Widget? child; 8 | final Color? systemNavigationBarColor; 9 | final Brightness? systemNavigationBarIconBrightness; 10 | 11 | AppUiOverlayStyle({ 12 | this.child, 13 | this.systemNavigationBarColor, 14 | this.systemNavigationBarIconBrightness, 15 | }); 16 | 17 | @override 18 | Widget build( 19 | BuildContext context, 20 | ) { 21 | AppState state = context.read().state; 22 | return AnnotatedRegion( 23 | value: SystemUiOverlayStyle( 24 | statusBarColor: Colors.transparent, 25 | statusBarBrightness: 26 | (state.themeMode == ThemeMode.light) && !state.colorTheme 27 | ? Brightness.light 28 | : Brightness.dark, 29 | statusBarIconBrightness: 30 | (state.themeMode == ThemeMode.light) && !state.colorTheme 31 | ? Brightness.dark 32 | : Brightness.light, 33 | systemNavigationBarColor: 34 | state.colorTheme && (systemNavigationBarColor != null) 35 | ? systemNavigationBarColor!.withOpacity(0.925) 36 | : Theme.of(context).appBarTheme.backgroundColor, 37 | systemNavigationBarIconBrightness: 38 | (systemNavigationBarIconBrightness != null) 39 | ? systemNavigationBarIconBrightness 40 | : (state.themeMode == ThemeMode.light) && !state.colorTheme 41 | ? Brightness.dark 42 | : Brightness.light, 43 | systemNavigationBarDividerColor: Colors.transparent, 44 | ), 45 | child: child!, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/app_ui_safe_area.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class AppUiSafeArea extends StatelessWidget { 5 | final Widget child; 6 | final bool top; 7 | final bool bottom; 8 | final Widget? topWidget; 9 | final Widget? bottomWidget; 10 | 11 | const AppUiSafeArea({ 12 | Key? key, 13 | required this.child, 14 | this.top: true, 15 | this.bottom: true, 16 | this.topWidget, 17 | this.bottomWidget, 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build( 22 | BuildContext context, 23 | ) => 24 | Column( 25 | mainAxisSize: MainAxisSize.min, 26 | children: [ 27 | if (top) SizedBox(height: MediaQuery.of(context).padding.top), 28 | ((topWidget == null) && (bottomWidget == null)) 29 | ? child 30 | : Expanded( 31 | child: Stack( 32 | children: [ 33 | child, 34 | if (topWidget != null) 35 | Align( 36 | alignment: Alignment.topCenter, 37 | child: topWidget, 38 | ), 39 | if (bottomWidget != null) 40 | Align( 41 | alignment: Alignment.bottomCenter, 42 | child: bottomWidget, 43 | ), 44 | ], 45 | ), 46 | ), 47 | if (bottom) SizedBox(height: MediaQuery.of(context).padding.bottom), 48 | ], 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/app/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'app_checkbox_tile.dart'; 2 | export 'app_color_theme.dart'; 3 | export 'app_day_night_switch.dart'; 4 | export 'app_fab.dart'; 5 | export 'app_form_button.dart'; 6 | export 'app_label.dart'; 7 | export 'app_none_found.dart'; 8 | export 'app_option_button.dart'; 9 | export 'app_pageview_scroll_physics.dart'; 10 | export 'app_progress_indicator.dart'; 11 | export 'app_radio_tile.dart'; 12 | export 'app_section_header.dart'; 13 | export 'app_temperature_display.dart'; 14 | export 'app_ui_overlay_style.dart'; 15 | export 'app_ui_safe_area.dart'; 16 | export 'app_update_dialog.dart'; 17 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/chart_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | 4 | enum ChartType { 5 | line, 6 | bar, 7 | } 8 | 9 | extension ChartTypeExtension on ChartType { 10 | String getText( 11 | BuildContext context, 12 | ) { 13 | switch (this) { 14 | case ChartType.bar: 15 | return AppLocalizations.of(context)!.chartBar; 16 | 17 | case ChartType.line: 18 | default: 19 | return AppLocalizations.of(context)!.chartLine; 20 | } 21 | } 22 | } 23 | 24 | ChartType getChartType( 25 | String? chartType, 26 | ) { 27 | switch (chartType) { 28 | case 'ChartType.bar': 29 | return ChartType.bar; 30 | 31 | case 'ChartType.line': 32 | default: 33 | return ChartType.line; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/crud_status.dart: -------------------------------------------------------------------------------- 1 | enum CRUDStatus { 2 | creating, 3 | created, 4 | reading, 5 | read, 6 | updating, 7 | updated, 8 | deleting, 9 | deleted, 10 | } 11 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/distance_unit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | 4 | enum DistanceUnit { 5 | km, 6 | mi, 7 | } 8 | 9 | extension DistanceUnitExtension on DistanceUnit { 10 | String get units { 11 | switch (this) { 12 | case DistanceUnit.km: 13 | return 'km'; 14 | 15 | case DistanceUnit.mi: 16 | default: 17 | return 'mi'; 18 | } 19 | } 20 | 21 | String getText( 22 | BuildContext context, 23 | ) { 24 | switch (this) { 25 | case DistanceUnit.km: 26 | return AppLocalizations.of(context)!.distanceKm; 27 | 28 | case DistanceUnit.mi: 29 | default: 30 | return AppLocalizations.of(context)!.distanceMi; 31 | } 32 | } 33 | } 34 | 35 | DistanceUnit getDistanceUnit( 36 | String? distanceUnit, 37 | ) { 38 | switch (distanceUnit) { 39 | case 'km': 40 | return DistanceUnit.km; 41 | 42 | case 'mi': 43 | default: 44 | return DistanceUnit.mi; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/enums.dart: -------------------------------------------------------------------------------- 1 | export 'chart_type.dart'; 2 | export 'crud_status.dart'; 3 | export 'distance_unit.dart'; 4 | export 'flavor.dart'; 5 | export 'hour_range.dart'; 6 | export 'lookup_status.dart'; 7 | export 'pressure_unit.dart'; 8 | export 'push_notifications.dart'; 9 | export 'refresh_status.dart'; 10 | export 'temperature_unit.dart'; 11 | export 'theme_mode.dart'; 12 | export 'update_period.dart'; 13 | export 'wind_speed_unit.dart'; 14 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/flavor.dart: -------------------------------------------------------------------------------- 1 | enum Flavor { 2 | dev, 3 | tst, 4 | prod, 5 | } 6 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/hour_range.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | 4 | enum HourRange { 5 | hours12, 6 | hours24, 7 | hours36, 8 | hours48, 9 | } 10 | 11 | extension HourRangeExtension on HourRange { 12 | int get hours { 13 | switch (this) { 14 | case HourRange.hours24: 15 | return 24; 16 | 17 | case HourRange.hours36: 18 | return 36; 19 | 20 | case HourRange.hours48: 21 | return 48; 22 | 23 | case HourRange.hours12: 24 | default: 25 | return 12; 26 | } 27 | } 28 | 29 | String getText( 30 | BuildContext context, 31 | ) { 32 | switch (this) { 33 | case HourRange.hours24: 34 | return AppLocalizations.of(context)!.hours24; 35 | 36 | case HourRange.hours36: 37 | return AppLocalizations.of(context)!.hours36; 38 | 39 | case HourRange.hours48: 40 | return AppLocalizations.of(context)!.hours48; 41 | 42 | case HourRange.hours12: 43 | default: 44 | return AppLocalizations.of(context)!.hours12; 45 | } 46 | } 47 | } 48 | 49 | HourRange getForecastHourRange( 50 | String? hourRange, 51 | ) { 52 | switch (hourRange) { 53 | case 'HourRange.hours24': 54 | return HourRange.hours24; 55 | 56 | case 'HourRange.hours36': 57 | return HourRange.hours36; 58 | 59 | case 'HourRange.hours48': 60 | return HourRange.hours48; 61 | 62 | case 'HourRange.hours12': 63 | default: 64 | return HourRange.hours12; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/lookup_status.dart: -------------------------------------------------------------------------------- 1 | enum LookupStatus { 2 | forecastFound, 3 | forecastNotFound, 4 | forecastConnectivity, 5 | } 6 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/message_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/app/app_theme.dart'; 3 | 4 | enum MessageType { 5 | info, 6 | success, 7 | warning, 8 | danger, 9 | } 10 | 11 | extension MessageTypeExtension on MessageType { 12 | Color get color { 13 | switch (this) { 14 | case MessageType.success: 15 | return AppTheme.successColor; 16 | 17 | case MessageType.warning: 18 | return AppTheme.warningColor; 19 | 20 | case MessageType.danger: 21 | return AppTheme.dangerColor; 22 | 23 | case MessageType.info: 24 | default: 25 | return AppTheme.infoColor; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/pressure_unit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | 4 | enum PressureUnit { 5 | hpa, 6 | inhg, 7 | } 8 | 9 | extension PressureUnitExtension on PressureUnit { 10 | String get units { 11 | switch (this) { 12 | case PressureUnit.hpa: 13 | return 'hpa'; 14 | 15 | case PressureUnit.inhg: 16 | default: 17 | return 'inhg'; 18 | } 19 | } 20 | 21 | String getText( 22 | BuildContext context, 23 | ) { 24 | switch (this) { 25 | case PressureUnit.hpa: 26 | return AppLocalizations.of(context)!.pressureHpa; 27 | 28 | case PressureUnit.inhg: 29 | default: 30 | return AppLocalizations.of(context)!.pressureInhg; 31 | } 32 | } 33 | } 34 | 35 | PressureUnit getPressureUnit( 36 | String? pressureUnit, 37 | ) { 38 | switch (pressureUnit) { 39 | case 'hpa': 40 | return PressureUnit.hpa; 41 | 42 | case 'inhg': 43 | default: 44 | return PressureUnit.inhg; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/push_notifications.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | 4 | enum PushNotification { 5 | off, 6 | currentLocation, 7 | savedLocation, 8 | } 9 | 10 | extension PushNotificationExtension on PushNotification { 11 | Map? getInfo({ 12 | BuildContext? context, 13 | }) { 14 | switch (this) { 15 | case PushNotification.currentLocation: 16 | return { 17 | 'id': 'current_location', 18 | 'text': (context == null) 19 | ? 'Current Location' 20 | : AppLocalizations.of(context)!.pushNotificationCurrent, 21 | 'subText': (context == null) 22 | ? 'Tap to update' 23 | : AppLocalizations.of(context)!.pushNotificationCurrentTap, 24 | }; 25 | 26 | case PushNotification.savedLocation: 27 | return { 28 | 'id': 'saved_location', 29 | 'text': (context == null) 30 | ? 'Saved Locations' 31 | : AppLocalizations.of(context)!.pushNotificationSaved, 32 | }; 33 | 34 | case PushNotification.off: 35 | default: 36 | return { 37 | 'id': 'off', 38 | 'text': (context == null) 39 | ? 'Off' 40 | : AppLocalizations.of(context)!.pushNotificationOff, 41 | }; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/refresh_status.dart: -------------------------------------------------------------------------------- 1 | enum RefreshStatus { 2 | refreshing, 3 | } 4 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/temperature_unit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | 4 | enum TemperatureUnit { 5 | celsius, 6 | fahrenheit, 7 | kelvin, 8 | } 9 | 10 | extension TemperatureUnitExtension on TemperatureUnit { 11 | String get units { 12 | switch (this) { 13 | case TemperatureUnit.celsius: 14 | return 'metric'; 15 | 16 | case TemperatureUnit.kelvin: 17 | return 'standard'; 18 | 19 | case TemperatureUnit.fahrenheit: 20 | default: 21 | return 'imperial'; 22 | } 23 | } 24 | 25 | String get unitSymbol { 26 | switch (this) { 27 | case TemperatureUnit.celsius: 28 | return '\u00B0C'; 29 | 30 | case TemperatureUnit.kelvin: 31 | return 'K'; 32 | 33 | case TemperatureUnit.fahrenheit: 34 | default: 35 | return '\u00B0F'; 36 | } 37 | } 38 | 39 | String getText( 40 | BuildContext context, 41 | ) { 42 | switch (this) { 43 | case TemperatureUnit.celsius: 44 | return AppLocalizations.of(context)!.celsius; 45 | 46 | case TemperatureUnit.kelvin: 47 | return AppLocalizations.of(context)!.kelvin; 48 | 49 | case TemperatureUnit.fahrenheit: 50 | default: 51 | return AppLocalizations.of(context)!.fahrenheit; 52 | } 53 | } 54 | } 55 | 56 | TemperatureUnit getTemperatureUnit( 57 | String? temperatureUnit, 58 | ) { 59 | switch (temperatureUnit) { 60 | case 'celsius': 61 | return TemperatureUnit.celsius; 62 | 63 | case 'kelvin': 64 | return TemperatureUnit.kelvin; 65 | 66 | case 'fahrenheit': 67 | default: 68 | return TemperatureUnit.fahrenheit; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/theme_mode.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | 4 | extension ThemeModeExtension on ThemeMode { 5 | String getText( 6 | BuildContext context, 7 | ) { 8 | switch (this) { 9 | case ThemeMode.light: 10 | return AppLocalizations.of(context)!.light; 11 | 12 | case ThemeMode.dark: 13 | default: 14 | return AppLocalizations.of(context)!.dark; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/update_period.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | 4 | enum UpdatePeriod { 5 | hour1, 6 | hour2, 7 | hour3, 8 | hour4, 9 | hour5, 10 | } 11 | 12 | extension UpdatePeriodExtension on UpdatePeriod { 13 | Map? getInfo({ 14 | BuildContext? context, 15 | }) { 16 | switch (this) { 17 | case UpdatePeriod.hour1: 18 | return { 19 | 'id': '1hr', 20 | 'text': (context == null) 21 | ? '1 hour' 22 | : AppLocalizations.of(context)!.updatePeriod1hr, 23 | 'minutes': 60, 24 | }; 25 | 26 | case UpdatePeriod.hour2: 27 | return { 28 | 'id': '2hrs', 29 | 'text': (context == null) 30 | ? '2 hours' 31 | : AppLocalizations.of(context)!.updatePeriod2hr, 32 | 'minutes': 120, 33 | }; 34 | 35 | case UpdatePeriod.hour3: 36 | return { 37 | 'id': '3hrs', 38 | 'text': (context == null) 39 | ? '3 hours' 40 | : AppLocalizations.of(context)!.updatePeriod3hr, 41 | 'minutes': 180, 42 | }; 43 | 44 | case UpdatePeriod.hour4: 45 | return { 46 | 'id': '4hrs', 47 | 'text': (context == null) 48 | ? '4 hours' 49 | : AppLocalizations.of(context)!.updatePeriod4hr, 50 | 'minutes': 240, 51 | }; 52 | 53 | case UpdatePeriod.hour5: 54 | return { 55 | 'id': '5hrs', 56 | 'text': (context == null) 57 | ? '5 hours' 58 | : AppLocalizations.of(context)!.updatePeriod5hr, 59 | 'minutes': 300, 60 | }; 61 | 62 | default: 63 | return null; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/enums/wind_speed_unit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | 4 | enum WindSpeedUnit { 5 | kmh, 6 | mph, 7 | ms, 8 | } 9 | 10 | extension WindSpeedUnitExtension on WindSpeedUnit { 11 | String get units { 12 | switch (this) { 13 | case WindSpeedUnit.kmh: 14 | return 'kmh'; 15 | 16 | case WindSpeedUnit.ms: 17 | return 'ms'; 18 | 19 | case WindSpeedUnit.mph: 20 | default: 21 | return 'mph'; 22 | } 23 | } 24 | 25 | String getText( 26 | BuildContext context, 27 | ) { 28 | switch (this) { 29 | case WindSpeedUnit.kmh: 30 | return AppLocalizations.of(context)!.speedKmh; 31 | 32 | case WindSpeedUnit.ms: 33 | return AppLocalizations.of(context)!.speedMs; 34 | 35 | case WindSpeedUnit.mph: 36 | default: 37 | return AppLocalizations.of(context)!.speedMph; 38 | } 39 | } 40 | } 41 | 42 | WindSpeedUnit getWindSpeedUnit( 43 | String? windSpeedUnit, 44 | ) { 45 | switch (windSpeedUnit) { 46 | case 'kmh': 47 | return WindSpeedUnit.kmh; 48 | 49 | case 'ms': 50 | return WindSpeedUnit.ms; 51 | 52 | case 'mph': 53 | default: 54 | return WindSpeedUnit.mph; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/bloc/bloc.dart: -------------------------------------------------------------------------------- 1 | export 'forecast_form_bloc.dart'; 2 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/forecast.dart: -------------------------------------------------------------------------------- 1 | export 'bloc/bloc.dart'; 2 | export 'forecast_extension.dart'; 3 | export 'forecast_form.dart'; 4 | export 'forecast_utils.dart'; 5 | export 'view/view.dart'; 6 | export 'widgets/widgets.dart'; 7 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/view/view.dart: -------------------------------------------------------------------------------- 1 | export 'forecast_alerts_view.dart'; 2 | export 'forecast_form_view.dart'; 3 | export 'forecast_view.dart'; 4 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_alert_description.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/app/widgets/widgets.dart'; 3 | import 'package:flutter_weather/forecast/forecast.dart'; 4 | 5 | class ForecastAlertDescription extends StatefulWidget { 6 | final String? description; 7 | 8 | ForecastAlertDescription({ 9 | Key? key, 10 | required this.description, 11 | }) : super(key: key); 12 | 13 | @override 14 | _ForecastAlertDescriptionState createState() => 15 | _ForecastAlertDescriptionState(); 16 | } 17 | 18 | class _ForecastAlertDescriptionState extends State { 19 | late List> lines; 20 | 21 | @override 22 | void initState() { 23 | lines = scrubAlertDescription(widget.description); 24 | super.initState(); 25 | } 26 | 27 | @override 28 | Widget build( 29 | BuildContext context, 30 | ) => 31 | Column( 32 | mainAxisSize: MainAxisSize.min, 33 | children: _buildContent(), 34 | ); 35 | 36 | List _buildContent() { 37 | List children = []; 38 | 39 | for (Map entry in lines) { 40 | if (entry['label'] == null) { 41 | children.add(_buildText(entry['text'])); 42 | } else { 43 | children.add( 44 | Column( 45 | mainAxisSize: MainAxisSize.min, 46 | children: [ 47 | _buildLabel(entry['label']), 48 | _buildText(entry['text']), 49 | ], 50 | ), 51 | ); 52 | } 53 | } 54 | 55 | return children; 56 | } 57 | 58 | Widget _buildLabel( 59 | String? label, 60 | ) => 61 | AppSectionHeader(text: label ?? ''); 62 | 63 | Widget _buildText( 64 | String? text, 65 | ) => 66 | Padding( 67 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), 68 | child: Align( 69 | alignment: Alignment.centerLeft, 70 | child: Text( 71 | text ?? '', 72 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 73 | height: 1.5, 74 | fontSize: 18.0, 75 | ), 76 | ), 77 | ), 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_condition.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/models/models.dart'; 3 | 4 | class ForecastCondition extends StatelessWidget { 5 | final ForecastDay currentDay; 6 | 7 | const ForecastCondition({ 8 | Key? key, 9 | required this.currentDay, 10 | }) : super(key: key); 11 | 12 | @override 13 | Widget build( 14 | BuildContext context, 15 | ) => 16 | Container( 17 | padding: const EdgeInsets.only(top: 10.0, bottom: 20.0), 18 | child: Text( 19 | currentDay.weather!.first.description!.toUpperCase(), 20 | style: Theme.of(context).textTheme.headline4!.copyWith( 21 | fontWeight: FontWeight.w300, 22 | ), 23 | ), 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_dew_point.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_weather/app/app_localization.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | import 'package:flutter_weather/forecast/forecast.dart'; 6 | import 'package:flutter_weather/models/models.dart'; 7 | import 'package:weather_icons/weather_icons.dart'; 8 | 9 | class ForecastDewPoint extends StatelessWidget { 10 | final ForecastDetails details; 11 | 12 | const ForecastDewPoint({ 13 | Key? key, 14 | required this.details, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build( 19 | BuildContext context, 20 | ) { 21 | AppState state = context.read().state; 22 | return Container( 23 | child: Row( 24 | mainAxisAlignment: MainAxisAlignment.center, 25 | children: [ 26 | ForecastMetaInfo( 27 | label: AppLocalizations.of(context)!.dewPoint, 28 | value: (details.current == null) 29 | ? '0' 30 | : getTemperature( 31 | details.current!.dewPoint, 32 | state.units.temperature, 33 | ).toString(), 34 | unit: getUnitSymbol(state.units.temperature), 35 | ), 36 | SizedBox( 37 | height: 30.0, 38 | width: 30.0, 39 | child: ForecastIcon( 40 | iconSize: 20.0, 41 | icon: WeatherIcons.raindrops, 42 | shadowColor: Colors.black26, 43 | ), 44 | ), 45 | ], 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_divider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_weather/app/app_theme.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | 6 | class ForecastDivider extends StatelessWidget { 7 | final EdgeInsets padding; 8 | 9 | const ForecastDivider({ 10 | Key? key, 11 | this.padding: const EdgeInsets.only( 12 | left: 10.0, 13 | right: 10.0, 14 | bottom: 20.0, 15 | top: 10.0, 16 | ), 17 | }) : super(key: key); 18 | 19 | @override 20 | Widget build( 21 | BuildContext context, 22 | ) => 23 | Padding( 24 | padding: padding, 25 | child: Divider( 26 | thickness: 2.0, 27 | color: AppTheme.getBorderColor( 28 | context.read().state.themeMode, 29 | colorTheme: context.read().state.colorTheme, 30 | ), 31 | ), 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_edit_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_weather/app/app_localization.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | import 'package:flutter_weather/forecast/forecast_utils.dart'; 6 | import 'package:flutter_weather/forecast/view/view.dart'; 7 | import 'package:flutter_weather/models/models.dart'; 8 | 9 | class ForecastEditButton extends StatelessWidget { 10 | const ForecastEditButton({ 11 | Key? key, 12 | }) : super(key: key); 13 | 14 | @override 15 | Widget build( 16 | BuildContext context, 17 | ) { 18 | AppState state = context.watch().state; 19 | return !hasForecasts(state.forecasts) 20 | ? Container() 21 | : Tooltip( 22 | message: AppLocalizations.of(context)!.editForecast, 23 | child: Material( 24 | type: MaterialType.transparency, 25 | child: Container( 26 | height: 40.0, 27 | width: 40.0, 28 | child: InkWell( 29 | borderRadius: BorderRadius.circular(40.0), 30 | child: const Icon(Icons.edit), 31 | onTap: () => _tapEdit(context, state), 32 | ), 33 | ), 34 | ), 35 | ); 36 | } 37 | 38 | void _tapEdit( 39 | BuildContext context, 40 | AppState state, 41 | ) { 42 | if (state.forecasts.length > state.selectedForecastIndex) { 43 | Forecast? forecast = state.forecasts[state.selectedForecastIndex]; 44 | context.read().add(SetActiveForecastId(forecast.id)); 45 | ScaffoldMessenger.of(context).hideCurrentSnackBar(); 46 | Navigator.push(context, ForecastFormView.route()); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_hour_display.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_weather/app/app_theme.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | import 'package:flutter_weather/app/utils/utils.dart'; 6 | import 'package:flutter_weather/forecast/forecast.dart'; 7 | import 'package:flutter_weather/models/models.dart'; 8 | 9 | class ForecastHourDisplay extends StatelessWidget { 10 | final ForecastHour hour; 11 | 12 | const ForecastHourDisplay({ 13 | Key? key, 14 | required this.hour, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build( 19 | BuildContext context, 20 | ) => 21 | Row( 22 | children: [ 23 | Text( 24 | formatHour(dateTime: hour.dt) ?? '', 25 | style: Theme.of(context).textTheme.headline5!.copyWith( 26 | shadows: context.read().state.colorTheme 27 | ? commonTextShadow() 28 | : null, 29 | ), 30 | ), 31 | Padding( 32 | padding: const EdgeInsets.only(left: 2.0), 33 | child: Text( 34 | formatHour(dateTime: hour.dt, format: 'a') ?? '', 35 | style: Theme.of(context).textTheme.subtitle2!.copyWith( 36 | color: AppTheme.getHintColor( 37 | context.read().state.themeMode, 38 | colorTheme: context.read().state.colorTheme, 39 | ), 40 | fontWeight: FontWeight.bold, 41 | ), 42 | ), 43 | ), 44 | ], 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_humidity.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | import 'package:flutter_weather/forecast/forecast.dart'; 4 | import 'package:flutter_weather/models/models.dart'; 5 | import 'package:weather_icons/weather_icons.dart'; 6 | 7 | class ForecastHumidity extends StatelessWidget { 8 | final ForecastDay currentDay; 9 | 10 | const ForecastHumidity({ 11 | Key? key, 12 | required this.currentDay, 13 | }) : super(key: key); 14 | 15 | @override 16 | Widget build( 17 | BuildContext context, 18 | ) => 19 | Container( 20 | child: Row( 21 | mainAxisAlignment: MainAxisAlignment.center, 22 | children: [ 23 | ForecastMetaInfo( 24 | label: AppLocalizations.of(context)!.humidity, 25 | value: currentDay.humidity.toString(), 26 | unit: '%', 27 | ), 28 | SizedBox( 29 | height: 30.0, 30 | width: 30.0, 31 | child: ForecastIcon( 32 | iconSize: 20.0, 33 | icon: WeatherIcons.humidity, 34 | shadowColor: Colors.black26, 35 | ), 36 | ), 37 | ], 38 | ), 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_last_updated.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | import 'package:flutter_weather/app/utils/utils.dart'; 4 | import 'package:flutter_weather/models/models.dart'; 5 | 6 | class ForecastLastUpdated extends StatelessWidget { 7 | final Forecast forecast; 8 | final Color? fillColor; 9 | final String shortFormat; 10 | final String longFormat; 11 | 12 | ForecastLastUpdated({ 13 | Key? key, 14 | required this.forecast, 15 | this.fillColor, 16 | this.shortFormat: 'h:mm a', 17 | this.longFormat: 'EEE, MMM d, yyyy @ h:mm a', 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build( 22 | BuildContext context, 23 | ) { 24 | DateTime? lastUpdated = forecast.lastUpdated; 25 | if (lastUpdated == null) { 26 | return Container(); 27 | } 28 | 29 | String formattedLastUpdated; 30 | 31 | if (lastUpdated.isToday()) { 32 | formattedLastUpdated = AppLocalizations.of(context)!.getLastUpdatedAt( 33 | formatDateTime( 34 | date: lastUpdated.toLocal(), 35 | format: shortFormat, 36 | )!, 37 | ); 38 | } else { 39 | formattedLastUpdated = AppLocalizations.of(context)!.getLastUpdatedOn( 40 | formatDateTime( 41 | date: lastUpdated.toLocal(), 42 | format: longFormat, 43 | )!, 44 | ); 45 | } 46 | 47 | return Center( 48 | child: Container( 49 | decoration: BoxDecoration( 50 | color: (fillColor == null) 51 | ? Colors.black.withOpacity(0.1) 52 | : fillColor!.withOpacity(0.2), 53 | borderRadius: BorderRadius.all(Radius.circular(10.0)), 54 | ), 55 | padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), 56 | margin: EdgeInsets.only( 57 | bottom: (MediaQuery.of(context).padding.bottom + 16.0), 58 | ), 59 | child: Text( 60 | formattedLastUpdated, 61 | style: Theme.of(context).textTheme.subtitle2, 62 | ), 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_options.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | import 'package:flutter_weather/app/widgets/widgets.dart'; 6 | import 'package:flutter_weather/forecast/forecast.dart'; 7 | 8 | class ForecastOptions extends StatelessWidget { 9 | static final double height = 50.0; 10 | 11 | @override 12 | Widget build( 13 | BuildContext context, 14 | ) => 15 | Container( 16 | height: (ForecastOptions.height + MediaQuery.of(context).padding.top), 17 | padding: EdgeInsets.only( 18 | left: 10.0, 19 | right: 10.0, 20 | top: MediaQuery.of(context).padding.top, 21 | ), 22 | child: Row( 23 | mainAxisAlignment: MainAxisAlignment.start, 24 | children: [ 25 | ForecastEditButton(), 26 | ForecastRefresh(), 27 | Expanded(child: Container()), 28 | AppColorThemeToggle( 29 | callback: () => context.read().add(ToggleColorTheme()), 30 | ), 31 | AppDayNightSwitch( 32 | callback: () => context.read().add(ToggleThemeMode()), 33 | ), 34 | ForecastSettingsButton(), 35 | ], 36 | ), 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_pressure.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_weather/app/app_localization.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | import 'package:flutter_weather/enums/enums.dart'; 6 | import 'package:flutter_weather/forecast/forecast.dart'; 7 | import 'package:flutter_weather/models/models.dart'; 8 | import 'package:weather_icons/weather_icons.dart'; 9 | 10 | class ForecastPressure extends StatelessWidget { 11 | final ForecastDay currentDay; 12 | 13 | const ForecastPressure({ 14 | Key? key, 15 | required this.currentDay, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build( 20 | BuildContext context, 21 | ) => 22 | Container( 23 | child: Row( 24 | mainAxisAlignment: MainAxisAlignment.center, 25 | children: [ 26 | ForecastMetaInfo( 27 | label: AppLocalizations.of(context)!.pressure, 28 | value: getPressure( 29 | currentDay.pressure, 30 | context.read().state.units.pressure, 31 | ).toString(), 32 | unit: 33 | context.read().state.units.pressure.getText(context), 34 | ), 35 | SizedBox( 36 | height: 30.0, 37 | width: 30.0, 38 | child: ForecastIcon( 39 | iconSize: 20.0, 40 | icon: WeatherIcons.barometer, 41 | shadowColor: Colors.black26, 42 | ), 43 | ), 44 | ], 45 | ), 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_settings_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | import 'package:flutter_weather/settings/view/view.dart'; 4 | 5 | class ForecastSettingsButton extends StatelessWidget { 6 | const ForecastSettingsButton({ 7 | Key? key, 8 | }) : super(key: key); 9 | 10 | @override 11 | Widget build( 12 | BuildContext context, 13 | ) => 14 | Container( 15 | padding: EdgeInsets.only(left: 10.0), 16 | child: Tooltip( 17 | message: AppLocalizations.of(context)!.settings, 18 | child: Material( 19 | type: MaterialType.transparency, 20 | child: Container( 21 | height: 40.0, 22 | width: 40.0, 23 | child: InkWell( 24 | borderRadius: BorderRadius.circular(40.0), 25 | child: const Icon(Icons.settings), 26 | onTap: () => _tapSettings(context), 27 | ), 28 | ), 29 | ), 30 | ), 31 | ); 32 | 33 | void _tapSettings( 34 | BuildContext context, 35 | ) { 36 | ScaffoldMessenger.of(context).hideCurrentSnackBar(); 37 | Navigator.push(context, SettingsView.route()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_uv_index.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/app/app_localization.dart'; 3 | import 'package:flutter_weather/app/utils/utils.dart'; 4 | import 'package:flutter_weather/forecast/forecast.dart'; 5 | import 'package:flutter_weather/models/models.dart'; 6 | import 'package:weather_icons/weather_icons.dart'; 7 | 8 | class ForecastUVIndex extends StatelessWidget { 9 | final ForecastDetails details; 10 | 11 | const ForecastUVIndex({ 12 | Key? key, 13 | required this.details, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build( 18 | BuildContext context, 19 | ) => 20 | Container( 21 | child: Row( 22 | mainAxisAlignment: MainAxisAlignment.center, 23 | children: [ 24 | ForecastMetaInfo( 25 | label: AppLocalizations.of(context)!.uvIndex, 26 | value: (details.current == null) 27 | ? '0' 28 | : (details.current!.uvi ?? 0) 29 | .toDouble() 30 | .formatDecimal(decimals: 1) 31 | .toString(), 32 | unit: '', // TODO! 33 | ), 34 | SizedBox( 35 | height: 30.0, 36 | width: 30.0, 37 | child: ForecastIcon( 38 | iconSize: 20.0, 39 | icon: WeatherIcons.day_sunny, 40 | shadowColor: Colors.black26, 41 | ), 42 | ), 43 | ], 44 | ), 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_visibility.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_weather/app/app_localization.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | import 'package:flutter_weather/enums/enums.dart'; 6 | import 'package:flutter_weather/forecast/forecast.dart'; 7 | import 'package:flutter_weather/models/models.dart'; 8 | import 'package:weather_icons/weather_icons.dart'; 9 | 10 | class ForecastVisibility extends StatelessWidget { 11 | final ForecastDetails details; 12 | 13 | const ForecastVisibility({ 14 | Key? key, 15 | required this.details, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build( 20 | BuildContext context, 21 | ) => 22 | Container( 23 | child: Row( 24 | mainAxisAlignment: MainAxisAlignment.center, 25 | children: [ 26 | ForecastMetaInfo( 27 | label: AppLocalizations.of(context)!.visibility, 28 | value: (details.current == null) 29 | ? '0' 30 | : getDistance( 31 | details.current!.visibility, 32 | context.read().state.units.distance, 33 | ).toString(), 34 | unit: 35 | context.read().state.units.distance.getText(context), 36 | ), 37 | SizedBox( 38 | height: 30.0, 39 | width: 30.0, 40 | child: ForecastIcon( 41 | iconSize: 20.0, 42 | icon: WeatherIcons.horizon, 43 | shadowColor: Colors.black26, 44 | ), 45 | ), 46 | ], 47 | ), 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_wind_direction.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_weather/app/bloc/bloc.dart'; 4 | 5 | class ForecastWindDirection extends StatelessWidget { 6 | final IconData icon; 7 | final double size; 8 | final Color? color; 9 | final Color shadowColor; 10 | final num? degree; 11 | 12 | ForecastWindDirection({ 13 | this.icon: Icons.navigation, 14 | this.size: 20.0, 15 | this.color, 16 | this.shadowColor: Colors.black38, 17 | this.degree, 18 | }); 19 | 20 | @override 21 | Widget build( 22 | BuildContext context, 23 | ) { 24 | AppState state = context.watch().state; 25 | if ((state.themeMode == ThemeMode.light) && !state.colorTheme) { 26 | return _rotate( 27 | degree ?? 0.0, 28 | Icon( 29 | Icons.navigation, 30 | color: color, 31 | size: size, 32 | ), 33 | ); 34 | } 35 | 36 | return Align( 37 | alignment: Alignment.center, 38 | child: Stack( 39 | children: [ 40 | Positioned( 41 | top: 1.0, 42 | left: 1.0, 43 | child: _rotate( 44 | degree ?? 0.0, 45 | Icon( 46 | Icons.navigation, 47 | color: shadowColor, 48 | size: size, 49 | ), 50 | ), 51 | ), 52 | _rotate( 53 | degree ?? 0.0, 54 | Icon( 55 | Icons.navigation, 56 | color: color, 57 | size: size, 58 | ), 59 | ), 60 | ], 61 | ), 62 | ); 63 | } 64 | 65 | Widget _rotate( 66 | num degree, 67 | Widget child, 68 | ) => 69 | RotationTransition( 70 | turns: AlwaysStoppedAnimation(degree / 360.0), 71 | child: child, 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/forecast_wind_speed.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_weather/app/app_localization.dart'; 5 | import 'package:flutter_weather/app/bloc/bloc.dart'; 6 | import 'package:flutter_weather/enums/enums.dart'; 7 | import 'package:flutter_weather/forecast/forecast.dart'; 8 | import 'package:flutter_weather/models/models.dart'; 9 | 10 | class ForecastWindSpeed extends StatefulWidget { 11 | final ForecastDay currentDay; 12 | 13 | ForecastWindSpeed({ 14 | Key? key, 15 | required this.currentDay, 16 | }) : super(key: key); 17 | 18 | @override 19 | State createState() => _ForecastWindSpeedState(); 20 | } 21 | 22 | class _ForecastWindSpeedState extends State { 23 | @override 24 | Widget build( 25 | BuildContext context, 26 | ) => 27 | Container( 28 | child: Row( 29 | mainAxisAlignment: MainAxisAlignment.center, 30 | children: [ 31 | ForecastMetaInfo( 32 | label: AppLocalizations.of(context)!.wind, 33 | value: getWindSpeed( 34 | widget.currentDay.speed, 35 | context.read().state.units.windSpeed, 36 | ).toString(), 37 | unit: context 38 | .read() 39 | .state 40 | .units 41 | .windSpeed 42 | .getText(context), 43 | ), 44 | _buildWindDirection( 45 | heading: context.read().state.compassEvent?.heading, 46 | ), 47 | ], 48 | ), 49 | ); 50 | 51 | Widget _buildWindDirection({ 52 | double? heading, 53 | }) => 54 | SizedBox( 55 | height: 20.0, 56 | width: 30.0, 57 | child: ForecastWindDirection( 58 | degree: getWindDirection( 59 | windDirection: widget.currentDay.deg ?? 0.0, 60 | heading: heading ?? 0.0, 61 | ), 62 | size: 20.0, 63 | ), 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/forecast/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'forecast_alert_button.dart'; 2 | export 'forecast_alert_description.dart'; 3 | export 'forecast_condition.dart'; 4 | export 'forecast_country_picker.dart'; 5 | export 'forecast_current_temp.dart'; 6 | export 'forecast_day_charts.dart'; 7 | export 'forecast_day_scroller.dart'; 8 | export 'forecast_detail_display.dart'; 9 | export 'forecast_dew_point.dart'; 10 | export 'forecast_display.dart'; 11 | export 'forecast_divider.dart'; 12 | export 'forecast_edit_button.dart'; 13 | export 'forecast_hi_lo.dart'; 14 | export 'forecast_hour_display.dart'; 15 | export 'forecast_hour_tile.dart'; 16 | export 'forecast_hours.dart'; 17 | export 'forecast_humidity.dart'; 18 | export 'forecast_icon.dart'; 19 | export 'forecast_last_updated.dart'; 20 | export 'forecast_location.dart'; 21 | export 'forecast_meta.dart'; 22 | export 'forecast_meta_info.dart'; 23 | export 'forecast_options.dart'; 24 | export 'forecast_pressure.dart'; 25 | export 'forecast_refresh.dart'; 26 | export 'forecast_settings_button.dart'; 27 | export 'forecast_sliver_header.dart'; 28 | export 'forecast_uv_index.dart'; 29 | export 'forecast_visibility.dart'; 30 | export 'forecast_wind_direction.dart'; 31 | export 'forecast_wind_speed.dart'; 32 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/lookup/bloc/bloc.dart: -------------------------------------------------------------------------------- 1 | export 'lookup_bloc.dart'; 2 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/lookup/bloc/lookup_events.dart: -------------------------------------------------------------------------------- 1 | part of 'lookup_bloc.dart'; 2 | 3 | abstract class LookupEvent extends Equatable { 4 | const LookupEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class LookupForecast extends LookupEvent { 11 | final Map lookupData; 12 | final TemperatureUnit temperatureUnit; 13 | 14 | const LookupForecast( 15 | this.lookupData, 16 | this.temperatureUnit, 17 | ); 18 | 19 | @override 20 | List get props => [lookupData, temperatureUnit]; 21 | } 22 | 23 | class ClearLookupForecast extends LookupEvent { 24 | const ClearLookupForecast(); 25 | 26 | @override 27 | List get props => []; 28 | } 29 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/lookup/bloc/lookup_state.dart: -------------------------------------------------------------------------------- 1 | part of 'lookup_bloc.dart'; 2 | 3 | @immutable 4 | class LookupState extends Equatable { 5 | final String? cityName; 6 | final String? postalCode; 7 | final String? countryCode; 8 | final Forecast? lookupForecast; 9 | final LookupStatus? status; 10 | 11 | LookupState({ 12 | this.cityName, 13 | this.postalCode, 14 | this.countryCode, 15 | this.lookupForecast, 16 | this.status, 17 | }); 18 | 19 | const LookupState._({ 20 | this.cityName, 21 | this.postalCode, 22 | this.countryCode, 23 | this.lookupForecast, 24 | this.status, 25 | }); 26 | 27 | const LookupState.initial() : this._(); 28 | 29 | const LookupState.clear() : this._(); 30 | 31 | LookupState copyWith({ 32 | Nullable? cityName, 33 | Nullable? postalCode, 34 | Nullable? countryCode, 35 | Nullable? lookupForecast, 36 | Nullable? status, 37 | }) => 38 | LookupState._( 39 | cityName: (cityName == null) ? this.cityName : cityName.value, 40 | postalCode: (postalCode == null) ? this.postalCode : postalCode.value, 41 | countryCode: 42 | (countryCode == null) ? this.countryCode : countryCode.value, 43 | lookupForecast: (lookupForecast == null) 44 | ? this.lookupForecast 45 | : lookupForecast.value, 46 | status: (status == null) ? this.status : status.value, 47 | ); 48 | 49 | @override 50 | List get props => [ 51 | cityName, 52 | postalCode, 53 | countryCode, 54 | lookupForecast, 55 | status, 56 | ]; 57 | 58 | @override 59 | String toString() => 60 | 'LookupState{cityName: $cityName, postalCode: $postalCode, ' + 61 | 'countryCode: $countryCode, lookupForecast: $lookupForecast, ' + 62 | 'status: $status}'; 63 | } 64 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/lookup/lookup.dart: -------------------------------------------------------------------------------- 1 | export 'bloc/bloc.dart'; 2 | export 'lookup_utils.dart'; 3 | export 'view/view.dart'; 4 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/lookup/lookup_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_weather/app/app_localization.dart'; 2 | 3 | String getLookupTitle( 4 | AppLocalizations? localization, 5 | num? currentPage, 6 | ) { 7 | if (localization == null) { 8 | return ''; 9 | } 10 | 11 | if ((currentPage != null) && (currentPage.toInt() == 1)) { 12 | return localization.country; 13 | } 14 | 15 | return localization.addForecast; 16 | } 17 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/lookup/view/view.dart: -------------------------------------------------------------------------------- 1 | export 'lookup_view.dart'; 2 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/main_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/app/app_config.dart'; 3 | import 'package:flutter_weather/app/app_root.dart'; 4 | import 'package:flutter_weather/app/mocks/mocks.dart'; 5 | import 'package:flutter_weather/enums/enums.dart'; 6 | import 'package:flutter_weather/models/models.dart'; 7 | import 'package:hydrated_bloc/hydrated_bloc.dart'; 8 | 9 | Future main() async { 10 | WidgetsFlutterBinding.ensureInitialized(); 11 | 12 | // Mock Storage 13 | Storage storage = MockStorage(); 14 | 15 | // TEST Environment Specific Configuration 16 | AppConfig config = AppConfig( 17 | flavor: Flavor.tst, 18 | config: Config.mock(), 19 | child: WeatherApp(), 20 | ); 21 | 22 | // Error listening 23 | FlutterError.onError = (FlutterErrorDetails details) async { 24 | print(details.exception); 25 | print(details.stack); 26 | }; 27 | 28 | HydratedBlocOverrides.runZoned( 29 | () => runApp(config), 30 | storage: storage, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/models/models.dart: -------------------------------------------------------------------------------- 1 | export 'config.dart'; 2 | export 'forecast.dart'; 3 | export 'notification.dart'; 4 | export 'units.dart'; 5 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/models/units.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_weather/enums/enums.dart'; 3 | 4 | class Units extends Equatable { 5 | final TemperatureUnit temperature; 6 | final WindSpeedUnit windSpeed; 7 | final PressureUnit pressure; 8 | final DistanceUnit distance; 9 | 10 | Units({ 11 | this.temperature: TemperatureUnit.fahrenheit, 12 | this.windSpeed: WindSpeedUnit.mph, 13 | this.pressure: PressureUnit.inhg, 14 | this.distance: DistanceUnit.mi, 15 | }); 16 | 17 | const Units.initial({ 18 | this.temperature: TemperatureUnit.fahrenheit, 19 | this.windSpeed: WindSpeedUnit.mph, 20 | this.pressure: PressureUnit.inhg, 21 | this.distance: DistanceUnit.mi, 22 | }); 23 | 24 | Units copyWith({ 25 | TemperatureUnit? temperature, 26 | WindSpeedUnit? windSpeed, 27 | PressureUnit? pressure, 28 | DistanceUnit? distance, 29 | }) => 30 | Units( 31 | temperature: temperature ?? this.temperature, 32 | windSpeed: windSpeed ?? this.windSpeed, 33 | pressure: pressure ?? this.pressure, 34 | distance: distance ?? this.distance, 35 | ); 36 | 37 | static Units fromJson( 38 | dynamic json, 39 | ) => 40 | (json == null) 41 | ? Units() 42 | : Units( 43 | temperature: getTemperatureUnit(json['temperatureUnit']), 44 | windSpeed: getWindSpeedUnit(json['windSpeedUnit']), 45 | pressure: getPressureUnit(json['pressureUnit']), 46 | distance: getDistanceUnit(json['distanceUnit']), 47 | ); 48 | 49 | Map toJson() => { 50 | 'temperature': temperature.units, 51 | 'windSpeed': windSpeed.units, 52 | 'pressure': pressure.units, 53 | 'distance': distance.units, 54 | }; 55 | 56 | @override 57 | List get props => [ 58 | temperature, 59 | windSpeed, 60 | pressure, 61 | distance, 62 | ]; 63 | 64 | @override 65 | String toString() => 66 | 'Units{temperature: $temperature, windSpeed: $windSpeed, ' + 67 | 'pressure: $pressure, distance: $distance}'; 68 | } 69 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/services/connectivity_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_weather/models/models.dart'; 2 | import 'package:http/http.dart' as http; 3 | 4 | Future connectivityStatus({ 5 | required http.Client client, 6 | required Config config, 7 | }) async => 8 | client.get(Uri.parse(config.appConnectivityStatus)); 9 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/services/firebase_remoteconfig_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_remote_config/firebase_remote_config.dart'; 2 | import 'package:sentry/sentry.dart'; 3 | 4 | class FirebaseRemoteConfigService { 5 | late FirebaseRemoteConfig _remoteConfig; 6 | 7 | Future initialize() async { 8 | try { 9 | await _fetchAndActivate(); 10 | } on Exception catch (exception, stackTrace) { 11 | _remoteConfig = FirebaseRemoteConfig 12 | .instance; // TODO! set the 'retry' flag in the state instead 13 | 14 | print('Remote config fetch throttled: $exception'); 15 | await Sentry.captureException(exception, stackTrace: stackTrace); 16 | } catch (exception, stackTrace) { 17 | _remoteConfig = FirebaseRemoteConfig 18 | .instance; // TODO! set the 'retry' flag in the state instead 19 | 20 | print('Unable to fetch remote config. Cached or default values will be ' 21 | 'used'); 22 | 23 | await Sentry.captureException(exception, stackTrace: stackTrace); 24 | } 25 | } 26 | 27 | Future _fetchAndActivate({ 28 | int fetchTimeoutSecs: 10, 29 | minimumFetchIntervalHours: 1, 30 | }) async { 31 | final FirebaseRemoteConfig initialRemoteConfig = 32 | FirebaseRemoteConfig.instance; 33 | 34 | await initialRemoteConfig.setConfigSettings(RemoteConfigSettings( 35 | fetchTimeout: Duration(seconds: fetchTimeoutSecs), 36 | minimumFetchInterval: Duration(hours: minimumFetchIntervalHours), 37 | )); 38 | 39 | await initialRemoteConfig.setDefaults(defaults); 40 | await initialRemoteConfig.fetch(); 41 | _remoteConfig = initialRemoteConfig; 42 | 43 | bool fetchActivated = await _remoteConfig.activate(); 44 | print('[FirebaseRemoteConfig] Fetched: $fetchActivated'); 45 | 46 | DateTime lastFetch = _remoteConfig.lastFetchTime; 47 | print('[FirebaseRemoteConfig] Last fetch: $lastFetch'); 48 | } 49 | 50 | Map get data => _remoteConfig.getAll(); 51 | 52 | Map get defaults => { 53 | 'app_version': '1.0.0', 54 | 'app_build': '1', 55 | 'refresh_timeout': 300000, 56 | 'default_country_code': 'us', 57 | 'supported_locales': 'en', 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/services/forecast_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_weather/forecast/forecast.dart'; 2 | import 'package:http/http.dart' as http; 3 | 4 | Future fetchDetailedForecast({ 5 | required http.Client client, 6 | required num longitude, 7 | required num latitude, 8 | }) async { 9 | Map params = Map(); 10 | params['lon'] = longitude.toString(); 11 | params['lat'] = latitude.toString(); 12 | return client.get(getDetailedUri(params)); 13 | } 14 | 15 | Future fetchCurrentForecastByCoords({ 16 | required http.Client client, 17 | required num longitude, 18 | required num latitude, 19 | }) async { 20 | Map params = Map(); 21 | params['lon'] = longitude.toString(); 22 | params['lat'] = latitude.toString(); 23 | return client.get(getCurrentApiUri(params)); 24 | } 25 | 26 | Future tryLookupForecast({ 27 | required http.Client client, 28 | required Map lookupData, 29 | }) async { 30 | Map params = buildLookupParams(lookupData); 31 | return client.get(getDailyApiUri(params)); 32 | } 33 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/services/push_notification_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_weather/app/app_config.dart'; 4 | import 'package:flutter_weather/enums/enums.dart'; 5 | import 'package:flutter_weather/models/models.dart'; 6 | import 'package:http/http.dart' as http; 7 | 8 | Future pushNotificationSave({ 9 | required String? deviceId, 10 | UpdatePeriod? period, 11 | PushNotification? pushNotification, 12 | NotificationExtras? pushNotificationExtras, 13 | Units? units, 14 | String? fcmToken, 15 | }) async { 16 | if (deviceId == null) { 17 | return null; 18 | } 19 | 20 | return http.post( 21 | Uri.parse(AppConfig.instance.config.appPushNotificationsSave), 22 | body: { 23 | 'device': deviceId, 24 | 'period': (period == null) ? null : period.getInfo()!['id'], 25 | 'pushNotification': 26 | (pushNotification == null) ? null : pushNotification.getInfo()!['id'], 27 | 'pushNotificationExtras': (pushNotificationExtras == null) 28 | ? null 29 | : json.encode(pushNotificationExtras.toJson()), 30 | 'units': (units == null) ? null : json.encode(units.toJson()), 31 | 'fcmToken': (fcmToken == null) ? null : fcmToken, 32 | }, 33 | ); 34 | } 35 | 36 | Future pushNotificationRemove({ 37 | required String? deviceId, 38 | }) async { 39 | if (deviceId == null) { 40 | return null; 41 | } 42 | 43 | return http.post( 44 | Uri.parse(AppConfig.instance.config.appPushNotificationsRemove), 45 | body: { 46 | 'device': deviceId, 47 | }, 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/services/services.dart: -------------------------------------------------------------------------------- 1 | export 'connectivity_service.dart'; 2 | export 'firebase_remoteconfig_service.dart'; 3 | export 'forecast_service.dart'; 4 | export 'push_notification_service.dart'; 5 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/settings/settings.dart: -------------------------------------------------------------------------------- 1 | export 'settings_utils.dart'; 2 | export 'view/view.dart'; 3 | export 'widgets/widgets.dart'; 4 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/settings/view/view.dart: -------------------------------------------------------------------------------- 1 | export 'settings_view.dart'; 2 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/settings/widgets/settings_auto_update.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | class SettingsAutoUpdate extends StatelessWidget { 5 | const SettingsAutoUpdate({ 6 | Key? key, 7 | }) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) => Row( 11 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 12 | children: [], 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/settings/widgets/settings_chart_type_picker.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_form_bloc/flutter_form_bloc.dart'; 4 | import 'package:flutter_weather/app/app_theme.dart'; 5 | import 'package:flutter_weather/app/bloc/bloc.dart'; 6 | import 'package:flutter_weather/app/widgets/widgets.dart'; 7 | import 'package:flutter_weather/enums/enums.dart'; 8 | 9 | class SettingsChartTypePicker extends StatefulWidget { 10 | SettingsChartTypePicker({ 11 | Key? key, 12 | }) : super(key: key); 13 | 14 | @override 15 | State createState() => _SettingsChartTypePickerState(); 16 | } 17 | 18 | class _SettingsChartTypePickerState extends State { 19 | @override 20 | Widget build( 21 | BuildContext context, 22 | ) => 23 | AppUiOverlayStyle( 24 | systemNavigationBarIconBrightness: 25 | context.watch().state.colorTheme ? Brightness.dark : null, 26 | child: Theme( 27 | data: (context.watch().state.themeMode == ThemeMode.dark) 28 | ? appDarkThemeData 29 | : appLightThemeData, 30 | child: Scaffold( 31 | body: _buildBody(), 32 | extendBody: true, 33 | extendBodyBehindAppBar: true, 34 | ), 35 | ), 36 | ); 37 | 38 | Widget _buildBody() { 39 | int count = 0; 40 | List chartTypes = ChartType.values; 41 | List widgets = []; 42 | ChartType _chartType = context.read().state.chartType; 43 | 44 | for (ChartType chartType in chartTypes) { 45 | widgets.add( 46 | AppRadioTile( 47 | title: chartType.getText(context), 48 | value: chartType, 49 | groupValue: _chartType, 50 | onTap: _tapChartType, 51 | ), 52 | ); 53 | 54 | if ((count + 1) < chartTypes.length) { 55 | widgets.add(Divider()); 56 | } 57 | 58 | count++; 59 | } 60 | 61 | return ListView(children: widgets); 62 | } 63 | 64 | void _tapChartType( 65 | ChartType? chartType, 66 | ) => 67 | context.read().add(SetChartType(chartType)); 68 | } 69 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/settings/widgets/settings_option.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_weather/app/app_theme.dart'; 4 | import 'package:flutter_weather/app/bloc/bloc.dart'; 5 | import 'package:flutter_weather/app/utils/utils.dart'; 6 | 7 | class SettingsOption extends StatelessWidget { 8 | final PageController pageController; 9 | final String title; 10 | final String trailingText; 11 | final int pageIndex; 12 | final Function()? onTapCallback; 13 | 14 | const SettingsOption({ 15 | Key? key, 16 | required this.pageController, 17 | required this.title, 18 | required this.trailingText, 19 | this.pageIndex: 1, 20 | this.onTapCallback, 21 | }) : super(key: key); 22 | 23 | @override 24 | Widget build( 25 | BuildContext context, 26 | ) => 27 | ListTile( 28 | contentPadding: const EdgeInsets.only(left: 16.0, right: 10.0), 29 | title: Text( 30 | title, 31 | style: Theme.of(context).textTheme.subtitle1, 32 | ), 33 | trailing: Row( 34 | mainAxisSize: MainAxisSize.min, 35 | children: [ 36 | Text( 37 | trailingText, 38 | style: Theme.of(context).textTheme.subtitle1, 39 | ), 40 | Padding( 41 | padding: const EdgeInsets.only(left: 8.0), 42 | child: Icon( 43 | Icons.chevron_right, 44 | color: AppTheme.getHintColor( 45 | context.read().state.themeMode, 46 | )! 47 | .withOpacity(0.3), 48 | ), 49 | ), 50 | ], 51 | ), 52 | onTap: () async { 53 | if (onTapCallback != null) { 54 | onTapCallback!(); 55 | } 56 | 57 | await animatePage(pageController, page: pageIndex); 58 | }, 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /apps/mobile_flutter/lib/settings/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'settings_auto_update.dart'; 2 | export 'settings_chart_type_picker.dart'; 3 | export 'settings_distance_units_picker.dart'; 4 | export 'settings_hour_range_picker.dart'; 5 | export 'settings_open_source_info.dart'; 6 | export 'settings_option.dart'; 7 | export 'settings_pressure_units_picker.dart'; 8 | export 'settings_push_notification_picker.dart'; 9 | export 'settings_temperature_units_picker.dart'; 10 | export 'settings_theme_mode_picker.dart'; 11 | export 'settings_update_period_picker.dart'; 12 | export 'settings_version_status_text.dart'; 13 | export 'settings_wind_speed_units_picker.dart'; 14 | -------------------------------------------------------------------------------- /apps/mobile_flutter/test/app/utils/common_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_weather/app/utils/utils.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('Should be an integer', () { 6 | expect(isInteger(1), true); 7 | expect(isInteger(1.0), true); 8 | expect(isInteger(-1), true); 9 | expect(isInteger(-1.0), true); 10 | }); 11 | 12 | test('Should NOT be an integer', () { 13 | expect(isInteger(0.5), false); 14 | expect(isInteger(1.5), false); 15 | expect(isInteger(-0.5), false); 16 | expect(isInteger(-1.5), false); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /apps/mobile_flutter/test/app/utils/date_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_weather/app/utils/utils.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('Should format an epoch date', () { 6 | expect(formatEpoch(epoch: 1626386790), '7/15/21 at 10:06 PM'); 7 | expect(formatEpoch(epoch: 1626386790, format: 'M/d/yy'), '7/15/21'); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /apps/mobile_flutter/test/app/utils/math_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_weather/app/utils/utils.dart'; 2 | import 'package:flutter_weather/enums/enums.dart'; 3 | import 'package:flutter_weather/forecast/forecast.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | test('Should be 90', () { 8 | expect(round5(number: 85.0), 90.0); 9 | expect(round5(number: 86.0), 90.0); 10 | expect(round5(number: 87.0), 90.0); 11 | expect(round5(number: 88.0), 90.0); 12 | expect(round5(number: 89.0), 90.0); 13 | }); 14 | 15 | test('Should be 95', () { 16 | expect(round5(number: 90.0), 95.0); 17 | expect(round5(number: 91.0), 95.0); 18 | expect(round5(number: 92.0), 95.0); 19 | expect(round5(number: 93.0), 95.0); 20 | expect(round5(number: 94.0), 95.0); 21 | 22 | num temp = getTemperature(305.372, TemperatureUnit.fahrenheit); // 90.0F 23 | expect(round5(number: temp), 95.0); 24 | 25 | temp = getTemperature(305.928, TemperatureUnit.fahrenheit); // 91.0F 26 | expect(round5(number: temp), 95.0); 27 | 28 | temp = getTemperature(306.483, TemperatureUnit.fahrenheit); // 92.0F 29 | expect(round5(number: temp), 95.0); 30 | 31 | temp = getTemperature(307.039, TemperatureUnit.fahrenheit); // 93.0F 32 | expect(round5(number: temp), 95.0); 33 | 34 | temp = getTemperature(307.594, TemperatureUnit.fahrenheit); // 94.0F 35 | expect(round5(number: temp), 95.0); 36 | }); 37 | 38 | test('Should be 100', () { 39 | expect(round5(number: 95.0), 100.0); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /apps/mobile_flutter/test/app/utils/version_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_weather/app/utils/utils.dart'; 2 | import 'package:test/test.dart'; 3 | import 'package:version/version.dart'; 4 | 5 | void main() { 6 | test('Should scrub the version', () { 7 | expect(scrubVersion(null), ''); 8 | expect(scrubVersion(''), ''); 9 | expect(scrubVersion('1.0.0'), '1.0.0'); 10 | expect(scrubVersion('1.0.0+abc123'), '1.0.0+123'); 11 | expect(scrubVersion('1.0.0+00001beta'), '1.0.0+00001'); 12 | }); 13 | 14 | test('Should get the app version', () { 15 | expect(getAppVersion(null), null); 16 | expect(getAppVersion('1.0.0'), Version(1, 0, 0)); 17 | expect(getAppVersion('1.0.0+abc123'), Version(1, 0, 0)); 18 | expect(getAppVersion('1.0.0+00001beta'), Version(1, 0, 0)); 19 | }); 20 | 21 | test('Should need to update app', () { 22 | expect(needsAppUpdate('1.0.0', null), true); 23 | expect(needsAppUpdate(null, '1.0.0'), true); 24 | expect(needsAppUpdate('1.0.0', '0.9.0'), true); 25 | expect(needsAppUpdate('1.0.0+abc123', '0.9.0'), true); 26 | }); 27 | 28 | test('Should NOT need to update app', () { 29 | expect(needsAppUpdate('1.0.0', '1.0.0'), false); 30 | expect(needsAppUpdate('1.0.0', '1.0.1'), false); 31 | expect(needsAppUpdate('1.0.0+abc123', '1.0.1'), false); 32 | expect(needsAppUpdate('1.0.0', '99.99.99'), false); 33 | }); 34 | 35 | test('Should be a beta version', () { 36 | expect(isAppBeta('1.0.0', '1.0.1'), true); 37 | expect(isAppBeta('1.0.0', '99.99.99'), true); 38 | }); 39 | 40 | test('Should NOT be a beta version', () { 41 | expect(isAppBeta('1.0.0', '0.9.0'), false); 42 | expect(isAppBeta('1.0.0', '1.0.0'), false); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /apps/mobile_flutter/test/forecast/view/forecast_form_view_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:flutter_weather/app/app_keys.dart'; 5 | import 'package:flutter_weather/app/bloc/bloc.dart'; 6 | import 'package:flutter_weather/forecast/view/forecast_form_view.dart'; 7 | import 'package:mocktail/mocktail.dart'; 8 | 9 | import '../../utils/pump_utils.dart'; 10 | 11 | class MockAppBloc extends MockBloc implements AppBloc {} 12 | 13 | class FakeAppEvent extends Fake implements AppEvent {} 14 | 15 | class FakeAppState extends Fake implements AppState {} 16 | 17 | void main() { 18 | setUpAll(() { 19 | registerFallbackValue(FakeAppEvent()); 20 | registerFallbackValue(FakeAppState()); 21 | }); 22 | 23 | group('SettingsVersionStatusText', () { 24 | late AppBloc _appBloc; 25 | 26 | setUp(() { 27 | _appBloc = MockAppBloc(); 28 | when(() => _appBloc.state).thenReturn(AppState()); 29 | }); 30 | 31 | testWidgets('Should have \'Edit Forecast\' appbar text', 32 | (WidgetTester tester) async { 33 | await tester.pumpApp( 34 | ForecastFormView(), 35 | appBloc: _appBloc, 36 | ); 37 | 38 | await tester.pumpAndSettle(); 39 | expect(find.text('Edit Forecast'), findsOneWidget); 40 | }); 41 | 42 | testWidgets('Should have \'Country\' appbar text', 43 | (WidgetTester tester) async { 44 | await tester.pumpApp( 45 | ForecastFormView(), 46 | appBloc: _appBloc, 47 | ); 48 | 49 | await tester.pumpAndSettle(); 50 | await tester.tap( 51 | find.byKey(Key(AppKeys.locationCountryKey)), 52 | warnIfMissed: false, 53 | ); 54 | 55 | expect(find.text('Country'), findsOneWidget); 56 | }); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /apps/mobile_flutter/test/lookup/lookup_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_weather/app/app_localization.dart'; 2 | import 'package:flutter_weather/lookup/lookup.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | class MockAppLocalizations extends Mock implements AppLocalizations { 7 | String get addForecast => 'Add Forecast'; 8 | String get country => 'Country'; 9 | } 10 | 11 | void main() { 12 | late AppLocalizations _localizations; 13 | 14 | setUp(() { 15 | _localizations = MockAppLocalizations(); 16 | }); 17 | 18 | test('Should have NOT have a title', () { 19 | expect(getLookupTitle(null, null), ''); 20 | }); 21 | 22 | test('Should have \'add forecast\' title', () { 23 | expect(getLookupTitle(_localizations, 0), 'Add Forecast'); 24 | }); 25 | 26 | test('Should have \'add forecast\' title', () { 27 | expect(getLookupTitle(_localizations, null), 'Add Forecast'); 28 | }); 29 | 30 | test('Should have \'country\' title', () { 31 | expect(getLookupTitle(_localizations, 1), 'Country'); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /apps/mobile_flutter/test/utils/pump_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:flutter_weather/app/bloc/bloc.dart'; 7 | import 'package:flutter_weather/forecast/bloc/forecast_form_bloc.dart'; 8 | import 'package:flutter_weather/lookup/lookup.dart'; 9 | import 'package:mocktail/mocktail.dart'; 10 | 11 | import 'test_utils.dart'; 12 | 13 | class MockAppBloc extends MockBloc implements AppBloc {} 14 | 15 | class MockLookupBloc extends MockBloc 16 | implements LookupBloc {} 17 | 18 | class FakeAppEvent extends Fake implements AppEvent {} 19 | 20 | class FakeAppState extends Fake implements AppState {} 21 | 22 | class FakeLookupEvent extends Fake implements LookupEvent {} 23 | 24 | class FakeLookupState extends Fake implements LookupState {} 25 | 26 | extension PumpApp on WidgetTester { 27 | Future pumpApp( 28 | Widget widget, { 29 | ForecastFormBloc? forecastFormBloc, 30 | AppBloc? appBloc, 31 | LookupBloc? lookupBloc, 32 | }) async { 33 | registerFallbackValue(FakeAppEvent()); 34 | registerFallbackValue(FakeAppState()); 35 | registerFallbackValue(FakeLookupEvent()); 36 | registerFallbackValue(FakeLookupState()); 37 | 38 | return await pumpWidget( 39 | MultiBlocProvider( 40 | providers: [ 41 | BlocProvider.value(value: forecastFormBloc ?? ForecastFormBloc()), 42 | BlocProvider.value(value: appBloc ?? MockAppBloc()), 43 | BlocProvider.value(value: lookupBloc ?? MockLookupBloc()), 44 | ], 45 | child: buildFrame( 46 | buildContent: (BuildContext context) => widget, 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /apps/mobile_flutter/test/utils/test_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_weather/app/app_config.dart'; 3 | import 'package:flutter_weather/app/app_localization.dart'; 4 | import 'package:flutter_weather/enums/enums.dart'; 5 | import 'package:flutter_weather/models/models.dart'; 6 | 7 | Widget buildFrame({ 8 | required WidgetBuilder buildContent, 9 | }) => 10 | AppConfig( 11 | flavor: Flavor.tst, 12 | config: Config.mock(), 13 | child: MaterialApp( 14 | localizationsDelegates: [ 15 | AppLocalizationsDelegate(), 16 | FallbackCupertinoLocalisationsDelegate(), 17 | ], 18 | onGenerateRoute: (RouteSettings settings) => MaterialPageRoute( 19 | builder: (BuildContext context) => buildContent(context), 20 | ), 21 | ), 22 | ); 23 | -------------------------------------------------------------------------------- /apps/mobile_flutter/test_driver/integration_test.dart: -------------------------------------------------------------------------------- 1 | // @dart=2.10 2 | import 'package:integration_test/integration_test_driver.dart'; 3 | 4 | Future main() => integrationDriver(); 5 | -------------------------------------------------------------------------------- /apps/web_ng/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /apps/web_ng/README.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /apps/web_ng/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'web_ng', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | astTransformers: { 10 | before: [ 11 | 'jest-preset-angular/build/InlineFilesTransformer', 12 | 'jest-preset-angular/build/StripStylesTransformer', 13 | ], 14 | }, 15 | }, 16 | }, 17 | coverageDirectory: '../../coverage/apps/web_ng', 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 20 | 'jest-preset-angular/build/AngularSnapshotSerializer.js', 21 | 'jest-preset-angular/build/HTMLCommentSerializer.js', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /apps/web_ng/src/app/app.component.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/web_ng/src/app/app.component.html -------------------------------------------------------------------------------- /apps/web_ng/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | /* styles here */ -------------------------------------------------------------------------------- /apps/web_ng/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing' 2 | import { AppComponent } from './app.component' 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | declarations: [AppComponent], 8 | }).compileComponents() 9 | }) 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent) 13 | const app = fixture.componentInstance 14 | expect(app).toBeTruthy() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /apps/web_ng/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'flutter-weather-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent {} 9 | -------------------------------------------------------------------------------- /apps/web_ng/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser' 2 | import { NgModule } from '@angular/core' 3 | import { AppComponent } from './app.component' 4 | 5 | @NgModule({ 6 | declarations: [AppComponent], 7 | imports: [BrowserModule], 8 | providers: [], 9 | bootstrap: [AppComponent] 10 | }) 11 | export class AppModule {} 12 | -------------------------------------------------------------------------------- /apps/web_ng/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/web_ng/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/web_ng/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | } 4 | -------------------------------------------------------------------------------- /apps/web_ng/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false 3 | } 4 | -------------------------------------------------------------------------------- /apps/web_ng/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/apps/web_ng/src/favicon.ico -------------------------------------------------------------------------------- /apps/web_ng/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flutter Weather 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/web_ng/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core' 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' 3 | import { AppModule } from './app/app.module' 4 | import { environment } from './environments/environment' 5 | 6 | if (environment.production) { 7 | enableProdMode() 8 | } 9 | 10 | platformBrowserDynamic() 11 | .bootstrapModule(AppModule) 12 | .catch((err) => console.error(err)) 13 | -------------------------------------------------------------------------------- /apps/web_ng/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* styles here */ -------------------------------------------------------------------------------- /apps/web_ng/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular' 2 | -------------------------------------------------------------------------------- /apps/web_ng/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/web_ng/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/web_ng/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.editor.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /apps/web_ng/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/web_ng/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "flutterWeather", "camelCase"], 5 | "component-selector": [true, "element", "flutter-weather", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/web_ng_e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "modifyObstructiveCode": false, 6 | "pluginsFile": "./src/plugins/index", 7 | "supportFile": "./src/support/index.ts", 8 | "video": true, 9 | "videosFolder": "../../dist/cypress/apps/web_ng_e2e/videos", 10 | "screenshotsFolder": "../../dist/cypress/apps/web_ng_e2e/screenshots", 11 | "chromeWebSecurity": false 12 | } 13 | -------------------------------------------------------------------------------- /apps/web_ng_e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /apps/web_ng_e2e/src/integration/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { getGreeting } from '../support/app.po'; 2 | 3 | describe('web_ng', () => { 4 | beforeEach(() => cy.visit('/')); 5 | 6 | it('should display welcome message', () => { 7 | // Custom command example, see `../support/commands.ts` file 8 | cy.login('my-email@something.com', 'myPassword'); 9 | 10 | // Function helper example, see `../support/app.po.ts` file 11 | getGreeting().contains('Welcome to web_ng!'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /apps/web_ng_e2e/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor'); 15 | 16 | module.exports = (on, config) => { 17 | // `on` is used to hook into various events Cypress emits 18 | // `config` is the resolved Cypress config 19 | 20 | // Preprocess Typescript file using Nx helper 21 | on('file:preprocessor', preprocessTypescript(config)); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/web_ng_e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /apps/web_ng_e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | declare namespace Cypress { 12 | interface Chainable { 13 | login(email: string, password: string): void; 14 | } 15 | } 16 | // 17 | // -- This is a parent command -- 18 | Cypress.Commands.add('login', (email, password) => { 19 | console.log('Custom command example: Login', email, password); 20 | }); 21 | // 22 | // -- This is a child command -- 23 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 24 | // 25 | // 26 | // -- This is a dual command -- 27 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 28 | // 29 | // 30 | // -- This will overwrite an existing command -- 31 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 32 | -------------------------------------------------------------------------------- /apps/web_ng_e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /apps/web_ng_e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"] 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/web_ng_e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.e2e.json" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/web_ng_e2e/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "linterOptions": { "exclude": ["!**/*"] }, 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /docs/images/app_store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/docs/images/app_store.png -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/play_store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/docs/images/play_store.png -------------------------------------------------------------------------------- /docs/images/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/docs/images/screen1.png -------------------------------------------------------------------------------- /docs/images/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/docs/images/screen2.png -------------------------------------------------------------------------------- /docs/images/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/docs/images/screen3.png -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist/apps/web_ng", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | }, 16 | "remoteconfig": { 17 | "template": "firebase-remote-config.json" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /git-version.yml: -------------------------------------------------------------------------------- 1 | assembly-versioning-scheme: MajorMinorPatch 2 | mode: ContinuousDeployment 3 | continuous-delivery-fallback-tag: '' 4 | next-version: 1.7.4 5 | branches: 6 | master: 7 | regex: (^main$|^origin\/main$) 8 | source-branches: [] 9 | tag: '' 10 | release: 11 | tag: 'beta' 12 | increment: Patch 13 | prevent-increment-of-merged-branch-version: true 14 | track-merge-target: false 15 | tracks-release-branches: false 16 | is-release-branch: true 17 | pre-release-weight: 1000 18 | source-branches: 19 | - main 20 | feature: 21 | tag: 'beta' 22 | increment: Inherit 23 | prevent-increment-of-merged-branch-version: false 24 | bugfix: 25 | tag: 'beta' 26 | increment: Inherit 27 | prevent-increment-of-merged-branch-version: false 28 | regex: bugfix(es)?[/-] 29 | source-branches: 30 | - release 31 | ignore: 32 | sha: [] 33 | merge-message-formats: {} 34 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: ['/apps/web_ng'], 3 | }; 4 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset'); 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/libs/.gitkeep -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "flutter-weather", 3 | "affected": { 4 | "defaultBase": "main" 5 | }, 6 | "implicitDependencies": { 7 | "workspace.json": "*", 8 | "package.json": { 9 | "dependencies": "*", 10 | "devDependencies": "*" 11 | }, 12 | "tsconfig.base.json": "*", 13 | "tslint.json": "*", 14 | ".eslintrc.json": "*", 15 | "nx.json": "*" 16 | }, 17 | "tasksRunnerOptions": { 18 | "default": { 19 | "runner": "@nrwl/workspace/tasks-runners/default", 20 | "options": { 21 | "cacheableOperations": ["build", "lint", "test", "e2e"] 22 | } 23 | } 24 | }, 25 | "projects": { 26 | "mobile_flutter": { 27 | "tags": [] 28 | }, 29 | "firebase": { 30 | "tags": [] 31 | }, 32 | "web_ng": { 33 | "tags": [] 34 | }, 35 | "web_ng_e2e": { 36 | "tags": [], 37 | "implicitDependencies": ["web_ng"] 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=scarnett_flutter_weather 2 | sonar.organization=scarnett 3 | 4 | sonar.projectName=flutter_weather 5 | sonar.projectVersion=99.99.99 6 | 7 | sonar.c.file.suffixes=- 8 | sonar.cpp.file.suffixes=- 9 | sonar.objc.file.suffixes=- -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scarnett/flutter_weather/adc3be5ed44670de6f3280a386956ddef45a0923/tools/generators/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"] 9 | }, 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": {} 18 | }, 19 | "exclude": ["node_modules", "tmp"] 20 | } 21 | --------------------------------------------------------------------------------