├── .flutter-plugins ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── app │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── jhomlala │ │ │ └── feather │ │ │ └── MainActivity.java │ │ └── res │ │ ├── drawable │ │ ├── feather_loading.png │ │ └── launch_background.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 │ │ └── values │ │ └── styles.xml ├── build.gradle ├── feather_android.iml ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties └── settings.gradle ├── assets ├── icon_barometer.png ├── icon_cloud.png ├── icon_cloud_little_rain.png ├── icon_cloud_sun.png ├── icon_dust.png ├── icon_logo.png ├── icon_rain.png ├── icon_snow.png ├── icon_sun.png ├── icon_thermometer.png ├── icon_thunder.png └── icon_wind.png ├── feather.iml ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── 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 │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── l10n.yaml ├── lib ├── main.dart └── src │ ├── data │ ├── model │ │ ├── internal │ │ │ ├── application_error.dart │ │ │ ├── chart_data.dart │ │ │ ├── chart_line.dart │ │ │ ├── forecast_navigation_params.dart │ │ │ ├── geo_position.dart │ │ │ ├── navigation_route.dart │ │ │ ├── overflow_menu_element.dart │ │ │ ├── pair.dart │ │ │ ├── point.dart │ │ │ ├── settings_navigation_params.dart │ │ │ ├── unit.dart │ │ │ └── weather_forecast_holder.dart │ │ └── remote │ │ │ ├── city.dart │ │ │ ├── clouds.dart │ │ │ ├── coordinates.dart │ │ │ ├── main_weather_data.dart │ │ │ ├── overall_weather_data.dart │ │ │ ├── rain.dart │ │ │ ├── system.dart │ │ │ ├── weather_forecast_list_response.dart │ │ │ ├── weather_forecast_response.dart │ │ │ ├── weather_response.dart │ │ │ └── wind.dart │ └── repository │ │ ├── local │ │ ├── application_local_repository.dart │ │ ├── location_manager.dart │ │ ├── location_provider.dart │ │ ├── storage_manager.dart │ │ ├── storage_provider.dart │ │ ├── weather_helper.dart │ │ └── weather_local_repository.dart │ │ └── remote │ │ ├── weather_api_provider.dart │ │ └── weather_remote_repository.dart │ ├── l10n │ ├── app_en.arb │ └── app_pl.arb │ ├── resources │ └── config │ │ ├── application_colors.dart │ │ ├── application_config.dart │ │ ├── assets.dart │ │ ├── dimensions.dart │ │ └── ids.dart │ ├── ui │ ├── about │ │ ├── about_screen.dart │ │ └── bloc │ │ │ ├── about_screen_bloc.dart │ │ │ ├── about_screen_event.dart │ │ │ └── about_screen_state.dart │ ├── app │ │ ├── app_bloc.dart │ │ ├── app_event.dart │ │ └── app_state.dart │ ├── forecast │ │ ├── weather_forecast_screen.dart │ │ └── widget │ │ │ ├── chart_widget.dart │ │ │ ├── weather_forecast_base_page.dart │ │ │ ├── weather_forecast_pressure_page.dart │ │ │ ├── weather_forecast_rain_page.dart │ │ │ ├── weather_forecast_temperature_page.dart │ │ │ ├── weather_forecast_widget.dart │ │ │ └── weather_forecast_wind_page.dart │ ├── main │ │ ├── bloc │ │ │ ├── main_screen_bloc.dart │ │ │ ├── main_screen_event.dart │ │ │ └── main_screen_state.dart │ │ ├── main_screen.dart │ │ └── widget │ │ │ ├── sun_path_widget.dart │ │ │ ├── weather_forecast_thumbnail_list_widget.dart │ │ │ ├── weather_forecast_thumbnail_widget.dart │ │ │ └── weather_main_sun_path_widget.dart │ ├── navigation │ │ ├── bloc │ │ │ ├── navigation_bloc.dart │ │ │ ├── navigation_event.dart │ │ │ └── navigation_state.dart │ │ └── navigation_provider.dart │ ├── settings │ │ ├── bloc │ │ │ ├── settings_screen_bloc.dart │ │ │ ├── settings_screen_event.dart │ │ │ └── settings_screen_state.dart │ │ └── settings_screen.dart │ └── widget │ │ ├── animated_gradient.dart │ │ ├── animated_state.dart │ │ ├── animated_text_widget.dart │ │ ├── current_weather_widget.dart │ │ ├── empty_animation.dart │ │ ├── loading_widget.dart │ │ ├── transparent_app_bar.dart │ │ └── widget_helper.dart │ └── utils │ ├── app_logger.dart │ ├── date_time_helper.dart │ └── types_helper.dart ├── media ├── 1.png ├── 10.png ├── 11.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── 9.png ├── logo.png └── video.gif ├── pubspec.lock ├── pubspec.yaml └── test ├── data ├── model │ └── weather_utils.dart └── repository │ ├── local │ ├── application_local_repository_test.dart │ ├── fake_location_provider.dart │ ├── fake_storage_manager.dart │ ├── fake_storage_provider.dart │ ├── location_manager_test.dart │ ├── storage_manager_test.dart │ ├── weather_helper_test.dart │ └── weather_local_repository_test.dart │ └── remote │ ├── fake_weather_api_provider.dart │ ├── weather_api_provider_test.dart │ └── weather_remote_repository_test.dart ├── test_helper.dart ├── ui ├── about │ ├── about_screen_bloc_test.dart │ └── about_screen_test.dart ├── app │ └── app_bloc_test.dart ├── forecast │ ├── chart_widget_test.dart │ ├── weather_forecast_pressure_page_test.dart │ ├── weather_forecast_rain_page_test.dart │ ├── weather_forecast_temperature_page_test.dart │ ├── weather_forecast_widget_test.dart │ └── weather_forecast_wind_page_test.dart ├── main │ ├── bloc │ │ └── main_screen_bloc_test.dart │ ├── main_screen_test.dart │ └── widget │ │ ├── sun_path_widget_test.dart │ │ ├── weather_current_widget_test.dart │ │ ├── weather_forecast_thumbnail_list_widget_test.dart │ │ ├── weather_forecast_thumbnail_widget_test.dart │ │ └── weather_main_sun_path_page_test.dart ├── navigation │ └── bloc │ │ ├── fake_navigation_provider.dart │ │ ├── navigation_bloc_test.dart │ │ └── navigation_provider_test.dart ├── settings │ ├── bloc │ │ └── settings_bloc_test.dart │ └── widget │ │ └── settings_screen_test.dart └── widget │ └── gradient_cycle_test.dart ├── utils └── types_helper_test.dart └── weather_api_provider_test.dart /.flutter-plugins: -------------------------------------------------------------------------------- 1 | # This is a generated file; do not edit or check into version control. 2 | app_settings=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/app_settings-4.1.0/ 3 | geolocator=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/geolocator-7.0.3/ 4 | geolocator_web=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/geolocator_web-2.0.3/ 5 | package_info=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/package_info-2.0.0/ 6 | path_provider_linux=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-2.0.0/ 7 | path_provider_windows=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-2.0.0/ 8 | shared_preferences=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences-2.0.5/ 9 | shared_preferences_linux=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_linux-2.0.0/ 10 | shared_preferences_macos=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_macos-2.0.0/ 11 | shared_preferences_web=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_web-2.0.0/ 12 | shared_preferences_windows=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_windows-2.0.0/ 13 | url_launcher=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.3/ 14 | url_launcher_linux=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-2.0.0/ 15 | url_launcher_macos=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-2.0.0/ 16 | url_launcher_web=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_web-2.0.0/ 17 | url_launcher_windows=/Users/jakubhomlala/development/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-2.0.0/ 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | .idea 4 | 5 | .packages 6 | .pub/ 7 | .idea/ 8 | 9 | build/ 10 | ios/.generated/ 11 | ios/Flutter/Generated.xcconfig 12 | ios/Runner/GeneratedPluginRegistrant.* 13 | 14 | *.txt 15 | android/.gradle 16 | 17 | .flutter-plugins 18 | *.lock 19 | *.lock 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | sudo: false 4 | addons: 5 | apt: 6 | # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18 7 | sources: 8 | - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version 9 | packages: 10 | #- libstdc++6 11 | - lib32stdc++6 12 | - fonts-droid 13 | before_script: 14 | - git clone https://github.com/flutter/flutter.git 15 | - ./flutter/bin/flutter doctor 16 | script: 17 | - ./flutter/bin/flutter test 18 | cache: 19 | directories: 20 | - $HOME/.pub-cache 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # :sunny: Feather 6 | [![Build Status](https://app.bitrise.io/app/555fd3365953cd2f.svg?token=nPJStq5nJhmQDlgdtIzSqw)](https://github.com/jhomlala/feather) 7 | [![Flutter Awesome](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) 8 | 9 | 10 | Beautiful Flutter weather application. Entirely written in Dart and Flutter. Application is ready for Android and iOS. 11 | 12 | ### :camera: Media 13 |

14 | 15 |

16 | 17 | 18 | 19 | 22 | 25 | 28 | 31 | 34 | 37 | 38 | 39 | 42 | 45 | 48 | 51 | 54 | 55 |
20 | 21 | 23 | 24 | 26 | 27 | 29 | 30 | 32 | 33 | 35 | 36 |
40 | 41 | 43 | 44 | 46 | 47 | 49 | 50 | 52 | 53 |
56 | 57 | ## :cloud: Features 58 | :heavy_check_mark: Beautiful UI and great UX 59 | :heavy_check_mark: Current weather: current temperature, max and min temperature, humidity, pressure, wind 60 | :heavy_check_mark: Current sun/moon position, animated countdown until sunset/sunrise, time of sunset/sunrise 61 | :heavy_check_mark: Weather forecast for 5 days (temperature, wind, rain and pressure) 62 | :heavy_check_mark: Custom-written chart with animation 63 | :heavy_check_mark: Sun/moon animation 64 | :heavy_check_mark: App background based on day cycle 65 | :heavy_check_mark: Automatically picks user location (also error handling when location can't be selected!) 66 | :heavy_check_mark: Persist location and weather data in local storage 67 | :heavy_check_mark: Works offline (user need to download data before) 68 | :heavy_check_mark: Automatically refresh data every 15 minutes 69 | :heavy_check_mark: I18n support (currently PL and EN) 70 | :heavy_check_mark: Bloc architecture, Dio 71 | :heavy_check_mark: Unit and widget tests 72 | :heavy_check_mark: Bitrise CI/CD 73 | 74 | ## :cloud: Credits 75 | API: OpenWeatherAPI 76 | Icons: Icons8, FlatIcon 77 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lint/analysis_options_package.yaml 2 | 3 | analyzer: 4 | strong-mode: 5 | implicit-dynamic: false 6 | 7 | linter: 8 | rules: 9 | close_sinks: true 10 | sort_constructors_first: false 11 | avoid_classes_with_only_static_members: false 12 | avoid_void_async: false 13 | avoid_positional_boolean_parameters: false 14 | avoid_function_literals_in_foreach_calls: false 15 | prefer_constructors_over_static_methods: false 16 | sort_unnamed_constructors_first: false 17 | sized_box_for_whitespace: false 18 | invalid_dependency: false 19 | sort_pub_dependencies: false 20 | implicit-dynamic: false -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 29 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.jhomlala.feather" 37 | minSdkVersion 16 38 | targetSdkVersion 29 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | } 60 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | 16 | 19 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/jhomlala/feather/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.jhomlala.feather; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/feather_loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/android/app/src/main/res/drawable/feather_loading.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.5.3' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/feather_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/local.properties: -------------------------------------------------------------------------------- 1 | sdk.dir=/Users/jakubhomlala/Library/Android/sdk 2 | flutter.sdk=/Users/jakubhomlala/development/flutter 3 | flutter.versionName=2.0.0 4 | flutter.buildMode=debug -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /assets/icon_barometer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/assets/icon_barometer.png -------------------------------------------------------------------------------- /assets/icon_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/assets/icon_cloud.png -------------------------------------------------------------------------------- /assets/icon_cloud_little_rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/assets/icon_cloud_little_rain.png -------------------------------------------------------------------------------- /assets/icon_cloud_sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/assets/icon_cloud_sun.png -------------------------------------------------------------------------------- /assets/icon_dust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/assets/icon_dust.png -------------------------------------------------------------------------------- /assets/icon_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/assets/icon_logo.png -------------------------------------------------------------------------------- /assets/icon_rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/assets/icon_rain.png -------------------------------------------------------------------------------- /assets/icon_snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/assets/icon_snow.png -------------------------------------------------------------------------------- /assets/icon_sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/assets/icon_sun.png -------------------------------------------------------------------------------- /assets/icon_thermometer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/assets/icon_thermometer.png -------------------------------------------------------------------------------- /assets/icon_thunder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/assets/icon_thunder.png -------------------------------------------------------------------------------- /assets/icon_wind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/assets/icon_wind.png -------------------------------------------------------------------------------- /feather.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.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 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | feather 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | NSLocationWhenInUseUsageDescription 45 | This app needs access to location when open. Location is used to select weather for user location. 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/src/l10n 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart -------------------------------------------------------------------------------- /lib/src/data/model/internal/application_error.dart: -------------------------------------------------------------------------------- 1 | enum ApplicationError { 2 | apiError, 3 | connectionError, 4 | locationNotSelectedError, 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/data/model/internal/chart_line.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | class ChartLine { 4 | final String? _label; 5 | final Offset _textOffset; 6 | final Offset _lineStartOffset; 7 | final Offset _lineEndOffset; 8 | 9 | ChartLine(this._label, this._textOffset, this._lineStartOffset, 10 | this._lineEndOffset); 11 | 12 | Offset get lineEndOffset => _lineEndOffset; 13 | 14 | Offset get lineStartOffset => _lineStartOffset; 15 | 16 | Offset get textOffset => _textOffset; 17 | 18 | String? get label => _label; 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/data/model/internal/forecast_navigation_params.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 2 | 3 | class ForecastNavigationParams { 4 | final WeatherForecastHolder weatherForecastHolder; 5 | 6 | ForecastNavigationParams(this.weatherForecastHolder); 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/data/model/internal/geo_position.dart: -------------------------------------------------------------------------------- 1 | import 'package:geolocator/geolocator.dart'; 2 | 3 | class GeoPosition { 4 | final double? lat; 5 | final double? long; 6 | 7 | GeoPosition(this.lat, this.long); 8 | 9 | GeoPosition.fromJson(Map json) 10 | : lat = json["lat"] as double?, 11 | long = json["long"] as double?; 12 | 13 | GeoPosition.fromPosition(Position position) 14 | : lat = position.latitude, 15 | long = position.longitude; 16 | 17 | Map toJson() => { 18 | "lat": lat, 19 | "long": long, 20 | }; 21 | 22 | @override 23 | String toString() { 24 | return 'GeoPosition{lat: $lat, long: $long}'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/data/model/internal/navigation_route.dart: -------------------------------------------------------------------------------- 1 | enum NavigationRoute { 2 | mainScreen, 3 | forecastScreen, 4 | aboutScreen, 5 | settingsScreen, 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/data/model/internal/overflow_menu_element.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class PopupMenuElement { 4 | final Key? key; 5 | final String? title; 6 | 7 | const PopupMenuElement({this.key,this.title}); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/data/model/internal/pair.dart: -------------------------------------------------------------------------------- 1 | class Pair{ 2 | T first; 3 | V second; 4 | 5 | Pair(this.first, this.second); 6 | 7 | } -------------------------------------------------------------------------------- /lib/src/data/model/internal/point.dart: -------------------------------------------------------------------------------- 1 | class Point { 2 | final double x; 3 | final double y; 4 | 5 | Point(this.x, this.y); 6 | 7 | @override 8 | String toString() { 9 | return 'Point{_x: $x, _y: $y}'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/data/model/internal/settings_navigation_params.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SettingsNavigationParams { 4 | final List? startGradientColors; 5 | 6 | SettingsNavigationParams(this.startGradientColors); 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/data/model/internal/unit.dart: -------------------------------------------------------------------------------- 1 | enum Unit { 2 | metric, 3 | imperial, 4 | } 5 | -------------------------------------------------------------------------------- /lib/src/data/model/remote/city.dart: -------------------------------------------------------------------------------- 1 | class City { 2 | final int? id; 3 | final String? name; 4 | 5 | City(this.id, this.name); 6 | 7 | City.fromJson(Map json) 8 | : id = json["id"] as int?, 9 | name = json["name"] as String?; 10 | 11 | Map toJson() => { 12 | "id": id, 13 | "name": name, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/data/model/remote/clouds.dart: -------------------------------------------------------------------------------- 1 | class Clouds { 2 | final int? all; 3 | 4 | Clouds(this.all); 5 | 6 | Clouds.fromJson(Map json) : all = json["all"] as int?; 7 | 8 | Map toJson() => { 9 | "all": all, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/data/model/remote/coordinates.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/utils/types_helper.dart'; 2 | 3 | class Coordinates { 4 | final double longitude; 5 | final double latitude; 6 | 7 | Coordinates(this.longitude, this.latitude); 8 | 9 | Coordinates.fromJson(Map json) 10 | : longitude = TypesHelper.toDouble(json["lon"] as num?), 11 | latitude = TypesHelper.toDouble(json["lat"] as num?); 12 | 13 | Map toJson() => 14 | {"longitude": longitude, "latitude": latitude}; 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/data/model/remote/main_weather_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/utils/types_helper.dart'; 2 | 3 | class MainWeatherData { 4 | final double temp; 5 | final double pressure; 6 | final double humidity; 7 | final double tempMin; 8 | final double tempMax; 9 | final double pressureSeaLevel; 10 | final double pressureGroundLevel; 11 | 12 | MainWeatherData(this.temp, this.pressure, this.humidity, this.tempMin, 13 | this.tempMax, this.pressureSeaLevel, this.pressureGroundLevel); 14 | 15 | MainWeatherData.fromJson(Map json) 16 | : temp = TypesHelper.toDouble(json["temp"] as num?), 17 | pressure = TypesHelper.toDouble(json["pressure"] as num?), 18 | humidity = TypesHelper.toDouble(json["humidity"] as num?), 19 | tempMin = TypesHelper.toDouble(json["temp_min"] as num?), 20 | tempMax = TypesHelper.toDouble(json["temp_max"] as num?), 21 | pressureSeaLevel = TypesHelper.toDouble(json["sea_level"] as num?), 22 | pressureGroundLevel = 23 | TypesHelper.toDouble(json["ground_level"] as num?); 24 | 25 | Map toJson() => { 26 | "temp": temp, 27 | "pressure": pressure, 28 | "humidity": humidity, 29 | "temp_min": tempMin, 30 | "temp_max": tempMax, 31 | "sea_level": pressureSeaLevel, 32 | "ground_level": pressureGroundLevel 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/data/model/remote/overall_weather_data.dart: -------------------------------------------------------------------------------- 1 | class OverallWeatherData { 2 | final int? id; 3 | final String? main; 4 | final String? description; 5 | final String? icon; 6 | 7 | OverallWeatherData(this.id, this.main, this.description, this.icon); 8 | 9 | OverallWeatherData.fromJson(Map json) 10 | : id = json["id"] as int?, 11 | main = json["main"] as String?, 12 | description = json["description"] as String?, 13 | icon = json["icon"] as String?; 14 | 15 | Map toJson() => { 16 | "id": id, 17 | "main": main, 18 | "description": description, 19 | "icon": icon, 20 | }; 21 | 22 | @override 23 | String toString() { 24 | return toJson().toString(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/data/model/remote/rain.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/utils/types_helper.dart'; 2 | 3 | class Rain { 4 | final double amount; 5 | 6 | Rain(this.amount); 7 | 8 | Rain.fromJson(Map json) 9 | : amount = TypesHelper.toDouble(json["3h"] as num?); 10 | 11 | Map toJson() => { 12 | "3h": amount, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/data/model/remote/system.dart: -------------------------------------------------------------------------------- 1 | class System { 2 | final String? country; 3 | final int? sunrise; 4 | final int? sunset; 5 | 6 | System(this.country, this.sunrise, this.sunset); 7 | 8 | System.fromJson(Map json) 9 | : country = json["country"] as String?, 10 | sunrise = json["sunrise"] as int?, 11 | sunset = json["sunset"] as int?; 12 | 13 | Map toJson() => { 14 | "country": country, 15 | "sunrise": sunrise, 16 | "sunset": sunset 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/data/model/remote/weather_forecast_list_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/application_error.dart'; 2 | import 'package:feather/src/data/model/remote/city.dart'; 3 | import 'package:feather/src/data/model/remote/weather_forecast_response.dart'; 4 | 5 | class WeatherForecastListResponse { 6 | final List? list; 7 | final City? city; 8 | ApplicationError? _errorCode; 9 | 10 | WeatherForecastListResponse(this.list, this.city); 11 | 12 | WeatherForecastListResponse.fromJson(Map json) 13 | : list = (json["list"] as List) 14 | .map((dynamic data) => 15 | WeatherForecastResponse.fromJson(data as Map)) 16 | .toList(), 17 | city = City.fromJson(json["city"] as Map); 18 | 19 | Map toJson() => 20 | {"list": list, "city": city}; 21 | 22 | static WeatherForecastListResponse withErrorCode(ApplicationError errorCode) { 23 | final WeatherForecastListResponse response = 24 | WeatherForecastListResponse(null, null); 25 | response._errorCode = errorCode; 26 | return response; 27 | } 28 | 29 | ApplicationError? get errorCode => _errorCode; 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/data/model/remote/weather_forecast_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/remote/clouds.dart'; 2 | import 'package:feather/src/data/model/remote/main_weather_data.dart'; 3 | import 'package:feather/src/data/model/remote/overall_weather_data.dart'; 4 | import 'package:feather/src/data/model/remote/rain.dart'; 5 | import 'package:feather/src/data/model/remote/wind.dart'; 6 | 7 | class WeatherForecastResponse { 8 | final MainWeatherData? mainWeatherData; 9 | final List? overallWeatherData; 10 | final Clouds? clouds; 11 | final Wind? wind; 12 | final DateTime dateTime; 13 | final Rain? rain; 14 | final Rain? snow; 15 | 16 | WeatherForecastResponse(this.mainWeatherData, this.overallWeatherData, 17 | this.clouds, this.wind, this.dateTime, this.rain, this.snow); 18 | 19 | WeatherForecastResponse.fromJson(Map json) 20 | : overallWeatherData = (json["weather"] as List) 21 | .map((dynamic data) => 22 | OverallWeatherData.fromJson(data as Map)) 23 | .toList(), 24 | mainWeatherData = 25 | MainWeatherData.fromJson(json["main"] as Map), 26 | wind = Wind.fromJson(json["wind"] as Map), 27 | clouds = Clouds.fromJson(json["clouds"] as Map), 28 | dateTime = DateTime.parse(json["dt_txt"] as String), 29 | rain = _getRain(json["rain"]), 30 | snow = _getRain(json["snow"]); 31 | 32 | static Rain _getRain(dynamic json) { 33 | if (json == null) { 34 | return Rain(0); 35 | } else { 36 | return Rain.fromJson(json as Map); 37 | } 38 | } 39 | 40 | Map toJson() => { 41 | "weather": overallWeatherData, 42 | "main": mainWeatherData, 43 | "clouds": clouds!.toJson(), 44 | "wind": wind!.toJson(), 45 | "dt_txt": dateTime.toIso8601String(), 46 | "rain": rain!.toJson(), 47 | "snow": snow!.toJson() 48 | }; 49 | 50 | @override 51 | String toString() { 52 | return toJson().toString(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/data/model/remote/weather_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/application_error.dart'; 2 | import 'package:feather/src/data/model/remote/clouds.dart'; 3 | import 'package:feather/src/data/model/remote/coordinates.dart'; 4 | import 'package:feather/src/data/model/remote/main_weather_data.dart'; 5 | import 'package:feather/src/data/model/remote/overall_weather_data.dart'; 6 | import 'package:feather/src/data/model/remote/system.dart'; 7 | import 'package:feather/src/data/model/remote/wind.dart'; 8 | 9 | class WeatherResponse { 10 | final Coordinates? cord; 11 | final List? overallWeatherData; 12 | final MainWeatherData? mainWeatherData; 13 | final Wind? wind; 14 | final Clouds? clouds; 15 | final System? system; 16 | final int? id; 17 | final String? name; 18 | final int? cod; 19 | final String? station; 20 | ApplicationError? _errorCode; 21 | 22 | WeatherResponse({ 23 | this.cord, 24 | this.overallWeatherData, 25 | this.mainWeatherData, 26 | this.wind, 27 | this.clouds, 28 | this.system, 29 | this.id, 30 | this.name, 31 | this.cod, 32 | this.station, 33 | }); 34 | 35 | WeatherResponse.fromJson(Map json) 36 | : cord = Coordinates.fromJson(json["coord"] as Map), 37 | system = System.fromJson(json["sys"] as Map), 38 | overallWeatherData = (json["weather"] as List) 39 | .map((dynamic data) => 40 | OverallWeatherData.fromJson(data as Map)) 41 | .toList(), 42 | mainWeatherData = 43 | MainWeatherData.fromJson(json["main"] as Map), 44 | wind = Wind.fromJson(json["wind"] as Map), 45 | clouds = Clouds.fromJson(json["clouds"] as Map), 46 | id = json["id"] as int?, 47 | name = json["name"] as String?, 48 | cod = json["cod"] as int?, 49 | station = json["station"] as String?; 50 | 51 | Map toJson() => { 52 | "coord": cord, 53 | "sys": system, 54 | "weather": overallWeatherData, 55 | "main": mainWeatherData, 56 | "wind": wind, 57 | "clouds": clouds, 58 | "id": id, 59 | "name": name, 60 | "cod": cod, 61 | "station": station, 62 | }; 63 | 64 | static WeatherResponse withErrorCode(ApplicationError errorCode) { 65 | final WeatherResponse response = WeatherResponse(); 66 | response._errorCode = errorCode; 67 | return response; 68 | } 69 | 70 | ApplicationError? get errorCode => _errorCode; 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/data/model/remote/wind.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/utils/types_helper.dart'; 2 | 3 | class Wind { 4 | final double speed; 5 | final double deg; 6 | 7 | Wind(this.speed, this.deg); 8 | 9 | Wind.fromJson(Map json) 10 | : speed = TypesHelper.toDouble(json["speed"] as num), 11 | deg = TypesHelper.toDouble(json["deg"] as num); 12 | 13 | Map toJson() => { 14 | "speed": speed, 15 | "deg": deg, 16 | }; 17 | 18 | String getDegCode() { 19 | if (deg == 0.0) { 20 | return "N"; 21 | } 22 | if (deg >= 0 && deg < 45) { 23 | return "N"; 24 | } else if (deg >= 45 && deg < 90) { 25 | return "NE"; 26 | } else if (deg >= 90 && deg < 135) { 27 | return "E"; 28 | } else if (deg >= 135 && deg < 180) { 29 | return "SE"; 30 | } else if (deg >= 180 && deg < 225) { 31 | return "S"; 32 | } else if (deg >= 225 && deg < 270) { 33 | return "SW"; 34 | } else if (deg >= 270 && deg < 315) { 35 | return "W"; 36 | } else if (deg >= 315 && deg <= 360) { 37 | return "NW"; 38 | } else { 39 | return "N"; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/data/repository/local/application_local_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/unit.dart'; 2 | import 'package:feather/src/data/repository/local/storage_manager.dart'; 3 | 4 | class ApplicationLocalRepository { 5 | final StorageManager _storageManager; 6 | 7 | ApplicationLocalRepository(this._storageManager); 8 | 9 | Future getSavedUnit() async { 10 | return _storageManager.getUnit(); 11 | } 12 | 13 | void saveUnit(Unit unit) { 14 | _storageManager.saveUnit(unit); 15 | } 16 | 17 | Future getSavedRefreshTime() async { 18 | return _storageManager.getRefreshTime(); 19 | } 20 | 21 | void saveRefreshTime(int refreshTime) { 22 | _storageManager.saveRefreshTime(refreshTime); 23 | } 24 | 25 | Future getLastRefreshTime() { 26 | return _storageManager.getLastRefreshTime(); 27 | } 28 | 29 | void saveLastRefreshTime(int lastRefreshTime) { 30 | _storageManager.saveLastRefreshTime(lastRefreshTime); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/data/repository/local/location_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/repository/local/location_provider.dart'; 2 | import 'package:feather/src/utils/app_logger.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:geolocator/geolocator.dart'; 5 | 6 | ///Class used to provide location of the device. 7 | class LocationManager { 8 | final LocationProvider _locationProvider; 9 | Position? _lastPosition; 10 | 11 | LocationManager(this._locationProvider); 12 | 13 | Future getLocation() async { 14 | try { 15 | if (_lastPosition != null) { 16 | return _lastPosition; 17 | } 18 | // ignore: join_return_with_assignment 19 | _lastPosition = await _locationProvider.providePosition(); 20 | return _lastPosition; 21 | } catch (exc, stackTrace) { 22 | Log.e("Exception occurred: $exc in $stackTrace"); 23 | return null; 24 | } 25 | } 26 | 27 | Future isLocationEnabled() { 28 | return _locationProvider.isLocationEnabled(); 29 | } 30 | 31 | Future checkLocationPermission() async{ 32 | return _locationProvider.checkLocationPermission(); 33 | } 34 | 35 | Future requestLocationPermission() async{ 36 | return _locationProvider.requestLocationPermission(); 37 | } 38 | 39 | 40 | @visibleForTesting 41 | Position? get lastPosition => _lastPosition; 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/data/repository/local/location_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:geolocator/geolocator.dart'; 2 | 3 | class LocationProvider { 4 | Future providePosition() async { 5 | return Geolocator.getCurrentPosition( 6 | desiredAccuracy: LocationAccuracy.high); 7 | } 8 | 9 | Future isLocationEnabled() { 10 | return Geolocator.isLocationServiceEnabled(); 11 | } 12 | 13 | Future checkLocationPermission() async { 14 | return Geolocator.checkPermission(); 15 | } 16 | 17 | Future requestLocationPermission() async { 18 | return Geolocator.requestPermission(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/data/repository/local/storage_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class StorageProvider { 4 | Future getInt(String key) async { 5 | final sharedPreferences = await SharedPreferences.getInstance(); 6 | return sharedPreferences.getInt(key); 7 | } 8 | 9 | Future setInt(String key, int value) async { 10 | final sharedPreferences = await SharedPreferences.getInstance(); 11 | return sharedPreferences.setInt(key, value); 12 | } 13 | 14 | Future getString(String key) async { 15 | final sharedPreferences = await SharedPreferences.getInstance(); 16 | return sharedPreferences.getString(key); 17 | } 18 | 19 | Future setString(String key, String value) async { 20 | final sharedPreferences = await SharedPreferences.getInstance(); 21 | return sharedPreferences.setString(key, value); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/data/repository/local/weather_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/remote/system.dart'; 2 | import 'package:feather/src/data/model/remote/weather_forecast_response.dart'; 3 | import 'package:feather/src/resources/config/assets.dart'; 4 | 5 | class WeatherHelper { 6 | static String getWeatherIcon(int code) { 7 | String asset = Assets.iconCloud; 8 | if (code >= 200 && code <= 299) { 9 | asset = Assets.iconThunder; 10 | } else if (code >= 300 && code <= 399) { 11 | asset = Assets.iconCloudLittleRain; 12 | } else if (code >= 500 && code <= 599) { 13 | asset = Assets.iconRain; 14 | } else if (code >= 600 && code <= 699) { 15 | asset = Assets.iconSnow; 16 | } else if (code >= 700 && code <= 799) { 17 | asset = Assets.iconDust; 18 | } else if (code == 800) { 19 | asset = Assets.iconSun; 20 | } else if (code == 801) { 21 | asset = Assets.iconCloudSun; 22 | } else if (code >= 802) { 23 | asset = Assets.iconCloud; 24 | } 25 | return asset; 26 | } 27 | 28 | static Map> getMapForecastsForSameDay( 29 | List forecastList) { 30 | final Map> map = {}; 31 | for (int i = 0; i < forecastList.length; i++) { 32 | final WeatherForecastResponse response = forecastList[i]; 33 | final String dayKey = _getDayKey(response.dateTime); 34 | if (!map.containsKey(dayKey)) { 35 | map[dayKey] = []; 36 | } 37 | map[dayKey]!.add(response); 38 | } 39 | return map; 40 | } 41 | 42 | static String _getDayKey(DateTime dateTime) { 43 | return "${dateTime.day.toString()}-${dateTime.month.toString()}-${dateTime.year.toString()}"; 44 | } 45 | 46 | static String formatTemperature({ 47 | double? temperature, 48 | int positions = 0, 49 | bool round = true, 50 | bool metricUnits = true, 51 | }) { 52 | var unit = "°C"; 53 | var temperatureValue = temperature; 54 | 55 | if (!metricUnits) { 56 | unit = "°F"; 57 | } 58 | 59 | if (round) { 60 | temperatureValue = temperature!.floor().toDouble(); 61 | } 62 | 63 | return "${temperatureValue!.toStringAsFixed(positions)} $unit"; 64 | } 65 | 66 | static double convertCelsiusToFahrenheit(double temperature) { 67 | return 32 + temperature * 1.8; 68 | } 69 | 70 | 71 | static double convertFahrenheitToCelsius(double temperature) { 72 | return (temperature - 32) * 5/9; 73 | } 74 | 75 | static String formatPressure(double pressure, bool isMetricUnits) { 76 | String unit = "hPa"; 77 | if (!isMetricUnits) { 78 | unit = "mbar"; 79 | } 80 | return "${pressure.toStringAsFixed(0)} $unit"; 81 | } 82 | 83 | static double convertMetersPerSecondToKilometersPerHour(double? speed) { 84 | if (speed != null) { 85 | return speed * 3.6; 86 | } else { 87 | return 0; 88 | } 89 | } 90 | 91 | static double convertMetersPerSecondToMilesPerHour(double? speed) { 92 | if (speed != null) { 93 | return speed * 2.236936292; 94 | } else { 95 | return 0; 96 | } 97 | } 98 | 99 | static String formatRain(double rain) { 100 | return "${rain.toStringAsFixed(2)} mm/h"; 101 | } 102 | 103 | static String formatWind(double wind, bool isMetricUnits) { 104 | String unit = "km/h"; 105 | if (!isMetricUnits) { 106 | unit = "mi/h"; 107 | } 108 | return "${wind.toStringAsFixed(1)} $unit"; 109 | } 110 | 111 | static String formatHumidity(double humidity) { 112 | return "${humidity.toStringAsFixed(0)}%"; 113 | } 114 | 115 | static int getDayMode(System system) { 116 | final int sunrise = system.sunrise! * 1000; 117 | final int sunset = system.sunset! * 1000; 118 | return getDayModeFromSunriseSunset(sunrise, sunset); 119 | } 120 | 121 | static int getDayModeFromSunriseSunset(int sunrise, int? sunset) { 122 | final int now = DateTime.now().millisecondsSinceEpoch; 123 | if (now >= sunrise && now <= sunset!) { 124 | return 0; 125 | } else if (now >= sunrise) { 126 | return 1; 127 | } else { 128 | return -1; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/src/data/repository/local/weather_local_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/geo_position.dart'; 2 | import 'package:feather/src/data/model/remote/weather_forecast_list_response.dart'; 3 | import 'package:feather/src/data/model/remote/weather_response.dart'; 4 | import 'package:feather/src/data/repository/local/storage_manager.dart'; 5 | 6 | class WeatherLocalRepository { 7 | final StorageManager _storageManager; 8 | 9 | WeatherLocalRepository(this._storageManager); 10 | 11 | Future saveLocation(GeoPosition geoPosition) async { 12 | await _storageManager.saveLocation(geoPosition); 13 | } 14 | 15 | Future getLocation() async { 16 | return _storageManager.getLocation(); 17 | } 18 | 19 | Future saveWeather(WeatherResponse response) async { 20 | await _storageManager.saveWeather(response); 21 | } 22 | 23 | Future getWeather() async { 24 | return _storageManager.getWeather(); 25 | } 26 | 27 | Future saveWeatherForecast(WeatherForecastListResponse response) async { 28 | await _storageManager.saveWeatherForecast(response); 29 | } 30 | 31 | Future getWeatherForecast() async { 32 | return _storageManager.getWeatherForecast(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/data/repository/remote/weather_api_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:feather/src/data/model/internal/application_error.dart'; 3 | import 'package:feather/src/data/model/remote/weather_forecast_list_response.dart'; 4 | import 'package:feather/src/data/model/remote/weather_response.dart'; 5 | import 'package:feather/src/resources/config/application_config.dart'; 6 | import 'package:feather/src/utils/app_logger.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:pretty_dio_logger/pretty_dio_logger.dart'; 9 | // ignore: argument_type_not_assignable 10 | 11 | class WeatherApiProvider { 12 | final String _apiBaseUrl = "api.openweathermap.org"; 13 | final String _apiPath = "/data/2.5"; 14 | final String _apiWeatherEndpoint = "/weather"; 15 | final String _apiWeatherForecastEndpoint = "/forecast"; 16 | final Dio _dio = Dio(); 17 | 18 | Future fetchWeather( 19 | double? latitude, double? longitude) async { 20 | try { 21 | final Uri uri = _buildUri(_apiWeatherEndpoint, latitude, longitude); 22 | final Response> response = 23 | await _dio.get(uri.toString()); 24 | if (response.statusCode == 200) { 25 | return WeatherResponse.fromJson(response.data!); 26 | } else { 27 | return WeatherResponse.withErrorCode(ApplicationError.apiError); 28 | } 29 | } catch (exc, stacktrace) { 30 | Log.e("Exception occurred: $exc stack trace: ${stacktrace.toString()}"); 31 | 32 | return WeatherResponse.withErrorCode(ApplicationError.connectionError); 33 | } 34 | } 35 | 36 | Future fetchWeatherForecast( 37 | double? latitude, double? longitude) async { 38 | try { 39 | final Uri uri = 40 | _buildUri(_apiWeatherForecastEndpoint, latitude, longitude); 41 | final Response> response = 42 | await _dio.get(uri.toString()); 43 | if (response.statusCode == 200) { 44 | return WeatherForecastListResponse.fromJson(response.data!); 45 | } else { 46 | return WeatherForecastListResponse.withErrorCode( 47 | ApplicationError.apiError); 48 | } 49 | } catch (exc, stackTrace) { 50 | Log.e("Exception occurred: $exc $stackTrace"); 51 | return WeatherForecastListResponse.withErrorCode( 52 | ApplicationError.connectionError); 53 | } 54 | } 55 | 56 | Uri _buildUri(String endpoint, double? latitude, double? longitude) { 57 | return Uri( 58 | scheme: "https", 59 | host: _apiBaseUrl, 60 | path: "$_apiPath$endpoint", 61 | queryParameters: { 62 | "lat": latitude.toString(), 63 | "lon": longitude.toString(), 64 | "apiKey": ApplicationConfig.apiKey, 65 | "units": "metric" 66 | }, 67 | ); 68 | } 69 | 70 | void setupInterceptors() { 71 | _dio.interceptors.add(PrettyDioLogger()); 72 | } 73 | 74 | @visibleForTesting 75 | Dio get dio => _dio; 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/data/repository/remote/weather_remote_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/remote/weather_forecast_list_response.dart'; 2 | import 'package:feather/src/data/model/remote/weather_response.dart'; 3 | import 'package:feather/src/data/repository/remote/weather_api_provider.dart'; 4 | 5 | class WeatherRemoteRepository { 6 | final WeatherApiProvider _weatherApiProvider; 7 | WeatherRemoteRepository(this._weatherApiProvider); 8 | 9 | Future fetchWeather(double? latitude, double? longitude) { 10 | return _weatherApiProvider.fetchWeather(latitude, longitude); 11 | } 12 | 13 | Future fetchWeatherForecast( 14 | double? latitude, double? longitude) { 15 | return _weatherApiProvider.fetchWeatherForecast(latitude, longitude); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/l10n/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "error_location_not_selected": "Couldn't select your location. Please make sure that you have given location permission.", 3 | "error_server_connection": "Couldn't connect to server. Please check your internet connection.", 4 | "error_api": "Server error. Please contact with developer.", 5 | "error_unknown": "Unknown error. Please contact with developer.", 6 | "error_location_disabled": "Your location service is disabled. Please enable it and try again.", 7 | "error_permissions_not_granted": "Permissions not granted. Please accept permission.", 8 | "error_permissions_not_granted_permanently": "Permissions not granted permanently. Please open app settings and accept permission.", 9 | "error_failed_to_load_weather_data": "Failed to load weather data.", 10 | "pressure": "Pressure", 11 | "rain": "Rain", 12 | "temperature": "Temperature", 13 | "wind": "Wind", 14 | "day": "Day", 15 | "sunset": "Sunset", 16 | "sunrise": "Sunrise", 17 | "sunset_in": "Sunset in", 18 | "sunrise_in": "Sunrise in", 19 | "night": "Night", 20 | "chart_unavailable": "Chart unavailable", 21 | "retry": "Retry", 22 | "your_location": "Your location", 23 | "humidity": "Humidity", 24 | "units": "Units", 25 | "metric": "Metric", 26 | "imperial": "Imperial", 27 | "units_description": "Type of units (metric/imperial)", 28 | "refresh_time": "Refresh time", 29 | "refresh_time_description": "Interval between calls to weather service", 30 | "minutes": "minutes", 31 | "last_refresh_time": "Last refresh time", 32 | "settings": "Settings", 33 | "about": "About", 34 | "contributors": "Contributors", 35 | "credits": "Credits", 36 | "weather_data": "Weather data: OpenWeatherAPI", 37 | "icon_data": "Icons: Icons8, Freepik/Flaticon", 38 | "retry":"Retry", 39 | "open_app_settings":"Open app settings" 40 | } -------------------------------------------------------------------------------- /lib/src/l10n/app_pl.arb: -------------------------------------------------------------------------------- 1 | { 2 | "error_location_not_selected": "Nie udało się pobrać lokalizacji. Sprawdź czy nadałeś aplikacji odpowiednie pozwolenia.", 3 | "error_server_connection": "Nie udało się połączyć z serwerem. Sprawdź połączenie internetowe.", 4 | "error_api": "Błąd serwera. Skontaktuj się z deweloperem", 5 | "error_unknown": "Nieznany błąd. Skontaktuj się z deweloperem.", 6 | "error_location_disabled":"Usługa lokalizacji jest wyłączona. Włącz ją i spróbuj ponownie.", 7 | "error_permissions_not_granted":"Pozwolenia nie zostały przyznane. Proszę zaakceptować pozwolenia.", 8 | "error_permissions_not_granted_permanently": "Pozwolenia są wyłączone permanentnie. Proszę otworzyć ustawienia aplikacji i zaakceptować pozwolenia.", 9 | "error_failed_to_load_weather_data": "Błąd podczas ładowania danych pogodowych.", 10 | "pressure": "Ciśnienie", 11 | "rain": "Opady", 12 | "temperature": "Temperatura", 13 | "wind": "Wiatr", 14 | "day": "Dzień", 15 | "sunset": "Zachód słońca", 16 | "sunrise": "Wschód słońca", 17 | "sunset_in": "Zachód słońca za", 18 | "sunrise_in": "Wschód słońca za", 19 | "night": "Noc", 20 | "chart_unavailable": "Wykres niedostępny", 21 | "retry": "Spróbuj ponownie", 22 | "your_location": "Twoja lokalizacja", 23 | "humidity": "Wilgotność", 24 | "units": "Jednostki", 25 | "metric": "Metryczne", 26 | "imperial": "Imperialne", 27 | "units_description": "Typ jednostek (metryczne/imperialne)", 28 | "refresh_time": "Czas odświeżania", 29 | "refresh_time_description": "Interwał pomiędzy kolejnymi zapytaniami do serwisu pogody", 30 | "minutes": "minut", 31 | "last_refresh_time": "Ostatnie odświeżenie", 32 | "settings": "Ustawienia", 33 | "about": "O aplikacji", 34 | "contributors": "Współtwórcy", 35 | "credits": "Podziękowania", 36 | "weather_data": "Dane pogodowe: OpenWeatherAPI", 37 | "icon_data": "Ikony: Icons8, Freepik/Flaticon", 38 | "retry":"Ponów", 39 | "open_app_settings":"Otwórz ustawienia aplikacji" 40 | } -------------------------------------------------------------------------------- /lib/src/resources/config/application_colors.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class ApplicationColors{ 6 | static final Color midnightStartColor = Colors.black; 7 | static final Color midnightEndColor = Colors.deepPurple.shade900; 8 | static final Color nightStartColor = Colors.deepPurple.shade900; 9 | static final Color nightEndColor = Colors.indigo; 10 | static final Color twilightStartColor= Colors.indigo; 11 | static final Color twilightEndColor = Colors.purple; 12 | static final Color dawnDuskStartColor = Colors.indigo; 13 | static final Color dawnDuskEndColor = Colors.pinkAccent; 14 | static final Color morningEveStartColor = Colors.blue.shade700; 15 | static final Color morningEveEndColor = Colors.pink.shade200; 16 | static final Color dayStartColor = Colors.blue.shade700; 17 | static final Color dayEndColor = Colors.lightBlue.shade500; 18 | static final Color middayStartColor = Colors.blue.shade700; 19 | static final Color middayEndColor = Colors.lightBlue; 20 | static final Color swiperInactiveDotColor = Colors.white54; 21 | static final Color swiperActiveDotColor = Colors.white; 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/resources/config/application_config.dart: -------------------------------------------------------------------------------- 1 | class ApplicationConfig{ 2 | static const String apiKey = "2b557cc4c291a6293e22bc44e49231d8"; 3 | static const bool isDebug = bool.fromEnvironment("dart.vm.product") == false; 4 | } -------------------------------------------------------------------------------- /lib/src/resources/config/assets.dart: -------------------------------------------------------------------------------- 1 | class Assets { 2 | static const String iconCloud = "assets/icon_cloud.png"; 3 | static const String iconThunder = "assets/icon_thunder.png"; 4 | static const String iconCloudLittleRain = "assets/icon_cloud_little_rain.png"; 5 | static const String iconRain = "assets/icon_rain.png"; 6 | static const String iconSnow = "assets/icon_snow.png"; 7 | static const String iconDust = "assets/icon_dust.png"; 8 | static const String iconSun = "assets/icon_sun.png"; 9 | static const String iconCloudSun = "assets/icon_cloud_sun.png"; 10 | static const String iconWind = "assets/icon_wind.png"; 11 | static const String iconThermometer = "assets/icon_thermometer.png"; 12 | static const String iconBarometer = "assets/icon_barometer.png"; 13 | static const String iconLogo = "assets/icon_logo.png"; 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/resources/config/dimensions.dart: -------------------------------------------------------------------------------- 1 | class Dimensions{ 2 | static const int chartPadding = 15; 3 | static const double weatherMainWidgetSwiperHeight = 450; 4 | } -------------------------------------------------------------------------------- /lib/src/resources/config/ids.dart: -------------------------------------------------------------------------------- 1 | class Ids { 2 | static const String imageWeatherHeroTag = "image_weather"; 3 | static const String temperaturePage = "temperature_page"; 4 | static const String windPage = "wind_page"; 5 | static const String rainPage = "rain_page"; 6 | static const String pressurePage = "pressure_page"; 7 | static const String storageLocationKey = "feather_location"; 8 | static const String storageWeatherKey = "feather_weather"; 9 | static const String storageWeatherForecastKey = "feather_weather_forecast"; 10 | static const String mainWeatherPage = "main_weather_page"; 11 | static const String weatherMainSunPathPage = "weather_main_sun_path_page"; 12 | static const String storageUnitKey = "feather_unit"; 13 | static const String storageRefreshTimeKey = "refresh_time"; 14 | static const String storageLastRefreshTimeKey = "last_refresh_time"; 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/ui/about/bloc/about_screen_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/ui/about/bloc/about_screen_event.dart'; 2 | import 'package:feather/src/ui/about/bloc/about_screen_state.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | 5 | 6 | class AboutScreenBloc extends Bloc { 7 | AboutScreenBloc() : super(InitialAboutScreenState()); 8 | 9 | @override 10 | Stream mapEventToState(AboutScreenEvent event) async* {} 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/ui/about/bloc/about_screen_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class AboutScreenEvent extends Equatable { 4 | @override 5 | List get props => []; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /lib/src/ui/about/bloc/about_screen_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class AboutScreenState extends Equatable { 4 | const AboutScreenState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class InitialAboutScreenState extends AboutScreenState {} 11 | -------------------------------------------------------------------------------- /lib/src/ui/app/app_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/unit.dart'; 2 | import 'package:feather/src/data/repository/local/application_local_repository.dart'; 3 | import 'package:feather/src/ui/app/app_event.dart'; 4 | import 'package:feather/src/ui/app/app_state.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | 7 | class AppBloc extends Bloc { 8 | final ApplicationLocalRepository _applicationLocalRepository; 9 | 10 | AppBloc(this._applicationLocalRepository) 11 | : super(const AppState(Unit.metric)); 12 | 13 | @override 14 | Stream mapEventToState(AppEvent event) async* { 15 | if (event is LoadSettingsAppEvent) { 16 | yield await _loadSettings(); 17 | } 18 | } 19 | 20 | Future _loadSettings() async { 21 | return AppState(await _applicationLocalRepository.getSavedUnit()); 22 | } 23 | 24 | bool isMetricUnits() { 25 | return state.unit == Unit.metric; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/ui/app/app_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class AppEvent extends Equatable { 4 | @override 5 | List get props => []; 6 | } 7 | 8 | class LoadSettingsAppEvent extends AppEvent {} 9 | -------------------------------------------------------------------------------- /lib/src/ui/app/app_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:feather/src/data/model/internal/unit.dart'; 3 | 4 | class AppState extends Equatable { 5 | final Unit unit; 6 | 7 | const AppState(this.unit); 8 | 9 | @override 10 | List get props => [unit]; 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/ui/forecast/weather_forecast_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/overflow_menu_element.dart'; 2 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 3 | import 'package:feather/src/ui/app/app_bloc.dart'; 4 | import 'package:feather/src/ui/navigation/bloc/navigation_bloc.dart'; 5 | import 'package:feather/src/ui/navigation/bloc/navigation_event.dart'; 6 | import 'package:feather/src/ui/widget/transparent_app_bar.dart'; 7 | import 'package:feather/src/ui/forecast/widget/weather_forecast_widget.dart'; 8 | import 'package:feather/src/ui/widget/widget_helper.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter/widgets.dart'; 11 | import 'package:flutter_bloc/flutter_bloc.dart'; 12 | 13 | class WeatherForecastScreen extends StatefulWidget { 14 | final WeatherForecastHolder _holder; 15 | 16 | const WeatherForecastScreen(this._holder, {Key? key}) : super(key: key); 17 | 18 | @override 19 | _WeatherForecastScreenState createState() => _WeatherForecastScreenState(); 20 | } 21 | 22 | class _WeatherForecastScreenState extends State { 23 | late AppBloc _appBloc; 24 | late NavigationBloc _navigationBloc; 25 | 26 | @override 27 | void initState() { 28 | _appBloc = BlocProvider.of(context); 29 | super.initState(); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | final LinearGradient gradient = WidgetHelper.getGradient( 35 | sunriseTime: widget._holder.system!.sunrise, 36 | sunsetTime: widget._holder.system!.sunset); 37 | return Scaffold( 38 | body: Stack( 39 | children: [ 40 | BlocBuilder( 41 | bloc: _appBloc, 42 | builder: (context, state) { 43 | return Container( 44 | key: const Key("weather_main_screen_container"), 45 | decoration: BoxDecoration(gradient: gradient), 46 | child: WeatherForecastWidget( 47 | holder: widget._holder, 48 | width: 300, 49 | height: 150, 50 | isMetricUnits: _appBloc.isMetricUnits(), 51 | ), 52 | ); 53 | }, 54 | ), 55 | TransparentAppBar( 56 | withPopupMenu: true, 57 | onPopupMenuClicked: _onMenuElementClicked, 58 | ) 59 | ], 60 | ), 61 | ); 62 | } 63 | 64 | void _onMenuElementClicked(PopupMenuElement value) { 65 | List startGradientColors = []; 66 | 67 | final LinearGradient gradient = WidgetHelper.getGradient( 68 | sunriseTime: widget._holder.system!.sunrise, 69 | sunsetTime: widget._holder.system!.sunset); 70 | startGradientColors = gradient.colors; 71 | 72 | if (value.key == const Key("menu_overflow_settings")) { 73 | _navigationBloc.add(SettingsScreenNavigationEvent(startGradientColors)); 74 | } 75 | if (value.key == const Key("menu_overflow_about")) { 76 | _navigationBloc.add(AboutScreenNavigationEvent(startGradientColors)); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/ui/forecast/widget/weather_forecast_base_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/chart_data.dart'; 2 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 3 | import 'package:feather/src/ui/forecast/widget/chart_widget.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | abstract class WeatherForecastBasePage extends StatelessWidget { 8 | final WeatherForecastHolder? holder; 9 | final double? width; 10 | final double? height; 11 | final bool isMetricUnits; 12 | 13 | const WeatherForecastBasePage({ 14 | Key? key, 15 | required this.holder, 16 | required this.width, 17 | required this.height, 18 | required this.isMetricUnits, 19 | }) : super(key: key); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final ChartData chartData = getChartData(); 24 | return Center( 25 | child: Column( 26 | children: [ 27 | getPageIconWidget(), 28 | getPageTitleWidget(context), 29 | const SizedBox(height: 20), 30 | getPageSubtitleWidget(context), 31 | const SizedBox(height: 40), 32 | ChartWidget( 33 | key: const Key("weather_forecast_base_page_chart"), 34 | chartData: chartData), 35 | const SizedBox(height: 10), 36 | getBottomRowWidget(context) 37 | ], 38 | ), 39 | ); 40 | } 41 | 42 | Image getPageIconWidget() { 43 | return Image.asset( 44 | getIcon(), 45 | key: const Key("weather_forecast_base_page_icon"), 46 | width: 100, 47 | height: 100, 48 | ); 49 | } 50 | 51 | Widget getPageTitleWidget(BuildContext context) { 52 | return Text(getTitleText(context)!, 53 | key: const Key("weather_forecast_base_page_title"), 54 | textDirection: TextDirection.ltr, 55 | style: Theme.of(context).textTheme.subtitle2); 56 | } 57 | 58 | String getIcon(); 59 | 60 | String? getTitleText(BuildContext context); 61 | 62 | RichText getPageSubtitleWidget(BuildContext context); 63 | 64 | Widget getBottomRowWidget(BuildContext context); 65 | 66 | ChartData getChartData(); 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/ui/forecast/widget/weather_forecast_pressure_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/chart_data.dart'; 2 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | import 'package:feather/src/resources/config/assets.dart'; 5 | import 'package:feather/src/data/repository/local/weather_helper.dart'; 6 | import 'package:feather/src/ui/forecast/widget/weather_forecast_base_page.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter/widgets.dart'; 9 | 10 | class WeatherForecastPressurePage extends WeatherForecastBasePage { 11 | const WeatherForecastPressurePage( 12 | WeatherForecastHolder? holder, 13 | double? width, 14 | double? height, 15 | bool isMetricUnits, { 16 | Key? key, 17 | }) : super( 18 | holder: holder, 19 | width: width, 20 | height: height, 21 | isMetricUnits: isMetricUnits, 22 | key: key, 23 | ); 24 | 25 | @override 26 | Row getBottomRowWidget(BuildContext context) { 27 | return Row( 28 | key: const Key("weather_forecast_pressure_page_bottom_row"), 29 | mainAxisAlignment: MainAxisAlignment.center, 30 | ); 31 | } 32 | 33 | @override 34 | ChartData getChartData() { 35 | return super 36 | .holder! 37 | .setupChartData(ChartDataType.pressure, width!, height!, isMetricUnits); 38 | } 39 | 40 | @override 41 | String getIcon() { 42 | return Assets.iconBarometer; 43 | } 44 | 45 | @override 46 | RichText getPageSubtitleWidget(BuildContext context) { 47 | return RichText( 48 | key: const Key("weather_forecast_pressure_page_subtitle"), 49 | textDirection: TextDirection.ltr, 50 | text: TextSpan( 51 | children: [ 52 | TextSpan(text: 'min ', style: Theme.of(context).textTheme.bodyText1), 53 | TextSpan( 54 | text: WeatherHelper.formatPressure( 55 | holder!.minPressure!, isMetricUnits), 56 | style: Theme.of(context).textTheme.subtitle2), 57 | TextSpan( 58 | text: ' max ', style: Theme.of(context).textTheme.bodyText1), 59 | TextSpan( 60 | text: WeatherHelper.formatPressure( 61 | holder!.maxPressure!, isMetricUnits), 62 | style: Theme.of(context).textTheme.subtitle2) 63 | ], 64 | ), 65 | ); 66 | } 67 | 68 | @override 69 | String? getTitleText(BuildContext context) { 70 | return AppLocalizations.of(context)!.pressure; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/ui/forecast/widget/weather_forecast_rain_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/chart_data.dart'; 2 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | import 'package:feather/src/resources/config/assets.dart'; 5 | import 'package:feather/src/data/repository/local/weather_helper.dart'; 6 | import 'package:feather/src/ui/forecast/widget/weather_forecast_base_page.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter/widgets.dart'; 9 | 10 | class WeatherForecastRainPage extends WeatherForecastBasePage { 11 | const WeatherForecastRainPage(WeatherForecastHolder? holder, double? width, 12 | double? height, bool isMetricUnits, {Key? key}) 13 | : super( 14 | holder: holder, 15 | width: width, 16 | height: height, 17 | isMetricUnits: isMetricUnits, 18 | key: key); 19 | 20 | @override 21 | Row getBottomRowWidget(BuildContext context) { 22 | return Row( 23 | key: const Key("weather_forecast_rain_page_bottom_row"), 24 | mainAxisAlignment: MainAxisAlignment.center, 25 | ); 26 | } 27 | 28 | @override 29 | ChartData getChartData() { 30 | return super 31 | .holder! 32 | .setupChartData(ChartDataType.rain, width!, height!, isMetricUnits); 33 | } 34 | 35 | @override 36 | String getIcon() { 37 | return Assets.iconRain; 38 | } 39 | 40 | @override 41 | RichText getPageSubtitleWidget(BuildContext context) { 42 | return RichText( 43 | key: const Key("weather_forecast_rain_page_subtitle"), 44 | textDirection: TextDirection.ltr, 45 | text: TextSpan( 46 | children: [ 47 | TextSpan(text: 'min ', style: Theme.of(context).textTheme.bodyText1), 48 | TextSpan( 49 | text: WeatherHelper.formatRain(holder!.minRain!), 50 | style: Theme.of(context).textTheme.subtitle2), 51 | TextSpan( 52 | text: ' max ', style: Theme.of(context).textTheme.bodyText1), 53 | TextSpan( 54 | text: WeatherHelper.formatRain(holder!.maxRain!), 55 | style: Theme.of(context).textTheme.subtitle2) 56 | ], 57 | ), 58 | ); 59 | } 60 | 61 | @override 62 | String? getTitleText(BuildContext context) { 63 | return AppLocalizations.of(context)!.rain; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/ui/forecast/widget/weather_forecast_temperature_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/chart_data.dart'; 2 | import 'package:feather/src/data/model/internal/point.dart'; 3 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | import 'package:feather/src/resources/config/assets.dart'; 6 | import 'package:feather/src/data/repository/local/weather_helper.dart'; 7 | import 'package:feather/src/ui/forecast/widget/weather_forecast_base_page.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/widgets.dart'; 10 | 11 | class WeatherForecastTemperaturePage extends WeatherForecastBasePage { 12 | const WeatherForecastTemperaturePage(WeatherForecastHolder? holder, 13 | double? width, double? height, bool isMetricUnits, 14 | {Key? key}) 15 | : super( 16 | holder: holder, 17 | width: width, 18 | height: height, 19 | isMetricUnits: isMetricUnits, 20 | key: key, 21 | ); 22 | 23 | @override 24 | Widget getBottomRowWidget(BuildContext context) { 25 | final List points = getChartData().points!; 26 | final List widgets = []; 27 | if (points.length > 2) { 28 | final double padding = points[1].x - points[0].x - 30; 29 | widgets.add(const SizedBox( 30 | height: 4, 31 | )); 32 | for (int index = 0; index < points.length; index++) { 33 | widgets.add(Image.asset( 34 | WeatherHelper.getWeatherIcon( 35 | holder!.forecastList![index].overallWeatherData![0].id!), 36 | width: 30, 37 | height: 30)); 38 | widgets.add(SizedBox(width: padding)); 39 | } 40 | widgets.removeLast(); 41 | } 42 | 43 | return Row( 44 | key: const Key("weather_forecast_temperature_page_bottom_row"), 45 | mainAxisAlignment: MainAxisAlignment.center, 46 | children: widgets); 47 | } 48 | 49 | @override 50 | ChartData getChartData() { 51 | return holder!.setupChartData( 52 | ChartDataType.temperature, width!, height!, isMetricUnits); 53 | } 54 | 55 | @override 56 | String getIcon() { 57 | return Assets.iconThermometer; 58 | } 59 | 60 | @override 61 | RichText getPageSubtitleWidget(BuildContext context) { 62 | var minTemperature = holder!.minTemperature; 63 | var maxTemperature = holder!.maxTemperature; 64 | 65 | if (!isMetricUnits) { 66 | minTemperature = 67 | WeatherHelper.convertCelsiusToFahrenheit(minTemperature!); 68 | maxTemperature = 69 | WeatherHelper.convertCelsiusToFahrenheit(maxTemperature!); 70 | } 71 | 72 | final minTemperatureFormatted = WeatherHelper.formatTemperature( 73 | temperature: minTemperature, 74 | positions: 1, 75 | round: false, 76 | metricUnits: isMetricUnits); 77 | final maxTemperatureFormatted = WeatherHelper.formatTemperature( 78 | temperature: maxTemperature, 79 | positions: 1, 80 | round: false, 81 | metricUnits: isMetricUnits); 82 | 83 | return RichText( 84 | key: const Key("weather_forecast_temperature_page_subtitle"), 85 | textDirection: TextDirection.ltr, 86 | text: TextSpan( 87 | children: [ 88 | TextSpan(text: 'min ', style: Theme.of(context).textTheme.bodyText1), 89 | TextSpan( 90 | text: minTemperatureFormatted, 91 | style: Theme.of(context).textTheme.subtitle2), 92 | TextSpan( 93 | text: ' max ', style: Theme.of(context).textTheme.bodyText1), 94 | TextSpan( 95 | text: maxTemperatureFormatted, 96 | style: Theme.of(context).textTheme.subtitle2) 97 | ], 98 | ), 99 | ); 100 | } 101 | 102 | @override 103 | String? getTitleText(BuildContext context) { 104 | return AppLocalizations.of(context)!.temperature; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/src/ui/forecast/widget/weather_forecast_wind_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/chart_data.dart'; 2 | import 'package:feather/src/data/model/internal/point.dart'; 3 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | import 'package:feather/src/resources/config/assets.dart'; 6 | import 'package:feather/src/data/repository/local/weather_helper.dart'; 7 | import 'package:feather/src/ui/forecast/widget/weather_forecast_base_page.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/widgets.dart'; 10 | 11 | class WeatherForecastWindPage extends WeatherForecastBasePage { 12 | const WeatherForecastWindPage( 13 | WeatherForecastHolder? holder, 14 | double? width, 15 | double? height, 16 | bool isMetricUnits, { 17 | Key? key, 18 | }) : super( 19 | holder: holder, 20 | width: width, 21 | height: height, 22 | isMetricUnits: isMetricUnits, 23 | key: key, 24 | ); 25 | 26 | @override 27 | Row getBottomRowWidget(BuildContext context) { 28 | final List rowElements = []; 29 | final List points = getChartData().points!; 30 | if (points.length > 2) { 31 | final double padding = points[1].x - points[0].x - 30; 32 | for (final String direction in holder!.getWindDirectionList()) { 33 | rowElements.add( 34 | SizedBox( 35 | width: 30, 36 | child: Center( 37 | child: Text(direction, 38 | textDirection: TextDirection.ltr, 39 | style: Theme.of(context).textTheme.bodyText1), 40 | ), 41 | ), 42 | ); 43 | rowElements.add(SizedBox(width: padding)); 44 | } 45 | rowElements.removeLast(); 46 | } 47 | return Row( 48 | key: const Key("weather_forecast_wind_page_bottom_row"), 49 | mainAxisAlignment: MainAxisAlignment.center, 50 | children: rowElements, 51 | ); 52 | } 53 | 54 | @override 55 | ChartData getChartData() { 56 | return super 57 | .holder! 58 | .setupChartData(ChartDataType.wind, width!, height!, isMetricUnits); 59 | } 60 | 61 | @override 62 | String getIcon() { 63 | return Assets.iconWind; 64 | } 65 | 66 | @override 67 | RichText getPageSubtitleWidget(BuildContext context) { 68 | var minWind = holder!.minWind; 69 | var maxWind = holder!.maxWind; 70 | if (isMetricUnits) { 71 | minWind = 72 | WeatherHelper.convertMetersPerSecondToKilometersPerHour(minWind); 73 | maxWind = 74 | WeatherHelper.convertMetersPerSecondToKilometersPerHour(maxWind); 75 | } else { 76 | minWind = WeatherHelper.convertMetersPerSecondToMilesPerHour(minWind); 77 | maxWind = WeatherHelper.convertMetersPerSecondToMilesPerHour(maxWind); 78 | } 79 | 80 | return RichText( 81 | key: const Key("weather_forecast_wind_page_subtitle"), 82 | textDirection: TextDirection.ltr, 83 | text: TextSpan( 84 | children: [ 85 | TextSpan(text: 'min ', style: Theme.of(context).textTheme.bodyText1), 86 | TextSpan( 87 | text: WeatherHelper.formatWind(minWind, isMetricUnits), 88 | style: Theme.of(context).textTheme.subtitle2), 89 | TextSpan( 90 | text: ' max ', style: Theme.of(context).textTheme.bodyText1), 91 | TextSpan( 92 | text: WeatherHelper.formatWind(maxWind, isMetricUnits), 93 | style: Theme.of(context).textTheme.subtitle2) 94 | ], 95 | ), 96 | ); 97 | } 98 | 99 | @override 100 | String? getTitleText(BuildContext context) { 101 | return AppLocalizations.of(context)!.wind; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/ui/main/bloc/main_screen_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class MainScreenEvent extends Equatable { 4 | const MainScreenEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class LocationCheckMainScreenEvent extends MainScreenEvent {} 11 | 12 | class LoadWeatherDataMainScreenEvent extends MainScreenEvent {} 13 | -------------------------------------------------------------------------------- /lib/src/ui/main/bloc/main_screen_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:feather/src/data/model/internal/application_error.dart'; 3 | import 'package:feather/src/data/model/internal/unit.dart'; 4 | import 'package:feather/src/data/model/remote/weather_forecast_list_response.dart'; 5 | import 'package:feather/src/data/model/remote/weather_response.dart'; 6 | 7 | abstract class MainScreenState extends Equatable { 8 | final Unit? unit; 9 | 10 | const MainScreenState({this.unit}); 11 | 12 | @override 13 | List get props => [unit]; 14 | } 15 | 16 | class InitialMainScreenState extends MainScreenState {} 17 | 18 | class CheckingLocationMainScreenState extends MainScreenState {} 19 | 20 | class LocationServiceDisabledMainScreenState extends MainScreenState {} 21 | 22 | class PermissionNotGrantedMainScreenState extends MainScreenState { 23 | final bool permanentlyDeniedPermission; 24 | 25 | const PermissionNotGrantedMainScreenState(this.permanentlyDeniedPermission); 26 | } 27 | 28 | class LoadingMainScreenState extends MainScreenState {} 29 | 30 | class SuccessLoadMainScreenState extends MainScreenState { 31 | final WeatherResponse weatherResponse; 32 | final WeatherForecastListResponse weatherForecastListResponse; 33 | 34 | const SuccessLoadMainScreenState( 35 | this.weatherResponse, this.weatherForecastListResponse); 36 | 37 | @override 38 | List get props => [unit, weatherResponse]; 39 | } 40 | 41 | class FailedLoadMainScreenState extends MainScreenState { 42 | final ApplicationError applicationError; 43 | 44 | const FailedLoadMainScreenState(this.applicationError); 45 | 46 | @override 47 | List get props => [unit, applicationError]; 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/ui/main/widget/sun_path_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:feather/src/data/repository/local/weather_helper.dart'; 4 | import 'package:feather/src/ui/widget/animated_state.dart'; 5 | import 'package:feather/src/utils/date_time_helper.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter/widgets.dart'; 8 | 9 | class SunPathWidget extends StatefulWidget { 10 | final int? sunrise; 11 | final int? sunset; 12 | 13 | const SunPathWidget({Key? key, this.sunrise, this.sunset}) : super(key: key); 14 | 15 | @override 16 | State createState() => _SunPathWidgetState(); 17 | } 18 | 19 | class _SunPathWidgetState extends AnimatedState { 20 | double _fraction = 0.0; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | animateTween(duration: 2000); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return SizedBox( 31 | key: const Key("sun_path_widget_sized_box"), 32 | width: 300, 33 | height: 150, 34 | child: CustomPaint( 35 | key: const Key("sun_path_widget_custom_paint"), 36 | painter: _SunPathPainter(widget.sunrise, widget.sunset, _fraction), 37 | ), 38 | ); 39 | } 40 | 41 | @override 42 | void dispose() { 43 | super.dispose(); 44 | } 45 | 46 | @override 47 | void onAnimatedValue(double value) { 48 | setState(() { 49 | _fraction = value; 50 | }); 51 | } 52 | } 53 | 54 | class _SunPathPainter extends CustomPainter { 55 | final double fraction; 56 | final double pi = 3.14159; 57 | final int dayAsMs = 86400000; 58 | final int? sunrise; 59 | final int? sunset; 60 | 61 | _SunPathPainter(this.sunrise, this.sunset, this.fraction); 62 | 63 | @override 64 | void paint(Canvas canvas, Size size) { 65 | final Paint arcPaint = _getArcPaint(); 66 | final Rect rect = Rect.fromLTWH(0, 5, size.width, size.height * 2); 67 | canvas.drawArc(rect, 0, -pi, false, arcPaint); 68 | final Paint circlePaint = _getCirclePaint(); 69 | canvas.drawCircle(_getPosition(fraction), 10, circlePaint); 70 | } 71 | 72 | @override 73 | bool shouldRepaint(_SunPathPainter oldDelegate) { 74 | return oldDelegate.fraction != fraction; 75 | } 76 | 77 | Paint _getArcPaint() { 78 | final Paint paint = Paint(); 79 | paint.color = Colors.white; 80 | paint.strokeWidth = 2; 81 | paint.style = PaintingStyle.stroke; 82 | return paint; 83 | } 84 | 85 | Paint _getCirclePaint() { 86 | final Paint circlePaint = Paint(); 87 | final int mode = 88 | WeatherHelper.getDayModeFromSunriseSunset(sunrise!, sunset); 89 | if (mode == 0) { 90 | circlePaint.color = Colors.yellow; 91 | } else { 92 | circlePaint.color = Colors.white; 93 | } 94 | return circlePaint; 95 | } 96 | 97 | Offset _getPosition(double fraction) { 98 | final int now = DateTimeHelper.getCurrentTime(); 99 | final int mode = 100 | WeatherHelper.getDayModeFromSunriseSunset(sunrise!, sunset); 101 | double difference = 0; 102 | if (mode == 0) { 103 | difference = (now - sunrise!) / (sunset! - sunrise!); 104 | } else if (mode == 1) { 105 | final DateTime nextSunrise = 106 | DateTime.fromMillisecondsSinceEpoch(sunrise! + dayAsMs); 107 | difference = 108 | (now - sunset!) / (nextSunrise.millisecondsSinceEpoch - sunset!); 109 | } else if (mode == -1) { 110 | final DateTime previousSunset = 111 | DateTime.fromMillisecondsSinceEpoch(sunset! - dayAsMs); 112 | difference = 1 - 113 | ((sunrise! - now) / 114 | (sunrise! - previousSunset.millisecondsSinceEpoch)); 115 | } 116 | 117 | final x = 150 * cos((1 + difference * fraction) * pi) + 150; 118 | final y = 145 * sin((1 + difference * fraction) * pi) + 150; 119 | return Offset(x, y); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/src/ui/main/widget/weather_forecast_thumbnail_list_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 2 | import 'package:feather/src/data/model/remote/system.dart'; 3 | import 'package:feather/src/data/model/remote/weather_forecast_list_response.dart'; 4 | import 'package:feather/src/data/model/remote/weather_forecast_response.dart'; 5 | import 'package:feather/src/data/repository/local/weather_helper.dart'; 6 | import 'package:feather/src/ui/app/app_bloc.dart'; 7 | import 'package:feather/src/ui/main/widget/weather_forecast_thumbnail_widget.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/widgets.dart'; 10 | import 'package:flutter_bloc/flutter_bloc.dart'; 11 | 12 | class WeatherForecastThumbnailListWidget extends StatefulWidget { 13 | final System? system; 14 | final WeatherForecastListResponse? forecastListResponse; 15 | 16 | const WeatherForecastThumbnailListWidget( 17 | {Key? key, this.system, this.forecastListResponse}) 18 | : super(key: key); 19 | 20 | @override 21 | State createState() { 22 | return WeatherForecastThumbnailListWidgetState(); 23 | } 24 | } 25 | 26 | class WeatherForecastThumbnailListWidgetState 27 | extends State { 28 | late AppBloc _appBloc; 29 | 30 | @override 31 | void initState() { 32 | _appBloc = BlocProvider.of(context); 33 | super.initState(); 34 | } 35 | 36 | @override 37 | void dispose() { 38 | super.dispose(); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | ///If forecast list response is empty, then it won't be displayed. 44 | ///MainScreen will present error to user. 45 | if (widget.forecastListResponse != null) { 46 | return buildForecastWeatherContainer(widget.forecastListResponse!); 47 | } else { 48 | return const SizedBox(); 49 | } 50 | } 51 | 52 | Widget buildForecastWeatherContainer( 53 | WeatherForecastListResponse forecastListResponse) { 54 | final List forecastList = 55 | forecastListResponse.list!; 56 | final map = WeatherHelper.getMapForecastsForSameDay(forecastList); 57 | return Row( 58 | key: const Key("weather_forecast_thumbnail_list_widget_container"), 59 | textDirection: TextDirection.ltr, 60 | mainAxisAlignment: MainAxisAlignment.center, 61 | children: buildForecastWeatherWidgets(map, forecastListResponse), 62 | ); 63 | } 64 | 65 | List buildForecastWeatherWidgets( 66 | Map> map, 67 | WeatherForecastListResponse? data) { 68 | final List forecastWidgets = []; 69 | map.forEach((key, value) { 70 | forecastWidgets.add(WeatherForecastThumbnailWidget( 71 | WeatherForecastHolder(value, data!.city, widget.system), 72 | _appBloc.isMetricUnits(), 73 | )); 74 | }); 75 | return forecastWidgets; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/ui/main/widget/weather_forecast_thumbnail_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 2 | import 'package:feather/src/data/repository/local/weather_helper.dart'; 3 | import 'package:feather/src/ui/navigation/bloc/navigation_bloc.dart'; 4 | import 'package:feather/src/ui/navigation/bloc/navigation_event.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/widgets.dart'; 7 | import 'package:flutter_bloc/flutter_bloc.dart'; 8 | 9 | class WeatherForecastThumbnailWidget extends StatefulWidget { 10 | final WeatherForecastHolder _holder; 11 | final bool _isMetricUnits; 12 | 13 | const WeatherForecastThumbnailWidget( 14 | this._holder, 15 | this._isMetricUnits, { 16 | Key? key, 17 | }) : super(key: key); 18 | 19 | @override 20 | _WeatherForecastThumbnailWidgetState createState() => 21 | _WeatherForecastThumbnailWidgetState(); 22 | } 23 | 24 | class _WeatherForecastThumbnailWidgetState 25 | extends State { 26 | late NavigationBloc _navigationBloc; 27 | 28 | @override 29 | void initState() { 30 | _navigationBloc = BlocProvider.of(context); 31 | super.initState(); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | final holder = widget._holder; 37 | var temperature = holder.averageTemperature; 38 | if (!widget._isMetricUnits) { 39 | temperature = WeatherHelper.convertCelsiusToFahrenheit(temperature!); 40 | } 41 | 42 | return Material( 43 | key: const Key("weather_forecast_thumbnail_widget"), 44 | color: Colors.transparent, 45 | child: Directionality( 46 | textDirection: TextDirection.ltr, 47 | child: InkWell( 48 | onTap: _onWeatherForecastClicked, 49 | child: Container( 50 | padding: const EdgeInsets.only(left: 4, right: 4, top: 8), 51 | child: Column( 52 | children: [ 53 | Text(holder.dateShortFormatted!, 54 | key: const Key("weather_forecast_thumbnail_date"), 55 | textDirection: TextDirection.ltr, 56 | style: Theme.of(context).textTheme.bodyText2), 57 | const SizedBox(height: 4), 58 | Image.asset(holder.weatherCodeAsset!, 59 | key: const Key("weather_forecast_thumbnail_icon"), 60 | width: 30, 61 | height: 30), 62 | const SizedBox(height: 4), 63 | Text( 64 | WeatherHelper.formatTemperature( 65 | temperature: temperature, 66 | metricUnits: widget._isMetricUnits, 67 | ), 68 | key: const Key("weather_forecast_thumbnail_temperature"), 69 | textDirection: TextDirection.ltr, 70 | style: Theme.of(context).textTheme.bodyText2), 71 | const SizedBox(height: 4), 72 | ], 73 | ), 74 | ), 75 | ), 76 | ), 77 | ); 78 | } 79 | 80 | void _onWeatherForecastClicked() { 81 | _navigationBloc.add(ForecastScreenNavigationEvent(widget._holder)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/ui/navigation/bloc/navigation_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/forecast_navigation_params.dart'; 2 | import 'package:feather/src/data/model/internal/settings_navigation_params.dart'; 3 | import 'package:feather/src/data/model/internal/navigation_route.dart'; 4 | import 'package:feather/src/ui/navigation/bloc/navigation_event.dart'; 5 | import 'package:feather/src/ui/navigation/navigation_provider.dart'; 6 | import 'package:feather/src/ui/navigation/bloc/navigation_state.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_bloc/flutter_bloc.dart'; 9 | 10 | class NavigationBloc extends Bloc { 11 | final NavigationProvider _navigationProvider; 12 | final GlobalKey _navigatorKey; 13 | 14 | NavigationBloc( 15 | this._navigationProvider, 16 | this._navigatorKey, 17 | ) : super(const NavigationState(NavigationRoute.mainScreen)); 18 | 19 | @override 20 | Stream mapEventToState(NavigationEvent event) async* { 21 | if (event is MainScreenNavigationEvent) { 22 | _navigateToPath("/"); 23 | yield const NavigationState(NavigationRoute.mainScreen); 24 | } 25 | if (event is ForecastScreenNavigationEvent) { 26 | _navigateToPath( 27 | "/forecast", 28 | routeSettings: RouteSettings( 29 | arguments: ForecastNavigationParams(event.holder), 30 | ), 31 | ); 32 | yield const NavigationState(NavigationRoute.forecastScreen); 33 | } 34 | if (event is AboutScreenNavigationEvent) { 35 | _navigateToPath( 36 | "/about", 37 | routeSettings: RouteSettings( 38 | arguments: SettingsNavigationParams(event.startGradientColors), 39 | ), 40 | ); 41 | yield const NavigationState( 42 | NavigationRoute.aboutScreen, 43 | ); 44 | } 45 | if (event is SettingsScreenNavigationEvent) { 46 | _navigateToPath( 47 | "/settings", 48 | routeSettings: RouteSettings( 49 | arguments: SettingsNavigationParams(event.startGradientColors), 50 | ), 51 | ); 52 | yield const NavigationState(NavigationRoute.settingsScreen); 53 | } 54 | } 55 | 56 | void _navigateToPath(String path, {RouteSettings? routeSettings}) { 57 | _navigationProvider.navigateToPath(path, _navigatorKey, 58 | routeSettings: routeSettings); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/ui/navigation/bloc/navigation_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | abstract class NavigationEvent extends Equatable { 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class MainScreenNavigationEvent extends NavigationEvent {} 11 | 12 | class ForecastScreenNavigationEvent extends NavigationEvent { 13 | final WeatherForecastHolder holder; 14 | 15 | ForecastScreenNavigationEvent(this.holder); 16 | } 17 | 18 | class AboutScreenNavigationEvent extends NavigationEvent { 19 | final List startGradientColors; 20 | 21 | AboutScreenNavigationEvent(this.startGradientColors); 22 | } 23 | 24 | class SettingsScreenNavigationEvent extends NavigationEvent { 25 | final List startGradientColors; 26 | 27 | SettingsScreenNavigationEvent(this.startGradientColors); 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/ui/navigation/bloc/navigation_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:feather/src/data/model/internal/navigation_route.dart'; 3 | 4 | class NavigationState extends Equatable { 5 | final NavigationRoute navigationRoute; 6 | 7 | const NavigationState(this.navigationRoute); 8 | 9 | @override 10 | List get props => [ 11 | navigationRoute, 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/ui/navigation/navigation_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/forecast_navigation_params.dart'; 2 | import 'package:feather/src/data/model/internal/settings_navigation_params.dart'; 3 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 4 | import 'package:feather/src/ui/forecast/weather_forecast_screen.dart'; 5 | import 'package:feather/src/ui/main/main_screen.dart'; 6 | import 'package:feather/src/ui/about/about_screen.dart'; 7 | import 'package:feather/src/ui/settings/settings_screen.dart'; 8 | import 'package:fluro/fluro.dart'; 9 | import 'package:flutter/material.dart'; 10 | 11 | class NavigationProvider { 12 | final router = FluroRouter(); 13 | 14 | final _mainScreenHandler = Handler( 15 | handlerFunc: (BuildContext? context, Map params) { 16 | return const MainScreen(); 17 | }); 18 | 19 | final _forecastScreenHandler = Handler( 20 | handlerFunc: (BuildContext? context, Map params) { 21 | WeatherForecastHolder? holder; 22 | if (context?.arguments != null) { 23 | final ForecastNavigationParams navigationParams = 24 | context!.arguments! as ForecastNavigationParams; 25 | holder = navigationParams.weatherForecastHolder; 26 | } 27 | if (holder != null) { 28 | return WeatherForecastScreen(holder); 29 | } else { 30 | throw ArgumentError("WeatherForecastHolder can't be null"); 31 | } 32 | }); 33 | final _aboutScreenHandler = Handler( 34 | handlerFunc: (BuildContext? context, Map params) { 35 | List startGradientColors = []; 36 | if (context?.arguments != null) { 37 | final SettingsNavigationParams navigationParams = 38 | context!.arguments! as SettingsNavigationParams; 39 | startGradientColors = navigationParams.startGradientColors!; 40 | } 41 | return AboutScreen( 42 | startGradientColors: startGradientColors, 43 | ); 44 | }); 45 | 46 | final _settingsScreenHandler = Handler( 47 | handlerFunc: (BuildContext? context, Map params) { 48 | List startGradientColors = []; 49 | if (context?.arguments != null) { 50 | final SettingsNavigationParams navigationParams = 51 | context!.arguments! as SettingsNavigationParams; 52 | startGradientColors = navigationParams.startGradientColors!; 53 | } 54 | return SettingsScreen( 55 | startGradientColors: startGradientColors, 56 | ); 57 | }); 58 | 59 | void defineRoutes() { 60 | router.define("/", handler: _mainScreenHandler); 61 | router.define("/forecast", handler: _forecastScreenHandler); 62 | router.define("/about", handler: _aboutScreenHandler); 63 | router.define("/settings", handler: _settingsScreenHandler); 64 | } 65 | 66 | void navigateToPath( 67 | String path, 68 | GlobalKey navigatorKey, { 69 | RouteSettings? routeSettings, 70 | }) { 71 | router.navigateTo( 72 | navigatorKey.currentState!.context, 73 | path, 74 | routeSettings: routeSettings, 75 | transition: TransitionType.material, 76 | transitionDuration: const Duration(milliseconds: 300), 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/ui/settings/bloc/settings_screen_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/repository/local/application_local_repository.dart'; 2 | import 'package:feather/src/ui/settings/bloc/settings_screen_event.dart'; 3 | import 'package:feather/src/ui/settings/bloc/settings_screen_state.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | 6 | class SettingsScreenBloc 7 | extends Bloc { 8 | final ApplicationLocalRepository _applicationLocalRepository; 9 | 10 | SettingsScreenBloc(this._applicationLocalRepository) 11 | : super(InitialSettingsScreenState()); 12 | 13 | @override 14 | Stream mapEventToState( 15 | SettingsScreenEvent event) async* { 16 | if (event is LoadSettingsScreenEvent) { 17 | final unit = await _applicationLocalRepository.getSavedUnit(); 18 | final savedRefreshTime = 19 | await _applicationLocalRepository.getSavedRefreshTime(); 20 | final lastRefreshedTime = 21 | await _applicationLocalRepository.getLastRefreshTime(); 22 | yield LoadedSettingsScreenState( 23 | unit, savedRefreshTime, lastRefreshedTime); 24 | } 25 | 26 | if (event is ChangeUnitsSettingsScreenEvent) { 27 | final unitsEvent = event; 28 | _applicationLocalRepository.saveUnit(unitsEvent.unit); 29 | if (state is LoadedSettingsScreenState) { 30 | final loadedState = state as LoadedSettingsScreenState; 31 | yield loadedState.copyWith(unit: unitsEvent.unit); 32 | } 33 | } 34 | 35 | if (event is ChangeRefreshTimeSettingsScreenEvent) { 36 | _applicationLocalRepository.saveRefreshTime(event.refreshTime); 37 | if (state is LoadedSettingsScreenState) { 38 | final loadedState = state as LoadedSettingsScreenState; 39 | yield loadedState.copyWith(refreshTime: event.refreshTime); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/ui/settings/bloc/settings_screen_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:feather/src/data/model/internal/unit.dart'; 3 | 4 | abstract class SettingsScreenEvent extends Equatable { 5 | @override 6 | List get props => []; 7 | } 8 | 9 | class LoadSettingsScreenEvent extends SettingsScreenEvent {} 10 | 11 | class ChangeUnitsSettingsScreenEvent extends SettingsScreenEvent { 12 | final Unit unit; 13 | 14 | ChangeUnitsSettingsScreenEvent(this.unit); 15 | 16 | @override 17 | List get props => [unit]; 18 | } 19 | 20 | class ChangeRefreshTimeSettingsScreenEvent extends SettingsScreenEvent { 21 | final int refreshTime; 22 | 23 | ChangeRefreshTimeSettingsScreenEvent(this.refreshTime); 24 | 25 | @override 26 | List get props => [refreshTime]; 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/ui/settings/bloc/settings_screen_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:feather/src/data/model/internal/unit.dart'; 3 | 4 | abstract class SettingsScreenState extends Equatable { 5 | const SettingsScreenState(); 6 | 7 | @override 8 | List get props => []; 9 | } 10 | 11 | class InitialSettingsScreenState extends SettingsScreenState {} 12 | 13 | class LoadingSettingsScreenState extends SettingsScreenState {} 14 | 15 | class LoadedSettingsScreenState extends SettingsScreenState { 16 | final Unit unit; 17 | final int refreshTime; 18 | final int lastRefreshTime; 19 | 20 | const LoadedSettingsScreenState( 21 | this.unit, this.refreshTime, this.lastRefreshTime); 22 | 23 | @override 24 | List get props => [unit, refreshTime, lastRefreshTime]; 25 | 26 | LoadedSettingsScreenState copyWith( 27 | {Unit? unit, int? refreshTime, int? lastRefreshTime}) { 28 | return LoadedSettingsScreenState( 29 | unit ?? this.unit, 30 | refreshTime ?? this.refreshTime, 31 | lastRefreshTime ?? this.lastRefreshTime, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/ui/widget/animated_gradient.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:feather/src/resources/config/application_colors.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class AnimatedGradientWidget extends StatefulWidget { 7 | final Duration duration; 8 | final List startGradientColors; 9 | 10 | const AnimatedGradientWidget({ 11 | Key? key, 12 | this.duration = const Duration(seconds: 1), 13 | this.startGradientColors = const [], 14 | }) : super(key: key); 15 | 16 | @override 17 | _AnimatedGradientWidgetState createState() => _AnimatedGradientWidgetState(); 18 | } 19 | 20 | class _AnimatedGradientWidgetState extends State { 21 | List colorList = [ 22 | ApplicationColors.dayStartColor, 23 | ApplicationColors.dayEndColor, 24 | ApplicationColors.midnightStartColor, 25 | ApplicationColors.midnightEndColor 26 | ]; 27 | List alignmentList = [ 28 | Alignment.bottomLeft, 29 | Alignment.bottomRight, 30 | Alignment.topRight, 31 | Alignment.topLeft, 32 | ]; 33 | int index = 0; 34 | Color bottomColor = ApplicationColors.dayStartColor; 35 | Color topColor = ApplicationColors.midnightEndColor; 36 | Alignment begin = Alignment.bottomLeft; 37 | Alignment end = Alignment.topRight; 38 | Timer? _startTimer; 39 | 40 | @override 41 | void initState() { 42 | super.initState(); 43 | if (widget.startGradientColors.isNotEmpty) { 44 | colorList.add( widget.startGradientColors[0]); 45 | colorList.add( widget.startGradientColors[1]); 46 | topColor = widget.startGradientColors[0]; 47 | bottomColor = widget.startGradientColors[1]; 48 | } 49 | 50 | _startTimer = Timer(const Duration(seconds: 1), (){ 51 | if (mounted) { 52 | setState(() { 53 | bottomColor = colorList[1]; 54 | }); 55 | } 56 | _startTimer?.cancel(); 57 | _startTimer = null; 58 | }); 59 | } 60 | 61 | @override 62 | void dispose() { 63 | _startTimer?.cancel(); 64 | _startTimer = null; 65 | super.dispose(); 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | return AnimatedContainer( 71 | duration: widget.duration, 72 | onEnd: () { 73 | setState(() { 74 | index = index + 1; 75 | bottomColor = colorList[index % colorList.length]; 76 | topColor = colorList[(index + 1) % colorList.length]; 77 | }); 78 | }, 79 | decoration: BoxDecoration( 80 | gradient: LinearGradient( 81 | begin: begin, 82 | end: end, 83 | colors: [bottomColor, topColor], 84 | ), 85 | ), 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/ui/widget/animated_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:feather/src/ui/widget/empty_animation.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | 6 | abstract class AnimatedState extends State 7 | with SingleTickerProviderStateMixin { 8 | AnimationController? controller; 9 | late StreamController _streamController; 10 | StreamSubscription? subscription; 11 | 12 | void animateTween({ 13 | double start = 0.0, 14 | double? end = 1.0, 15 | int duration = 1000, 16 | Curve curve = Curves.easeInOut, 17 | }) { 18 | controller = _getAnimationController(this, duration); 19 | final Animation animation = _getCurvedAnimation(controller!, curve); 20 | _streamController = StreamController(); 21 | 22 | final Animation tween = _getTween(start, end, animation); 23 | 24 | tween.addListener(() { 25 | _streamController.sink.add(tween.value); 26 | }); 27 | tween.addStatusListener((status) { 28 | if (status == AnimationStatus.completed || 29 | status == AnimationStatus.dismissed) { 30 | _streamController.close(); 31 | } 32 | }); 33 | subscription = _streamController.stream 34 | .listen((dynamic value) => onAnimatedValue(value as double)); 35 | controller!.forward(); 36 | } 37 | 38 | Animation setupAnimation( 39 | {Curve curve = Curves.easeInOut, 40 | int duration = 2000, 41 | bool noAnimation = false}) { 42 | controller ??= _getAnimationController(this, duration); 43 | controller!.forward(); 44 | if (!noAnimation) { 45 | return _getCurvedAnimation(controller!, curve) as Animation; 46 | } else { 47 | return EmptyAnimation(); 48 | } 49 | } 50 | 51 | AnimationController _getAnimationController( 52 | SingleTickerProviderStateMixin object, int duration) { 53 | return AnimationController( 54 | duration: Duration(milliseconds: duration), vsync: object); 55 | } 56 | 57 | Animation _getCurvedAnimation(AnimationController controller, Curve curve) { 58 | return CurvedAnimation(parent: controller, curve: curve); 59 | } 60 | 61 | static Animation _getTween( 62 | double start, double? end, Animation animation) { 63 | return Tween(begin: start, end: end) 64 | .animate(animation as Animation); 65 | } 66 | 67 | void onAnimatedValue(double value); 68 | 69 | @override 70 | void dispose() { 71 | if (controller != null) { 72 | controller!.dispose(); 73 | } 74 | subscription?.cancel(); 75 | super.dispose(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/ui/widget/animated_text_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/ui/widget/animated_state.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | 5 | class AnimatedTextWidget extends StatefulWidget { 6 | final String? textBefore; 7 | final double? maxValue; 8 | 9 | const AnimatedTextWidget({this.textBefore, this.maxValue, Key? key}) 10 | : super(key: key); 11 | 12 | @override 13 | State createState() => _AnimatedTextWidgetState(); 14 | } 15 | 16 | class _AnimatedTextWidgetState extends AnimatedState { 17 | double _value = 0; 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | animateTween(end: widget.maxValue, duration: 2000); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Text( 28 | "${widget.textBefore} ${_value.toStringAsFixed(0)}%", 29 | textDirection: TextDirection.ltr, 30 | style: Theme.of(context).textTheme.headline6, 31 | ); 32 | } 33 | 34 | @override 35 | void dispose() { 36 | super.dispose(); 37 | } 38 | 39 | @override 40 | void onAnimatedValue(double value) { 41 | setState(() { 42 | _value = value; 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/ui/widget/empty_animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/animation.dart'; 2 | 3 | class EmptyAnimation extends Animation { 4 | @override 5 | AnimationStatus get status => AnimationStatus.completed; 6 | 7 | @override 8 | double get value => 1; 9 | 10 | @override 11 | void addListener(dynamic listener) {} 12 | 13 | @override 14 | void addStatusListener(dynamic listener) {} 15 | 16 | @override 17 | void removeListener(dynamic listener) {} 18 | 19 | @override 20 | void removeStatusListener(dynamic listener) {} 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/ui/widget/loading_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoadingWidget extends StatelessWidget { 4 | 5 | const LoadingWidget({Key? key}) :super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return const Center( 10 | child: CircularProgressIndicator( 11 | valueColor: AlwaysStoppedAnimation( 12 | Colors.white, 13 | ), 14 | ), 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/ui/widget/transparent_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/overflow_menu_element.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class TransparentAppBar extends StatelessWidget { 6 | final bool withPopupMenu; 7 | final Function(PopupMenuElement value)? onPopupMenuClicked; 8 | 9 | const TransparentAppBar( 10 | {Key? key, this.withPopupMenu = false, this.onPopupMenuClicked}) 11 | : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Positioned( 16 | //Place it at the top, and not use the entire screen 17 | top: 0.0, 18 | left: 0.0, 19 | right: 0.0, 20 | child: AppBar( 21 | actions: [ 22 | if (withPopupMenu) 23 | Theme( 24 | data: Theme.of(context).copyWith( 25 | cardColor: Colors.white, 26 | ), 27 | child: PopupMenuButton( 28 | onSelected: (PopupMenuElement element) { 29 | if (onPopupMenuClicked != null) { 30 | onPopupMenuClicked!(element); 31 | } 32 | }, 33 | icon: const Icon( 34 | Icons.more_vert, 35 | color: Colors.white, 36 | ), 37 | itemBuilder: (BuildContext context) { 38 | return _getOverflowMenu(context) 39 | .map((PopupMenuElement element) { 40 | return PopupMenuItem( 41 | value: element, 42 | child: Text( 43 | element.title!, 44 | style: const TextStyle(color: Colors.black), 45 | ), 46 | ); 47 | }).toList(); 48 | }, 49 | )) 50 | ], 51 | backgroundColor: Colors.transparent, //No more green 52 | elevation: 0.0, //Shadow gone 53 | ), 54 | ); 55 | } 56 | 57 | List _getOverflowMenu(BuildContext context) { 58 | final applicationLocalization = AppLocalizations.of(context)!; 59 | final List menuList = []; 60 | menuList.add(PopupMenuElement( 61 | key: const Key("menu_overflow_settings"), 62 | title: applicationLocalization.settings, 63 | )); 64 | menuList.add(PopupMenuElement( 65 | key: const Key("menu_overflow_about"), 66 | title: applicationLocalization.about, 67 | )); 68 | return menuList; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/ui/widget/widget_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/resources/config/application_colors.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | 5 | class WidgetHelper { 6 | static LinearGradient buildGradient(Color startColor, Color endColor) { 7 | return LinearGradient( 8 | begin: Alignment.topCenter, 9 | end: Alignment.bottomCenter, 10 | stops: const [0.2, 0.99], 11 | colors: [ 12 | // Colors are easy thanks to Flutter's Colors class. 13 | startColor, 14 | endColor, 15 | ], 16 | ); 17 | } 18 | 19 | static LinearGradient buildGradientBasedOnDayCycle(int sunrise, int sunset) { 20 | final DateTime now = DateTime.now(); 21 | final int nowMs = now.millisecondsSinceEpoch; 22 | final int sunriseMs = sunrise * 1000; 23 | final int sunsetMs = sunset * 1000; 24 | 25 | if (nowMs < sunriseMs) { 26 | final int lastMidnight = 27 | DateTime(now.year, now.month, now.day).millisecondsSinceEpoch; 28 | return getNightGradient((sunriseMs - nowMs) / (sunriseMs - lastMidnight)); 29 | } else if (nowMs > sunsetMs) { 30 | final int nextMidnight = 31 | DateTime(now.year, now.month, now.day + 1).millisecondsSinceEpoch; 32 | return getNightGradient((nowMs - sunsetMs) / (nextMidnight - sunsetMs)); 33 | } else { 34 | return getDayGradient((nowMs - sunriseMs) / (sunsetMs - sunriseMs)); 35 | } 36 | } 37 | 38 | static LinearGradient getNightGradient(double percentage) { 39 | if (percentage <= 0.1) { 40 | return buildGradient(ApplicationColors.dawnDuskStartColor, 41 | ApplicationColors.dawnDuskEndColor); 42 | } else if (percentage <= 0.2) { 43 | return buildGradient(ApplicationColors.twilightStartColor, 44 | ApplicationColors.twilightEndColor); 45 | } else if (percentage <= 0.6) { 46 | return buildGradient( 47 | ApplicationColors.nightStartColor, ApplicationColors.nightEndColor); 48 | } else { 49 | return buildGradient(ApplicationColors.midnightStartColor, 50 | ApplicationColors.midnightEndColor); 51 | } 52 | } 53 | 54 | static LinearGradient getDayGradient(double percentage) { 55 | if (percentage <= 0.1 || percentage >= 0.9) { 56 | return buildGradient(ApplicationColors.dawnDuskStartColor, 57 | ApplicationColors.dawnDuskEndColor); 58 | } else if (percentage <= 0.2 || percentage >= 0.8) { 59 | return buildGradient(ApplicationColors.morningEveStartColor, 60 | ApplicationColors.morningEveEndColor); 61 | } else if (percentage <= 0.4 || percentage >= 0.6) { 62 | return buildGradient( 63 | ApplicationColors.dayStartColor, ApplicationColors.dayEndColor); 64 | } else { 65 | return buildGradient( 66 | ApplicationColors.middayStartColor, ApplicationColors.middayEndColor); 67 | } 68 | } 69 | 70 | static LinearGradient getGradient( 71 | {int? sunriseTime = 0, int? sunsetTime = 0}) { 72 | if (sunriseTime == 0 && sunsetTime == 0) { 73 | return buildGradient(ApplicationColors.midnightStartColor, 74 | ApplicationColors.midnightEndColor); 75 | } else { 76 | return buildGradientBasedOnDayCycle(sunriseTime!, sunsetTime!); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/utils/app_logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | 3 | class Log { 4 | static final Logger _logger = Logger( 5 | printer: PrefixPrinter( 6 | PrettyPrinter(colors: false, methodCount: 0), 7 | ), 8 | ); 9 | 10 | static void i(dynamic message) { 11 | _logger.i(message); 12 | } 13 | 14 | static void d(dynamic message) { 15 | _logger.d(message); 16 | } 17 | 18 | static void e(dynamic message, {dynamic error, StackTrace? stackTrace}) { 19 | _logger.e(message, error, stackTrace); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/utils/date_time_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | class DateTimeHelper{ 4 | static const int dayAsMs = 86400000; 5 | static const int hoursAsMs = 3600000; 6 | static const int minutesAsMs = 60000; 7 | static const int secondsAsMs = 1000; 8 | 9 | 10 | static String formatDateTime(DateTime dateTime){ 11 | return DateFormat('dd/MM/yyyy').format(DateTime.now()); 12 | } 13 | 14 | static int getCurrentTime(){ 15 | return DateTime.now().millisecondsSinceEpoch; 16 | } 17 | 18 | static String getTimeFormatted(DateTime dateTime) { 19 | final String hourFormatted = formatTimeUnit(dateTime.hour); 20 | final String minuteFormatted = formatTimeUnit(dateTime.minute); 21 | return "$hourFormatted:$minuteFormatted"; 22 | } 23 | 24 | static String formatTimeUnit(int timeUnit){ 25 | return timeUnit < 10 ? "0$timeUnit" : "$timeUnit"; 26 | } 27 | 28 | static String formatTime(int time) { 29 | final int hours = (time / hoursAsMs).floor(); 30 | final int minutes = ((time - hours * hoursAsMs) / minutesAsMs).floor(); 31 | final int seconds = 32 | ((time - hours * hoursAsMs - minutes * minutesAsMs) / secondsAsMs) 33 | .floor(); 34 | String text = ""; 35 | if (hours > 0) { 36 | text += "${formatTimeUnit(hours)}h "; 37 | } 38 | if (minutes > 0) { 39 | text += "${formatTimeUnit(minutes)}m "; 40 | } 41 | if (seconds >= 0) { 42 | text += "${formatTimeUnit(seconds)}s"; 43 | } 44 | return text; 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /lib/src/utils/types_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/utils/app_logger.dart'; 2 | 3 | class TypesHelper { 4 | static double toDouble(num? val) { 5 | try { 6 | if (val == null) { 7 | return 0; 8 | } 9 | if (val is double) { 10 | return val; 11 | } else { 12 | return val.toDouble(); 13 | } 14 | } catch (exception, stackTrace) { 15 | Log.e("toDouble failed: $exception $stackTrace"); 16 | return 0; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /media/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/1.png -------------------------------------------------------------------------------- /media/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/10.png -------------------------------------------------------------------------------- /media/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/11.png -------------------------------------------------------------------------------- /media/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/2.png -------------------------------------------------------------------------------- /media/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/3.png -------------------------------------------------------------------------------- /media/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/4.png -------------------------------------------------------------------------------- /media/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/5.png -------------------------------------------------------------------------------- /media/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/6.png -------------------------------------------------------------------------------- /media/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/7.png -------------------------------------------------------------------------------- /media/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/8.png -------------------------------------------------------------------------------- /media/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/9.png -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/logo.png -------------------------------------------------------------------------------- /media/video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhomlala/feather/c69cdc625dd630411dacc121854ddb5d3149e746/media/video.gif -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: feather 2 | description: Flutter weather application 3 | version: 2.0.0 4 | publish_to: none 5 | 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | flutter_localizations: 13 | sdk: flutter 14 | intl: ^0.17.0 15 | flutter_bloc: ^7.0.0 16 | equatable: ^2.0.0 17 | geolocator: ^7.0.3 18 | dio: ^4.0.0 19 | logger: ^1.0.0 20 | flutter_swiper_null_safety: 21 | git: https://github.com/lianyagang/flutter_swiper_null_safety.git 22 | shared_preferences: ^2.0.5 23 | package_info: ^2.0.0 24 | url_launcher: ^6.0.3 25 | pretty_dio_logger: ^1.2.0-beta-1 26 | fluro: ^2.0.3 27 | timeago: ^3.0.2 28 | app_settings: 4.1.0 29 | 30 | dev_dependencies: 31 | flutter_test: 32 | sdk: flutter 33 | test: ^1.16.5 34 | lint: ^1.5.3 35 | bloc_test: ^8.0.2 36 | mockito: ^5.0.9 37 | build_runner: ^2.0.4 38 | http_mock_adapter: ^0.2.1 39 | 40 | 41 | flutter: 42 | generate: true 43 | uses-material-design: true 44 | assets: 45 | - assets/ 46 | -------------------------------------------------------------------------------- /test/data/model/weather_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/remote/city.dart'; 2 | import 'package:feather/src/data/model/remote/clouds.dart'; 3 | import 'package:feather/src/data/model/remote/coordinates.dart'; 4 | import 'package:feather/src/data/model/remote/main_weather_data.dart'; 5 | import 'package:feather/src/data/model/remote/overall_weather_data.dart'; 6 | import 'package:feather/src/data/model/remote/system.dart'; 7 | import 'package:feather/src/data/model/remote/weather_forecast_list_response.dart'; 8 | import 'package:feather/src/data/model/remote/weather_response.dart'; 9 | import 'package:feather/src/data/model/remote/wind.dart'; 10 | 11 | class WeatherUtils { 12 | static WeatherResponse getWeather({int id = 0}) { 13 | final Wind wind = Wind(5, 200); 14 | final MainWeatherData mainWeatherData = 15 | MainWeatherData(0, 0, 0, 0, 0, 0, 0); 16 | final OverallWeatherData overallWeatherData = 17 | OverallWeatherData(0, "", "", ""); 18 | final List list = []; 19 | list.add(overallWeatherData); 20 | final System system = System("", 0, 0); 21 | final Coordinates coordinates = Coordinates(0, 0); 22 | final Clouds clouds = Clouds(0); 23 | final WeatherResponse weatherResponse = WeatherResponse( 24 | cord: coordinates, 25 | wind: wind, 26 | clouds: clouds, 27 | mainWeatherData: mainWeatherData, 28 | overallWeatherData: list, 29 | name: "", 30 | system: system, 31 | id: id, 32 | cod: 0, 33 | station: "", 34 | ); 35 | return weatherResponse; 36 | } 37 | 38 | static WeatherForecastListResponse getWeatherForecastListResponse( 39 | {int id = 0}) { 40 | return WeatherForecastListResponse([], City(id, "")); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/data/repository/local/application_local_repository_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/unit.dart'; 2 | import 'package:feather/src/data/repository/local/application_local_repository.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'fake_storage_manager.dart'; 6 | 7 | ///Tests for application_local_repository.dart 8 | void main() { 9 | late ApplicationLocalRepository applicationLocalRepository; 10 | 11 | setUp(() { 12 | applicationLocalRepository = 13 | ApplicationLocalRepository(FakeStorageManager()); 14 | }); 15 | 16 | group("Application Unit", () { 17 | test("getUnit returns default Unit", () async { 18 | final savedUnit = await applicationLocalRepository.getSavedUnit(); 19 | expect(savedUnit, Unit.imperial); 20 | }); 21 | 22 | test("getUnit returns saved Unit", () async { 23 | applicationLocalRepository.saveUnit(Unit.metric); 24 | var savedUnit = await applicationLocalRepository.getSavedUnit(); 25 | expect(savedUnit, Unit.metric); 26 | 27 | applicationLocalRepository.saveUnit(Unit.imperial); 28 | savedUnit = await applicationLocalRepository.getSavedUnit(); 29 | expect(savedUnit, Unit.imperial); 30 | }); 31 | }); 32 | 33 | group("Saved Refresh time", () { 34 | test("getSavedRefreshTime returns default time", () async { 35 | final savedRefreshTime = 36 | await applicationLocalRepository.getSavedRefreshTime(); 37 | expect(savedRefreshTime, 0); 38 | }); 39 | 40 | test("getSavedRefreshTime returns saved time", () async { 41 | var savedRefreshTime = 42 | await applicationLocalRepository.getSavedRefreshTime(); 43 | expect(savedRefreshTime, 0); 44 | final time = DateTime.now(); 45 | applicationLocalRepository.saveRefreshTime(time.millisecondsSinceEpoch); 46 | savedRefreshTime = await applicationLocalRepository.getSavedRefreshTime(); 47 | expect(savedRefreshTime, time.millisecondsSinceEpoch); 48 | }); 49 | }); 50 | 51 | group("Refresh time", () { 52 | test("getSavedRefreshTime returns default time", () async { 53 | final savedRefreshTime = 54 | await applicationLocalRepository.getSavedRefreshTime(); 55 | expect(savedRefreshTime, 0); 56 | }); 57 | 58 | test("getSavedRefreshTime returns saved time", () async { 59 | var savedRefreshTime = 60 | await applicationLocalRepository.getSavedRefreshTime(); 61 | expect(savedRefreshTime, 0); 62 | final time = DateTime.now(); 63 | applicationLocalRepository.saveRefreshTime(time.millisecondsSinceEpoch); 64 | savedRefreshTime = await applicationLocalRepository.getSavedRefreshTime(); 65 | expect(savedRefreshTime, time.millisecondsSinceEpoch); 66 | }); 67 | }); 68 | 69 | group("Last refresh time", () { 70 | test("getLastRefreshTime returns default time", () async { 71 | final savedLastRefreshTime = 72 | await applicationLocalRepository.getLastRefreshTime(); 73 | expect(savedLastRefreshTime, 0); 74 | }); 75 | 76 | test("getLastRefreshTime returns saved time", () async { 77 | var savedLastRefreshTime = 78 | await applicationLocalRepository.getLastRefreshTime(); 79 | expect(savedLastRefreshTime, 0); 80 | final time = DateTime.now(); 81 | applicationLocalRepository 82 | .saveLastRefreshTime(time.millisecondsSinceEpoch); 83 | savedLastRefreshTime = 84 | await applicationLocalRepository.getLastRefreshTime(); 85 | expect(savedLastRefreshTime, time.millisecondsSinceEpoch); 86 | }); 87 | }); 88 | } 89 | -------------------------------------------------------------------------------- /test/data/repository/local/fake_location_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/repository/local/location_provider.dart'; 2 | import 'package:geolocator/geolocator.dart'; 3 | 4 | ///Fake location provider which always return fake data. 5 | class FakeLocationProvider extends LocationProvider { 6 | final bool useSecondDataSet; 7 | 8 | LocationPermission _locationPermission = LocationPermission.always; 9 | 10 | LocationPermission _requestedLocationPermission = LocationPermission.always; 11 | bool _locationEnabled = true; 12 | 13 | FakeLocationProvider({this.useSecondDataSet = false}); 14 | 15 | @override 16 | Future providePosition() async { 17 | return Position( 18 | longitude: 0.0, 19 | latitude: 0.0, 20 | timestamp: DateTime.now(), 21 | altitude: 0.0, 22 | accuracy: 0.0, 23 | heading: 0.0, 24 | floor: 0, 25 | speed: 0.0, 26 | speedAccuracy: 0.0); 27 | } 28 | 29 | @override 30 | Future isLocationEnabled() async { 31 | return _locationEnabled; 32 | } 33 | 34 | @override 35 | Future checkLocationPermission() async { 36 | return _locationPermission; 37 | } 38 | 39 | @override 40 | Future requestLocationPermission() async { 41 | return _requestedLocationPermission; 42 | } 43 | 44 | // ignore: avoid_setters_without_getters 45 | set locationPermission(LocationPermission value) { 46 | _locationPermission = value; 47 | } 48 | 49 | // ignore: avoid_setters_without_getters 50 | set locationEnabled(bool value) { 51 | _locationEnabled = value; 52 | } 53 | 54 | // ignore: avoid_setters_without_getters 55 | set requestedLocationPermission(LocationPermission value) { 56 | _requestedLocationPermission = value; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/data/repository/local/fake_storage_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/geo_position.dart'; 2 | import 'package:feather/src/data/model/internal/unit.dart'; 3 | import 'package:feather/src/data/model/remote/weather_forecast_list_response.dart'; 4 | import 'package:feather/src/data/model/remote/weather_response.dart'; 5 | import 'package:feather/src/data/repository/local/storage_manager.dart'; 6 | 7 | import '../../model/weather_utils.dart'; 8 | import 'fake_storage_provider.dart'; 9 | 10 | ///Fake class which mocks StorageManager 11 | class FakeStorageManager extends StorageManager { 12 | Unit _unit = Unit.imperial; 13 | int _refreshTime = 0; 14 | int _lastRefreshTime = 0; 15 | GeoPosition _geoPosition = GeoPosition(0, 0); 16 | WeatherResponse? _weatherResponse = WeatherUtils.getWeather(); 17 | WeatherForecastListResponse? _weatherForecastListResponse = 18 | WeatherUtils.getWeatherForecastListResponse(); 19 | 20 | FakeStorageManager() : super(FakeStorageProvider()); 21 | 22 | @override 23 | Future getUnit() async { 24 | return _unit; 25 | } 26 | 27 | @override 28 | Future saveUnit(Unit unit) async { 29 | _unit = unit; 30 | return true; 31 | } 32 | 33 | @override 34 | Future saveRefreshTime(int refreshTime) async { 35 | _refreshTime = refreshTime; 36 | return true; 37 | } 38 | 39 | @override 40 | Future getRefreshTime() async { 41 | return _refreshTime; 42 | } 43 | 44 | @override 45 | Future saveLastRefreshTime(int lastRefreshTime) async { 46 | _lastRefreshTime = lastRefreshTime; 47 | return true; 48 | } 49 | 50 | @override 51 | Future getLastRefreshTime() async { 52 | return _lastRefreshTime; 53 | } 54 | 55 | @override 56 | Future saveLocation(GeoPosition geoPosition) async { 57 | _geoPosition = geoPosition; 58 | return true; 59 | } 60 | 61 | @override 62 | Future getLocation() async { 63 | return _geoPosition; 64 | } 65 | 66 | @override 67 | Future saveWeather(WeatherResponse weatherResponse) async { 68 | _weatherResponse = weatherResponse; 69 | return true; 70 | } 71 | 72 | @override 73 | Future getWeather() async { 74 | return _weatherResponse; 75 | } 76 | 77 | @override 78 | Future saveWeatherForecast( 79 | WeatherForecastListResponse weatherForecastListResponse) async { 80 | _weatherForecastListResponse = weatherForecastListResponse; 81 | return true; 82 | } 83 | 84 | @override 85 | Future getWeatherForecast() async { 86 | return _weatherForecastListResponse; 87 | } 88 | 89 | // ignore: avoid_setters_without_getters 90 | set weatherResponse(WeatherResponse? value) { 91 | _weatherResponse = value; 92 | } 93 | // ignore: avoid_setters_without_getters 94 | set weatherForecastListResponse(WeatherForecastListResponse? value) { 95 | _weatherForecastListResponse = value; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/data/repository/local/fake_storage_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:feather/src/data/repository/local/storage_provider.dart'; 4 | 5 | class FakeStorageProvider extends StorageProvider { 6 | final intMap = HashMap(); 7 | final stringMap = HashMap(); 8 | 9 | @override 10 | Future getInt(String key) async { 11 | return intMap[key]; 12 | } 13 | 14 | @override 15 | Future setInt(String key, int value) async { 16 | intMap[key] = value; 17 | return true; 18 | } 19 | 20 | @override 21 | Future getString(String key) async { 22 | return stringMap[key]; 23 | } 24 | 25 | @override 26 | Future setString(String key, String value) async { 27 | stringMap[key] = value; 28 | return true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/data/repository/local/location_manager_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/repository/local/location_manager.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'fake_location_provider.dart'; 5 | 6 | void main() { 7 | late LocationManager locationManager; 8 | setUp(() { 9 | locationManager = LocationManager(FakeLocationProvider()); 10 | }); 11 | 12 | group("Location", () { 13 | test("Last position is null when location wasn't selected", () { 14 | expect(locationManager.lastPosition, null); 15 | }); 16 | 17 | test("Get position returns value", () async { 18 | final location = await locationManager.getLocation(); 19 | expect(location != null, true); 20 | }); 21 | 22 | test("Last position is same as returned location by normal method", 23 | () async { 24 | final location = await locationManager.getLocation(); 25 | final lastLocation = locationManager.lastPosition; 26 | expect(location == lastLocation, true); 27 | }); 28 | 29 | test("Last position is cached", () async { 30 | final location = await locationManager.getLocation(); 31 | final location2 = await locationManager.getLocation(); 32 | expect(location == location2, true); 33 | }); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test/data/repository/local/weather_local_repository_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/geo_position.dart'; 2 | import 'package:feather/src/data/repository/local/weather_local_repository.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import '../../model/weather_utils.dart'; 6 | import 'fake_storage_manager.dart'; 7 | 8 | void main() { 9 | late WeatherLocalRepository weatherLocalRepository; 10 | 11 | setUp(() { 12 | weatherLocalRepository = WeatherLocalRepository(FakeStorageManager()); 13 | }); 14 | 15 | group("Location", () { 16 | test("get location returns default location", () async { 17 | final savedLocation = await weatherLocalRepository.getLocation(); 18 | expect(savedLocation != null, true); 19 | expect(savedLocation?.lat == 0, true); 20 | expect(savedLocation?.long == 0, true); 21 | }); 22 | 23 | test("save location saves location", () async { 24 | weatherLocalRepository.saveLocation(GeoPosition(1, 1)); 25 | final savedLocation = await weatherLocalRepository.getLocation(); 26 | expect(savedLocation != null, true); 27 | expect(savedLocation?.lat == 1, true); 28 | expect(savedLocation?.long == 1, true); 29 | }); 30 | }); 31 | 32 | group("Weather", () { 33 | test("get weather returns default weather data", () async { 34 | final savedWeather = await weatherLocalRepository.getWeather(); 35 | expect(savedWeather != null, true); 36 | expect(savedWeather?.id, WeatherUtils.getWeather().id); 37 | }); 38 | 39 | test("save weather saves weather data", () async { 40 | weatherLocalRepository.saveWeather(WeatherUtils.getWeather(id: 1)); 41 | final savedWeather = await weatherLocalRepository.getWeather(); 42 | expect(savedWeather != null, true); 43 | expect(savedWeather?.id == 1, true); 44 | }); 45 | }); 46 | 47 | group("Weather forecast", () { 48 | test("get weather forecast returns default weather data", () async { 49 | final savedWeatherForecast = 50 | await weatherLocalRepository.getWeatherForecast(); 51 | expect(savedWeatherForecast != null, true); 52 | expect(savedWeatherForecast?.city!.id == 0, true); 53 | }); 54 | 55 | test("save weather saves weather data", () async { 56 | weatherLocalRepository.saveWeatherForecast( 57 | WeatherUtils.getWeatherForecastListResponse(id: 1)); 58 | final savedWeatherForecast = 59 | await weatherLocalRepository.getWeatherForecast(); 60 | expect(savedWeatherForecast != null, true); 61 | expect(savedWeatherForecast?.city?.id == 1, true); 62 | }); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /test/data/repository/remote/fake_weather_api_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/application_error.dart'; 2 | import 'package:feather/src/data/model/remote/weather_forecast_list_response.dart'; 3 | import 'package:feather/src/data/model/remote/weather_response.dart'; 4 | import 'package:feather/src/data/repository/remote/weather_api_provider.dart'; 5 | 6 | import '../../model/weather_utils.dart'; 7 | 8 | class FakeWeatherApiProvider extends WeatherApiProvider { 9 | ApplicationError? _weatherError; 10 | ApplicationError? _weatherForecastError; 11 | 12 | @override 13 | Future fetchWeather( 14 | double? latitude, double? longitude) async { 15 | if (_weatherError != null) { 16 | return WeatherResponse.withErrorCode(_weatherError!); 17 | } 18 | return WeatherUtils.getWeather(); 19 | } 20 | 21 | @override 22 | Future fetchWeatherForecast( 23 | double? latitude, double? longitude) async { 24 | if (_weatherForecastError != null) { 25 | return WeatherForecastListResponse.withErrorCode(_weatherForecastError!); 26 | } 27 | return WeatherUtils.getWeatherForecastListResponse(); 28 | } 29 | 30 | // ignore: avoid_setters_without_getters 31 | set weatherForecastError(ApplicationError value) { 32 | _weatherForecastError = value; 33 | } 34 | 35 | // ignore: avoid_setters_without_getters 36 | set weatherError(ApplicationError value) { 37 | _weatherError = value; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/data/repository/remote/weather_remote_repository_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/remote/weather_forecast_list_response.dart'; 2 | import 'package:feather/src/data/model/remote/weather_response.dart'; 3 | import 'package:feather/src/data/repository/remote/weather_remote_repository.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import 'fake_weather_api_provider.dart'; 7 | 8 | void main() { 9 | late WeatherRemoteRepository weatherRepository; 10 | 11 | setUpAll(() { 12 | weatherRepository = WeatherRemoteRepository(FakeWeatherApiProvider()); 13 | }); 14 | 15 | group("Weather", () { 16 | test("Fetched weather not null and not empty", () async { 17 | final WeatherResponse weatherResponse = 18 | await weatherRepository.fetchWeather(0, 0); 19 | expect(weatherResponse.mainWeatherData != null, true); 20 | expect(weatherResponse.wind != null, true); 21 | expect(weatherResponse.clouds != null, true); 22 | expect(weatherResponse.overallWeatherData != null, true); 23 | expect(weatherResponse.system != null, true); 24 | expect(weatherResponse.cord != null, true); 25 | expect(weatherResponse.id != null, true); 26 | expect(weatherResponse.name != null, true); 27 | }); 28 | }); 29 | 30 | group("Forecast weather", () { 31 | test("Fetched weather forecast not null", () async { 32 | final WeatherForecastListResponse weatherForecastListResponse = 33 | await weatherRepository.fetchWeatherForecast(0, 0); 34 | expect(weatherForecastListResponse.city != null, true); 35 | }); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/test_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_localizations/flutter_localizations.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | 5 | class TestHelper { 6 | static Widget wrapWidgetWithLocalizationApp(Widget widget) { 7 | return MaterialApp( 8 | home: Container(child: widget), 9 | debugShowCheckedModeBanner: false, 10 | localizationsDelegates: const [ 11 | AppLocalizations.delegate, 12 | GlobalMaterialLocalizations.delegate, 13 | GlobalWidgetsLocalizations.delegate, 14 | GlobalCupertinoLocalizations.delegate, 15 | ], 16 | supportedLocales: const [ 17 | Locale("en"), 18 | Locale("pl"), 19 | ], 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/ui/about/about_screen_bloc_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/ui/about/bloc/about_screen_bloc.dart'; 2 | import 'package:feather/src/ui/about/bloc/about_screen_state.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | late AboutScreenBloc aboutScreenBloc; 7 | 8 | setUp(() { 9 | aboutScreenBloc = AboutScreenBloc(); 10 | }); 11 | 12 | test("Initial state is InitialAboutScreenState", () { 13 | expect(aboutScreenBloc.state, InitialAboutScreenState()); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /test/ui/about/about_screen_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/ui/about/about_screen.dart'; 2 | import 'package:feather/src/ui/about/bloc/about_screen_bloc.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 '../../test_helper.dart'; 7 | 8 | void main() { 9 | testWidgets("About screen should display widgets", 10 | (WidgetTester tester) async { 11 | await tester.pumpWidget( 12 | BlocProvider( 13 | create: (context) => AboutScreenBloc(), 14 | child: TestHelper.wrapWidgetWithLocalizationApp( 15 | const AboutScreen(), 16 | ), 17 | ), 18 | const Duration(seconds: 2), 19 | ); 20 | 21 | expect(find.byKey(const Key("about_screen_logo")), findsOneWidget); 22 | expect(find.byKey(const Key("about_screen_app_name")), findsOneWidget); 23 | expect(find.byKey(const Key("about_screen_app_version_and_build")), 24 | findsOneWidget); 25 | expect(find.byKey(const Key("about_screen_contributors")), findsOneWidget); 26 | expect(find.byKey(const Key("about_screen_credits")), findsOneWidget); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /test/ui/app/app_bloc_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:feather/src/data/model/internal/unit.dart'; 3 | import 'package:feather/src/data/repository/local/application_local_repository.dart'; 4 | import 'package:feather/src/ui/app/app_bloc.dart'; 5 | import 'package:feather/src/ui/app/app_event.dart'; 6 | import 'package:feather/src/ui/app/app_state.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | 9 | import '../../data/repository/local/fake_storage_manager.dart'; 10 | 11 | void main() { 12 | late FakeStorageManager _fakeStorageManager; 13 | late AppBloc _appBloc; 14 | 15 | setUpAll(() { 16 | _fakeStorageManager = FakeStorageManager(); 17 | _appBloc = buildAppBloc(fakeStorageManager: _fakeStorageManager); 18 | }); 19 | 20 | group("Initial unit settings", () { 21 | test("Initial state has metric unit", () { 22 | expect(_appBloc.state.unit, Unit.metric); 23 | }); 24 | }); 25 | 26 | group("Is metric units returns correct flag", () { 27 | test("Returns false for imperial unit", () async { 28 | _fakeStorageManager.saveUnit(Unit.imperial); 29 | _appBloc.add(LoadSettingsAppEvent()); 30 | await expectLater(_appBloc.stream, 31 | emitsInOrder([const AppState(Unit.imperial)])); 32 | expect(_appBloc.isMetricUnits(), equals(false)); 33 | }); 34 | 35 | test("Returns true for metric unit", () async { 36 | _fakeStorageManager.saveUnit(Unit.metric); 37 | _appBloc.add(LoadSettingsAppEvent()); 38 | await expectLater(_appBloc.stream, 39 | emitsInOrder([const AppState(Unit.metric)])); 40 | expect(_appBloc.isMetricUnits(), equals(true)); 41 | }); 42 | }); 43 | 44 | group("Updated bloc state", () { 45 | setUp(() { 46 | _fakeStorageManager.saveUnit(Unit.imperial); 47 | }); 48 | 49 | blocTest( 50 | "Load app settings updates unit", 51 | build: () => _appBloc, 52 | act: (AppBloc bloc) => bloc.add( 53 | LoadSettingsAppEvent(), 54 | ), 55 | expect: () => [ 56 | const AppState(Unit.imperial), 57 | ], 58 | ); 59 | }); 60 | } 61 | 62 | AppBloc buildAppBloc({FakeStorageManager? fakeStorageManager}) { 63 | return AppBloc( 64 | ApplicationLocalRepository( 65 | fakeStorageManager ?? FakeStorageManager(), 66 | ), 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /test/ui/forecast/chart_widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/chart_data.dart'; 2 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 3 | import 'package:feather/src/data/model/remote/city.dart'; 4 | import 'package:feather/src/data/model/remote/main_weather_data.dart'; 5 | import 'package:feather/src/data/model/remote/overall_weather_data.dart'; 6 | import 'package:feather/src/data/model/remote/rain.dart'; 7 | import 'package:feather/src/data/model/remote/system.dart'; 8 | import 'package:feather/src/data/model/remote/weather_forecast_response.dart'; 9 | import 'package:feather/src/data/model/remote/wind.dart'; 10 | import 'package:feather/src/ui/forecast/widget/chart_widget.dart'; 11 | import 'package:flutter/material.dart'; 12 | import 'package:flutter_test/flutter_test.dart'; 13 | 14 | import '../../test_helper.dart'; 15 | 16 | void main() { 17 | testWidgets("Chart widget should display chart", (WidgetTester tester) async { 18 | await tester.pumpWidget(TestHelper.wrapWidgetWithLocalizationApp( 19 | ChartWidget(chartData: setupChartData()))); 20 | 21 | expect(find.byKey(const Key("chart_widget_container")), findsOneWidget); 22 | expect(find.byKey(const Key("chart_widget_custom_paint")), findsOneWidget); 23 | }); 24 | 25 | testWidgets("Chart widget should not display chart", 26 | (WidgetTester tester) async { 27 | await tester.pumpWidget( 28 | TestHelper.wrapWidgetWithLocalizationApp( 29 | ChartWidget( 30 | chartData: setupEmptyChartData(), 31 | ), 32 | ), 33 | ); 34 | expect(find.byKey(const Key("chart_widget_container")), findsOneWidget); 35 | expect(find.byKey(const Key("chart_widget_custom_paint")), findsNothing); 36 | expect(find.byKey(const Key("chart_widget_unavailable")), findsOneWidget); 37 | }); 38 | } 39 | 40 | ChartData setupEmptyChartData() { 41 | return setupWeatherForecastHolder(1) 42 | .setupChartData(ChartDataType.rain, 300, 100, false); 43 | } 44 | 45 | ChartData setupChartData() { 46 | return setupWeatherForecastHolder(8) 47 | .setupChartData(ChartDataType.rain, 300, 100, false); 48 | } 49 | 50 | WeatherForecastHolder setupWeatherForecastHolder(int objectsCount) { 51 | final List forecastList = 52 | []; 53 | 54 | for (int index = 0; index < objectsCount; index++) { 55 | final DateTime dateTime = DateTime.utc(2019, 1, index + 1); 56 | forecastList.add(buildForecastResponseForDateTime(dateTime)); 57 | } 58 | final System system = System(null, 0, 0); 59 | final City city = City(0, null); 60 | final WeatherForecastHolder holder = 61 | WeatherForecastHolder(forecastList, city, system); 62 | return holder; 63 | } 64 | 65 | WeatherForecastResponse buildForecastResponseForDateTime(DateTime dateTime) { 66 | final Wind wind = Wind(5, 200); 67 | final MainWeatherData mainWeatherData = MainWeatherData(0, 0, 0, 0, 0, 0, 0); 68 | final OverallWeatherData overallWeatherData = 69 | OverallWeatherData(0, "", "", ""); 70 | final List list = []; 71 | list.add(overallWeatherData); 72 | final Rain rain = Rain(0); 73 | final Rain snow = Rain(0); 74 | return WeatherForecastResponse( 75 | mainWeatherData, list, null, wind, dateTime, rain, snow); 76 | } 77 | -------------------------------------------------------------------------------- /test/ui/forecast/weather_forecast_pressure_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 2 | import 'package:feather/src/data/model/remote/city.dart'; 3 | import 'package:feather/src/data/model/remote/main_weather_data.dart'; 4 | import 'package:feather/src/data/model/remote/overall_weather_data.dart'; 5 | import 'package:feather/src/data/model/remote/rain.dart'; 6 | import 'package:feather/src/data/model/remote/system.dart'; 7 | import 'package:feather/src/data/model/remote/weather_forecast_response.dart'; 8 | import 'package:feather/src/data/model/remote/wind.dart'; 9 | import 'package:feather/src/ui/forecast/widget/weather_forecast_pressure_page.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_test/flutter_test.dart'; 12 | 13 | import '../../test_helper.dart'; 14 | 15 | void main() { 16 | testWidgets("Pressure page should contains widgets", 17 | (WidgetTester tester) async { 18 | await tester.pumpWidget( 19 | TestHelper.wrapWidgetWithLocalizationApp( 20 | WeatherForecastPressurePage( 21 | setupWeatherForecastHolder(), 300, 100, true), 22 | ), 23 | ); 24 | 25 | expect(find.byKey(const Key("weather_forecast_base_page_icon")), 26 | findsOneWidget); 27 | expect(find.byKey(const Key("weather_forecast_base_page_title")), 28 | findsOneWidget); 29 | expect(find.byKey(const Key("weather_forecast_pressure_page_subtitle")), 30 | findsOneWidget); 31 | expect(find.byKey(const Key("weather_forecast_pressure_page_bottom_row")), 32 | findsOneWidget); 33 | expect(find.byKey(const Key("weather_forecast_base_page_title")), 34 | findsOneWidget); 35 | expect(find.byKey(const Key("chart_widget_container")), findsOneWidget); 36 | 37 | final Text title = tester 38 | .widget(find.byKey(const Key("weather_forecast_base_page_title"))); 39 | expect(title.data, "Pressure"); 40 | 41 | final RichText subtitle = tester.widget( 42 | find.byKey(const Key("weather_forecast_pressure_page_subtitle"))); 43 | final TextSpan textSpan = subtitle.text as TextSpan; 44 | expect(textSpan.text == null, true); 45 | expect(textSpan.children!.length == 4, true); 46 | 47 | expect(textSpan.children![0].toPlainText().contains("min"), true); 48 | expect(textSpan.children![1].toPlainText().contains("hPa"), true); 49 | expect(textSpan.children![2].toPlainText().contains("max"), true); 50 | expect(textSpan.children![3].toPlainText().contains("hPa"), true); 51 | 52 | final Row bottomRow = tester.widget( 53 | find.byKey(const Key("weather_forecast_pressure_page_bottom_row"))); 54 | expect(bottomRow.children.isEmpty, true); 55 | }); 56 | } 57 | 58 | WeatherForecastHolder setupWeatherForecastHolder() { 59 | final List forecastList = []; 60 | 61 | final Wind wind = Wind(5, 200); 62 | final MainWeatherData mainWeatherData = MainWeatherData(0, 0, 0, 0, 0, 0, 0); 63 | final OverallWeatherData overallWeatherData = 64 | OverallWeatherData(0, "", "", ""); 65 | final List list = []; 66 | list.add(overallWeatherData); 67 | final Rain rain = Rain(10); 68 | final Rain snow = Rain(10); 69 | 70 | forecastList.add(WeatherForecastResponse( 71 | mainWeatherData, list, null, wind, DateTime.now(), rain, snow)); 72 | 73 | final System system = System(null, 0, 0); 74 | final City city = City(0, null); 75 | 76 | final WeatherForecastHolder holder = 77 | WeatherForecastHolder(forecastList, city, system); 78 | 79 | return holder; 80 | } 81 | -------------------------------------------------------------------------------- /test/ui/forecast/weather_forecast_rain_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 2 | import 'package:feather/src/data/model/remote/city.dart'; 3 | import 'package:feather/src/data/model/remote/main_weather_data.dart'; 4 | import 'package:feather/src/data/model/remote/overall_weather_data.dart'; 5 | import 'package:feather/src/data/model/remote/rain.dart'; 6 | import 'package:feather/src/data/model/remote/system.dart'; 7 | import 'package:feather/src/data/model/remote/weather_forecast_response.dart'; 8 | import 'package:feather/src/data/model/remote/wind.dart'; 9 | import 'package:feather/src/ui/forecast/widget/weather_forecast_rain_page.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_test/flutter_test.dart'; 12 | 13 | import '../../test_helper.dart'; 14 | 15 | void main() { 16 | testWidgets("Rain page should contains widgets", (WidgetTester tester) async { 17 | await tester.pumpWidget( 18 | TestHelper.wrapWidgetWithLocalizationApp( 19 | WeatherForecastRainPage(setupWeatherForecastHolder(), 300, 100, false), 20 | ), 21 | ); 22 | 23 | expect(find.byKey(const Key("weather_forecast_base_page_icon")), 24 | findsOneWidget); 25 | expect(find.byKey(const Key("weather_forecast_base_page_title")), 26 | findsOneWidget); 27 | expect(find.byKey(const Key("weather_forecast_rain_page_subtitle")), 28 | findsOneWidget); 29 | expect(find.byKey(const Key("weather_forecast_rain_page_bottom_row")), 30 | findsOneWidget); 31 | expect(find.byKey(const Key("weather_forecast_base_page_title")), 32 | findsOneWidget); 33 | expect(find.byKey(const Key("chart_widget_container")), findsOneWidget); 34 | 35 | final Text title = tester 36 | .widget(find.byKey(const Key("weather_forecast_base_page_title"))); 37 | expect(title.data, "Rain"); 38 | 39 | final RichText subtitle = tester 40 | .widget(find.byKey(const Key("weather_forecast_rain_page_subtitle"))); 41 | final TextSpan textSpan = subtitle.text as TextSpan; 42 | expect(textSpan.text == null, true); 43 | expect(textSpan.children!.length == 4, true); 44 | expect(textSpan.children![0].toPlainText().contains("min"), true); 45 | expect(textSpan.children![1].toPlainText().contains("mm/h"), true); 46 | expect(textSpan.children![2].toPlainText().contains("max"), true); 47 | expect(textSpan.children![3].toPlainText().contains("mm/h"), true); 48 | }); 49 | } 50 | 51 | WeatherForecastHolder setupWeatherForecastHolder() { 52 | final List forecastList = []; 53 | 54 | final Wind wind = Wind(5, 200); 55 | final MainWeatherData mainWeatherData = MainWeatherData(0, 0, 0, 0, 0, 0, 0); 56 | final OverallWeatherData overallWeatherData = 57 | OverallWeatherData(0, "", "", ""); 58 | final List list = []; 59 | list.add(overallWeatherData); 60 | final Rain rain = Rain(10); 61 | final Rain snow = Rain(10); 62 | 63 | forecastList.add(WeatherForecastResponse( 64 | mainWeatherData, list, null, wind, DateTime.now(), rain, snow)); 65 | 66 | final System system = System(null, 0, 0); 67 | final City city = City(0, null); 68 | 69 | final WeatherForecastHolder holder = 70 | WeatherForecastHolder(forecastList, city, system); 71 | 72 | return holder; 73 | } 74 | -------------------------------------------------------------------------------- /test/ui/forecast/weather_forecast_temperature_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 2 | import 'package:feather/src/data/model/remote/city.dart'; 3 | import 'package:feather/src/data/model/remote/main_weather_data.dart'; 4 | import 'package:feather/src/data/model/remote/overall_weather_data.dart'; 5 | import 'package:feather/src/data/model/remote/rain.dart'; 6 | import 'package:feather/src/data/model/remote/system.dart'; 7 | import 'package:feather/src/data/model/remote/weather_forecast_response.dart'; 8 | import 'package:feather/src/data/model/remote/wind.dart'; 9 | import 'package:feather/src/ui/forecast/widget/weather_forecast_temperature_page.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_test/flutter_test.dart'; 12 | 13 | import '../../test_helper.dart'; 14 | 15 | void main() { 16 | testWidgets("Temperature page should contains widgets", 17 | (WidgetTester tester) async { 18 | await tester.pumpWidget( 19 | TestHelper.wrapWidgetWithLocalizationApp( 20 | WeatherForecastTemperaturePage( 21 | setupWeatherForecastHolder(), 300, 100, false), 22 | ), 23 | ); 24 | 25 | expect(find.byKey(const Key("weather_forecast_base_page_icon")), 26 | findsOneWidget); 27 | expect(find.byKey(const Key("weather_forecast_base_page_title")), 28 | findsOneWidget); 29 | expect(find.byKey(const Key("weather_forecast_temperature_page_subtitle")), 30 | findsOneWidget); 31 | expect( 32 | find.byKey(const Key("weather_forecast_temperature_page_bottom_row")), 33 | findsOneWidget); 34 | expect(find.byKey(const Key("weather_forecast_base_page_title")), 35 | findsOneWidget); 36 | expect(find.byKey(const Key("chart_widget_container")), findsOneWidget); 37 | 38 | final Text title = tester 39 | .widget(find.byKey(const Key("weather_forecast_base_page_title"))); 40 | expect(title.data, "Temperature"); 41 | 42 | final RichText subtitle = tester.widget( 43 | find.byKey(const Key("weather_forecast_temperature_page_subtitle"))); 44 | final TextSpan textSpan = subtitle.text as TextSpan; 45 | expect(textSpan.text == null, true); 46 | expect(textSpan.children!.length == 4, true); 47 | expect(textSpan.children![0].toPlainText().contains("min"), true); 48 | expect(textSpan.children![1].toPlainText().contains("F"), true); 49 | expect(textSpan.children![2].toPlainText().contains("max"), true); 50 | expect(textSpan.children![3].toPlainText().contains("F"), true); 51 | 52 | final Row bottomRow = tester.widget( 53 | find.byKey(const Key("weather_forecast_temperature_page_bottom_row"))); 54 | expect(bottomRow.children.isEmpty, true); 55 | }); 56 | } 57 | 58 | WeatherForecastHolder setupWeatherForecastHolder() { 59 | final List forecastList = []; 60 | 61 | final Wind wind = Wind(5, 200); 62 | final MainWeatherData mainWeatherData = MainWeatherData(0, 0, 0, 0, 0, 0, 0); 63 | final OverallWeatherData overallWeatherData = 64 | OverallWeatherData(0, "", "", ""); 65 | final List list = []; 66 | list.add(overallWeatherData); 67 | final Rain rain = Rain(10); 68 | final Rain snow = Rain(10); 69 | 70 | forecastList.add(WeatherForecastResponse( 71 | mainWeatherData, list, null, wind, DateTime.now(), rain, snow)); 72 | 73 | final System system = System(null, 0, 0); 74 | final City city = City(0, null); 75 | 76 | final WeatherForecastHolder holder = 77 | WeatherForecastHolder(forecastList, city, system); 78 | 79 | return holder; 80 | } 81 | -------------------------------------------------------------------------------- /test/ui/forecast/weather_forecast_widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 2 | import 'package:feather/src/data/model/remote/city.dart'; 3 | import 'package:feather/src/data/model/remote/main_weather_data.dart'; 4 | import 'package:feather/src/data/model/remote/overall_weather_data.dart'; 5 | import 'package:feather/src/data/model/remote/rain.dart'; 6 | import 'package:feather/src/data/model/remote/system.dart'; 7 | import 'package:feather/src/data/model/remote/weather_forecast_response.dart'; 8 | import 'package:feather/src/data/model/remote/wind.dart'; 9 | import 'package:feather/src/ui/forecast/widget/weather_forecast_widget.dart'; 10 | import 'package:flutter/widgets.dart'; 11 | import 'package:flutter_test/flutter_test.dart'; 12 | 13 | import '../../test_helper.dart'; 14 | 15 | void main() { 16 | testWidgets("Wind page should contains widgets", (WidgetTester tester) async { 17 | await tester.pumpWidget( 18 | TestHelper.wrapWidgetWithLocalizationApp( 19 | WeatherForecastWidget( 20 | holder: setupWeatherForecastHolder(), 21 | width: 300, 22 | height: 100, 23 | isMetricUnits: false, 24 | ), 25 | ), 26 | ); 27 | expect(find.byKey(const Key("weather_forecast_container")), findsOneWidget); 28 | expect(find.byKey(const Key("weather_forecast_location_name")), 29 | findsOneWidget); 30 | expect(find.byKey(const Key("weather_forecast_date_formatted")), 31 | findsOneWidget); 32 | expect(find.byKey(const Key("weather_forecast_swiper")), findsOneWidget); 33 | }); 34 | } 35 | 36 | WeatherForecastHolder setupWeatherForecastHolder() { 37 | final List forecastList = []; 38 | 39 | final Wind wind = Wind(5, 200); 40 | final MainWeatherData mainWeatherData = MainWeatherData(0, 0, 0, 0, 0, 0, 0); 41 | final OverallWeatherData overallWeatherData = 42 | OverallWeatherData(0, "", "", ""); 43 | final List list = []; 44 | list.add(overallWeatherData); 45 | final Rain rain = Rain(10); 46 | final Rain snow = Rain(10); 47 | 48 | forecastList.add(WeatherForecastResponse( 49 | mainWeatherData, list, null, wind, DateTime.now(), rain, snow)); 50 | 51 | final System system = System(null, 0, 0); 52 | final City city = City(0, null); 53 | 54 | final WeatherForecastHolder holder = 55 | WeatherForecastHolder(forecastList, city, system); 56 | 57 | return holder; 58 | } 59 | -------------------------------------------------------------------------------- /test/ui/forecast/weather_forecast_wind_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 2 | import 'package:feather/src/data/model/remote/city.dart'; 3 | import 'package:feather/src/data/model/remote/main_weather_data.dart'; 4 | import 'package:feather/src/data/model/remote/overall_weather_data.dart'; 5 | import 'package:feather/src/data/model/remote/rain.dart'; 6 | import 'package:feather/src/data/model/remote/system.dart'; 7 | import 'package:feather/src/data/model/remote/weather_forecast_response.dart'; 8 | import 'package:feather/src/data/model/remote/wind.dart'; 9 | import 'package:feather/src/ui/forecast/widget/weather_forecast_wind_page.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_test/flutter_test.dart'; 12 | 13 | import '../../test_helper.dart'; 14 | 15 | void main() { 16 | testWidgets("Wind page should contains widgets", (WidgetTester tester) async { 17 | await tester.pumpWidget( 18 | TestHelper.wrapWidgetWithLocalizationApp( 19 | WeatherForecastWindPage(setupWeatherForecastHolder(), 300, 100, true), 20 | ), 21 | ); 22 | 23 | expect(find.byKey(const Key("weather_forecast_base_page_icon")), 24 | findsOneWidget); 25 | expect(find.byKey(const Key("weather_forecast_base_page_title")), 26 | findsOneWidget); 27 | expect(find.byKey(const Key("weather_forecast_wind_page_subtitle")), 28 | findsOneWidget); 29 | expect(find.byKey(const Key("weather_forecast_wind_page_bottom_row")), 30 | findsOneWidget); 31 | expect(find.byKey(const Key("weather_forecast_base_page_title")), 32 | findsOneWidget); 33 | expect(find.byKey(const Key("chart_widget_container")), findsOneWidget); 34 | 35 | final Text title = tester 36 | .widget(find.byKey(const Key("weather_forecast_base_page_title"))); 37 | expect(title.data, "Wind"); 38 | 39 | final RichText subtitle = tester 40 | .widget(find.byKey(const Key("weather_forecast_wind_page_subtitle"))); 41 | final TextSpan textSpan = subtitle.text as TextSpan; 42 | expect(textSpan.text == null, true); 43 | expect(textSpan.children!.length == 4, true); 44 | expect(textSpan.children![0].toPlainText().contains("min"), true); 45 | expect(textSpan.children![1].toPlainText().contains("km/h"), true); 46 | expect(textSpan.children![2].toPlainText().contains("max"), true); 47 | expect(textSpan.children![3].toPlainText().contains("km/h"), true); 48 | 49 | final Row bottomRow = tester.widget( 50 | find.byKey( 51 | const Key("weather_forecast_wind_page_bottom_row"), 52 | ), 53 | ); 54 | expect(bottomRow.children.isEmpty, true); 55 | }); 56 | } 57 | 58 | WeatherForecastHolder setupWeatherForecastHolder() { 59 | final List forecastList = []; 60 | 61 | final Wind wind = Wind(5, 200); 62 | final MainWeatherData mainWeatherData = MainWeatherData(0, 0, 0, 0, 0, 0, 0); 63 | final OverallWeatherData overallWeatherData = 64 | OverallWeatherData(0, "", "", ""); 65 | final List list = []; 66 | list.add(overallWeatherData); 67 | final rain = Rain(0); 68 | final Rain snow = Rain(0); 69 | 70 | forecastList.add( 71 | WeatherForecastResponse( 72 | mainWeatherData, list, null, wind, DateTime.now(), rain, snow), 73 | ); 74 | 75 | final System system = System(null, 0, 0); 76 | final City city = City(0, null); 77 | 78 | final WeatherForecastHolder holder = 79 | WeatherForecastHolder(forecastList, city, system); 80 | 81 | return holder; 82 | } 83 | -------------------------------------------------------------------------------- /test/ui/main/widget/sun_path_widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/remote/system.dart'; 2 | import 'package:feather/src/ui/main/widget/sun_path_widget.dart'; 3 | import 'package:feather/src/utils/date_time_helper.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | void main() { 8 | testWidgets("Sun path widget should draw", (WidgetTester tester) async { 9 | await tester.pumpWidget( 10 | SunPathWidget( 11 | sunrise: DateTimeHelper.getCurrentTime(), 12 | sunset: DateTimeHelper.getCurrentTime(), 13 | ), 14 | ); 15 | 16 | expect(find.byKey(const Key("sun_path_widget_sized_box")), findsOneWidget); 17 | expect( 18 | find.byKey(const Key("sun_path_widget_custom_paint")), findsOneWidget); 19 | }); 20 | } 21 | 22 | System setupSystem() { 23 | return System("", 0, 0); 24 | } 25 | -------------------------------------------------------------------------------- /test/ui/main/widget/weather_current_widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/remote/city.dart'; 2 | import 'package:feather/src/data/model/remote/coordinates.dart'; 3 | import 'package:feather/src/data/model/remote/main_weather_data.dart'; 4 | import 'package:feather/src/data/model/remote/overall_weather_data.dart'; 5 | import 'package:feather/src/data/model/remote/system.dart'; 6 | import 'package:feather/src/data/model/remote/weather_forecast_list_response.dart'; 7 | import 'package:feather/src/data/model/remote/weather_response.dart'; 8 | import 'package:feather/src/data/model/remote/wind.dart'; 9 | import 'package:feather/src/data/repository/local/application_local_repository.dart'; 10 | import 'package:feather/src/data/repository/local/storage_manager.dart'; 11 | import 'package:feather/src/data/repository/local/storage_provider.dart'; 12 | import 'package:feather/src/ui/app/app_bloc.dart'; 13 | import 'package:feather/src/ui/widget/current_weather_widget.dart'; 14 | import 'package:flutter/widgets.dart'; 15 | import 'package:flutter_bloc/flutter_bloc.dart'; 16 | import 'package:flutter_test/flutter_test.dart'; 17 | 18 | import '../../../test_helper.dart'; 19 | 20 | void main() { 21 | testWidgets("Weather widget should show widgets", 22 | (WidgetTester tester) async { 23 | await tester.pumpWidget( 24 | BlocProvider( 25 | create: (context) => AppBloc( 26 | ApplicationLocalRepository( 27 | StorageManager(StorageProvider()), 28 | ), 29 | ), 30 | child: TestHelper.wrapWidgetWithLocalizationApp( 31 | CurrentWeatherWidget( 32 | weatherResponse: setupWeatherResponse(), 33 | forecastListResponse: setupWeatherForecastListResponse(), 34 | ), 35 | ), 36 | ), 37 | ); 38 | 39 | expect(find.byKey(const Key("weather_current_widget_container")), 40 | findsOneWidget); 41 | expect(find.byKey(const Key("weather_current_widget_temperature")), 42 | findsOneWidget); 43 | expect(find.byKey(const Key("weather_current_widget_min_max_temperature")), 44 | findsOneWidget); 45 | expect(find.byKey(const Key("weather_current_widget_pressure_humidity")), 46 | findsOneWidget); 47 | expect(find.byKey(const Key("weather_current_widget_thumbnail_list")), 48 | findsOneWidget); 49 | }); 50 | } 51 | 52 | WeatherResponse setupWeatherResponse() { 53 | final Coordinates coordiantes = Coordinates(0, 0); 54 | final Wind wind = Wind(5, 200); 55 | final MainWeatherData mainWeatherData = MainWeatherData(0, 0, 0, 0, 0, 0, 0); 56 | final OverallWeatherData overallWeatherData = 57 | OverallWeatherData(0, "", "", ""); 58 | final List list = []; 59 | list.add(overallWeatherData); 60 | final System system = System("", 0, 0); 61 | final WeatherResponse weatherResponse = WeatherResponse( 62 | cord: coordiantes, 63 | wind: wind, 64 | mainWeatherData: mainWeatherData, 65 | overallWeatherData: list, 66 | name: "", 67 | system: system); 68 | return weatherResponse; 69 | } 70 | 71 | WeatherForecastListResponse setupWeatherForecastListResponse() { 72 | return WeatherForecastListResponse([], City(0, "")); 73 | } 74 | -------------------------------------------------------------------------------- /test/ui/main/widget/weather_forecast_thumbnail_list_widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/remote/city.dart'; 2 | import 'package:feather/src/data/model/remote/clouds.dart'; 3 | import 'package:feather/src/data/model/remote/main_weather_data.dart'; 4 | import 'package:feather/src/data/model/remote/overall_weather_data.dart'; 5 | import 'package:feather/src/data/model/remote/rain.dart'; 6 | import 'package:feather/src/data/model/remote/system.dart'; 7 | import 'package:feather/src/data/model/remote/weather_forecast_list_response.dart'; 8 | import 'package:feather/src/data/model/remote/weather_forecast_response.dart'; 9 | import 'package:feather/src/data/model/remote/wind.dart'; 10 | import 'package:feather/src/data/repository/local/application_local_repository.dart'; 11 | import 'package:feather/src/data/repository/local/storage_manager.dart'; 12 | import 'package:feather/src/data/repository/local/storage_provider.dart'; 13 | import 'package:feather/src/ui/app/app_bloc.dart'; 14 | import 'package:feather/src/ui/main/widget/weather_forecast_thumbnail_list_widget.dart'; 15 | import 'package:feather/src/ui/navigation/bloc/navigation_bloc.dart'; 16 | import 'package:feather/src/ui/navigation/navigation_provider.dart'; 17 | import 'package:flutter/material.dart'; 18 | import 'package:flutter_bloc/flutter_bloc.dart'; 19 | import 'package:flutter_test/flutter_test.dart'; 20 | 21 | import '../../../test_helper.dart'; 22 | 23 | void main() { 24 | testWidgets("Weather forecast thumbnail list widget show widgets", 25 | (WidgetTester tester) async { 26 | await tester.runAsync(() async { 27 | final WeatherForecastThumbnailListWidget widget = 28 | WeatherForecastThumbnailListWidget( 29 | system: setupSystem(), 30 | forecastListResponse: setupWeatherForecastResponse(), 31 | ); 32 | await tester.pumpWidget( 33 | MultiBlocProvider( 34 | providers: [ 35 | BlocProvider( 36 | create: (context) => AppBloc( 37 | ApplicationLocalRepository( 38 | StorageManager(StorageProvider()), 39 | ), 40 | ), 41 | ), 42 | BlocProvider( 43 | create: (context) => NavigationBloc(NavigationProvider(), GlobalKey()), 44 | ) 45 | ], 46 | child: TestHelper.wrapWidgetWithLocalizationApp(widget), 47 | ), 48 | ); 49 | expect( 50 | find.byKey( 51 | const Key("weather_forecast_thumbnail_list_widget_container")), 52 | findsOneWidget); 53 | expect(find.byKey(const Key("weather_forecast_thumbnail_widget")), 54 | findsNWidgets(8)); 55 | }); 56 | }); 57 | } 58 | 59 | System setupSystem() { 60 | return System("", 0, 0); 61 | } 62 | 63 | WeatherForecastListResponse setupWeatherForecastResponse() { 64 | final City city = City(0, ""); 65 | final List list = []; 66 | for (int index = 0; index < 8; index++) { 67 | list.add(buildForecastResponseForDateTime(DateTime(2019, 1, index + 1))); 68 | } 69 | return WeatherForecastListResponse(list, city); 70 | } 71 | 72 | WeatherForecastResponse buildForecastResponseForDateTime(DateTime dateTime) { 73 | final Wind wind = Wind(5, 200); 74 | final Clouds clouds = Clouds(0); 75 | final MainWeatherData mainWeatherData = MainWeatherData(0, 0, 0, 0, 0, 0, 0); 76 | final OverallWeatherData overallWeatherData = 77 | OverallWeatherData(0, "", "", ""); 78 | final List list = []; 79 | list.add(overallWeatherData); 80 | final Rain rain = Rain(0); 81 | final Rain snow = Rain(0); 82 | return WeatherForecastResponse( 83 | mainWeatherData, list, clouds, wind, dateTime, rain, snow); 84 | } 85 | -------------------------------------------------------------------------------- /test/ui/main/widget/weather_forecast_thumbnail_widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 2 | import 'package:feather/src/data/model/remote/city.dart'; 3 | import 'package:feather/src/data/model/remote/main_weather_data.dart'; 4 | import 'package:feather/src/data/model/remote/overall_weather_data.dart'; 5 | import 'package:feather/src/data/model/remote/rain.dart'; 6 | import 'package:feather/src/data/model/remote/system.dart'; 7 | import 'package:feather/src/data/model/remote/weather_forecast_response.dart'; 8 | import 'package:feather/src/data/model/remote/wind.dart'; 9 | import 'package:feather/src/ui/main/widget/weather_forecast_thumbnail_widget.dart'; 10 | import 'package:feather/src/ui/navigation/bloc/navigation_bloc.dart'; 11 | import 'package:feather/src/ui/navigation/navigation_provider.dart'; 12 | import 'package:flutter/material.dart'; 13 | import 'package:flutter_bloc/flutter_bloc.dart'; 14 | import 'package:flutter_test/flutter_test.dart'; 15 | 16 | void main() { 17 | testWidgets("Weather forecast thumbnail widget should display widgets", 18 | (WidgetTester tester) async { 19 | await tester.pumpWidget( 20 | BlocProvider( 21 | create: (context) => NavigationBloc(NavigationProvider(), GlobalKey()), 22 | child: 23 | WeatherForecastThumbnailWidget(setupWeatherForecastHolder(), false), 24 | ), 25 | ); 26 | expect(find.byKey(const Key("weather_forecast_thumbnail_date")), 27 | findsOneWidget); 28 | expect(find.byKey(const Key("weather_forecast_thumbnail_icon")), 29 | findsOneWidget); 30 | expect(find.byKey(const Key("weather_forecast_thumbnail_temperature")), 31 | findsOneWidget); 32 | }); 33 | } 34 | 35 | WeatherForecastHolder setupWeatherForecastHolder() { 36 | final List forecastList = []; 37 | 38 | final Wind wind = Wind(5, 200); 39 | final MainWeatherData mainWeatherData = MainWeatherData(0, 0, 0, 0, 0, 0, 0); 40 | final OverallWeatherData overallWeatherData = 41 | OverallWeatherData(0, "", "", ""); 42 | final List list = []; 43 | list.add(overallWeatherData); 44 | final Rain rain = Rain(10); 45 | final Rain snow = Rain(10); 46 | 47 | forecastList.add(WeatherForecastResponse( 48 | mainWeatherData, list, null, wind, DateTime.now(), rain, snow)); 49 | 50 | final System system = System(null, 0, 0); 51 | final City city = City(0, null); 52 | 53 | final WeatherForecastHolder holder = 54 | WeatherForecastHolder(forecastList, city, system); 55 | 56 | return holder; 57 | } 58 | -------------------------------------------------------------------------------- /test/ui/main/widget/weather_main_sun_path_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/remote/system.dart'; 2 | import 'package:feather/src/ui/main/widget/weather_main_sun_path_widget.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | import '../../../test_helper.dart'; 7 | 8 | void main() { 9 | testWidgets("Weather widget should show widgets", 10 | (WidgetTester tester) async { 11 | final WeatherMainSunPathWidget widget = WeatherMainSunPathWidget( 12 | system: setupSystem(), 13 | ); 14 | 15 | await tester.pumpWidget(TestHelper.wrapWidgetWithLocalizationApp(widget)); 16 | expect( 17 | find.byKey(const Key("weather_main_sun_path_widget")), findsOneWidget); 18 | expect(find.byKey(const Key("weather_main_sun_path_percentage")), 19 | findsOneWidget); 20 | expect(find.byKey(const Key("weather_main_sun_path_countdown")), 21 | findsOneWidget); 22 | expect( 23 | find.byKey(const Key("weather_main_sun_path_sunrise")), findsOneWidget); 24 | expect( 25 | find.byKey(const Key("weather_main_sun_path_sunset")), findsOneWidget); 26 | }); 27 | } 28 | 29 | System setupSystem() { 30 | return System("", 0, 0); 31 | } 32 | -------------------------------------------------------------------------------- /test/ui/navigation/bloc/fake_navigation_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/ui/navigation/navigation_provider.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class FakeNavigationProvider extends NavigationProvider { 5 | String _path = ""; 6 | 7 | @override 8 | void navigateToPath( 9 | String path, 10 | GlobalKey navigatorKey, { 11 | RouteSettings? routeSettings, 12 | }) { 13 | _path = path; 14 | } 15 | 16 | String get path => _path; 17 | } 18 | -------------------------------------------------------------------------------- /test/ui/navigation/bloc/navigation_bloc_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/navigation_route.dart'; 2 | import 'package:feather/src/data/model/internal/weather_forecast_holder.dart'; 3 | import 'package:feather/src/ui/navigation/bloc/navigation_bloc.dart'; 4 | import 'package:feather/src/ui/navigation/bloc/navigation_event.dart'; 5 | import 'package:feather/src/ui/navigation/bloc/navigation_state.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | 9 | import 'fake_navigation_provider.dart'; 10 | 11 | void main() { 12 | late FakeNavigationProvider fakeNavigationProvider; 13 | late NavigationBloc navigationBloc; 14 | 15 | setUp(() { 16 | fakeNavigationProvider = FakeNavigationProvider(); 17 | navigationBloc = 18 | buildNavigationBloc(fakeNavigationProvider: fakeNavigationProvider); 19 | }); 20 | 21 | test("Should navigate to forecast screen", () async { 22 | navigationBloc.add(ForecastScreenNavigationEvent( 23 | WeatherForecastHolder.empty(), 24 | )); 25 | 26 | await expectLater( 27 | navigationBloc.stream, 28 | emitsInOrder( 29 | [ 30 | const NavigationState(NavigationRoute.forecastScreen) 31 | ], 32 | ), 33 | ); 34 | 35 | expect(fakeNavigationProvider.path, "/forecast"); 36 | }); 37 | 38 | test("Should navigate to main screen", () async { 39 | navigationBloc.add(MainScreenNavigationEvent()); 40 | 41 | await expectLater( 42 | navigationBloc.stream, 43 | emitsInOrder( 44 | [const NavigationState(NavigationRoute.mainScreen)], 45 | ), 46 | ); 47 | 48 | expect(fakeNavigationProvider.path, "/"); 49 | }); 50 | 51 | test("Should navigate to about screen", () async { 52 | navigationBloc.add(AboutScreenNavigationEvent(const [])); 53 | 54 | await expectLater( 55 | navigationBloc.stream, 56 | emitsInOrder( 57 | [const NavigationState(NavigationRoute.aboutScreen)], 58 | ), 59 | ); 60 | 61 | expect(fakeNavigationProvider.path, "/about"); 62 | }); 63 | 64 | test("Should navigate to settings screen", () async { 65 | navigationBloc.add(SettingsScreenNavigationEvent(const [])); 66 | 67 | await expectLater( 68 | navigationBloc.stream, 69 | emitsInOrder( 70 | [ 71 | const NavigationState(NavigationRoute.settingsScreen) 72 | ], 73 | ), 74 | ); 75 | 76 | expect(fakeNavigationProvider.path, "/settings"); 77 | }); 78 | } 79 | 80 | NavigationBloc buildNavigationBloc( 81 | {FakeNavigationProvider? fakeNavigationProvider}) => 82 | NavigationBloc( 83 | fakeNavigationProvider ?? FakeNavigationProvider(), 84 | GlobalKey(), 85 | ); 86 | -------------------------------------------------------------------------------- /test/ui/navigation/bloc/navigation_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'fake_navigation_provider.dart'; 5 | 6 | void main() { 7 | late FakeNavigationProvider navigationProvider; 8 | late GlobalKey navigatorKey; 9 | 10 | setUp(() { 11 | navigationProvider = FakeNavigationProvider(); 12 | navigatorKey = GlobalKey(); 13 | }); 14 | 15 | test("Updates path on route change", () { 16 | expect(navigationProvider.path, ""); 17 | 18 | navigationProvider.navigateToPath("/about", navigatorKey); 19 | expect(navigationProvider.path, "/about"); 20 | 21 | navigationProvider.navigateToPath("/settings", navigatorKey); 22 | expect(navigationProvider.path, "/settings"); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/ui/settings/bloc/settings_bloc_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/model/internal/unit.dart'; 2 | import 'package:feather/src/data/repository/local/application_local_repository.dart'; 3 | import 'package:feather/src/ui/settings/bloc/settings_screen_bloc.dart'; 4 | import 'package:feather/src/ui/settings/bloc/settings_screen_event.dart'; 5 | import 'package:feather/src/ui/settings/bloc/settings_screen_state.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | 8 | import '../../../data/repository/local/fake_storage_manager.dart'; 9 | 10 | void main() { 11 | late SettingsScreenBloc settingsScreenBloc; 12 | 13 | setUp(() { 14 | settingsScreenBloc = buildSettingsScreenBloc(); 15 | }); 16 | 17 | test("Initial state is InitialSettingsScreenState", () { 18 | expect(settingsScreenBloc.state, InitialSettingsScreenState()); 19 | }); 20 | 21 | test("LoadSettingsScreenEvent loads settings", () async { 22 | settingsScreenBloc.add(LoadSettingsScreenEvent()); 23 | await expectLater( 24 | settingsScreenBloc.stream, 25 | emitsInOrder( 26 | [isA()], 27 | ), 28 | ); 29 | 30 | final loadedSettingsScreenState = 31 | settingsScreenBloc.state as LoadedSettingsScreenState; 32 | expect(loadedSettingsScreenState.unit, Unit.imperial); 33 | expect(loadedSettingsScreenState.lastRefreshTime, 0); 34 | expect(loadedSettingsScreenState.refreshTime, 0); 35 | }); 36 | 37 | test("ChangeUnitsSettingsScreenEvent changes unit", () async { 38 | settingsScreenBloc.add(LoadSettingsScreenEvent()); 39 | await expectLater( 40 | settingsScreenBloc.stream, 41 | emitsInOrder( 42 | [isA()], 43 | ), 44 | ); 45 | 46 | settingsScreenBloc.add(ChangeUnitsSettingsScreenEvent(Unit.metric)); 47 | await expectLater( 48 | settingsScreenBloc.stream, 49 | emitsInOrder( 50 | [isA()], 51 | ), 52 | ); 53 | 54 | final loadedSettingsScreenState = 55 | settingsScreenBloc.state as LoadedSettingsScreenState; 56 | expect(loadedSettingsScreenState.unit, Unit.metric); 57 | }); 58 | 59 | test("ChangeRefreshTimeSettingsScreenEvent changes refresh time", () async { 60 | settingsScreenBloc.add(LoadSettingsScreenEvent()); 61 | await expectLater( 62 | settingsScreenBloc.stream, 63 | emitsInOrder( 64 | [isA()], 65 | ), 66 | ); 67 | 68 | settingsScreenBloc.add(ChangeRefreshTimeSettingsScreenEvent(10)); 69 | await expectLater( 70 | settingsScreenBloc.stream, 71 | emitsInOrder( 72 | [isA()], 73 | ), 74 | ); 75 | 76 | final loadedSettingsScreenState = 77 | settingsScreenBloc.state as LoadedSettingsScreenState; 78 | expect(loadedSettingsScreenState.refreshTime, 10); 79 | }); 80 | } 81 | 82 | SettingsScreenBloc buildSettingsScreenBloc( 83 | {FakeStorageManager? fakeStorageManager}) { 84 | return SettingsScreenBloc( 85 | ApplicationLocalRepository( 86 | fakeStorageManager ?? FakeStorageManager(), 87 | ), 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /test/ui/settings/widget/settings_screen_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/data/repository/local/application_local_repository.dart'; 2 | import 'package:feather/src/ui/app/app_bloc.dart'; 3 | import 'package:feather/src/ui/settings/bloc/settings_screen_bloc.dart'; 4 | import 'package:feather/src/ui/settings/bloc/settings_screen_event.dart'; 5 | import 'package:feather/src/ui/settings/settings_screen.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_bloc/flutter_bloc.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | import '../../../data/repository/local/fake_storage_manager.dart'; 11 | import '../../../test_helper.dart'; 12 | import '../../app/app_bloc_test.dart'; 13 | 14 | void main() { 15 | testWidgets("Settings screen should display widgets", 16 | (WidgetTester tester) async { 17 | final settingsScreenBloc = SettingsScreenBloc(ApplicationLocalRepository( 18 | FakeStorageManager(), 19 | )); 20 | 21 | settingsScreenBloc.add(LoadSettingsScreenEvent()); 22 | 23 | await tester.pumpWidget( 24 | MultiBlocProvider( 25 | providers: [ 26 | BlocProvider( 27 | create: (context) => buildAppBloc(), 28 | ), 29 | BlocProvider( 30 | create: (context) => settingsScreenBloc, 31 | ) 32 | ], 33 | child: TestHelper.wrapWidgetWithLocalizationApp( 34 | const SettingsScreen(), 35 | ), 36 | ), 37 | const Duration(seconds: 2), 38 | ); 39 | 40 | expect(find.byKey(const Key("settings_screen_container")), findsOneWidget); 41 | expect( 42 | find.byKey(const Key("settings_screen_refresh_timer")), findsOneWidget); 43 | expect( 44 | find.byKey(const Key("settings_screen_units_picker")), findsOneWidget); 45 | expect(find.byKey(const Key("settings_screen_last_refresh_time")), 46 | findsOneWidget); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /test/utils/types_helper_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:feather/src/utils/types_helper.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test("Get double should return valid double", () { 6 | expect(TypesHelper.toDouble(0.0), 0.0); 7 | expect(TypesHelper.toDouble(0), 0.0); 8 | expect(TypesHelper.toDouble(-10), -10.0); 9 | expect(TypesHelper.toDouble(-10.0), -10.0); 10 | }); 11 | } 12 | --------------------------------------------------------------------------------