diff --git a/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs b/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs index 94fa505..95ab577 100644 --- a/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs @@ -11,6 +11,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Styling; using Avalonia.Threading; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -48,6 +49,7 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget private bool _isRefreshing; private bool _autoRefreshEnabled = true; private string _sourceType = BaiduHotSearchSourceTypes.Official; + private bool _isNightVisual = true; private sealed record HotItemVisual( Border Host, @@ -79,6 +81,7 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget AttachedToVisualTree += OnAttachedToVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree; SizeChanged += OnSizeChanged; + ActualThemeVariantChanged += OnActualThemeVariantChanged; ApplyCellSize(_currentCellSize); UpdateLanguageCode(); @@ -133,6 +136,67 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget ApplyCellSize(_currentCellSize); } + private void OnActualThemeVariantChanged(object? sender, EventArgs e) + { + _isNightVisual = ResolveNightMode(); + UpdateAdaptiveLayout(); + } + + private bool ResolveNightMode() + { + if (ActualThemeVariant == ThemeVariant.Dark) + { + return true; + } + + if (ActualThemeVariant == ThemeVariant.Light) + { + return false; + } + + if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) && + value is ISolidColorBrush brush) + { + return CalculateRelativeLuminance(brush.Color) < 0.45; + } + + return true; + } + + private static double CalculateRelativeLuminance(Color color) + { + static double ToLinear(double channel) + { + return channel <= 0.03928 + ? channel / 12.92 + : Math.Pow((channel + 0.055) / 1.055, 2.4); + } + + var r = ToLinear(color.R / 255d); + var g = ToLinear(color.G / 255d); + var b = ToLinear(color.B / 255d); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } + + private void ApplyNightModeVisual() + { + CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFCFD")); + RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#00000000")); + + BrandTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#5D93FF") : Color.Parse("#2932E1")); + + RefreshButton.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#EFF1F5")); + RefreshGlyphIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671")); + + foreach (var visual in _hotItemVisuals) + { + visual.IndexTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#5D93FF") : Color.Parse("#2932E1")); + visual.TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")); + } + + StatusTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#6A6F77")); + } + private async void OnRefreshTimerTick(object? sender, EventArgs e) { await RefreshHotSearchAsync(forceRefresh: true); @@ -375,6 +439,7 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget } StatusTextBlock.FontSize = Math.Clamp(itemFont, 10, 20); + ApplyNightModeVisual(); } private void UpdateInteractionState() diff --git a/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs b/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs index a8c5f17..746e3be 100644 --- a/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs @@ -9,6 +9,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; +using Avalonia.Styling; using Avalonia.Threading; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -46,6 +47,7 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid private bool _isAttached; private bool _isRefreshing; private bool _autoRefreshEnabled = true; + private bool _isNightVisual = true; private sealed record HotItemVisual( Border Host, @@ -78,6 +80,7 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid AttachedToVisualTree += OnAttachedToVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree; SizeChanged += OnSizeChanged; + ActualThemeVariantChanged += OnActualThemeVariantChanged; ApplyCellSize(_currentCellSize); UpdateLanguageCode(); @@ -129,6 +132,69 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid ApplyCellSize(_currentCellSize); } + private void OnActualThemeVariantChanged(object? sender, EventArgs e) + { + _isNightVisual = ResolveNightMode(); + UpdateAdaptiveLayout(); + } + + private bool ResolveNightMode() + { + if (ActualThemeVariant == ThemeVariant.Dark) + { + return true; + } + + if (ActualThemeVariant == ThemeVariant.Light) + { + return false; + } + + if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) && + value is ISolidColorBrush brush) + { + return CalculateRelativeLuminance(brush.Color) < 0.45; + } + + return true; + } + + private static double CalculateRelativeLuminance(Color color) + { + static double ToLinear(double channel) + { + return channel <= 0.03928 + ? channel / 12.92 + : Math.Pow((channel + 0.055) / 1.055, 2.4); + } + + var r = ToLinear(color.R / 255d); + var g = ToLinear(color.G / 255d); + var b = ToLinear(color.B / 255d); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } + + private void ApplyNightModeVisual() + { + CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFCFD")); + RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#00000000")); + + SearchBoxBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#ECF2FA")); + SearchBoxBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#3FFFFFFF") : Color.Parse("#22000000")); + SearchEntryTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")); + SearchGlyphIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671")); + + TopRightTitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#F472C4") : Color.Parse("#F44C9F")); + + foreach (var visual in _hotItemVisuals) + { + visual.IndexTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#F472C4") : Color.Parse("#F44C9F")); + visual.TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")); + } + + StatusTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#6A6F77")); + } + private async void OnRefreshTimerTick(object? sender, EventArgs e) { await RefreshHotSearchAsync(forceRefresh: false); @@ -396,6 +462,7 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid } StatusTextBlock.FontSize = Math.Clamp(itemFont, 10, 20); + ApplyNightModeVisual(); } private void UpdateInteractionState() diff --git a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs index a685312..7008f03 100644 --- a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs @@ -14,6 +14,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Styling; using Avalonia.Threading; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -88,6 +89,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, private bool _isAttached; private bool _isRefreshing; private bool _autoRotateEnabled = true; + private bool _isNightVisual = true; public CnrDailyNewsWidget() { @@ -105,6 +107,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, AttachedToVisualTree += OnAttachedToVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree; SizeChanged += OnSizeChanged; + ActualThemeVariantChanged += OnActualThemeVariantChanged; ApplyCellSize(_currentCellSize); UpdateLanguageCode(); @@ -161,6 +164,66 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, ApplyCellSize(_currentCellSize); } + private void OnActualThemeVariantChanged(object? sender, EventArgs e) + { + _isNightVisual = ResolveNightMode(); + UpdateAdaptiveLayout(); + } + + private bool ResolveNightMode() + { + if (ActualThemeVariant == ThemeVariant.Dark) + { + return true; + } + + if (ActualThemeVariant == ThemeVariant.Light) + { + return false; + } + + if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) && + value is ISolidColorBrush brush) + { + return CalculateRelativeLuminance(brush.Color) < 0.45; + } + + return true; + } + + private static double CalculateRelativeLuminance(Color color) + { + static double ToLinear(double channel) + { + return channel <= 0.03928 + ? channel / 12.92 + : Math.Pow((channel + 0.055) / 1.055, 2.4); + } + + var r = ToLinear(color.R / 255d); + var g = ToLinear(color.G / 255d); + var b = ToLinear(color.B / 255d); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } + + private void ApplyNightModeVisual() + { + CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFCFD")); + RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#00000000")); + + BrandPrimaryTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")); + BrandSecondaryTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#6A6F77")); + + RefreshButton.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#EFF1F5")); + RefreshGlyphIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671")); + RefreshLabelTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671")); + + News1TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")); + News2TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")); + + StatusTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#6A6F77")); + } + private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e) { if (_isRefreshing) @@ -354,9 +417,11 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, { var normalizedTitle = NormalizeCompactText(title); var hotLabel = L("cnrnews.widget.hot_label", "Hot"); + var primaryForeground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")); if (News1TitleTextBlock.Inlines is null) { News1TitleTextBlock.Text = $"{hotLabel} | {normalizedTitle}"; + News1TitleTextBlock.Foreground = primaryForeground; return; } @@ -368,7 +433,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, }); News1TitleTextBlock.Inlines.Add(new Run(normalizedTitle) { - Foreground = new SolidColorBrush(Color.Parse("#202327")), + Foreground = primaryForeground, FontWeight = FontWeight.SemiBold }); } @@ -401,7 +466,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, var textBlock = new TextBlock { Text = NormalizeCompactText(item.Title), - Foreground = new SolidColorBrush(Color.Parse("#202327")), + Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")), FontFamily = MiSansFontFamily, FontWeight = FontWeight.SemiBold, TextWrapping = TextWrapping.Wrap, @@ -556,6 +621,8 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, } ExtraNewsItemsPanel.Spacing = Math.Clamp(6 * scale, 3, 10); + + ApplyNightModeVisual(); } private void UpdateRefreshButtonState() diff --git a/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs b/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs index 6a6b89c..5faf5d1 100644 --- a/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs +++ b/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs @@ -9,6 +9,7 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Styling; using Avalonia.VisualTree; using Avalonia.Threading; using LanMountainDesktop.Models; @@ -44,6 +45,7 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget, private bool _isAttached; private bool _isRefreshing; private bool _autoRefreshEnabled = true; + private bool _isNightVisual = true; private bool _isMeaningVisible; public DailyWord2x2Widget() @@ -59,6 +61,7 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget, AttachedToVisualTree += OnAttachedToVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree; SizeChanged += OnSizeChanged; + ActualThemeVariantChanged += OnActualThemeVariantChanged; ApplyCellSize(_currentCellSize); UpdateLanguageCode(); @@ -113,6 +116,62 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget, ApplyCellSize(_currentCellSize); } + private void OnActualThemeVariantChanged(object? sender, EventArgs e) + { + _isNightVisual = ResolveNightMode(); + ApplyNightModeVisual(); + } + + private bool ResolveNightMode() + { + if (ActualThemeVariant == ThemeVariant.Dark) + { + return true; + } + + if (ActualThemeVariant == ThemeVariant.Light) + { + return false; + } + + if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) && + value is ISolidColorBrush brush) + { + return CalculateRelativeLuminance(brush.Color) < 0.45; + } + + return true; + } + + private static double CalculateRelativeLuminance(Color color) + { + static double ToLinear(double channel) + { + return channel <= 0.03928 + ? channel / 12.92 + : Math.Pow((channel + 0.055) / 1.055, 2.4); + } + + var r = ToLinear(color.R / 255d); + var g = ToLinear(color.G / 255d); + var b = ToLinear(color.B / 255d); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } + + private void ApplyNightModeVisual() + { + CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFBFA")); + + WordTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#2B2F35")); + MeaningTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5A6069")); + HiddenHintTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#8A9099")); + + RefreshButton.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#EEF1F4")); + RefreshIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671")); + + StatusTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#6A6F77")); + } + private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e) { if (_isRefreshing) diff --git a/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs index 0785c33..e7368bf 100644 --- a/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs @@ -8,6 +8,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Styling; using Avalonia.Threading; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -41,6 +42,7 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe private bool _isAttached; private bool _isRefreshing; private bool _autoRefreshEnabled = true; + private bool _isNightVisual = true; public DailyWordWidget() { @@ -58,6 +60,7 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe AttachedToVisualTree += OnAttachedToVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree; SizeChanged += OnSizeChanged; + ActualThemeVariantChanged += OnActualThemeVariantChanged; ApplyCellSize(_currentCellSize); UpdateLanguageCode(); @@ -112,6 +115,64 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe ApplyCellSize(_currentCellSize); } + private void OnActualThemeVariantChanged(object? sender, EventArgs e) + { + _isNightVisual = ResolveNightMode(); + ApplyNightModeVisual(); + } + + private bool ResolveNightMode() + { + if (ActualThemeVariant == ThemeVariant.Dark) + { + return true; + } + + if (ActualThemeVariant == ThemeVariant.Light) + { + return false; + } + + if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) && + value is ISolidColorBrush brush) + { + return CalculateRelativeLuminance(brush.Color) < 0.45; + } + + return true; + } + + private static double CalculateRelativeLuminance(Color color) + { + static double ToLinear(double channel) + { + return channel <= 0.03928 + ? channel / 12.92 + : Math.Pow((channel + 0.055) / 1.055, 2.4); + } + + var r = ToLinear(color.R / 255d); + var g = ToLinear(color.G / 255d); + var b = ToLinear(color.B / 255d); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } + + private void ApplyNightModeVisual() + { + CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFBFA")); + + WordTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#FF9D6C") : Color.Parse("#F07541")); + PronunciationTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#6B7078")); + MeaningTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#2B2F35")); + ExampleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#2B2F35")); + ExampleTranslationTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#7A8088")); + + RefreshButton.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#14A0A6AF")); + RefreshIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#626870")); + + StatusTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#6A6F77")); + } + private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e) { if (_isRefreshing) @@ -229,6 +290,14 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells; var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells; + var isFourByThree = false; + if (Bounds.Width > 1 && Bounds.Height > 1) + { + var widthRatio = Bounds.Width / (_currentCellSize * BaseWidthCells); + var heightRatio = Bounds.Height / (_currentCellSize * BaseHeightCells); + isFourByThree = widthRatio >= 0.9 && heightRatio >= 1.35; + } + RootBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 16, 52)); RootBorder.Padding = new Thickness(0); @@ -261,15 +330,15 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe ExampleTranslationTextBlock.MaxWidth = contentWidth; var compactLayout = totalHeight < _currentCellSize * 1.72; - MeaningTextBlock.MaxLines = compactLayout ? 1 : 2; - ExampleTextBlock.MaxLines = compactLayout ? 1 : 2; - ExampleTranslationTextBlock.IsVisible = !compactLayout; - ExampleTranslationTextBlock.MaxLines = 1; + MeaningTextBlock.MaxLines = compactLayout ? 1 : (isFourByThree ? 3 : 2); + ExampleTextBlock.MaxLines = compactLayout ? 1 : (isFourByThree ? 4 : 2); + ExampleTranslationTextBlock.IsVisible = !compactLayout || isFourByThree; + ExampleTranslationTextBlock.MaxLines = isFourByThree ? 2 : 1; var contentHeight = Math.Max(52, totalHeight - RootBorder.Padding.Top - RootBorder.Padding.Bottom - CardBorder.Padding.Top - CardBorder.Padding.Bottom); var wordHeightBudget = Math.Max(18, contentHeight * 0.24); var pronunciationHeightBudget = Math.Max(14, contentHeight * 0.16); - var meaningHeightBudget = Math.Max(16, contentHeight * (compactLayout ? 0.26 : 0.30)); + var meaningHeightBudget = Math.Max(16, contentHeight * (compactLayout ? 0.26 : (isFourByThree ? 0.35 : 0.30))); var exampleHeightBudget = Math.Max(16, contentHeight - wordHeightBudget - pronunciationHeightBudget - meaningHeightBudget - Math.Clamp(16 * scale, 8, 24)); if (!ExampleTranslationTextBlock.IsVisible) { @@ -433,11 +502,26 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe private double ResolveScale() { var cellScale = Math.Clamp(_currentCellSize / BaseCellSize, 0.56, 2.0); + + var widthCells = BaseWidthCells; + var heightCells = BaseHeightCells; + + if (Bounds.Width > 1 && Bounds.Height > 1) + { + var widthRatio = Bounds.Width / (_currentCellSize * widthCells); + var heightRatio = Bounds.Height / (_currentCellSize * heightCells); + + if (widthRatio >= 0.9 && heightRatio >= 1.35) + { + heightCells = 3; + } + } + var widthScale = Bounds.Width > 1 - ? Math.Clamp(Bounds.Width / Math.Max(1, _currentCellSize * BaseWidthCells), 0.56, 2.0) + ? Math.Clamp(Bounds.Width / Math.Max(1, _currentCellSize * widthCells), 0.56, 2.0) : 1; var heightScale = Bounds.Height > 1 - ? Math.Clamp(Bounds.Height / Math.Max(1, _currentCellSize * BaseHeightCells), 0.56, 2.0) + ? Math.Clamp(Bounds.Height / Math.Max(1, _currentCellSize * heightCells), 0.56, 2.0) : 1; return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.56, 2.0); } diff --git a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs index 31184ee..8e6f5b5 100644 --- a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -13,6 +13,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Styling; using Avalonia.Threading; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -58,6 +59,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe private bool _isAttached; private bool _isRefreshing; private bool _autoRefreshEnabled = true; + private bool _isNightVisual = true; private sealed record NewsItemVisual( Border Host, @@ -86,6 +88,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe AttachedToVisualTree += OnAttachedToVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree; SizeChanged += OnSizeChanged; + ActualThemeVariantChanged += OnActualThemeVariantChanged; ApplyCellSize(_currentCellSize); UpdateLanguageCode(); @@ -141,6 +144,67 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe ApplyCellSize(_currentCellSize); } + private void OnActualThemeVariantChanged(object? sender, EventArgs e) + { + _isNightVisual = ResolveNightMode(); + UpdateAdaptiveLayout(); + } + + private bool ResolveNightMode() + { + if (ActualThemeVariant == ThemeVariant.Dark) + { + return true; + } + + if (ActualThemeVariant == ThemeVariant.Light) + { + return false; + } + + if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) && + value is ISolidColorBrush brush) + { + return CalculateRelativeLuminance(brush.Color) < 0.45; + } + + return true; + } + + private static double CalculateRelativeLuminance(Color color) + { + static double ToLinear(double channel) + { + return channel <= 0.03928 + ? channel / 12.92 + : Math.Pow((channel + 0.055) / 1.055, 2.4); + } + + var r = ToLinear(color.R / 255d); + var g = ToLinear(color.G / 255d); + var b = ToLinear(color.B / 255d); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } + + private void ApplyNightModeVisual() + { + CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFCFD")); + RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#00000000")); + + BrandTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")); + + RefreshButton.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#EFF1F5")); + RefreshGlyphIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671")); + + foreach (var visual in _itemVisuals) + { + visual.Host.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#F7F8FA")); + visual.TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")); + } + + StatusTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#6A6F77")); + } + private async void OnRefreshTimerTick(object? sender, EventArgs e) { await RefreshNewsAsync(forceRefresh: true); @@ -398,6 +462,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe } StatusTextBlock.FontSize = Math.Clamp(titleFont, 10, 20); + ApplyNightModeVisual(); } private void UpdateInteractionState() diff --git a/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs b/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs index 123c3dc..37ef82b 100644 --- a/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -8,6 +8,7 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using Avalonia.Platform.Storage; +using Avalonia.Styling; using Avalonia.Threading; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -36,6 +37,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe private bool _isAttached; private bool _isOnActivePage = true; private bool _pausedStudyMonitoringForRecording; + private bool _isNightVisual = true; public RecordingWidget() { @@ -45,6 +47,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe AttachedToVisualTree += OnAttachedToVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree; SizeChanged += OnSizeChanged; + ActualThemeVariantChanged += OnActualThemeVariantChanged; InitializeWaveBars(); ReloadLanguageCode(); @@ -146,6 +149,68 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe ApplyCellSize(_currentCellSize); } + private void OnActualThemeVariantChanged(object? sender, EventArgs e) + { + _isNightVisual = ResolveNightMode(); + ApplyNightModeVisual(); + } + + private bool ResolveNightMode() + { + if (ActualThemeVariant == ThemeVariant.Dark) + { + return true; + } + + if (ActualThemeVariant == ThemeVariant.Light) + { + return false; + } + + if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) && + value is ISolidColorBrush brush) + { + return CalculateRelativeLuminance(brush.Color) < 0.45; + } + + return true; + } + + private static double CalculateRelativeLuminance(Color color) + { + static double ToLinear(double channel) + { + return channel <= 0.03928 + ? channel / 12.92 + : Math.Pow((channel + 0.055) / 1.055, 2.4); + } + + var r = ToLinear(color.R / 255d); + var g = ToLinear(color.G / 255d); + var b = ToLinear(color.B / 255d); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } + + private void ApplyNightModeVisual() + { + RootBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#ECEFF3")); + RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#D9DEE7")); + + TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#11151D")); + TimerTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#A4A9B2")); + FutureLine.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#A3A8B3")); + + DiscardButtonBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#F8FAFD")); + DiscardButtonBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4451") : Color.Parse("#E0E5EC")); + DiscardIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#141922")); + + SaveButtonBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#F8FAFD")); + SaveButtonBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4451") : Color.Parse("#E0E5EC")); + SaveIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#141922")); + + HintTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#7A818E")); + } + private void OnUiTick(object? sender, EventArgs e) { if (!_isAttached || !_isOnActivePage) @@ -291,11 +356,18 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe SaveButtonBorder.Opacity = SaveButtonBorder.IsHitTestVisible ? 1 : 0.42; RecordToggleButtonBorder.Opacity = RecordToggleButtonBorder.IsHitTestVisible ? 1 : 0.54; - TimerTextBlock.Foreground = CreateBrush(!isSupported - ? "#B2B7C0" - : isReady - ? "#A4A9B2" - : "#151922"); + if (!isSupported) + { + TimerTextBlock.Foreground = CreateBrush(_isNightVisual ? "#A8B1C2" : "#B2B7C0"); + } + else if (isReady) + { + TimerTextBlock.Foreground = CreateBrush(_isNightVisual ? "#A8B1C2" : "#A4A9B2"); + } + else + { + TimerTextBlock.Foreground = CreateBrush(_isNightVisual ? "#E8EAED" : "#151922"); + } HintTextBlock.IsVisible = !isReady || !isSupported; RecordDot.IsVisible = snapshot.State == AudioRecorderRuntimeState.Ready; diff --git a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs index 5ceef64..53efc4c 100644 --- a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs @@ -13,6 +13,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Styling; using Avalonia.Threading; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -60,6 +61,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I private bool _isAttached; private bool _isRefreshing; private bool _autoRefreshEnabled = true; + private bool _isNightVisual = true; private sealed record ForumItemVisual( Border Host, @@ -153,6 +155,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I AttachedToVisualTree += OnAttachedToVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree; SizeChanged += OnSizeChanged; + ActualThemeVariantChanged += OnActualThemeVariantChanged; ApplyCellSize(_currentCellSize); UpdateLanguageCode(); @@ -208,6 +211,70 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I ApplyCellSize(_currentCellSize); } + private void OnActualThemeVariantChanged(object? sender, EventArgs e) + { + _isNightVisual = ResolveNightMode(); + UpdateAdaptiveLayout(); + } + + private bool ResolveNightMode() + { + if (ActualThemeVariant == ThemeVariant.Dark) + { + return true; + } + + if (ActualThemeVariant == ThemeVariant.Light) + { + return false; + } + + if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) && + value is ISolidColorBrush brush) + { + return CalculateRelativeLuminance(brush.Color) < 0.45; + } + + return true; + } + + private static double CalculateRelativeLuminance(Color color) + { + static double ToLinear(double channel) + { + return channel <= 0.03928 + ? channel / 12.92 + : Math.Pow((channel + 0.055) / 1.055, 2.4); + } + + var r = ToLinear(color.R / 255d); + var g = ToLinear(color.G / 255d); + var b = ToLinear(color.B / 255d); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } + + private void ApplyNightModeVisual() + { + CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFCFD")); + RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#00000000")); + + HeaderTitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")); + HeaderDot.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#FF6B6B") : Color.Parse("#FF4D4F")); + + RefreshButton.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#EFF1F5")); + RefreshGlyphIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671")); + + foreach (var visual in _itemVisuals) + { + visual.Host.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#F7F8FA")); + visual.AvatarHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4451") : Color.Parse("#E7EBF4")); + visual.AvatarFallbackText.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#4A5466")); + visual.TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")); + } + + StatusTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#6A6F77")); + } + private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e) { if (_isRefreshing) @@ -606,6 +673,8 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I StatusTextBlock.FontSize = Math.Clamp(14 * softScale, 10, 18); + ApplyNightModeVisual(); + if (_visibleItemCount != previousVisibleItemCount && _isAttached && !_isRefreshing && diff --git a/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs index 65ac45e..e36d56a 100644 --- a/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs @@ -6,6 +6,7 @@ using Avalonia.Controls; using Avalonia.Controls.Shapes; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Styling; using Avalonia.Threading; using LanMountainDesktop.Services; @@ -81,6 +82,8 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT public required TextBlock OffsetTextBlock { get; init; } public bool? IsNightApplied { get; set; } + + public bool? IsSystemNightApplied { get; set; } } private readonly DispatcherTimer _clockTimer = new() @@ -99,6 +102,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT private double _currentCellSize = BaseCellSize; private DateTime _nextLanguageProbeUtc = DateTime.MinValue; private string _secondHandMode = ClockSecondHandMode.Tick; + private bool _isNightVisual = true; public WorldClockWidget() { @@ -114,6 +118,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT AttachedToVisualTree += OnAttachedToVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree; SizeChanged += OnSizeChanged; + ActualThemeVariantChanged += OnActualThemeVariantChanged; } public void SetTimeZoneService(TimeZoneService timeZoneService) @@ -211,6 +216,79 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT ApplyCellSize(_currentCellSize); } + private void OnActualThemeVariantChanged(object? sender, EventArgs e) + { + _ = sender; + _ = e; + _isNightVisual = ResolveNightMode(); + ApplyNightModeVisual(); + } + + private bool ResolveNightMode() + { + if (ActualThemeVariant == ThemeVariant.Dark) + { + return true; + } + + if (ActualThemeVariant == ThemeVariant.Light) + { + return false; + } + + if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) && + value is ISolidColorBrush brush) + { + return CalculateRelativeLuminance(brush.Color) < 0.45; + } + + return true; + } + + private static double CalculateRelativeLuminance(Color color) + { + static double ToLinear(double channel) + { + return channel <= 0.03928 + ? channel / 12.92 + : Math.Pow((channel + 0.055) / 1.055, 2.4); + } + + var r = ToLinear(color.R / 255d); + var g = ToLinear(color.G / 255d); + var b = ToLinear(color.B / 255d); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } + + private void ApplyNightModeVisual() + { + RootBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#F4F5F7")); + RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#16000000")); + + foreach (var entry in _entryVisuals) + { + ApplyTextThemeForSystemNight(entry, _isNightVisual); + } + } + + private void ApplyTextThemeForSystemNight(ClockEntryVisual entry, bool isSystemNight) + { + if (entry.IsSystemNightApplied.HasValue && entry.IsSystemNightApplied.Value == isSystemNight) + { + return; + } + + entry.IsSystemNightApplied = isSystemNight; + + var cityForeground = isSystemNight ? "#E8EAED" : "#20232A"; + var dayForeground = isSystemNight ? "#A8B1C2" : "#646C79"; + var offsetForeground = isSystemNight ? "#A8B1C2" : "#7A7F89"; + + entry.CityTextBlock.Foreground = CreateBrush(cityForeground); + entry.DayTextBlock.Foreground = CreateBrush(dayForeground); + entry.OffsetTextBlock.Foreground = CreateBrush(offsetForeground); + } + private void OnTimeZoneChanged(object? sender, EventArgs e) { _ = sender;