diff --git a/Samples/Nalu.Maui.Weather/Converters/WeatherCodeToImageConverter.cs b/Samples/Nalu.Maui.Weather/Converters/WeatherCodeToImageConverter.cs new file mode 100644 index 0000000..afd7dc8 --- /dev/null +++ b/Samples/Nalu.Maui.Weather/Converters/WeatherCodeToImageConverter.cs @@ -0,0 +1,22 @@ +namespace Nalu.Maui.Weather.Converters; + +using System; +using System.Globalization; +using Microsoft.Maui.Controls; + +public class WeatherCodeToImageConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is int weatherCode && WeatherData.WeatherCodes.TryGetValue(weatherCode, out var weatherInfo)) + { + return weatherInfo.Image; + } + + return WeatherData.WeatherCodes[0].Image; // Default to clear sky + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotImplementedException(); // One-way binding, no need for ConvertBack +} + diff --git a/Samples/Nalu.Maui.Weather/Converters/WeatherData.cs b/Samples/Nalu.Maui.Weather/Converters/WeatherData.cs index 600069f..8549c45 100644 --- a/Samples/Nalu.Maui.Weather/Converters/WeatherData.cs +++ b/Samples/Nalu.Maui.Weather/Converters/WeatherData.cs @@ -4,36 +4,36 @@ namespace Nalu.Maui.Weather.Converters; public static class WeatherData { - public static readonly Dictionary WeatherCodes = new() + public static readonly Dictionary WeatherCodes = new() { - { 0, ("Clear sky", "\ue430") }, - { 1, ("Mainly clear", "\ue430") }, - { 2, ("Partly cloudy", "\ue42d") }, - { 3, ("Overcast", "\ue3dd") }, - { 45, ("Fog", "\uf029") }, - { 48, ("Depositing rime fog", "\uf029") }, - { 51, ("Drizzle: Light intensity", "\ue3ea") }, - { 53, ("Drizzle: Moderate intensity", "\ue3ea") }, - { 55, ("Drizzle: Dense intensity", "\ue3ea") }, - { 56, ("Freezing Drizzle: Light intensity", "\ue798") }, - { 57, ("Freezing Drizzle: Dense intensity", "\ue798") }, - { 61, ("Rain: Slight intensity", "\uf1ad") }, - { 63, ("Rain: Moderate intensity", "\uf1ad") }, - { 65, ("Rain: Heavy intensity", "\uf1ad") }, - { 66, ("Freezing Rain: Light intensity", "\ueb3b") }, - { 67, ("Freezing Rain: Heavy intensity", "\ueb3b") }, - { 71, ("Snow fall: Slight intensity", "\uebd3") }, - { 73, ("Snow fall: Moderate intensity", "\uebd3") }, - { 75, ("Snow fall: Heavy intensity", "\uebd3") }, - { 77, ("Snow grains", "\uebd3") }, - { 80, ("Rain showers: Slight", "\uf061") }, - { 81, ("Rain showers: Moderate", "\uf061") }, - { 82, ("Rain showers: Violent", "\uebdb") }, - { 85, ("Snow showers: Slight", "\uebd3") }, - { 86, ("Snow showers: Heavy", "\uebd3") }, - { 95, ("Thunderstorm: Slight or moderate", "\uebdb") }, - { 96, ("Thunderstorm with slight hail", "\uebdb") }, - { 99, ("Thunderstorm with heavy hail", "\uebdb") } + { 0, ("Clear sky", "\ue430", "https://images.unsplash.com/photo-1601297183305-6df142704ea2?ixlib=rb-4.0.3&q=75&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=ritam-baishya-ROVBDer29PQ-unsplash.jpg") }, + { 1, ("Mainly clear", "\ue430", "https://images.unsplash.com/photo-1601297183305-6df142704ea2?ixlib=rb-4.0.3&q=75&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=ritam-baishya-ROVBDer29PQ-unsplash.jpg") }, + { 2, ("Partly cloudy", "\ue42d", "https://images.unsplash.com/photo-1601297183305-6df142704ea2?ixlib=rb-4.0.3&q=75&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=ritam-baishya-ROVBDer29PQ-unsplash.jpg") }, + { 3, ("Overcast", "\ue3dd", "https://images.unsplash.com/photo-1499956827185-0d63ee78a910?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=barry-simon-4C6Rp23RjnE-unsplash.jpg") }, + { 45, ("Fog", "\uf029", "https://images.unsplash.com/photo-1512923927402-a9867a68180e?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=chris-lawton-6tfO1M8_gas-unsplash.jpg") }, + { 48, ("Depositing rime fog", "\uf029", "https://images.unsplash.com/photo-1512923927402-a9867a68180e?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=chris-lawton-6tfO1M8_gas-unsplash.jpg") }, + { 51, ("Drizzle: Light intensity", "\ue3ea", "https://images.unsplash.com/photo-1556485689-33e55ab56127?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=roman-synkevych-qPvBmSvmohs-unsplash.jpg") }, + { 53, ("Drizzle: Moderate intensity", "\ue3ea", "https://images.unsplash.com/photo-1556485689-33e55ab56127?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=roman-synkevych-qPvBmSvmohs-unsplash.jpg") }, + { 55, ("Drizzle: Dense intensity", "\ue3ea", "https://images.unsplash.com/photo-1556485689-33e55ab56127?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=roman-synkevych-qPvBmSvmohs-unsplash.jpg") }, + { 56, ("Freezing Drizzle: Light intensity", "\ue798", "https://images.unsplash.com/photo-1505404919723-002ecad81b92?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=philippe-tarbouriech-rWwj4zcOcIs-unsplash.jpg") }, + { 57, ("Freezing Drizzle: Dense intensity", "\ue798", "https://images.unsplash.com/photo-1505404919723-002ecad81b92?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=philippe-tarbouriech-rWwj4zcOcIs-unsplash.jpg") }, + { 61, ("Rain: Slight intensity", "\uf1ad", "https://images.unsplash.com/photo-1519692933481-e162a57d6721?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=osman-rana-GXEZuWo5m4I-unsplash.jpg") }, + { 63, ("Rain: Moderate intensity", "\uf1ad", "https://images.unsplash.com/photo-1519692933481-e162a57d6721?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=osman-rana-GXEZuWo5m4I-unsplash.jpg") }, + { 65, ("Rain: Heavy intensity", "\uf1ad", "https://images.unsplash.com/photo-1519692933481-e162a57d6721?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=osman-rana-GXEZuWo5m4I-unsplash.jpg") }, + { 66, ("Freezing Rain: Light intensity", "\ueb3b", "https://images.unsplash.com/photo-1645221986876-8e3255ba006c?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=benjamin-lehman-jHpqNFSOSA0-unsplash.jpg") }, + { 67, ("Freezing Rain: Heavy intensity", "\ueb3b", "https://images.unsplash.com/photo-1645221986876-8e3255ba006c?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=benjamin-lehman-jHpqNFSOSA0-unsplash.jpg") }, + { 71, ("Snow fall: Slight intensity", "\uebd3", "https://images.unsplash.com/photo-1627854879776-c6eab3d4b7a6?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=martina-de-marchena-HJ2Ayo5yOFE-unsplash.jpg") }, + { 73, ("Snow fall: Moderate intensity", "\uebd3", "https://images.unsplash.com/photo-1627854879776-c6eab3d4b7a6?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=martina-de-marchena-HJ2Ayo5yOFE-unsplash.jpg") }, + { 75, ("Snow fall: Heavy intensity", "\uebd3", "https://images.unsplash.com/photo-1627854879776-c6eab3d4b7a6?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=martina-de-marchena-HJ2Ayo5yOFE-unsplash.jpg") }, + { 77, ("Snow grains", "\uebd3", "https://images.unsplash.com/photo-1627854879776-c6eab3d4b7a6?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=martina-de-marchena-HJ2Ayo5yOFE-unsplash.jpg") }, + { 80, ("Rain showers: Slight", "\uf061", "https://images.unsplash.com/photo-1496034663057-6245f11be793?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=liv-bruce-8yt8kBuEqok-unsplash.jpg") }, + { 81, ("Rain showers: Moderate", "\uf061", "https://images.unsplash.com/photo-1496034663057-6245f11be793?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=liv-bruce-8yt8kBuEqok-unsplash.jpg") }, + { 82, ("Rain showers: Violent", "\uebdb", "https://images.unsplash.com/photo-1496034663057-6245f11be793?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=liv-bruce-8yt8kBuEqok-unsplash.jpg") }, + { 85, ("Snow showers: Slight", "\uebd3", "https://images.unsplash.com/photo-1496034663057-6245f11be793?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=liv-bruce-8yt8kBuEqok-unsplash.jpg") }, + { 86, ("Snow showers: Heavy", "\uebd3", "https://images.unsplash.com/photo-1496034663057-6245f11be793?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=liv-bruce-8yt8kBuEqok-unsplash.jpg") }, + { 95, ("Thunderstorm: Slight or moderate", "\uebdb", "https://images.unsplash.com/photo-1605727216801-e27ce1d0cc28?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=tasos-mansour-_hGPdpyMV-8-unsplash.jpg") }, + { 96, ("Thunderstorm with slight hail", "\uebdb", "https://images.unsplash.com/photo-1605727216801-e27ce1d0cc28?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=tasos-mansour-_hGPdpyMV-8-unsplash.jpg") }, + { 99, ("Thunderstorm with heavy hail", "\uebdb", "https://images.unsplash.com/photo-1605727216801-e27ce1d0cc28?ixlib=rb-4.0.3&q=80&fm=jpg&w=1200&auto=format&fit=crop&cs=srgb&dl=tasos-mansour-_hGPdpyMV-8-unsplash.jpg") } }; } diff --git a/Samples/Nalu.Maui.Weather/MauiProgram.cs b/Samples/Nalu.Maui.Weather/MauiProgram.cs index 186e3e5..b9a0ee0 100644 --- a/Samples/Nalu.Maui.Weather/MauiProgram.cs +++ b/Samples/Nalu.Maui.Weather/MauiProgram.cs @@ -1,8 +1,11 @@ namespace Nalu.Maui.Weather; +#if DEBUG using Microsoft.Extensions.Logging; +#endif using CommunityToolkit.Maui; +using FFImageLoading.Maui; using Services; using ViewModels; @@ -23,6 +26,7 @@ public static MauiApp CreateMauiApp() .UseNaluLayouts() .UseMauiCommunityToolkit() .UseOpenMeteo() + .UseFFImageLoading() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "Regular"); diff --git a/Samples/Nalu.Maui.Weather/Models/AirQualityModel.cs b/Samples/Nalu.Maui.Weather/Models/AirQualityModel.cs deleted file mode 100644 index a720e55..0000000 --- a/Samples/Nalu.Maui.Weather/Models/AirQualityModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Nalu.Maui.Weather.Models; - -public class AirQualityModel -{ - public required DateTime Time { get; set; } - public required float? Pm25 { get; set; } - public required float? Pm10 { get; set; } - public required float? O3 { get; set; } - public required float? Co { get; set; } -} diff --git a/Samples/Nalu.Maui.Weather/Models/DailyWeatherModel.cs b/Samples/Nalu.Maui.Weather/Models/DailyWeatherModel.cs new file mode 100644 index 0000000..eed1b04 --- /dev/null +++ b/Samples/Nalu.Maui.Weather/Models/DailyWeatherModel.cs @@ -0,0 +1,18 @@ +namespace Nalu.Maui.Weather.Models; + +public class DailyWeatherModel +{ + public DateTime Time { get; set; } + public float WindSpeed { get; set; } + public float WindDirection { get; set; } + public float TemperatureMin { get; set; } + public float TemperatureMax { get; set; } + public float RainSum { get; set; } + public int WeatherCode { get; set; } + public string DayName => Time.ToString("dddd"); + public string Date => Time.ToString("M"); + public string TemperatureMinDegrees => $"{TemperatureMin:N0}°"; + public string TemperatureMaxDegrees => $"{TemperatureMax:N0}°"; + public string RainSumMm => $"{RainSum:N1}mm"; + public string WindSpeedKmh => $"{WindSpeed:N0}km/h {WindDirection}°"; +} diff --git a/Samples/Nalu.Maui.Weather/Models/HourlyAirQualityModel.cs b/Samples/Nalu.Maui.Weather/Models/HourlyAirQualityModel.cs new file mode 100644 index 0000000..16c2f86 --- /dev/null +++ b/Samples/Nalu.Maui.Weather/Models/HourlyAirQualityModel.cs @@ -0,0 +1,122 @@ +namespace Nalu.Maui.Weather.Models; + +using CommunityToolkit.Mvvm.ComponentModel; +using Resources; + +public class HourlyAirQualityModel +{ + public required DateTime Time { get; set; } + public required float? Pm25 { get; set; } + public required float? Pm10 { get; set; } + public required float? O3 { get; set; } + public required float? Co { get; set; } + + public string Icon + { + get + { + if (Pm25 is > 50 || Pm10 is > 50 || Co is > 1000) + { + return "\ue99a"; + } + + if (Pm25 is > 25 || Pm10 is > 25 || Co is > 600) + { + return "\uf083"; + } + + return "\ue1d5"; + } + } + + public Color IconColor + { + get + { + if (Pm25 is > 50 || Pm10 is > 50 || Co is > 1000) + { + return Colors.Red; + } + + if (Pm25 is > 25 || Pm10 is > 25 || Co is > 600) + { + return Colors.Orange; + } + + return Colors.Green; + } + } + + public string DangerousUnit + { + get + { + var pm25Danger = Pm25.HasValue ? Pm25.Value / 50 : 0; + var pm10Danger = Pm10.HasValue ? Pm10.Value / 50 : 0; + var o3Danger = O3.HasValue ? O3.Value / 180 : 0; + var coDanger = Co.HasValue ? Co.Value / 1000 : 0; + + var maxDanger = new[] { pm25Danger, pm10Danger, o3Danger, coDanger }.Max(); + + if (maxDanger == pm25Danger) + { + return Texts.PM25; + } + + if (maxDanger == pm10Danger) + { + return Texts.PM10; + } + + return maxDanger == o3Danger ? "O3" : "CO"; + } + } + + public string DangerousValue + { + get + { + var dangerousUnit = DangerousUnit; + if (dangerousUnit == Texts.PM25) + { + return Pm25Value; + } + + if (dangerousUnit == Texts.PM10) + { + return Pm10Value; + } + + if (dangerousUnit == "O3") + { + return O3Value; + } + + return CoValue; + } + } + + public string DangerousLevel + { + get + { + if (Pm25 is > 50 || Pm10 is > 50 || Co is > 1000) + { + return "Dangerous"; + } + + if (Pm25 is > 25 || Pm10 is > 25 || Co is > 600) + { + return "Unhealthy"; + } + + return "Good"; + } + } + + public string Hour => Time.ToString("HH:mm"); + public string Pm25Value => $"{Pm25 ?? 0:N0} μg/m³"; + public string Pm10Value => $"{Pm10 ?? 0:N0} μg/m³"; + public string O3Value => $"{O3 ?? 0:N0} μg/m³"; + public string CoValue => $"{Co ?? 0:N1} mg/m³"; +} diff --git a/Samples/Nalu.Maui.Weather/Models/WeatherModel.cs b/Samples/Nalu.Maui.Weather/Models/HourlyWeatherModel.cs similarity index 74% rename from Samples/Nalu.Maui.Weather/Models/WeatherModel.cs rename to Samples/Nalu.Maui.Weather/Models/HourlyWeatherModel.cs index 5bbf7df..c08adef 100644 --- a/Samples/Nalu.Maui.Weather/Models/WeatherModel.cs +++ b/Samples/Nalu.Maui.Weather/Models/HourlyWeatherModel.cs @@ -1,6 +1,6 @@ namespace Nalu.Maui.Weather.Models; -public class WeatherModel +public class HourlyWeatherModel { public required DateTime Time { get; set; } public required float? Temperature { get; set; } @@ -10,4 +10,6 @@ public class WeatherModel public required float? WindSpeed { get; set; } public required float? WindDirection { get; set; } public required int? WeatherCode { get; set; } + public string Hour => Time.ToString("HH:mm"); + public string TemperatureDegrees => $"{Temperature ?? 0:N0}°"; } diff --git a/Samples/Nalu.Maui.Weather/Nalu.Maui.Weather.csproj b/Samples/Nalu.Maui.Weather/Nalu.Maui.Weather.csproj index b2b3814..6763902 100644 --- a/Samples/Nalu.Maui.Weather/Nalu.Maui.Weather.csproj +++ b/Samples/Nalu.Maui.Weather/Nalu.Maui.Weather.csproj @@ -58,6 +58,7 @@ + @@ -83,6 +84,12 @@ Designer + + Designer + + + Designer + @@ -99,6 +106,14 @@ HomePage.xaml Code + + ForecastCard.xaml + Code + + + AirQualityCard.xaml + Code + diff --git a/Samples/Nalu.Maui.Weather/PageModels/InitializationPageModel.cs b/Samples/Nalu.Maui.Weather/PageModels/InitializationPageModel.cs index f2dbf98..4e5efc4 100644 --- a/Samples/Nalu.Maui.Weather/PageModels/InitializationPageModel.cs +++ b/Samples/Nalu.Maui.Weather/PageModels/InitializationPageModel.cs @@ -36,17 +36,25 @@ public async ValueTask OnAppearingAsync(StartupIntent intent) weatherState.Location = location; var time = timeProvider.GetLocalNow(); - var start = time.Date; - var end = start.AddDays(1); + var today = time.Date; + var forecastEnd = today.AddDays(14); Message = Texts.LoadingIQ; - var iq = await weatherService.GetAirQualityAsync((float)location.Latitude, (float)location.Longitude, start, end); + var hourlyAirQualityModels = await weatherService.GetHourlyAirQualityAsync( + (float)location.Latitude, (float)location.Longitude, today, today); Message = Texts.LoadingWeatherForecast; - var forecast = await weatherService.GetWeatherAsync((float)location.Latitude, (float)location.Longitude, start, end); - weatherState.WeatherData.AddRange(forecast); - weatherState.AirQualityData.AddRange(iq); + var hourlyWeatherModels = await weatherService.GetHourlyWeatherAsync( + (float)location.Latitude, (float)location.Longitude, today, today); + var dailyWeatherModels = await weatherService.GetDailyWeatherAsync( + (float)location.Latitude, (float)location.Longitude, today, forecastEnd); + + weatherState.TodayWeather = dailyWeatherModels.First(); + weatherState.TodayHourlyWeatherData.AddRange(hourlyWeatherModels); + weatherState.TodayHourlyAirQualityData.AddRange(hourlyAirQualityModels.Take(24)); + weatherState.DailyWeatherData.AddRange(dailyWeatherModels.Skip(1)); + weatherState.UpdateCurrent(); Message = string.Empty; @@ -58,7 +66,13 @@ public async ValueTask OnAppearingAsync(StartupIntent intent) } [RelayCommand] - private Task NavigateToHomePage() => navigationService.GoToAsync(Navigation.Absolute().ShellContent()); + private Task NavigateToHomePage() + { + var navigation = Navigation + .Absolute(NavigationBehavior.Immediate | NavigationBehavior.PopAllPagesOnItemChange) + .ShellContent(); + return navigationService.GoToAsync(navigation); + } private async Task GetGeoLocationAsync() { diff --git a/Samples/Nalu.Maui.Weather/Pages/HomePage.xaml b/Samples/Nalu.Maui.Weather/Pages/HomePage.xaml index c21d475..49fc518 100644 --- a/Samples/Nalu.Maui.Weather/Pages/HomePage.xaml +++ b/Samples/Nalu.Maui.Weather/Pages/HomePage.xaml @@ -7,130 +7,22 @@ xmlns:nalu="https://nalu-development.github.com/nalu/layouts" xmlns:pageModels="clr-namespace:Nalu.Maui.Weather.PageModels" xmlns:models="clr-namespace:Nalu.Maui.Weather.Models" - Shell.NavBarIsVisible="False" + xmlns:ff="clr-namespace:FFImageLoading.Maui;assembly=FFImageLoading.Maui" + xmlns:views="clr-namespace:Nalu.Maui.Weather.Views" + Title="{x:Static r:Texts.HomePage}" x:DataType="pageModels:HomePageModel" x:Class="Nalu.Maui.Weather.Pages.HomePage"> - + -