mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-26 20:24:26 +08:00
0.2.6
媒体播放组件,录音组件
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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="北京"
|
||||
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="晴"
|
||||
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°"
|
||||
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="空气优 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="今天"
|
||||
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° / 28°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime1"
|
||||
Text="明天"
|
||||
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° / 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="周六"
|
||||
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° / 28°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="3"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime3"
|
||||
Text="周日"
|
||||
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° / 28°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="4"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime4"
|
||||
Text="周一"
|
||||
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° / 28°"
|
||||
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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
255
LanMontainDesktop/Views/Components/MusicControlWidget.axaml
Normal file
255
LanMontainDesktop/Views/Components/MusicControlWidget.axaml
Normal 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>
|
||||
407
LanMontainDesktop/Views/Components/MusicControlWidget.axaml.cs
Normal file
407
LanMontainDesktop/Views/Components/MusicControlWidget.axaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
170
LanMontainDesktop/Views/Components/RecordingWidget.axaml
Normal file
170
LanMontainDesktop/Views/Components/RecordingWidget.axaml
Normal 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>
|
||||
338
LanMontainDesktop/Views/Components/RecordingWidget.axaml.cs
Normal file
338
LanMontainDesktop/Views/Components/RecordingWidget.axaml.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user