From cc1c04020311b97840d5cdfe1a35ccd90c1e11ef Mon Sep 17 00:00:00 2001 From: lincube Date: Mon, 18 May 2026 19:43:15 +0800 Subject: [PATCH] =?UTF-8?q?changed.=E5=A4=A9=E6=B0=94=E9=80=89=E9=A1=B9?= =?UTF-8?q?=E5=8D=A1=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WeatherPreviewDataTests.cs | 102 ++++++++++ LanMountainDesktop/Localization/en-US.json | 13 ++ LanMountainDesktop/Localization/ja-JP.json | 13 ++ LanMountainDesktop/Localization/ko-KR.json | 13 ++ LanMountainDesktop/Localization/zh-CN.json | 13 ++ .../Models/WeatherDataModels.cs | 13 +- .../Services/XiaomiWeatherService.cs | 88 ++++++++- .../WeatherSettingsPageViewModel.cs | 165 +++++++++++++++- .../SettingsPages/WeatherSettingsPage.axaml | 180 ++++++++++++++---- 9 files changed, 553 insertions(+), 47 deletions(-) create mode 100644 LanMountainDesktop.Tests/WeatherPreviewDataTests.cs diff --git a/LanMountainDesktop.Tests/WeatherPreviewDataTests.cs b/LanMountainDesktop.Tests/WeatherPreviewDataTests.cs new file mode 100644 index 0000000..3974940 --- /dev/null +++ b/LanMountainDesktop.Tests/WeatherPreviewDataTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using LanMountainDesktop.Models; +using LanMountainDesktop.Services; +using Xunit; + +namespace LanMountainDesktop.Tests; + +public sealed class WeatherPreviewDataTests +{ + [Fact] + public void WeatherSnapshot_DefaultAlerts_IsEmpty() + { + var snapshot = new WeatherSnapshot( + Provider: "Test", + LocationKey: "test", + LocationName: "Test City", + Latitude: 0, + Longitude: 0, + FetchedAt: DateTimeOffset.UtcNow, + ObservationTime: null, + Current: new WeatherCurrentCondition(24, 25, 58, 42, 12, 180, 1, true, "Partly cloudy"), + DailyForecasts: [], + HourlyForecasts: []); + + Assert.NotNull(snapshot.Alerts); + Assert.Empty(snapshot.Alerts); + } + + [Fact] + public async Task XiaomiWeatherService_GetWeatherAsync_ParsesAlerts() + { + const string payload = """ + { + "code": 0, + "data": { + "current": { + "temperature": { "value": 24 }, + "feelsLike": { "value": 26 }, + "humidity": { "value": 58 }, + "weather": { "value": 7, "text": "Light rain" }, + "wind": { "speed": { "value": 12 }, "direction": { "value": 180 } }, + "pubTime": "2026-05-18T10:00:00+08:00" + }, + "aqi": { "value": 42 }, + "forecastDaily": { + "temperature": { "value": [{ "from": 20, "to": 28 }] }, + "weather": { "value": [{ "from": 7, "to": 7 }] }, + "sunRiseSet": { "value": [{ "from": "05:42", "to": "18:54" }] }, + "precipitationProbability": { "value": [{ "value": 60 }] } + }, + "alerts": [ + { + "title": "Heavy rain warning", + "detail": "Rain is expected within the next hour.", + "type": "Rain", + "level": "Yellow", + "pubTime": "2026-05-18T09:30:00+08:00", + "images": { "icon": "https://example.test/rain.webp" } + } + ] + } + } + """; + + using var httpClient = new HttpClient(new StubHandler(payload)); + var service = new XiaomiWeatherService( + new XiaomiWeatherApiOptions { BaseUrl = "https://example.test" }, + httpClient); + + var result = await service.GetWeatherAsync(new WeatherQuery( + "101010100", + 39.9042, + 116.4074, + ForecastDays: 3, + Locale: "en_us", + ForceRefresh: true)); + + Assert.True(result.Success, result.ErrorMessage); + var alert = Assert.Single(result.Data!.Alerts); + Assert.Equal("Heavy rain warning", alert.Title); + Assert.Equal("Rain is expected within the next hour.", alert.Detail); + Assert.Equal("Rain", alert.Type); + Assert.Equal("Yellow", alert.Level); + Assert.Equal("https://example.test/rain.webp", alert.IconUri); + Assert.NotNull(alert.PublishedAt); + } + + private sealed class StubHandler(string responseText) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseText) + }); + } + } +} diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json index 420c7f1..48f1f39 100644 --- a/LanMountainDesktop/Localization/en-US.json +++ b/LanMountainDesktop/Localization/en-US.json @@ -175,6 +175,19 @@ "settings.weather.settings_section": "Settings", "settings.weather.preview_panel_header": "Weather Preview", "settings.weather.preview_panel_desc": "Refresh and verify current weather service status.", + "settings.weather.preview_metrics_header": "Current conditions", + "settings.weather.preview_alerts_header": "Weather alerts", + "settings.weather.preview_no_alerts": "No active weather alerts.", + "settings.weather.metric_humidity": "Humidity", + "settings.weather.metric_aqi": "AQI", + "settings.weather.metric_wind": "Wind", + "settings.weather.metric_feels_like": "Feels like", + "settings.weather.metric_precipitation": "Precipitation", + "settings.weather.metric_sun": "Sunrise / sunset", + "settings.weather.alert_untitled": "Weather alert", + "settings.weather.alert_no_detail": "No details were provided.", + "settings.weather.alert_active": "Active alert", + "settings.weather.alert_published_format": "Published {0}", "settings.weather.refresh_button": "Refresh", "settings.weather.preview_updated_format": "Updated {0}", "settings.weather.preview_hint": "Use test fetch to verify your weather configuration.", diff --git a/LanMountainDesktop/Localization/ja-JP.json b/LanMountainDesktop/Localization/ja-JP.json index c0c664d..850d343 100644 --- a/LanMountainDesktop/Localization/ja-JP.json +++ b/LanMountainDesktop/Localization/ja-JP.json @@ -173,6 +173,19 @@ "settings.weather.settings_section": "設定", "settings.weather.preview_panel_header": "天気プレビュー", "settings.weather.preview_panel_desc": "現在の天気サービスの状態を更新して確認します。", + "settings.weather.preview_metrics_header": "現在の気象データ", + "settings.weather.preview_alerts_header": "気象アラート", + "settings.weather.preview_no_alerts": "有効な気象アラートはありません。", + "settings.weather.metric_humidity": "湿度", + "settings.weather.metric_aqi": "AQI", + "settings.weather.metric_wind": "風", + "settings.weather.metric_feels_like": "体感", + "settings.weather.metric_precipitation": "降水確率", + "settings.weather.metric_sun": "日の出 / 日の入", + "settings.weather.alert_untitled": "気象アラート", + "settings.weather.alert_no_detail": "詳細はありません。", + "settings.weather.alert_active": "有効なアラート", + "settings.weather.alert_published_format": "{0}に発表", "settings.weather.refresh_button": "更新", "settings.weather.preview_updated_format": "{0}に更新", "settings.weather.preview_hint": "テスト取得を使用して天気の設定を確認します。", diff --git a/LanMountainDesktop/Localization/ko-KR.json b/LanMountainDesktop/Localization/ko-KR.json index 454e59b..81d8637 100644 --- a/LanMountainDesktop/Localization/ko-KR.json +++ b/LanMountainDesktop/Localization/ko-KR.json @@ -174,6 +174,19 @@ "settings.weather.settings_section": "설정", "settings.weather.preview_panel_header": "날씨 미리보기", "settings.weather.preview_panel_desc": "현재 날씨 서비스 상태를 새로고침하고 확인합니다.", + "settings.weather.preview_metrics_header": "현재 날씨 데이터", + "settings.weather.preview_alerts_header": "기상 경보", + "settings.weather.preview_no_alerts": "활성 기상 경보가 없습니다.", + "settings.weather.metric_humidity": "습도", + "settings.weather.metric_aqi": "AQI", + "settings.weather.metric_wind": "바람", + "settings.weather.metric_feels_like": "체감", + "settings.weather.metric_precipitation": "강수 확률", + "settings.weather.metric_sun": "일출 / 일몰", + "settings.weather.alert_untitled": "기상 경보", + "settings.weather.alert_no_detail": "제공된 세부 정보가 없습니다.", + "settings.weather.alert_active": "활성 경보", + "settings.weather.alert_published_format": "{0}에 발표됨", "settings.weather.refresh_button": "새로고침", "settings.weather.preview_updated_format": "{0}에 업데이트됨", "settings.weather.preview_hint": "테스트 가져오기를 통해 날씨 구성을 빠르게 확인할 수 있습니다.", diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json index 421fbdb..118be85 100644 --- a/LanMountainDesktop/Localization/zh-CN.json +++ b/LanMountainDesktop/Localization/zh-CN.json @@ -175,6 +175,19 @@ "settings.weather.settings_section": "设置", "settings.weather.preview_panel_header": "天气预览", "settings.weather.preview_panel_desc": "刷新并验证当前天气服务状态。", + "settings.weather.preview_metrics_header": "当前天气数据", + "settings.weather.preview_alerts_header": "气象预警", + "settings.weather.preview_no_alerts": "暂无活动气象预警。", + "settings.weather.metric_humidity": "湿度", + "settings.weather.metric_aqi": "AQI", + "settings.weather.metric_wind": "风力", + "settings.weather.metric_feels_like": "体感", + "settings.weather.metric_precipitation": "降水概率", + "settings.weather.metric_sun": "日出 / 日落", + "settings.weather.alert_untitled": "气象预警", + "settings.weather.alert_no_detail": "暂无预警详情。", + "settings.weather.alert_active": "活动预警", + "settings.weather.alert_published_format": "发布于 {0}", "settings.weather.refresh_button": "刷新", "settings.weather.preview_updated_format": "更新于 {0}", "settings.weather.preview_hint": "可通过测试获取快速验证天气配置。", diff --git a/LanMountainDesktop/Models/WeatherDataModels.cs b/LanMountainDesktop/Models/WeatherDataModels.cs index 6d3afd7..bbefe00 100644 --- a/LanMountainDesktop/Models/WeatherDataModels.cs +++ b/LanMountainDesktop/Models/WeatherDataModels.cs @@ -39,6 +39,14 @@ public sealed record WeatherHourlyForecast( int? WeatherCode, string? WeatherText); +public sealed record WeatherAlert( + string Title, + string? Detail, + string? Type, + string? Level, + DateTimeOffset? PublishedAt, + string? IconUri); + public sealed record WeatherSnapshot( string Provider, string LocationKey, @@ -49,4 +57,7 @@ public sealed record WeatherSnapshot( DateTimeOffset? ObservationTime, WeatherCurrentCondition Current, IReadOnlyList DailyForecasts, - IReadOnlyList HourlyForecasts); + IReadOnlyList HourlyForecasts) +{ + public IReadOnlyList Alerts { get; init; } = Array.Empty(); +} diff --git a/LanMountainDesktop/Services/XiaomiWeatherService.cs b/LanMountainDesktop/Services/XiaomiWeatherService.cs index 0fc8df9..0b64c78 100644 --- a/LanMountainDesktop/Services/XiaomiWeatherService.cs +++ b/LanMountainDesktop/Services/XiaomiWeatherService.cs @@ -375,6 +375,10 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable var hourlyNode = TryGetNode(payload, "forecastHourly") ?? TryGetNode(payload, "hourly") ?? TryGetNode(payload, "hourlyForecast"); + var alertsNode = TryGetNode(payload, "alerts") ?? + TryGetNode(payload, "alert") ?? + TryGetNode(payload, "warning") ?? + TryGetNode(payload, "warnings"); var weatherCode = ReadWeatherCode(currentNode); @@ -422,7 +426,84 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable ObservationTime: observationTime, Current: current, DailyForecasts: forecasts, - HourlyForecasts: hourlyForecasts); + HourlyForecasts: hourlyForecasts) + { + Alerts = ParseAlerts(alertsNode) + }; + } + + private static IReadOnlyList ParseAlerts(JsonElement? alertsNode) + { + if (!alertsNode.HasValue) + { + return Array.Empty(); + } + + var array = alertsNode.Value.ValueKind == JsonValueKind.Array + ? alertsNode + : ReadArray(alertsNode.Value, "value") ?? + ReadArray(alertsNode.Value, "alerts") ?? + ReadArray(alertsNode.Value, "alert") ?? + (alertsNode.Value.ValueKind == JsonValueKind.Object ? alertsNode : null); + + if (!array.HasValue) + { + return Array.Empty(); + } + + var alerts = new List(); + if (array.Value.ValueKind == JsonValueKind.Object) + { + AddAlert(array.Value, alerts); + return alerts; + } + + if (array.Value.ValueKind != JsonValueKind.Array) + { + return Array.Empty(); + } + + foreach (var item in array.Value.EnumerateArray()) + { + if (item.ValueKind == JsonValueKind.Object) + { + AddAlert(item, alerts); + } + } + + return alerts; + } + + private static void AddAlert(JsonElement item, ICollection alerts) + { + var title = ReadString(item, "title") ?? + ReadString(item, "name") ?? + ReadString(item, "headline") ?? + ReadString(item, "text"); + var detail = ReadString(item, "detail") ?? + ReadString(item, "description") ?? + ReadString(item, "content") ?? + ReadString(item, "desc"); + + if (string.IsNullOrWhiteSpace(title) && string.IsNullOrWhiteSpace(detail)) + { + return; + } + + alerts.Add(new WeatherAlert( + string.IsNullOrWhiteSpace(title) ? Truncate(detail, 60) : title.Trim(), + string.IsNullOrWhiteSpace(detail) ? null : detail.Trim(), + NullIfWhiteSpace(ReadString(item, "type") ?? ReadString(item, "category")), + NullIfWhiteSpace(ReadString(item, "level") ?? ReadString(item, "severity")), + ParseTime(ReadString(item, "pubTime") ?? + ReadString(item, "publishTime") ?? + ReadString(item, "publishedAt") ?? + ReadString(item, "time")), + NullIfWhiteSpace(ReadString(item, "images", "icon") ?? + ReadString(item, "image", "icon") ?? + ReadString(item, "icon") ?? + ReadString(item, "iconUri") ?? + ReadString(item, "iconUrl")))); } private IReadOnlyList ParseDailyForecasts(JsonElement? dailyNode, int days, string locale) @@ -977,4 +1058,9 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable ? text : $"{text[..maxLength]}..."; } + + private static string? NullIfWhiteSpace(string? text) + { + return string.IsNullOrWhiteSpace(text) ? null : text.Trim(); + } } diff --git a/LanMountainDesktop/ViewModels/WeatherSettingsPageViewModel.cs b/LanMountainDesktop/ViewModels/WeatherSettingsPageViewModel.cs index d489f02..6169131 100644 --- a/LanMountainDesktop/ViewModels/WeatherSettingsPageViewModel.cs +++ b/LanMountainDesktop/ViewModels/WeatherSettingsPageViewModel.cs @@ -66,6 +66,10 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase public ObservableCollection SearchResults { get; } = []; + public ObservableCollection PreviewMetrics { get; } = []; + + public ObservableCollection PreviewAlerts { get; } = []; + [ObservableProperty] private string _pageTitle = string.Empty; @@ -168,6 +172,15 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase [ObservableProperty] private string _footerHint = string.Empty; + [ObservableProperty] + private string _previewMetricsHeader = string.Empty; + + [ObservableProperty] + private string _previewAlertsHeader = string.Empty; + + [ObservableProperty] + private string _previewNoAlertsText = string.Empty; + [ObservableProperty] private SelectionOption _selectedLocationMode = new("CitySearch", "City Search"); @@ -246,6 +259,9 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase [ObservableProperty] private string _previewStatus = string.Empty; + [ObservableProperty] + private bool _hasPreviewAlerts; + partial void OnSelectedLocationModeChanged(SelectionOption value) { UpdateModeVisibility(); @@ -279,6 +295,7 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase } _settingsFacade.Weather.Save(CreateEditableState()); + UpdatePreviewDetails(_previewSnapshot); } partial void OnExcludedAlertsChanged(string value) @@ -447,6 +464,7 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase PreviewTemperature = "--"; PreviewCondition = string.Empty; PreviewUpdated = string.Empty; + UpdatePreviewDetails(null); return; } @@ -466,6 +484,7 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase result.ErrorMessage ?? result.ErrorCode ?? L("settings.weather.preview_unknown", "Unknown")); _previewSnapshot = null; UpdatePreviewIcon(null); + UpdatePreviewDetails(null); return; } @@ -475,10 +494,9 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase PreviewLocation = string.IsNullOrWhiteSpace(snapshot.LocationName) ? state.LocationName : snapshot.LocationName!; - PreviewTemperature = snapshot.Current.TemperatureC.HasValue - ? string.Format(CultureInfo.InvariantCulture, "{0:0.#}°C", snapshot.Current.TemperatureC.Value) - : "--"; + PreviewTemperature = FormatTemperatureWithUnit(snapshot.Current.TemperatureC); PreviewCondition = ResolveWeatherDisplayText(snapshot.Current.WeatherText, snapshot.Current.WeatherCode); + UpdatePreviewDetails(snapshot); var updatedAt = (snapshot.ObservationTime ?? snapshot.FetchedAt).ToLocalTime(); PreviewUpdated = string.Format( @@ -546,10 +564,17 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase PreviewIcon = null; UpdatePreviewIcon(null); PreviewLocation = previewLocation.Name; - PreviewTemperature = "24 deg C"; + PreviewTemperature = "24°C"; PreviewCondition = ResolveWeatherDisplayText("Partly cloudy", 4); PreviewUpdated = "Updated 09:42"; PreviewStatus = "Preview data is mocked for Avalonia design mode."; + PreviewMetrics.Clear(); + PreviewMetrics.Add(new WeatherPreviewMetric("Humidity", "58%", "\uE794")); + PreviewMetrics.Add(new WeatherPreviewMetric("AQI", "42", "\uEA7D")); + PreviewMetrics.Add(new WeatherPreviewMetric("Wind", "12 km/h", "\uF43B")); + PreviewMetrics.Add(new WeatherPreviewMetric("Feels like", "25°C", "\uE706")); + PreviewAlerts.Clear(); + HasPreviewAlerts = false; } private void RefreshLocalizedText() @@ -558,6 +583,9 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase PageDescription = L("settings.weather.description", "Configure weather location, weather preview, and startup positioning behavior."); PreviewHeader = L("settings.weather.preview_panel_header", "Weather Preview"); PreviewDescription = L("settings.weather.preview_panel_desc", "Refresh and verify current weather service status."); + PreviewMetricsHeader = L("settings.weather.preview_metrics_header", "Current conditions"); + PreviewAlertsHeader = L("settings.weather.preview_alerts_header", "Weather alerts"); + PreviewNoAlertsText = L("settings.weather.preview_no_alerts", "No active weather alerts."); LocationSourceHeader = L("settings.weather.location_source_header", "Location Source"); LocationSourceDescription = L("settings.weather.location_source_desc", "Choose how weather widgets resolve location."); CitySearchHeader = L("settings.weather.city_search_header", "City Search"); @@ -746,6 +774,131 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase : WeatherIconAssetResolver.LoadIcon(styleId, snapshot); } + private void UpdatePreviewDetails(WeatherSnapshot? snapshot) + { + PreviewMetrics.Clear(); + PreviewAlerts.Clear(); + + if (snapshot is null) + { + HasPreviewAlerts = false; + return; + } + + AddPreviewMetric( + L("settings.weather.metric_humidity", "Humidity"), + snapshot.Current.RelativeHumidityPercent is int humidity + ? string.Format(CultureInfo.InvariantCulture, "{0}%", humidity) + : "--", + "\uE794"); + AddPreviewMetric( + L("settings.weather.metric_aqi", "AQI"), + snapshot.Current.AirQualityIndex?.ToString(CultureInfo.InvariantCulture) ?? "--", + "\uEA7D"); + AddPreviewMetric( + L("settings.weather.metric_wind", "Wind"), + snapshot.Current.WindSpeedKph.HasValue + ? string.Format(CultureInfo.InvariantCulture, "{0:0.#} km/h", snapshot.Current.WindSpeedKph.Value) + : "--", + "\uF43B"); + AddPreviewMetric( + L("settings.weather.metric_feels_like", "Feels like"), + FormatTemperatureWithUnit(snapshot.Current.FeelsLikeC), + "\uE706"); + + var today = snapshot.DailyForecasts.FirstOrDefault(); + if (today is not null) + { + AddPreviewMetric( + L("settings.weather.metric_precipitation", "Precipitation"), + today.PrecipitationProbabilityPercent is int precipitation + ? string.Format(CultureInfo.InvariantCulture, "{0}%", precipitation) + : "--", + "\uE9C4"); + + var sunValue = string.IsNullOrWhiteSpace(today.SunriseTime) && string.IsNullOrWhiteSpace(today.SunsetTime) + ? "--" + : string.Format( + CultureInfo.InvariantCulture, + "{0} / {1}", + string.IsNullOrWhiteSpace(today.SunriseTime) ? "--" : today.SunriseTime, + string.IsNullOrWhiteSpace(today.SunsetTime) ? "--" : today.SunsetTime); + AddPreviewMetric(L("settings.weather.metric_sun", "Sunrise / sunset"), sunValue, "\uE706"); + } + + var excludedRules = SplitAlertExclusions(); + foreach (var alert in snapshot.Alerts.Where(alert => !IsAlertExcluded(alert, excludedRules))) + { + PreviewAlerts.Add(new WeatherPreviewAlert( + string.IsNullOrWhiteSpace(alert.Title) ? L("settings.weather.alert_untitled", "Weather alert") : alert.Title, + string.IsNullOrWhiteSpace(alert.Detail) ? L("settings.weather.alert_no_detail", "No details were provided.") : alert.Detail!, + BuildAlertMetadata(alert), + alert.IconUri)); + } + + HasPreviewAlerts = PreviewAlerts.Count > 0; + } + + private void AddPreviewMetric(string label, string value, string glyph) + { + PreviewMetrics.Add(new WeatherPreviewMetric(label, value, glyph)); + } + + private string[] SplitAlertExclusions() + { + return (ExcludedAlerts ?? string.Empty) + .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + private static bool IsAlertExcluded(WeatherAlert alert, IReadOnlyList excludedRules) + { + if (excludedRules.Count == 0) + { + return false; + } + + var haystack = string.Join( + '\n', + alert.Title, + alert.Detail, + alert.Type, + alert.Level); + return excludedRules.Any(rule => haystack.Contains(rule, StringComparison.OrdinalIgnoreCase)); + } + + private string BuildAlertMetadata(WeatherAlert alert) + { + var parts = new List(); + if (!string.IsNullOrWhiteSpace(alert.Level)) + { + parts.Add(alert.Level!); + } + + if (!string.IsNullOrWhiteSpace(alert.Type)) + { + parts.Add(alert.Type!); + } + + if (alert.PublishedAt.HasValue) + { + parts.Add(string.Format( + ResolveCulture(), + L("settings.weather.alert_published_format", "Published {0}"), + alert.PublishedAt.Value.ToLocalTime().ToString("g", ResolveCulture()))); + } + + return parts.Count == 0 + ? L("settings.weather.alert_active", "Active alert") + : string.Join(" · ", parts); + } + + private static string FormatTemperatureWithUnit(double? value) + { + return value.HasValue + ? string.Format(CultureInfo.InvariantCulture, "{0:0.#}°C", value.Value) + : "--"; + } + private string ResolveWeatherDisplayText(string? weatherText, int? weatherCode) { if (!string.IsNullOrWhiteSpace(weatherText)) @@ -777,3 +930,7 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase : "zh_cn"; } } + +public sealed record WeatherPreviewMetric(string Label, string Value, string Glyph); + +public sealed record WeatherPreviewAlert(string Title, string Detail, string Metadata, string? IconUri); diff --git a/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml index 0644d42..c60d39f 100644 --- a/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml @@ -10,51 +10,149 @@ - - - - - - - - - - - - - - - - - + + + + + +