diff --git a/README.md b/README.md index 6b2b849..fb0681e 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,71 @@ -# Overmorrow weather -![app_gallery](Screenshots/new_feature_graphic_yellow.jpg) +
+ +
-## Minimalist colorful weather app +
-![app_gallery](Screenshots/app_gallery4_tranparent.png) +

Overmorrow

+
+ + colorful1 + + + colorful2 + +
-| [![Download on Google Play](/Screenshots/play_badge4.png 'Download')](https://play.google.com/store/apps/details?id=com.marotidev.Overmorrow) | [![Download on IzzyOnDroid](/Screenshots/IzzyOnDroid_c.png 'Download')](https://apt.izzysoft.de/fdroid/index/apk/com.marotidev.Overmorrow/) | -|---|---| +

minimalist colorful weather.

+
+ + + +
+colorful1 +colorful2 +colorful3 +colorful4 +colorful5 +
## Weather providers 🌨️ - [open-meteo](https://open-meteo.com) - [weatherapi.com](https://www.weatherapi.com) +- [met-norway](https://api.met.no/) - [rainvewer](https://www.rainviewer.com/api.html) Only now after working on Overmorrow for more than 6 months, have I realized that my app is defined as "non commercial" in open-meteo's documentation 😂. Which is great because i can use it completely for free 😎! So now i can add 14 days of forecast . Also its one of the most accurate weather providers. -I get my sunrise sunset times and air quality from weatherapi.com 🍃. -Also it is offered as a second weather provider, but it only has 3 days of weather data. +you can also change your provider to met-norway or weatherapi.com. -And all the radar images are from rainviewer's radar 💧. +All the radar images are from rainviewer's radar 💧. ## Features 🎉 +- network images that change based on location and weather condition - accurate weather forecast - open source - no ads - no data collected - minimalist colorful design -- detailed forecast -- sunrise sunset times -- air quality insights -- full screen radar -- 14 days of forecast -- dynamically adapting color scheme -- 5 color themes (original, colorful, monochrome, light, dark) -- languages support +- sunrise / sunset times, current time in city +- rain in the next 6 hours with 15 minute precision +- air quality index, description, summary, pm_2.5, pm10, o3, no2 +- compact and fullscreen radar, with 2 hour past, and 30 minutes of future timestamps. +- 3 day detailed forecast, with options for temp, precip, wind and uv +- 14 day compact forecast, with option to open into detailed view. +- rain charts showing the rain on a given day +- option to choose from 3 weather providers +- 5 beautiful color theme options +- 2 search providers + +## Tablet mode + +![page48](Screenshots/page48.png) +![üpage50](Screenshots/page50.png) ## Why make Overmorrow? ❓ I am 15 and i have been programing since the age of 7. I started small (Scratch and NetsBlox) @@ -53,6 +80,7 @@ So instead here is my take on the weather app ui (but i did kep it free and ad f - ✅ Add place searching - ✅ Add radar - ✅ Add air quality +- ✅ Add sunrise sunset - ✅ Add translations - ✅ 14 day forecast - ✅ Settings/Info/Donate pages diff --git a/Screenshots/Overmorrow_white_circle.png b/Screenshots/Overmorrow_white_circle.png new file mode 100644 index 0000000..fdc6f13 Binary files /dev/null and b/Screenshots/Overmorrow_white_circle.png differ diff --git a/Screenshots/app_gallery3.png b/Screenshots/app_gallery3.png deleted file mode 100644 index 5b7cd06..0000000 Binary files a/Screenshots/app_gallery3.png and /dev/null differ diff --git a/Screenshots/app_gallery4_tranparent.png b/Screenshots/app_gallery4_tranparent.png deleted file mode 100644 index 4744e39..0000000 Binary files a/Screenshots/app_gallery4_tranparent.png and /dev/null differ diff --git a/Screenshots/colorful1.png b/Screenshots/colorful1.png new file mode 100644 index 0000000..ebc93b0 Binary files /dev/null and b/Screenshots/colorful1.png differ diff --git a/Screenshots/colorful2.png b/Screenshots/colorful2.png new file mode 100644 index 0000000..2253d00 Binary files /dev/null and b/Screenshots/colorful2.png differ diff --git a/Screenshots/colorful3.png b/Screenshots/colorful3.png new file mode 100644 index 0000000..e228549 Binary files /dev/null and b/Screenshots/colorful3.png differ diff --git a/Screenshots/colorful4.png b/Screenshots/colorful4.png new file mode 100644 index 0000000..23547c2 Binary files /dev/null and b/Screenshots/colorful4.png differ diff --git a/Screenshots/colorful5.png b/Screenshots/colorful5.png new file mode 100644 index 0000000..72ce05d Binary files /dev/null and b/Screenshots/colorful5.png differ diff --git a/Screenshots/page48.png b/Screenshots/page48.png new file mode 100644 index 0000000..277d02f Binary files /dev/null and b/Screenshots/page48.png differ diff --git a/Screenshots/page50.png b/Screenshots/page50.png new file mode 100644 index 0000000..fea5169 Binary files /dev/null and b/Screenshots/page50.png differ diff --git a/android/app/build.gradle b/android/app/build.gradle index f2015cd..90dce79 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -41,7 +41,8 @@ android { compileSdk 34 // compileSdkVersion flutter.compileSdkVersion <- this vas the original one but // geolocator insisted on it being 33 - ndkVersion flutter.ndkVersion + //ndkVersion flutter.ndkVersion + ndkVersion "27.0.12077973" compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -63,8 +64,8 @@ android { // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion - versionCode 42 - versionName "2.4.2" + versionCode 43 + versionName "2.4.3" } buildTypes { diff --git a/android/gradle.properties b/android/gradle.properties index 94adc3a..b9a9a24 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,6 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c472b9..09523c0 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index adce2b0..175b330 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false + id "com.android.application" version '8.7.0' apply false id "org.jetbrains.kotlin.android" version "2.0.0" apply false } diff --git a/lib/aqi_page.dart b/lib/aqi_page.dart new file mode 100644 index 0000000..9c5eb44 --- /dev/null +++ b/lib/aqi_page.dart @@ -0,0 +1,829 @@ +/* +Copyright (C) <2024> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + + + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:loading_animation_widget/loading_animation_widget.dart'; +import 'package:overmorrow/decoders/decode_OM.dart'; +import 'package:overmorrow/settings_page.dart'; +import 'package:overmorrow/ui_helper.dart'; + + +class SquigglyCirclePainter extends CustomPainter { + + final Color circleColor; + + SquigglyCirclePainter(this.circleColor); + + @override + void paint(Canvas canvas, Size size) { + final Paint paint = Paint() + ..color = circleColor + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeWidth = 2.8; + + final Path path = Path(); + double radius = size.width / 2; + double centerX = size.width / 2; + double centerY = size.height / 2; + + double waves = 10; + double waveAmplitude = size.width / 50; + + for (double i = 0; i <= 360; i += 0.1) { + double angle = i * pi / 180; + double x = centerX + (radius + waveAmplitude * sin(waves * angle)) * cos(angle); + double y = centerY + (radius + waveAmplitude * sin(waves * angle)) * sin(angle); + + if (i == 0) { + path.moveTo(x, y); + } else { + path.lineTo(x, y); + } + } + + path.close(); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +Widget pollenWidget(IconData icon, String name, double value, data) { + const categoryBoundaries = [-1, 0, 20, 80, 200]; + const categoryNames = ["--", "none", "low", "medium", "high"]; + + int categoryIndex = 0; + for (int i = 0; i < categoryBoundaries.length; i++) { + if (value > categoryBoundaries[i]) { + categoryIndex = i + 1; + } + } + + String severity = categoryNames[categoryIndex]; + + return Padding( + padding: const EdgeInsets.only(left: 10, right: 6, top:6, bottom: 6), + child: Row( + children: [ + Icon(icon, size: 22, color: data.current.primaryLight), + Padding( + padding: const EdgeInsets.only(left: 17), + child: comfortatext(name, 17, data.settings, color: data.current.onSurface), + ), + const Spacer(), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: data.current.primaryLighter, + ), + padding: const EdgeInsets.only(top: 6.5, bottom: 6.5), + width: 65, + child: Center(child: comfortatext(severity, 15, data.settings, color: data.current.onPrimaryLight)) + ), + ], + ), + ); +} + + +class ThreeQuarterCirclePainter extends CustomPainter { + final double percentage; + final Color color; + final Color secondColor; + + ThreeQuarterCirclePainter({required this.percentage, required this.color, required this.secondColor}); + + @override + void paint(Canvas canvas, Size size) { + double angle = 2 * 3.14159265359 * (max(min(percentage, 100), 0) / 100) * 0.75; // 3 quarters of a circle + + // Background Circle + Paint baseCircle = Paint() + ..color = secondColor + ..strokeWidth = 9 + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + canvas.drawArc( + Rect.fromLTWH(0, 0, size.width, size.height), + -3.14159265359 * 5 / 4, + 3.14159265359 * 1.5, + false, + baseCircle, + ); + + // Foreground Circle + Paint progressCircle = Paint() + ..color = color + ..strokeWidth = 9 + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + canvas.drawArc( + Rect.fromLTWH(0, 0, size.width, size.height), + -3.14159265359 * 5 / 4, + angle, + false, + progressCircle, + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} + +Widget pollutantWidget(data, name, value, percent) { + return Padding( + padding: EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 0, bottom: 0), + child: Center( + child: AspectRatio( + aspectRatio: 1, + child: CustomPaint( + painter: ThreeQuarterCirclePainter(percentage: percent, color: data.current.primaryLight, + secondColor: data.current.containerHigh), + child: Center( + child: comfortatext(value.toString(), 18, data.settings, color: data.current.primary, weight: FontWeight.w600) + ), + ), + ), + ), + ), + ), + comfortatext(name, 14, data.settings, color: data.current.onSurface), + ], + ), + ); +} + +class AllergensPage extends StatefulWidget { + final data; + + const AllergensPage({Key? key, required this.data}) + : super(key: key); + + @override + _AllergensPageState createState() => + _AllergensPageState(data:data); +} + +class _AllergensPageState extends State { + + final data; + + _AllergensPageState({required this.data}); + + void goBack() { + HapticFeedback.selectionClick(); + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return Material( + color: data.current.surface, + child: CustomScrollView( + slivers: [ + SliverAppBar.large( + leading: IconButton( + icon: Icon(Icons.arrow_back, color: data.current.primary), + onPressed: () { + goBack(); + }, + ), + title: comfortatext(translation("Air Quality", data.settings["Language"]), 30, data.settings, color: data.current.primary), + backgroundColor: data.current.surface, + pinned: false, + ), + SliverToBoxAdapter( + child: FutureBuilder( + future: OMExtendedAqi.fromJson(data.lat, data.lng, data.settings), + builder: (BuildContext context, + AsyncSnapshot snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return Padding( + padding: const EdgeInsets.only(top: 200), + child: Center( + child: LoadingAnimationWidget.staggeredDotsWave( + color: data.current.primaryLight, + size: 40, + ), + ), + ); + } else if (snapshot.hasError) { + print((snapshot.error, snapshot.stackTrace)); + return Padding( + padding: const EdgeInsets.only(top: 100), + child: Column( + children: [ + comfortatext("unable to load air quality data", 18, data.settings, color: data.current.primary), + Padding( + padding: const EdgeInsets.all(30.0), + child: comfortatext("${snapshot.error} ${snapshot.stackTrace}", 15, data.settings, color: data.current.onSurface, + align: TextAlign.center), + ) + ], + ), + ); + } + final OMExtendedAqi extendedAqi = snapshot.data!; + final highestAqi = extendedAqi.dailyAqi.reduce(max); + return Padding( + padding: const EdgeInsets.only(left: 30, right: 30), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 50, right: 50, top: 50, bottom: 30), + child: AspectRatio( + aspectRatio: 1, + child: CustomPaint( + painter: SquigglyCirclePainter(data.current.primaryLight), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 15), + child: comfortatext(data.aqi.aqi_index.toString(), 52, data.settings, color: data.current.primary, weight: FontWeight.w300), + ), + comfortatext(data.aqi.aqi_title, 23, data.settings, color: data.current.primary, weight: FontWeight.w600), + ], + ), + ), + ), + ), + + Padding( + padding: const EdgeInsets.only(top: 10, bottom: 20, left: 10, right: 10), + child: comfortatext(data.aqi.aqi_desc, 17, data.settings, color: data.current.onSurface, weight: FontWeight.w400, align: TextAlign.center), + ), + + + /* + GridView.count( + padding: const EdgeInsets.only(top: 15, bottom: 20, left: 10, right: 10), + crossAxisSpacing: 10, + mainAxisSpacing: 10, + crossAxisCount: 3, + childAspectRatio: 4.8, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + NewAqiDataPoints("PM2.5", data.aqi.pm2_5, data, 18.0), + NewAqiDataPoints("PM10", data.aqi.pm10, data, 18.0), + NewAqiDataPoints("O3", data.aqi.o3, data, 18.0), + NewAqiDataPoints("NO2", data.aqi.no2, data, 18.0), + NewAqiDataPoints("CO", extendedAqi.co, data, 18.0), + NewAqiDataPoints("SO2", extendedAqi.so2, data, 18.0), + ] + ), + */ + + + Padding( + padding: const EdgeInsets.only(top: 10, bottom: 5), + child: Container( + decoration: BoxDecoration( + color: data.current.containerLow, + //border: Border.all(width: 1.5, color: data.current.containerHigh), + borderRadius: BorderRadius.circular(18), + ), + padding: const EdgeInsets.all(17), + child: Row( + children: [ + comfortatext(translation("main pollutant", data.settings["Language"]), 16, data.settings, color: data.current.onSurface), + const Spacer(), + comfortatext(extendedAqi.mainPollutant, 18, data.settings, color: data.current.primary, weight: FontWeight.w600) + ], + ), + ), + ), + + Padding( + padding: const EdgeInsets.only(top: 15, bottom: 40), + child: Container( + decoration: BoxDecoration( + //color: data.current.containerLow, + border: Border.all(width: 2, color: data.current.containerHigh), + borderRadius: BorderRadius.circular(18), + ), + padding: const EdgeInsets.all(11), + child: Column( + children: [ + pollenWidget(Icons.forest_outlined, + translation("Alder Pollen", data.settings["Language"]), extendedAqi.alder, data), + pollenWidget(Icons.eco_outlined, + translation("Birch Pollen", data.settings["Language"]), extendedAqi.birch, data), + pollenWidget(Icons.grass_outlined, + translation("Grass Pollen", data.settings["Language"]), extendedAqi.grass, data), + pollenWidget(Icons.local_florist_outlined, + translation("Mugwort Pollen", data.settings["Language"]), extendedAqi.mugwort, data), + pollenWidget(Icons.park_outlined, + translation("Olive Pollen", data.settings["Language"]), extendedAqi.olive, data), + pollenWidget(Icons.filter_vintage_outlined, + translation("Ragweed Pollen", data.settings["Language"]), extendedAqi.ragweed, data), + ], + ), + ), + ), + + NewHourlyAqi(data: data, extendedAqi: extendedAqi), + + Padding( + padding: const EdgeInsets.only(bottom: 20, top: 50), + child: Container( + decoration: BoxDecoration( + //color: data.current.containerLow, + border: Border.all(width: 2, color: data.current.containerHigh), + borderRadius: BorderRadius.circular(18), + ), + padding: EdgeInsets.all(10), + child: GridView.count( + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + crossAxisCount: 3, + shrinkWrap: true, + childAspectRatio: 0.9, + children: [ + pollutantWidget(data, "pm2.5", data.aqi.pm2_5, extendedAqi.pm2_5_p), + pollutantWidget(data, "pm10", data.aqi.pm10, extendedAqi.pm10_p), + pollutantWidget(data, "o3", data.aqi.o3, extendedAqi.o3_p), + pollutantWidget(data, "no2", data.aqi.no2, extendedAqi.no2_p), + pollutantWidget(data, "co", extendedAqi.co, extendedAqi.co_p), + pollutantWidget(data, "so2", extendedAqi.so2, extendedAqi.so2_p), + ], + ), + ), + ), + + Padding( + padding: const EdgeInsets.only(top: 0, bottom: 0), + child: Row( + children: [ + Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.only(right: 4), + child: Container( + decoration: BoxDecoration( + color: data.current.containerLow, + //border: Border.all(width: 1.5, color: data.current.containerHigh), + borderRadius: BorderRadius.circular(18), + ), + height: 115, + padding: const EdgeInsets.only(left: 14, top: 14, right: 10, bottom: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + comfortatext(translation("european aqi", data.settings["Language"]), 14, data.settings, color: data.current.onSurface), + const Spacer(), + comfortatext(extendedAqi.european_aqi.toString(), 25, data.settings, color: data.current.primary, weight: FontWeight.w400), + Padding( + padding: const EdgeInsets.only(left: 2, top: 1), + child: comfortatext("good", 15, data.settings, color: data.current.outline, weight: FontWeight.w600), + ), + ], + ), + ), + ) + ), + Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.only(left: 4), + child: Container( + decoration: BoxDecoration( + color: data.current.containerLow, + //border: Border.all(width: 1.5, color: data.current.containerHigh), + borderRadius: BorderRadius.circular(18), + ), + height: 115, + padding: const EdgeInsets.only(left: 14, top: 14, right: 10, bottom: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + comfortatext(translation("united states aqi", data.settings["Language"]), 14, data.settings, color: data.current.onSurface), + const Spacer(), + comfortatext(extendedAqi.us_aqi.toString(), 25, data.settings, color: data.current.primary, weight: FontWeight.w400), + Padding( + padding: const EdgeInsets.only(left: 2, top: 1), + child: comfortatext("good", 15, data.settings, color: data.current.outline, weight: FontWeight.w600), + ), + ], + ), + ), + ) + ) + ], + ), + ), + + Padding( + padding: const EdgeInsets.only(bottom: 10, top: 35), + child: Align( + alignment: Alignment.centerLeft, + child: comfortatext(translation("daily aqi", data.settings["Language"]), 16, data.settings, color: data.current.primary) + ), + ), + + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: List.generate(extendedAqi.dailyAqi.length, (index) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 4, right: 4, bottom: 10), + child: SizedBox( + height: 130, + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(22), + color: extendedAqi.dailyAqi[index] == highestAqi ? data.current.surface : data.current.primaryLight, + border: Border.all(color: extendedAqi.dailyAqi[index] == highestAqi ? data.current.primaryLight + : data.current.surface, width: 2) + ), + width: 43, + alignment: Alignment.topCenter, + padding: const EdgeInsets.only(top: 12), + //tried to do some null safety and not allowing the bars to be too short + height: max(110 / max(highestAqi, 1) * extendedAqi.dailyAqi[index], 42), + child: comfortatext(extendedAqi.dailyAqi[index].toString(), 16, data.settings, + color: extendedAqi.dailyAqi[index] == highestAqi ? data.current.primaryLight : data.current.surface, + weight: FontWeight.w600), + ), + ), + ), + ), + comfortatext(index == 0 ? translation("now", data.settings["Language"]) + : "${index}${translation("d", data.settings["Language"])}", + 14, data.settings, color: data.current.outline) + ], + ); + } + ) + ), + + Padding( + padding: const EdgeInsets.only(top: 45, bottom: 0), + child: Row( + children: [ + Expanded( + flex: 5, + child: Padding( + padding: const EdgeInsets.only(right: 4), + child: Container( + decoration: BoxDecoration( + //color: data.current.containerLow, + border: Border.all(width: 1.5, color: data.current.containerHigh), + borderRadius: BorderRadius.circular(18), + ), + height: 125, + padding: const EdgeInsets.only(left: 14, top: 14, right: 10, bottom: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.grain, size: 18, color: data.current.primaryLight), + Padding( + padding: const EdgeInsets.only(left: 5), + child: comfortatext(translation("dust", data.settings["Language"]), 14, data.settings, color: data.current.onSurface), + ) + ], + ), + const Spacer(), + comfortatext(extendedAqi.dust.toString(), 25, data.settings, color: data.current.primary, weight: FontWeight.w400), + Padding( + padding: const EdgeInsets.only(left: 2, top: 1), + child: comfortatext("μg/m³", 15, data.settings, color: data.current.outline, weight: FontWeight.w600), + ), + ], + ), + ), + ) + ), + Expanded( + flex: 8, + child: Padding( + padding: const EdgeInsets.only(left: 4), + child: Container( + decoration: BoxDecoration( + //color: data.current.containerLow, + border: Border.all(width: 1.5, color: data.current.containerHigh), + borderRadius: BorderRadius.circular(18), + ), + height: 125, + padding: const EdgeInsets.only(left: 14, top: 14, right: 10, bottom: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.grain, size: 18, color: data.current.primaryLight), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 5), + child: comfortatext(translation("aerosol optical depth", data.settings["Language"]), + 14, data.settings, color: data.current.onSurface), + ), + ) + ], + ), + const Spacer(), + comfortatext(extendedAqi.aod.toString(), 25, data.settings, color: data.current.primary, weight: FontWeight.w400), + Padding( + padding: const EdgeInsets.only(left: 2, top: 1), + child: comfortatext(extendedAqi.aod_desc, 15, data.settings, color: data.current.outline, weight: FontWeight.w600), + ), + ], + ), + ), + ) + ) + ], + ), + ), + + Align( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.only(top: 60, bottom: 70), + child: comfortatext("powered by open-meteo", 15, data.settings, color: data.current.outline), + ), + ) + + ], + ), + ); + }, + ), + ), + ], + ), + ); + } +} + + +class NewHourlyAqi extends StatefulWidget { + final data; + final extendedAqi; + + NewHourlyAqi({Key? key, required this.data, required this.extendedAqi}) : super(key: key); + + @override + _NewHourlyAqiState createState() => _NewHourlyAqiState(data, extendedAqi); +} + +class _NewHourlyAqiState extends State with AutomaticKeepAliveClientMixin { + final data; + final extendedAqi; + int _value = 0; + + PageController _pageController = PageController(); + + void _onItemTapped(int index) { + _pageController.animateToPage( + index, + duration: const Duration(milliseconds: 400), + curve: Curves.fastEaseInToSlowEaseOut, + ); + } + + @override + void initState() { + super.initState(); + _value = ["pm2.5", "pm10", "ozone", "carbon monoxide", "sulphur dioxide", "nitrogen dioxide"]. + indexOf(extendedAqi.mainPollutant); + _pageController = PageController(initialPage: _value); + } + + @override + bool get wantKeepAlive => true; + + _NewHourlyAqiState(this.data, this.extendedAqi); + + @override + Widget build(BuildContext context) { + super.build(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 5), + child: SizedBox( + height: 300, + child: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: _pageController, + children: [ + HourlyQqi(data, extendedAqi.pm2_5_h, "PM2.5", extendedAqi), + HourlyQqi(data, extendedAqi.pm10_h, "PM10", extendedAqi), + HourlyQqi(data, extendedAqi.o3_h, "O3", extendedAqi), + HourlyQqi(data, extendedAqi.no2_h, "NO2", extendedAqi), + HourlyQqi(data, extendedAqi.co_h, "CO", extendedAqi), + HourlyQqi(data, extendedAqi.so2_h, "SO2", extendedAqi), + ], + ), + ), + ), + Wrap( + spacing: 5.0, + children: List.generate( + 6, + (int index) { + + return ChoiceChip( + elevation: 0.0, + checkmarkColor: data.current.onPrimaryLight, + color: WidgetStateProperty.resolveWith((states) { + if (index == _value) { + return data.current.primaryLighter; + } + return data.current.surface; + }), + side: BorderSide(color: data.current.primaryLighter, width: 1.5), + label: comfortatext( + ['pm2.5', 'pm10', 'o3', 'no2', 'co', 'so2'][index], 14, data.settings, + color: _value == index ? data.current.onPrimaryLight : data.current.onSurface), + selected: _value == index, + onSelected: (bool selected) { + _value = index; + setState(() { + HapticFeedback.lightImpact(); + _onItemTapped(index); + }); + }, + ); + }, + ).toList(), + ), + ], + ); + + } +} + +class AQIGraphPainter extends CustomPainter { + final List aqiData; + final int maxAQI; + final Color color; + + AQIGraphPainter({required this.aqiData, required this.maxAQI, required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeCap = StrokeCap.round + ..strokeWidth = 2.5; + + final double chartHeight = size.height; + final double chartWidth = size.width; + final double yScale = chartHeight / maxAQI; + final double xSpacing = chartWidth / (aqiData.length - 1); + + for (int i = 0; i < aqiData.length - 1; i++) { + final startX = i * xSpacing; + final startY = chartHeight - (aqiData[i] * yScale); + final endX = (i + 1) * xSpacing; + final endY = chartHeight - (aqiData[i + 1] * yScale); + canvas.drawLine(Offset(startX, startY), Offset(endX, endY), paint); + } + + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; //if data changes -> repaints + } +} + +Widget HourlyQqi(data, hourValues, name, extendedAqi) { + + const List> chartTypes = [ + [0, 2, 4, 6, 8, 10], + [0, 5, 10, 15, 20, 25], + [0, 10, 20, 30, 40, 50], + [0, 20, 40, 60, 80, 100], + [0, 30, 60, 90, 120, 150], + [0, 50, 100, 150, 200, 250], + [0, 100, 200, 300, 400, 500], + [0, 200, 400, 600, 800, 1000] + ]; + + double valueMax = hourValues.reduce((a, b) => max(a, b)); + int currentChart = 0; + + for (int i = 0; i < chartTypes.length; i++) { + if (valueMax * 1.3 > chartTypes[i][chartTypes[i].length - 1]) { //because it looks weird if it is close to the top + currentChart = min(i + 1, chartTypes.length - 1); //just for null safety + } + } + + int len = chartTypes[currentChart].length; + + return Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Row( + children: [ + Icon(Icons.grain, size: 20, color: data.current.primaryLight), + Padding( + padding: const EdgeInsets.only(left: 5), + child: comfortatext(name, 17, data.settings, color: data.current.primary), + ) + ] + ), + ), + + Stack( + children: [ + Padding( + padding: const EdgeInsets.only(left: 2, right: 10), + child: SizedBox( + height: 220, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(len, (index) { + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + comfortatext(chartTypes[currentChart][len - 1 - index].toString(), 14, data.settings, + color: data.current.outline), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 15), + child: Container( + color: data.current.containerHigh, + height: 1, + ), + ), + ) + ], + ); + }), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 40, right: 20), + child: CustomPaint( + painter: AQIGraphPainter(aqiData: hourValues, + maxAQI: chartTypes[currentChart][len - 1], + color: data.current.primaryLight), + child: const SizedBox( + width: double.infinity, + height: 220.0, + ), + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 7, bottom: 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(extendedAqi.dailyAqi.length, (index) { + return comfortatext(index == 0 ? translation("now", data.settings["Language"]) + : "${index}${translation("d", data.settings["Language"])}", + 14, data.settings, color: data.current.outline); + } + ) + ), + ) + ] + ); +} diff --git a/lib/decoders/decode_OM.dart b/lib/decoders/decode_OM.dart index 453642d..50cb274 100644 --- a/lib/decoders/decode_OM.dart +++ b/lib/decoders/decode_OM.dart @@ -492,6 +492,7 @@ class OM15MinutePrecip { sum += x; precips.add(x); + print(x); } sum = max(sum, 0.1); //if there is rain then it shouldn't write 0 @@ -626,28 +627,32 @@ class OMSunstatus { class OMAqi{ final int aqi_index; + final double pm2_5; final double pm10; final double o3; final double no2; - final String aqi_title; + final String aqi_desc; + final String aqi_title; const OMAqi({ + + required this.aqi_desc, + required this.aqi_title, + required this.no2, required this.o3, required this.pm2_5, required this.pm10, required this.aqi_index, - required this.aqi_desc, - required this.aqi_title, }); - static Future fromJson(item, lat, lng, settings) async { + static Future fromJson(lat, lng, settings) async { final params = { "latitude": lat.toString(), "longitude": lng.toString(), - "current": ["european_aqi", "pm10", "pm2_5", "nitrogen_dioxide", 'ozone'], + "current": ["european_aqi", "pm10", "pm2_5", "nitrogen_dioxide", "ozone"], }; final url = Uri.https("air-quality-api.open-meteo.com", 'v1/air-quality', params); var file = await cacheManager2.getSingleFile(url.toString(), key: "$lat, $lng, aqi open-meteo").timeout(const Duration(seconds: 6)); @@ -670,6 +675,232 @@ class OMAqi{ } } + +class OMExtendedAqi{ //this data will only be called if you open the Air quality page + //this is done to reduce the amount of unused calls to the open-meteo servers + final double co; + final double so2; + + //percent + final double pm2_5_p; + final double pm10_p; + final double o3_p; + final double no2_p; + final double co_p; + final double so2_p; + + final double alder; + final double birch; + final double grass; + final double mugwort; + final double olive; + final double ragweed; + + //hourly + final List pm2_5_h; + final List pm10_h; + final List no2_h; + final List o3_h; + final List co_h; + final List so2_h; + + final String mainPollutant; + + final List dailyAqi; + + final int european_aqi; + final int us_aqi; + + final double aod; + final String aod_desc; + + final double dust; + + const OMExtendedAqi({ + required this.co, + required this.so2, + required this.alder, + required this.birch, + required this.grass, + required this.mugwort, + required this.olive, + required this.ragweed, + + required this.aod, + required this.aod_desc, + + required this.dust, + + required this.european_aqi, + required this.us_aqi, + + required this.no2_h, + required this.o3_h, + required this.pm2_5_h, + required this.pm10_h, + required this.co_h, + required this.so2_h, + + required this.pm2_5_p, + required this.pm10_p, + required this.o3_p, + required this.no2_p, + required this.co_p, + required this.so2_p, + + required this.dailyAqi, + + required this.mainPollutant, + }); + + static Future fromJson(lat, lng, settings) async { + final params = { + "latitude": lat.toString(), + "longitude": lng.toString(), + "current": ['carbon_monoxide', 'sulphur_dioxide', + 'alder_pollen', 'birch_pollen', 'grass_pollen', 'mugwort_pollen', 'olive_pollen', 'ragweed_pollen', + 'aerosol_optical_depth', 'dust', 'european_aqi', 'us_aqi'], + "hourly" : ["pm10", "pm2_5", "nitrogen_dioxide", "ozone", "sulphur_dioxide", "carbon_monoxide"], + "timezone": "auto", + "forecast_days" : "5", + }; + final url = Uri.https("air-quality-api.open-meteo.com", 'v1/air-quality', params); + var file = await cacheManager2.getSingleFile(url.toString(), key: "$lat, $lng, aqi open-meteo extended").timeout(const Duration(seconds: 3)); + var response = await file.readAsString(); + final item = jsonDecode(response); + + final no2_h = List.from((item["hourly"]["nitrogen_dioxide"] as List?) ?.map((e) => (e as double?) ?? 0.0) ?? []); + final o3_h = List.from((item["hourly"]["ozone"] as List?) ?.map((e) => (e as double?) ?? 0.0) ?? []); + final pm2_5_h = List.from((item["hourly"]["pm2_5"] as List?) ?.map((e) => (e as double?) ?? 0.0) ?? []); + final pm10_h = List.from((item["hourly"]["pm10"] as List?) ?.map((e) => (e as double?) ?? 0.0) ?? []); + final co_h = List.from((item["hourly"]["carbon_monoxide"] as List?) ?.map((e) => (e as double?) ?? 0.0) ?? []); + final so2_h = List.from((item["hourly"]["sulphur_dioxide"] as List?) ?.map((e) => (e as double?) ?? 0.0) ?? []); + + + //determine the individual air quality indexes for each day using the hourly values of the different contaminants + // https://www.airnow.gov/publications/air-quality-index/technical-assistance-document-for-reporting-the-daily-aqi/ + + const List aqiCategories = [0, 51, 101, 151, 201, 301, 500]; + const List pollutantNames = ["ozone", "pm2.5", "pm10", "carbon monoxide", "sulphur dioxide", "nitrogen dioxide"]; + const List> breakpoints = [ + [0, 0.055, 0.071, 0.086, 0.106, 0.201, 0.604], //o3 + [0, 9.1, 35.5, 55.5, 125.5, 225.5, 325.4], //pm2.5 + [0, 55, 155, 255, 355, 425, 604], //pm10 + [0, 4.5, 9.5, 12.5, 15.5, 30.5, 50.4], //co + [0, 36, 76, 186, 305, 605, 1004], //so2 + [0, 54, 101, 361, 650, 1250, 2049] //no2 + ]; + + List dailyAqi = []; + String mainPollutant = "hehe"; + for (int i = 0; i < item["hourly"]["pm2_5"].length / 24; i++) { + //some of the values in the documentation are in ppm so open-meteo's mg/m^3 data has to be converted to ppm + //https://teesing.com/en/tools/ppm-mg3-converter <- used this as a reference + //the division by 1000 is because is because we're converting micrograms to grams + + List values = [ + double.parse((o3_h.getRange(i * 24, (i + 1) * 24).reduce(max) * 24.45 / 48 / 1000).toStringAsFixed(3)), + double.parse(pm2_5_h.getRange(i * 24, (i + 1) * 24).reduce(max).toStringAsFixed(1)), + double.parse(pm10_h.getRange(i * 24, (i + 1) * 24).reduce(max).toStringAsFixed(0)), + double.parse((co_h.getRange(i * 24, (i + 1) * 24).reduce(max) * 24.45 / 28.01 / 1000).toStringAsFixed(1)), + double.parse((so2_h.getRange(i * 24, (i + 1) * 24).reduce(max) * 24.45 / 64.066 / 1000).toStringAsFixed(0)), + double.parse((no2_h.getRange(i * 24, (i + 1) * 24).reduce(max) * 24.45 / 46.0055 / 1000).toStringAsFixed(0)), + ]; + + List final_indexes = []; + + for (int x = 0; x < 6; x++) { + + double current = values[x]; + + //find the above and below breakpoints + double bp_hi = 1; + double bp_lo = 0; + + int i_hi = 1; + int i_lo = 0; + + for (int z = 0; z < breakpoints[x].length - 1; z++) { + if (current >= breakpoints[x][z]) { + bp_lo = breakpoints[x][z]; + bp_hi = breakpoints[x][z + 1]; + + i_lo = aqiCategories[z]; + i_hi = aqiCategories[z + 1]; + } + } + + int final_index = (((i_hi - i_lo) / (bp_hi - bp_lo)) * (current - bp_lo) + i_lo).round(); + final_indexes.add(final_index); + } + int biggest = final_indexes.reduce(max); + + //determine the main pollutant for today + if (i == 0) { + print(("final indexes", final_indexes)); + mainPollutant = pollutantNames[final_indexes.indexOf(biggest)]; + } + + dailyAqi.add(biggest); + } + + const aod_names = ["extremely clear", "very clear", "clear", "slightly hazy", "hazy", "very hazy", "extremely hazy"]; + const aod_breakpoints = [0, 0.05, 0.1, 0.2, 0.4, 0.7, 1.0]; + + final aod_value = item["current"]["aerosol_optical_depth"]; + + int aod_index = 0; + for (int i = 0; i < aod_breakpoints.length; i++) { + if (aod_value > aod_breakpoints[i]) { + aod_index = i; + } + } + + final String aod_desc = aod_names[aod_index]; + + return OMExtendedAqi( + co: item["current"]["carbon_monoxide"], + so2: item["current"]["sulphur_dioxide"], + + alder: item["current"]["alder_pollen"] ?? -1, + birch: item["current"]["birch_pollen"] ?? -1, + grass: item["current"]["grass_pollen"] ?? -1, + mugwort: item["current"]["mugwort_pollen"] ?? -1, + olive: item["current"]["olive_pollen"] ?? -1, + ragweed: item["current"]["ragweed_pollen"] ?? -1, + + aod: aod_value, + aod_desc: aod_desc, + + dust: item["current"]["dust"], + + no2_h: no2_h, + o3_h: o3_h, + pm2_5_h: pm2_5_h, + pm10_h: pm10_h, + co_h: co_h, + so2_h: so2_h, + + mainPollutant: mainPollutant, + + dailyAqi: dailyAqi, + + european_aqi: item["current"]["european_aqi"], + us_aqi: item["current"]["us_aqi"], + + + //i am looking at the one before last because the last is basically only for calculating the high + //and not actually expected to be reached + o3_p: o3_h[0] * 24.45 / 48 / 1000 / breakpoints[0][breakpoints[0].length - 2] * 100, + pm2_5_p: pm2_5_h[0] / breakpoints[1][breakpoints[1].length - 2] * 100, + pm10_p: pm10_h[0] / breakpoints[2][breakpoints[2].length - 2] * 100, + co_p: co_h[0] * 24.45 / 28.01 / 1000 / breakpoints[3][breakpoints[3].length - 2] * 100, + so2_p: so2_h[0] * 24.45 / 64.066 / 1000 / breakpoints[4][breakpoints[4].length - 2] * 100, + no2_p: no2_h[0] * 24.45 / 46.0055 / 1000 / breakpoints[5][breakpoints[5].length - 2] * 100, + ); + } +} + Future OMGetWeatherData(lat, lng, real_loc, settings, placeName) async { var OM = await OMRequestData(lat, lng, real_loc); @@ -690,7 +921,7 @@ Future OMGetWeatherData(lat, lng, real_loc, settings, placeName) as return WeatherData( radar: await RainviewerRadar.getData(), - aqi: await OMAqi.fromJson(oMBody, lat, lng, settings), + aqi: await OMAqi.fromJson(lat, lng, settings), sunstatus: sunstatus, minutely_15_precip: OM15MinutePrecip.fromJson(oMBody, settings), diff --git a/lib/decoders/decode_mn.dart b/lib/decoders/decode_mn.dart index 9e6466d..e75d4ab 100644 --- a/lib/decoders/decode_mn.dart +++ b/lib/decoders/decode_mn.dart @@ -73,14 +73,16 @@ int metNcalculateFeelsLike(double t, double r, double v) { } -String metNGetName(index, settings, item, start) { +String metNGetName(index, settings, item, start, hourDif) { if (index < 3) { const names = ['Today', 'Tomorrow', 'Overmorrow']; return translation(names[index], settings["Language"]); } String x = item["properties"]["timeseries"][start]["time"].split("T")[0]; + String hour = item["properties"]["timeseries"][start]["time"].split("T")[1].split(":")[0]; List z = x.split("-"); - DateTime time = DateTime(int.parse(z[0]), int.parse(z[1]), int.parse(z[2])); + DateTime time_before = DateTime(int.parse(z[0]), int.parse(z[1]), int.parse(z[2]), int.parse(hour)); + DateTime time = time_before.add(-Duration(hours: hourDif)); const weeks = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; String weekname = translation(weeks[time.weekday - 1], settings["Language"]); return "$weekname, ${time.month}/${time.day}"; @@ -153,6 +155,7 @@ Future> MetNMakeRequest(double lat, double lng, String real_loc) a "User-Agent": "Overmorrow weather (com.marotidev.overmorrow)" }; final MnUrl = Uri.https("api.met.no", 'weatherapi/locationforecast/2.0/complete', MnParams); + print(MnUrl); var MnFile = await cacheManager2.getSingleFile(MnUrl.toString(), key: "$real_loc, met.no", headers: headers).timeout(const Duration(seconds: 6)); var MnResponse = await MnFile.readAsString(); @@ -409,7 +412,7 @@ class MetNDay { hourly_for_precip: hours, total_precip: double.parse(precip.reduce((a, b) => a + b).toStringAsFixed(1)), windspeed: (windspeeds.reduce((a, b) => a + b) / windspeeds.length).round(), - name: metNGetName(index, settings, item, start), + name: metNGetName(index, settings, item, start, hourDif), text: translation(weather_names[BIndex], settings["Language"]), icon: metNIconCorrection(weather_names[BIndex]), iconSize: oMIconSizeCorrection(weather_names[BIndex]), @@ -551,6 +554,91 @@ class MetNSunstatus { } } + +class MetN15MinutePrecip { //met norway doesn't actaully have 15 minute forecast, but i figured i could just use the + //hourly data and just use some smoothing between the hours to emulate the 15 minutes + //still better than not having it + final String t_minus; + final double precip_sum; + final List precips; + + const MetN15MinutePrecip({ + required this.t_minus, + required this.precip_sum, + required this.precips, + }); + + static MetN15MinutePrecip fromJson(item, settings) { + int closest = 100; + int end = -1; + double sum = 0; + + List precips = []; + List hourly = []; + + for (int i = 0; i < 6; i++) { + double x = double.parse(item["properties"]["timeseries"][i]["data"]["next_1_hours"]["details"]["precipitation_amount"].toStringAsFixed(1)); + + if (x > 0.0) { + if (closest == 100) { + closest = i + 1; + } + if (i >= end) { + end = i + 1; + } + } + + hourly.add(x); + } + + //smooth the hours into 15 minute segments + + for (int i = 0; i < hourly.length - 1; i++) { + double now = hourly[i]; + double next = hourly[i + 1]; + + double dif = next - now; + for (double x = 0; x <= 1; x += 0.25) { + double g = now + (dif * x); + sum += g; + precips.add(g); + } + } + + String t_minus = ""; + if (closest != 100) { + if (closest <= 2) { + if (end <= 1) { + t_minus = translation("rain expected in the next 1 hour", settings["Language"]); + } + else { + String x = " $end "; + t_minus = translation("rain expected in the next x hours", settings["Language"]); + t_minus = t_minus.replaceAll(" x ", x); + } + } + else if (closest < 1) { + t_minus = translation("rain expected in 1 hour", settings["Language"]); + } + else { + String x = " $closest "; + t_minus = translation("rain expected in x hours", settings["Language"]); + t_minus = t_minus.replaceAll(" x ", x); + } + } + + sum = max(sum, 0.1); //if there is rain then it shouldn't write 0 + + return MetN15MinutePrecip( + t_minus: t_minus, + precip_sum: unit_coversion(sum, settings["Precipitation"]), + precips: precips, + ); + + } + +} + Future MetNGetWeatherData(lat, lng, real_loc, settings, placeName) async { DateTime localTime = await MetNGetLocalTime(lat, lng); @@ -582,9 +670,9 @@ Future MetNGetWeatherData(lat, lng, real_loc, settings, placeName) return WeatherData( radar: await RainviewerRadar.getData(), - aqi: await OMAqi.fromJson(MnBody, lat, lng, settings), + aqi: await OMAqi.fromJson(lat, lng, settings), sunstatus: sunstatus, - minutely_15_precip: const OM15MinutePrecip(t_minus: "", precip_sum: 0, precips: []), //because MetN has no 15 minute forecast + minutely_15_precip: MetN15MinutePrecip.fromJson(MnBody, settings), current: await MetNCurrent.fromJson(MnBody, settings, real_loc, lat, lng), days: days, diff --git a/lib/decoders/decode_wapi.dart b/lib/decoders/decode_wapi.dart index 997d494..560c732 100644 --- a/lib/decoders/decode_wapi.dart +++ b/lib/decoders/decode_wapi.dart @@ -589,10 +589,10 @@ class WapiAqi { static WapiAqi fromJson(item) => WapiAqi( aqi_index: item["current"]["air_quality"]["us-epa-index"], - pm10: item["current"]["air_quality"]["pm10"], - pm2_5: item["current"]["air_quality"]["pm2_5"], - o3: item["current"]["air_quality"]["o3"], - no2: item["current"]["air_quality"]["no2"], + pm10: double.parse(item["current"]["air_quality"]["pm10"].toStringAsFixed(1)), + pm2_5: double.parse(item["current"]["air_quality"]["pm2_5"].toStringAsFixed(1)), + o3: double.parse(item["current"]["air_quality"]["o3"].toStringAsFixed(1)), + no2: double.parse(item["current"]["air_quality"]["no2"].toStringAsFixed(1)), aqi_title: ['good', 'fair', 'moderate', 'poor', 'very poor', 'unhealthy'] [item["current"]["air_quality"]["us-epa-index"] - 1], @@ -608,6 +608,110 @@ class WapiAqi { ); } + +class Wapi15MinutePrecip { //weatherapi doesn't actaully have 15 minute forecast, but i figured i could just use the + //hourly data and just use some smoothing between the hours to emulate the 15 minutes + //still better than not having it + final String t_minus; + final double precip_sum; + final List precips; + + const Wapi15MinutePrecip({ + required this.t_minus, + required this.precip_sum, + required this.precips, + }); + + static Wapi15MinutePrecip fromJson(item, settings) { + int closest = 100; + int end = -1; + double sum = 0; + + List precips = []; + List hourly = []; + + int day = 0; + int hour = 0; + + int i = 0; + + while (i < 6) { + if (item["forecast"]["forecastday"][day]["hour"].length > hour) { + double x; + if (i == 0) { + x = double.parse(item["current"]["precip_mm"].toStringAsFixed(1)); + } + else { + x = double.parse(item["forecast"]["forecastday"][day]["hour"][hour]["precip_mm"].toStringAsFixed(1)); + } + + if (x > 0.0) { + if (closest == 100) { + closest = i + 1; + } + if (i >= end) { + end = i + 1; + } + } + + hourly.add(x); + + i += 1; + hour += 1; + } + else { + day += 1; + } + } + + //smooth the hours into 15 minute segments + + for (int i = 0; i < hourly.length - 1; i++) { + double now = hourly[i]; + double next = hourly[i + 1]; + + double dif = next - now; + for (double x = 0; x <= 1; x += 0.25) { + double g = now + (dif * x); + sum += g; + precips.add(g); + } + } + + String t_minus = ""; + if (closest != 100) { + if (closest <= 2) { + if (end <= 1) { + t_minus = translation("rain expected in the next 1 hour", settings["Language"]); + } + else { + String x = " $end "; + t_minus = translation("rain expected in the next x hours", settings["Language"]); + t_minus = t_minus.replaceAll(" x ", x); + } + } + else if (closest < 1) { + t_minus = translation("rain expected in 1 hour", settings["Language"]); + } + else { + String x = " $closest "; + t_minus = translation("rain expected in x hours", settings["Language"]); + t_minus = t_minus.replaceAll(" x ", x); + } + } + + sum = max(sum, 0.1); //if there is rain then it shouldn't write 0 + + return Wapi15MinutePrecip( + t_minus: t_minus, + precip_sum: unit_coversion(sum, settings["Precipitation"]), + precips: precips, + ); + + } + +} + Future WapiGetWeatherData(lat, lng, real_loc, settings, placeName) async { var wapi = await WapiMakeRequest("$lat,$lng", real_loc); @@ -643,7 +747,9 @@ Future WapiGetWeatherData(lat, lng, real_loc, settings, placeName) fetch_datetime: fetch_datetime, updatedTime: DateTime.now(), localtime: WapiGetLocalTime(wapi_body), - minutely_15_precip: const OM15MinutePrecip(t_minus: "", precip_sum: 0, precips: []), //because wapi doesn't have 15 minutely + //minutely_15_precip: const OM15MinutePrecip(t_minus: "", precip_sum: 0, precips: []), //because wapi doesn't have 15 minutely + + minutely_15_precip: Wapi15MinutePrecip.fromJson(wapi_body, settings), //image: Uimage, ); diff --git a/lib/decoders/extra_info.dart b/lib/decoders/extra_info.dart index 3005035..ee8bab4 100644 --- a/lib/decoders/extra_info.dart +++ b/lib/decoders/extra_info.dart @@ -143,7 +143,12 @@ Future> getUnsplashImage(String _text, String real_loc, double lat //print(unsplash_body[index]["links"]["html"]); final String userLink = (unsplash_body[index]["user"]["links"]["html"]) ?? ""; - final String username = unsplash_body[index]["user"]["name"] ?? ""; + + //i don't want emojis because they ruin the one color aspect of the app + String username = unsplash_body[index]["user"]["name"] ?? ""; + final RegExp regExp = RegExp(r'(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])'); + username = username.replaceAll(regExp, "_"); + final String photoLink = unsplash_body[index]["links"]["html"] ?? ""; //final Color color = HexColor(unsplash_body[index]["color"]); diff --git a/lib/languages.dart b/lib/languages.dart index 20b4e69..c3131d8 100644 --- a/lib/languages.dart +++ b/lib/languages.dart @@ -1529,6 +1529,207 @@ Map> mainTranslate = { '30λ', ], + "Air Quality": [ + 'Air Quality', + 'Légszennyezettség', + 'Calidad del aire', + 'Qualité de l\'air', + 'Luftqualität', + 'Qualità dell\'aria', + 'Qualidade do ar', + 'Качество воздуха', + '空气质量', + '大気質', + 'Jakość powietrza', + 'Ποιότητα αέρα' + ], + "main pollutant": [ + 'main pollutant', + 'fő szennyezőanyag', + 'principal contaminante', + 'polluant principal', + 'Hauptschadstoff', + 'inquinante principale', + 'principal poluente', + 'основной загрязнитель', + '主要污染物', + '主要汚染物質', + 'główny zanieczyszczający', + 'κύριος ρύπος' + ], + "Alder Pollen": [ + 'Alder Pollen', + 'Éger pollen', + 'polen de aliso', + 'pollen d\'aulne', + 'Erlenpollen', + 'polline di ontano', + 'pólen de amieiro', + 'пыльца ольхи', + '桤木花粉', + 'ハンノキの花粉', + 'pyłek olchy', + 'γύρη σκλήθρου' + ], + "Birch Pollen": [ + 'Birch Pollen', + 'Nyír pollen', + 'polen de abedul', + 'pollen de bouleau', + 'Birkenpollen', + 'polline di betulla', + 'pólen de bétula', + 'пыльца берёзы', + '桦木花粉', + 'カバノキの花粉', + 'pyłek brzozy', + 'γύρη σημύδας' + ], + "Grass Pollen": [ + 'Grass Pollen', + 'Fű pollen', + 'polen de hierba', + 'pollen de graminées', + 'Gräserpollen', + 'polline d\'erba', + 'pólen de grama', + 'пыльца трав', + '草花粉', + '草の花粉', + 'pyłek traw', + 'γύρη χόρτου' + ], + "Mugwort Pollen": [ + 'Mugwort Pollen', + 'Üröm pollen', + 'polen de artemisa', + 'pollen d\'armoise', + 'Beifußpollen', + 'polline di artemisia', + 'pólen de losna', + 'пыльца полыни', + '艾蒿花粉', + 'ヨモギの花粉', + 'pyłek bylicy', + 'γύρη αψιθιάς' + ], + "Olive Pollen": [ + 'Olive Pollen', + 'Olíva pollen', + 'polen de olivo', + 'pollen d\'olivier', + 'Olivenpollen', + 'polline di ulivo', + 'pólen de oliveira', + 'пыльца оливы', + '橄榄花粉', + 'オリーブの花粉', + 'pyłek oliwny', + 'γύρη ελιάς' + ], + "Ragweed Pollen": [ + 'Ragweed Pollen', + 'Parlagfű pollen', + 'polen de ambrosía', + 'pollen d\'ambroisie', + 'Ambrosiapollen', + 'polline di ambrosia', + 'pólen de ambrósia', + 'пыльца амброзии', + '豚草花粉', + 'ブタクサの花粉', + 'pyłek ambrozji', + 'γύρη αμβροσίας' + ], + "daily aqi": [ + 'daily AQI', + 'napi AQI', + 'AQI diario', + 'AQI quotidien', + 'täglicher AQI', + 'AQI giornaliero', + 'AQI diário', + 'ежедневный AQI', + '每日空气质量指数', + '毎日のAQI', + 'dzienne AQI', + 'καθημερινό AQI' + ], + + //day + "d": [ + "d", + "n", + "d", + "j", + "T", + "g", + "d", + "д", + "天", + "日", + "d", + "η", + ], + + "aerosol optical depth": [ + "aerosol optical depth", + "aeroszoloptikai mélység", + "profundidad óptica de aerosol", + "profondeur optique d'aérosol", + "aerosol-optische tiefe", + "profondità ottica degli aerosol", + "profundidade óptica do aerossol", + "оптическая глубина аэрозолей", + "气溶胶光学深度", + "エアロゾル光学的厚さ", + "głębokość optyczna aerozolu", + "οπτικό βάθος αερολύματος" + ], + "dust": [ + "dust", + "por", + "polvo", + "poussière", + "staub", + "polvere", + "poeira", + "пыль", + "灰尘", + "ほこり", + "pył", + "σκόνη" + ], + "european aqi": [ + "european aqi", + "európai aqi", + "aqi europeo", + "aqi européen", + "eu aqi", + "aqi europeo", + "aqi europeu", + "евро aqi", + "欧盟aqi", + "eu aqi", + "europejski aqi", + "ευρωπαϊκός aqi" + ], + "united states aqi": [ + "united states aqi", + "usa aqi", + "aqi de ee. uu.", + "aqi des états-unis", + "us aqi", + "aqi degli stati uniti", + "aqi dos eua", + "aqi сша", + "美国aqi", + "アメリカaqi", + "amerykański aqi", + "aqi ηπα" + ], + + 'Search translation': [ //used for getting the codes used for translation of city names //If none are available then en (english) is the default // for more info visit https://open-meteo.com/en/docs/geocoding-api diff --git a/lib/main_screens.dart b/lib/main_screens.dart index 245b0c1..f09303f 100644 --- a/lib/main_screens.dart +++ b/lib/main_screens.dart @@ -132,7 +132,7 @@ class _NewMainState extends State { final Map widgetsMap = { 'sunstatus': NewSunriseSunset(data: data, key: Key(data.place), size: size,), 'rain indicator': NewRain15MinuteIndicator(data), - 'air quality': NewAirQuality(data), + 'air quality': NewAirQuality(data, context), 'radar': RadarSmall(data: data, key: Key("${data.place}, ${data.current.surface}")), 'forecast': buildNewDays(data), 'daily': buildNewGlanceDay(data: data), @@ -164,7 +164,7 @@ class _NewMainState extends State { headerData: HeaderData( //backgroundColor: WHITE, blurContent: false, - headerHeight: max(size.height * 0.525, 400), + headerHeight: max(size.height * 0.518, 400), //we don't want it to be smaller than 400 header: ParrallaxBackground(image: data.current.image, key: Key(data.place), color: data.current.surface == BLACK ? BLACK @@ -395,7 +395,7 @@ Widget TabletLayout(data, updateLocation, context) { children: [ NewSunriseSunset(data: data, key: Key(data.place), size: size,), NewRain15MinuteIndicator(data), - NewAirQuality(data), + NewAirQuality(data, context), RadarSmall(data: data, key: Key("${data.place}, ${data.current.surface}")), buildNewGlanceDay(data: data, key: Key("${data.place}, ${data.current.primary}"),), Padding( diff --git a/lib/new_displays.dart b/lib/new_displays.dart index e2fd451..c55eb86 100644 --- a/lib/new_displays.dart +++ b/lib/new_displays.dart @@ -23,6 +23,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:overmorrow/settings_page.dart'; import 'package:overmorrow/ui_helper.dart'; +import 'aqi_page.dart'; import 'decoders/decode_OM.dart'; class WavePainter extends CustomPainter { @@ -157,7 +158,7 @@ class _NewSunriseSunsetState extends State with SingleTickerPr final textWidth = textPainter.width; return Padding( - padding: const EdgeInsets.only(left: 25, right: 25, bottom: 23), + padding: const EdgeInsets.only(left: 25, right: 25, bottom: 11), child: Column( children: [ Padding( @@ -241,73 +242,98 @@ class _NewSunriseSunsetState extends State with SingleTickerPr } } -Widget NewAirQuality(var data) { +Widget NewAirQuality(var data, context) { return Padding( padding: const EdgeInsets.only(left: 20, right: 20, bottom: 59), - child: Column( - children: [ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(bottom: 6, left: 5), - child: comfortatext( - translation('air quality', data.settings["Language"]), - 16, - data.settings, - color: data.current.onSurface), + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: (){ + Navigator.push( + context, + MaterialPageRoute(builder: (context) => AllergensPage(data: data)) + ); + }, + child: Column( + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 0, left: 5), + child: comfortatext( + translation('air quality', data.settings["Language"]), + 16, + data.settings, + color: data.current.onSurface), + ), + const Spacer(), + GestureDetector( + onTap: (){ + Navigator.push( + context, + MaterialPageRoute(builder: (context) => AllergensPage(data: data)) + ); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 1), + child: SizedBox( + width: 40, height: 36, + child: Icon(Icons.keyboard_arrow_right, color: data.current.primary, size: 21,)), + ) + ), + ], ), - ), - Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 5, top: 5, right: 14), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - color: data.current.containerLow), - width: 65, - height: 65, - child: Center( - child: comfortatext( - data.aqi.aqi_index.toString(), 32, data.settings, - color: data.current.primarySecond)), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 5, top: 0, right: 14), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: data.current.containerLow), + width: 71, + height: 71, + child: Center( + child: comfortatext( + data.aqi.aqi_index.toString(), 32, data.settings, + color: data.current.primarySecond)), + ), ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Align( - alignment: Alignment.topLeft, - child: comfortatext( - data.aqi.aqi_title, 19, data.settings, color: data.current.primarySecond, align: TextAlign.left, - weight: FontWeight.w500, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: Alignment.topLeft, + child: comfortatext( + data.aqi.aqi_title, 19, data.settings, color: data.current.primarySecond, align: TextAlign.left, + weight: FontWeight.w500, + ), ), - ), - Padding( - padding: const EdgeInsets.only(top: 5, left: 2), - child: comfortatext(data.aqi.aqi_desc, 14, data.settings, - color: data.settings["Color mode"] == "light" ? data.current.primary : data.current.onSurface, - weight: FontWeight.w500), - ), - ], + Padding( + padding: const EdgeInsets.only(top: 5, left: 2), + child: comfortatext(data.aqi.aqi_desc, 14, data.settings, + color: data.settings["Color mode"] == "light2" ? data.current.primary : data.current.onSurface, + weight: FontWeight.w500), + ), + ], + ), ), - ), - ], - ), - Padding( - padding: const EdgeInsets.only(top: 15, left: 14, right: 14), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - NewAqiDataPoints("PM2.5", data.aqi.pm2_5, data), - NewAqiDataPoints("PM10", data.aqi.pm10, data), - NewAqiDataPoints("O3", data.aqi.o3, data), - NewAqiDataPoints("NO2", data.aqi.no2, data), ], ), - ) - ], + Padding( + padding: const EdgeInsets.only(top: 15, left: 14, right: 14), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + NewAqiDataPoints("PM2.5", data.aqi.pm2_5, data), + NewAqiDataPoints("PM10", data.aqi.pm10, data), + NewAqiDataPoints("O3", data.aqi.o3, data), + NewAqiDataPoints("NO2", data.aqi.no2, data), + ], + ), + ) + ], + ), ), ); } @@ -316,7 +342,7 @@ Widget NewRain15MinuteIndicator(var data) { return Visibility( visible: data.minutely_15_precip.t_minus != "", child: Padding( - padding: const EdgeInsets.only(left: 21, right: 21, bottom: 38), + padding: const EdgeInsets.only(left: 21, right: 21, bottom: 33, top: 13), child: Container( decoration: BoxDecoration( color: data.current.containerLow, @@ -331,7 +357,7 @@ Widget NewRain15MinuteIndicator(var data) { children: [ Padding( padding: - const EdgeInsets.only(left: 5, bottom: 2, right: 3), + const EdgeInsets.only(left: 5, bottom: 2, right: 3, top: 1), child: Icon( Icons.water_drop_outlined, color: data.current.primary, @@ -397,4 +423,4 @@ Widget NewRain15MinuteIndicator(var data) { ), ) ); -} \ No newline at end of file +} diff --git a/lib/new_forecast.dart b/lib/new_forecast.dart index 3cf5002..68fbb6a 100644 --- a/lib/new_forecast.dart +++ b/lib/new_forecast.dart @@ -69,216 +69,222 @@ class _NewDayState extends State with AutomaticKeepAliveClientMixin { Color highlight = state ? data.current.containerHigh : data.current.container; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Stack( children: [ - Padding( - padding: EdgeInsets.only(left: state ? 15 : 5, top: 0), - child: Row( - children: [ - comfortatext( - day.name, 16, - data.settings, - color: data.current.onSurface), - const Spacer(), - Visibility( - visible: state, - child: Padding( - padding: const EdgeInsets.only(right: 13), - child: GestureDetector( - child: Icon(Icons.expand_less, color: data - .current.primaryLight, size: 20), - onTap: () { - HapticFeedback.selectionClick(); - onExpandTapped(index); - }, - ), - ), - ), - ], - ), + if (state) GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + onExpandTapped(index); + }, + child: const SizedBox(height: 70, width: double.infinity,) ), - Padding( - padding: const EdgeInsets.only(top: 18, left: 23, right: 25), - child: Row( - children: [ - SizedBox( - width: 35, - child: Icon(day.icon, size: 38.0 * day.iconSize, color: data.current.primary,)), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 12.0, top: 3), - child: comfortatext(day.text, 20, data.settings, color: data.current.onSurface, - weight: FontWeight.w400), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 4), - child: Row( - children: [ - comfortatext(day.minmaxtemp.split("/")[0], 20, data.settings, color: data.current.primary), - Padding( - padding: const EdgeInsets.only(left: 5, right: 7), - child: comfortatext("/", 19, data.settings, color: data.current.onSurface), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: state ? 15 : 5, top: state ? 20 : 0), + child: Row( + children: [ + comfortatext( + day.name, 16, + data.settings, + color: data.current.onSurface), + const Spacer(), + if (state) Padding( + padding: const EdgeInsets.only(right: 13), + child: GestureDetector( + child: Icon(Icons.expand_less, color: data + .current.primaryLight, size: 20), + onTap: () { + onExpandTapped(index); + }, ), - comfortatext(day.minmaxtemp.split("/")[1], 20, data.settings, color: data.current.primary), - ], - ), - ) - ], - ), - ), - Visibility( - visible: day.mm_precip > 0.1, - child: RainWidget(data, day, highlight) - ), - Padding( - padding: const EdgeInsets.only(left: 8, right: 8, top: 15, bottom: 10), - child: Container( - height: 85, - padding: const EdgeInsets.only(top: 8, bottom: 8, left: 10, right: 10), - decoration: BoxDecoration( - //border: Border.all(width: 1, color: data.current.outline), - color: state ? data.current.container : data.current.containerLow, - borderRadius: BorderRadius.circular(18), + ), + ], + ), ), - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return GridView.count( - padding: const EdgeInsets.all(0), - physics: const NeverScrollableScrollPhysics(), - crossAxisSpacing: 1, - mainAxisSpacing: 1, - crossAxisCount: 2, - childAspectRatio: constraints.maxWidth / constraints.maxHeight, + Padding( + padding: const EdgeInsets.only(top: 18, left: 23, right: 25, bottom: 5), + child: Row( + children: [ + SizedBox( + width: 35, + child: Icon(day.icon, size: 38.0 * day.iconSize, color: data.current.primary,)), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 12.0, top: 3), + child: comfortatext(day.text, 20, data.settings, color: data.current.onSurface, + weight: FontWeight.w400), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( children: [ + comfortatext(day.minmaxtemp.split("/")[0], 20, data.settings, color: data.current.primary), Padding( - padding: const EdgeInsets.only( - left: 8, right: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.water_drop_outlined, - color: data.current.primaryLight, size: 21), - Padding( - padding: const EdgeInsets.only(left: 10, top: 3), - child: comfortatext('${day.precip_prob}%', 18, data.settings, - color: data.current.primary), - ), - ], - ), + padding: const EdgeInsets.only(left: 5, right: 7), + child: comfortatext("/", 19, data.settings, color: data.current.onSurface), ), - Padding( - padding: const EdgeInsets.only( - left: 8, right: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.water_drop, color: data.current.primaryLight, size: 21), - Padding( - padding: const EdgeInsets.only(top: 3, left: 10), - child: comfortatext(day.total_precip.toString() + - data.settings["Precipitation"], 18, data.settings, - color: data.current.primary), + comfortatext(day.minmaxtemp.split("/")[1], 20, data.settings, color: data.current.primary), + ], + ), + ) + ], + ), + ), + Visibility( + visible: day.mm_precip > 0.1, + child: RainWidget(data, day, highlight, data.current.containerHigh) + ), + Padding( + padding: const EdgeInsets.only(left: 8, right: 8, top: 18, bottom: 10), + child: Container( + height: 85, + padding: const EdgeInsets.only(top: 8, bottom: 8, left: 10, right: 10), + decoration: BoxDecoration( + //border: Border.all(width: 1, color: data.current.outline), + color: state ? data.current.container : data.current.containerLow, + borderRadius: BorderRadius.circular(18), + ), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return GridView.count( + padding: const EdgeInsets.all(0), + physics: const NeverScrollableScrollPhysics(), + crossAxisSpacing: 1, + mainAxisSpacing: 1, + crossAxisCount: 2, + childAspectRatio: constraints.maxWidth / constraints.maxHeight, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 8, right: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.water_drop_outlined, + color: data.current.primaryLight, size: 21), + Padding( + padding: const EdgeInsets.only(left: 10, top: 3), + child: comfortatext('${day.precip_prob}%', 18, data.settings, + color: data.current.primary), + ), + ], ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 8, right: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - CupertinoIcons.wind, color: data.current.primaryLight, size: 21,), - Padding( - padding: const EdgeInsets.only(top: 3, left: 10), - child: comfortatext('${day.windspeed} ${data - .settings["Wind"]}', 18, data.settings, - color: data.current.primary), + ), + Padding( + padding: const EdgeInsets.only( + left: 8, right: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.water_drop, color: data.current.primaryLight, size: 21), + Padding( + padding: const EdgeInsets.only(top: 3, left: 10), + child: comfortatext(day.total_precip.toString() + + data.settings["Precipitation"], 18, data.settings, + color: data.current.primary), + ), + ], ), - Padding( - padding: const EdgeInsets.only(left: 5, right: 3), - child: RotationTransition( - turns: AlwaysStoppedAnimation(day.wind_dir / 360), - child: Icon(CupertinoIcons.arrow_down_circle, - color: data.current.primaryLight, size: 18,) - ) + ), + Padding( + padding: const EdgeInsets.only( + left: 8, right: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + CupertinoIcons.wind, color: data.current.primaryLight, size: 21,), + Padding( + padding: const EdgeInsets.only(top: 3, left: 10), + child: comfortatext('${day.windspeed} ${data + .settings["Wind"]}', 18, data.settings, + color: data.current.primary), + ), + Padding( + padding: const EdgeInsets.only(left: 5, right: 3), + child: RotationTransition( + turns: AlwaysStoppedAnimation(day.wind_dir / 360), + child: Icon(CupertinoIcons.arrow_down_circle, + color: data.current.primaryLight, size: 18,) + ) + ), + ], ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 8, right: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(CupertinoIcons.sun_max, - color: data.current.primaryLight, size: 21), - Padding( - padding: const EdgeInsets.only(top: 3, left: 10), - child: comfortatext('${day.uv} UV', 18, data.settings, - color: data.current.primary), + ), + Padding( + padding: const EdgeInsets.only( + left: 8, right: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(CupertinoIcons.sun_max, + color: data.current.primaryLight, size: 21), + Padding( + padding: const EdgeInsets.only(top: 3, left: 10), + child: comfortatext('${day.uv} UV', 18, data.settings, + color: data.current.primary), + ), + ], ), - ], - ), - ), - ] - ); - } - ), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 15), - child: Wrap( - spacing: 5.0, - children: List.generate( - 4, - (int index) { - - - return ChoiceChip( - elevation: 0.0, - checkmarkColor: data.current.onPrimaryLight, - color: WidgetStateProperty.resolveWith((states) { - if (index == _value) { - return data.current.primaryLighter; + ), + ] + ); } - return state ? data.current.containerLow : data.current.surface; - }), - side: BorderSide(color: data.current.primaryLighter, width: 1.5), - label: comfortatext( - translation(['temp', 'precip', 'wind', 'uv'][index], data.settings["Language"]), 14, data.settings, - color: _value == index ? data.current.onPrimaryLight : data.current.onSurface), - selected: _value == index, - onSelected: (bool selected) { - _value = index; - setState(() { - HapticFeedback.selectionClick(); - _onItemTapped(index); - }); + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 15), + child: Wrap( + spacing: 5.0, + children: List.generate( + 4, + (int index) { + + return ChoiceChip( + elevation: 0.0, + checkmarkColor: data.current.onPrimaryLight, + color: WidgetStateProperty.resolveWith((states) { + if (index == _value) { + return data.current.primaryLighter; + } + return state ? data.current.containerLow : data.current.surface; + }), + side: BorderSide(color: data.current.primaryLighter, width: 1.5), + label: comfortatext( + translation(['temp', 'precip', 'wind', 'uv'][index], data.settings["Language"]), 14, data.settings, + color: _value == index ? data.current.onPrimaryLight : data.current.onSurface), + selected: _value == index, + onSelected: (bool selected) { + _value = index; + setState(() { + HapticFeedback.lightImpact(); + _onItemTapped(index); + }); + }, + ); }, - ); - }, - ).toList(), - ), - ), - SizedBox( - height: state? 280 : 260, - child: PageView( - physics: const NeverScrollableScrollPhysics(), - controller: _pageController, - children: [ - buildTemp(day.hourly, data, highlight), - buildPrecip(day.hourly, data, data.current.containerHigh), - WindReport(hours: day.hourly, data: data, highlight: data.current.containerHigh,), - buildUV(day.hourly, data, highlight), - ], - ), + ).toList(), + ), + ), + SizedBox( + height: state? 280 : 260, + child: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: _pageController, + children: [ + buildTemp(day.hourly, data, data.current.containerHigh), + buildPrecip(day.hourly, data, data.current.containerHigh), + WindReport(hours: day.hourly, data: data, highlight: data.current.containerHigh,), + buildUV(day.hourly, data, data.current.containerHigh), + ], + ), + ), + ], ), ], ); @@ -693,7 +699,7 @@ class _buildNewGlanceDayState extends State with AutomaticKee void _onExpandTapped(int index) { setState(() { - HapticFeedback.selectionClick(); + HapticFeedback.lightImpact(); expand[index] = !expand[index]; }); } @@ -749,7 +755,7 @@ class _buildNewGlanceDayState extends State with AutomaticKee : BorderRadius.circular(8), color: data.current.containerLow), child: Padding( - padding: const EdgeInsets.only(top: 25, left: 3, right: 3), + padding: const EdgeInsets.only(top: 5, left: 3, right: 3), child: NewDay(data: data, index: index, state: true, onExpandTapped: _onExpandTapped, day: day,), ), @@ -772,187 +778,192 @@ class _buildNewGlanceDayState extends State with AutomaticKee Widget GlanceDayEntry(data, index, day, onExpandTapped) { - return Container( - decoration: BoxDecoration( - borderRadius: - index == 0 ? const BorderRadius.vertical( - top: Radius.circular(18.0), - bottom: Radius.circular(8)) - : index == data.days.length - 4 ? const BorderRadius - .vertical(bottom: Radius.circular(18.0), - top: Radius.circular(8)) - : BorderRadius.circular(8), - color: data.current.containerLow), - child: Column( - children: [ - Row( - children: [ - SizedBox( - width: 60, - height: 73, - child: Padding( - padding: const EdgeInsets.only(left: 18), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - comfortatext(day.name.split(", ")[0], 18, - data.settings, - color: data.current.primary), - comfortatext(day.name.split(", ")[1], 14, - data.settings, - color: data.current.onSurface), - ], + return GestureDetector( + onTap: () { + onExpandTapped(index); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: + index == 0 ? const BorderRadius.vertical( + top: Radius.circular(18.0), + bottom: Radius.circular(8)) + : index == data.days.length - 4 ? const BorderRadius + .vertical(bottom: Radius.circular(18.0), + top: Radius.circular(8)) + : BorderRadius.circular(8), + color: data.current.containerLow), + child: Column( + children: [ + Row( + children: [ + SizedBox( + width: 60, + height: 73, + child: Padding( + padding: const EdgeInsets.only(left: 18), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + comfortatext(day.name.split(", ")[0], 18, + data.settings, + color: data.current.primary), + comfortatext(day.name.split(", ")[1], 14, + data.settings, + color: data.current.onSurface), + ], + ), ), ), - ), - SizedBox( - height: 30, - width: 43, - child: Icon( - day.icon, - color: data.current.primary, - size: 31.0 * day.iconSize, - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 10, top: 2, bottom: 2), - child: Container( - height: 56, + SizedBox( + height: 30, width: 43, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(13), - //border: Border.all(width: 1.5, color: data.current.primaryLight) - color: data.current.primaryLighter + child: Icon( + day.icon, + color: data.current.primary, + size: 31.0 * day.iconSize, ), - child: Row( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: 2), - child: Icon(Icons.keyboard_arrow_up, - color: data.current.onPrimaryLight, - size: 14,), - ), - Icon(Icons.keyboard_arrow_down, - color: data.current.onPrimaryLight, size: 14,), - ], - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment - .center, + ), + Padding( + padding: const EdgeInsets.only( + left: 10, top: 2, bottom: 2), + child: Container( + height: 56, + width: 43, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(13), + //border: Border.all(width: 1.5, color: data.current.primaryLight) + color: data.current.primaryLighter + ), + child: Row( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.only( bottom: 2), - child: comfortatext( - day.minmaxtemp.split("/")[1], 14, + child: Icon(Icons.keyboard_arrow_up, + color: data.current.onPrimaryLight, + size: 14,), + ), + Icon(Icons.keyboard_arrow_down, + color: data.current.onPrimaryLight, size: 14,), + ], + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment + .center, + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 2), + child: comfortatext( + day.minmaxtemp.split("/")[1], 14, + data.settings, + color: data.current.onPrimaryLight), + ), + comfortatext( + day.minmaxtemp.split("/")[0], 14, data.settings, color: data.current.onPrimaryLight), + ], + ), + ), + ], + ), + ), + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.water_drop_outlined, + color: data.current.primaryLight, + size: 18,), + Padding( + padding: const EdgeInsets.only( + left: 2, right: 8), + child: comfortatext( + '${day.precip_prob}%', 17, + data.settings, + color: data.current.onSurface), + ), + Icon( + Icons.water_drop, + color: data.current.primaryLight, + size: 18,), + Padding( + padding: const EdgeInsets.only( + left: 2, right: 2), + child: comfortatext( + day.total_precip.toString() + + data.settings["Precipitation"], + 17, data.settings, + color: data.current.onSurface), ), - comfortatext( - day.minmaxtemp.split("/")[0], 14, - data.settings, - color: data.current.onPrimaryLight), ], ), ), - ], - ), - ), - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row( + Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.water_drop_outlined, + Icon( + CupertinoIcons.wind, color: data.current.primaryLight, size: 18,), Padding( padding: const EdgeInsets.only( - left: 2, right: 8), + left: 2, right: 2), child: comfortatext( - '${day.precip_prob}%', 17, + '${day.windspeed} ${data + .settings["Wind"]}', 17, data.settings, color: data.current.onSurface), ), - Icon( - Icons.water_drop, - color: data.current.primaryLight, - size: 18,), Padding( - padding: const EdgeInsets.only( - left: 2, right: 2), - child: comfortatext( - day.total_precip.toString() + - data.settings["Precipitation"], - 17, data.settings, - color: data.current.onSurface), + padding: const EdgeInsets.only( + left: 3, right: 3, bottom: 1), + child: RotationTransition( + turns: AlwaysStoppedAnimation( + day.wind_dir / 360), + child: Icon( + CupertinoIcons.arrow_down_circle, + color: data.current.primary, + size: 16,) + ) ), ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - CupertinoIcons.wind, - color: data.current.primaryLight, - size: 18,), - Padding( - padding: const EdgeInsets.only( - left: 2, right: 2), - child: comfortatext( - '${day.windspeed} ${data - .settings["Wind"]}', 17, - data.settings, - color: data.current.onSurface), - ), - Padding( - padding: const EdgeInsets.only( - left: 3, right: 3, bottom: 1), - child: RotationTransition( - turns: AlwaysStoppedAnimation( - day.wind_dir / 360), - child: Icon( - CupertinoIcons.arrow_down_circle, - color: data.current.primary, - size: 16,) - ) - ), - ], - ) - ], + ) + ], + ), ), - ), - Padding( - padding: const EdgeInsets.only(right: 10), - child: SizedBox( - width: 28, - child: IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - icon: Icon(Icons.expand_more, color: data - .current.primaryLight, size: 20,), - onPressed: () { - onExpandTapped(index); - }, + Padding( + padding: const EdgeInsets.only(right: 10), + child: SizedBox( + width: 28, + child: IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: Icon(Icons.expand_more, color: data + .current.primaryLight, size: 20,), + onPressed: () { + onExpandTapped(index); + }, + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ); } \ No newline at end of file diff --git a/lib/radar.dart b/lib/radar.dart index 53d0856..5529dff 100644 --- a/lib/radar.dart +++ b/lib/radar.dart @@ -139,7 +139,7 @@ class _RadarSmallState extends State { color: data.current.surface, borderRadius: BorderRadius.circular(18), border: Border.all( - width: 2.2, color: data.current.containerHigh) + width: 2.5, color: data.current.containerHigh) ), child: Stack( children: [ diff --git a/lib/search_screens.dart b/lib/search_screens.dart index 4cfa759..48c5914 100644 --- a/lib/search_screens.dart +++ b/lib/search_screens.dart @@ -68,7 +68,7 @@ Widget searchBar(Color color, List recommend, elevation: 0, height: 62, scrollPadding: const EdgeInsets.only(top: 16, bottom: 56), - transitionDuration: const Duration(milliseconds: 800), + transitionDuration: const Duration(milliseconds: 700), transitionCurve: Curves.easeInOut, physics: const BouncingScrollPhysics(), debounceDelay: const Duration(milliseconds: 500), diff --git a/lib/ui_helper.dart b/lib/ui_helper.dart index 8839553..7ecb34c 100644 --- a/lib/ui_helper.dart +++ b/lib/ui_helper.dart @@ -387,11 +387,11 @@ class DescriptionCircle extends StatelessWidget { } } -Widget NewAqiDataPoints(String name, double value, var data) { +Widget NewAqiDataPoints(String name, double value, var data, [double size = 15]) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - comfortatext(name, 15, data.settings, color: data.current.primary, + comfortatext(name, size, data.settings, color: data.current.primary, align: TextAlign.end, weight: FontWeight.w500), Padding( padding: const EdgeInsets.all(3.0), @@ -404,13 +404,13 @@ Widget NewAqiDataPoints(String name, double value, var data) { ), ), ), - comfortatext(value.toString(), 15, data.settings, color: data.current.primarySecond, + comfortatext(value.toString(), size, data.settings, color: data.current.primarySecond, align: TextAlign.end, weight: FontWeight.w600), ], ); } -Widget RainWidget(data, day, highlight) { +Widget RainWidget(data, day, highlight, border) { List hours = day.hourly_for_precip; List precip = []; @@ -429,7 +429,7 @@ Widget RainWidget(data, day, highlight) { } return Padding( - padding: const EdgeInsets.only(left: 10, right: 10, top: 15), + padding: const EdgeInsets.only(left: 8, right: 8, top: 15), child: Container( constraints: const BoxConstraints(minWidth: 0, maxWidth: 450), decoration: BoxDecoration( @@ -437,14 +437,14 @@ Widget RainWidget(data, day, highlight) { //color: data.current.containerLow, border: data.settings["Color mode"] == "dark" || data.settings["Color mode"] == "light" || data.settings["Color mode"] == "auto" - ? Border.all(width: 3, color: highlight) + ? Border.all(width: 2.6, color: border) : Border.all(width: 1.6, color: data.current.primaryLight) ), child: Column( children: [ Padding( - padding: const EdgeInsets.only(top: 14, right: 15, left: 18), + padding: const EdgeInsets.only(top: 16, right: 17, left: 17), child: AspectRatio( aspectRatio: 2.2, child: MyChart(precip, data, highlight) @@ -534,7 +534,8 @@ class BarChartPainter extends CustomPainter { if (i <= smallerThan) { canvas.drawArc( Rect.fromCenter( - center: Offset(x + barWidth * 0.5, start), + center: Offset(x + barWidth * 0.5, start - 0.05), //this small offset is there + // to remove the small line between the two half circles height: barWidth * 0.8, width: barWidth * 0.8, ), diff --git a/lib/weather_refact.dart b/lib/weather_refact.dart index e104143..4655234 100644 --- a/lib/weather_refact.dart +++ b/lib/weather_refact.dart @@ -383,14 +383,19 @@ Map textFilter = { 'black and white' : -100000, 'graffiti' : -2000, 'meat' : -5000, + 'toy' : -100000, 'man': -10000000, //trying to not have people in images + 'men': -10000000, 'male': -1000000, 'couple': -1000000, 'female': -1000000, 'human': -1000000, 'girl': -1000000, 'boy': -1000000, + 'kid': -1000000, + 'toddler': -1000000, 'woman': -10000000, + 'women': -10000000, 'person': -10000000, 'child': -10000000, 'crowd': -10000,