修复
This commit is contained in:
lincube
2026-03-05 12:34:39 +08:00
parent 00694e715f
commit 469f7e1132
46 changed files with 1535 additions and 344 deletions

View File

@@ -23,7 +23,23 @@
Background="#FFFFFFFF"
BorderBrush="#22000000"
BorderThickness="1">
<webview:WebView x:Name="BrowserWebView" />
<Grid>
<webview:WebView x:Name="BrowserWebView" />
<Border x:Name="UnavailableOverlay"
IsVisible="False"
Background="#CC0F172A"
Padding="16">
<TextBlock x:Name="UnavailableMessageTextBlock"
Foreground="#F8FAFC"
TextWrapping="Wrap"
TextAlignment="Center"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MaxWidth="360"
FontSize="13"
Text="Browser runtime unavailable." />
</Border>
</Grid>
</Border>
<Border x:Name="AddressBarBorder"

View File

@@ -6,6 +6,7 @@ using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Styling;
using AvaloniaWebView;
using LanMountainDesktop.Services;
using WebViewCore.Events;
namespace LanMountainDesktop.Views.Components;
@@ -20,6 +21,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
private bool _isOnActiveDesktopPage;
private bool _isEditMode;
private bool _isWebViewActive = true;
private readonly WebView2RuntimeAvailability _runtimeAvailability;
public BrowserWidget()
{
@@ -31,7 +33,17 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
ApplyCellSize(_currentCellSize);
ApplyTheme(force: true);
BrowserWebView.NavigationStarting += OnBrowserWebViewNavigationStarting;
_runtimeAvailability = WebView2RuntimeProbe.GetAvailability();
if (_runtimeAvailability.IsAvailable)
{
BrowserWebView.NavigationStarting += OnBrowserWebViewNavigationStarting;
}
else
{
ApplyRuntimeUnavailableState();
}
UpdateWebViewActiveState();
NavigateTo(DefaultHomeUri);
}
@@ -169,6 +181,11 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
private void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
{
if (!_runtimeAvailability.IsAvailable)
{
return;
}
if (!_isWebViewActive)
{
return;
@@ -185,11 +202,21 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
private void OnGoButtonClick(object? sender, RoutedEventArgs e)
{
if (!_runtimeAvailability.IsAvailable)
{
return;
}
NavigateFromAddressBar();
}
private void OnAddressTextBoxKeyDown(object? sender, KeyEventArgs e)
{
if (!_runtimeAvailability.IsAvailable)
{
return;
}
if (e.Key != Key.Enter)
{
return;
@@ -201,6 +228,11 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
private void NavigateFromAddressBar()
{
if (!_runtimeAvailability.IsAvailable)
{
return;
}
var target = TryNormalizeUri(AddressTextBox.Text);
if (target is null)
{
@@ -240,6 +272,15 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
private void UpdateWebViewActiveState()
{
if (!_runtimeAvailability.IsAvailable)
{
_isWebViewActive = false;
BrowserWebView.Url = null;
BrowserWebView.IsVisible = false;
BrowserWebView.IsHitTestVisible = false;
return;
}
var shouldBeActive = _isOnActiveDesktopPage && !_isEditMode && IsVisible;
if (_isWebViewActive == shouldBeActive)
{
@@ -265,6 +306,24 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
BrowserWebView.Url = _lastKnownUri;
}
private void ApplyRuntimeUnavailableState()
{
_isWebViewActive = false;
BrowserWebView.Url = null;
BrowserWebView.IsVisible = false;
BrowserWebView.IsHitTestVisible = false;
RefreshButton.IsEnabled = false;
GoButton.IsEnabled = false;
AddressTextBox.IsEnabled = false;
AddressTextBox.Text = string.Empty;
UnavailableMessageTextBlock.Text = string.IsNullOrWhiteSpace(_runtimeAvailability.Message)
? "WebView runtime unavailable."
: _runtimeAvailability.Message;
UnavailableOverlay.IsVisible = true;
}
private static Uri? TryNormalizeUri(string? rawText)
{
if (string.IsNullOrWhiteSpace(rawText))

View File

@@ -0,0 +1,54 @@
<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="420"
d:DesignHeight="280"
x:Class="LanMountainDesktop.Views.Components.DailyArtworkSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<StackPanel Spacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="每日图片设置"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Text="切换每日图片的数据源。"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="10">
<StackPanel Spacing="8">
<TextBlock x:Name="MirrorSourceLabelTextBlock"
Text="镜像源"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="MirrorSourceComboBox"
Width="240"
SelectionChanged="OnMirrorSourceSelectionChanged">
<ComboBoxItem x:Name="MirrorSourceDomesticItem"
Tag="Domestic"
Content="国内镜像" />
<ComboBoxItem x:Name="MirrorSourceOverseasItem"
Tag="Overseas"
Content="国外镜像" />
</ComboBox>
</StackPanel>
</Border>
<TextBlock x:Name="StatusTextBlock"
Text="当前源:国内镜像"
FontSize="11"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
</StackPanel>
</Border>
</UserControl>

View File

@@ -0,0 +1,97 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class DailyArtworkSettingsWindow : UserControl
{
private readonly AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new();
private string _languageCode = "zh-CN";
private bool _suppressEvents;
public event EventHandler? SettingsChanged;
public string CurrentSource => GetSelectedSource();
public DailyArtworkSettingsWindow()
{
InitializeComponent();
LoadState();
ApplyLocalization();
}
private void LoadState()
{
var snapshot = _appSettingsService.Load();
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
var source = DailyArtworkMirrorSources.Normalize(snapshot.DailyArtworkMirrorSource);
_suppressEvents = true;
MirrorSourceComboBox.SelectedIndex = string.Equals(source, DailyArtworkMirrorSources.Domestic, StringComparison.OrdinalIgnoreCase)
? 0
: 1;
_suppressEvents = false;
UpdateSourceStatus(source);
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("artwork.settings.title", "每日图片设置");
DescriptionTextBlock.Text = L("artwork.settings.desc", "切换每日图片的数据源。");
MirrorSourceLabelTextBlock.Text = L("artwork.settings.source_label", "镜像源");
MirrorSourceDomesticItem.Content = L("artwork.settings.source_domestic", "国内镜像");
MirrorSourceOverseasItem.Content = L("artwork.settings.source_overseas", "国外镜像");
UpdateSourceStatus(GetSelectedSource());
}
private void OnMirrorSourceSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
var source = GetSelectedSource();
var snapshot = _appSettingsService.Load();
snapshot.DailyArtworkMirrorSource = source;
_appSettingsService.Save(snapshot);
UpdateSourceStatus(source);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private string GetSelectedSource()
{
if (MirrorSourceComboBox.SelectedItem is ComboBoxItem comboBoxItem &&
comboBoxItem.Tag is string tagValue)
{
return DailyArtworkMirrorSources.Normalize(tagValue);
}
return DailyArtworkMirrorSources.Overseas;
}
private void UpdateSourceStatus(string source)
{
if (StatusTextBlock is null)
{
return;
}
StatusTextBlock.Text = string.Equals(source, DailyArtworkMirrorSources.Domestic, StringComparison.OrdinalIgnoreCase)
? L("artwork.settings.source_status_domestic", "当前源:国内镜像(优先中国网络)")
: L("artwork.settings.source_status_overseas", "当前源:国外镜像(艺术馆推荐)");
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
}

View File

@@ -122,6 +122,15 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
}
}
public void RefreshFromSettings()
{
_recommendationService.ClearCache();
if (_isAttached)
{
_ = RefreshArtworkAsync(forceRefresh: true);
}
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = true;
@@ -219,7 +228,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
UpdateAdaptiveLayout();
var bitmap = await TryLoadArtworkBitmapAsync(snapshot.ImageUrl, cancellationToken);
var bitmap = await TryLoadArtworkBitmapAsync(snapshot.ImageUrl, snapshot.ThumbnailDataUrl, cancellationToken);
if (cancellationToken.IsCancellationRequested || !_isAttached)
{
bitmap?.Dispose();
@@ -229,35 +238,118 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
SetArtworkBitmap(bitmap);
}
private static async Task<Bitmap?> TryLoadArtworkBitmapAsync(string? imageUrl, CancellationToken cancellationToken)
private static async Task<Bitmap?> TryLoadArtworkBitmapAsync(
string? imageUrl,
string? thumbnailDataUrl,
CancellationToken cancellationToken)
{
foreach (var candidateUrl in BuildImageUrlCandidates(imageUrl))
{
var remoteBitmap = await TryDownloadBitmapAsync(candidateUrl, cancellationToken);
if (remoteBitmap is not null)
{
return remoteBitmap;
}
}
return TryDecodeBitmapFromDataUrl(thumbnailDataUrl);
}
private static IEnumerable<string> BuildImageUrlCandidates(string? imageUrl)
{
if (string.IsNullOrWhiteSpace(imageUrl))
{
yield break;
}
var normalizedUrl = imageUrl.Trim();
yield return normalizedUrl;
const string preferredSizeSegment = "/full/843,/0/default.jpg";
if (normalizedUrl.Contains(preferredSizeSegment, StringComparison.OrdinalIgnoreCase))
{
yield return normalizedUrl.Replace(
preferredSizeSegment,
"/full/1024,/0/default.jpg",
StringComparison.OrdinalIgnoreCase);
}
}
private static async Task<Bitmap?> TryDownloadBitmapAsync(string imageUrl, CancellationToken cancellationToken)
{
var withReferrer = await SendImageRequestAsync(imageUrl, includeReferrer: true, cancellationToken);
if (withReferrer is not null)
{
return withReferrer;
}
return await SendImageRequestAsync(imageUrl, includeReferrer: false, cancellationToken);
}
private static async Task<Bitmap?> SendImageRequestAsync(
string imageUrl,
bool includeReferrer,
CancellationToken cancellationToken)
{
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, imageUrl);
request.Headers.TryAddWithoutValidation("User-Agent", BrowserUserAgent);
request.Headers.TryAddWithoutValidation("Accept", "image/avif,image/webp,image/apng,image/*,*/*;q=0.8");
if (includeReferrer && Uri.TryCreate(imageUrl, UriKind.Absolute, out var imageUri))
{
request.Headers.Referrer = new Uri($"{imageUri.Scheme}://{imageUri.Host}/", UriKind.Absolute);
}
using var response = await ImageHttpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
if (!response.IsSuccessStatusCode)
{
return null;
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
var memory = new MemoryStream();
await stream.CopyToAsync(memory, cancellationToken);
memory.Position = 0;
return new Bitmap(memory);
}
catch (OperationCanceledException)
{
throw;
}
catch
{
return null;
}
}
using var request = new HttpRequestMessage(HttpMethod.Get, imageUrl.Trim());
request.Headers.TryAddWithoutValidation("User-Agent", BrowserUserAgent);
request.Headers.TryAddWithoutValidation("Accept", "image/avif,image/webp,image/apng,image/*,*/*;q=0.8");
if (Uri.TryCreate(imageUrl.Trim(), UriKind.Absolute, out var imageUri))
{
request.Headers.Referrer = new Uri($"{imageUri.Scheme}://{imageUri.Host}/", UriKind.Absolute);
}
using var response = await ImageHttpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
if (!response.IsSuccessStatusCode)
private static Bitmap? TryDecodeBitmapFromDataUrl(string? dataUrl)
{
if (string.IsNullOrWhiteSpace(dataUrl))
{
return null;
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
var memory = new MemoryStream();
await stream.CopyToAsync(memory, cancellationToken);
memory.Position = 0;
return new Bitmap(memory);
var trimmed = dataUrl.Trim();
var markerIndex = trimmed.IndexOf("base64,", StringComparison.OrdinalIgnoreCase);
if (markerIndex < 0 || markerIndex + 7 >= trimmed.Length)
{
return null;
}
var base64Payload = trimmed[(markerIndex + 7)..];
try
{
var bytes = Convert.FromBase64String(base64Payload);
return new Bitmap(new MemoryStream(bytes));
}
catch
{
return null;
}
}
private void ApplyLoadingState()

View File

@@ -88,7 +88,7 @@
FontFeatures="tnum"
VerticalAlignment="Center"
Margin="0,-2,0,0"
TextTrimming="CharacterEllipsis"
TextTrimming="None"
MaxLines="1" />
<Grid x:Name="SummaryInfoGrid"

View File

@@ -109,7 +109,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
RangeTextBlock.MaxLines = 1;
TemperatureTextBlock.TextWrapping = TextWrapping.NoWrap;
TemperatureTextBlock.TextTrimming = TextTrimming.CharacterEllipsis;
TemperatureTextBlock.TextTrimming = TextTrimming.None;
TemperatureTextBlock.MaxLines = 1;
}
@@ -126,9 +126,11 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
BackgroundTintLayer.CornerRadius = new CornerRadius(radius);
BackgroundLightLayer.CornerRadius = new CornerRadius(radius);
BackgroundShadeLayer.CornerRadius = new CornerRadius(radius);
var horizontalPadding = Math.Clamp(Math.Min(width * metrics.HorizontalPaddingScale * 0.30, width * 0.11), 4, 34);
var verticalPadding = Math.Clamp(Math.Min(height * metrics.VerticalPaddingScale * 0.30, height * 0.11), 4, 34);
ContentPaddingBorder.Padding = new Thickness(
Math.Clamp(width * metrics.HorizontalPaddingScale * 0.30, 10, 30),
Math.Clamp(height * metrics.VerticalPaddingScale * 0.30, 10, 30));
horizontalPadding,
verticalPadding);
ApplyTypography(width, height);
}
@@ -274,7 +276,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
var kind = HyperOS3WeatherTheme.ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
ApplyVisualTheme(kind);
SetLoadingSkeleton(false);
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveIconAsset(kind));
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveHeroIconAsset(kind));
CityTextBlock.Text = ResolveLocation(snapshot.LocationName, fallbackLocationName);
ConditionTextBlock.Text = ResolveWeatherText(snapshot.Current.WeatherText, kind);
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
@@ -302,7 +304,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
? L("weather.hourly.sunset", "Sunset")
: FormatTemperature(item?.Source.TemperatureC ?? snapshot.Current.TemperatureC);
_hourlyTimeBlocks[i].Text = target.ToString("HH:mm", CultureInfo.InvariantCulture);
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveIconAsset(hourKind));
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(hourKind));
}
var todayDate = DateOnly.FromDateTime(now);
@@ -316,7 +318,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
_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));
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(dayKind));
}
}
@@ -324,7 +326,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
{
ApplyVisualTheme(HyperOS3WeatherVisualKind.CloudyDay);
SetLoadingSkeleton(false);
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
CityTextBlock.Text = L("weather.widget.location_unknown", "Unknown location");
ConditionTextBlock.Text = L("weather.widget.loading", "Loading...");
TemperatureTextBlock.Text = "--°";
@@ -335,7 +337,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
{
_hourlyTempBlocks[i].Text = i == 3 ? L("weather.hourly.sunset", "Sunset") : "--°";
_hourlyTimeBlocks[i].Text = timelineStart.AddHours(i).ToString("HH:mm", CultureInfo.InvariantCulture);
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
}
for (var i = 0; i < _dailyLabelBlocks.Length; i++)
@@ -343,7 +345,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
_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));
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
}
}
@@ -422,78 +424,122 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
private void ApplyTypography(double width, double height)
{
var scale = ResolveScale(width, height);
var compactness = Math.Clamp((0.90 - scale) / 0.55, 0, 1);
LayoutRoot.RowSpacing = Math.Clamp(height * 0.012, 5, 13);
SummaryGrid.ColumnSpacing = Math.Clamp(width * 0.016, 8, 22);
SummaryInfoGrid.RowSpacing = Math.Clamp(height * 0.003, 1, 4);
BottomInfoStack.Spacing = Math.Clamp(2.2 * scale, 1, 6);
ConditionRangeStack.Spacing = Math.Clamp(7 * scale, 4, 13);
HourlyGrid.ColumnSpacing = Math.Clamp(width * 0.007, 3, 10);
DailyGrid.RowSpacing = Math.Clamp(height * 0.009, 4, 10);
TemperatureTextBlock.FontSize = Math.Clamp(height * 0.18, 52, 154);
TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 370, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
var topScaleH = Math.Clamp(height / 640d, 0.62, 2.0);
var topScaleW = Math.Clamp(width / 640d, 0.62, 2.0);
var topScale = Math.Clamp((topScaleH * 0.68) + (topScaleW * 0.32), 0.62, 2.0);
var cityFontSize = Math.Clamp(18 * topScale, 11, 26);
var conditionFontSize = Math.Clamp(19 * topScale, 12, 27);
var rangeFontSize = Math.Clamp(20 * topScale, 12, 30);
var innerWidth = Math.Max(140, width - ContentPaddingBorder.Padding.Left - ContentPaddingBorder.Padding.Right);
var innerHeight = Math.Max(140, height - ContentPaddingBorder.Padding.Top - ContentPaddingBorder.Padding.Bottom);
var fitScale = Math.Clamp(Math.Min(innerWidth / 592d, innerHeight / 600d), 0.30, 3.20);
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.34, 3.80);
var visualScale = Math.Clamp((fitScale * 0.72) + (cellScale * 0.28), 0.30, 3.80);
var emphasis = Math.Clamp((visualScale - 0.82) / 1.90, 0, 1);
LayoutRoot.RowSpacing = Math.Clamp(8 * fitScale, 1, 22);
SummaryGrid.ColumnSpacing = Math.Clamp(16 * fitScale, 4, 38);
SummaryInfoGrid.RowSpacing = Math.Clamp(2 * fitScale, 0.2, 9);
BottomInfoStack.Spacing = Math.Clamp(2 * fitScale, 0.3, 14);
ConditionRangeStack.Spacing = Math.Clamp(9 * fitScale, 1, 24);
HourlyGrid.ColumnSpacing = Math.Clamp(4 * fitScale, 0.5, 22);
var summaryHeight = Math.Clamp(innerHeight * 0.22, 34, Math.Max(34, innerHeight * 0.42));
var hourlyHeight = Math.Clamp(innerHeight * 0.20, 34, Math.Max(34, innerHeight * 0.36));
var separatorBandHeight = Math.Clamp(innerHeight * 0.03, 4, 40);
if (LayoutRoot.RowDefinitions.Count >= 4)
{
LayoutRoot.RowDefinitions[0].Height = new GridLength(summaryHeight, GridUnitType.Pixel);
LayoutRoot.RowDefinitions[1].Height = new GridLength(hourlyHeight, GridUnitType.Pixel);
LayoutRoot.RowDefinitions[2].Height = new GridLength(separatorBandHeight, GridUnitType.Pixel);
LayoutRoot.RowDefinitions[3].Height = new GridLength(1, GridUnitType.Star);
}
var topScale = Math.Clamp(((summaryHeight / 118d) * 0.44) + (visualScale * 0.84), 0.24, 4.00);
var iconGrowth = Math.Clamp((visualScale - 0.88) / 1.70, 0, 1);
var iconScaleBoost = ResolveHeroIconScaleBoost(_activeVisualKind);
var iconSize = Math.Clamp(Lerp(90, 122, iconGrowth) * topScale * iconScaleBoost, 14, 360);
iconSize = Math.Min(iconSize, Math.Max(14, innerWidth * Lerp(0.18, 0.26, iconGrowth)));
var temperatureSample = string.IsNullOrWhiteSpace(TemperatureTextBlock.Text)
? "00°"
: TemperatureTextBlock.Text.Trim();
var temperatureGlyphCount = Math.Clamp(temperatureSample.Length, 3, 6);
var temperatureMaxWidth = Math.Max(30, innerWidth - iconSize - SummaryGrid.ColumnSpacing - 6);
var rawTemperatureSize = Math.Clamp(Lerp(72, 102, iconGrowth) * topScale, 14, 340);
var fitTemperatureSize = temperatureMaxWidth / (temperatureGlyphCount * 0.62);
TemperatureTextBlock.FontSize = Math.Clamp(Math.Min(rawTemperatureSize, fitTemperatureSize), 10, 340);
TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 380, emphasis));
TemperatureTextBlock.MaxWidth = Math.Clamp(temperatureMaxWidth, 30, Math.Max(300, innerWidth * 0.66));
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-2.2 * topScale, -12, 0), 0, 0);
var cityFontSize = Math.Clamp(18.5 * topScale, 7, 86);
var conditionFontSize = Math.Clamp(20 * topScale, 7, 90);
var rangeFontSize = Math.Clamp(20 * topScale, 7, 90);
CityTextBlock.FontSize = cityFontSize;
ConditionTextBlock.FontSize = conditionFontSize;
RangeTextBlock.FontSize = rangeFontSize;
CityTextBlock.FontWeight = ToVariableWeight(540);
ConditionTextBlock.FontWeight = ToVariableWeight(600);
RangeTextBlock.FontWeight = ToVariableWeight(620);
CityTextBlock.FontWeight = ToVariableWeight(Lerp(530, 620, emphasis));
ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(580, 660, emphasis));
RangeTextBlock.FontWeight = ToVariableWeight(Lerp(600, 680, emphasis));
CityTextBlock.LineHeight = cityFontSize * 1.08;
ConditionTextBlock.LineHeight = conditionFontSize * 1.06;
RangeTextBlock.LineHeight = rangeFontSize * 1.06;
var iconSize = Math.Clamp(height * 0.116, 36, 102);
WeatherIconImage.Width = iconSize;
WeatherIconImage.Height = iconSize;
ConditionTextBlock.MaxWidth = Math.Clamp(width * 0.24, 58, 220);
RangeTextBlock.MaxWidth = Math.Clamp(width * 0.30, 88, 270);
CityTextBlock.MaxWidth = Math.Clamp(width * 0.36, 112, 300);
WeatherIconImage.Margin = new Thickness(0, Math.Clamp(-2.4 * topScale, -12, 0), 0, 0);
ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.25, 28, 340);
RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.31, 34, 380);
CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.36, 34, 420);
HourlyPanelBorder.Padding = new Thickness(0);
HourlyPanelBorder.CornerRadius = new CornerRadius(0);
HourlyPanelBorder.Margin = new Thickness(0, Math.Clamp(4 * fitScale, 0, 18), 0, 0);
var hourlyBandHeight = Math.Clamp(height * 0.195, 74, 160);
var hourlyCellWidth = Math.Max(34, (width - HourlyPanelBorder.Padding.Left - HourlyPanelBorder.Padding.Right - (HourlyGrid.ColumnSpacing * 5)) / 6d);
var hourlyTempSize = Math.Clamp(hourlyBandHeight * 0.24, 10, 32);
var hourlyTimeSize = Math.Clamp(hourlyBandHeight * 0.18, 8, 22);
var hourlyIconSize = Math.Clamp(hourlyBandHeight * 0.20, 12, 30);
var hourlyStackSpacing = Math.Clamp(hourlyBandHeight * 0.03, 1, 4);
var hourlyCellWidth = Math.Max(12, (innerWidth - (HourlyGrid.ColumnSpacing * 5)) / 6d);
var hourlyCellScale = Math.Clamp(
Math.Min((visualScale * 0.44) + ((hourlyHeight / 120d) * 0.62), hourlyCellWidth / 76d),
0.22,
3.80);
var hourlyTempSize = Math.Clamp(19 * hourlyCellScale, 6, 72);
var hourlyTimeSize = Math.Clamp(14 * hourlyCellScale, 6, 52);
var hourlyIconSize = Math.Clamp(34 * hourlyCellScale, 8, 114);
var hourlyStackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10);
for (var i = 0; i < _hourlyTempBlocks.Length; i++)
{
_hourlyTempBlocks[i].FontSize = hourlyTempSize;
_hourlyTimeBlocks[i].FontSize = hourlyTimeSize;
_hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(540, 610, 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;
_hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(540, 650, emphasis));
_hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(450, 560, emphasis));
_hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 260);
_hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 260);
_hourlyIconBlocks[i].Width = hourlyIconSize;
_hourlyIconBlocks[i].Height = hourlyIconSize;
if (_hourlyTempBlocks[i].Parent is StackPanel stack) stack.Spacing = hourlyStackSpacing;
}
var dailyLabelSize = Math.Clamp(height * 0.041, 10, 30);
var dailyTempSize = Math.Clamp(height * 0.043, 10, 33);
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);
SeparatorLine.Margin = new Thickness(0, Math.Clamp(separatorBandHeight * 0.45, 1, 16), 0, 0);
DailyGrid.Margin = new Thickness(0, Math.Clamp(6 * fitScale, 0.5, 24), 0, 0);
var dailyAreaHeight = Math.Max(50, innerHeight - summaryHeight - hourlyHeight - separatorBandHeight - (LayoutRoot.RowSpacing * 3) - DailyGrid.Margin.Top);
var dailyRowSpacing = Math.Clamp(dailyAreaHeight * 0.028, 1, 22);
DailyGrid.RowSpacing = dailyRowSpacing;
var dailyRowHeight = Math.Max(8, (dailyAreaHeight - (dailyRowSpacing * 4)) / 5d);
var dailyRowScale = Math.Clamp(((dailyRowHeight / 40d) * 0.62) + (visualScale * 0.44), 0.22, 3.80);
var dailyLabelSize = Math.Clamp(18.5 * dailyRowScale, 6, 70);
var dailyTempSize = Math.Clamp(19 * dailyRowScale, 6, 72);
var dailyIconSize = Math.Clamp(30 * dailyRowScale, 8, 102);
var dailyLabelMaxWidth = Math.Clamp(innerWidth * 0.52, 28, 460);
var dailyHighWidth = Math.Clamp(innerWidth * 0.14, 14, 140);
var dailyLowWidth = Math.Clamp(innerWidth * 0.11, 12, 120);
var dailyHighRightGap = Math.Clamp(innerWidth * 0.018, 1, 28);
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].FontWeight = ToVariableWeight(Lerp(520, 620, emphasis));
_dailyHighBlocks[i].FontWeight = ToVariableWeight(Lerp(560, 680, emphasis));
_dailyLowBlocks[i].FontWeight = ToVariableWeight(Lerp(470, 590, emphasis));
_dailyLabelBlocks[i].MaxWidth = dailyLabelMaxWidth;
_dailyHighBlocks[i].Width = dailyHighWidth;
_dailyLowBlocks[i].Width = dailyLowWidth;
_dailyHighBlocks[i].Margin = new Thickness(0, 0, dailyHighRightGap, 0);
_dailyLowBlocks[i].Margin = new Thickness(0);
_dailyHighBlocks[i].HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right;
_dailyLowBlocks[i].HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right;
_dailyHighBlocks[i].TextAlignment = TextAlignment.Right;
@@ -784,6 +830,13 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
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 double ResolveHeroIconScaleBoost(HyperOS3WeatherVisualKind kind) =>
kind switch
{
HyperOS3WeatherVisualKind.RainLight or HyperOS3WeatherVisualKind.RainHeavy or HyperOS3WeatherVisualKind.Storm or HyperOS3WeatherVisualKind.Snow => 1.16,
HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.CloudyNight => 1.08,
_ => 1.0
};
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)); }

View File

@@ -63,7 +63,7 @@
FontFeatures="tnum"
VerticalAlignment="Center"
Margin="0,-2,0,0"
TextTrimming="CharacterEllipsis"
TextTrimming="None"
MaxLines="1" />
<StackPanel Grid.Column="1"

View File

@@ -159,13 +159,13 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
RangeTextBlock.MaxLines = 1;
TemperatureTextBlock.TextWrapping = TextWrapping.NoWrap;
TemperatureTextBlock.TextTrimming = TextTrimming.CharacterEllipsis;
TemperatureTextBlock.TextTrimming = TextTrimming.None;
TemperatureTextBlock.MaxLines = 1;
foreach (var timeBlock in _hourlyTimeBlocks)
{
timeBlock.TextWrapping = TextWrapping.NoWrap;
timeBlock.TextTrimming = TextTrimming.CharacterEllipsis;
timeBlock.TextTrimming = TextTrimming.None;
timeBlock.MaxLines = 1;
timeBlock.TextAlignment = TextAlignment.Center;
}
@@ -173,7 +173,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
foreach (var tempBlock in _hourlyTempBlocks)
{
tempBlock.TextWrapping = TextWrapping.NoWrap;
tempBlock.TextTrimming = TextTrimming.CharacterEllipsis;
tempBlock.TextTrimming = TextTrimming.None;
tempBlock.MaxLines = 1;
tempBlock.TextAlignment = TextAlignment.Center;
}
@@ -822,7 +822,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private void ApplyHourlyForecastItems(IReadOnlyList<HourlyForecastItem> items)
{
var fallbackIcon = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(_activeVisualKind)));
HyperOS3WeatherTheme.ResolveMiniIconAsset(ToThemeKind(_activeVisualKind)));
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
{
if (i >= items.Count)
@@ -836,7 +836,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
var item = items[i];
_hourlyTimeBlocks[i].Text = item.TimeLabel;
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(item.IconKind));
HyperOS3WeatherTheme.ResolveMiniIconAsset(item.IconKind));
_hourlyTempBlocks[i].Text = item.TemperatureText;
}
}
@@ -1168,68 +1168,84 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private void ApplyAdaptiveTypography()
{
var (layoutWidth, layoutHeight) = ResolveLayoutViewport();
var scaleX = Math.Clamp(layoutWidth / 608d, 0.58, 1.90);
var scaleY = Math.Clamp(layoutHeight / 288d, 0.58, 1.90);
var innerWidth = Math.Max(120, layoutWidth);
var innerHeight = Math.Max(72, layoutHeight);
var compactness = Math.Clamp((1.0 - scaleY) / 0.55, 0, 1);
var innerHeight = Math.Max(56, layoutHeight);
var fitScale = Math.Clamp(Math.Min(innerWidth / 592d, innerHeight / 284d), 0.30, 3.20);
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.34, 3.60);
var visualScale = Math.Clamp((fitScale * 0.72) + (cellScale * 0.28), 0.30, 3.60);
var emphasis = Math.Clamp((visualScale - 0.82) / 1.90, 0, 1);
ContentGrid.RowSpacing = Math.Clamp((4.2 - (compactness * 0.7)) * scaleY, 2, 8);
TopRowGrid.ColumnSpacing = Math.Clamp(8 * scaleX, 6, 13);
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp((1.0 - (compactness * 0.4)) * scaleY, 0, 2));
ContentGrid.RowSpacing = Math.Clamp(8 * fitScale, 1, 20);
TopRowGrid.ColumnSpacing = Math.Clamp(11 * fitScale, 3, 30);
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(1.2 * fitScale, 0, 7));
var contentHeight = Math.Max(60, innerHeight - ContentGrid.RowSpacing);
var topZoneRatio = Math.Clamp(0.38 + (compactness * 0.09), 0.36, 0.50);
var topZoneHeight = Math.Clamp(contentHeight * topZoneRatio, 60, 170);
var bottomZoneHeight = Math.Max(42, contentHeight - topZoneHeight);
var topScaleH = Math.Clamp(topZoneHeight / 102d, 0.62, 2.0);
var topScaleW = Math.Clamp(innerWidth / 620d, 0.62, 2.0);
var topScale = Math.Clamp((topScaleH * 0.68) + (topScaleW * 0.32), 0.62, 2.0);
var bottomScaleH = Math.Clamp(bottomZoneHeight / 122d, 0.56, 2.0);
var bottomScale = Math.Clamp((bottomScaleH * 0.74) + (scaleX * 0.26), 0.56, 1.95);
var bodyHeight = bottomZoneHeight;
var contentHeight = Math.Max(36, innerHeight - ContentGrid.RowSpacing);
var topZoneHeight = Math.Clamp(contentHeight * 0.47, 24, Math.Max(24, contentHeight - 12));
var bottomZoneHeight = Math.Max(10, contentHeight - topZoneHeight);
if (ContentGrid.RowDefinitions.Count >= 2)
{
ContentGrid.RowDefinitions[0].Height = new GridLength(topZoneHeight, GridUnitType.Pixel);
ContentGrid.RowDefinitions[1].Height = new GridLength(1, GridUnitType.Star);
}
TemperatureTextBlock.FontSize = Math.Clamp(88 * topScale, 56, 132);
TemperatureTextBlock.FontWeight = ToVariableWeight(315);
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-1.2 * topScale, -4, 0), 0, 0);
TemperatureTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 88, 196);
var topScale = Math.Clamp(((topZoneHeight / 116d) * 0.42) + (visualScale * 0.86), 0.24, 3.90);
var bottomScale = Math.Clamp(((bottomZoneHeight / 156d) * 0.44) + (visualScale * 0.72), 0.24, 3.80);
var iconGrowth = Math.Clamp((visualScale - 0.88) / 1.70, 0, 1);
var iconScaleBoost = ResolveHeroIconScaleBoost(_activeVisualKind);
var iconSize = Math.Clamp(Lerp(88, 116, iconGrowth) * topScale * iconScaleBoost, 14, 360);
iconSize = Math.Min(iconSize, Math.Max(14, innerWidth * Lerp(0.22, 0.32, iconGrowth)));
var temperatureSample = string.IsNullOrWhiteSpace(TemperatureTextBlock.Text)
? "00°"
: TemperatureTextBlock.Text.Trim();
var temperatureGlyphCount = Math.Clamp(temperatureSample.Length, 3, 6);
var temperatureMaxWidth = Math.Max(28, innerWidth - iconSize - TopRowGrid.ColumnSpacing - 4);
var rawTemperatureSize = Math.Clamp(Lerp(64, 92, iconGrowth) * topScale, 12, 320);
var fitTemperatureSize = temperatureMaxWidth / (temperatureGlyphCount * 0.62);
TemperatureTextBlock.FontSize = Math.Clamp(Math.Min(rawTemperatureSize, fitTemperatureSize), 9, 320);
TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 360, emphasis));
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-2.0 * topScale, -10, 0), 0, 0);
TemperatureTextBlock.MaxWidth = Math.Clamp(temperatureMaxWidth, 28, Math.Max(280, innerWidth * 0.68));
CityInfoBadge.Padding = new Thickness(0);
CityInfoBadge.CornerRadius = new CornerRadius(0);
LocationIcon.FontSize = Math.Clamp(12 * topScale, 9, 17);
CityTextBlock.FontSize = Math.Clamp(18 * topScale, 11, 26);
CityTextBlock.FontWeight = ToVariableWeight(540);
CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.36, 112, 300);
LocationIcon.FontSize = Math.Clamp(13 * topScale, 6, 52);
CityTextBlock.FontSize = Math.Clamp(18.5 * topScale, 7, 88);
CityTextBlock.FontWeight = ToVariableWeight(Lerp(530, 620, emphasis));
CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.37, 34, 460);
ConditionInfoBadge.Padding = new Thickness(0);
ConditionInfoBadge.CornerRadius = new CornerRadius(0);
ConditionRangeStack.Spacing = Math.Clamp(7 * topScale, 4, 13);
ConditionTextBlock.FontSize = Math.Clamp(19 * topScale, 12, 27);
RangeTextBlock.FontSize = Math.Clamp(20 * topScale, 12, 30);
ConditionTextBlock.FontWeight = ToVariableWeight(600);
RangeTextBlock.FontWeight = ToVariableWeight(620);
ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 58, 220);
RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.30, 88, 270);
BottomInfoStack.Spacing = Math.Clamp(2.2 * topScale, 1, 6);
ConditionRangeStack.Spacing = Math.Clamp(8.5 * topScale, 1, 24);
ConditionTextBlock.FontSize = Math.Clamp(19 * topScale, 7, 78);
RangeTextBlock.FontSize = Math.Clamp(21 * topScale, 7, 84);
ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(580, 660, emphasis));
RangeTextBlock.FontWeight = ToVariableWeight(Lerp(600, 680, emphasis));
ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 26, 320);
RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.31, 32, 360);
BottomInfoStack.Spacing = Math.Clamp(2.0 * topScale, 0.4, 14);
var iconSize = Math.Clamp(68 * topScale, 42, 98);
WeatherIconImage.Width = iconSize;
WeatherIconImage.Height = iconSize;
WeatherIconImage.Margin = new Thickness(0, Math.Clamp(-2.2 * topScale, -10, 0), 0, 0);
HourlyPanelBorder.Padding = new Thickness(0, Math.Clamp(1 * scaleY, 0, 2), 0, 0);
HourlyPanelBorder.Margin = new Thickness(0, Math.Clamp(1.2 * scaleY, 0, 3), 0, 0);
HourlyPanelBorder.Padding = new Thickness(0);
HourlyPanelBorder.Margin = new Thickness(0, Math.Clamp(6 * fitScale, 1, 24), 0, 0);
HourlyPanelBorder.CornerRadius = new CornerRadius(0);
HourlyGrid.ColumnSpacing = Math.Clamp(7 * scaleX, 4, 11);
HourlyGrid.ColumnSpacing = Math.Clamp(4 * fitScale, 0.5, 24);
var hourlyColumnCount = Math.Max(1, _hourlyTimeBlocks.Length);
var hourlyInnerWidth = Math.Max(
96,
innerWidth - HourlyPanelBorder.Padding.Left - HourlyPanelBorder.Padding.Right - (HourlyGrid.ColumnSpacing * (hourlyColumnCount - 1)));
var hourlyCellWidth = Math.Max(34, hourlyInnerWidth / hourlyColumnCount);
var stackSpacing = Math.Clamp((1.6 + (bottomScale * 0.8)) * scaleY, 1, 4);
var hourlyTempSize = Math.Clamp(Math.Max(13, bodyHeight * 0.22) * (0.76 + (bottomScale * 0.24)), 13, 31);
var hourlyTimeSize = Math.Clamp(Math.Max(10, bodyHeight * 0.17) * (0.78 + (bottomScale * 0.22)), 10, 23);
var hourlyIconSize = Math.Clamp(Math.Max(14, bodyHeight * 0.25) * (0.78 + (bottomScale * 0.22)), 14, 35);
32,
innerWidth - (HourlyGrid.ColumnSpacing * (hourlyColumnCount - 1)));
var hourlyCellWidth = Math.Max(12, hourlyInnerWidth / hourlyColumnCount);
var hourlyCellScale = Math.Clamp(
Math.Min((bottomScale * 0.66) + (visualScale * 0.44), hourlyCellWidth / 74d),
0.22,
3.60);
var stackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10);
var hourlyTempSize = Math.Clamp(19.5 * hourlyCellScale, 6, 72);
var hourlyTimeSize = Math.Clamp(14.5 * hourlyCellScale, 6, 50);
var hourlyIconSize = Math.Clamp(34 * hourlyCellScale, 8, 108);
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
{
@@ -1237,10 +1253,10 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
_hourlyTimeBlocks[i].FontSize = hourlyTimeSize;
_hourlyIconBlocks[i].Width = hourlyIconSize;
_hourlyIconBlocks[i].Height = hourlyIconSize;
_hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 34, 112);
_hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 34, 112);
_hourlyTimeBlocks[i].FontWeight = ToVariableWeight(500);
_hourlyTempBlocks[i].FontWeight = ToVariableWeight(590);
_hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 240);
_hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 240);
_hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(500, 600, emphasis));
_hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(580, 690, emphasis));
if (_hourlyTimeBlocks[i].Parent is StackPanel hourlyStack)
{
hourlyStack.Spacing = stackSpacing;
@@ -1253,10 +1269,20 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
return from + ((to - from) * t);
}
private static double ResolveHeroIconScaleBoost(WeatherVisualKind kind)
{
return kind switch
{
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16,
WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight => 1.08,
_ => 1.0
};
}
private void SetMainWeatherIcon(WeatherVisualKind kind)
{
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(kind)));
HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind)));
}
private void SetLoadingSkeleton(bool isLoading)

View File

@@ -102,18 +102,32 @@ public static class HyperOS3WeatherTheme
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png"
};
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> IconAssets =
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> HeroIconAssets =
new Dictionary<HyperOS3WeatherVisualKind, string>
{
[HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_sunny_day.webp",
[HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_moon_clear.webp",
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_partly_cloudy_day.webp",
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_partly_cloudy_night.webp",
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_rain_light.webp",
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_rain_heavy.webp",
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_thunder.webp",
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_snow.webp",
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_haze.webp"
[HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png",
[HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png",
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png",
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png",
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png",
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png",
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png",
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_snow_soft.png",
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png"
};
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> MiniIconAssets =
new Dictionary<HyperOS3WeatherVisualKind, string>
{
[HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png",
[HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png",
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png",
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png",
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png",
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png",
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png",
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_snow_soft.png",
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_fog_soft.png"
};
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherPalette> Palettes =
@@ -319,7 +333,17 @@ public static class HyperOS3WeatherTheme
public static string? ResolveIconAsset(HyperOS3WeatherVisualKind kind)
{
return IconAssets.TryGetValue(kind, out var asset) ? asset : null;
return ResolveMiniIconAsset(kind);
}
public static string? ResolveHeroIconAsset(HyperOS3WeatherVisualKind kind)
{
return HeroIconAssets.TryGetValue(kind, out var asset) ? asset : null;
}
public static string? ResolveMiniIconAsset(HyperOS3WeatherVisualKind kind)
{
return MiniIconAssets.TryGetValue(kind, out var asset) ? asset : null;
}
public static string ResolveSunCoreAsset()

View File

@@ -63,7 +63,7 @@
FontFeatures="tnum"
VerticalAlignment="Center"
Margin="0,-2,0,0"
TextTrimming="CharacterEllipsis"
TextTrimming="None"
MaxLines="1" />
<StackPanel Grid.Column="1"

View File

@@ -157,13 +157,13 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
RangeTextBlock.MaxLines = 1;
TemperatureTextBlock.TextWrapping = TextWrapping.NoWrap;
TemperatureTextBlock.TextTrimming = TextTrimming.CharacterEllipsis;
TemperatureTextBlock.TextTrimming = TextTrimming.None;
TemperatureTextBlock.MaxLines = 1;
foreach (var timeBlock in _hourlyTimeBlocks)
{
timeBlock.TextWrapping = TextWrapping.NoWrap;
timeBlock.TextTrimming = TextTrimming.CharacterEllipsis;
timeBlock.TextTrimming = TextTrimming.None;
timeBlock.MaxLines = 1;
timeBlock.TextAlignment = TextAlignment.Center;
}
@@ -171,7 +171,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
foreach (var tempBlock in _hourlyTempBlocks)
{
tempBlock.TextWrapping = TextWrapping.NoWrap;
tempBlock.TextTrimming = TextTrimming.CharacterEllipsis;
tempBlock.TextTrimming = TextTrimming.None;
tempBlock.MaxLines = 1;
tempBlock.TextAlignment = TextAlignment.Center;
}
@@ -812,14 +812,14 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
_hourlyTimeBlocks[i].Text = "--";
_hourlyTempBlocks[i].Text = "--°/--°";
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(_activeVisualKind)));
HyperOS3WeatherTheme.ResolveMiniIconAsset(ToThemeKind(_activeVisualKind)));
continue;
}
var item = items[i];
_hourlyTimeBlocks[i].Text = item.TimeLabel;
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(item.IconKind));
HyperOS3WeatherTheme.ResolveMiniIconAsset(item.IconKind));
_hourlyTempBlocks[i].Text = item.TemperatureText;
}
}
@@ -1015,68 +1015,85 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private void ApplyAdaptiveTypography()
{
var (layoutWidth, layoutHeight) = ResolveLayoutViewport();
var scaleX = Math.Clamp(layoutWidth / 608d, 0.58, 1.90);
var scaleY = Math.Clamp(layoutHeight / 288d, 0.58, 1.90);
var innerWidth = Math.Max(120, layoutWidth);
var innerHeight = Math.Max(72, layoutHeight);
var compactness = Math.Clamp((1.0 - scaleY) / 0.55, 0, 1);
var innerHeight = Math.Max(56, layoutHeight);
var fitScale = Math.Clamp(Math.Min(innerWidth / 592d, innerHeight / 284d), 0.30, 3.20);
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.34, 3.60);
var visualScale = Math.Clamp((fitScale * 0.72) + (cellScale * 0.28), 0.30, 3.60);
var emphasis = Math.Clamp((visualScale - 0.82) / 1.90, 0, 1);
ContentGrid.RowSpacing = Math.Clamp((4.2 - (compactness * 0.7)) * scaleY, 2, 8);
TopRowGrid.ColumnSpacing = Math.Clamp(8 * scaleX, 6, 13);
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp((1.0 - (compactness * 0.4)) * scaleY, 0, 2));
ContentGrid.RowSpacing = Math.Clamp(8 * fitScale, 1, 20);
TopRowGrid.ColumnSpacing = Math.Clamp(11 * fitScale, 3, 30);
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(1.2 * fitScale, 0, 7));
var separatorHeight = Math.Clamp(6 * scaleY, 2, 10);
var contentHeight = Math.Max(60, innerHeight - ContentGrid.RowSpacing - separatorHeight);
var topZoneRatio = Math.Clamp(0.38 + (compactness * 0.09), 0.36, 0.50);
var topZoneHeight = Math.Clamp(contentHeight * topZoneRatio, 60, 170);
var bottomZoneHeight = Math.Max(42, contentHeight - topZoneHeight);
var topScaleH = Math.Clamp(topZoneHeight / 102d, 0.62, 2.0);
var topScaleW = Math.Clamp(innerWidth / 620d, 0.62, 2.0);
var topScale = Math.Clamp((topScaleH * 0.68) + (topScaleW * 0.32), 0.62, 2.0);
var bottomScaleH = Math.Clamp(bottomZoneHeight / 122d, 0.56, 2.0);
var bottomScale = Math.Clamp((bottomScaleH * 0.74) + (scaleX * 0.26), 0.56, 1.95);
var bodyHeight = bottomZoneHeight;
var separatorHeight = Math.Clamp(2.0 * fitScale, 1, 8);
var contentHeight = Math.Max(36, innerHeight - ContentGrid.RowSpacing - separatorHeight);
var topZoneHeight = Math.Clamp(contentHeight * 0.47, 24, Math.Max(24, contentHeight - 12));
var bottomZoneHeight = Math.Max(10, contentHeight - topZoneHeight);
if (ContentGrid.RowDefinitions.Count >= 3)
{
ContentGrid.RowDefinitions[0].Height = new GridLength(topZoneHeight, GridUnitType.Pixel);
ContentGrid.RowDefinitions[1].Height = new GridLength(separatorHeight, GridUnitType.Pixel);
ContentGrid.RowDefinitions[2].Height = new GridLength(1, GridUnitType.Star);
}
TemperatureTextBlock.FontSize = Math.Clamp(88 * topScale, 56, 132);
TemperatureTextBlock.FontWeight = ToVariableWeight(315);
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-1.2 * topScale, -4, 0), 0, 0);
TemperatureTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 88, 196);
var topScale = Math.Clamp(((topZoneHeight / 116d) * 0.42) + (visualScale * 0.86), 0.24, 3.90);
var bottomScale = Math.Clamp(((bottomZoneHeight / 156d) * 0.44) + (visualScale * 0.72), 0.24, 3.80);
var iconGrowth = Math.Clamp((visualScale - 0.88) / 1.70, 0, 1);
var iconScaleBoost = ResolveHeroIconScaleBoost(_activeVisualKind);
var iconSize = Math.Clamp(Lerp(88, 116, iconGrowth) * topScale * iconScaleBoost, 14, 360);
iconSize = Math.Min(iconSize, Math.Max(14, innerWidth * Lerp(0.22, 0.32, iconGrowth)));
var temperatureSample = string.IsNullOrWhiteSpace(TemperatureTextBlock.Text)
? "00°"
: TemperatureTextBlock.Text.Trim();
var temperatureGlyphCount = Math.Clamp(temperatureSample.Length, 3, 6);
var temperatureMaxWidth = Math.Max(28, innerWidth - iconSize - TopRowGrid.ColumnSpacing - 4);
var rawTemperatureSize = Math.Clamp(Lerp(64, 92, iconGrowth) * topScale, 12, 320);
var fitTemperatureSize = temperatureMaxWidth / (temperatureGlyphCount * 0.62);
TemperatureTextBlock.FontSize = Math.Clamp(Math.Min(rawTemperatureSize, fitTemperatureSize), 9, 320);
TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 360, emphasis));
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-2.0 * topScale, -10, 0), 0, 0);
TemperatureTextBlock.MaxWidth = Math.Clamp(temperatureMaxWidth, 28, Math.Max(280, innerWidth * 0.68));
CityInfoBadge.Padding = new Thickness(0);
CityInfoBadge.CornerRadius = new CornerRadius(0);
LocationIcon.FontSize = Math.Clamp(12 * topScale, 9, 17);
CityTextBlock.FontSize = Math.Clamp(18 * topScale, 11, 26);
CityTextBlock.FontWeight = ToVariableWeight(540);
CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.36, 112, 300);
LocationIcon.FontSize = Math.Clamp(13 * topScale, 6, 52);
CityTextBlock.FontSize = Math.Clamp(18.5 * topScale, 7, 88);
CityTextBlock.FontWeight = ToVariableWeight(Lerp(530, 620, emphasis));
CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.37, 34, 460);
ConditionInfoBadge.Padding = new Thickness(0);
ConditionInfoBadge.CornerRadius = new CornerRadius(0);
ConditionIconStack.Spacing = Math.Clamp(7 * topScale, 4, 13);
ConditionTextBlock.FontSize = Math.Clamp(19 * topScale, 12, 27);
RangeTextBlock.FontSize = Math.Clamp(20 * topScale, 12, 30);
ConditionTextBlock.FontWeight = ToVariableWeight(600);
RangeTextBlock.FontWeight = ToVariableWeight(620);
ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 58, 220);
RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.30, 88, 270);
BottomInfoStack.Spacing = Math.Clamp(2.2 * topScale, 1, 6);
ConditionIconStack.Spacing = Math.Clamp(8.5 * topScale, 1, 24);
ConditionTextBlock.FontSize = Math.Clamp(19 * topScale, 7, 78);
RangeTextBlock.FontSize = Math.Clamp(21 * topScale, 7, 84);
ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(580, 660, emphasis));
RangeTextBlock.FontWeight = ToVariableWeight(Lerp(600, 680, emphasis));
ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 26, 320);
RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.31, 32, 360);
BottomInfoStack.Spacing = Math.Clamp(2.0 * topScale, 0.4, 14);
var iconSize = Math.Clamp(68 * topScale, 42, 98);
WeatherIconImage.Width = iconSize;
WeatherIconImage.Height = iconSize;
WeatherIconImage.Margin = new Thickness(0, Math.Clamp(-2.2 * topScale, -10, 0), 0, 0);
HourlyPanelBorder.Padding = new Thickness(0, Math.Clamp(1 * scaleY, 0, 2), 0, 0);
HourlyPanelBorder.Margin = new Thickness(0, Math.Clamp(1.2 * scaleY, 0, 3), 0, 0);
HourlyPanelBorder.Padding = new Thickness(0);
HourlyPanelBorder.Margin = new Thickness(0, Math.Clamp(6 * fitScale, 1, 24), 0, 0);
HourlyPanelBorder.CornerRadius = new CornerRadius(0);
HourlyGrid.ColumnSpacing = Math.Clamp(7 * scaleX, 4, 11);
HourlyGrid.ColumnSpacing = Math.Clamp(5 * fitScale, 0.5, 28);
var hourlyColumnCount = Math.Max(1, _hourlyTimeBlocks.Length);
var hourlyInnerWidth = Math.Max(
96,
innerWidth - HourlyPanelBorder.Padding.Left - HourlyPanelBorder.Padding.Right - (HourlyGrid.ColumnSpacing * (hourlyColumnCount - 1)));
var hourlyCellWidth = Math.Max(34, hourlyInnerWidth / hourlyColumnCount);
var stackSpacing = Math.Clamp((1.6 + (bottomScale * 0.8)) * scaleY, 1, 4);
var forecastRangeSize = Math.Clamp(Math.Max(13, bodyHeight * 0.22) * (0.76 + (bottomScale * 0.24)), 13, 31);
var forecastLabelSize = Math.Clamp(Math.Max(10, bodyHeight * 0.17) * (0.78 + (bottomScale * 0.22)), 10, 23);
var forecastIconSize = Math.Clamp(Math.Max(14, bodyHeight * 0.25) * (0.78 + (bottomScale * 0.22)), 14, 35);
32,
innerWidth - (HourlyGrid.ColumnSpacing * (hourlyColumnCount - 1)));
var hourlyCellWidth = Math.Max(12, hourlyInnerWidth / hourlyColumnCount);
var hourlyCellScale = Math.Clamp(
Math.Min((bottomScale * 0.66) + (visualScale * 0.44), hourlyCellWidth / 78d),
0.22,
3.60);
var stackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10);
var forecastRangeSize = Math.Clamp(18.0 * hourlyCellScale, 6, 62);
var forecastLabelSize = Math.Clamp(13.8 * hourlyCellScale, 6, 48);
var forecastIconSize = Math.Clamp(32 * hourlyCellScale, 8, 100);
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
{
@@ -1084,10 +1101,10 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
_hourlyTempBlocks[i].FontSize = forecastRangeSize;
_hourlyIconBlocks[i].Width = forecastIconSize;
_hourlyIconBlocks[i].Height = forecastIconSize;
_hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 34, 112);
_hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 34, 112);
_hourlyTimeBlocks[i].FontWeight = ToVariableWeight(500);
_hourlyTempBlocks[i].FontWeight = ToVariableWeight(590);
_hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 260);
_hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 260);
_hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(500, 600, emphasis));
_hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(580, 690, emphasis));
_hourlyTimeBlocks[i].TextAlignment = TextAlignment.Center;
_hourlyTempBlocks[i].TextAlignment = TextAlignment.Center;
if (_hourlyTimeBlocks[i].Parent is StackPanel hourlyStack)
@@ -1102,10 +1119,20 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
return from + ((to - from) * t);
}
private static double ResolveHeroIconScaleBoost(WeatherVisualKind kind)
{
return kind switch
{
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16,
WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight => 1.08,
_ => 1.0
};
}
private void SetMainWeatherIcon(WeatherVisualKind kind)
{
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(kind)));
HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind)));
}
private void SetLoadingSkeleton(bool isLoading)

View File

@@ -9,6 +9,7 @@ using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
@@ -23,6 +24,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
};
private readonly IAudioRecorderService _audioRecorderService = AudioRecorderServiceFactory.CreateRecorder();
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
private readonly AppSettingsService _settingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly List<Border> _waveBars = [];
@@ -32,6 +34,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
private string _lastSavedFilePath = string.Empty;
private double _currentCellSize = 48;
private bool _isAttached;
private bool _pausedStudyMonitoringForRecording;
public RecordingWidget()
{
@@ -115,6 +118,12 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
{
_isAttached = false;
_uiTimer.Stop();
var snapshot = _audioRecorderService.GetSnapshot();
if (snapshot.State is not AudioRecorderRuntimeState.Recording and not AudioRecorderRuntimeState.Paused)
{
ResumeStudyMonitoringIfNeeded();
}
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
@@ -140,6 +149,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
}
_audioRecorderService.Discard();
ResumeStudyMonitoringIfNeeded();
RefreshVisual();
e.Handled = true;
}
@@ -165,7 +175,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
}
else
{
_audioRecorderService.StartOrResume();
_ = TryStartRecordingWithMonitoringHandoff();
}
RefreshVisual();
@@ -201,6 +211,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
}
_ = _audioRecorderService.StopAndSave(outputPath);
ResumeStudyMonitoringIfNeeded();
RefreshVisual();
e.Handled = true;
}
@@ -208,6 +219,12 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
private void RefreshVisual()
{
var snapshot = _audioRecorderService.GetSnapshot();
if (_pausedStudyMonitoringForRecording &&
snapshot.State is AudioRecorderRuntimeState.Ready or AudioRecorderRuntimeState.Error or AudioRecorderRuntimeState.Unsupported)
{
ResumeStudyMonitoringIfNeeded();
snapshot = _audioRecorderService.GetSnapshot();
}
TitleTextBlock.Text = L("recording.widget.title", "Recorder");
TimerTextBlock.Text = FormatDuration(snapshot.Duration);
@@ -300,6 +317,60 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
HintTextBlock.Text = L("recording.widget.hint.ready", "Tap red button to record");
}
private bool TryStartRecordingWithMonitoringHandoff()
{
if (_audioRecorderService.StartOrResume())
{
return true;
}
if (!TryPauseStudyMonitoringForRecording())
{
return false;
}
if (_audioRecorderService.StartOrResume())
{
return true;
}
ResumeStudyMonitoringIfNeeded();
return false;
}
private bool TryPauseStudyMonitoringForRecording()
{
if (_pausedStudyMonitoringForRecording)
{
return true;
}
var snapshot = _studyAnalyticsService.GetSnapshot();
if (snapshot.State != StudyAnalyticsRuntimeState.Running)
{
return false;
}
if (!_studyAnalyticsService.PauseMonitoring())
{
return false;
}
_pausedStudyMonitoringForRecording = true;
return true;
}
private void ResumeStudyMonitoringIfNeeded()
{
if (!_pausedStudyMonitoringForRecording)
{
return;
}
_pausedStudyMonitoringForRecording = false;
_ = _studyAnalyticsService.StartOrResumeMonitoring();
}
private void InitializeWaveBars()
{
if (_waveBars.Count > 0)

View File

@@ -91,7 +91,7 @@
FontFeatures="tnum"
VerticalAlignment="Top"
Margin="-1,-7,0,0"
TextTrimming="CharacterEllipsis"
TextTrimming="None"
MaxLines="1" />
<Image x:Name="WeatherIconImage"

View File

@@ -154,6 +154,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
_currentCellSize = Math.Max(1, cellSize);
var scale = ResolveScale();
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Realtime2x2);
var hostWidth = Bounds.Width > 1 ? Bounds.Width : Math.Max(80, _currentCellSize * 2);
var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(80, _currentCellSize * 2);
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);
@@ -165,8 +167,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, 10, 24),
Math.Clamp(verticalPadding * scale, 10, 24));
Math.Clamp(Math.Min(horizontalPadding * scale, hostWidth * 0.12), 3, 24),
Math.Clamp(Math.Min(verticalPadding * scale, hostHeight * 0.12), 3, 24));
ApplyAdaptiveTypography();
ResetParticles();
}
@@ -472,7 +474,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
palette.TertiaryText,
backgroundSamples,
WeatherTypographyAccessibility.WcagNormalTextContrast,
isNightVisual ? (byte)0xD6 : (byte)0xC2);
isNightVisual ? (byte)0xC4 : (byte)0xAE);
var particleBrush = ResolveParticleBrush(ToThemeKind(kind), palette.ParticleColor);
LocationIcon.Foreground = tertiary;
CityTextBlock.Foreground = tertiary;
@@ -815,25 +817,19 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
{
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 = Math.Clamp(innerWidth / 288d, 0.56, 2.2);
var scaleY = Math.Clamp(innerHeight / 288d, 0.56, 2.2);
var compactness = Math.Clamp((1.0 - scaleY) / 0.60, 0, 1);
var innerWidth = Math.Max(56, width - ContentPaddingBorder.Padding.Left - ContentPaddingBorder.Padding.Right);
var innerHeight = Math.Max(56, height - ContentPaddingBorder.Padding.Top - ContentPaddingBorder.Padding.Bottom);
var fitScale = Math.Clamp(Math.Min(innerWidth / 288d, innerHeight / 288d), 0.30, 3.20);
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.34, 3.80);
var visualScale = Math.Clamp((fitScale * 0.72) + (cellScale * 0.28), 0.30, 3.80);
var emphasis = Math.Clamp((visualScale - 0.82) / 1.90, 0, 1);
ContentGrid.RowSpacing = Math.Clamp((2.8 - (compactness * 0.5)) * scaleY, 1, 6);
TopRowGrid.ColumnSpacing = Math.Clamp(7.5 * scaleX, 4, 13);
ContentGrid.RowSpacing = Math.Clamp(2.2 * fitScale, 0.5, 9);
TopRowGrid.ColumnSpacing = Math.Clamp(6.0 * fitScale, 2, 20);
var availableHeight = Math.Max(80, innerHeight - (ContentGrid.RowSpacing * 2));
var topZoneRatio = Math.Clamp(0.52 + ((1 - compactness) * 0.03), 0.48, 0.56);
var bottomZoneRatio = Math.Clamp(0.36 - (compactness * 0.02), 0.32, 0.40);
var topZoneHeight = Math.Clamp(availableHeight * topZoneRatio, 44, availableHeight - 30);
var bottomZoneHeight = Math.Clamp(availableHeight * bottomZoneRatio, 34, availableHeight - topZoneHeight - 6);
if (topZoneHeight + bottomZoneHeight > availableHeight - 6)
{
bottomZoneHeight = Math.Max(24, availableHeight - topZoneHeight - 6);
topZoneHeight = Math.Max(42, availableHeight - bottomZoneHeight - 6);
}
var availableHeight = Math.Max(40, innerHeight - (ContentGrid.RowSpacing * 2));
var topZoneHeight = Math.Clamp(availableHeight * 0.60, 22, Math.Max(22, availableHeight - 16));
var bottomZoneHeight = Math.Max(12, availableHeight - topZoneHeight - 2);
if (ContentGrid.RowDefinitions.Count >= 3)
{
@@ -842,46 +838,38 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
ContentGrid.RowDefinitions[2].Height = new GridLength(bottomZoneHeight, GridUnitType.Pixel);
}
var topScaleH = Math.Clamp(topZoneHeight / 112d, 0.58, 2.2);
var topScaleW = Math.Clamp(innerWidth / 288d, 0.60, 2.2);
var topScale = Math.Clamp((topScaleH * 0.70) + (topScaleW * 0.30), 0.58, 2.2);
var bottomScaleH = Math.Clamp(bottomZoneHeight / 80d, 0.62, 2.2);
var bottomScale = Math.Clamp((bottomScaleH * 0.80) + (scaleX * 0.20), 0.62, 2.2);
var iconSize = Math.Clamp(
Math.Max(52, topZoneHeight * 0.50) * (0.76 + (topScale * 0.24)),
52,
136);
var topScale = Math.Clamp(((topZoneHeight / 170d) * 0.42) + (visualScale * 0.84), 0.24, 4.00);
var bottomScale = Math.Clamp(((bottomZoneHeight / 84d) * 0.46) + (visualScale * 0.66), 0.24, 3.90);
var iconGrowth = Math.Clamp((visualScale - 0.88) / 1.70, 0, 1);
var iconScaleBoost = ResolveHeroIconScaleBoost(_activeVisualKind);
var iconSize = Math.Clamp(Lerp(96, 124, iconGrowth) * topScale * iconScaleBoost, 18, 360);
iconSize = Math.Min(iconSize, Math.Max(18, innerWidth * Lerp(0.34, 0.44, iconGrowth)));
WeatherIconImage.Width = iconSize;
WeatherIconImage.Height = iconSize;
WeatherIconImage.Margin = new Thickness(0, Math.Clamp(-5 * topScale, -12, 0), 0, 0);
WeatherIconImage.Margin = new Thickness(0, Math.Clamp(-4.2 * topScale, -14, 0), 0, 0);
TemperatureTextBlock.FontSize = Math.Clamp(
Math.Max(52, topZoneHeight * 0.69) * (0.74 + (topScale * 0.24)),
50,
146);
TemperatureTextBlock.FontWeight = ToVariableWeight(310);
TemperatureTextBlock.Margin = new Thickness(Math.Clamp(-2 * topScale, -5, 0), Math.Clamp(-8 * topScale, -14, -3), 0, 0);
var temperatureMaxWidthLimit = Math.Max(90, innerWidth * 0.70);
TemperatureTextBlock.MaxWidth = Math.Clamp(
innerWidth - iconSize - TopRowGrid.ColumnSpacing - 8,
90,
temperatureMaxWidthLimit);
var temperatureSample = string.IsNullOrWhiteSpace(TemperatureTextBlock.Text)
? "00°"
: TemperatureTextBlock.Text.Trim();
var temperatureGlyphCount = Math.Clamp(temperatureSample.Length, 3, 6);
var temperatureMaxWidth = Math.Max(34, innerWidth - iconSize - TopRowGrid.ColumnSpacing - 2);
var rawTemperatureSize = Math.Clamp(Lerp(94, 118, iconGrowth) * topScale, 22, 340);
var fitTemperatureSize = temperatureMaxWidth / (temperatureGlyphCount * 0.62);
TemperatureTextBlock.FontSize = Math.Clamp(Math.Min(rawTemperatureSize, fitTemperatureSize), 10, 340);
TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 360, emphasis));
TemperatureTextBlock.Margin = new Thickness(Math.Clamp(-1.4 * topScale, -6, 0), Math.Clamp(-7.6 * topScale, -16, -1), 0, 0);
TemperatureTextBlock.MaxWidth = Math.Clamp(temperatureMaxWidth, 34, Math.Max(34, innerWidth * 0.76));
var bottomStackSpacing = Math.Clamp(1.2 * bottomScale, 1, 4);
var bottomStackSpacing = Math.Clamp(1.2 * bottomScale, 0.6, 8);
BottomInfoStack.Spacing = bottomStackSpacing;
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(1.8 * scaleY, 0, 4));
BottomInfoStack.MaxHeight = Math.Max(32, bottomZoneHeight);
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(1.4 * fitScale, 0, 6));
BottomInfoStack.MaxHeight = Math.Max(10, bottomZoneHeight);
var bottomTextMaxWidth = Math.Min(innerWidth, Math.Max(56, innerWidth * 0.84));
var conditionStackSpacing = Math.Clamp(1.4 + (2.1 * bottomScale), 1.2, 7);
var bottomTextMaxWidth = Math.Min(innerWidth, Math.Max(36, innerWidth * 0.86));
var conditionStackSpacing = Math.Clamp(1.2 + (2.0 * bottomScale), 0.5, 12);
ConditionStack.Spacing = conditionStackSpacing;
ConditionStack.Margin = new Thickness(0);
var infoFontSizeRaw = Math.Clamp(
Math.Max(14, bottomZoneHeight * 0.38) * (0.82 + (bottomScale * 0.24)),
15,
42);
var infoFontSize = infoFontSizeRaw;
var infoFontSize = Math.Clamp(27 * bottomScale, 7, 86);
const double infoLineHeightFactor = 1.10;
var estimatedBottomUsedHeight =
(infoFontSize * infoLineHeightFactor * 3) +
@@ -890,35 +878,35 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
2;
if (estimatedBottomUsedHeight > bottomZoneHeight)
{
var shrink = Math.Clamp(bottomZoneHeight / estimatedBottomUsedHeight, 0.58, 1.0);
infoFontSize = Math.Max(11, infoFontSize * shrink);
conditionStackSpacing = Math.Max(0.8, conditionStackSpacing * shrink);
bottomStackSpacing = Math.Max(0.8, bottomStackSpacing * shrink);
var shrink = Math.Clamp(bottomZoneHeight / estimatedBottomUsedHeight, 0.36, 1.0);
infoFontSize = Math.Max(6, infoFontSize * shrink);
conditionStackSpacing = Math.Max(0.3, conditionStackSpacing * shrink);
bottomStackSpacing = Math.Max(0.3, bottomStackSpacing * shrink);
ConditionStack.Spacing = conditionStackSpacing;
BottomInfoStack.Spacing = bottomStackSpacing;
}
var infoFontWeight = ToVariableWeight(590);
ConditionTextBlock.FontSize = infoFontSize;
var infoFontWeight = ToVariableWeight(Lerp(580, 690, emphasis));
ConditionTextBlock.FontSize = Math.Max(6, infoFontSize * 0.96);
ConditionTextBlock.FontWeight = infoFontWeight;
ConditionTextBlock.LineHeight = infoFontSize * infoLineHeightFactor;
ConditionTextBlock.LineHeight = ConditionTextBlock.FontSize * infoLineHeightFactor;
ConditionTextBlock.MaxWidth = bottomTextMaxWidth;
RangeTextBlock.FontSize = infoFontSize;
RangeTextBlock.FontSize = Math.Max(6, infoFontSize * 1.03);
RangeTextBlock.FontWeight = infoFontWeight;
RangeTextBlock.LineHeight = infoFontSize * infoLineHeightFactor;
RangeTextBlock.LineHeight = RangeTextBlock.FontSize * infoLineHeightFactor;
RangeTextBlock.MaxWidth = bottomTextMaxWidth;
CityInfoBadge.Padding = new Thickness(0);
CityInfoBadge.CornerRadius = new CornerRadius(0);
CityInfoBadge.MaxWidth = bottomTextMaxWidth;
LocationIcon.FontSize = Math.Clamp(
Math.Max(9, bottomZoneHeight * 0.16) * (0.76 + (bottomScale * 0.22)),
9,
18);
12 * bottomScale,
6,
34);
LocationIcon.FontSize = Math.Min(LocationIcon.FontSize, infoFontSize * 0.72);
CityTextBlock.FontSize = infoFontSize;
CityTextBlock.FontWeight = infoFontWeight;
CityTextBlock.LineHeight = infoFontSize * infoLineHeightFactor;
CityTextBlock.FontSize = Math.Max(6, infoFontSize * 0.84);
CityTextBlock.FontWeight = ToVariableWeight(Lerp(500, 620, emphasis));
CityTextBlock.LineHeight = CityTextBlock.FontSize * infoLineHeightFactor;
CityTextBlock.MaxWidth = bottomTextMaxWidth;
}
@@ -927,10 +915,20 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
return from + ((to - from) * t);
}
private static double ResolveHeroIconScaleBoost(WeatherVisualKind kind)
{
return kind switch
{
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16,
WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight => 1.08,
_ => 1.0
};
}
private void SetWeatherIcon(WeatherVisualKind kind)
{
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(kind)));
HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind)));
}
private void SetLoadingSkeleton(bool isLoading)

View File

@@ -706,6 +706,12 @@ public partial class MainWindow
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopDailyArtwork)
{
OpenDailyArtworkComponentSettings();
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopStudyEnvironment)
{
OpenStudyEnvironmentComponentSettings();
@@ -760,6 +766,22 @@ public partial class MainWindow
ComponentSettingsWindow.Opacity = 1;
}
private void OpenDailyArtworkComponentSettings()
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new DailyArtworkSettingsWindow();
settingsContent.SettingsChanged += OnDailyArtworkSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
}
private void OnClassScheduleSettingsChanged(object? sender, EventArgs e)
{
if (_selectedDesktopComponentHost is null)
@@ -788,6 +810,34 @@ public partial class MainWindow
}
}
private void OnDailyArtworkSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
_dailyArtworkMirrorSource = sender is DailyArtworkSettingsWindow settingsWindow
? DailyArtworkMirrorSources.Normalize(settingsWindow.CurrentSource)
: DailyArtworkMirrorSources.Normalize(_appSettingsService.Load().DailyArtworkMirrorSource);
foreach (var pageGrid in _desktopPageComponentGrids.Values)
{
foreach (var host in pageGrid.Children.OfType<Border>())
{
if (!host.Classes.Contains(DesktopComponentHostClass))
{
continue;
}
if (TryGetContentHost(host)?.Child is DailyArtworkWidget widget)
{
widget.RefreshFromSettings();
}
}
}
PersistSettings();
}
private void CloseComponentSettingsWindow()
{
if (ComponentSettingsWindow is null)
@@ -805,6 +855,11 @@ public partial class MainWindow
studyEnvironmentSettingsWindow.SettingsChanged -= OnStudyEnvironmentSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is DailyArtworkSettingsWindow dailyArtworkSettingsWindow)
{
dailyArtworkSettingsWindow.SettingsChanged -= OnDailyArtworkSettingsChanged;
}
ComponentSettingsWindow.Opacity = 0;
DispatcherTimer.RunOnce(() =>

View File

@@ -262,6 +262,13 @@ public partial class MainWindow
"settings.about.font_format",
"Font: {0}",
AppFontName);
AboutStartupSettingsExpander.Header = L("settings.about.startup_header", "Windows Startup");
AboutStartupSettingsExpander.Description = L(
"settings.about.startup_desc",
"Launch the app automatically when signing in to Windows.");
AutoStartWithWindowsToggleSwitch.Content = L(
"settings.about.startup_toggle",
"Launch at Windows sign-in");
if (WallpaperPlacementComboBox?.ItemCount >= 5)
{

View File

@@ -658,6 +658,8 @@ public partial class MainWindow
WeatherExcludedAlerts = _weatherExcludedAlertsRaw,
WeatherIconPackId = _weatherIconPackId,
WeatherNoTlsRequests = _weatherNoTlsRequests,
DailyArtworkMirrorSource = DailyArtworkMirrorSources.Normalize(_dailyArtworkMirrorSource),
AutoStartWithWindows = _autoStartWithWindows,
TopStatusComponentIds = _topStatusComponentIds.ToList(),
PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList(),
EnableDynamicTaskbarActions = _enableDynamicTaskbarActions,
@@ -790,14 +792,37 @@ public partial class MainWindow
weatherCode: null,
temperatureText: "--",
updatedAt: null);
UpdateWeatherLocationModePanels();
UpdateWeatherLocationStatusText();
}
finally
{
_suppressWeatherLocationEvents = false;
}
UpdateWeatherLocationModePanels();
UpdateWeatherLocationStatusText();
}
private void InitializeAutoStartWithWindowsSetting(AppSettingsSnapshot snapshot)
{
_autoStartWithWindows = OperatingSystem.IsWindows()
? _windowsStartupService.IsEnabled()
: snapshot.AutoStartWithWindows;
if (AutoStartWithWindowsToggleSwitch is null)
{
return;
}
_suppressAutoStartToggleEvents = true;
try
{
AutoStartWithWindowsToggleSwitch.IsEnabled = OperatingSystem.IsWindows();
AutoStartWithWindowsToggleSwitch.IsChecked = _autoStartWithWindows;
}
finally
{
_suppressAutoStartToggleEvents = false;
}
}
private static WeatherLocationMode ParseWeatherLocationMode(string? value)
@@ -1022,6 +1047,51 @@ public partial class MainWindow
PersistSettings();
}
private void OnAutoStartWithWindowsToggled(object? sender, RoutedEventArgs e)
{
if (_suppressAutoStartToggleEvents || AutoStartWithWindowsToggleSwitch is null)
{
return;
}
var requested = AutoStartWithWindowsToggleSwitch.IsChecked == true;
if (!OperatingSystem.IsWindows())
{
_autoStartWithWindows = false;
_suppressAutoStartToggleEvents = true;
try
{
AutoStartWithWindowsToggleSwitch.IsEnabled = false;
AutoStartWithWindowsToggleSwitch.IsChecked = false;
}
finally
{
_suppressAutoStartToggleEvents = false;
}
PersistSettings();
return;
}
var applied = _windowsStartupService.SetEnabled(requested);
_autoStartWithWindows = _windowsStartupService.IsEnabled();
if (!applied || _autoStartWithWindows != requested)
{
_suppressAutoStartToggleEvents = true;
try
{
AutoStartWithWindowsToggleSwitch.IsChecked = _autoStartWithWindows;
}
finally
{
_suppressAutoStartToggleEvents = false;
}
}
PersistSettings();
}
private async void OnSearchWeatherCityClick(object? sender, RoutedEventArgs e)
{
if (_isWeatherSearchInProgress || WeatherCitySearchTextBox is null || WeatherCityResultsComboBox is null)
@@ -1942,6 +2012,15 @@ public partial class MainWindow
};
}
if (AboutStartupSettingsExpander is not null)
{
AboutStartupSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = Symbol.Play,
IconVariant = variant
};
}
UpdateThemeModeIcon();
}

View File

@@ -1390,6 +1390,19 @@
<TextBlock x:Name="FontInfoTextBlock" Text="Font: MiSans" FontSize="12" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
</StackPanel>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="AboutStartupSettingsExpander"
Header="Windows Startup"
Description="Launch the app automatically when signing in to Windows."
IsExpanded="True">
<ui:SettingsExpander.Footer>
<ToggleSwitch x:Name="AutoStartWithWindowsToggleSwitch"
Checked="OnAutoStartWithWindowsToggled"
Unchecked="OnAutoStartWithWindowsToggled"
Content="Launch at Windows sign-in" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</Grid>
</Border>

View File

@@ -90,6 +90,7 @@ public partial class MainWindow : Window
private readonly AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly TimeZoneService _timeZoneService = new();
private readonly WindowsStartupService _windowsStartupService = new();
private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService();
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
private readonly ComponentRegistry _componentRegistry = ComponentRegistry
@@ -151,6 +152,9 @@ public partial class MainWindow : Window
private string _weatherExcludedAlertsRaw = string.Empty;
private string _weatherIconPackId = "FluentRegular";
private bool _weatherNoTlsRequests;
private string _dailyArtworkMirrorSource = DailyArtworkMirrorSources.Overseas;
private bool _autoStartWithWindows;
private bool _suppressAutoStartToggleEvents;
private string _weatherSearchKeyword = string.Empty;
private bool _isWeatherSearchInProgress;
private bool _isWeatherPreviewInProgress;
@@ -225,6 +229,8 @@ public partial class MainWindow : Window
ApplyTaskbarSettings(snapshot);
InitializeLocalization(snapshot.LanguageCode);
InitializeWeatherSettings(snapshot);
_dailyArtworkMirrorSource = DailyArtworkMirrorSources.Normalize(snapshot.DailyArtworkMirrorSource);
InitializeAutoStartWithWindowsSetting(snapshot);
InitializeDesktopSurfaceState(snapshot);
InitializeDesktopComponentPlacements(snapshot);
InitializeSettingsIcons();