媒体播放组件,录音组件
This commit is contained in:
lincube
2026-03-03 18:26:29 +08:00
parent 478ed115a1
commit 094745122e
42 changed files with 4661 additions and 1093 deletions

View File

@@ -155,6 +155,16 @@ public sealed class DesktopComponentRuntimeRegistry
"component.class_schedule",
() => new ClassScheduleWidget(),
cellSize => Math.Clamp(cellSize * 0.45, 24, 44)),
new DesktopComponentRuntimeRegistration(
BuiltInComponentIds.DesktopMusicControl,
"component.music_control",
() => new MusicControlWidget(),
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
new DesktopComponentRuntimeRegistration(
BuiltInComponentIds.DesktopAudioRecorder,
"component.audio_recorder",
() => new RecordingWidget(),
cellSize => Math.Clamp(cellSize * 0.36, 16, 34)),
new DesktopComponentRuntimeRegistration(
BuiltInComponentIds.DesktopWhiteboard,
"component.whiteboard",

View File

@@ -1,19 +1,464 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="using:LanMontainDesktop.Views.Components"
mc:Ignorable="d"
d:DesignWidth="640"
d:DesignHeight="640"
x:Class="LanMontainDesktop.Views.Components.ExtendedWeatherWidget">
<Grid x:Name="ContainerGrid"
RowDefinitions="*,*"
RowSpacing="8">
<local:HourlyWeatherWidget x:Name="HourlyHost"
Grid.Row="0" />
<local:MultiDayWeatherWidget x:Name="MultiDayHost"
Grid.Row="1" />
</Grid>
<Border x:Name="RootBorder"
CornerRadius="34"
ClipToBounds="True"
Background="#6A8BB3">
<Grid>
<Border x:Name="BackgroundImageLayer"
CornerRadius="34"
ClipToBounds="True" />
<Border x:Name="BackgroundMotionLayer"
CornerRadius="34"
ClipToBounds="True"
Opacity="0.24"
RenderTransformOrigin="0.5,0.5">
<Border.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1.05"
ScaleY="1.05" />
<TranslateTransform />
</TransformGroup>
</Border.RenderTransform>
</Border>
<Border x:Name="BackgroundTintLayer"
CornerRadius="34"
ClipToBounds="True"
Opacity="0.20" />
<Border x:Name="BackgroundLightLayer"
CornerRadius="34"
ClipToBounds="True"
Opacity="0.64">
<Border.Background>
<LinearGradientBrush StartPoint="0,0"
EndPoint="1,1">
<GradientStop Color="#56FFFFFF"
Offset="0" />
<GradientStop Color="#18FFFFFF"
Offset="0.30" />
<GradientStop Color="#00000000"
Offset="0.58" />
</LinearGradientBrush>
</Border.Background>
</Border>
<Border x:Name="BackgroundShadeLayer"
CornerRadius="34"
ClipToBounds="True"
Opacity="0.80">
<Border.Background>
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<GradientStop Color="#00040A16"
Offset="0.50" />
<GradientStop Color="#2E0B1C34"
Offset="1" />
</LinearGradientBrush>
</Border.Background>
</Border>
<Canvas x:Name="ParticleLayer"
IsHitTestVisible="False"
ClipToBounds="True" />
<Border x:Name="ContentPaddingBorder"
Padding="20"
Background="Transparent">
<Grid x:Name="LayoutRoot"
RowDefinitions="Auto,Auto,Auto,*"
RowSpacing="9">
<Grid x:Name="SummaryGrid"
Grid.Row="0"
ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="14">
<TextBlock x:Name="TemperatureTextBlock"
Grid.Column="0"
Text="7°"
FontSize="112"
FontWeight="Light"
FontFeatures="tnum"
VerticalAlignment="Top"
Margin="0,-2,0,0"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<StackPanel Grid.Column="1"
VerticalAlignment="Top"
Spacing="10"
Margin="0,6,0,0">
<Border x:Name="CityInfoBadge"
Background="#2AFFFFFF"
CornerRadius="12"
Padding="12,5"
HorizontalAlignment="Left">
<TextBlock x:Name="CityTextBlock"
Text="Beijing"
FontSize="24"
FontWeight="Medium"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</Border>
<Border x:Name="ConditionInfoBadge"
Background="#22FFFFFF"
CornerRadius="12"
Padding="10,5"
HorizontalAlignment="Left">
<StackPanel Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<TextBlock x:Name="ConditionTextBlock"
Text="Fog"
FontSize="30"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<TextBlock x:Name="RangeTextBlock"
Text="11°/4°"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</StackPanel>
</Border>
</StackPanel>
<Image x:Name="WeatherIconImage"
Grid.Column="2"
Width="70"
Height="70"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0,2,0,0"
Stretch="Uniform" />
</Grid>
<Border x:Name="HourlyPanelBorder"
Grid.Row="1"
Background="#0EFFFFFF"
CornerRadius="16"
ClipToBounds="True"
Padding="8,6">
<Grid x:Name="HourlyGrid"
ColumnDefinitions="*,*,*,*,*,*"
ColumnSpacing="6">
<StackPanel Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTemp0"
Text="7°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon0"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTime0"
Text="15:00"
FontSize="22"
FontWeight="Medium"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTemp1"
Text="7°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon1"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTime1"
Text="16:00"
FontSize="22"
FontWeight="Medium"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTemp2"
Text="7°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon2"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTime2"
Text="17:00"
FontSize="22"
FontWeight="Medium"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTemp3"
Text="Sunset"
FontSize="28"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon3"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTime3"
Text="18:00"
FontSize="22"
FontWeight="Medium"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="4"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTemp4"
Text="7°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon4"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTime4"
Text="19:00"
FontSize="22"
FontWeight="Medium"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTemp5"
Text="7°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon5"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTime5"
Text="20:00"
FontSize="22"
FontWeight="Medium"
HorizontalAlignment="Center" />
</StackPanel>
</Grid>
</Border>
<Border x:Name="SeparatorLine"
Grid.Row="2"
Height="1"
Margin="0,2,0,2"
Background="#2AFFFFFF" />
<Grid x:Name="DailyGrid"
Grid.Row="3"
RowDefinitions="Auto,Auto,Auto,Auto,Auto"
RowSpacing="8">
<Grid Grid.Row="0"
ColumnDefinitions="Auto,*,Auto,Auto"
ColumnSpacing="10">
<Image x:Name="DailyIcon0"
Width="24"
Height="24"
VerticalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="DailyLabel0"
Grid.Column="1"
Text="Tomorrow · Cloudy"
FontSize="30"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
MaxLines="1"
VerticalAlignment="Center" />
<TextBlock x:Name="DailyHigh0"
Grid.Column="2"
Text="10"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
VerticalAlignment="Center" />
<TextBlock x:Name="DailyLow0"
Grid.Column="3"
Text="5"
FontSize="30"
FontWeight="Medium"
FontFeatures="tnum"
VerticalAlignment="Center" />
</Grid>
<Grid Grid.Row="1"
ColumnDefinitions="Auto,*,Auto,Auto"
ColumnSpacing="10">
<Image x:Name="DailyIcon1"
Width="24"
Height="24"
VerticalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="DailyLabel1"
Grid.Column="1"
Text="Thu · Partly Cloudy"
FontSize="30"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
MaxLines="1"
VerticalAlignment="Center" />
<TextBlock x:Name="DailyHigh1"
Grid.Column="2"
Text="13"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
VerticalAlignment="Center" />
<TextBlock x:Name="DailyLow1"
Grid.Column="3"
Text="4"
FontSize="30"
FontWeight="Medium"
FontFeatures="tnum"
VerticalAlignment="Center" />
</Grid>
<Grid Grid.Row="2"
ColumnDefinitions="Auto,*,Auto,Auto"
ColumnSpacing="10">
<Image x:Name="DailyIcon2"
Width="24"
Height="24"
VerticalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="DailyLabel2"
Grid.Column="1"
Text="Fri · Cloudy"
FontSize="30"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
MaxLines="1"
VerticalAlignment="Center" />
<TextBlock x:Name="DailyHigh2"
Grid.Column="2"
Text="12"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
VerticalAlignment="Center" />
<TextBlock x:Name="DailyLow2"
Grid.Column="3"
Text="3"
FontSize="30"
FontWeight="Medium"
FontFeatures="tnum"
VerticalAlignment="Center" />
</Grid>
<Grid Grid.Row="3"
ColumnDefinitions="Auto,*,Auto,Auto"
ColumnSpacing="10">
<Image x:Name="DailyIcon3"
Width="24"
Height="24"
VerticalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="DailyLabel3"
Grid.Column="1"
Text="Sat · Partly Cloudy"
FontSize="30"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
MaxLines="1"
VerticalAlignment="Center" />
<TextBlock x:Name="DailyHigh3"
Grid.Column="2"
Text="10"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
VerticalAlignment="Center" />
<TextBlock x:Name="DailyLow3"
Grid.Column="3"
Text="2"
FontSize="30"
FontWeight="Medium"
FontFeatures="tnum"
VerticalAlignment="Center" />
</Grid>
<Grid Grid.Row="4"
ColumnDefinitions="Auto,*,Auto,Auto"
ColumnSpacing="10">
<Image x:Name="DailyIcon4"
Width="24"
Height="24"
VerticalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="DailyLabel4"
Grid.Column="1"
Text="Sun · Cloudy"
FontSize="30"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
MaxLines="1"
VerticalAlignment="Center" />
<TextBlock x:Name="DailyHigh4"
Grid.Column="2"
Text="11"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
VerticalAlignment="Center" />
<TextBlock x:Name="DailyLow4"
Grid.Column="3"
Text="3"
FontSize="30"
FontWeight="Medium"
FontFeatures="tnum"
VerticalAlignment="Center" />
</Grid>
</Grid>
</Grid>
</Border>
</Grid>
</Border>
</UserControl>

View File

@@ -1,48 +1,525 @@
using System;
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 IWeatherInfoService? _weatherInfoService;
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);
ContainerGrid.RowSpacing = Math.Clamp(_currentCellSize * metrics.SectionGap * 0.22, 6, 18);
HourlyHost.ApplyCellSize(_currentCellSize);
MultiDayHost.ApplyCellSize(_currentCellSize);
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;
HourlyHost.SetTimeZoneService(timeZoneService);
MultiDayHost.SetTimeZoneService(timeZoneService);
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
}
public void ClearTimeZoneService()
{
HourlyHost.ClearTimeZoneService();
MultiDayHost.ClearTimeZoneService();
if (_timeZoneService is null)
{
return;
}
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
_timeZoneService = null;
}
public void SetWeatherInfoService(IWeatherInfoService weatherInfoService)
{
_weatherInfoService = weatherInfoService;
HourlyHost.SetWeatherInfoService(weatherInfoService);
MultiDayHost.SetWeatherInfoService(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;
}
}

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -9,16 +9,16 @@
x:Class="LanMontainDesktop.Views.Components.HourlyWeatherWidget">
<Border x:Name="RootBorder"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Background="#68A9EC">
<Grid>
<Border x:Name="BackgroundImageLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True" />
<Border x:Name="BackgroundMotionLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Opacity="0.24"
RenderTransformOrigin="0.5,0.5">
@@ -32,12 +32,12 @@
</Border>
<Border x:Name="BackgroundTintLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Opacity="0.20" />
<Border x:Name="BackgroundLightLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Opacity="0.66">
<Border.Background>
@@ -54,7 +54,7 @@
</Border>
<Border x:Name="BackgroundShadeLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Opacity="0.78">
<Border.Background>
@@ -73,224 +73,243 @@
ClipToBounds="True" />
<Border x:Name="ContentPaddingBorder"
Padding="18"
Padding="16"
Background="Transparent">
<Grid x:Name="LayoutRoot">
<Grid x:Name="ContentGrid"
RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<Grid x:Name="TopRowGrid"
Grid.Row="0"
ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="8">
<fi:SymbolIcon x:Name="LocationIcon"
Symbol="Location"
FontSize="20"
VerticalAlignment="Center" />
<Grid x:Name="ContentGrid"
RowDefinitions="Auto,*"
RowSpacing="6">
<Grid x:Name="TopRowGrid"
Grid.Row="0"
RowDefinitions="Auto,Auto"
ColumnDefinitions="Auto,*,Auto"
RowSpacing="4"
ColumnSpacing="10">
<TextBlock x:Name="TemperatureTextBlock"
Grid.Column="0"
Grid.RowSpan="2"
Text="24°"
FontSize="98"
FontWeight="Light"
FontFeatures="tnum"
VerticalAlignment="Top"
Margin="0,-1,0,0"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<TextBlock x:Name="CityTextBlock"
Grid.Column="1"
Text="北京"
FontSize="30"
FontWeight="SemiBold"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<fi:SymbolIcon x:Name="WeatherIconSymbol"
Grid.Column="2"
Symbol="WeatherSunny"
IconVariant="Regular"
FontSize="40"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
</Grid>
<Grid Grid.Row="1"
ColumnDefinitions="Auto,*,Auto">
<TextBlock x:Name="TemperatureTextBlock"
Grid.Column="0"
Text="26°"
FontSize="108"
FontWeight="Bold"
FontFeatures="tnum"
VerticalAlignment="Center"
Margin="0,4,0,10"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<Border x:Name="CityInfoBadge"
Grid.Column="1"
Grid.Row="0"
Background="#2AFFFFFF"
CornerRadius="11"
Padding="10,4"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<StackPanel Orientation="Horizontal"
Spacing="6"
VerticalAlignment="Center">
<fi:SymbolIcon x:Name="LocationIcon"
Symbol="Location"
FontSize="14"
IsVisible="False"
VerticalAlignment="Center" />
<TextBlock x:Name="CityTextBlock"
Text="Beijing"
FontSize="19"
FontWeight="Medium"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</StackPanel>
</Border>
<Border x:Name="ConditionInfoBadge"
Grid.Column="1"
Grid.Row="1"
Background="Transparent"
Padding="0"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<StackPanel x:Name="ConditionRangeStack"
Grid.Column="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Spacing="8"
Margin="0,0,0,10">
VerticalAlignment="Center">
<TextBlock x:Name="ConditionTextBlock"
Text=""
FontSize="30"
Text="Clear"
FontSize="21"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<TextBlock x:Name="RangeTextBlock"
Text="20° / 28°"
FontSize="36"
Text="20°/28°"
FontSize="21"
FontWeight="SemiBold"
FontFeatures="tnum"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</StackPanel>
</Grid>
</Border>
<StackPanel x:Name="BottomInfoStack"
Grid.Row="2"
VerticalAlignment="Bottom"
Spacing="4"
Margin="0,0,0,8">
<Border x:Name="HourlyPanelBorder"
Background="#1CFFFFFF"
CornerRadius="18"
ClipToBounds="True"
Padding="12,8">
<Grid x:Name="HourlyGrid"
ColumnDefinitions="*,*,*,*,*,*">
<StackPanel Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTime0"
Text="现在"
FontSize="24"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<fi:SymbolIcon x:Name="HourlyIcon0"
Symbol="WeatherSunny"
FontSize="28"
IconVariant="Regular"
HorizontalAlignment="Center" />
<TextBlock x:Name="HourlyTemp0"
Text="26°"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTime1"
Text="09:00"
FontSize="24"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<fi:SymbolIcon x:Name="HourlyIcon1"
Symbol="WeatherSunny"
FontSize="28"
IconVariant="Regular"
HorizontalAlignment="Center" />
<TextBlock x:Name="HourlyTemp1"
Text="28°"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTime2"
Text="10:00"
FontSize="24"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<fi:SymbolIcon x:Name="HourlyIcon2"
Symbol="WeatherSunny"
FontSize="28"
IconVariant="Regular"
HorizontalAlignment="Center" />
<TextBlock x:Name="HourlyTemp2"
Text="26°"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTime3"
Text="11:00"
FontSize="24"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<fi:SymbolIcon x:Name="HourlyIcon3"
Symbol="WeatherSunny"
FontSize="28"
IconVariant="Regular"
HorizontalAlignment="Center" />
<TextBlock x:Name="HourlyTemp3"
Text="24°"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="4"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTime4"
Text="12:00"
FontSize="24"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<fi:SymbolIcon x:Name="HourlyIcon4"
Symbol="WeatherSunny"
FontSize="28"
IconVariant="Regular"
HorizontalAlignment="Center" />
<TextBlock x:Name="HourlyTemp4"
Text="24°"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTime5"
Text="13:00"
FontSize="24"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<fi:SymbolIcon x:Name="HourlyIcon5"
Symbol="WeatherSunny"
FontSize="28"
IconVariant="Regular"
HorizontalAlignment="Center" />
<TextBlock x:Name="HourlyTemp5"
Text="23°"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
<Image x:Name="WeatherIconImage"
Grid.Column="2"
Grid.RowSpan="2"
Width="56"
Height="56"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Stretch="Uniform" />
</Grid>
<StackPanel x:Name="BottomInfoStack"
Grid.Row="1"
VerticalAlignment="Bottom"
Spacing="2"
Margin="0,0,0,1">
<Border x:Name="HourlyPanelBorder"
Background="#10FFFFFF"
CornerRadius="15"
ClipToBounds="True"
Padding="5,3">
<Grid x:Name="HourlyGrid"
ColumnDefinitions="*,*,*,*,*,*"
ColumnSpacing="8">
<StackPanel Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock x:Name="HourlyTemp0"
Text="24°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon0"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTime0"
Text="Now"
FontSize="22"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock x:Name="HourlyTemp1"
Text="23°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon1"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTime1"
Text="14:00"
FontSize="22"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock x:Name="HourlyTemp2"
Text="23°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon2"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTime2"
Text="15:00"
FontSize="22"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock x:Name="HourlyTemp3"
Text="21°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon3"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTime3"
Text="16:00"
FontSize="22"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="4"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock x:Name="HourlyTemp4"
Text="20°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon4"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTime4"
Text="17:00"
FontSize="22"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock x:Name="HourlyTemp5"
Text="20°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon5"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTime5"
Text="18:00"
FontSize="22"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
</Grid>
</Grid>
</Border>
</Grid>
</Border>
</UserControl>

View File

@@ -11,8 +11,6 @@ using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using FluentIcons.Avalonia;
using FluentIcons.Common;
using LanMontainDesktop.Models;
using LanMontainDesktop.Services;
@@ -79,7 +77,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private readonly record struct HourlyForecastItem(
DateTime Time,
string TimeLabel,
Symbol Icon,
HyperOS3WeatherVisualKind IconKind,
string TemperatureText);
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
@@ -114,7 +112,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private bool _isAttached;
private bool _isRefreshing;
private readonly TextBlock[] _hourlyTimeBlocks;
private readonly SymbolIcon[] _hourlyIconBlocks;
private readonly Image[] _hourlyIconBlocks;
private readonly TextBlock[] _hourlyTempBlocks;
public HourlyWeatherWidget()
@@ -215,7 +213,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
var scale = ResolveScale();
var hostWidth = Bounds.Width > 1 ? Bounds.Width : Math.Max(140, _currentCellSize * 4);
var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(78, _currentCellSize * 2);
var cornerRadius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 24, 44);
var cornerRadius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 24, 46);
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
BackgroundImageLayer.CornerRadius = new CornerRadius(cornerRadius);
@@ -224,8 +222,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
BackgroundLightLayer.CornerRadius = new CornerRadius(cornerRadius);
BackgroundShadeLayer.CornerRadius = new CornerRadius(cornerRadius);
ContentPaddingBorder.Padding = new Thickness(
Math.Clamp(Math.Min((_currentCellSize * metrics.HorizontalPaddingScale) * scale, hostWidth * 0.028), 3, 18),
Math.Clamp(Math.Min((_currentCellSize * metrics.VerticalPaddingScale) * scale, hostHeight * 0.060), 2, 14));
Math.Clamp(Math.Min((_currentCellSize * metrics.HorizontalPaddingScale) * scale, hostWidth * 0.034), 4, 22),
Math.Clamp(Math.Min((_currentCellSize * metrics.VerticalPaddingScale) * scale, hostHeight * 0.068), 3, 18));
ApplyAdaptiveTypography();
ResetParticles();
}
@@ -448,11 +446,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
CityTextBlock.Text = ResolvePreciseDisplayLocation(rawLocation, _languageCode, L("weather.widget.location_unknown", "Unknown location"));
ConditionTextBlock.Text = ResolveWeatherConditionText(snapshot.Current.WeatherText, visualKind);
WeatherIconSymbol.Symbol = ResolveWeatherSymbol(visualKind);
WeatherIconSymbol.Foreground = CreateSolidBrush(
ResolveWeatherIconAccent(
WeatherIconSymbol.Symbol,
visualKind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight));
SetMainWeatherIcon(visualKind);
SetLoadingSkeleton(false);
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
var (low, high) = ResolveTemperatureRange(snapshot);
@@ -465,13 +460,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
{
var fallbackKind = ResolveFallbackVisualKind();
ApplyVisualTheme(fallbackKind);
WeatherIconSymbol.Symbol = fallbackKind == WeatherVisualKind.ClearNight
? Symbol.WeatherMoon
: Symbol.WeatherSunny;
WeatherIconSymbol.Foreground = CreateSolidBrush(
ResolveWeatherIconAccent(
WeatherIconSymbol.Symbol,
fallbackKind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight));
SetMainWeatherIcon(fallbackKind);
SetLoadingSkeleton(false);
CityTextBlock.Text = L("weather.widget.location_not_configured", "Weather location is not configured");
ConditionTextBlock.Text = L("weather.widget.configure_hint", "Open Settings > Weather to configure");
TemperatureTextBlock.Text = "--°";
@@ -485,13 +475,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
{
var loadingKind = IsNightNow() ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay;
ApplyVisualTheme(loadingKind);
WeatherIconSymbol.Symbol = loadingKind == WeatherVisualKind.CloudyNight
? Symbol.WeatherPartlyCloudyNight
: Symbol.WeatherPartlyCloudyDay;
WeatherIconSymbol.Foreground = CreateSolidBrush(
ResolveWeatherIconAccent(
WeatherIconSymbol.Symbol,
loadingKind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight));
SetMainWeatherIcon(loadingKind);
SetLoadingSkeleton(true);
CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName,
_languageCode,
@@ -506,8 +491,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private void ApplyFailedState(string locationName)
{
ApplyVisualTheme(WeatherVisualKind.Fog);
WeatherIconSymbol.Symbol = Symbol.WeatherFog;
WeatherIconSymbol.Foreground = CreateSolidBrush(ResolveWeatherIconAccent(WeatherIconSymbol.Symbol, false));
SetMainWeatherIcon(WeatherVisualKind.Fog);
SetLoadingSkeleton(false);
CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName,
_languageCode,
@@ -532,22 +517,21 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
var primary = CreateSolidBrush(palette.PrimaryText);
var particleBrush = ResolveParticleBrush(ToThemeKind(kind), palette.ParticleColor);
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
var conditionSecondary = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xF0 : (byte)0xE6);
var rangeSecondary = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xE8 : (byte)0xD6);
var forecastTimeBrush = CreateSolidBrush(palette.TertiaryText, isNightVisual ? (byte)0xDA : (byte)0xC6);
var forecastTempBrush = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xF4 : (byte)0xEA);
HourlyPanelBorder.Background = CreateSolidBrush(isNightVisual ? "#1BFFFFFF" : "#1EFFFFFF");
var cityBrush = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xDC : (byte)0xCC);
var conditionSecondary = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xEE : (byte)0xE2);
var rangeSecondary = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xE6 : (byte)0xD9);
var forecastTimeBrush = CreateSolidBrush(palette.TertiaryText, isNightVisual ? (byte)0xCA : (byte)0xB6);
var forecastTempBrush = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xEA : (byte)0xDC);
HourlyPanelBorder.Background = CreateSolidBrush(isNightVisual ? "#12FFFFFF" : "#0CFFFFFF");
LocationIcon.Foreground = primary;
CityTextBlock.Foreground = primary;
CityTextBlock.Foreground = cityBrush;
TemperatureTextBlock.Foreground = primary;
WeatherIconSymbol.Foreground = CreateSolidBrush(ResolveWeatherIconAccent(WeatherIconSymbol.Symbol, isNightVisual));
ConditionTextBlock.Foreground = conditionSecondary;
RangeTextBlock.Foreground = rangeSecondary;
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
{
_hourlyTimeBlocks[i].Foreground = forecastTimeBrush;
_hourlyTempBlocks[i].Foreground = forecastTempBrush;
_hourlyIconBlocks[i].Foreground = CreateSolidBrush(ResolveWeatherIconAccent(_hourlyIconBlocks[i].Symbol, isNightVisual));
}
foreach (var particle in _particleVisuals)
@@ -660,11 +644,6 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
palette.ParticleColor);
}
private static Symbol ResolveWeatherSymbol(WeatherVisualKind kind)
{
return HyperOS3WeatherTheme.ResolveWeatherSymbol(ToThemeKind(kind));
}
private static HyperOS3WeatherVisualKind ToThemeKind(WeatherVisualKind kind)
{
return kind switch
@@ -720,14 +699,14 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
{
if (!low.HasValue && !high.HasValue)
{
return L("weather.widget.range_unknown", "-- / --");
return L("weather.widget.range_unknown", "--/--");
}
var lowText = FormatTemperature(low);
var highText = FormatTemperature(high);
return string.Format(
GetUiCulture(),
L("weather.widget.range_format", "{0} / {1}"),
L("weather.widget.range_format", "{0}/{1}"),
lowText,
highText);
}
@@ -769,7 +748,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
var candidate = TryFindNearestHourlyCandidate(hourlyCandidates, targetTime);
var weatherCode = candidate?.Hourly.WeatherCode ??
ResolveFallbackWeatherCode(targetTime, snapshot, fallbackDaily);
var icon = ResolveWeatherSymbol(ResolveVisualKind(weatherCode, IsNightHour(targetTime)));
var iconKind = ToThemeKind(ResolveVisualKind(weatherCode, IsNightHour(targetTime)));
var estimatedTemp = candidate?.Hourly.TemperatureC ??
EstimateHourlyTemperature(
@@ -782,7 +761,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
items.Add(new HourlyForecastItem(
targetTime,
displayLabel,
icon,
iconKind,
FormatTemperature(estimatedTemp)));
}
@@ -794,7 +773,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
const int itemCount = 6;
var items = new List<HourlyForecastItem>(itemCount);
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
var symbol = ResolveWeatherSymbol(visualKind);
var iconKind = ToThemeKind(visualKind);
for (var i = 0; i < itemCount; i++)
{
var targetTime = now.AddHours(i);
@@ -803,7 +782,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
i == 0
? L("weather.hourly.now", "Now")
: targetTime.ToString("HH:mm", CultureInfo.InvariantCulture),
symbol,
iconKind,
"--°"));
}
@@ -812,21 +791,22 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private void ApplyHourlyForecastItems(IReadOnlyList<HourlyForecastItem> items)
{
var isNightVisual = _activeVisualKind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
var fallbackIcon = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(_activeVisualKind)));
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
{
if (i >= items.Count)
{
_hourlyTimeBlocks[i].Text = "--";
_hourlyTempBlocks[i].Text = "--°";
_hourlyIconBlocks[i].Symbol = ResolveWeatherSymbol(_activeVisualKind);
_hourlyIconBlocks[i].Source = fallbackIcon;
continue;
}
var item = items[i];
_hourlyTimeBlocks[i].Text = item.TimeLabel;
_hourlyIconBlocks[i].Symbol = item.Icon;
_hourlyIconBlocks[i].Foreground = CreateSolidBrush(ResolveWeatherIconAccent(item.Icon, isNightVisual));
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(item.IconKind));
_hourlyTempBlocks[i].Text = item.TemperatureText;
}
}
@@ -951,12 +931,6 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
return estimated;
}
private static string ResolveWeatherIconAccent(Symbol symbol, bool isNightVisual)
{
var kind = isNightVisual ? HyperOS3WeatherVisualKind.ClearNight : HyperOS3WeatherVisualKind.ClearDay;
return HyperOS3WeatherTheme.ResolveIconAccent(kind, symbol);
}
private static string ResolvePreciseDisplayLocation(string? rawName, string languageCode, string fallback)
{
if (string.IsNullOrWhiteSpace(rawName))
@@ -1103,93 +1077,78 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private void ApplyAdaptiveTypography()
{
var (layoutWidth, layoutHeight) = ResolveLayoutViewport();
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Hourly4x2);
var scale = ResolveScale(layoutWidth, layoutHeight);
var densityBoost = scale <= 0.55 ? 0.80 : scale <= 0.72 ? 0.88 : scale <= 0.92 ? 0.95 : scale >= 1.45 ? 1.06 : 1.0;
var compactness = Math.Clamp((0.88 - scale) / 0.50, 0, 1);
var cityLength = Math.Max(1, CityTextBlock.Text?.Length ?? 2);
var cityCompression = cityLength >= 12 ? 0.68 : cityLength >= 9 ? 0.80 : cityLength >= 6 ? 0.90 : 1.0;
var conditionLength = Math.Max(1, ConditionTextBlock.Text?.Length ?? 2);
var conditionCompression = conditionLength >= 12 ? 0.72 : conditionLength >= 8 ? 0.85 : conditionLength >= 6 ? 0.92 : 1.0;
var scaleX = Math.Clamp(layoutWidth / 608d, 0.58, 1.90);
var scaleY = Math.Clamp(layoutHeight / 288d, 0.58, 1.90);
var uiScale = Math.Clamp(Math.Min(scaleX, scaleY), 0.58, 1.75);
var innerWidth = Math.Max(120, layoutWidth);
var innerHeight = Math.Max(72, layoutHeight);
ContentGrid.RowSpacing = Math.Clamp(Math.Max(metrics.MainGap * scale, layoutHeight * Lerp(0.030, 0.018, compactness)), 2, 14);
TopRowGrid.ColumnSpacing = Math.Clamp(Math.Max(metrics.MainGap * scale, layoutWidth * 0.014), 3, 14);
BottomInfoStack.Spacing = Math.Clamp(Math.Max(metrics.SectionGap * scale, layoutHeight * 0.016), 2, 10);
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(layoutHeight * 0.018, 0, 10));
ConditionRangeStack.Spacing = Math.Clamp(layoutWidth * 0.010, 3, 12);
ConditionRangeStack.Margin = new Thickness(0, 0, 0, Math.Clamp(layoutHeight * 0.018, 0, 10));
ContentGrid.RowSpacing = Math.Clamp(7 * scaleY, 2, 12);
TopRowGrid.ColumnSpacing = Math.Clamp(10 * scaleX, 6, 16);
TopRowGrid.RowSpacing = Math.Clamp(5 * scaleY, 2, 9);
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(2 * scaleY, 0, 5));
BottomInfoStack.Spacing = Math.Clamp(2 * scaleY, 1, 5);
var summaryHeight = Math.Clamp(116 * scaleY, 82, 164);
var bodyHeight = Math.Max(52, innerHeight - summaryHeight - ContentGrid.RowSpacing);
TemperatureTextBlock.FontSize = Math.Clamp(94 * uiScale, 56, 126);
TemperatureTextBlock.FontWeight = ToVariableWeight(320);
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-2 * uiScale, -5, 0), 0, 0);
TemperatureTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.22, 84, 168);
CityInfoBadge.Padding = new Thickness(
Math.Clamp(10 * uiScale, 6, 14),
Math.Clamp(4 * uiScale, 2, 8));
CityInfoBadge.CornerRadius = new CornerRadius(Math.Clamp(11 * uiScale, 8, 16));
LocationIcon.FontSize = Math.Clamp(14 * uiScale, 10, 20);
CityTextBlock.FontSize = Math.Clamp(21 * uiScale, 13, 31);
CityTextBlock.FontWeight = ToVariableWeight(560);
CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.25, 80, 220);
ConditionInfoBadge.Padding = new Thickness(0);
ConditionInfoBadge.CornerRadius = new CornerRadius(Math.Clamp(8 * uiScale, 4, 12));
ConditionRangeStack.Spacing = Math.Clamp(12 * uiScale, 6, 18);
ConditionTextBlock.FontSize = Math.Clamp(34 * uiScale, 16, 46);
RangeTextBlock.FontSize = Math.Clamp(34 * uiScale, 16, 46);
ConditionTextBlock.FontWeight = ToVariableWeight(610);
RangeTextBlock.FontWeight = ToVariableWeight(620);
ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.16, 46, 170);
RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.20, 60, 200);
var iconSize = Math.Clamp(68 * uiScale, 40, 90);
WeatherIconImage.Width = iconSize;
WeatherIconImage.Height = iconSize;
HourlyPanelBorder.Padding = new Thickness(
Math.Clamp(layoutWidth * 0.018, 4, 16),
Math.Clamp(layoutHeight * 0.020, 3, 12));
HourlyPanelBorder.CornerRadius = new CornerRadius(Math.Clamp(Math.Min(layoutWidth, layoutHeight) * 0.065, 8, 22));
HourlyGrid.ColumnSpacing = Math.Clamp(layoutWidth * 0.010, 1.5, 12);
var topBandHeight = Math.Max(18, layoutHeight * 0.22);
var middleBandHeight = Math.Max(24, layoutHeight * 0.30);
var bottomBandHeight = Math.Max(22, layoutHeight - topBandHeight - middleBandHeight - (ContentGrid.RowSpacing * 2));
LocationIcon.FontSize = Math.Min(Math.Clamp((metrics.IconFont * 0.6) * scale * densityBoost, 9, 30), topBandHeight * 0.58);
CityTextBlock.FontSize = Math.Min(Math.Clamp((metrics.PrimaryTextFont * 1.42) * scale * cityCompression * densityBoost, 12, 46), topBandHeight * 0.76);
WeatherIconSymbol.FontSize = Math.Min(Math.Clamp((metrics.IconFont * 1.02) * scale * densityBoost, 12, 56), topBandHeight * 0.95);
TemperatureTextBlock.FontSize = Math.Min(Math.Clamp((metrics.PrimaryTemperatureFont * 1.40) * scale * densityBoost, 26, 138), middleBandHeight * 0.92);
ConditionTextBlock.FontSize = Math.Min(Math.Clamp((metrics.PrimaryTextFont * 1.14) * scale * conditionCompression * densityBoost, 9, 40), middleBandHeight * 0.42);
RangeTextBlock.FontSize = Math.Min(Math.Clamp((metrics.SecondaryTextFont * 1.54) * scale * densityBoost, 10, 46), middleBandHeight * 0.50);
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(layoutHeight * 0.008, 0, 6), 0, Math.Clamp(layoutHeight * 0.012, 0, 8));
var weightProgress = Math.Clamp((scale - 0.34) / 1.18, 0, 1);
CityTextBlock.FontWeight = ToVariableWeight(Lerp(540, 680, weightProgress));
TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(600, 760, weightProgress));
ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(490, 620, weightProgress));
RangeTextBlock.FontWeight = ToVariableWeight(Lerp(490, 610, weightProgress));
var topRightMaxWidth = Math.Clamp(layoutWidth * Lerp(0.42, 0.34, compactness), 128, 280);
ConditionRangeStack.MaxWidth = topRightMaxWidth;
ConditionTextBlock.MaxWidth = Math.Max(44, topRightMaxWidth * Lerp(0.45, 0.40, compactness));
RangeTextBlock.MaxWidth = Math.Max(62, topRightMaxWidth * Lerp(0.55, 0.60, compactness));
var leftTopBudget = Math.Max(140, layoutWidth - topRightMaxWidth - Math.Clamp(64 * scale, 26, 92));
TemperatureTextBlock.MaxWidth = leftTopBudget;
CityTextBlock.MaxWidth = Math.Max(110, layoutWidth - Math.Clamp(86 * scale, 28, 120));
Math.Clamp(5 * scaleX, 3, 10),
Math.Clamp(3 * scaleY, 1, 7));
HourlyPanelBorder.CornerRadius = new CornerRadius(Math.Clamp(14 * uiScale, 8, 20));
HourlyGrid.ColumnSpacing = Math.Clamp(9 * scaleX, 4, 14);
var hourlyColumnCount = Math.Max(1, _hourlyTimeBlocks.Length);
var hourlyInnerWidth = Math.Max(
80,
layoutWidth - HourlyPanelBorder.Padding.Left - HourlyPanelBorder.Padding.Right - (HourlyGrid.ColumnSpacing * (hourlyColumnCount - 1)));
var hourlyCellWidth = Math.Max(32, hourlyInnerWidth / hourlyColumnCount);
var hourlyStackSpacing = Math.Clamp(bottomBandHeight * 0.065, 1, 5);
var hourlyInnerHeight = Math.Max(
20,
bottomBandHeight - HourlyPanelBorder.Padding.Top - HourlyPanelBorder.Padding.Bottom);
var hourlyLineHeight = Math.Max(6, (hourlyInnerHeight - (hourlyStackSpacing * 2)) / 3d);
var hourlyTimeMaxByWidth = Math.Clamp(hourlyCellWidth / Lerp(4.4, 3.8, 1 - compactness), 7, 24);
var hourlyTempMaxByWidth = Math.Clamp(hourlyCellWidth / Lerp(5.0, 4.4, 1 - compactness), 7, 28);
var hourlyIconMaxByWidth = Math.Clamp(hourlyCellWidth * Lerp(0.32, 0.38, 1 - compactness), 7, 30);
var hourlyTimeMaxByHeight = Math.Clamp(hourlyLineHeight * 0.95, 7, 24);
var hourlyTempMaxByHeight = Math.Clamp(hourlyLineHeight * 0.95, 7, 28);
var hourlyIconMaxByHeight = Math.Clamp(hourlyLineHeight * 1.05, 8, 30);
var hourlyTimeSize = Math.Min(
Math.Clamp((metrics.CaptionFont * 1.20) * scale * densityBoost, 8, 30),
Math.Min(hourlyTimeMaxByWidth, hourlyTimeMaxByHeight));
var hourlyIconSize = Math.Min(
Math.Clamp((metrics.IconFont * 0.64) * scale * densityBoost, 8, 34),
Math.Min(hourlyIconMaxByWidth, hourlyIconMaxByHeight));
var hourlyTempSize = Math.Min(
Math.Clamp((metrics.SecondaryTextFont * 1.34) * scale * densityBoost, 8, 34),
Math.Min(hourlyTempMaxByWidth, hourlyTempMaxByHeight));
96,
innerWidth - HourlyPanelBorder.Padding.Left - HourlyPanelBorder.Padding.Right - (HourlyGrid.ColumnSpacing * (hourlyColumnCount - 1)));
var hourlyCellWidth = Math.Max(34, hourlyInnerWidth / hourlyColumnCount);
var stackSpacing = Math.Clamp(2 * scaleY, 1, 4);
var hourlyTempSize = Math.Clamp(bodyHeight * 0.24, 14, 30);
var hourlyTimeSize = Math.Clamp(bodyHeight * 0.20, 10, 24);
var hourlyIconSize = Math.Clamp(bodyHeight * 0.28, 14, 34);
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
{
_hourlyTimeBlocks[i].FontSize = hourlyTimeSize;
_hourlyTempBlocks[i].FontSize = hourlyTempSize;
_hourlyIconBlocks[i].FontSize = hourlyIconSize;
_hourlyTimeBlocks[i].MaxWidth = hourlyCellWidth;
_hourlyTempBlocks[i].MaxWidth = hourlyCellWidth;
_hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(480, 620, weightProgress));
_hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(500, 650, weightProgress));
_hourlyTimeBlocks[i].FontSize = hourlyTimeSize;
_hourlyIconBlocks[i].Width = hourlyIconSize;
_hourlyIconBlocks[i].Height = hourlyIconSize;
_hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 36, 128);
_hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 36, 128);
_hourlyTimeBlocks[i].FontWeight = ToVariableWeight(500);
_hourlyTempBlocks[i].FontWeight = ToVariableWeight(590);
if (_hourlyTimeBlocks[i].Parent is StackPanel hourlyStack)
{
hourlyStack.Spacing = hourlyStackSpacing;
hourlyStack.Spacing = stackSpacing;
}
}
}
@@ -1199,6 +1158,18 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
return from + ((to - from) * t);
}
private void SetMainWeatherIcon(WeatherVisualKind kind)
{
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(kind)));
}
private void SetLoadingSkeleton(bool isLoading)
{
CityInfoBadge.Background = isLoading ? CreateSolidBrush("#24FFFFFF") : Brushes.Transparent;
ConditionInfoBadge.Background = isLoading ? CreateSolidBrush("#1CFFFFFF") : Brushes.Transparent;
}
private static FontWeight ToVariableWeight(double weight)
{
return (FontWeight)(int)Math.Clamp(Math.Round(weight), 1, 1000);

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Concurrent;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace LanMontainDesktop.Views.Components;
internal static class HyperOS3WeatherAssetLoader
{
private static readonly ConcurrentDictionary<string, IImage?> ImageCache = new(StringComparer.OrdinalIgnoreCase);
public static IImage? LoadImage(string? uriText)
{
if (string.IsNullOrWhiteSpace(uriText))
{
return null;
}
return ImageCache.GetOrAdd(uriText, static key =>
{
try
{
var uri = new Uri(key, UriKind.Absolute);
using var stream = AssetLoader.Open(uri);
return new Bitmap(stream);
}
catch
{
return null;
}
});
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using FluentIcons.Common;
using LanMontainDesktop.Models;
namespace LanMontainDesktop.Views.Components;
@@ -72,13 +71,13 @@ public readonly record struct HyperOS3WeatherMetrics(
public static class HyperOS3WeatherTheme
{
private static readonly HyperOS3WeatherPalette FallbackPalette = new(
GradientFrom: "#7187A8",
GradientTo: "#92A5C2",
Tint: "#3C4E66",
GradientFrom: "#5C7696",
GradientTo: "#90A6C1",
Tint: "#4E6682",
PrimaryText: "#FFFFFFFF",
SecondaryText: "#E4ECF7",
TertiaryText: "#C9D4E4",
ParticleColor: "#66EAF2FF");
SecondaryText: "#DCE6F1",
TertiaryText: "#B8C7D9",
ParticleColor: "#70D3E2F4");
private static readonly HyperOS3WeatherMotion FallbackMotion = new(
DriftX: 8.0, DriftY: 6.0, ZoomBase: 1.050, ZoomAmplitude: 0.010,
@@ -103,81 +102,95 @@ public static class HyperOS3WeatherTheme
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png"
};
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> IconAssets =
new Dictionary<HyperOS3WeatherVisualKind, string>
{
[HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/Icons/icon_sunny_day.webp",
[HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/Icons/icon_moon_clear.webp",
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/Icons/icon_partly_cloudy_day.webp",
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/Icons/icon_partly_cloudy_night.webp",
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/Icons/icon_rain_light.webp",
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/Icons/icon_rain_heavy.webp",
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/Icons/icon_thunder.webp",
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/Icons/icon_snow.webp",
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/Icons/icon_haze.webp"
};
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherPalette> Palettes =
new Dictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherPalette>
{
[HyperOS3WeatherVisualKind.ClearDay] = new(
GradientFrom: "#2D87DA",
GradientTo: "#79BAF2",
Tint: "#2E6CB5",
PrimaryText: "#F7FCFF",
SecondaryText: "#E8F1FD",
TertiaryText: "#D6E5F8",
GradientFrom: "#4D7097",
GradientTo: "#89A4C3",
Tint: "#4E6D8E",
PrimaryText: "#F8FCFF",
SecondaryText: "#DDE8F4",
TertiaryText: "#BACADB",
ParticleColor: "#00FFFFFF"),
[HyperOS3WeatherVisualKind.ClearNight] = new(
GradientFrom: "#5A6B85",
GradientTo: "#9DADC2",
Tint: "#495B78",
GradientFrom: "#576B86",
GradientTo: "#889CB6",
Tint: "#495F79",
PrimaryText: "#F9FBFF",
SecondaryText: "#E2EAF6",
TertiaryText: "#C6D2E3",
SecondaryText: "#D9E4F0",
TertiaryText: "#B4C3D6",
ParticleColor: "#00FFFFFF"),
[HyperOS3WeatherVisualKind.CloudyDay] = new(
GradientFrom: "#5F88B6",
GradientTo: "#8FB0D1",
Tint: "#496F98",
GradientFrom: "#607896",
GradientTo: "#94A9C1",
Tint: "#526C88",
PrimaryText: "#F8FCFF",
SecondaryText: "#E4EDF8",
TertiaryText: "#CBD9EA",
SecondaryText: "#DCE7F3",
TertiaryText: "#B9C8D9",
ParticleColor: "#26FFFFFF"),
[HyperOS3WeatherVisualKind.CloudyNight] = new(
GradientFrom: "#556A85",
GradientTo: "#95A5BC",
Tint: "#43566E",
GradientFrom: "#51637A",
GradientTo: "#8398AF",
Tint: "#45586D",
PrimaryText: "#F6FAFF",
SecondaryText: "#DEE7F4",
TertiaryText: "#C1CDDE",
SecondaryText: "#D4E0ED",
TertiaryText: "#B0BFD2",
ParticleColor: "#30F0F5FF"),
[HyperOS3WeatherVisualKind.RainLight] = new(
GradientFrom: "#5A7DA7",
GradientTo: "#8FAAC8",
Tint: "#3F5F84",
GradientFrom: "#4F6786",
GradientTo: "#7A92AF",
Tint: "#425C7A",
PrimaryText: "#F8FBFF",
SecondaryText: "#E3EAF5",
TertiaryText: "#C4D0E0",
ParticleColor: "#88D7E8FF"),
SecondaryText: "#D7E2EE",
TertiaryText: "#AEBED0",
ParticleColor: "#86CCDEFF"),
[HyperOS3WeatherVisualKind.RainHeavy] = new(
GradientFrom: "#4C678A",
GradientTo: "#7D95AF",
Tint: "#354C69",
GradientFrom: "#435770",
GradientTo: "#667F98",
Tint: "#364961",
PrimaryText: "#F9FCFF",
SecondaryText: "#E0E8F4",
TertiaryText: "#C0CBDA",
ParticleColor: "#A2CDE1FF"),
SecondaryText: "#D3DEEB",
TertiaryText: "#A9B8CB",
ParticleColor: "#9FC4D8FF"),
[HyperOS3WeatherVisualKind.Storm] = new(
GradientFrom: "#435D7B",
GradientTo: "#6F869F",
Tint: "#2B3D53",
GradientFrom: "#3A4D63",
GradientTo: "#5C7288",
Tint: "#2F4055",
PrimaryText: "#F9FCFF",
SecondaryText: "#DBE5F2",
TertiaryText: "#B9C5D7",
ParticleColor: "#A8C2D6F2"),
SecondaryText: "#CEDAE8",
TertiaryText: "#A6B6C8",
ParticleColor: "#9EB8CCF2"),
[HyperOS3WeatherVisualKind.Snow] = new(
GradientFrom: "#9FB7D0",
GradientTo: "#B7CAE0",
Tint: "#6D839D",
GradientFrom: "#8A9FBA",
GradientTo: "#AEC1D6",
Tint: "#6E829A",
PrimaryText: "#F8FBFF",
SecondaryText: "#E5EDF7",
TertiaryText: "#CDD9E7",
SecondaryText: "#D9E4EF",
TertiaryText: "#B5C4D6",
ParticleColor: "#CCFFFFFF"),
[HyperOS3WeatherVisualKind.Fog] = new(
GradientFrom: "#687E9A",
GradientTo: "#9AACBE",
Tint: "#4B6078",
GradientFrom: "#657B97",
GradientTo: "#90A5BC",
Tint: "#4F637B",
PrimaryText: "#F8FBFF",
SecondaryText: "#E3EAF4",
TertiaryText: "#C4D0DF",
ParticleColor: "#88E4EDF7")
SecondaryText: "#D8E3EE",
TertiaryText: "#AFBED0",
ParticleColor: "#88D9E5F1")
};
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherMotion> Motions =
@@ -260,11 +273,11 @@ public static class HyperOS3WeatherTheme
private static readonly IReadOnlyDictionary<HyperOS3WeatherWidgetKind, HyperOS3WeatherMetrics> Metrics =
new Dictionary<HyperOS3WeatherWidgetKind, HyperOS3WeatherMetrics>
{
[HyperOS3WeatherWidgetKind.Realtime2x2] = new(0.45, 0.38, 0.38, 108, 30, 30, 24, 40, 8, 4),
[HyperOS3WeatherWidgetKind.Hourly4x2] = new(0.45, 0.32, 0.30, 96, 28, 24, 20, 30, 8, 4),
[HyperOS3WeatherWidgetKind.MultiDay4x2] = new(0.45, 0.32, 0.30, 96, 28, 24, 20, 30, 8, 4),
[HyperOS3WeatherWidgetKind.Realtime2x2] = new(0.47, 0.32, 0.30, 112, 28, 24, 20, 36, 8, 5),
[HyperOS3WeatherWidgetKind.Hourly4x2] = new(0.47, 0.24, 0.22, 96, 24, 20, 16, 26, 7, 4),
[HyperOS3WeatherWidgetKind.MultiDay4x2] = new(0.47, 0.24, 0.22, 96, 24, 20, 16, 26, 7, 4),
[HyperOS3WeatherWidgetKind.WeatherClock2x1] = new(0.40, 0.18, 0.14, 42, 18, 15, 12, 18, 4, 3),
[HyperOS3WeatherWidgetKind.Extended4x4] = new(0.45, 0.28, 0.28, 88, 24, 20, 18, 24, 8, 6)
[HyperOS3WeatherWidgetKind.Extended4x4] = new(0.47, 0.24, 0.22, 112, 26, 22, 18, 28, 9, 6)
};
public static HyperOS3WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
@@ -304,6 +317,11 @@ public static class HyperOS3WeatherTheme
return BackgroundAssets.TryGetValue(kind, out var asset) ? asset : null;
}
public static string? ResolveIconAsset(HyperOS3WeatherVisualKind kind)
{
return IconAssets.TryGetValue(kind, out var asset) ? asset : null;
}
public static string ResolveSunCoreAsset()
{
return "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_sun_core.png";
@@ -328,40 +346,6 @@ public static class HyperOS3WeatherTheme
};
}
public static Symbol ResolveWeatherSymbol(HyperOS3WeatherVisualKind kind)
{
return kind switch
{
HyperOS3WeatherVisualKind.ClearDay => Symbol.WeatherSunny,
HyperOS3WeatherVisualKind.ClearNight => Symbol.WeatherMoon,
HyperOS3WeatherVisualKind.CloudyDay => Symbol.WeatherPartlyCloudyDay,
HyperOS3WeatherVisualKind.CloudyNight => Symbol.WeatherPartlyCloudyNight,
HyperOS3WeatherVisualKind.RainLight => Symbol.WeatherRainShowersDay,
HyperOS3WeatherVisualKind.RainHeavy => Symbol.WeatherRain,
HyperOS3WeatherVisualKind.Storm => Symbol.WeatherThunderstorm,
HyperOS3WeatherVisualKind.Snow => Symbol.WeatherSnow,
_ => Symbol.WeatherFog
};
}
public static string ResolveIconAccent(HyperOS3WeatherVisualKind kind, Symbol symbol)
{
var isNight = kind is HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.CloudyNight;
return symbol switch
{
Symbol.WeatherSunny => isNight ? "#F0D18A" : "#F5C65C",
Symbol.WeatherMoon => "#EED49A",
Symbol.WeatherPartlyCloudyDay => "#F3D68E",
Symbol.WeatherPartlyCloudyNight => "#CFDCFF",
Symbol.WeatherRainShowersDay => "#C7DCF9",
Symbol.WeatherRain => "#BCD4F4",
Symbol.WeatherThunderstorm => "#F0D38B",
Symbol.WeatherSnow => "#EBF5FF",
Symbol.WeatherFog => "#E3EBF6",
_ => isNight ? "#D2DDEE" : "#E5EEF9"
};
}
public static bool ResolveIsNightPreferred(
WeatherSnapshot snapshot,
TimeZoneInfo? timeZone,

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -9,16 +9,16 @@
x:Class="LanMontainDesktop.Views.Components.MultiDayWeatherWidget">
<Border x:Name="RootBorder"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Background="#68A9EC">
<Grid>
<Border x:Name="BackgroundImageLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True" />
<Border x:Name="BackgroundMotionLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Opacity="0.24"
RenderTransformOrigin="0.5,0.5">
@@ -32,12 +32,12 @@
</Border>
<Border x:Name="BackgroundTintLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Opacity="0.20" />
<Border x:Name="BackgroundLightLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Opacity="0.66">
<Border.Background>
@@ -54,7 +54,7 @@
</Border>
<Border x:Name="BackgroundShadeLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Opacity="0.78">
<Border.Background>
@@ -73,204 +73,221 @@
ClipToBounds="True" />
<Border x:Name="ContentPaddingBorder"
Padding="18"
Padding="16"
Background="Transparent">
<Grid x:Name="LayoutRoot">
<Grid x:Name="ContentGrid"
RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<Grid x:Name="TopRowGrid"
Grid.Row="0"
ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="8">
<fi:SymbolIcon x:Name="LocationIcon"
Symbol="Location"
FontSize="20"
VerticalAlignment="Center" />
<Grid x:Name="ContentGrid"
RowDefinitions="Auto,*"
RowSpacing="6">
<Grid x:Name="TopRowGrid"
Grid.Row="0"
RowDefinitions="Auto,Auto"
ColumnDefinitions="Auto,*,Auto"
RowSpacing="4"
ColumnSpacing="10">
<TextBlock x:Name="TemperatureTextBlock"
Grid.Column="0"
Grid.RowSpan="2"
Text="24°"
FontSize="98"
FontWeight="Light"
FontFeatures="tnum"
VerticalAlignment="Top"
Margin="0,-1,0,0"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<TextBlock x:Name="CityTextBlock"
Grid.Column="1"
Text="&#x5317;&#x4EAC;"
FontSize="30"
FontWeight="SemiBold"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<StackPanel x:Name="ConditionIconStack"
Grid.Column="2"
Orientation="Horizontal"
<Border x:Name="CityInfoBadge"
Grid.Column="1"
Grid.Row="0"
Background="#2AFFFFFF"
CornerRadius="11"
Padding="10,4"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<StackPanel Orientation="Horizontal"
Spacing="6"
HorizontalAlignment="Right"
VerticalAlignment="Center">
<fi:SymbolIcon x:Name="LocationIcon"
Symbol="Location"
FontSize="14"
IsVisible="False"
VerticalAlignment="Center" />
<TextBlock x:Name="CityTextBlock"
Text="Beijing"
FontSize="19"
FontWeight="Medium"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</StackPanel>
</Border>
<Border x:Name="ConditionInfoBadge"
Grid.Column="1"
Grid.Row="1"
Background="Transparent"
Padding="0"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<StackPanel x:Name="ConditionIconStack"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<TextBlock x:Name="ConditionTextBlock"
Text="&#x6674;"
FontSize="30"
Text="Clear"
FontSize="21"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<fi:SymbolIcon x:Name="WeatherIconSymbol"
Symbol="WeatherSunny"
IconVariant="Regular"
FontSize="40"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
<TextBlock x:Name="RangeTextBlock"
Text="20°/28°"
FontSize="21"
FontWeight="SemiBold"
FontFeatures="tnum"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</StackPanel>
</Grid>
</Border>
<Grid Grid.Row="1"
ColumnDefinitions="Auto,*,Auto">
<TextBlock x:Name="TemperatureTextBlock"
Grid.Column="0"
Text="26&#176;"
FontSize="108"
FontWeight="Bold"
FontFeatures="tnum"
VerticalAlignment="Center"
Margin="0,4,0,10"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<TextBlock x:Name="RangeTextBlock"
Grid.Column="2"
Text="&#x7A7A;&#x6C14;&#x4F18; 22"
FontSize="36"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Margin="0,0,0,10"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</Grid>
<StackPanel x:Name="BottomInfoStack"
Grid.Row="2"
VerticalAlignment="Bottom"
Spacing="4"
Margin="0,0,0,8">
<Border x:Name="HourlyPanelBorder"
Background="#1CFFFFFF"
CornerRadius="18"
ClipToBounds="True"
Padding="12,8">
<Grid x:Name="HourlyGrid"
ColumnDefinitions="*,*,*,*,*">
<StackPanel Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTime0"
Text="&#x4ECA;&#x5929;"
FontSize="24"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<fi:SymbolIcon x:Name="HourlyIcon0"
Symbol="WeatherSunny"
FontSize="28"
IconVariant="Regular"
HorizontalAlignment="Center" />
<TextBlock x:Name="HourlyTemp0"
Text="20&#176; / 28&#176;"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTime1"
Text="&#x660E;&#x5929;"
FontSize="24"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<fi:SymbolIcon x:Name="HourlyIcon1"
Symbol="WeatherSunny"
FontSize="28"
IconVariant="Regular"
HorizontalAlignment="Center" />
<TextBlock x:Name="HourlyTemp1"
Text="20&#176; / 28&#176;"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTime2"
Text="&#x5468;&#x516D;"
FontSize="24"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<fi:SymbolIcon x:Name="HourlyIcon2"
Symbol="WeatherSunny"
FontSize="28"
IconVariant="Regular"
HorizontalAlignment="Center" />
<TextBlock x:Name="HourlyTemp2"
Text="20&#176; / 28&#176;"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTime3"
Text="&#x5468;&#x65E5;"
FontSize="24"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<fi:SymbolIcon x:Name="HourlyIcon3"
Symbol="WeatherSunny"
FontSize="28"
IconVariant="Regular"
HorizontalAlignment="Center" />
<TextBlock x:Name="HourlyTemp3"
Text="20&#176; / 28&#176;"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="4"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="HourlyTime4"
Text="&#x5468;&#x4E00;"
FontSize="24"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<fi:SymbolIcon x:Name="HourlyIcon4"
Symbol="WeatherSunny"
FontSize="28"
IconVariant="Regular"
HorizontalAlignment="Center" />
<TextBlock x:Name="HourlyTemp4"
Text="20&#176; / 28&#176;"
FontSize="30"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
<Image x:Name="WeatherIconImage"
Grid.Column="2"
Grid.RowSpan="2"
Width="56"
Height="56"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Stretch="Uniform" />
</Grid>
<StackPanel x:Name="BottomInfoStack"
Grid.Row="1"
VerticalAlignment="Bottom"
Spacing="2"
Margin="0,0,0,1">
<Border x:Name="HourlyPanelBorder"
Background="#0EFFFFFF"
CornerRadius="15"
ClipToBounds="True"
Padding="5,3">
<Grid x:Name="HourlyGrid"
ColumnDefinitions="*,*,*,*,*"
ColumnSpacing="8">
<StackPanel Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock x:Name="HourlyTime0"
Text="Today"
FontSize="21"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon0"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTemp0"
Text="20° / 28°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock x:Name="HourlyTime1"
Text="Tomorrow"
FontSize="21"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon1"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTemp1"
Text="20° / 28°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock x:Name="HourlyTime2"
Text="Sat"
FontSize="21"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon2"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTemp2"
Text="20° / 28°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock x:Name="HourlyTime3"
Text="Sun"
FontSize="21"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon3"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTemp3"
Text="20° / 28°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
<StackPanel Grid.Column="4"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="1">
<TextBlock x:Name="HourlyTime4"
Text="Mon"
FontSize="21"
FontWeight="SemiBold"
HorizontalAlignment="Center" />
<Image x:Name="HourlyIcon4"
Width="26"
Height="26"
HorizontalAlignment="Center"
Stretch="Uniform" />
<TextBlock x:Name="HourlyTemp4"
Text="20° / 28°"
FontSize="28"
FontWeight="SemiBold"
FontFeatures="tnum"
HorizontalAlignment="Center" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
</Grid>
</Grid>
</Border>
</Grid>
</Border>
</UserControl>

View File

@@ -8,11 +8,7 @@ using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using FluentIcons.Avalonia;
using FluentIcons.Common;
using LanMontainDesktop.Models;
using LanMontainDesktop.Services;
@@ -79,7 +75,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private readonly record struct HourlyForecastItem(
DateTime Time,
string TimeLabel,
Symbol Icon,
HyperOS3WeatherVisualKind IconKind,
string TemperatureText);
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
@@ -114,7 +110,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private bool _isAttached;
private bool _isRefreshing;
private readonly TextBlock[] _hourlyTimeBlocks;
private readonly SymbolIcon[] _hourlyIconBlocks;
private readonly Image[] _hourlyIconBlocks;
private readonly TextBlock[] _hourlyTempBlocks;
public MultiDayWeatherWidget()
@@ -215,7 +211,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
var scale = ResolveScale();
var hostWidth = Bounds.Width > 1 ? Bounds.Width : Math.Max(140, _currentCellSize * 4);
var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(78, _currentCellSize * 2);
var cornerRadius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 24, 44);
var cornerRadius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 24, 46);
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
BackgroundImageLayer.CornerRadius = new CornerRadius(cornerRadius);
@@ -224,8 +220,8 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
BackgroundLightLayer.CornerRadius = new CornerRadius(cornerRadius);
BackgroundShadeLayer.CornerRadius = new CornerRadius(cornerRadius);
ContentPaddingBorder.Padding = new Thickness(
Math.Clamp(Math.Min((_currentCellSize * metrics.HorizontalPaddingScale) * scale, hostWidth * 0.028), 3, 18),
Math.Clamp(Math.Min((_currentCellSize * metrics.VerticalPaddingScale) * scale, hostHeight * 0.060), 2, 14));
Math.Clamp(Math.Min((_currentCellSize * metrics.HorizontalPaddingScale) * scale, hostWidth * 0.034), 4, 22),
Math.Clamp(Math.Min((_currentCellSize * metrics.VerticalPaddingScale) * scale, hostHeight * 0.068), 3, 18));
ApplyAdaptiveTypography();
ResetParticles();
}
@@ -448,14 +444,12 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
CityTextBlock.Text = ResolvePreciseDisplayLocation(rawLocation, _languageCode, L("weather.widget.location_unknown", "Unknown location"));
ConditionTextBlock.Text = ResolveWeatherConditionText(snapshot.Current.WeatherText, visualKind);
WeatherIconSymbol.Symbol = ResolveWeatherSymbol(visualKind);
WeatherIconSymbol.Foreground = CreateSolidBrush(
ResolveWeatherIconAccent(
WeatherIconSymbol.Symbol,
visualKind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight));
SetMainWeatherIcon(visualKind);
SetLoadingSkeleton(false);
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
RangeTextBlock.Text = FormatAirQualityText(snapshot.Current.AirQualityIndex);
var (low, high) = ResolveTemperatureRange(snapshot);
RangeTextBlock.Text = FormatTemperatureRange(low, high);
ApplyHourlyForecastItems(BuildHourlyForecastItems(snapshot));
ApplyAdaptiveTypography();
}
@@ -464,17 +458,12 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
{
var fallbackKind = ResolveFallbackVisualKind();
ApplyVisualTheme(fallbackKind);
WeatherIconSymbol.Symbol = fallbackKind == WeatherVisualKind.ClearNight
? Symbol.WeatherMoon
: Symbol.WeatherSunny;
WeatherIconSymbol.Foreground = CreateSolidBrush(
ResolveWeatherIconAccent(
WeatherIconSymbol.Symbol,
fallbackKind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight));
SetMainWeatherIcon(fallbackKind);
SetLoadingSkeleton(false);
CityTextBlock.Text = L("weather.widget.location_not_configured", "Weather location is not configured");
ConditionTextBlock.Text = L("weather.widget.condition_unknown", "Unknown");
TemperatureTextBlock.Text = "--°";
RangeTextBlock.Text = L("weather.multiday.aqi_unknown", "Air --");
RangeTextBlock.Text = L("weather.widget.range_unknown", "-- / --");
ApplyHourlyForecastItems(BuildPlaceholderHourlyForecastItems(fallbackKind));
ApplyAdaptiveTypography();
_latestSnapshot = null;
@@ -484,20 +473,15 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
{
var loadingKind = IsNightNow() ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay;
ApplyVisualTheme(loadingKind);
WeatherIconSymbol.Symbol = loadingKind == WeatherVisualKind.CloudyNight
? Symbol.WeatherPartlyCloudyNight
: Symbol.WeatherPartlyCloudyDay;
WeatherIconSymbol.Foreground = CreateSolidBrush(
ResolveWeatherIconAccent(
WeatherIconSymbol.Symbol,
loadingKind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight));
SetMainWeatherIcon(loadingKind);
SetLoadingSkeleton(true);
CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName,
_languageCode,
L("weather.widget.location_unknown", "Unknown location"));
ConditionTextBlock.Text = L("weather.widget.loading", "Loading...");
TemperatureTextBlock.Text = "--°";
RangeTextBlock.Text = L("weather.multiday.aqi_unknown", "Air --");
RangeTextBlock.Text = L("weather.widget.range_unknown", "-- / --");
ApplyHourlyForecastItems(BuildPlaceholderHourlyForecastItems(loadingKind));
ApplyAdaptiveTypography();
}
@@ -505,15 +489,15 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private void ApplyFailedState(string locationName)
{
ApplyVisualTheme(WeatherVisualKind.Fog);
WeatherIconSymbol.Symbol = Symbol.WeatherFog;
WeatherIconSymbol.Foreground = CreateSolidBrush(ResolveWeatherIconAccent(WeatherIconSymbol.Symbol, false));
SetMainWeatherIcon(WeatherVisualKind.Fog);
SetLoadingSkeleton(false);
CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName,
_languageCode,
L("weather.widget.location_unknown", "Unknown location"));
ConditionTextBlock.Text = L("weather.widget.fetch_failed", "Weather fetch failed");
TemperatureTextBlock.Text = "--°";
RangeTextBlock.Text = L("weather.multiday.aqi_unknown", "Air --");
RangeTextBlock.Text = L("weather.widget.range_unknown", "-- / --");
ApplyHourlyForecastItems(BuildPlaceholderHourlyForecastItems(WeatherVisualKind.Fog));
ApplyAdaptiveTypography();
_latestSnapshot = null;
@@ -531,22 +515,21 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
var primary = CreateSolidBrush(palette.PrimaryText);
var particleBrush = ResolveParticleBrush(ToThemeKind(kind), palette.ParticleColor);
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
var conditionSecondary = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xF0 : (byte)0xE6);
var airQualitySecondary = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xDE : (byte)0xCC);
var forecastTimeBrush = CreateSolidBrush(palette.TertiaryText, isNightVisual ? (byte)0xEA : (byte)0xB6);
var forecastTempBrush = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xF8 : (byte)0xE4);
HourlyPanelBorder.Background = CreateSolidBrush(isNightVisual ? "#24FFFFFF" : "#1EFFFFFF");
var cityBrush = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xDC : (byte)0xCC);
var conditionSecondary = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xEE : (byte)0xE2);
var rangeSecondary = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xE6 : (byte)0xD9);
var forecastTimeBrush = CreateSolidBrush(palette.TertiaryText, isNightVisual ? (byte)0xCA : (byte)0xB6);
var forecastTempBrush = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xEA : (byte)0xDC);
HourlyPanelBorder.Background = CreateSolidBrush(isNightVisual ? "#12FFFFFF" : "#0BFFFFFF");
LocationIcon.Foreground = primary;
CityTextBlock.Foreground = primary;
CityTextBlock.Foreground = cityBrush;
TemperatureTextBlock.Foreground = primary;
WeatherIconSymbol.Foreground = CreateSolidBrush(ResolveWeatherIconAccent(WeatherIconSymbol.Symbol, isNightVisual));
ConditionTextBlock.Foreground = conditionSecondary;
RangeTextBlock.Foreground = airQualitySecondary;
RangeTextBlock.Foreground = rangeSecondary;
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
{
_hourlyTimeBlocks[i].Foreground = forecastTimeBrush;
_hourlyTempBlocks[i].Foreground = forecastTempBrush;
_hourlyIconBlocks[i].Foreground = CreateSolidBrush(ResolveWeatherIconAccent(_hourlyIconBlocks[i].Symbol, isNightVisual));
}
foreach (var particle in _particleVisuals)
@@ -568,14 +551,12 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
var uriText = HyperOS3WeatherTheme.ResolveBackgroundAsset(ToThemeKind(kind));
if (!string.IsNullOrWhiteSpace(uriText))
{
try
var imageSource = HyperOS3WeatherAssetLoader.LoadImage(uriText);
if (imageSource is IImageBrushSource brushSource)
{
var uri = new Uri(uriText, UriKind.Absolute);
using var stream = AssetLoader.Open(uri);
var bitmap = new Bitmap(stream);
var imageBrush = new ImageBrush
{
Source = bitmap,
Source = brushSource,
Stretch = Stretch.UniformToFill,
AlignmentX = AlignmentX.Center,
AlignmentY = AlignmentY.Center
@@ -583,10 +564,6 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
_backgroundBrushCache[kind] = imageBrush;
return imageBrush;
}
catch
{
// Fall through to gradient background when the image cannot be loaded.
}
}
var gradientBrush = CreateGradientBrush(palette.GradientFrom, palette.GradientTo);
@@ -604,14 +581,12 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
var uriText = HyperOS3WeatherTheme.ResolveParticleAsset(kind);
if (!string.IsNullOrWhiteSpace(uriText))
{
try
var imageSource = HyperOS3WeatherAssetLoader.LoadImage(uriText);
if (imageSource is IImageBrushSource brushSource)
{
var uri = new Uri(uriText, UriKind.Absolute);
using var stream = AssetLoader.Open(uri);
var bitmap = new Bitmap(stream);
var imageBrush = new ImageBrush
{
Source = bitmap,
Source = brushSource,
Stretch = Stretch.UniformToFill,
AlignmentX = AlignmentX.Center,
AlignmentY = AlignmentY.Center
@@ -619,10 +594,6 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
_particleBrushCache[kind] = imageBrush;
return imageBrush;
}
catch
{
// Fall through to solid particle color when the image cannot be loaded.
}
}
var solidBrush = CreateSolidBrush(fallbackColor);
@@ -659,11 +630,6 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
palette.ParticleColor);
}
private static Symbol ResolveWeatherSymbol(WeatherVisualKind kind)
{
return HyperOS3WeatherTheme.ResolveWeatherSymbol(ToThemeKind(kind));
}
private static HyperOS3WeatherVisualKind ToThemeKind(WeatherVisualKind kind)
{
return kind switch
@@ -719,14 +685,14 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
{
if (!low.HasValue && !high.HasValue)
{
return L("weather.widget.range_unknown", "-- / --");
return L("weather.widget.range_unknown", "--/--");
}
var lowText = FormatTemperature(low);
var highText = FormatTemperature(high);
return string.Format(
GetUiCulture(),
L("weather.widget.range_format", "{0} / {1}"),
L("weather.widget.range_format", "{0}/{1}"),
lowText,
highText);
}
@@ -774,14 +740,14 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
var high = daily?.HighTemperatureC;
var rangeText = string.Format(
CultureInfo.InvariantCulture,
"{0} / {1}",
"{0}/{1}",
FormatTemperature(low),
FormatTemperature(high));
items.Add(new HourlyForecastItem(
date.ToDateTime(TimeOnly.MinValue),
ResolveForecastDayLabel(date, i),
ResolveWeatherSymbol(visualKind),
ToThemeKind(visualKind),
rangeText));
}
@@ -793,15 +759,15 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
const int itemCount = 5;
var items = new List<HourlyForecastItem>(itemCount);
var start = DateOnly.FromDateTime(_timeZoneService?.GetCurrentTime() ?? DateTime.Now);
var symbol = ResolveWeatherSymbol(visualKind);
var iconKind = ToThemeKind(visualKind);
for (var i = 0; i < itemCount; i++)
{
var date = start.AddDays(i);
items.Add(new HourlyForecastItem(
date.ToDateTime(TimeOnly.MinValue),
ResolveForecastDayLabel(date, i),
symbol,
"--° / --°"));
iconKind,
"--°/--°"));
}
return items;
@@ -809,22 +775,22 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private void ApplyHourlyForecastItems(IReadOnlyList<HourlyForecastItem> items)
{
var isNightVisual = _activeVisualKind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
var compactRangeText = ResolveScale() <= 0.78;
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
{
if (i >= items.Count)
{
_hourlyTimeBlocks[i].Text = "--";
_hourlyTempBlocks[i].Text = compactRangeText ? "--°/--°" : "--° / --°";
_hourlyIconBlocks[i].Symbol = ResolveWeatherSymbol(_activeVisualKind);
_hourlyTempBlocks[i].Text = "--°/--°";
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(_activeVisualKind)));
continue;
}
var item = items[i];
_hourlyTimeBlocks[i].Text = item.TimeLabel;
_hourlyIconBlocks[i].Symbol = item.Icon;
_hourlyIconBlocks[i].Foreground = CreateSolidBrush(ResolveWeatherIconAccent(item.Icon, isNightVisual));
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(item.IconKind));
_hourlyTempBlocks[i].Text = compactRangeText
? CompactRangeLabel(item.TemperatureText)
: item.TemperatureText;
@@ -889,12 +855,6 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
}
}
private static string ResolveWeatherIconAccent(Symbol symbol, bool isNightVisual)
{
var kind = isNightVisual ? HyperOS3WeatherVisualKind.ClearNight : HyperOS3WeatherVisualKind.ClearDay;
return HyperOS3WeatherTheme.ResolveIconAccent(kind, symbol);
}
private static string ResolvePreciseDisplayLocation(string? rawName, string languageCode, string fallback)
{
if (string.IsNullOrWhiteSpace(rawName))
@@ -1041,96 +1001,78 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private void ApplyAdaptiveTypography()
{
var (layoutWidth, layoutHeight) = ResolveLayoutViewport();
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.MultiDay4x2);
var scale = ResolveScale(layoutWidth, layoutHeight);
var densityBoost = scale <= 0.55 ? 0.80 : scale <= 0.72 ? 0.88 : scale <= 0.92 ? 0.95 : scale >= 1.45 ? 1.06 : 1.0;
var compactness = Math.Clamp((0.88 - scale) / 0.50, 0, 1);
var cityLength = Math.Max(1, CityTextBlock.Text?.Length ?? 2);
var cityCompression = cityLength >= 12 ? 0.68 : cityLength >= 9 ? 0.80 : cityLength >= 6 ? 0.90 : 1.0;
var conditionLength = Math.Max(1, ConditionTextBlock.Text?.Length ?? 2);
var conditionCompression = conditionLength >= 12 ? 0.72 : conditionLength >= 8 ? 0.85 : conditionLength >= 6 ? 0.92 : 1.0;
var scaleX = Math.Clamp(layoutWidth / 608d, 0.58, 1.90);
var scaleY = Math.Clamp(layoutHeight / 288d, 0.58, 1.90);
var uiScale = Math.Clamp(Math.Min(scaleX, scaleY), 0.58, 1.75);
var innerWidth = Math.Max(120, layoutWidth);
var innerHeight = Math.Max(72, layoutHeight);
ContentGrid.RowSpacing = Math.Clamp(Math.Max(metrics.MainGap * scale, layoutHeight * Lerp(0.030, 0.018, compactness)), 2, 14);
TopRowGrid.ColumnSpacing = Math.Clamp(Math.Max(metrics.MainGap * scale, layoutWidth * 0.014), 3, 14);
BottomInfoStack.Spacing = Math.Clamp(Math.Max(metrics.SectionGap * scale, layoutHeight * 0.016), 2, 10);
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(layoutHeight * 0.018, 0, 10));
ConditionIconStack.Spacing = Math.Clamp(layoutWidth * 0.009, 3, 12);
RangeTextBlock.Margin = new Thickness(0, 0, 0, Math.Clamp(layoutHeight * 0.020, 0, 12));
ContentGrid.RowSpacing = Math.Clamp(7 * scaleY, 2, 12);
TopRowGrid.ColumnSpacing = Math.Clamp(10 * scaleX, 6, 16);
TopRowGrid.RowSpacing = Math.Clamp(5 * scaleY, 2, 9);
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(2 * scaleY, 0, 5));
BottomInfoStack.Spacing = Math.Clamp(2 * scaleY, 1, 5);
var summaryHeight = Math.Clamp(116 * scaleY, 82, 164);
var bodyHeight = Math.Max(52, innerHeight - summaryHeight - ContentGrid.RowSpacing);
TemperatureTextBlock.FontSize = Math.Clamp(94 * uiScale, 56, 126);
TemperatureTextBlock.FontWeight = ToVariableWeight(320);
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-2 * uiScale, -5, 0), 0, 0);
TemperatureTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.22, 84, 168);
CityInfoBadge.Padding = new Thickness(
Math.Clamp(10 * uiScale, 6, 14),
Math.Clamp(4 * uiScale, 2, 8));
CityInfoBadge.CornerRadius = new CornerRadius(Math.Clamp(11 * uiScale, 8, 16));
LocationIcon.FontSize = Math.Clamp(14 * uiScale, 10, 20);
CityTextBlock.FontSize = Math.Clamp(21 * uiScale, 13, 31);
CityTextBlock.FontWeight = ToVariableWeight(560);
CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.25, 80, 220);
ConditionInfoBadge.Padding = new Thickness(0);
ConditionInfoBadge.CornerRadius = new CornerRadius(Math.Clamp(8 * uiScale, 4, 12));
ConditionIconStack.Spacing = Math.Clamp(12 * uiScale, 6, 18);
ConditionTextBlock.FontSize = Math.Clamp(34 * uiScale, 16, 46);
RangeTextBlock.FontSize = Math.Clamp(34 * uiScale, 16, 46);
ConditionTextBlock.FontWeight = ToVariableWeight(610);
RangeTextBlock.FontWeight = ToVariableWeight(620);
ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.16, 46, 170);
RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.20, 60, 200);
var iconSize = Math.Clamp(68 * uiScale, 40, 90);
WeatherIconImage.Width = iconSize;
WeatherIconImage.Height = iconSize;
HourlyPanelBorder.Padding = new Thickness(
Math.Clamp(layoutWidth * 0.018, 4, 16),
Math.Clamp(layoutHeight * 0.020, 3, 12));
HourlyPanelBorder.CornerRadius = new CornerRadius(Math.Clamp(Math.Min(layoutWidth, layoutHeight) * 0.065, 8, 22));
HourlyGrid.ColumnSpacing = Math.Clamp(layoutWidth * 0.010, 1.5, 12);
var topBandHeight = Math.Max(18, layoutHeight * 0.22);
var middleBandHeight = Math.Max(24, layoutHeight * 0.30);
var bottomBandHeight = Math.Max(22, layoutHeight - topBandHeight - middleBandHeight - (ContentGrid.RowSpacing * 2));
LocationIcon.FontSize = Math.Min(Math.Clamp((metrics.IconFont * 0.6) * scale * densityBoost, 9, 30), topBandHeight * 0.58);
CityTextBlock.FontSize = Math.Min(Math.Clamp((metrics.PrimaryTextFont * 1.42) * scale * cityCompression * densityBoost, 12, 46), topBandHeight * 0.76);
WeatherIconSymbol.FontSize = Math.Min(Math.Clamp((metrics.IconFont * 1.02) * scale * densityBoost, 12, 56), topBandHeight * 0.95);
TemperatureTextBlock.FontSize = Math.Min(Math.Clamp((metrics.PrimaryTemperatureFont * 1.40) * scale * densityBoost, 26, 138), middleBandHeight * 0.92);
ConditionTextBlock.FontSize = Math.Min(Math.Clamp((metrics.PrimaryTextFont * 1.10) * scale * conditionCompression * densityBoost, 9, 40), topBandHeight * 0.70);
RangeTextBlock.FontSize = Math.Min(Math.Clamp((metrics.SecondaryTextFont * 1.42) * scale * densityBoost, 9, 42), middleBandHeight * 0.50);
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(layoutHeight * 0.008, 0, 6), 0, Math.Clamp(layoutHeight * 0.012, 0, 8));
var weightProgress = Math.Clamp((scale - 0.34) / 1.18, 0, 1);
CityTextBlock.FontWeight = ToVariableWeight(Lerp(540, 680, weightProgress));
TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(600, 760, weightProgress));
ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(490, 620, weightProgress));
RangeTextBlock.FontWeight = ToVariableWeight(Lerp(480, 600, weightProgress));
var topRightMaxWidth = Math.Clamp(layoutWidth * Lerp(0.36, 0.31, compactness), 112, 230);
ConditionIconStack.MaxWidth = topRightMaxWidth;
ConditionTextBlock.MaxWidth = Math.Max(
42,
topRightMaxWidth - WeatherIconSymbol.FontSize - ConditionIconStack.Spacing - 4);
RangeTextBlock.MaxWidth = Math.Clamp(layoutWidth * Lerp(0.36, 0.32, compactness), 112, 250);
var leftTopBudget = Math.Max(
132,
layoutWidth - Math.Max(topRightMaxWidth, RangeTextBlock.MaxWidth) - Math.Clamp(66 * scale, 26, 96));
TemperatureTextBlock.MaxWidth = leftTopBudget;
CityTextBlock.MaxWidth = Math.Max(110, layoutWidth - Math.Clamp(90 * scale, 30, 128));
Math.Clamp(5 * scaleX, 3, 10),
Math.Clamp(3 * scaleY, 1, 7));
HourlyPanelBorder.CornerRadius = new CornerRadius(Math.Clamp(14 * uiScale, 8, 20));
HourlyGrid.ColumnSpacing = Math.Clamp(10 * scaleX, 4, 15);
var forecastColumnCount = Math.Max(1, _hourlyTimeBlocks.Length);
var forecastInnerWidth = Math.Max(
80,
layoutWidth - HourlyPanelBorder.Padding.Left - HourlyPanelBorder.Padding.Right - (HourlyGrid.ColumnSpacing * (forecastColumnCount - 1)));
var forecastCellWidth = Math.Max(32, forecastInnerWidth / forecastColumnCount);
var forecastStackSpacing = Math.Clamp(bottomBandHeight * 0.065, 1, 5);
var forecastInnerHeight = Math.Max(
20,
bottomBandHeight - HourlyPanelBorder.Padding.Top - HourlyPanelBorder.Padding.Bottom);
var forecastLineHeight = Math.Max(6, (forecastInnerHeight - (forecastStackSpacing * 2)) / 3d);
var hourlyTimeMaxByWidth = Math.Clamp(forecastCellWidth / Lerp(4.3, 3.7, 1 - compactness), 7, 24);
var hourlyTempMaxByWidth = Math.Clamp(forecastCellWidth / Lerp(5.8, 5.0, 1 - compactness), 7, 24);
var hourlyIconMaxByWidth = Math.Clamp(forecastCellWidth * Lerp(0.30, 0.36, 1 - compactness), 7, 30);
var hourlyTimeMaxByHeight = Math.Clamp(forecastLineHeight * 0.95, 7, 22);
var hourlyTempMaxByHeight = Math.Clamp(forecastLineHeight * 0.95, 7, 22);
var hourlyIconMaxByHeight = Math.Clamp(forecastLineHeight * 1.05, 8, 28);
96,
innerWidth - HourlyPanelBorder.Padding.Left - HourlyPanelBorder.Padding.Right - (HourlyGrid.ColumnSpacing * (forecastColumnCount - 1)));
var forecastCellWidth = Math.Max(40, forecastInnerWidth / forecastColumnCount);
var stackSpacing = Math.Clamp(2 * scaleY, 1, 4);
var forecastLabelSize = Math.Clamp(bodyHeight * 0.20, 10, 23);
var forecastIconSize = Math.Clamp(bodyHeight * 0.28, 14, 34);
var forecastRangeSize = Math.Clamp(bodyHeight * 0.24, 11, 28);
var hourlyTimeSize = Math.Min(
Math.Clamp((metrics.CaptionFont * 1.15) * scale * densityBoost, 8, 30),
Math.Min(hourlyTimeMaxByWidth, hourlyTimeMaxByHeight));
var hourlyIconSize = Math.Min(
Math.Clamp((metrics.IconFont * 0.64) * scale * densityBoost, 8, 34),
Math.Min(hourlyIconMaxByWidth, hourlyIconMaxByHeight));
var hourlyTempSize = Math.Min(
Math.Clamp((metrics.SecondaryTextFont * 1.24) * scale * densityBoost, 8, 32),
Math.Min(hourlyTempMaxByWidth, hourlyTempMaxByHeight));
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
{
_hourlyTimeBlocks[i].FontSize = hourlyTimeSize;
_hourlyTempBlocks[i].FontSize = hourlyTempSize;
_hourlyIconBlocks[i].FontSize = hourlyIconSize;
_hourlyTimeBlocks[i].MaxWidth = forecastCellWidth;
_hourlyTempBlocks[i].MaxWidth = forecastCellWidth;
_hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(470, 600, weightProgress));
_hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(490, 620, weightProgress));
_hourlyTimeBlocks[i].FontSize = forecastLabelSize;
_hourlyTempBlocks[i].FontSize = forecastRangeSize;
_hourlyIconBlocks[i].Width = forecastIconSize;
_hourlyIconBlocks[i].Height = forecastIconSize;
_hourlyTimeBlocks[i].MaxWidth = Math.Clamp(forecastCellWidth, 42, 148);
_hourlyTempBlocks[i].MaxWidth = Math.Clamp(forecastCellWidth, 42, 148);
_hourlyTimeBlocks[i].FontWeight = ToVariableWeight(500);
_hourlyTempBlocks[i].FontWeight = ToVariableWeight(590);
if (_hourlyTimeBlocks[i].Parent is StackPanel forecastStack)
{
forecastStack.Spacing = forecastStackSpacing;
forecastStack.Spacing = stackSpacing;
}
}
}
@@ -1140,6 +1082,18 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
return from + ((to - from) * t);
}
private void SetMainWeatherIcon(WeatherVisualKind kind)
{
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(kind)));
}
private void SetLoadingSkeleton(bool isLoading)
{
CityInfoBadge.Background = isLoading ? CreateSolidBrush("#24FFFFFF") : Brushes.Transparent;
ConditionInfoBadge.Background = isLoading ? CreateSolidBrush("#1CFFFFFF") : Brushes.Transparent;
}
private static FontWeight ToVariableWeight(double weight)
{
return (FontWeight)(int)Math.Clamp(Math.Round(weight), 1, 1000);

View File

@@ -0,0 +1,255 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="640"
d:DesignHeight="320"
x:Class="LanMontainDesktop.Views.Components.MusicControlWidget">
<UserControl.Styles>
<Style Selector="Button.music-action">
<Setter Property="Background" Value="#24FFFFFF" />
<Setter Property="BorderBrush" Value="#44FFFFFF" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.music-action:pointerover">
<Setter Property="Background" Value="#30FFFFFF" />
</Style>
<Style Selector="Button.music-action:pressed">
<Setter Property="Background" Value="#4AFFFFFF" />
</Style>
<Style Selector="Button.music-action:disabled">
<Setter Property="Opacity" Value="0.55" />
</Style>
<Style Selector="Button.music-link">
<Setter Property="Background" Value="#14FFFFFF" />
<Setter Property="BorderBrush" Value="#3FFFFFFF" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="8,3" />
<Setter Property="CornerRadius" Value="9" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
<Style Selector="Button.music-link:pointerover">
<Setter Property="Background" Value="#24FFFFFF" />
</Style>
<Style Selector="Button.music-link:pressed">
<Setter Property="Background" Value="#3AFFFFFF" />
</Style>
</UserControl.Styles>
<Border x:Name="RootBorder"
CornerRadius="30"
ClipToBounds="True"
BorderThickness="1"
BorderBrush="#54FFFFFF"
Padding="14,11,14,11">
<Border.Background>
<LinearGradientBrush StartPoint="0,0"
EndPoint="1,1">
<GradientStop Color="#AB9E84"
Offset="0" />
<GradientStop Color="#8D8066"
Offset="0.52" />
<GradientStop Color="#75684F"
Offset="1" />
</LinearGradientBrush>
</Border.Background>
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="8">
<Border Grid.Row="0"
Grid.RowSpan="2"
Background="#22FFFFFF"
CornerRadius="16"
IsHitTestVisible="False" />
<Grid Grid.Row="0"
ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="10">
<Border x:Name="CoverBorder"
Width="56"
Height="56"
CornerRadius="12"
ClipToBounds="True"
BorderThickness="1"
BorderBrush="#6AFFFFFF"
Background="#38FFFFFF">
<Grid>
<Image x:Name="CoverImage"
IsVisible="False"
Stretch="UniformToFill" />
<Path x:Name="CoverFallbackGlyph"
Width="18"
Height="18"
Stretch="Uniform"
Fill="#F3FFFFFF"
Data="M 9,1 C 6.2,1 4,3.2 4,6 C 4,8.8 6.2,11 9,11 C 11.8,11 14,8.8 14,6 C 14,3.2 11.8,1 9,1 Z M 11,6 C 11,7.1 10.1,8 9,8 C 7.9,8 7,7.1 7,6 C 7,4.9 7.9,4 9,4 C 10.1,4 11,4.9 11,6 Z M 9.5,10.8 L 8.5,10.8 L 8.5,18 L 9.5,18 Z" />
</Grid>
</Border>
<StackPanel Grid.Column="1"
Spacing="2"
VerticalAlignment="Center">
<TextBlock x:Name="TitleTextBlock"
Text="Music"
FontSize="22"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
MaxLines="1"
Foreground="#FFFFFFFF" />
<TextBlock x:Name="ArtistTextBlock"
Text="No active media session"
FontSize="16"
FontWeight="Medium"
TextTrimming="CharacterEllipsis"
MaxLines="1"
Foreground="#DBFFFFFF" />
<Button x:Name="SourceAppButton"
Classes="music-link"
Click="OnSourceAppButtonClick">
<StackPanel Orientation="Horizontal"
Spacing="5"
VerticalAlignment="Center">
<Path Width="11"
Height="11"
Stretch="Uniform"
Fill="#F7FFFFFF"
Data="M 2,2 H 12 V 5 H 10 V 4 H 4 V 12 H 8 V 10 H 9 V 13 H 3 C 2.4,13 2,12.6 2,12 Z M 7,1 H 14 V 8 H 13 V 3.4 L 9.4,7 L 8.6,6.2 L 12.2,2.6 H 7 Z" />
<TextBlock x:Name="SourceAppTextBlock"
Text="Open player"
FontSize="12"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
MaxLines="1"
Foreground="#F7FFFFFF" />
</StackPanel>
</Button>
</StackPanel>
<Border Grid.Column="2"
x:Name="StatusBadgeBorder"
CornerRadius="10"
BorderThickness="1"
BorderBrush="#5FFFFFFF"
Background="#1EFFFFFF"
Padding="8,4"
VerticalAlignment="Top">
<TextBlock x:Name="StatusTextBlock"
Text="--"
FontSize="12"
FontWeight="SemiBold"
Foreground="#F3FFFFFF" />
</Border>
</Grid>
<Grid Grid.Row="1"
ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="8"
VerticalAlignment="Center">
<TextBlock x:Name="PositionTextBlock"
Text="00:00"
FontSize="13"
FontWeight="SemiBold"
Foreground="#E8FFFFFF"
VerticalAlignment="Center" />
<ProgressBar x:Name="ProgressBar"
Grid.Column="1"
MinWidth="160"
Minimum="0"
Maximum="100"
Value="0"
Height="5"
VerticalAlignment="Center"
Foreground="#ECFFFFFF"
Background="#45FFFFFF" />
<TextBlock x:Name="DurationTextBlock"
Grid.Column="2"
Text="00:00"
FontSize="13"
FontWeight="SemiBold"
Foreground="#E8FFFFFF"
VerticalAlignment="Center" />
</Grid>
<Grid Grid.Row="2"
ColumnDefinitions="Auto,Auto,Auto,Auto,Auto"
ColumnSpacing="8"
HorizontalAlignment="Center"
VerticalAlignment="Bottom">
<Button x:Name="QueueButton"
Grid.Column="0"
Classes="music-action"
Width="32"
Height="32"
IsEnabled="False">
<Path Width="14"
Height="14"
Stretch="Uniform"
Fill="#FFFFFFFF"
Data="M 2,3 H 18 V 5 H 2 Z M 2,8 H 14 V 10 H 2 Z M 2,13 H 10 V 15 H 2 Z" />
</Button>
<Button x:Name="PreviousButton"
Grid.Column="1"
Classes="music-action"
Width="34"
Height="34"
Click="OnPreviousButtonClick">
<Path Width="14"
Height="14"
Stretch="Uniform"
Fill="#FFFFFFFF"
Data="M 3,2 V 14 H 5 V 2 Z M 6,8 L 14,2 V 14 Z" />
</Button>
<Button x:Name="PlayPauseButton"
Grid.Column="2"
Classes="music-action"
Width="42"
Height="42"
Click="OnPlayPauseButtonClick">
<Path x:Name="PlayPauseGlyphPath"
Width="14"
Height="14"
Stretch="Uniform"
Fill="#FFFFFFFF"
Data="M 2,1 L 2,13 L 12,7 Z" />
</Button>
<Button x:Name="NextButton"
Grid.Column="3"
Classes="music-action"
Width="34"
Height="34"
Click="OnNextButtonClick">
<Path Width="14"
Height="14"
Stretch="Uniform"
Fill="#FFFFFFFF"
Data="M 11,2 V 14 H 13 V 2 Z M 2,2 L 10,8 L 2,14 Z" />
</Button>
<Button x:Name="FavoriteButton"
Grid.Column="4"
Classes="music-action"
Width="32"
Height="32"
IsEnabled="False">
<Path Width="14"
Height="14"
Stretch="Uniform"
Fill="#FFFFFFFF"
Data="M 10,3 L 12.4,7.2 L 17.2,8.1 L 13.8,11.5 L 14.4,16.3 L 10,14.1 L 5.6,16.3 L 6.2,11.5 L 2.8,8.1 L 7.6,7.2 Z" />
</Button>
</Grid>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,407 @@
using System;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using LanMontainDesktop.Services;
namespace LanMontainDesktop.Views.Components;
public partial class MusicControlWidget : UserControl, IDesktopComponentWidget
{
private static readonly Geometry PlayGlyph = Geometry.Parse("M 2,1 L 2,13 L 12,7 Z");
private static readonly Geometry PauseGlyph = Geometry.Parse("M 2,1 H 5 V 13 H 2 Z M 9,1 H 12 V 13 H 9 Z");
private readonly DispatcherTimer _refreshTimer = new()
{
Interval = TimeSpan.FromSeconds(2.4)
};
private readonly IMusicControlService _musicControlService = MusicControlServiceFactory.CreateDefault();
private readonly AppSettingsService _settingsService = new();
private readonly LocalizationService _localizationService = new();
private CancellationTokenSource? _refreshCts;
private Bitmap? _coverBitmap;
private MusicPlaybackState _currentState = MusicPlaybackState.NoSession(isSupported: true);
private string _languageCode = "zh-CN";
private double _currentCellSize = 48;
private bool _isAttached;
private bool _isRefreshing;
private bool _isExecutingCommand;
public MusicControlWidget()
{
InitializeComponent();
_refreshTimer.Tick += OnRefreshTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ApplyCellSize(_currentCellSize);
ApplyState(MusicPlaybackState.NoSession(isSupported: OperatingSystem.IsWindows()));
}
public void ApplyCellSize(double cellSize)
{
_currentCellSize = Math.Max(1, cellSize);
var scale = ResolveScale();
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(30 * scale, 16, 44));
RootBorder.Padding = new Thickness(
Math.Clamp(14 * scale, 8, 24),
Math.Clamp(11 * scale, 7, 18),
Math.Clamp(14 * scale, 8, 24),
Math.Clamp(11 * scale, 7, 18));
CoverBorder.Width = Math.Clamp(56 * scale, 38, 92);
CoverBorder.Height = Math.Clamp(56 * scale, 38, 92);
CoverBorder.CornerRadius = new CornerRadius(Math.Clamp(12 * scale, 8, 18));
StatusBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(10 * scale, 6, 14));
StatusBadgeBorder.Padding = new Thickness(
Math.Clamp(8 * scale, 5, 12),
Math.Clamp(4 * scale, 3, 8));
TitleTextBlock.FontSize = Math.Clamp(22 * scale, 13, 30);
ArtistTextBlock.FontSize = Math.Clamp(16 * scale, 10, 20);
SourceAppTextBlock.FontSize = Math.Clamp(12 * scale, 9, 15);
SourceAppButton.Padding = new Thickness(
Math.Clamp(8 * scale, 5, 12),
Math.Clamp(3 * scale, 2, 6));
StatusTextBlock.FontSize = Math.Clamp(12 * scale, 9, 14);
PositionTextBlock.FontSize = Math.Clamp(13 * scale, 9, 16);
DurationTextBlock.FontSize = Math.Clamp(13 * scale, 9, 16);
ProgressBar.Height = Math.Clamp(5 * scale, 3, 8);
QueueButton.Width = QueueButton.Height = Math.Clamp(32 * scale, 24, 44);
FavoriteButton.Width = FavoriteButton.Height = Math.Clamp(32 * scale, 24, 44);
PreviousButton.Width = PreviousButton.Height = Math.Clamp(34 * scale, 25, 46);
NextButton.Width = NextButton.Height = Math.Clamp(34 * scale, 25, 46);
PlayPauseButton.Width = PlayPauseButton.Height = Math.Clamp(42 * scale, 30, 58);
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = true;
_refreshTimer.Start();
_ = RefreshStateAsync();
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = false;
_refreshTimer.Stop();
CancelRefreshRequest();
DisposeCoverBitmap();
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
ApplyCellSize(_currentCellSize);
}
private async void OnRefreshTimerTick(object? sender, EventArgs e)
{
await RefreshStateAsync();
}
private async void OnPlayPauseButtonClick(object? sender, RoutedEventArgs e)
{
await ExecuteCommandAsync(token => _musicControlService.TogglePlayPauseAsync(token));
}
private async void OnPreviousButtonClick(object? sender, RoutedEventArgs e)
{
await ExecuteCommandAsync(token => _musicControlService.SkipPreviousAsync(token));
}
private async void OnNextButtonClick(object? sender, RoutedEventArgs e)
{
await ExecuteCommandAsync(token => _musicControlService.SkipNextAsync(token));
}
private async void OnSourceAppButtonClick(object? sender, RoutedEventArgs e)
{
await ExecuteCommandAsync(token => _musicControlService.LaunchSourceAppAsync(token), refreshAfterCommand: false);
}
private async Task ExecuteCommandAsync(Func<CancellationToken, Task<bool>> command, bool refreshAfterCommand = true)
{
if (_isExecutingCommand || !_currentState.IsSupported || !_currentState.HasSession)
{
return;
}
_isExecutingCommand = true;
ApplyActionButtonState(_currentState);
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(4));
_ = await command(cts.Token);
}
catch
{
// Ignore command transport errors and recover on next poll.
}
finally
{
_isExecutingCommand = false;
}
if (refreshAfterCommand)
{
await RefreshStateAsync();
}
}
private async Task RefreshStateAsync()
{
if (!_isAttached || _isRefreshing)
{
return;
}
_isRefreshing = true;
UpdateLanguageCode();
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var previous = Interlocked.Exchange(ref _refreshCts, cts);
previous?.Cancel();
previous?.Dispose();
try
{
var state = await _musicControlService.GetCurrentStateAsync(cts.Token);
if (cts.IsCancellationRequested || !_isAttached)
{
return;
}
_currentState = state;
ApplyState(state);
}
catch (OperationCanceledException)
{
// Ignore cancellation.
}
catch
{
var fallbackState = MusicPlaybackState.NoSession(isSupported: OperatingSystem.IsWindows());
_currentState = fallbackState;
ApplyState(fallbackState);
}
finally
{
if (ReferenceEquals(_refreshCts, cts))
{
_refreshCts = null;
}
cts.Dispose();
_isRefreshing = false;
}
}
private void ApplyState(MusicPlaybackState state)
{
var hasMediaSession = state.IsSupported && state.HasSession;
if (!state.IsSupported)
{
TitleTextBlock.Text = L("music.widget.unsupported", "Music control is only available on Windows");
ArtistTextBlock.Text = L("music.widget.unsupported_hint", "SMTC backend is unavailable");
SourceAppTextBlock.Text = L("music.widget.open_player", "Open player");
StatusTextBlock.Text = "--";
PositionTextBlock.Text = "00:00";
DurationTextBlock.Text = "00:00";
ProgressBar.IsIndeterminate = false;
ProgressBar.Value = 0;
PlayPauseGlyphPath.Data = PlayGlyph;
SetCoverImage(null);
ApplyActionButtonState(state);
return;
}
if (!state.HasSession)
{
TitleTextBlock.Text = L("music.widget.no_session", "No active media session");
ArtistTextBlock.Text = L("music.widget.no_session_hint", "Open a player that supports SMTC");
SourceAppTextBlock.Text = L("music.widget.open_player", "Open player");
StatusTextBlock.Text = "--";
PositionTextBlock.Text = "00:00";
DurationTextBlock.Text = "00:00";
ProgressBar.IsIndeterminate = false;
ProgressBar.Value = 0;
PlayPauseGlyphPath.Data = PlayGlyph;
SetCoverImage(null);
ApplyActionButtonState(state);
return;
}
var title = string.IsNullOrWhiteSpace(state.Title)
? L("music.widget.unknown_title", "Unknown title")
: state.Title;
var subtitle = !string.IsNullOrWhiteSpace(state.Artist)
? state.Artist
: !string.IsNullOrWhiteSpace(state.AlbumTitle)
? state.AlbumTitle
: L("music.widget.unknown_artist", "Unknown artist");
TitleTextBlock.Text = title;
ArtistTextBlock.Text = subtitle;
SourceAppTextBlock.Text = string.IsNullOrWhiteSpace(state.SourceAppName)
? L("music.widget.open_player", "Open player")
: state.SourceAppName;
StatusTextBlock.Text = ResolveStatusText(state.PlaybackStatus);
var position = ClampToNonNegative(state.Position);
var duration = ClampToNonNegative(state.Duration);
var progress = duration.TotalMilliseconds <= 1
? 0
: Math.Clamp((position.TotalMilliseconds / duration.TotalMilliseconds) * 100d, 0, 100);
PositionTextBlock.Text = FormatTimeline(position);
DurationTextBlock.Text = duration.TotalMilliseconds > 1
? FormatTimeline(duration)
: "00:00";
ProgressBar.IsIndeterminate = hasMediaSession && duration.TotalMilliseconds <= 1;
ProgressBar.Value = ProgressBar.IsIndeterminate ? 0 : progress;
PlayPauseGlyphPath.Data = state.PlaybackStatus == MusicPlaybackStatus.Playing
? PauseGlyph
: PlayGlyph;
SetCoverImage(state.ThumbnailBytes);
ApplyActionButtonState(state);
}
private void ApplyActionButtonState(MusicPlaybackState state)
{
var canOperate = !_isExecutingCommand && state.IsSupported && state.HasSession;
PlayPauseButton.IsEnabled = canOperate && state.CanPlayPause;
PreviousButton.IsEnabled = canOperate && state.CanSkipPrevious;
NextButton.IsEnabled = canOperate && state.CanSkipNext;
SourceAppButton.IsEnabled = canOperate && !string.IsNullOrWhiteSpace(state.SourceAppId);
QueueButton.IsEnabled = false;
FavoriteButton.IsEnabled = false;
}
private void UpdateLanguageCode()
{
try
{
var snapshot = _settingsService.Load();
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
}
catch
{
_languageCode = "zh-CN";
}
}
private void CancelRefreshRequest()
{
var cts = Interlocked.Exchange(ref _refreshCts, null);
if (cts is null)
{
return;
}
cts.Cancel();
cts.Dispose();
}
private string ResolveStatusText(MusicPlaybackStatus status)
{
return status switch
{
MusicPlaybackStatus.Playing => L("music.widget.status.playing", "Playing"),
MusicPlaybackStatus.Paused => L("music.widget.status.paused", "Paused"),
MusicPlaybackStatus.Stopped => L("music.widget.status.stopped", "Stopped"),
MusicPlaybackStatus.Changing => L("music.widget.status.changing", "Changing"),
MusicPlaybackStatus.Opened => L("music.widget.status.opened", "Opened"),
_ => "--"
};
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
private double ResolveScale()
{
var cellScale = Math.Clamp(_currentCellSize / 48d, 0.62, 2.1);
var widthScale = Bounds.Width > 1
? Math.Clamp(Bounds.Width / Math.Max(1, _currentCellSize * 4), 0.60, 1.8)
: 1;
var heightScale = Bounds.Height > 1
? Math.Clamp(Bounds.Height / Math.Max(1, _currentCellSize * 2), 0.60, 1.8)
: 1;
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale) * 1.05), 0.58, 2.0);
}
private static TimeSpan ClampToNonNegative(TimeSpan value)
{
return value < TimeSpan.Zero ? TimeSpan.Zero : value;
}
private static string FormatTimeline(TimeSpan value)
{
if (value.TotalHours >= 1)
{
return value.ToString(@"h\:mm\:ss", CultureInfo.InvariantCulture);
}
return value.ToString(@"mm\:ss", CultureInfo.InvariantCulture);
}
private void SetCoverImage(byte[]? thumbnailBytes)
{
DisposeCoverBitmap();
if (thumbnailBytes is null || thumbnailBytes.Length == 0)
{
CoverImage.Source = null;
CoverImage.IsVisible = false;
CoverFallbackGlyph.IsVisible = true;
return;
}
try
{
using var stream = new MemoryStream(thumbnailBytes, writable: false);
_coverBitmap = new Bitmap(stream);
CoverImage.Source = _coverBitmap;
CoverImage.IsVisible = true;
CoverFallbackGlyph.IsVisible = false;
}
catch
{
CoverImage.Source = null;
CoverImage.IsVisible = false;
CoverFallbackGlyph.IsVisible = true;
}
}
private void DisposeCoverBitmap()
{
if (_coverBitmap is null)
{
return;
}
_coverBitmap.Dispose();
_coverBitmap = null;
}
}

View File

@@ -0,0 +1,170 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="320"
d:DesignHeight="320"
x:Class="LanMontainDesktop.Views.Components.RecordingWidget">
<Border x:Name="RootBorder"
CornerRadius="34"
Padding="10"
ClipToBounds="True"
Background="#ECEFF3"
BorderBrush="#DEE3EA"
BorderThickness="1">
<Viewbox Stretch="Uniform">
<Grid Width="300"
Height="300">
<Border x:Name="RecorderCardBorder"
Width="248"
Height="248"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CornerRadius="30"
BorderBrush="#E6EAF0"
BorderThickness="1"
Background="#F4F6FA">
<Grid Margin="16,14,16,12"
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock x:Name="TitleTextBlock"
Grid.Row="0"
Text="录音"
FontSize="19"
FontWeight="SemiBold"
Foreground="#11151D"
HorizontalAlignment="Center" />
<TextBlock x:Name="TimerTextBlock"
Grid.Row="1"
Margin="0,8,0,0"
Text="00:00"
FontSize="66"
FontWeight="SemiBold"
FontFeatures="tnum"
Foreground="#151922"
HorizontalAlignment="Center" />
<Grid Grid.Row="2"
Margin="0,10,0,0"
ColumnDefinitions="*,2,68"
VerticalAlignment="Center">
<StackPanel x:Name="WaveformBarsPanel"
Grid.Column="0"
Orientation="Horizontal"
Spacing="3"
HorizontalAlignment="Left"
VerticalAlignment="Center" />
<Border Grid.Column="1"
Margin="0,0,0,0"
Width="2"
Height="32"
CornerRadius="1"
Background="#F14A40"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<Border x:Name="FutureLine"
Grid.Column="2"
Margin="8,0,0,0"
Height="2"
CornerRadius="1"
Background="#A3A8B3"
Opacity="0.55"
HorizontalAlignment="Stretch"
VerticalAlignment="Center" />
</Grid>
<Grid Grid.Row="3"
Margin="0,16,0,0"
HorizontalAlignment="Center"
ColumnDefinitions="Auto,Auto,Auto"
ColumnSpacing="16">
<Border x:Name="DiscardButtonBorder"
Grid.Column="0"
Width="54"
Height="54"
CornerRadius="27"
Background="#F8FAFD"
BorderBrush="#E0E5EC"
BorderThickness="1"
Cursor="Hand"
PointerPressed="OnDiscardButtonPointerPressed">
<Viewbox Width="20"
Height="20"
Stretch="Uniform">
<Path Data="M 5,2 V 18 M 5,3 H 15 L 13,7 L 15,11 H 5"
Stroke="#141922"
StrokeThickness="1.9" />
</Viewbox>
</Border>
<Border x:Name="RecordToggleButtonBorder"
Grid.Column="1"
Width="68"
Height="68"
CornerRadius="34"
Background="#EF3E38"
Cursor="Hand"
PointerPressed="OnRecordToggleButtonPointerPressed">
<Grid>
<Ellipse x:Name="RecordDot"
Width="15"
Height="15"
Fill="#FFFFFF"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<Path x:Name="PauseGlyphPath"
Width="14"
Height="16"
Stretch="Uniform"
Fill="#FFFFFF"
Data="M 0,0 H 4 V 16 H 0 Z M 8,0 H 12 V 16 H 8 Z"
IsVisible="False" />
<Path x:Name="PlayGlyphPath"
Width="16"
Height="16"
Stretch="Uniform"
Fill="#FFFFFF"
Data="M 0,0 L 0,16 L 13,8 Z"
IsVisible="False" />
</Grid>
</Border>
<Border x:Name="SaveButtonBorder"
Grid.Column="2"
Width="54"
Height="54"
CornerRadius="27"
Background="#F8FAFD"
BorderBrush="#E0E5EC"
BorderThickness="1"
Cursor="Hand"
PointerPressed="OnSaveButtonPointerPressed">
<Viewbox Width="22"
Height="22"
Stretch="Uniform">
<Path Data="M 3,11 L 8,16 L 19,5"
Stroke="#141922"
StrokeThickness="2.2" />
</Viewbox>
</Border>
</Grid>
<TextBlock x:Name="HintTextBlock"
Grid.Row="4"
Margin="0,10,0,0"
HorizontalAlignment="Center"
TextAlignment="Center"
FontSize="13"
FontWeight="Medium"
Foreground="#7A818E"
Text="点击红色按钮开始" />
</Grid>
</Border>
</Grid>
</Viewbox>
</Border>
</UserControl>

View File

@@ -0,0 +1,338 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Threading;
using LanMontainDesktop.Services;
namespace LanMontainDesktop.Views.Components;
public partial class RecordingWidget : UserControl, IDesktopComponentWidget
{
private const int WaveBarCount = 22;
private readonly DispatcherTimer _uiTimer = new()
{
Interval = TimeSpan.FromMilliseconds(96)
};
private readonly IAudioRecorderService _audioRecorderService = AudioRecorderServiceFactory.CreateDefault();
private readonly AppSettingsService _settingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly List<Border> _waveBars = [];
private readonly double[] _waveLevels = new double[WaveBarCount];
private string _languageCode = "zh-CN";
private string _lastSavedFilePath = string.Empty;
private double _currentCellSize = 48;
private bool _isAttached;
public RecordingWidget()
{
InitializeComponent();
_uiTimer.Tick += OnUiTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
InitializeWaveBars();
ReloadLanguageCode();
ApplyCellSize(_currentCellSize);
RefreshVisual();
}
public void ApplyCellSize(double cellSize)
{
_currentCellSize = Math.Max(1, cellSize);
var scale = ResolveScale();
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 16, 56));
RootBorder.Padding = new Thickness(Math.Clamp(10 * scale, 6, 18));
RecorderCardBorder.CornerRadius = new CornerRadius(Math.Clamp(30 * scale, 14, 48));
var sideButtonSize = Math.Clamp(54 * scale, 38, 72);
DiscardButtonBorder.Width = sideButtonSize;
DiscardButtonBorder.Height = sideButtonSize;
DiscardButtonBorder.CornerRadius = new CornerRadius(sideButtonSize / 2d);
SaveButtonBorder.Width = sideButtonSize;
SaveButtonBorder.Height = sideButtonSize;
SaveButtonBorder.CornerRadius = new CornerRadius(sideButtonSize / 2d);
var centerButtonSize = Math.Clamp(68 * scale, 48, 86);
RecordToggleButtonBorder.Width = centerButtonSize;
RecordToggleButtonBorder.Height = centerButtonSize;
RecordToggleButtonBorder.CornerRadius = new CornerRadius(centerButtonSize / 2d);
WaveformBarsPanel.Spacing = Math.Clamp(3 * scale, 1.8, 5.4);
TitleTextBlock.FontSize = Math.Clamp(19 * scale, 13, 26);
TimerTextBlock.FontSize = Math.Clamp(66 * scale, 38, 84);
HintTextBlock.FontSize = Math.Clamp(13 * scale, 9, 16);
UpdateWaveformVisual();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = true;
_uiTimer.Start();
ReloadLanguageCode();
RefreshVisual();
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = false;
_uiTimer.Stop();
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
ApplyCellSize(_currentCellSize);
}
private void OnUiTick(object? sender, EventArgs e)
{
if (!_isAttached)
{
return;
}
RefreshVisual();
}
private void OnDiscardButtonPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}
_audioRecorderService.Discard();
RefreshVisual();
e.Handled = true;
}
private void OnRecordToggleButtonPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}
var snapshot = _audioRecorderService.GetSnapshot();
if (!snapshot.IsSupported)
{
RefreshVisual();
e.Handled = true;
return;
}
if (snapshot.State == AudioRecorderRuntimeState.Recording)
{
_audioRecorderService.Pause();
}
else
{
_audioRecorderService.StartOrResume();
}
RefreshVisual();
e.Handled = true;
}
private void OnSaveButtonPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}
_ = _audioRecorderService.StopAndSave();
RefreshVisual();
e.Handled = true;
}
private void RefreshVisual()
{
var snapshot = _audioRecorderService.GetSnapshot();
TitleTextBlock.Text = L("recording.widget.title", "Recorder");
TimerTextBlock.Text = FormatDuration(snapshot.Duration);
var incomingLevel = snapshot.State == AudioRecorderRuntimeState.Recording
? snapshot.InputLevel
: snapshot.State == AudioRecorderRuntimeState.Paused
? 0.10
: 0;
PushWaveLevel(incomingLevel);
UpdateWaveformVisual();
ApplyControlState(snapshot);
}
private void ApplyControlState(AudioRecorderSnapshot snapshot)
{
var isSupported = snapshot.IsSupported;
var canFinalize = snapshot.State == AudioRecorderRuntimeState.Recording ||
snapshot.State == AudioRecorderRuntimeState.Paused;
DiscardButtonBorder.IsHitTestVisible = isSupported && canFinalize;
SaveButtonBorder.IsHitTestVisible = isSupported && canFinalize;
RecordToggleButtonBorder.IsHitTestVisible = isSupported;
DiscardButtonBorder.Opacity = DiscardButtonBorder.IsHitTestVisible ? 1 : 0.42;
SaveButtonBorder.Opacity = SaveButtonBorder.IsHitTestVisible ? 1 : 0.42;
RecordToggleButtonBorder.Opacity = RecordToggleButtonBorder.IsHitTestVisible ? 1 : 0.54;
RecordDot.IsVisible = snapshot.State == AudioRecorderRuntimeState.Ready;
PauseGlyphPath.IsVisible = snapshot.State == AudioRecorderRuntimeState.Recording;
PlayGlyphPath.IsVisible = snapshot.State == AudioRecorderRuntimeState.Paused;
if (!isSupported)
{
HintTextBlock.Text = L("recording.widget.hint.unsupported", "Microphone is unavailable");
return;
}
if (snapshot.State == AudioRecorderRuntimeState.Recording)
{
HintTextBlock.Text = L("recording.widget.hint.recording", "Recording");
return;
}
if (snapshot.State == AudioRecorderRuntimeState.Paused)
{
HintTextBlock.Text = L("recording.widget.hint.paused", "Paused");
return;
}
if (snapshot.State == AudioRecorderRuntimeState.Error)
{
HintTextBlock.Text = string.IsNullOrWhiteSpace(snapshot.LastError)
? L("recording.widget.hint.error", "Recording failed")
: snapshot.LastError;
return;
}
if (!string.IsNullOrWhiteSpace(snapshot.LastSavedFilePath) &&
!string.Equals(snapshot.LastSavedFilePath, _lastSavedFilePath, StringComparison.OrdinalIgnoreCase))
{
_lastSavedFilePath = snapshot.LastSavedFilePath;
}
if (!string.IsNullOrWhiteSpace(_lastSavedFilePath))
{
var fileName = Path.GetFileName(_lastSavedFilePath);
HintTextBlock.Text = string.Format(
CultureInfo.InvariantCulture,
L("recording.widget.hint.saved_format", "Saved {0}"),
fileName);
return;
}
HintTextBlock.Text = L("recording.widget.hint.ready", "Tap red button to record");
}
private void InitializeWaveBars()
{
if (_waveBars.Count > 0)
{
return;
}
for (var i = 0; i < WaveBarCount; i++)
{
var bar = new Border
{
Width = 3,
Height = 6,
CornerRadius = new CornerRadius(1.5),
Background = CreateBrush("#121722"),
Opacity = 0.24,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};
_waveBars.Add(bar);
WaveformBarsPanel.Children.Add(bar);
}
}
private void PushWaveLevel(double level)
{
for (var i = 0; i < _waveLevels.Length - 1; i++)
{
_waveLevels[i] = _waveLevels[i + 1];
}
var previous = _waveLevels[^2];
var target = Math.Clamp(level, 0, 1);
_waveLevels[^1] = Math.Clamp((previous * 0.35) + (target * 0.65), 0, 1);
}
private void UpdateWaveformVisual()
{
var scale = ResolveScale();
var barWidth = Math.Clamp(3 * scale, 2, 5);
for (var i = 0; i < _waveBars.Count; i++)
{
var bar = _waveBars[i];
var eased = Math.Pow(Math.Clamp(_waveLevels[i], 0, 1), 0.62);
bar.Width = barWidth;
bar.Height = Math.Clamp((4 + (eased * 30)) * scale, 3, 46);
bar.CornerRadius = new CornerRadius(Math.Clamp(barWidth / 2d, 1, 3));
bar.Opacity = Math.Clamp(0.20 + (eased * 0.82), 0.20, 1.0);
}
}
private void ReloadLanguageCode()
{
try
{
var snapshot = _settingsService.Load();
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
}
catch
{
_languageCode = "zh-CN";
}
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
private double ResolveScale()
{
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.60, 2.0);
var widthScale = Bounds.Width > 1
? Math.Clamp(Bounds.Width / Math.Max(1, _currentCellSize * 2), 0.60, 2.0)
: 1;
var heightScale = Bounds.Height > 1
? Math.Clamp(Bounds.Height / Math.Max(1, _currentCellSize * 2), 0.60, 2.0)
: 1;
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale) * 1.02), 0.58, 2.04);
}
private static string FormatDuration(TimeSpan duration)
{
if (duration.TotalHours >= 1)
{
return duration.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture);
}
return duration.ToString(@"mm\:ss", CultureInfo.InvariantCulture);
}
private static IBrush CreateBrush(string colorHex)
{
return new SolidColorBrush(Color.Parse(colorHex));
}
}

View File

@@ -2,7 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:fi="using:FluentIcons.Avalonia"
mc:Ignorable="d"
d:DesignWidth="260"
d:DesignHeight="120"
@@ -46,12 +45,11 @@
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis" />
<fi:SymbolIcon x:Name="WeatherIconSymbol"
Symbol="WeatherPartlyCloudyDay"
FontSize="18"
Foreground="#5A9CFF"
VerticalAlignment="Center"
IconVariant="Regular" />
<Image x:Name="WeatherIconImage"
Width="18"
Height="18"
VerticalAlignment="Center"
Stretch="Uniform" />
</StackPanel>
</StackPanel>

View File

@@ -8,7 +8,6 @@ using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;
using FluentIcons.Common;
using LanMontainDesktop.Models;
using LanMontainDesktop.Services;
@@ -54,7 +53,6 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
private bool _isRefreshing;
private bool? _isNightModeApplied;
private string _languageCode = "zh-CN";
private Symbol _activeWeatherSymbol = Symbol.WeatherPartlyCloudyDay;
private HyperOS3WeatherVisualKind _activeVisualKind = HyperOS3WeatherVisualKind.CloudyDay;
public WeatherClockWidget()
@@ -173,7 +171,9 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
var leftWidthFactor = Math.Clamp(leftContentWidth / 122d, 0.48, 1.35);
TimeTextBlock.FontSize = Math.Clamp((metrics.PrimaryTemperatureFont * 0.74) * scale * compactFactor * leftWidthFactor, 10, 62);
DateTextBlock.FontSize = Math.Clamp(metrics.SecondaryTextFont * scale * compactFactor * leftWidthFactor, 8, 30);
WeatherIconSymbol.FontSize = Math.Clamp(metrics.IconFont * scale * compactFactor * leftWidthFactor, 9, 32);
var weatherIconSize = Math.Clamp(metrics.IconFont * scale * compactFactor * leftWidthFactor, 9, 32);
WeatherIconImage.Width = weatherIconSize;
WeatherIconImage.Height = weatherIconSize;
TimeTextBlock.FontWeight = ToVariableWeight(Lerp(620, 760, Math.Clamp((scale - 0.68) / 1.35, 0, 1)));
DateTextBlock.FontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.68) / 1.35, 0, 1)));
@@ -185,10 +185,10 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
var showDateLine = leftContentWidth >= Math.Max(40, TimeTextBlock.FontSize * 1.72);
DateWeatherStack.IsVisible = showDateLine;
WeatherIconSymbol.IsVisible = showDateLine && leftContentWidth >= Math.Max(56, DateTextBlock.FontSize * 2.4);
WeatherIconImage.IsVisible = showDateLine && leftContentWidth >= Math.Max(56, DateTextBlock.FontSize * 2.4);
var dateReservedWidth = WeatherIconSymbol.IsVisible
? WeatherIconSymbol.FontSize + DateWeatherStack.Spacing
var dateReservedWidth = WeatherIconImage.IsVisible
? weatherIconSize + DateWeatherStack.Spacing
: 0;
DateTextBlock.MaxWidth = Math.Max(12, leftContentWidth - dateReservedWidth);
@@ -312,18 +312,16 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
{
var isNight = ResolveIsNight(snapshot);
_activeVisualKind = HyperOS3WeatherTheme.ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
_activeWeatherSymbol = HyperOS3WeatherTheme.ResolveWeatherSymbol(_activeVisualKind);
WeatherIconSymbol.Symbol = _activeWeatherSymbol;
WeatherIconSymbol.Foreground = CreateBrush(HyperOS3WeatherTheme.ResolveIconAccent(_activeVisualKind, _activeWeatherSymbol));
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(_activeVisualKind));
}
private void ApplyDefaultWeatherIcon()
{
var isNight = IsNightNow();
_activeVisualKind = isNight ? HyperOS3WeatherVisualKind.ClearNight : HyperOS3WeatherVisualKind.CloudyDay;
_activeWeatherSymbol = HyperOS3WeatherTheme.ResolveWeatherSymbol(_activeVisualKind);
WeatherIconSymbol.Symbol = _activeWeatherSymbol;
WeatherIconSymbol.Foreground = CreateBrush(HyperOS3WeatherTheme.ResolveIconAccent(_activeVisualKind, _activeWeatherSymbol));
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(_activeVisualKind));
}
private void UpdateClockVisual()
@@ -430,7 +428,6 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
CenterDotInner.Fill = CreateBrush("#1A74F2");
BuildTicks(isNightMode);
WeatherIconSymbol.Foreground = CreateBrush(HyperOS3WeatherTheme.ResolveIconAccent(_activeVisualKind, _activeWeatherSymbol));
}
private WeatherClockConfig LoadConfig()

View File

@@ -9,16 +9,16 @@
x:Class="LanMontainDesktop.Views.Components.WeatherWidget">
<Border x:Name="RootBorder"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Background="#68A9EC">
<Grid>
<Border x:Name="BackgroundImageLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True" />
<Border x:Name="BackgroundMotionLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Opacity="0.24"
RenderTransformOrigin="0.5,0.5">
@@ -32,12 +32,12 @@
</Border>
<Border x:Name="BackgroundTintLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Opacity="0.20" />
<Border x:Name="BackgroundLightLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Opacity="0.66">
<Border.Background>
@@ -54,7 +54,7 @@
</Border>
<Border x:Name="BackgroundShadeLayer"
CornerRadius="28"
CornerRadius="30"
ClipToBounds="True"
Opacity="0.78">
<Border.Background>
@@ -73,76 +73,91 @@
ClipToBounds="True" />
<Border x:Name="ContentPaddingBorder"
Padding="18"
Padding="16"
Background="Transparent">
<Viewbox Stretch="Uniform">
<Grid x:Name="LayoutRoot"
Width="300"
Height="300">
<Grid x:Name="ContentGrid"
RowDefinitions="Auto,*,Auto"
RowSpacing="8">
<Grid x:Name="TopRowGrid"
Grid.Row="0"
ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="8">
<fi:SymbolIcon x:Name="LocationIcon"
Symbol="Location"
FontSize="20"
VerticalAlignment="Center" />
<TextBlock x:Name="CityTextBlock"
Grid.Column="1"
Text="Beijing"
FontSize="30"
FontWeight="SemiBold"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<fi:SymbolIcon x:Name="WeatherIconSymbol"
Grid.Column="2"
Symbol="WeatherSunny"
IconVariant="Regular"
FontSize="40"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
</Grid>
<Grid x:Name="LayoutRoot">
<Grid x:Name="ContentGrid"
RowDefinitions="Auto,*,Auto"
RowSpacing="2">
<Grid x:Name="TopRowGrid"
Grid.Row="0"
ColumnDefinitions="Auto,*">
<TextBlock x:Name="TemperatureTextBlock"
Grid.Row="1"
Text="26"
FontSize="108"
FontWeight="Bold"
Grid.Column="0"
Text="26°"
FontSize="96"
FontWeight="Light"
FontFeatures="tnum"
VerticalAlignment="Center"
Margin="0,4,0,10"
VerticalAlignment="Top"
Margin="0,-1,0,0"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<StackPanel x:Name="BottomInfoStack"
Grid.Row="2"
VerticalAlignment="Bottom"
Spacing="4"
Margin="0,0,0,10">
<Image x:Name="WeatherIconImage"
Grid.Column="1"
Width="76"
Height="76"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Stretch="Uniform" />
</Grid>
<Border x:Name="ConditionInfoBadge"
Grid.Row="1"
Background="Transparent"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Padding="0">
<StackPanel Orientation="Vertical"
Spacing="0"
VerticalAlignment="Center">
<TextBlock x:Name="ConditionTextBlock"
Text="Clear"
FontSize="30"
FontSize="44"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<TextBlock x:Name="RangeTextBlock"
Text="20 / 28"
FontSize="36"
Text="20°/28°"
FontSize="46"
FontWeight="SemiBold"
FontFeatures="tnum"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</StackPanel>
</Grid>
</Border>
<StackPanel x:Name="BottomInfoStack"
Grid.Row="2"
VerticalAlignment="Bottom"
Spacing="0"
Margin="0,0,0,1">
<Border x:Name="CityInfoBadge"
Background="#24FFFFFF"
CornerRadius="13"
Padding="10,5"
HorizontalAlignment="Left">
<StackPanel Orientation="Horizontal"
Spacing="6"
VerticalAlignment="Center">
<fi:SymbolIcon x:Name="LocationIcon"
Symbol="Location"
FontSize="14"
IsVisible="False"
VerticalAlignment="Center" />
<TextBlock x:Name="CityTextBlock"
Text="Beijing"
FontSize="23"
FontWeight="Medium"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</StackPanel>
</Border>
</StackPanel>
</Grid>
</Viewbox>
</Grid>
</Border>
</Grid>
</Border>

View File

@@ -11,7 +11,6 @@ using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using FluentIcons.Common;
using LanMontainDesktop.Models;
using LanMontainDesktop.Services;
@@ -154,9 +153,9 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
_currentCellSize = Math.Max(1, cellSize);
var scale = ResolveScale();
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Realtime2x2);
var cornerRadius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 24, 44);
var horizontalPadding = Math.Clamp(_currentCellSize * metrics.HorizontalPaddingScale, 12, 24);
var verticalPadding = Math.Clamp(_currentCellSize * metrics.VerticalPaddingScale, 12, 24);
var cornerRadius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 26, 46);
var horizontalPadding = Math.Clamp(_currentCellSize * metrics.HorizontalPaddingScale, 10, 24);
var verticalPadding = Math.Clamp(_currentCellSize * metrics.VerticalPaddingScale, 10, 24);
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
BackgroundImageLayer.CornerRadius = new CornerRadius(cornerRadius);
@@ -165,8 +164,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
BackgroundLightLayer.CornerRadius = new CornerRadius(cornerRadius);
BackgroundShadeLayer.CornerRadius = new CornerRadius(cornerRadius);
ContentPaddingBorder.Padding = new Thickness(
Math.Clamp(horizontalPadding * scale, 12, 24),
Math.Clamp(verticalPadding * scale, 12, 24));
Math.Clamp(horizontalPadding * scale, 10, 24),
Math.Clamp(verticalPadding * scale, 10, 24));
ApplyAdaptiveTypography();
ResetParticles();
}
@@ -389,7 +388,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
CityTextBlock.Text = ResolvePreciseDisplayLocation(rawLocation, _languageCode, L("weather.widget.location_unknown", "Unknown location"));
ConditionTextBlock.Text = ResolveWeatherConditionText(snapshot.Current.WeatherText, visualKind);
WeatherIconSymbol.Symbol = ResolveWeatherSymbol(visualKind);
SetWeatherIcon(visualKind);
SetLoadingSkeleton(false);
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
var (low, high) = ResolveTemperatureRange(snapshot);
@@ -401,9 +401,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
{
var fallbackKind = ResolveFallbackVisualKind();
ApplyVisualTheme(fallbackKind);
WeatherIconSymbol.Symbol = fallbackKind == WeatherVisualKind.ClearNight
? Symbol.WeatherMoon
: Symbol.WeatherSunny;
SetWeatherIcon(fallbackKind);
SetLoadingSkeleton(false);
CityTextBlock.Text = L("weather.widget.location_not_configured", "Weather location is not configured");
ConditionTextBlock.Text = L("weather.widget.configure_hint", "Open Settings > Weather to configure");
TemperatureTextBlock.Text = "--°";
@@ -416,9 +415,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
{
var loadingKind = IsNightNow() ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay;
ApplyVisualTheme(loadingKind);
WeatherIconSymbol.Symbol = loadingKind == WeatherVisualKind.CloudyNight
? Symbol.WeatherPartlyCloudyNight
: Symbol.WeatherPartlyCloudyDay;
SetWeatherIcon(loadingKind);
SetLoadingSkeleton(true);
CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName,
_languageCode,
@@ -432,7 +430,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
private void ApplyFailedState(string locationName)
{
ApplyVisualTheme(WeatherVisualKind.Fog);
WeatherIconSymbol.Symbol = Symbol.WeatherFog;
SetWeatherIcon(WeatherVisualKind.Fog);
SetLoadingSkeleton(false);
CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName,
_languageCode,
@@ -454,14 +453,15 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint);
var primary = CreateSolidBrush(palette.PrimaryText);
var secondary = CreateSolidBrush(palette.SecondaryText);
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
var secondary = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xEA : (byte)0xDC);
var cityBrush = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xD8 : (byte)0xC8);
var particleBrush = ResolveParticleBrush(ToThemeKind(kind), palette.ParticleColor);
LocationIcon.Foreground = primary;
CityTextBlock.Foreground = primary;
CityTextBlock.Foreground = cityBrush;
TemperatureTextBlock.Foreground = primary;
WeatherIconSymbol.Foreground = primary;
ConditionTextBlock.Foreground = secondary;
RangeTextBlock.Foreground = secondary;
RangeTextBlock.Foreground = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xE0 : (byte)0xD4);
foreach (var particle in _particleVisuals)
{
@@ -572,11 +572,6 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
palette.ParticleColor);
}
private static Symbol ResolveWeatherSymbol(WeatherVisualKind kind)
{
return HyperOS3WeatherTheme.ResolveWeatherSymbol(ToThemeKind(kind));
}
private static HyperOS3WeatherVisualKind ToThemeKind(WeatherVisualKind kind)
{
return kind switch
@@ -632,14 +627,14 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
{
if (!low.HasValue && !high.HasValue)
{
return L("weather.widget.range_unknown", "-- / --");
return L("weather.widget.range_unknown", "--/--");
}
var lowText = FormatTemperature(low);
var highText = FormatTemperature(high);
return string.Format(
GetUiCulture(),
L("weather.widget.range_format", "{0} / {1}"),
L("weather.widget.range_format", "{0}/{1}"),
lowText,
highText);
}
@@ -800,31 +795,46 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
private void ApplyAdaptiveTypography()
{
var scale = ResolveScale();
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Realtime2x2);
var densityBoost = scale <= 0.70 ? 0.88 : scale <= 0.88 ? 0.94 : scale >= 1.45 ? 1.06 : 1.0;
var cityLength = Math.Max(1, CityTextBlock.Text?.Length ?? 2);
var cityCompression = cityLength >= 10 ? 0.72 : cityLength >= 7 ? 0.83 : cityLength >= 5 ? 0.92 : 1.0;
var conditionLength = Math.Max(1, ConditionTextBlock.Text?.Length ?? 2);
var conditionCompression = conditionLength >= 9 ? 0.84 : conditionLength >= 6 ? 0.92 : 1.0;
var width = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 2;
var height = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 2;
var innerWidth = Math.Max(90, width - ContentPaddingBorder.Padding.Left - ContentPaddingBorder.Padding.Right);
var innerHeight = Math.Max(90, height - ContentPaddingBorder.Padding.Top - ContentPaddingBorder.Padding.Bottom);
var scaleX = innerWidth / 288d;
var scaleY = innerHeight / 288d;
var uiScale = Math.Clamp(Math.Min(scaleX, scaleY), 0.62, 1.58);
var verticalScale = Math.Clamp(scaleY, 0.58, 1.70);
ContentGrid.RowSpacing = Math.Clamp(metrics.MainGap * scale, 4, 14);
TopRowGrid.ColumnSpacing = Math.Clamp(metrics.MainGap * scale, 4, 12);
BottomInfoStack.Spacing = Math.Clamp(metrics.SectionGap * scale, 2, 8);
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(10 * scale, 4, 16));
ContentGrid.RowSpacing = Math.Clamp(2 * verticalScale, 1, 5);
TopRowGrid.ColumnSpacing = Math.Clamp(8 * uiScale, 4, 14);
BottomInfoStack.Spacing = 0;
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(2 * uiScale, 0, 4));
LocationIcon.FontSize = Math.Clamp((metrics.IconFont * 0.50) * scale * densityBoost, 10, 30);
CityTextBlock.FontSize = Math.Clamp(metrics.PrimaryTextFont * scale * cityCompression * densityBoost, 12, 42);
WeatherIconSymbol.FontSize = Math.Clamp(metrics.IconFont * scale * densityBoost, 14, 56);
TemperatureTextBlock.FontSize = Math.Clamp(metrics.PrimaryTemperatureFont * scale * densityBoost, 36, 144);
ConditionTextBlock.FontSize = Math.Clamp(metrics.PrimaryTextFont * scale * conditionCompression * densityBoost, 11, 44);
RangeTextBlock.FontSize = Math.Clamp(metrics.SecondaryTextFont * scale * densityBoost, 12, 50);
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(4 * scale, 1, 8), 0, Math.Clamp(10 * scale, 4, 16));
var iconSize = Math.Clamp(74 * uiScale, 46, 96);
WeatherIconImage.Width = iconSize;
WeatherIconImage.Height = iconSize;
CityTextBlock.FontWeight = ToVariableWeight(Lerp(560, 700, Math.Clamp((scale - 0.58) / 1.3, 0, 1)));
TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(620, 800, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(560, 700, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
RangeTextBlock.FontWeight = ToVariableWeight(Lerp(560, 700, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
TemperatureTextBlock.FontSize = Math.Clamp(92 * uiScale, 60, 132);
TemperatureTextBlock.FontWeight = ToVariableWeight(320);
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-2 * uiScale, -5, 0), 0, 0);
TemperatureTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.50, 96, 176);
ConditionInfoBadge.Padding = new Thickness(0);
ConditionInfoBadge.CornerRadius = new CornerRadius(Math.Clamp(10 * uiScale, 6, 14));
ConditionTextBlock.FontSize = Math.Clamp(44 * uiScale, 22, 58);
RangeTextBlock.FontSize = Math.Clamp(46 * uiScale, 24, 62);
ConditionTextBlock.FontWeight = ToVariableWeight(610);
RangeTextBlock.FontWeight = ToVariableWeight(620);
ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.62, 92, 204);
RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.66, 100, 224);
CityInfoBadge.Padding = new Thickness(
Math.Clamp(10 * uiScale, 6, 14),
Math.Clamp(5 * uiScale, 2, 8));
CityInfoBadge.CornerRadius = new CornerRadius(Math.Clamp(13 * uiScale, 8, 18));
LocationIcon.FontSize = Math.Clamp(14 * uiScale, 10, 20);
CityTextBlock.FontSize = Math.Clamp(23 * uiScale, 14, 34);
CityTextBlock.FontWeight = ToVariableWeight(560);
CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.56, 70, 196);
}
private static double Lerp(double from, double to, double t)
@@ -832,6 +842,18 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
return from + ((to - from) * t);
}
private void SetWeatherIcon(WeatherVisualKind kind)
{
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(kind)));
}
private void SetLoadingSkeleton(bool isLoading)
{
CityInfoBadge.Background = isLoading ? CreateSolidBrush("#24FFFFFF") : Brushes.Transparent;
ConditionInfoBadge.Background = isLoading ? CreateSolidBrush("#1FFFFFFF") : Brushes.Transparent;
}
private static FontWeight ToVariableWeight(double weight)
{
return (FontWeight)(int)Math.Clamp(Math.Round(weight), 1, 1000);
@@ -1112,6 +1134,12 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
return new SolidColorBrush(Color.Parse(colorHex));
}
private static IBrush CreateSolidBrush(string colorHex, byte alpha)
{
var color = Color.Parse(colorHex);
return new SolidColorBrush(Color.FromArgb(alpha, color.R, color.G, color.B));
}
private static IBrush CreateGradientBrush(string fromColorHex, string toColorHex)
{
return new LinearGradientBrush