mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3aee31c6c0 | ||
|
|
435b96c50c | ||
|
|
49b18d6af1 |
@@ -11,6 +11,7 @@ using Avalonia.Input;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Layout;
|
using Avalonia.Layout;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
@@ -48,6 +49,7 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
|||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
private bool _autoRefreshEnabled = true;
|
private bool _autoRefreshEnabled = true;
|
||||||
private string _sourceType = BaiduHotSearchSourceTypes.Official;
|
private string _sourceType = BaiduHotSearchSourceTypes.Official;
|
||||||
|
private bool _isNightVisual = true;
|
||||||
|
|
||||||
private sealed record HotItemVisual(
|
private sealed record HotItemVisual(
|
||||||
Border Host,
|
Border Host,
|
||||||
@@ -79,6 +81,7 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
|||||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||||
|
|
||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
UpdateLanguageCode();
|
UpdateLanguageCode();
|
||||||
@@ -133,6 +136,67 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
|||||||
ApplyCellSize(_currentCellSize);
|
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)
|
private async void OnRefreshTimerTick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
await RefreshHotSearchAsync(forceRefresh: true);
|
await RefreshHotSearchAsync(forceRefresh: true);
|
||||||
@@ -375,6 +439,7 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
|||||||
}
|
}
|
||||||
|
|
||||||
StatusTextBlock.FontSize = Math.Clamp(itemFont, 10, 20);
|
StatusTextBlock.FontSize = Math.Clamp(itemFont, 10, 20);
|
||||||
|
ApplyNightModeVisual();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateInteractionState()
|
private void UpdateInteractionState()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using Avalonia;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
@@ -46,6 +47,7 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
|
|||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
private bool _autoRefreshEnabled = true;
|
private bool _autoRefreshEnabled = true;
|
||||||
|
private bool _isNightVisual = true;
|
||||||
|
|
||||||
private sealed record HotItemVisual(
|
private sealed record HotItemVisual(
|
||||||
Border Host,
|
Border Host,
|
||||||
@@ -78,6 +80,7 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
|
|||||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||||
|
|
||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
UpdateLanguageCode();
|
UpdateLanguageCode();
|
||||||
@@ -129,6 +132,69 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
|
|||||||
ApplyCellSize(_currentCellSize);
|
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)
|
private async void OnRefreshTimerTick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
await RefreshHotSearchAsync(forceRefresh: false);
|
await RefreshHotSearchAsync(forceRefresh: false);
|
||||||
@@ -396,6 +462,7 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
|
|||||||
}
|
}
|
||||||
|
|
||||||
StatusTextBlock.FontSize = Math.Clamp(itemFont, 10, 20);
|
StatusTextBlock.FontSize = Math.Clamp(itemFont, 10, 20);
|
||||||
|
ApplyNightModeVisual();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateInteractionState()
|
private void UpdateInteractionState()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
@@ -12,7 +12,7 @@ using WebViewCore.Events;
|
|||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||||
, IDesktopPageVisibilityAwareComponentWidget
|
, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||||
{
|
{
|
||||||
private static readonly Uri DefaultHomeUri = new("https://www.bing.com");
|
private static readonly Uri DefaultHomeUri = new("https://www.bing.com");
|
||||||
private double _currentCellSize = 48;
|
private double _currentCellSize = 48;
|
||||||
@@ -22,6 +22,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
|||||||
private bool _isEditMode;
|
private bool _isEditMode;
|
||||||
private bool _isWebViewActive = true;
|
private bool _isWebViewActive = true;
|
||||||
private readonly WebView2RuntimeAvailability _runtimeAvailability;
|
private readonly WebView2RuntimeAvailability _runtimeAvailability;
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
public BrowserWidget()
|
public BrowserWidget()
|
||||||
{
|
{
|
||||||
@@ -48,6 +49,26 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
|||||||
NavigateTo(DefaultHomeUri);
|
NavigateTo(DefaultHomeUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
SizeChanged -= OnSizeChanged;
|
||||||
|
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
|
||||||
|
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||||
|
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||||
|
|
||||||
|
if (_runtimeAvailability.IsAvailable)
|
||||||
|
{
|
||||||
|
BrowserWebView.NavigationStarting -= OnBrowserWebViewNavigationStarting;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void ApplyCellSize(double cellSize)
|
public void ApplyCellSize(double cellSize)
|
||||||
{
|
{
|
||||||
_currentCellSize = Math.Max(1, cellSize);
|
_currentCellSize = Math.Max(1, cellSize);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using Avalonia.Input;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
@@ -88,6 +89,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
private bool _autoRotateEnabled = true;
|
private bool _autoRotateEnabled = true;
|
||||||
|
private bool _isNightVisual = true;
|
||||||
|
|
||||||
public CnrDailyNewsWidget()
|
public CnrDailyNewsWidget()
|
||||||
{
|
{
|
||||||
@@ -105,6 +107,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||||
|
|
||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
UpdateLanguageCode();
|
UpdateLanguageCode();
|
||||||
@@ -161,6 +164,66 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
ApplyCellSize(_currentCellSize);
|
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)
|
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_isRefreshing)
|
if (_isRefreshing)
|
||||||
@@ -354,9 +417,11 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
{
|
{
|
||||||
var normalizedTitle = NormalizeCompactText(title);
|
var normalizedTitle = NormalizeCompactText(title);
|
||||||
var hotLabel = L("cnrnews.widget.hot_label", "Hot");
|
var hotLabel = L("cnrnews.widget.hot_label", "Hot");
|
||||||
|
var primaryForeground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327"));
|
||||||
if (News1TitleTextBlock.Inlines is null)
|
if (News1TitleTextBlock.Inlines is null)
|
||||||
{
|
{
|
||||||
News1TitleTextBlock.Text = $"{hotLabel} | {normalizedTitle}";
|
News1TitleTextBlock.Text = $"{hotLabel} | {normalizedTitle}";
|
||||||
|
News1TitleTextBlock.Foreground = primaryForeground;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +433,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
});
|
});
|
||||||
News1TitleTextBlock.Inlines.Add(new Run(normalizedTitle)
|
News1TitleTextBlock.Inlines.Add(new Run(normalizedTitle)
|
||||||
{
|
{
|
||||||
Foreground = new SolidColorBrush(Color.Parse("#202327")),
|
Foreground = primaryForeground,
|
||||||
FontWeight = FontWeight.SemiBold
|
FontWeight = FontWeight.SemiBold
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -401,7 +466,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var textBlock = new TextBlock
|
var textBlock = new TextBlock
|
||||||
{
|
{
|
||||||
Text = NormalizeCompactText(item.Title),
|
Text = NormalizeCompactText(item.Title),
|
||||||
Foreground = new SolidColorBrush(Color.Parse("#202327")),
|
Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")),
|
||||||
FontFamily = MiSansFontFamily,
|
FontFamily = MiSansFontFamily,
|
||||||
FontWeight = FontWeight.SemiBold,
|
FontWeight = FontWeight.SemiBold,
|
||||||
TextWrapping = TextWrapping.Wrap,
|
TextWrapping = TextWrapping.Wrap,
|
||||||
@@ -556,6 +621,8 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
}
|
}
|
||||||
|
|
||||||
ExtraNewsItemsPanel.Spacing = Math.Clamp(6 * scale, 3, 10);
|
ExtraNewsItemsPanel.Spacing = Math.Clamp(6 * scale, 3, 10);
|
||||||
|
|
||||||
|
ApplyNightModeVisual();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateRefreshButtonState()
|
private void UpdateRefreshButtonState()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
Background="#C20A0A"
|
Background="#C20A0A"
|
||||||
Padding="20,16,20,14">
|
Padding="20,16,20,14">
|
||||||
<Grid RowDefinitions="Auto,*,Auto">
|
<Grid RowDefinitions="*,Auto">
|
||||||
<Canvas x:Name="DayDecorationCanvas"
|
<Canvas x:Name="DayDecorationCanvas"
|
||||||
Grid.RowSpan="3"
|
Grid.RowSpan="2"
|
||||||
IsVisible="False"
|
IsVisible="False"
|
||||||
Width="212"
|
Width="212"
|
||||||
Height="148"
|
Height="148"
|
||||||
@@ -43,34 +43,32 @@
|
|||||||
Data="M8,54 L38,24 L64,52 L8,54 Z" />
|
Data="M8,54 L38,24 L64,52 L8,54 Z" />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
<TextBlock x:Name="QuoteMarkTextBlock"
|
<Grid Grid.Row="0" RowDefinitions="Auto,*">
|
||||||
Text="“"
|
<TextBlock x:Name="QuoteMarkTextBlock"
|
||||||
Foreground="#5CFAD0B7"
|
Text="“"
|
||||||
FontSize="96"
|
Foreground="#5CFAD0B7"
|
||||||
FontWeight="SemiBold"
|
FontSize="72"
|
||||||
HorizontalAlignment="Left"
|
FontWeight="SemiBold"
|
||||||
VerticalAlignment="Top"
|
HorizontalAlignment="Left"
|
||||||
Margin="1,0,0,0"
|
VerticalAlignment="Top"
|
||||||
LineHeight="86" />
|
Margin="1,0,0,0"
|
||||||
|
LineHeight="65" />
|
||||||
|
|
||||||
<TextBlock x:Name="PoetryContentTextBlock"
|
<TextBlock x:Name="PoetryContentTextBlock"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Text="芳草年年惹恨幽。想前事悠悠。"
|
Text="芳草年年惹恨幽。想前事悠悠。"
|
||||||
Foreground="#F8D8A8"
|
Foreground="#F8D8A8"
|
||||||
FontSize="54"
|
FontSize="54"
|
||||||
FontWeight="Medium"
|
FontWeight="Medium"
|
||||||
LineHeight="60"
|
LineHeight="60"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
VerticalAlignment="Top"
|
MaxLines="2"
|
||||||
Margin="8,2,0,0" />
|
VerticalAlignment="Top"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Margin="8,2,0,0" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Grid x:Name="AuthorPanel"
|
<Grid Grid.Row="1" ColumnDefinitions="Auto,*" VerticalAlignment="Bottom" Margin="0,6,0,0">
|
||||||
Grid.Row="2"
|
|
||||||
ColumnDefinitions="Auto,*"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Bottom"
|
|
||||||
Margin="0,6,4,0"
|
|
||||||
IsHitTestVisible="False">
|
|
||||||
<Border x:Name="AuthorAccent"
|
<Border x:Name="AuthorAccent"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Width="6"
|
Width="6"
|
||||||
@@ -92,6 +90,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<TextBlock x:Name="StatusTextBlock"
|
<TextBlock x:Name="StatusTextBlock"
|
||||||
|
Grid.Row="0"
|
||||||
Text="Loading..."
|
Text="Loading..."
|
||||||
IsVisible="False"
|
IsVisible="False"
|
||||||
Foreground="#D9FFFFFF"
|
Foreground="#D9FFFFFF"
|
||||||
@@ -100,10 +99,10 @@
|
|||||||
VerticalAlignment="Top" />
|
VerticalAlignment="Top" />
|
||||||
|
|
||||||
<Button x:Name="RefreshButton"
|
<Button x:Name="RefreshButton"
|
||||||
Grid.RowSpan="3"
|
Grid.Row="1"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Bottom"
|
||||||
Margin="0,12,16,0"
|
Margin="0,8,4,0"
|
||||||
Width="42"
|
Width="42"
|
||||||
Height="42"
|
Height="42"
|
||||||
CornerRadius="21"
|
CornerRadius="21"
|
||||||
@@ -113,13 +112,12 @@
|
|||||||
Padding="0"
|
Padding="0"
|
||||||
Focusable="False">
|
Focusable="False">
|
||||||
<TextBlock x:Name="RefreshGlyphTextBlock"
|
<TextBlock x:Name="RefreshGlyphTextBlock"
|
||||||
HorizontalAlignment="Center"
|
Text=""
|
||||||
VerticalAlignment="Center"
|
FontFamily="{StaticResource SymbolFontFamily}"
|
||||||
Text="↻"
|
FontSize="22"
|
||||||
Foreground="#8C9097"
|
Foreground="#8C9097"
|
||||||
FontSize="26"
|
HorizontalAlignment="Center"
|
||||||
FontWeight="SemiLight"
|
VerticalAlignment="Center" />
|
||||||
LineHeight="26" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -45,8 +45,8 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
private const double BaseCellSize = 48d;
|
private const double BaseCellSize = 48d;
|
||||||
private const int BaseWidthCells = 4;
|
private const int BaseWidthCells = 4;
|
||||||
private const int BaseHeightCells = 2;
|
private const int BaseHeightCells = 2;
|
||||||
private const double MinPoetryFontSize = 12;
|
private const double MinPoetryFontSize = 8;
|
||||||
private const double MinAuthorFontSize = 10.5;
|
private const double MinAuthorFontSize = 7;
|
||||||
|
|
||||||
private readonly record struct TextFitResult(double FontSize, FontWeight FontWeight, double LineHeight);
|
private readonly record struct TextFitResult(double FontSize, FontWeight FontWeight, double LineHeight);
|
||||||
|
|
||||||
@@ -109,7 +109,6 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
0,
|
0,
|
||||||
0);
|
0);
|
||||||
|
|
||||||
AuthorPanel.Margin = new Thickness(0, Math.Clamp(5 * scale, 2, 10), Math.Clamp(4 * scale, 2, 8), 0);
|
|
||||||
AuthorAccent.Width = Math.Clamp(6 * scale, 3.2, 9.5);
|
AuthorAccent.Width = Math.Clamp(6 * scale, 3.2, 9.5);
|
||||||
AuthorAccent.Height = Math.Clamp(24 * scale, 12, 34);
|
AuthorAccent.Height = Math.Clamp(24 * scale, 12, 34);
|
||||||
AuthorAccent.Margin = new Thickness(0, 0, Math.Clamp(8 * scale, 4, 13), 0);
|
AuthorAccent.Margin = new Thickness(0, 0, Math.Clamp(8 * scale, 4, 13), 0);
|
||||||
@@ -351,11 +350,6 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
|
|
||||||
AuthorTextBlock.Foreground = CreateBrush("#F4D7A7");
|
AuthorTextBlock.Foreground = CreateBrush("#F4D7A7");
|
||||||
AuthorAccent.Background = CreateBrush("#63F2AF90");
|
AuthorAccent.Background = CreateBrush("#63F2AF90");
|
||||||
AuthorPanel.Margin = new Thickness(
|
|
||||||
0,
|
|
||||||
Math.Clamp(6 * scale, 2, 10),
|
|
||||||
Math.Clamp(6 * scale, 2, 10),
|
|
||||||
Math.Clamp(1 * scale, 0, 3));
|
|
||||||
|
|
||||||
DayDecorationCanvas.IsVisible = false;
|
DayDecorationCanvas.IsVisible = false;
|
||||||
RefreshButton.IsVisible = true;
|
RefreshButton.IsVisible = true;
|
||||||
@@ -380,11 +374,6 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
|
|
||||||
AuthorTextBlock.Foreground = CreateBrush("#272D38");
|
AuthorTextBlock.Foreground = CreateBrush("#272D38");
|
||||||
AuthorAccent.Background = CreateBrush("#C8090D");
|
AuthorAccent.Background = CreateBrush("#C8090D");
|
||||||
AuthorPanel.Margin = new Thickness(
|
|
||||||
0,
|
|
||||||
Math.Clamp(6 * scale, 2, 10),
|
|
||||||
Math.Clamp(6 * scale, 2, 10),
|
|
||||||
Math.Clamp(2 * scale, 0, 4));
|
|
||||||
|
|
||||||
DayDecorationCanvas.IsVisible = true;
|
DayDecorationCanvas.IsVisible = true;
|
||||||
RefreshButton.IsVisible = true;
|
RefreshButton.IsVisible = true;
|
||||||
@@ -475,83 +464,19 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
DayDecorationCanvas.IsVisible = showDayDecorations;
|
DayDecorationCanvas.IsVisible = showDayDecorations;
|
||||||
RefreshButton.IsVisible = true;
|
RefreshButton.IsVisible = true;
|
||||||
|
|
||||||
var refreshReservedWidth = RefreshButton.Width + Math.Clamp(8 * scale, 5, 14);
|
var refreshButtonWidth = 42 + Math.Clamp(8 * scale, 5, 14);
|
||||||
var decorationReservedWidth = showDayDecorations
|
var quoteMarkWidth = QuoteMarkTextBlock.IsVisible ? Math.Clamp(10 * scale, 5, 16) : 0;
|
||||||
? Math.Clamp(innerWidth * 0.24, 34, 96)
|
|
||||||
: 0;
|
var poemWidth = innerWidth - quoteMarkWidth - Math.Clamp(12 * scale, 6, 20);
|
||||||
var quoteReservedWidth = QuoteMarkTextBlock.IsVisible
|
poemWidth = Math.Min(Math.Max(64, poemWidth), innerWidth - Math.Clamp(16 * scale, 8, 24));
|
||||||
? Math.Clamp(10 * scale, 5, 16)
|
|
||||||
: 0;
|
|
||||||
var poemReservedRight = Math.Max(refreshReservedWidth, decorationReservedWidth);
|
|
||||||
var poemWidth = innerWidth - poemReservedRight - quoteReservedWidth;
|
|
||||||
var poemMinWidth = Math.Max(66, innerWidth * 0.56);
|
|
||||||
if (poemWidth < poemMinWidth)
|
|
||||||
{
|
|
||||||
poemWidth = poemMinWidth;
|
|
||||||
}
|
|
||||||
poemWidth = Math.Min(Math.Max(64, poemWidth), innerWidth);
|
|
||||||
|
|
||||||
var authorMaxLines = innerWidth < Math.Max(_currentCellSize * 5.2, 252) ? 2 : 1;
|
var poemMaxLines = 2;
|
||||||
var authorUnitsTarget = authorMaxLines == 1 ? 20 : 12;
|
|
||||||
var authorWidth = Math.Max(72, Math.Min(innerWidth * (isNightMode ? 0.5 : 0.56), innerWidth - 8));
|
|
||||||
var authorPrepared = PrepareAuthorText(_authorRawText, authorUnitsTarget, authorMaxLines);
|
|
||||||
var authorPreferredFontSize = Math.Clamp((isNightMode ? 25 : 23) * scale, 12, 34);
|
|
||||||
var authorMinFontSize = Math.Clamp(authorPreferredFontSize * 0.72, MinAuthorFontSize, authorPreferredFontSize);
|
|
||||||
var authorMinWeight = isNightMode ? 500 : 470;
|
|
||||||
var authorMaxWeight = isNightMode ? 650 : 600;
|
|
||||||
authorPrepared = EnsureTextFitsAtMinSize(
|
|
||||||
preparedText: authorPrepared,
|
|
||||||
sourceText: _authorRawText,
|
|
||||||
targetUnits: authorUnitsTarget,
|
|
||||||
maxLines: authorMaxLines,
|
|
||||||
maxWidth: authorWidth,
|
|
||||||
maxHeight: Math.Max(20, innerHeight * (authorMaxLines > 1 ? 0.38 : 0.28)),
|
|
||||||
minFontSize: authorMinFontSize,
|
|
||||||
minFontWeight: ToVariableWeight(authorMinWeight),
|
|
||||||
lineHeightFactor: 1.12);
|
|
||||||
|
|
||||||
var authorFit = FitTextStable(
|
|
||||||
authorPrepared,
|
|
||||||
authorWidth,
|
|
||||||
Math.Max(20, innerHeight * (authorMaxLines > 1 ? 0.38 : 0.28)),
|
|
||||||
minFontSize: authorMinFontSize,
|
|
||||||
maxFontSize: Math.Clamp(authorPreferredFontSize * 1.15, authorMinFontSize, 42),
|
|
||||||
maxLines: authorMaxLines,
|
|
||||||
lineHeightFactor: 1.12,
|
|
||||||
minWeight: authorMinWeight,
|
|
||||||
maxWeight: authorMaxWeight);
|
|
||||||
|
|
||||||
AuthorTextBlock.Text = authorPrepared;
|
|
||||||
AuthorTextBlock.TextWrapping = authorMaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
|
||||||
AuthorTextBlock.MaxLines = authorMaxLines;
|
|
||||||
AuthorTextBlock.MaxWidth = authorWidth;
|
|
||||||
AuthorTextBlock.FontSize = authorFit.FontSize;
|
|
||||||
AuthorTextBlock.LineHeight = authorFit.LineHeight;
|
|
||||||
AuthorTextBlock.FontWeight = authorFit.FontWeight;
|
|
||||||
AuthorPanel.MaxWidth = authorWidth + AuthorAccent.Width + AuthorAccent.Margin.Right + Math.Clamp(4 * scale, 2, 8);
|
|
||||||
|
|
||||||
var authorMeasured = MeasureTextSize(
|
|
||||||
authorPrepared,
|
|
||||||
authorFit.FontSize,
|
|
||||||
authorFit.FontWeight,
|
|
||||||
authorWidth,
|
|
||||||
authorFit.LineHeight);
|
|
||||||
var authorHeight = Math.Min(authorMeasured.Height, authorFit.LineHeight * authorMaxLines);
|
|
||||||
var authorBlockHeight = Math.Max(authorHeight, AuthorAccent.Height) +
|
|
||||||
AuthorPanel.Margin.Top +
|
|
||||||
AuthorPanel.Margin.Bottom +
|
|
||||||
Math.Clamp(4 * scale, 2, 8);
|
|
||||||
|
|
||||||
var poemMaxLines = innerHeight < _currentCellSize * 1.58
|
|
||||||
? 4
|
|
||||||
: innerHeight < _currentCellSize * 2.05
|
|
||||||
? 3
|
|
||||||
: 2;
|
|
||||||
var poemUnitsTarget = EstimateTargetUnitsPerLine(poemWidth, scale, isNightMode);
|
var poemUnitsTarget = EstimateTargetUnitsPerLine(poemWidth, scale, isNightMode);
|
||||||
var poemPrepared = PreparePoetryText(_poetryRawText, poemUnitsTarget, poemMaxLines);
|
var poemPrepared = PreparePoetryText(_poetryRawText, poemUnitsTarget, poemMaxLines);
|
||||||
var poemHeight = Math.Max(30, innerHeight - authorBlockHeight);
|
|
||||||
var poemPreferredFontSize = Math.Clamp((isNightMode ? 34 : 32) * scale, 16, 56);
|
var availablePoemHeight = innerHeight * 0.72;
|
||||||
var poemMinFontSize = Math.Clamp(poemPreferredFontSize * 0.72, MinPoetryFontSize, poemPreferredFontSize);
|
var poemPreferredFontSize = Math.Clamp((isNightMode ? 34 : 32) * scale, 14, 56);
|
||||||
|
var poemMinFontSize = Math.Clamp(poemPreferredFontSize * 0.65, MinPoetryFontSize, poemPreferredFontSize);
|
||||||
var poemMinWeight = isNightMode ? 540 : 500;
|
var poemMinWeight = isNightMode ? 540 : 500;
|
||||||
var poemMaxWeight = isNightMode ? 760 : 680;
|
var poemMaxWeight = isNightMode ? 760 : 680;
|
||||||
poemPrepared = EnsureTextFitsAtMinSize(
|
poemPrepared = EnsureTextFitsAtMinSize(
|
||||||
@@ -560,19 +485,19 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
targetUnits: poemUnitsTarget,
|
targetUnits: poemUnitsTarget,
|
||||||
maxLines: poemMaxLines,
|
maxLines: poemMaxLines,
|
||||||
maxWidth: poemWidth,
|
maxWidth: poemWidth,
|
||||||
maxHeight: poemHeight,
|
maxHeight: availablePoemHeight,
|
||||||
minFontSize: poemMinFontSize,
|
minFontSize: poemMinFontSize,
|
||||||
minFontWeight: ToVariableWeight(poemMinWeight),
|
minFontWeight: ToVariableWeight(poemMinWeight),
|
||||||
lineHeightFactor: 1.1);
|
lineHeightFactor: 1.12);
|
||||||
|
|
||||||
var poemFit = FitTextStable(
|
var poemFit = FitTextStable(
|
||||||
poemPrepared,
|
poemPrepared,
|
||||||
poemWidth,
|
poemWidth,
|
||||||
poemHeight,
|
availablePoemHeight,
|
||||||
minFontSize: poemMinFontSize,
|
minFontSize: poemMinFontSize,
|
||||||
maxFontSize: Math.Clamp(poemPreferredFontSize * 1.20, poemMinFontSize, 62),
|
maxFontSize: Math.Clamp(poemPreferredFontSize * 1.20, poemMinFontSize, 62),
|
||||||
maxLines: poemMaxLines,
|
maxLines: poemMaxLines,
|
||||||
lineHeightFactor: 1.10,
|
lineHeightFactor: 1.12,
|
||||||
minWeight: poemMinWeight,
|
minWeight: poemMinWeight,
|
||||||
maxWeight: poemMaxWeight);
|
maxWeight: poemMaxWeight);
|
||||||
|
|
||||||
@@ -582,6 +507,43 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
PoetryContentTextBlock.FontSize = poemFit.FontSize;
|
PoetryContentTextBlock.FontSize = poemFit.FontSize;
|
||||||
PoetryContentTextBlock.LineHeight = poemFit.LineHeight;
|
PoetryContentTextBlock.LineHeight = poemFit.LineHeight;
|
||||||
PoetryContentTextBlock.FontWeight = poemFit.FontWeight;
|
PoetryContentTextBlock.FontWeight = poemFit.FontWeight;
|
||||||
|
|
||||||
|
var authorWidth = Math.Max(72, Math.Min(innerWidth * (isNightMode ? 0.5 : 0.56), innerWidth - 8));
|
||||||
|
var authorUnitsTarget = 20;
|
||||||
|
var authorPrepared = PrepareAuthorText(_authorRawText, authorUnitsTarget, 1);
|
||||||
|
var authorPreferredFontSize = Math.Clamp((isNightMode ? 25 : 23) * scale, 10, 34);
|
||||||
|
var authorMinFontSize = Math.Clamp(authorPreferredFontSize * 0.65, MinAuthorFontSize, authorPreferredFontSize);
|
||||||
|
var authorMinWeight = isNightMode ? 500 : 470;
|
||||||
|
var authorMaxWeight = isNightMode ? 650 : 600;
|
||||||
|
authorPrepared = EnsureTextFitsAtMinSize(
|
||||||
|
preparedText: authorPrepared,
|
||||||
|
sourceText: _authorRawText,
|
||||||
|
targetUnits: authorUnitsTarget,
|
||||||
|
maxLines: 1,
|
||||||
|
maxWidth: authorWidth,
|
||||||
|
maxHeight: AuthorAccent.Height,
|
||||||
|
minFontSize: authorMinFontSize,
|
||||||
|
minFontWeight: ToVariableWeight(authorMinWeight),
|
||||||
|
lineHeightFactor: 1.12);
|
||||||
|
|
||||||
|
var authorFit = FitTextStable(
|
||||||
|
authorPrepared,
|
||||||
|
authorWidth,
|
||||||
|
AuthorAccent.Height,
|
||||||
|
minFontSize: authorMinFontSize,
|
||||||
|
maxFontSize: Math.Clamp(authorPreferredFontSize * 1.15, authorMinFontSize, 42),
|
||||||
|
maxLines: 1,
|
||||||
|
lineHeightFactor: 1.12,
|
||||||
|
minWeight: authorMinWeight,
|
||||||
|
maxWeight: authorMaxWeight);
|
||||||
|
|
||||||
|
AuthorTextBlock.Text = authorPrepared;
|
||||||
|
AuthorTextBlock.TextWrapping = TextWrapping.NoWrap;
|
||||||
|
AuthorTextBlock.MaxLines = 1;
|
||||||
|
AuthorTextBlock.MaxWidth = authorWidth;
|
||||||
|
AuthorTextBlock.FontSize = authorFit.FontSize;
|
||||||
|
AuthorTextBlock.LineHeight = authorFit.LineHeight;
|
||||||
|
AuthorTextBlock.FontWeight = authorFit.FontWeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateRefreshButtonState()
|
private void UpdateRefreshButtonState()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Styling;
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
@@ -44,6 +45,7 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
|||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
private bool _autoRefreshEnabled = true;
|
private bool _autoRefreshEnabled = true;
|
||||||
|
private bool _isNightVisual = true;
|
||||||
private bool _isMeaningVisible;
|
private bool _isMeaningVisible;
|
||||||
|
|
||||||
public DailyWord2x2Widget()
|
public DailyWord2x2Widget()
|
||||||
@@ -59,6 +61,7 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
|||||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||||
|
|
||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
UpdateLanguageCode();
|
UpdateLanguageCode();
|
||||||
@@ -113,6 +116,62 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
|||||||
ApplyCellSize(_currentCellSize);
|
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)
|
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_isRefreshing)
|
if (_isRefreshing)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Avalonia;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
@@ -41,6 +42,7 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
private bool _autoRefreshEnabled = true;
|
private bool _autoRefreshEnabled = true;
|
||||||
|
private bool _isNightVisual = true;
|
||||||
|
|
||||||
public DailyWordWidget()
|
public DailyWordWidget()
|
||||||
{
|
{
|
||||||
@@ -58,6 +60,7 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||||
|
|
||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
UpdateLanguageCode();
|
UpdateLanguageCode();
|
||||||
@@ -112,6 +115,64 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
ApplyCellSize(_currentCellSize);
|
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)
|
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_isRefreshing)
|
if (_isRefreshing)
|
||||||
@@ -229,6 +290,14 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
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.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 16, 52));
|
||||||
RootBorder.Padding = new Thickness(0);
|
RootBorder.Padding = new Thickness(0);
|
||||||
|
|
||||||
@@ -261,15 +330,15 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
ExampleTranslationTextBlock.MaxWidth = contentWidth;
|
ExampleTranslationTextBlock.MaxWidth = contentWidth;
|
||||||
|
|
||||||
var compactLayout = totalHeight < _currentCellSize * 1.72;
|
var compactLayout = totalHeight < _currentCellSize * 1.72;
|
||||||
MeaningTextBlock.MaxLines = compactLayout ? 1 : 2;
|
MeaningTextBlock.MaxLines = compactLayout ? 1 : (isFourByThree ? 3 : 2);
|
||||||
ExampleTextBlock.MaxLines = compactLayout ? 1 : 2;
|
ExampleTextBlock.MaxLines = compactLayout ? 1 : (isFourByThree ? 4 : 2);
|
||||||
ExampleTranslationTextBlock.IsVisible = !compactLayout;
|
ExampleTranslationTextBlock.IsVisible = !compactLayout || isFourByThree;
|
||||||
ExampleTranslationTextBlock.MaxLines = 1;
|
ExampleTranslationTextBlock.MaxLines = isFourByThree ? 2 : 1;
|
||||||
|
|
||||||
var contentHeight = Math.Max(52, totalHeight - RootBorder.Padding.Top - RootBorder.Padding.Bottom - CardBorder.Padding.Top - CardBorder.Padding.Bottom);
|
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 wordHeightBudget = Math.Max(18, contentHeight * 0.24);
|
||||||
var pronunciationHeightBudget = Math.Max(14, contentHeight * 0.16);
|
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));
|
var exampleHeightBudget = Math.Max(16, contentHeight - wordHeightBudget - pronunciationHeightBudget - meaningHeightBudget - Math.Clamp(16 * scale, 8, 24));
|
||||||
if (!ExampleTranslationTextBlock.IsVisible)
|
if (!ExampleTranslationTextBlock.IsVisible)
|
||||||
{
|
{
|
||||||
@@ -433,11 +502,26 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
private double ResolveScale()
|
private double ResolveScale()
|
||||||
{
|
{
|
||||||
var cellScale = Math.Clamp(_currentCellSize / BaseCellSize, 0.56, 2.0);
|
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
|
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;
|
: 1;
|
||||||
var heightScale = Bounds.Height > 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;
|
: 1;
|
||||||
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.56, 2.0);
|
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.56, 2.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -13,6 +13,7 @@ using Avalonia.Input;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
@@ -58,6 +59,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
private bool _autoRefreshEnabled = true;
|
private bool _autoRefreshEnabled = true;
|
||||||
|
private bool _isNightVisual = true;
|
||||||
|
|
||||||
private sealed record NewsItemVisual(
|
private sealed record NewsItemVisual(
|
||||||
Border Host,
|
Border Host,
|
||||||
@@ -86,6 +88,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||||
|
|
||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
UpdateLanguageCode();
|
UpdateLanguageCode();
|
||||||
@@ -141,6 +144,67 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
ApplyCellSize(_currentCellSize);
|
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)
|
private async void OnRefreshTimerTick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
await RefreshNewsAsync(forceRefresh: true);
|
await RefreshNewsAsync(forceRefresh: true);
|
||||||
@@ -398,6 +462,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
StatusTextBlock.FontSize = Math.Clamp(titleFont, 10, 20);
|
StatusTextBlock.FontSize = Math.Clamp(titleFont, 10, 20);
|
||||||
|
ApplyNightModeVisual();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateInteractionState()
|
private void UpdateInteractionState()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -8,13 +8,14 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||||
{
|
{
|
||||||
private const int WaveBarCount = 22;
|
private const int WaveBarCount = 22;
|
||||||
|
|
||||||
@@ -36,6 +37,8 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
|||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isOnActivePage = true;
|
private bool _isOnActivePage = true;
|
||||||
private bool _pausedStudyMonitoringForRecording;
|
private bool _pausedStudyMonitoringForRecording;
|
||||||
|
private bool _isNightVisual = true;
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
public RecordingWidget()
|
public RecordingWidget()
|
||||||
{
|
{
|
||||||
@@ -45,6 +48,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
|||||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||||
|
|
||||||
InitializeWaveBars();
|
InitializeWaveBars();
|
||||||
ReloadLanguageCode();
|
ReloadLanguageCode();
|
||||||
@@ -146,6 +150,68 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
|||||||
ApplyCellSize(_currentCellSize);
|
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)
|
private void OnUiTick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (!_isAttached || !_isOnActivePage)
|
if (!_isAttached || !_isOnActivePage)
|
||||||
@@ -291,11 +357,18 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
|||||||
SaveButtonBorder.Opacity = SaveButtonBorder.IsHitTestVisible ? 1 : 0.42;
|
SaveButtonBorder.Opacity = SaveButtonBorder.IsHitTestVisible ? 1 : 0.42;
|
||||||
RecordToggleButtonBorder.Opacity = RecordToggleButtonBorder.IsHitTestVisible ? 1 : 0.54;
|
RecordToggleButtonBorder.Opacity = RecordToggleButtonBorder.IsHitTestVisible ? 1 : 0.54;
|
||||||
|
|
||||||
TimerTextBlock.Foreground = CreateBrush(!isSupported
|
if (!isSupported)
|
||||||
? "#B2B7C0"
|
{
|
||||||
: isReady
|
TimerTextBlock.Foreground = CreateBrush(_isNightVisual ? "#A8B1C2" : "#B2B7C0");
|
||||||
? "#A4A9B2"
|
}
|
||||||
: "#151922");
|
else if (isReady)
|
||||||
|
{
|
||||||
|
TimerTextBlock.Foreground = CreateBrush(_isNightVisual ? "#A8B1C2" : "#A4A9B2");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TimerTextBlock.Foreground = CreateBrush(_isNightVisual ? "#E8EAED" : "#151922");
|
||||||
|
}
|
||||||
HintTextBlock.IsVisible = !isReady || !isSupported;
|
HintTextBlock.IsVisible = !isReady || !isSupported;
|
||||||
|
|
||||||
RecordDot.IsVisible = snapshot.State == AudioRecorderRuntimeState.Ready;
|
RecordDot.IsVisible = snapshot.State == AudioRecorderRuntimeState.Ready;
|
||||||
@@ -540,4 +613,23 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
|||||||
|
|
||||||
return (false, path.LocalPath);
|
return (false, path.LocalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
_uiTimer.Stop();
|
||||||
|
_uiTimer.Tick -= OnUiTick;
|
||||||
|
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||||
|
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||||
|
SizeChanged -= OnSizeChanged;
|
||||||
|
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
|
||||||
|
|
||||||
|
_audioRecorderService.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using Avalonia.Input;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
@@ -60,6 +61,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
private bool _autoRefreshEnabled = true;
|
private bool _autoRefreshEnabled = true;
|
||||||
|
private bool _isNightVisual = true;
|
||||||
|
|
||||||
private sealed record ForumItemVisual(
|
private sealed record ForumItemVisual(
|
||||||
Border Host,
|
Border Host,
|
||||||
@@ -153,6 +155,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||||
|
|
||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
UpdateLanguageCode();
|
UpdateLanguageCode();
|
||||||
@@ -208,6 +211,70 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
ApplyCellSize(_currentCellSize);
|
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)
|
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_isRefreshing)
|
if (_isRefreshing)
|
||||||
@@ -606,6 +673,8 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
|
|
||||||
StatusTextBlock.FontSize = Math.Clamp(14 * softScale, 10, 18);
|
StatusTextBlock.FontSize = Math.Clamp(14 * softScale, 10, 18);
|
||||||
|
|
||||||
|
ApplyNightModeVisual();
|
||||||
|
|
||||||
if (_visibleItemCount != previousVisibleItemCount &&
|
if (_visibleItemCount != previousVisibleItemCount &&
|
||||||
_isAttached &&
|
_isAttached &&
|
||||||
!_isRefreshing &&
|
!_isRefreshing &&
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
@@ -9,7 +9,7 @@ using LanMountainDesktop.Services;
|
|||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
|
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
|
||||||
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
|
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
|
||||||
@@ -27,6 +27,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
|||||||
private string _languageCode = "zh-CN";
|
private string _languageCode = "zh-CN";
|
||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isOnActivePage = true;
|
private bool _isOnActivePage = true;
|
||||||
|
private bool _isDisposed;
|
||||||
private IDisposable? _monitoringLease;
|
private IDisposable? _monitoringLease;
|
||||||
|
|
||||||
public StudyEnvironmentWidget()
|
public StudyEnvironmentWidget()
|
||||||
@@ -63,8 +64,16 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
|||||||
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||||
{
|
{
|
||||||
_ = isEditMode;
|
_ = isEditMode;
|
||||||
|
var wasOnActivePage = _isOnActivePage;
|
||||||
_isOnActivePage = isOnActivePage;
|
_isOnActivePage = isOnActivePage;
|
||||||
|
|
||||||
UpdateMonitoringLeaseState();
|
UpdateMonitoringLeaseState();
|
||||||
|
|
||||||
|
if (isOnActivePage && !wasOnActivePage)
|
||||||
|
{
|
||||||
|
RefreshVisual();
|
||||||
|
}
|
||||||
|
|
||||||
UpdateTimerState();
|
UpdateTimerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +124,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
|||||||
|
|
||||||
private void UpdateMonitoringLeaseState()
|
private void UpdateMonitoringLeaseState()
|
||||||
{
|
{
|
||||||
var shouldMonitor = _isAttached && _isOnActivePage;
|
if (_isAttached)
|
||||||
if (shouldMonitor)
|
|
||||||
{
|
{
|
||||||
_monitoringLease ??= _monitoringLeaseCoordinator.AcquireLease();
|
_monitoringLease ??= _monitoringLeaseCoordinator.AcquireLease();
|
||||||
return;
|
return;
|
||||||
@@ -329,4 +337,23 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
|||||||
|
|
||||||
return CreateBrush(fallbackHex);
|
return CreateBrush(fallbackHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
_uiTimer.Stop();
|
||||||
|
_uiTimer.Tick -= OnUiTimerTick;
|
||||||
|
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||||
|
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||||
|
SizeChanged -= OnSizeChanged;
|
||||||
|
|
||||||
|
_monitoringLease?.Dispose();
|
||||||
|
_monitoringLease = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
@@ -11,7 +11,7 @@ using LanMountainDesktop.Theme;
|
|||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||||
{
|
{
|
||||||
private const double NormalTextMinContrast = 4.5;
|
private const double NormalTextMinContrast = 4.5;
|
||||||
private const double LargeTextMinContrast = 4.5;
|
private const double LargeTextMinContrast = 4.5;
|
||||||
@@ -69,6 +69,7 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
|
|||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isOnActivePage = true;
|
private bool _isOnActivePage = true;
|
||||||
private bool _isSubscribed;
|
private bool _isSubscribed;
|
||||||
|
private bool _isDisposed;
|
||||||
private int _framesSinceCompaction;
|
private int _framesSinceCompaction;
|
||||||
private IDisposable? _monitoringLease;
|
private IDisposable? _monitoringLease;
|
||||||
|
|
||||||
@@ -131,8 +132,20 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
|
|||||||
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||||
{
|
{
|
||||||
_ = isEditMode;
|
_ = isEditMode;
|
||||||
|
var wasOnActivePage = _isOnActivePage;
|
||||||
_isOnActivePage = isOnActivePage;
|
_isOnActivePage = isOnActivePage;
|
||||||
|
|
||||||
UpdateMonitoringLeaseState();
|
UpdateMonitoringLeaseState();
|
||||||
|
|
||||||
|
if (isOnActivePage && !wasOnActivePage)
|
||||||
|
{
|
||||||
|
lock (_snapshotSync)
|
||||||
|
{
|
||||||
|
_pendingSnapshot = _studyAnalyticsService.GetSnapshot();
|
||||||
|
_hasPendingSnapshot = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
UpdateRenderLoopState();
|
UpdateRenderLoopState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,8 +244,7 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
|
|||||||
|
|
||||||
private void UpdateMonitoringLeaseState()
|
private void UpdateMonitoringLeaseState()
|
||||||
{
|
{
|
||||||
var shouldMonitor = _isAttached && _isOnActivePage;
|
if (_isAttached)
|
||||||
if (shouldMonitor)
|
|
||||||
{
|
{
|
||||||
_monitoringLease ??= _monitoringLeaseCoordinator.AcquireLease();
|
_monitoringLease ??= _monitoringLeaseCoordinator.AcquireLease();
|
||||||
return;
|
return;
|
||||||
@@ -553,4 +565,29 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
|
|||||||
{
|
{
|
||||||
return _localizationService.GetString(_languageCode, key, fallback);
|
return _localizationService.GetString(_languageCode, key, fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
_renderTimer.Stop();
|
||||||
|
_renderTimer.Tick -= OnRenderTimerTick;
|
||||||
|
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||||
|
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||||
|
SizeChanged -= OnSizeChanged;
|
||||||
|
|
||||||
|
if (_isSubscribed)
|
||||||
|
{
|
||||||
|
_studyAnalyticsService.SnapshotUpdated -= OnStudySnapshotUpdated;
|
||||||
|
_isSubscribed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_monitoringLease?.Dispose();
|
||||||
|
_monitoringLease = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ public sealed class StudyNoiseDistributionScatterChartControl : Control
|
|||||||
private static readonly Pen GridPen = new(GridBrush, 1);
|
private static readonly Pen GridPen = new(GridBrush, 1);
|
||||||
private static readonly Pen AxisPen = new(AxisBrush, 1.1);
|
private static readonly Pen AxisPen = new(AxisBrush, 1.1);
|
||||||
|
|
||||||
private static readonly IBrush QuietPointBrush = new SolidColorBrush(Color.Parse("#FF34D399"));
|
private static readonly IBrush QuietBrush = new SolidColorBrush(Color.Parse("#FF34D399"));
|
||||||
private static readonly IBrush NormalPointBrush = new SolidColorBrush(Color.Parse("#FF60A5FA"));
|
private static readonly IBrush NormalBrush = new SolidColorBrush(Color.Parse("#FF60A5FA"));
|
||||||
private static readonly IBrush NoisyPointBrush = new SolidColorBrush(Color.Parse("#FFF59E0B"));
|
private static readonly IBrush NoisyBrush = new SolidColorBrush(Color.Parse("#FFF59E0B"));
|
||||||
private static readonly IBrush ExtremePointBrush = new SolidColorBrush(Color.Parse("#FFEF4444"));
|
private static readonly IBrush ExtremeBrush = new SolidColorBrush(Color.Parse("#FFEF4444"));
|
||||||
|
|
||||||
private IReadOnlyList<NoiseRealtimePoint> _points = Array.Empty<NoiseRealtimePoint>();
|
private IReadOnlyList<NoiseRealtimePoint> _points = Array.Empty<NoiseRealtimePoint>();
|
||||||
private double _baselineDb = 45;
|
private double _baselineDb = 45;
|
||||||
@@ -47,34 +47,102 @@ public sealed class StudyNoiseDistributionScatterChartControl : Control
|
|||||||
|
|
||||||
DrawGrid(context, plot);
|
DrawGrid(context, plot);
|
||||||
|
|
||||||
if (_points.Count == 0)
|
if (_points.Count < 2)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DrawElectronCloud(context, plot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawElectronCloud(DrawingContext context, Rect plot)
|
||||||
|
{
|
||||||
var start = _points[0].Timestamp;
|
var start = _points[0].Timestamp;
|
||||||
var end = _points[^1].Timestamp;
|
var end = _points[^1].Timestamp;
|
||||||
var totalTicks = Math.Max(1, (end - start).Ticks);
|
var totalTicks = Math.Max(1, (end - start).Ticks);
|
||||||
|
|
||||||
var maxRenderPoints = Math.Clamp((int)Math.Floor(plot.Width * 1.5), 80, 520);
|
var pointCount = _points.Count;
|
||||||
var step = Math.Max(1, _points.Count / Math.Max(1, maxRenderPoints));
|
var cloudLayers = 8;
|
||||||
var radius = Math.Clamp(Math.Min(plot.Width, plot.Height) / 88d, 1.4, 3.8);
|
var baseRadius = Math.Clamp(Math.Min(plot.Width, plot.Height) / 45d, 3, 12);
|
||||||
|
|
||||||
for (var i = 0; i < _points.Count; i += step)
|
var sortedPoints = new List<(double X, double Y, NoiseDistributionLevel Level)>();
|
||||||
|
for (var i = 0; i < pointCount; i++)
|
||||||
{
|
{
|
||||||
var point = _points[i];
|
var point = _points[i];
|
||||||
var level = ResolveLevel(point.DisplayDb, _baselineDb);
|
|
||||||
var x = MapX(plot, point.Timestamp, start, totalTicks);
|
var x = MapX(plot, point.Timestamp, start, totalTicks);
|
||||||
var y = MapY(plot, level, point.Timestamp);
|
var y = MapYContinuous(plot, point.DisplayDb);
|
||||||
context.DrawEllipse(GetLevelBrush(level), pen: null, center: new Point(x, y), radiusX: radius, radiusY: radius);
|
var level = ResolveLevel(point.DisplayDb, _baselineDb);
|
||||||
|
sortedPoints.Add((x, y, level));
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedPoints.Sort((a, b) => a.X.CompareTo(b.X));
|
||||||
|
|
||||||
|
for (var layer = cloudLayers - 1; layer >= 0; layer--)
|
||||||
|
{
|
||||||
|
var layerRatio = (double)layer / (cloudLayers - 1);
|
||||||
|
var layerRadius = baseRadius * (1.2 + layerRatio * 0.8);
|
||||||
|
var layerAlpha = (byte)(40 + layerRatio * 25);
|
||||||
|
|
||||||
|
foreach (var pt in sortedPoints)
|
||||||
|
{
|
||||||
|
var brush = GetLevelBrushWithAlpha(pt.Level, layerAlpha);
|
||||||
|
var jitterX = ComputeJitter(pt.X * 1000 + layer) * layerRadius * 0.3;
|
||||||
|
var jitterY = ComputeJitter(pt.Y * 1000 + layer) * layerRadius * 0.3;
|
||||||
|
|
||||||
|
context.DrawEllipse(
|
||||||
|
brush,
|
||||||
|
pen: null,
|
||||||
|
center: new Point(pt.X + jitterX, pt.Y + jitterY),
|
||||||
|
radiusX: layerRadius,
|
||||||
|
radiusY: layerRadius * 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var glowLayers = 5;
|
||||||
|
for (var layer = glowLayers - 1; layer >= 0; layer--)
|
||||||
|
{
|
||||||
|
var layerRatio = (double)layer / (glowLayers - 1);
|
||||||
|
var layerRadius = baseRadius * (0.8 + layerRatio * 0.6);
|
||||||
|
var layerAlpha = (byte)(20 + layerRatio * 15);
|
||||||
|
|
||||||
|
foreach (var pt in sortedPoints)
|
||||||
|
{
|
||||||
|
var brush = GetLevelBrushWithAlpha(pt.Level, layerAlpha);
|
||||||
|
context.DrawEllipse(
|
||||||
|
brush,
|
||||||
|
pen: null,
|
||||||
|
center: new Point(pt.X, pt.Y),
|
||||||
|
radiusX: layerRadius,
|
||||||
|
radiusY: layerRadius * 0.6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure latest point is always visible.
|
|
||||||
var latest = _points[^1];
|
var latest = _points[^1];
|
||||||
var latestLevel = ResolveLevel(latest.DisplayDb, _baselineDb);
|
|
||||||
var latestX = MapX(plot, latest.Timestamp, start, totalTicks);
|
var latestX = MapX(plot, latest.Timestamp, start, totalTicks);
|
||||||
var latestY = MapY(plot, latestLevel, latest.Timestamp);
|
var latestY = MapYContinuous(plot, latest.DisplayDb);
|
||||||
context.DrawEllipse(GetLevelBrush(latestLevel), pen: new Pen(Brushes.White, 1), center: new Point(latestX, latestY), radiusX: radius + 0.8, radiusY: radius + 0.8);
|
var latestLevel = ResolveLevel(latest.DisplayDb, _baselineDb);
|
||||||
|
|
||||||
|
for (var i = 3; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var radius = baseRadius * (1.5 + i * 0.8);
|
||||||
|
var alpha = (byte)(30 - i * 6);
|
||||||
|
var glowBrush = GetLevelBrushWithAlpha(latestLevel, alpha);
|
||||||
|
context.DrawEllipse(glowBrush, null, new Point(latestX, latestY), radius, radius * 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.DrawEllipse(
|
||||||
|
GetLevelBrush(latestLevel),
|
||||||
|
new Pen(Brushes.White, 1.5),
|
||||||
|
new Point(latestX, latestY),
|
||||||
|
baseRadius + 1,
|
||||||
|
baseRadius * 0.7 + 1);
|
||||||
|
|
||||||
|
context.DrawEllipse(
|
||||||
|
Brushes.White,
|
||||||
|
null,
|
||||||
|
new Point(latestX, latestY),
|
||||||
|
2,
|
||||||
|
2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DrawGrid(DrawingContext context, Rect plot)
|
private static void DrawGrid(DrawingContext context, Rect plot)
|
||||||
@@ -103,34 +171,28 @@ public sealed class StudyNoiseDistributionScatterChartControl : Control
|
|||||||
return plot.Left + plot.Width * (offsetTicks / (double)totalTicks);
|
return plot.Left + plot.Width * (offsetTicks / (double)totalTicks);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double MapY(Rect plot, NoiseDistributionLevel level, DateTimeOffset timestamp)
|
private double MapYContinuous(Rect plot, double displayDb)
|
||||||
{
|
{
|
||||||
// 4 bands: quiet(bottom) -> extreme(top). Add deterministic jitter in each band.
|
var minDb = _baselineDb - 5;
|
||||||
var bandHeight = plot.Height / 4d;
|
var maxDb = _baselineDb + 25;
|
||||||
var levelIndex = level switch
|
var dbRange = maxDb - minDb;
|
||||||
{
|
if (dbRange <= 0) dbRange = 30;
|
||||||
NoiseDistributionLevel.Quiet => 0,
|
|
||||||
NoiseDistributionLevel.Normal => 1,
|
|
||||||
NoiseDistributionLevel.Noisy => 2,
|
|
||||||
NoiseDistributionLevel.Extreme => 3,
|
|
||||||
_ => 1
|
|
||||||
};
|
|
||||||
|
|
||||||
var centerY = plot.Bottom - ((levelIndex + 0.5) * bandHeight);
|
var normalizedDb = (displayDb - minDb) / dbRange;
|
||||||
var jitter = ComputeJitter(timestamp.Ticks) * bandHeight * 0.26;
|
normalizedDb = Math.Clamp(normalizedDb, 0, 1);
|
||||||
return Math.Clamp(centerY + jitter, plot.Top + 1.5, plot.Bottom - 1.5);
|
|
||||||
|
return plot.Bottom - (normalizedDb * plot.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double ComputeJitter(long ticks)
|
private static double ComputeJitter(double value)
|
||||||
{
|
{
|
||||||
// Deterministic pseudo-random value in [-1, 1] to avoid overlap without animation noise.
|
var hash = (ulong)(value * 1000000);
|
||||||
var value = (ulong)ticks;
|
hash ^= hash >> 33;
|
||||||
value ^= value >> 33;
|
hash *= 0xff51afd7ed558ccdUL;
|
||||||
value *= 0xff51afd7ed558ccdUL;
|
hash ^= hash >> 33;
|
||||||
value ^= value >> 33;
|
hash *= 0xc4ceb9fe1a85ec53UL;
|
||||||
value *= 0xc4ceb9fe1a85ec53UL;
|
hash ^= hash >> 33;
|
||||||
value ^= value >> 33;
|
var normalized = (hash & 0xFFFF) / 65535d;
|
||||||
var normalized = (value & 0xFFFF) / 65535d;
|
|
||||||
return (normalized * 2d) - 1d;
|
return (normalized * 2d) - 1d;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,11 +224,23 @@ public sealed class StudyNoiseDistributionScatterChartControl : Control
|
|||||||
{
|
{
|
||||||
return level switch
|
return level switch
|
||||||
{
|
{
|
||||||
NoiseDistributionLevel.Quiet => QuietPointBrush,
|
NoiseDistributionLevel.Quiet => QuietBrush,
|
||||||
NoiseDistributionLevel.Normal => NormalPointBrush,
|
NoiseDistributionLevel.Normal => NormalBrush,
|
||||||
NoiseDistributionLevel.Noisy => NoisyPointBrush,
|
NoiseDistributionLevel.Noisy => NoisyBrush,
|
||||||
NoiseDistributionLevel.Extreme => ExtremePointBrush,
|
NoiseDistributionLevel.Extreme => ExtremeBrush,
|
||||||
_ => NormalPointBrush
|
_ => NormalBrush
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IBrush GetLevelBrushWithAlpha(NoiseDistributionLevel level, byte alpha)
|
||||||
|
{
|
||||||
|
return level switch
|
||||||
|
{
|
||||||
|
NoiseDistributionLevel.Quiet => new SolidColorBrush(Color.FromArgb(alpha, 0x34, 0xD3, 0x99)),
|
||||||
|
NoiseDistributionLevel.Normal => new SolidColorBrush(Color.FromArgb(alpha, 0x60, 0xA5, 0xFA)),
|
||||||
|
NoiseDistributionLevel.Noisy => new SolidColorBrush(Color.FromArgb(alpha, 0xF5, 0x9E, 0x0B)),
|
||||||
|
NoiseDistributionLevel.Extreme => new SolidColorBrush(Color.FromArgb(alpha, 0xEF, 0x44, 0x44)),
|
||||||
|
_ => new SolidColorBrush(Color.FromArgb(alpha, 0x60, 0xA5, 0xFA))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -12,7 +12,7 @@ using LanMountainDesktop.Theme;
|
|||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
public partial class StudyNoiseDistributionWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
public partial class StudyNoiseDistributionWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||||
{
|
{
|
||||||
private static readonly Color[] ValueColorCandidates =
|
private static readonly Color[] ValueColorCandidates =
|
||||||
{
|
{
|
||||||
@@ -46,13 +46,14 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
|||||||
private readonly LocalizationService _localizationService = new();
|
private readonly LocalizationService _localizationService = new();
|
||||||
private readonly DispatcherTimer _uiTimer = new()
|
private readonly DispatcherTimer _uiTimer = new()
|
||||||
{
|
{
|
||||||
Interval = TimeSpan.FromMilliseconds(250)
|
Interval = TimeSpan.FromMilliseconds(100)
|
||||||
};
|
};
|
||||||
|
|
||||||
private double _currentCellSize = 48;
|
private double _currentCellSize = 48;
|
||||||
private string _languageCode = "zh-CN";
|
private string _languageCode = "zh-CN";
|
||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isOnActivePage = true;
|
private bool _isOnActivePage = true;
|
||||||
|
private bool _isDisposed;
|
||||||
private bool _isCompactMode;
|
private bool _isCompactMode;
|
||||||
private bool _isUltraCompactMode;
|
private bool _isUltraCompactMode;
|
||||||
private IDisposable? _monitoringLease;
|
private IDisposable? _monitoringLease;
|
||||||
@@ -92,8 +93,16 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
|||||||
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||||
{
|
{
|
||||||
_ = isEditMode;
|
_ = isEditMode;
|
||||||
|
var wasOnActivePage = _isOnActivePage;
|
||||||
_isOnActivePage = isOnActivePage;
|
_isOnActivePage = isOnActivePage;
|
||||||
|
|
||||||
UpdateMonitoringLeaseState();
|
UpdateMonitoringLeaseState();
|
||||||
|
|
||||||
|
if (isOnActivePage && !wasOnActivePage)
|
||||||
|
{
|
||||||
|
RefreshVisual();
|
||||||
|
}
|
||||||
|
|
||||||
UpdateTimerState();
|
UpdateTimerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,8 +151,7 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
|||||||
|
|
||||||
private void UpdateMonitoringLeaseState()
|
private void UpdateMonitoringLeaseState()
|
||||||
{
|
{
|
||||||
var shouldMonitor = _isAttached && _isOnActivePage;
|
if (_isAttached)
|
||||||
if (shouldMonitor)
|
|
||||||
{
|
{
|
||||||
_monitoringLease ??= _monitoringLeaseCoordinator.AcquireLease();
|
_monitoringLease ??= _monitoringLeaseCoordinator.AcquireLease();
|
||||||
return;
|
return;
|
||||||
@@ -604,6 +612,25 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
|||||||
{
|
{
|
||||||
return _localizationService.GetString(_languageCode, key, fallback);
|
return _localizationService.GetString(_languageCode, key, fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
_uiTimer.Stop();
|
||||||
|
_uiTimer.Tick -= OnUiTimerTick;
|
||||||
|
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||||
|
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||||
|
SizeChanged -= OnSizeChanged;
|
||||||
|
|
||||||
|
_monitoringLease?.Dispose();
|
||||||
|
_monitoringLease = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ using Material.Icons;
|
|||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
public partial class StudySessionControlWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
public partial class StudySessionControlWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||||
{
|
{
|
||||||
private static readonly Color[] PrimaryColorCandidates =
|
private static readonly Color[] PrimaryColorCandidates =
|
||||||
{
|
{
|
||||||
@@ -61,6 +61,7 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
|
|||||||
private string _languageCode = "zh-CN";
|
private string _languageCode = "zh-CN";
|
||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isOnActivePage = true;
|
private bool _isOnActivePage = true;
|
||||||
|
private bool _isDisposed;
|
||||||
private bool _isCompactMode;
|
private bool _isCompactMode;
|
||||||
private bool _isUltraCompactMode;
|
private bool _isUltraCompactMode;
|
||||||
private IDisposable? _monitoringLease;
|
private IDisposable? _monitoringLease;
|
||||||
@@ -468,4 +469,20 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
|
|||||||
{
|
{
|
||||||
return _localizationService.GetString(_languageCode, key, fallback);
|
return _localizationService.GetString(_languageCode, key, fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
_uiTimer.Stop();
|
||||||
|
_uiTimer.Tick -= OnUiTimerTick;
|
||||||
|
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||||
|
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||||
|
SizeChanged -= OnSizeChanged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
@@ -15,7 +15,7 @@ using LanMountainDesktop.Theme;
|
|||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||||
{
|
{
|
||||||
private const double MinTextContrast = 4.5;
|
private const double MinTextContrast = 4.5;
|
||||||
private enum HistoryDialogMode
|
private enum HistoryDialogMode
|
||||||
@@ -55,6 +55,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
|||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isOnActivePage = true;
|
private bool _isOnActivePage = true;
|
||||||
private bool _isSubscribed;
|
private bool _isSubscribed;
|
||||||
|
private bool _isDisposed;
|
||||||
private bool _isCompactMode;
|
private bool _isCompactMode;
|
||||||
private bool _isUltraCompactMode;
|
private bool _isUltraCompactMode;
|
||||||
private string? _loadingSessionId;
|
private string? _loadingSessionId;
|
||||||
@@ -733,6 +734,29 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
|||||||
|
|
||||||
return min;
|
return min;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||||
|
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||||
|
SizeChanged -= OnSizeChanged;
|
||||||
|
DialogCancelButton.Click -= (_, _) => CloseDialog();
|
||||||
|
DialogConfirmButton.Click -= (_, _) => ConfirmDialog();
|
||||||
|
DialogRenameTextBox.KeyDown -= OnDialogRenameTextBoxKeyDown;
|
||||||
|
|
||||||
|
if (_isSubscribed)
|
||||||
|
{
|
||||||
|
_studyAnalyticsService.SnapshotUpdated -= OnStudySnapshotUpdated;
|
||||||
|
_isSubscribed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -69,6 +69,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
|||||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||||
|
|
||||||
InitializeDialIfNeeded();
|
InitializeDialIfNeeded();
|
||||||
InitializeHandsIfNeeded();
|
InitializeHandsIfNeeded();
|
||||||
@@ -238,6 +239,12 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
|||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_isNightModeApplied = null;
|
||||||
|
ApplyModeVisualIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnClockTimerTick(object? sender, EventArgs e)
|
private void OnClockTimerTick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
UpdateClockVisual();
|
UpdateClockVisual();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Controls.Shapes;
|
using Avalonia.Controls.Shapes;
|
||||||
using Avalonia.Layout;
|
using Avalonia.Layout;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
@@ -81,6 +82,8 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
|
|||||||
public required TextBlock OffsetTextBlock { get; init; }
|
public required TextBlock OffsetTextBlock { get; init; }
|
||||||
|
|
||||||
public bool? IsNightApplied { get; set; }
|
public bool? IsNightApplied { get; set; }
|
||||||
|
|
||||||
|
public bool? IsSystemNightApplied { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly DispatcherTimer _clockTimer = new()
|
private readonly DispatcherTimer _clockTimer = new()
|
||||||
@@ -99,6 +102,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
|
|||||||
private double _currentCellSize = BaseCellSize;
|
private double _currentCellSize = BaseCellSize;
|
||||||
private DateTime _nextLanguageProbeUtc = DateTime.MinValue;
|
private DateTime _nextLanguageProbeUtc = DateTime.MinValue;
|
||||||
private string _secondHandMode = ClockSecondHandMode.Tick;
|
private string _secondHandMode = ClockSecondHandMode.Tick;
|
||||||
|
private bool _isNightVisual = true;
|
||||||
|
|
||||||
public WorldClockWidget()
|
public WorldClockWidget()
|
||||||
{
|
{
|
||||||
@@ -114,6 +118,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
|
|||||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||||
@@ -211,6 +216,79 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
|
|||||||
ApplyCellSize(_currentCellSize);
|
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)
|
private void OnTimeZoneChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
_ = sender;
|
_ = sender;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
@@ -681,23 +681,33 @@ public partial class MainWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
ClearTimeZoneServiceBindings(_selectedDesktopComponentHost);
|
ClearTimeZoneServiceBindings(_selectedDesktopComponentHost);
|
||||||
|
DisposeComponentIfNeeded(_selectedDesktopComponentHost);
|
||||||
|
|
||||||
if (_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
|
if (_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
|
||||||
{
|
{
|
||||||
pageGrid.Children.Remove(_selectedDesktopComponentHost);
|
pageGrid.Children.Remove(_selectedDesktopComponentHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from persisted placement list as well.
|
|
||||||
_desktopComponentPlacements.Remove(placement);
|
_desktopComponentPlacements.Remove(placement);
|
||||||
|
|
||||||
ClearDesktopComponentSelection();
|
ClearDesktopComponentSelection();
|
||||||
|
|
||||||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||||||
|
|
||||||
// 娣囨繂鐡ㄧ拋鍓х枂
|
|
||||||
PersistSettings();
|
PersistSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void DisposeComponentIfNeeded(Border host)
|
||||||
|
{
|
||||||
|
if (TryGetContentHost(host) is Border contentHost && contentHost.Child is Control componentControl)
|
||||||
|
{
|
||||||
|
if (componentControl is IDisposable disposableComponent)
|
||||||
|
{
|
||||||
|
disposableComponent.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OpenComponentSettings()
|
private void OpenComponentSettings()
|
||||||
{
|
{
|
||||||
if (_selectedDesktopComponentHost is null || _selectedDesktopComponentHost.Tag is not string placementId)
|
if (_selectedDesktopComponentHost is null || _selectedDesktopComponentHost.Tag is not string placementId)
|
||||||
@@ -1389,6 +1399,10 @@ public partial class MainWindow
|
|||||||
if (_desktopPageComponentGrids.TryGetValue(_currentDesktopSurfaceIndex, out var pageGrid))
|
if (_desktopPageComponentGrids.TryGetValue(_currentDesktopSurfaceIndex, out var pageGrid))
|
||||||
{
|
{
|
||||||
ClearTimeZoneServiceBindings(pageGrid.Children.OfType<Control>().ToList());
|
ClearTimeZoneServiceBindings(pageGrid.Children.OfType<Control>().ToList());
|
||||||
|
foreach (var child in pageGrid.Children.OfType<Border>())
|
||||||
|
{
|
||||||
|
DisposeComponentIfNeeded(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var placement in placementsToRemove)
|
foreach (var placement in placementsToRemove)
|
||||||
|
|||||||
@@ -11,6 +11,74 @@ ICONS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/256x256/apps"
|
|||||||
DESKTOP_TARGET="$APPLICATIONS_DIR/LanMountainDesktop.desktop"
|
DESKTOP_TARGET="$APPLICATIONS_DIR/LanMountainDesktop.desktop"
|
||||||
ICON_TARGET="$ICONS_DIR/lanmountaindesktop.png"
|
ICON_TARGET="$ICONS_DIR/lanmountaindesktop.png"
|
||||||
|
|
||||||
|
check_audio_dependencies() {
|
||||||
|
MISSING_DEPS=""
|
||||||
|
|
||||||
|
if command -v dpkg >/dev/null 2>&1; then
|
||||||
|
if ! dpkg -s libportaudio2 >/dev/null 2>&1; then
|
||||||
|
MISSING_DEPS="$MISSING_DEPS libportaudio2"
|
||||||
|
fi
|
||||||
|
if ! dpkg -s libasound2 >/dev/null 2>&1; then
|
||||||
|
MISSING_DEPS="$MISSING_DEPS libasound2"
|
||||||
|
fi
|
||||||
|
elif command -v rpm >/dev/null 2>&1; then
|
||||||
|
if ! rpm -q portaudio-libs >/dev/null 2>&1; then
|
||||||
|
MISSING_DEPS="$MISSING_DEPS portaudio-libs"
|
||||||
|
fi
|
||||||
|
if ! rpm -q alsa-lib >/dev/null 2>&1; then
|
||||||
|
MISSING_DEPS="$MISSING_DEPS alsa-lib"
|
||||||
|
fi
|
||||||
|
elif command -v pacman >/dev/null 2>&1; then
|
||||||
|
if ! pacman -Q portaudio >/dev/null 2>&1; then
|
||||||
|
MISSING_DEPS="$MISSING_DEPS portaudio"
|
||||||
|
fi
|
||||||
|
if ! pacman -Q alsa-lib >/dev/null 2>&1; then
|
||||||
|
MISSING_DEPS="$MISSING_DEPS alsa-lib"
|
||||||
|
fi
|
||||||
|
elif command -v apk >/dev/null 2>&1; then
|
||||||
|
if ! apk -e info portaudio >/dev/null 2>&1; then
|
||||||
|
MISSING_DEPS="$MISSING_DEPS portaudio"
|
||||||
|
fi
|
||||||
|
if ! apk -e info alsa-lib >/dev/null 2>&1; then
|
||||||
|
MISSING_DEPS="$MISSING_DEPS alsa-lib"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$MISSING_DEPS" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
install_audio_dependencies() {
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libportaudio2 libasound2
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
sudo dnf install -y portaudio-libs alsa-lib
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
sudo yum install -y portaudio-libs alsa-lib
|
||||||
|
elif command -v pacman >/dev/null 2>&1; then
|
||||||
|
sudo pacman -S --noconfirm portaudio alsa-lib
|
||||||
|
elif command -v apk >/dev/null 2>&1; then
|
||||||
|
sudo apk add portaudio alsa-lib
|
||||||
|
else
|
||||||
|
printf '%s\n' "Warning: Could not detect package manager. Please install audio dependencies manually:"
|
||||||
|
printf '%s\n' " - libportaudio2 (or portaudio-libs/portaudio)"
|
||||||
|
printf '%s\n' " - libasound2 (or alsa-lib)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! check_audio_dependencies; then
|
||||||
|
printf '%s\n' "Installing audio dependencies for recording features..."
|
||||||
|
install_audio_dependencies
|
||||||
|
|
||||||
|
if ! check_audio_dependencies; then
|
||||||
|
printf '%s\n' "Warning: Audio dependencies may not be installed correctly."
|
||||||
|
printf '%s\n' "Recording and study monitoring features may not work properly."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "$APPLICATIONS_DIR" "$ICONS_DIR"
|
mkdir -p "$APPLICATIONS_DIR" "$ICONS_DIR"
|
||||||
|
|
||||||
cp "$ICON_SOURCE" "$ICON_TARGET"
|
cp "$ICON_SOURCE" "$ICON_TARGET"
|
||||||
|
|||||||
26
run.md
26
run.md
@@ -26,3 +26,29 @@ dotnet run --project LanMountainDesktop/LanMountainDesktop.csproj
|
|||||||
- 启动失败提示 SDK 版本不匹配:确认 `dotnet --info` 中已安装 .NET 10 SDK。
|
- 启动失败提示 SDK 版本不匹配:确认 `dotnet --info` 中已安装 .NET 10 SDK。
|
||||||
- 桌面端视频相关能力异常:优先在 Windows 环境下验证。
|
- 桌面端视频相关能力异常:优先在 Windows 环境下验证。
|
||||||
- 配置重置:删除 `%LOCALAPPDATA%\LanMountainDesktop\settings.json` 后重启应用。
|
- 配置重置:删除 `%LOCALAPPDATA%\LanMountainDesktop\settings.json` 后重启应用。
|
||||||
|
|
||||||
|
## 6. Linux 音频功能依赖
|
||||||
|
|
||||||
|
如果在 Linux 上使用录音机组件或自习监测组件,需要安装以下音频库:
|
||||||
|
|
||||||
|
### Debian/Ubuntu
|
||||||
|
```bash
|
||||||
|
sudo apt install libportaudio2 libasound2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fedora/RHEL
|
||||||
|
```bash
|
||||||
|
sudo dnf install portaudio-libs alsa-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arch Linux
|
||||||
|
```bash
|
||||||
|
sudo pacman -S portaudio alsa-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alpine Linux
|
||||||
|
```bash
|
||||||
|
sudo apk add portaudio alsa-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注:如果未安装这些依赖,录音和自习监测功能将不可用,但应用其他功能可以正常运行。
|
||||||
|
|||||||
Reference in New Issue
Block a user