Files
LanMountainDesktop/LanMontainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs
lincube 094745122e 0.2.6
媒体播放组件,录音组件
2026-03-03 18:26:29 +08:00

526 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using LanMontainDesktop.Models;
using LanMontainDesktop.Services;
namespace LanMontainDesktop.Views.Components;
public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget
{
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromMinutes(12) };
private readonly DispatcherTimer _animationTimer = new() { Interval = TimeSpan.FromMilliseconds(48) };
private readonly AppSettingsService _settingsService = new();
private readonly LocalizationService _localizationService = new();
private IWeatherInfoService _weatherInfoService = DefaultWeatherInfoService;
private TimeZoneService? _timeZoneService;
private CancellationTokenSource? _refreshCts;
private double _currentCellSize = 48;
private double _phase;
private bool _isAttached;
private bool _isRefreshing;
private string _languageCode = "zh-CN";
private HyperOS3WeatherVisualKind _activeVisualKind = HyperOS3WeatherVisualKind.ClearDay;
private readonly TextBlock[] _hourlyTempBlocks;
private readonly TextBlock[] _hourlyTimeBlocks;
private readonly Image[] _hourlyIconBlocks;
private readonly TextBlock[] _dailyLabelBlocks;
private readonly TextBlock[] _dailyHighBlocks;
private readonly TextBlock[] _dailyLowBlocks;
private readonly Image[] _dailyIconBlocks;
public ExtendedWeatherWidget()
{
InitializeComponent();
_hourlyTempBlocks =
[
HourlyTemp0, HourlyTemp1, HourlyTemp2, HourlyTemp3, HourlyTemp4, HourlyTemp5
];
_hourlyTimeBlocks =
[
HourlyTime0, HourlyTime1, HourlyTime2, HourlyTime3, HourlyTime4, HourlyTime5
];
_hourlyIconBlocks =
[
HourlyIcon0, HourlyIcon1, HourlyIcon2, HourlyIcon3, HourlyIcon4, HourlyIcon5
];
_dailyLabelBlocks =
[
DailyLabel0, DailyLabel1, DailyLabel2, DailyLabel3, DailyLabel4
];
_dailyHighBlocks =
[
DailyHigh0, DailyHigh1, DailyHigh2, DailyHigh3, DailyHigh4
];
_dailyLowBlocks =
[
DailyLow0, DailyLow1, DailyLow2, DailyLow3, DailyLow4
];
_dailyIconBlocks =
[
DailyIcon0, DailyIcon1, DailyIcon2, DailyIcon3, DailyIcon4
];
_refreshTimer.Tick += OnRefreshTimerTick;
_animationTimer.Tick += OnAnimationTick;
AttachedToVisualTree += (_, _) =>
{
_isAttached = true;
_refreshTimer.Start();
_animationTimer.Start();
_ = RefreshWeatherAsync(false);
};
DetachedFromVisualTree += (_, _) =>
{
_isAttached = false;
_refreshTimer.Stop();
_animationTimer.Stop();
CancelRefresh();
};
SizeChanged += (_, _) => ApplyCellSize(_currentCellSize);
ApplyCellSize(_currentCellSize);
ApplyVisualTheme(_activeVisualKind);
ApplyFallback();
}
public void ApplyCellSize(double cellSize)
{
_currentCellSize = Math.Max(1, cellSize);
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Extended4x4);
var width = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 4;
var height = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 4;
var radius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 28, 54);
RootBorder.CornerRadius = new CornerRadius(radius);
BackgroundImageLayer.CornerRadius = new CornerRadius(radius);
BackgroundMotionLayer.CornerRadius = new CornerRadius(radius);
BackgroundTintLayer.CornerRadius = new CornerRadius(radius);
BackgroundLightLayer.CornerRadius = new CornerRadius(radius);
BackgroundShadeLayer.CornerRadius = new CornerRadius(radius);
ContentPaddingBorder.Padding = new Thickness(
Math.Clamp(width * metrics.HorizontalPaddingScale * 0.30, 10, 30),
Math.Clamp(height * metrics.VerticalPaddingScale * 0.30, 10, 30));
ApplyTypography(width, height);
}
public void SetTimeZoneService(TimeZoneService timeZoneService)
{
if (_timeZoneService is not null)
{
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
}
_timeZoneService = timeZoneService;
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
}
public void ClearTimeZoneService()
{
if (_timeZoneService is null)
{
return;
}
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
_timeZoneService = null;
}
public void SetWeatherInfoService(IWeatherInfoService weatherInfoService)
{
_weatherInfoService = weatherInfoService ?? DefaultWeatherInfoService;
if (_isAttached)
{
_ = RefreshWeatherAsync(false);
}
}
private void OnTimeZoneChanged(object? sender, EventArgs e)
{
if (_isAttached)
{
_ = RefreshWeatherAsync(false);
}
}
private async void OnRefreshTimerTick(object? sender, EventArgs e)
{
await RefreshWeatherAsync(false);
}
private void OnAnimationTick(object? sender, EventArgs e)
{
_phase += 0.018;
if (_phase > Math.PI * 2) _phase -= Math.PI * 2;
var sin = Math.Sin(_phase);
var cos = Math.Cos(_phase * 0.83);
BackgroundMotionLayer.RenderTransform = new TransformGroup
{
Children = new Transforms
{
new ScaleTransform(1.05 + (sin * 0.01), 1.05 + (sin * 0.01)),
new TranslateTransform(sin * 7.0, cos * 5.0)
}
};
BackgroundMotionLayer.Opacity = Math.Clamp(0.27 + (cos * 0.05), 0.10, 0.90);
BackgroundLightLayer.Opacity = Math.Clamp(0.62 + (sin * 0.06), 0.20, 0.95);
BackgroundShadeLayer.Opacity = Math.Clamp(0.80 + (cos * 0.03), 0.45, 0.95);
}
private async Task RefreshWeatherAsync(bool forceRefresh)
{
if (!_isAttached || _isRefreshing)
{
return;
}
_isRefreshing = true;
var app = _settingsService.Load();
_languageCode = _localizationService.NormalizeLanguageCode(app.LanguageCode);
var locale = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) ? "zh_cn" : "en_us";
var latitude = double.IsFinite(app.WeatherLatitude) ? Math.Clamp(app.WeatherLatitude, -90, 90) : 39.9042;
var longitude = double.IsFinite(app.WeatherLongitude) ? Math.Clamp(app.WeatherLongitude, -180, 180) : 116.4074;
var locationKey = (app.WeatherLocationKey ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(locationKey) && string.Equals(app.WeatherLocationMode, "Coordinates", StringComparison.OrdinalIgnoreCase))
{
locationKey = string.Create(CultureInfo.InvariantCulture, $"coord:{latitude:F4},{longitude:F4}");
}
if (string.IsNullOrWhiteSpace(locationKey))
{
ApplyFallback();
_isRefreshing = false;
return;
}
SetLoadingSkeleton(true);
var cts = new CancellationTokenSource();
var previous = Interlocked.Exchange(ref _refreshCts, cts);
previous?.Cancel();
previous?.Dispose();
try
{
var query = new WeatherQuery(locationKey, latitude, longitude, 7, locale, ForceRefresh: forceRefresh);
var result = await _weatherInfoService.GetWeatherAsync(query, cts.Token);
if (cts.IsCancellationRequested || !_isAttached)
{
return;
}
if (!result.Success || result.Data is null)
{
ApplyFallback();
return;
}
ApplySnapshot(result.Data, app.WeatherLocationName);
}
catch (OperationCanceledException)
{
// Ignore canceled requests.
}
catch
{
ApplyFallback();
}
finally
{
if (ReferenceEquals(_refreshCts, cts))
{
_refreshCts = null;
}
cts.Dispose();
_isRefreshing = false;
}
}
private void ApplySnapshot(WeatherSnapshot snapshot, string? fallbackLocationName)
{
var isNight = HyperOS3WeatherTheme.ResolveIsNightPreferred(
snapshot,
_timeZoneService?.CurrentTimeZone,
_timeZoneService?.GetCurrentTime() ?? DateTime.Now);
var kind = HyperOS3WeatherTheme.ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
ApplyVisualTheme(kind);
SetLoadingSkeleton(false);
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveIconAsset(kind));
CityTextBlock.Text = ResolveLocation(snapshot.LocationName, fallbackLocationName);
ConditionTextBlock.Text = ResolveWeatherText(snapshot.Current.WeatherText, kind);
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
var today = snapshot.DailyForecasts.FirstOrDefault();
RangeTextBlock.Text = $"{FormatTemperature(today?.HighTemperatureC)}/{FormatTemperature(today?.LowTemperatureC)}";
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
var localHourly = snapshot.HourlyForecasts
.Select(item => new { Source = item, Time = ConvertToConfiguredTime(item.Time) })
.OrderBy(item => item.Time)
.ToList();
for (var i = 0; i < _hourlyTempBlocks.Length; i++)
{
var target = now.AddHours(i);
var item = localHourly
.OrderBy(entry => Math.Abs((entry.Time - target).TotalMinutes))
.FirstOrDefault();
var weatherCode = item?.Source.WeatherCode ?? snapshot.Current.WeatherCode;
var hourKind = HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, IsNightHour(target));
_hourlyTempBlocks[i].Text = FormatTemperature(item?.Source.TemperatureC ?? snapshot.Current.TemperatureC);
_hourlyTimeBlocks[i].Text = i == 0 ? L("weather.hourly.now", "Now") : target.ToString("HH:mm", CultureInfo.InvariantCulture);
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveIconAsset(hourKind));
}
var todayDate = DateOnly.FromDateTime(now);
for (var i = 0; i < _dailyLabelBlocks.Length; i++)
{
var date = todayDate.AddDays(i + 1);
var daily = snapshot.DailyForecasts.FirstOrDefault(entry => entry.Date == date) ?? snapshot.DailyForecasts.FirstOrDefault();
var weatherCode = daily?.DayWeatherCode ?? daily?.NightWeatherCode ?? snapshot.Current.WeatherCode;
var dayKind = HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, false);
var dayText = ResolveWeatherText(daily?.DayWeatherText ?? daily?.NightWeatherText, dayKind);
_dailyLabelBlocks[i].Text = $"{ResolveDayLabel(date, i + 1)} · {dayText}";
_dailyHighBlocks[i].Text = FormatTemperatureValue(daily?.HighTemperatureC);
_dailyLowBlocks[i].Text = FormatTemperatureValue(daily?.LowTemperatureC);
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveIconAsset(dayKind));
}
}
private void ApplyFallback()
{
ApplyVisualTheme(HyperOS3WeatherVisualKind.CloudyDay);
SetLoadingSkeleton(false);
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
CityTextBlock.Text = L("weather.widget.location_unknown", "Unknown location");
ConditionTextBlock.Text = L("weather.widget.loading", "Loading...");
TemperatureTextBlock.Text = "--°";
RangeTextBlock.Text = "--/--";
for (var i = 0; i < _hourlyTempBlocks.Length; i++)
{
_hourlyTempBlocks[i].Text = "--°";
_hourlyTimeBlocks[i].Text = i == 0 ? L("weather.hourly.now", "Now") : $"{(i + 14):00}:00";
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
}
for (var i = 0; i < _dailyLabelBlocks.Length; i++)
{
_dailyLabelBlocks[i].Text = $"{ResolveDayLabel(DateOnly.FromDateTime(DateTime.Now).AddDays(i + 1), i + 1)} · {L("weather.widget.condition_cloudy", "Cloudy")}";
_dailyHighBlocks[i].Text = "--";
_dailyLowBlocks[i].Text = "--";
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
}
}
private void ApplyVisualTheme(HyperOS3WeatherVisualKind kind)
{
_activeVisualKind = kind;
var palette = HyperOS3WeatherTheme.ResolvePalette(kind);
RootBorder.Background = CreateGradientBrush(palette.GradientFrom, palette.GradientTo);
var background = CreateImageBrush(HyperOS3WeatherTheme.ResolveBackgroundAsset(kind));
BackgroundImageLayer.Background = background ?? CreateGradientBrush(palette.GradientFrom, palette.GradientTo);
BackgroundMotionLayer.Background = background ?? CreateGradientBrush(palette.GradientFrom, palette.GradientTo);
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint);
var isNightVisual = kind is HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.CloudyNight;
TemperatureTextBlock.Foreground = CreateSolidBrush(palette.PrimaryText);
CityTextBlock.Foreground = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xDC : (byte)0xCC);
ConditionTextBlock.Foreground = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xEE : (byte)0xE2);
RangeTextBlock.Foreground = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xDE : (byte)0xD2);
HourlyPanelBorder.Background = CreateSolidBrush(isNightVisual ? "#12FFFFFF" : "#0BFFFFFF");
SeparatorLine.Background = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0x3A : (byte)0x28);
var hourlyTempBrush = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xEA : (byte)0xDC);
var hourlyTimeBrush = CreateSolidBrush(palette.TertiaryText, isNightVisual ? (byte)0xCA : (byte)0xB6);
var dailyTextBrush = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xE8 : (byte)0xDE);
var dailyLowBrush = CreateSolidBrush(palette.TertiaryText, isNightVisual ? (byte)0xB6 : (byte)0xA0);
for (var i = 0; i < _hourlyTempBlocks.Length; i++)
{
_hourlyTempBlocks[i].Foreground = hourlyTempBrush;
_hourlyTimeBlocks[i].Foreground = hourlyTimeBrush;
}
for (var i = 0; i < _dailyLabelBlocks.Length; i++)
{
_dailyLabelBlocks[i].Foreground = dailyTextBrush;
_dailyHighBlocks[i].Foreground = dailyTextBrush;
_dailyLowBlocks[i].Foreground = dailyLowBrush;
}
}
private void ApplyTypography(double width, double height)
{
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Extended4x4);
var scale = ResolveScale(width, height);
var compactness = Math.Clamp((0.90 - scale) / 0.55, 0, 1);
LayoutRoot.RowSpacing = Math.Clamp(height * 0.014, 5, 14);
SummaryGrid.ColumnSpacing = Math.Clamp(width * 0.017, 8, 24);
HourlyGrid.ColumnSpacing = Math.Clamp(width * 0.008, 3, 10);
DailyGrid.RowSpacing = Math.Clamp(height * 0.010, 4, 11);
TemperatureTextBlock.FontSize = Math.Clamp(height * 0.19, 54, 162);
TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 380, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
CityTextBlock.FontSize = Math.Clamp(height * 0.042, 12, 32);
ConditionTextBlock.FontSize = Math.Clamp(height * 0.050, 13, 38);
RangeTextBlock.FontSize = Math.Clamp(height * 0.053, 13, 40);
CityTextBlock.FontWeight = ToVariableWeight(Lerp(520, 600, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(560, 640, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
RangeTextBlock.FontWeight = ToVariableWeight(Lerp(560, 650, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
var iconSize = Math.Clamp(height * 0.112, 36, 96);
WeatherIconImage.Width = iconSize;
WeatherIconImage.Height = iconSize;
ConditionTextBlock.MaxWidth = Math.Clamp(width * 0.23, 86, 260);
RangeTextBlock.MaxWidth = Math.Clamp(width * 0.23, 86, 260);
CityTextBlock.MaxWidth = Math.Clamp(width * 0.30, 92, 300);
HourlyPanelBorder.Padding = new Thickness(
Math.Clamp(width * metrics.HorizontalPaddingScale * 0.16, 6, 16),
Math.Clamp(height * metrics.VerticalPaddingScale * 0.16, 5, 14));
HourlyPanelBorder.CornerRadius = new CornerRadius(Math.Clamp(height * 0.042, 10, 20));
var hourlyBandHeight = Math.Clamp(height * 0.20, 74, 164);
var hourlyCellWidth = Math.Max(34, (width - HourlyPanelBorder.Padding.Left - HourlyPanelBorder.Padding.Right - (HourlyGrid.ColumnSpacing * 5)) / 6d);
var hourlyTempSize = Math.Clamp(hourlyBandHeight * 0.24, 10, 34);
var hourlyTimeSize = Math.Clamp(hourlyBandHeight * 0.18, 8, 24);
var hourlyIconSize = Math.Clamp(hourlyBandHeight * 0.20, 12, 32);
var hourlyStackSpacing = Math.Clamp(hourlyBandHeight * 0.03, 1, 4);
for (var i = 0; i < _hourlyTempBlocks.Length; i++)
{
_hourlyTempBlocks[i].FontSize = hourlyTempSize;
_hourlyTimeBlocks[i].FontSize = hourlyTimeSize;
_hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(540, 620, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
_hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(450, 530, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
_hourlyTempBlocks[i].MaxWidth = hourlyCellWidth;
_hourlyTimeBlocks[i].MaxWidth = hourlyCellWidth;
_hourlyIconBlocks[i].Width = hourlyIconSize;
_hourlyIconBlocks[i].Height = hourlyIconSize;
if (_hourlyTempBlocks[i].Parent is StackPanel stack) stack.Spacing = hourlyStackSpacing;
}
var dailyLabelSize = Math.Clamp(height * 0.043, 10, 32);
var dailyTempSize = Math.Clamp(height * 0.044, 10, 34);
var dailyIconSize = Math.Clamp(height * 0.040, 12, 30);
var dailyLabelMaxWidth = Math.Clamp(width * (compactness > 0.3 ? 0.48 : 0.56), 120, 380);
var dailyHighWidth = Math.Clamp(width * 0.11, 34, 72);
var dailyLowWidth = Math.Clamp(width * 0.10, 30, 68);
for (var i = 0; i < _dailyLabelBlocks.Length; i++)
{
_dailyLabelBlocks[i].FontSize = dailyLabelSize;
_dailyHighBlocks[i].FontSize = dailyTempSize;
_dailyLowBlocks[i].FontSize = dailyTempSize;
_dailyLabelBlocks[i].FontWeight = ToVariableWeight(Lerp(520, 600, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
_dailyHighBlocks[i].FontWeight = ToVariableWeight(Lerp(560, 640, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
_dailyLowBlocks[i].FontWeight = ToVariableWeight(Lerp(470, 560, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
_dailyLabelBlocks[i].MaxWidth = dailyLabelMaxWidth;
_dailyHighBlocks[i].Width = dailyHighWidth;
_dailyLowBlocks[i].Width = dailyLowWidth;
_dailyHighBlocks[i].HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right;
_dailyLowBlocks[i].HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right;
_dailyHighBlocks[i].TextAlignment = TextAlignment.Right;
_dailyLowBlocks[i].TextAlignment = TextAlignment.Right;
_dailyIconBlocks[i].Width = dailyIconSize;
_dailyIconBlocks[i].Height = dailyIconSize;
}
}
private static bool IsNightHour(DateTime time) => time.Hour < 6 || time.Hour >= 18;
private string ResolveDayLabel(DateOnly date, int offset)
{
if (offset == 1) return L("weather.multiday.tomorrow", "Tomorrow");
var dt = date.ToDateTime(TimeOnly.MinValue);
if (string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase))
{
return dt.ToString("ddd", CultureInfo.GetCultureInfo("zh-CN"))
.Replace("星期", "周", StringComparison.Ordinal);
}
return dt.ToString("ddd", CultureInfo.InvariantCulture);
}
private string ResolveLocation(string? rawLocation, string? fallbackLocation)
{
var input = string.IsNullOrWhiteSpace(rawLocation) ? fallbackLocation : rawLocation;
if (string.IsNullOrWhiteSpace(input))
{
return L("weather.widget.location_unknown", "Unknown location");
}
var tokens = input.Split(['|', '/', '\\', ',', '', '、'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (tokens.Length == 0) return input.Trim();
return string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) ? tokens.OrderByDescending(item => item.Length).First() : tokens.Last();
}
private string ResolveWeatherText(string? weatherText, HyperOS3WeatherVisualKind kind)
{
if (!string.IsNullOrWhiteSpace(weatherText)) return weatherText;
return kind switch
{
HyperOS3WeatherVisualKind.ClearDay or HyperOS3WeatherVisualKind.ClearNight => L("weather.widget.condition_clear", "Clear"),
HyperOS3WeatherVisualKind.CloudyDay or HyperOS3WeatherVisualKind.CloudyNight => L("weather.widget.condition_cloudy", "Cloudy"),
HyperOS3WeatherVisualKind.RainLight or HyperOS3WeatherVisualKind.RainHeavy => L("weather.widget.condition_rain", "Rain"),
HyperOS3WeatherVisualKind.Storm => L("weather.widget.condition_storm", "Thunderstorm"),
HyperOS3WeatherVisualKind.Snow => L("weather.widget.condition_snow", "Snow"),
_ => L("weather.widget.condition_fog", "Fog")
};
}
private DateTime ConvertToConfiguredTime(DateTimeOffset sourceTime)
{
try
{
return _timeZoneService is null
? sourceTime.ToLocalTime().DateTime
: TimeZoneInfo.ConvertTime(sourceTime, _timeZoneService.CurrentTimeZone).DateTime;
}
catch
{
return sourceTime.ToLocalTime().DateTime;
}
}
private static string FormatTemperature(double? value) => !value.HasValue || double.IsNaN(value.Value) || double.IsInfinity(value.Value) ? "--°" : $"{(int)Math.Round(value.Value, MidpointRounding.AwayFromZero)}°";
private static string FormatTemperatureValue(double? value) => !value.HasValue || double.IsNaN(value.Value) || double.IsInfinity(value.Value) ? "--" : $"{(int)Math.Round(value.Value, MidpointRounding.AwayFromZero)}";
private static IBrush? CreateImageBrush(string? uriText)
{
var source = HyperOS3WeatherAssetLoader.LoadImage(uriText);
if (source is not IImageBrushSource brushSource)
{
return null;
}
return new ImageBrush { Source = brushSource, Stretch = Stretch.UniformToFill, AlignmentX = AlignmentX.Center, AlignmentY = AlignmentY.Center };
}
private string L(string key, string fallback) => _localizationService.GetString(_languageCode, key, fallback);
private void CancelRefresh()
{
var cts = Interlocked.Exchange(ref _refreshCts, null);
cts?.Cancel();
cts?.Dispose();
}
private static double ResolveScale(double width, double height) => Math.Clamp(Math.Min(Math.Clamp(width / 620d, 0.42, 2.4), Math.Clamp(height / 620d, 0.42, 2.4)), 0.42, 2.4);
private static double Lerp(double from, double to, double t) => from + ((to - from) * t);
private static FontWeight ToVariableWeight(double weight) => (FontWeight)(int)Math.Clamp(Math.Round(weight), 1, 1000);
private static IBrush CreateSolidBrush(string colorHex) => new SolidColorBrush(Color.Parse(colorHex));
private static IBrush CreateSolidBrush(string colorHex, byte alpha) { var c = Color.Parse(colorHex); return new SolidColorBrush(Color.FromArgb(alpha, c.R, c.G, c.B)); }
private static IBrush CreateGradientBrush(string fromHex, string toHex) => new LinearGradientBrush { StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative), GradientStops = new GradientStops { new(Color.Parse(fromHex), 0), new(Color.Parse(toHex), 1) } };
private void SetLoadingSkeleton(bool isLoading)
{
CityInfoBadge.Background = isLoading ? CreateSolidBrush("#24FFFFFF") : Brushes.Transparent;
ConditionInfoBadge.Background = isLoading ? CreateSolidBrush("#1DFFFFFF") : Brushes.Transparent;
}
}