mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-21 16:14:28 +08:00
526 lines
25 KiB
C#
526 lines
25 KiB
C#
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;
|
||
}
|
||
}
|
||
|