mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
0.4.9.3
内存泄露问题解决
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
@@ -12,7 +12,7 @@ using WebViewCore.Events;
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
, IDesktopPageVisibilityAwareComponentWidget
|
||||
, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||
{
|
||||
private static readonly Uri DefaultHomeUri = new("https://www.bing.com");
|
||||
private double _currentCellSize = 48;
|
||||
@@ -22,6 +22,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
private bool _isEditMode;
|
||||
private bool _isWebViewActive = true;
|
||||
private readonly WebView2RuntimeAvailability _runtimeAvailability;
|
||||
private bool _isDisposed;
|
||||
|
||||
public BrowserWidget()
|
||||
{
|
||||
@@ -48,6 +49,26 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
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)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -14,9 +14,9 @@
|
||||
BorderThickness="0"
|
||||
Background="#C20A0A"
|
||||
Padding="20,16,20,14">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<Canvas x:Name="DayDecorationCanvas"
|
||||
Grid.RowSpan="3"
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="False"
|
||||
Width="212"
|
||||
Height="148"
|
||||
@@ -43,34 +43,32 @@
|
||||
Data="M8,54 L38,24 L64,52 L8,54 Z" />
|
||||
</Canvas>
|
||||
|
||||
<TextBlock x:Name="QuoteMarkTextBlock"
|
||||
Text="“"
|
||||
Foreground="#5CFAD0B7"
|
||||
FontSize="96"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Margin="1,0,0,0"
|
||||
LineHeight="86" />
|
||||
<Grid Grid.Row="0" RowDefinitions="Auto,*">
|
||||
<TextBlock x:Name="QuoteMarkTextBlock"
|
||||
Text="“"
|
||||
Foreground="#5CFAD0B7"
|
||||
FontSize="72"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Margin="1,0,0,0"
|
||||
LineHeight="65" />
|
||||
|
||||
<TextBlock x:Name="PoetryContentTextBlock"
|
||||
Grid.Row="1"
|
||||
Text="芳草年年惹恨幽。想前事悠悠。"
|
||||
Foreground="#F8D8A8"
|
||||
FontSize="54"
|
||||
FontWeight="Medium"
|
||||
LineHeight="60"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Top"
|
||||
Margin="8,2,0,0" />
|
||||
<TextBlock x:Name="PoetryContentTextBlock"
|
||||
Grid.Row="1"
|
||||
Text="芳草年年惹恨幽。想前事悠悠。"
|
||||
Foreground="#F8D8A8"
|
||||
FontSize="54"
|
||||
FontWeight="Medium"
|
||||
LineHeight="60"
|
||||
TextWrapping="Wrap"
|
||||
MaxLines="2"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="8,2,0,0" />
|
||||
</Grid>
|
||||
|
||||
<Grid x:Name="AuthorPanel"
|
||||
Grid.Row="2"
|
||||
ColumnDefinitions="Auto,*"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,6,4,0"
|
||||
IsHitTestVisible="False">
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*" VerticalAlignment="Bottom" Margin="0,6,0,0">
|
||||
<Border x:Name="AuthorAccent"
|
||||
Grid.Column="0"
|
||||
Width="6"
|
||||
@@ -92,6 +90,7 @@
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="StatusTextBlock"
|
||||
Grid.Row="0"
|
||||
Text="Loading..."
|
||||
IsVisible="False"
|
||||
Foreground="#D9FFFFFF"
|
||||
@@ -100,10 +99,10 @@
|
||||
VerticalAlignment="Top" />
|
||||
|
||||
<Button x:Name="RefreshButton"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,12,16,0"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,8,4,0"
|
||||
Width="42"
|
||||
Height="42"
|
||||
CornerRadius="21"
|
||||
@@ -113,13 +112,12 @@
|
||||
Padding="0"
|
||||
Focusable="False">
|
||||
<TextBlock x:Name="RefreshGlyphTextBlock"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="↻"
|
||||
Text=""
|
||||
FontFamily="{StaticResource SymbolFontFamily}"
|
||||
FontSize="22"
|
||||
Foreground="#8C9097"
|
||||
FontSize="26"
|
||||
FontWeight="SemiLight"
|
||||
LineHeight="26" />
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -45,8 +45,8 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
private const double BaseCellSize = 48d;
|
||||
private const int BaseWidthCells = 4;
|
||||
private const int BaseHeightCells = 2;
|
||||
private const double MinPoetryFontSize = 12;
|
||||
private const double MinAuthorFontSize = 10.5;
|
||||
private const double MinPoetryFontSize = 8;
|
||||
private const double MinAuthorFontSize = 7;
|
||||
|
||||
private readonly record struct TextFitResult(double FontSize, FontWeight FontWeight, double LineHeight);
|
||||
|
||||
@@ -109,7 +109,6 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
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.Height = Math.Clamp(24 * scale, 12, 34);
|
||||
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");
|
||||
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;
|
||||
RefreshButton.IsVisible = true;
|
||||
@@ -380,11 +374,6 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
|
||||
AuthorTextBlock.Foreground = CreateBrush("#272D38");
|
||||
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;
|
||||
RefreshButton.IsVisible = true;
|
||||
@@ -475,83 +464,19 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
DayDecorationCanvas.IsVisible = showDayDecorations;
|
||||
RefreshButton.IsVisible = true;
|
||||
|
||||
var refreshReservedWidth = RefreshButton.Width + Math.Clamp(8 * scale, 5, 14);
|
||||
var decorationReservedWidth = showDayDecorations
|
||||
? Math.Clamp(innerWidth * 0.24, 34, 96)
|
||||
: 0;
|
||||
var quoteReservedWidth = QuoteMarkTextBlock.IsVisible
|
||||
? 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 refreshButtonWidth = 42 + Math.Clamp(8 * scale, 5, 14);
|
||||
var quoteMarkWidth = QuoteMarkTextBlock.IsVisible ? Math.Clamp(10 * scale, 5, 16) : 0;
|
||||
|
||||
var poemWidth = innerWidth - quoteMarkWidth - Math.Clamp(12 * scale, 6, 20);
|
||||
poemWidth = Math.Min(Math.Max(64, poemWidth), innerWidth - Math.Clamp(16 * scale, 8, 24));
|
||||
|
||||
var authorMaxLines = innerWidth < Math.Max(_currentCellSize * 5.2, 252) ? 2 : 1;
|
||||
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 poemMaxLines = 2;
|
||||
var poemUnitsTarget = EstimateTargetUnitsPerLine(poemWidth, scale, isNightMode);
|
||||
var poemPrepared = PreparePoetryText(_poetryRawText, poemUnitsTarget, poemMaxLines);
|
||||
var poemHeight = Math.Max(30, innerHeight - authorBlockHeight);
|
||||
var poemPreferredFontSize = Math.Clamp((isNightMode ? 34 : 32) * scale, 16, 56);
|
||||
var poemMinFontSize = Math.Clamp(poemPreferredFontSize * 0.72, MinPoetryFontSize, poemPreferredFontSize);
|
||||
|
||||
var availablePoemHeight = innerHeight * 0.72;
|
||||
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 poemMaxWeight = isNightMode ? 760 : 680;
|
||||
poemPrepared = EnsureTextFitsAtMinSize(
|
||||
@@ -560,19 +485,19 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
targetUnits: poemUnitsTarget,
|
||||
maxLines: poemMaxLines,
|
||||
maxWidth: poemWidth,
|
||||
maxHeight: poemHeight,
|
||||
maxHeight: availablePoemHeight,
|
||||
minFontSize: poemMinFontSize,
|
||||
minFontWeight: ToVariableWeight(poemMinWeight),
|
||||
lineHeightFactor: 1.1);
|
||||
lineHeightFactor: 1.12);
|
||||
|
||||
var poemFit = FitTextStable(
|
||||
poemPrepared,
|
||||
poemWidth,
|
||||
poemHeight,
|
||||
availablePoemHeight,
|
||||
minFontSize: poemMinFontSize,
|
||||
maxFontSize: Math.Clamp(poemPreferredFontSize * 1.20, poemMinFontSize, 62),
|
||||
maxLines: poemMaxLines,
|
||||
lineHeightFactor: 1.10,
|
||||
lineHeightFactor: 1.12,
|
||||
minWeight: poemMinWeight,
|
||||
maxWeight: poemMaxWeight);
|
||||
|
||||
@@ -582,6 +507,43 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
PoetryContentTextBlock.FontSize = poemFit.FontSize;
|
||||
PoetryContentTextBlock.LineHeight = poemFit.LineHeight;
|
||||
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()
|
||||
|
||||
@@ -15,7 +15,7 @@ using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
||||
public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||
{
|
||||
private const int WaveBarCount = 22;
|
||||
|
||||
@@ -38,6 +38,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
||||
private bool _isOnActivePage = true;
|
||||
private bool _pausedStudyMonitoringForRecording;
|
||||
private bool _isNightVisual = true;
|
||||
private bool _isDisposed;
|
||||
|
||||
public RecordingWidget()
|
||||
{
|
||||
@@ -612,4 +613,23 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
@@ -9,7 +9,7 @@ using LanMountainDesktop.Services;
|
||||
|
||||
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 StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
|
||||
@@ -27,6 +27,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
||||
private string _languageCode = "zh-CN";
|
||||
private bool _isAttached;
|
||||
private bool _isOnActivePage = true;
|
||||
private bool _isDisposed;
|
||||
private IDisposable? _monitoringLease;
|
||||
|
||||
public StudyEnvironmentWidget()
|
||||
@@ -329,4 +330,23 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ public sealed class StudyNoiseDistributionScatterChartControl : Control
|
||||
private static readonly Pen GridPen = new(GridBrush, 1);
|
||||
private static readonly Pen AxisPen = new(AxisBrush, 1.1);
|
||||
|
||||
private static readonly IBrush QuietPointBrush = new SolidColorBrush(Color.Parse("#FF34D399"));
|
||||
private static readonly IBrush NormalPointBrush = new SolidColorBrush(Color.Parse("#FF60A5FA"));
|
||||
private static readonly IBrush NoisyPointBrush = new SolidColorBrush(Color.Parse("#FFF59E0B"));
|
||||
private static readonly IBrush ExtremePointBrush = new SolidColorBrush(Color.Parse("#FFEF4444"));
|
||||
private static readonly IBrush QuietBrush = new SolidColorBrush(Color.Parse("#FF34D399"));
|
||||
private static readonly IBrush NormalBrush = new SolidColorBrush(Color.Parse("#FF60A5FA"));
|
||||
private static readonly IBrush NoisyBrush = new SolidColorBrush(Color.Parse("#FFF59E0B"));
|
||||
private static readonly IBrush ExtremeBrush = new SolidColorBrush(Color.Parse("#FFEF4444"));
|
||||
|
||||
private IReadOnlyList<NoiseRealtimePoint> _points = Array.Empty<NoiseRealtimePoint>();
|
||||
private double _baselineDb = 45;
|
||||
@@ -47,34 +47,102 @@ public sealed class StudyNoiseDistributionScatterChartControl : Control
|
||||
|
||||
DrawGrid(context, plot);
|
||||
|
||||
if (_points.Count == 0)
|
||||
if (_points.Count < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawElectronCloud(context, plot);
|
||||
}
|
||||
|
||||
private void DrawElectronCloud(DrawingContext context, Rect plot)
|
||||
{
|
||||
var start = _points[0].Timestamp;
|
||||
var end = _points[^1].Timestamp;
|
||||
var totalTicks = Math.Max(1, (end - start).Ticks);
|
||||
|
||||
var maxRenderPoints = Math.Clamp((int)Math.Floor(plot.Width * 1.5), 80, 520);
|
||||
var step = Math.Max(1, _points.Count / Math.Max(1, maxRenderPoints));
|
||||
var radius = Math.Clamp(Math.Min(plot.Width, plot.Height) / 88d, 1.4, 3.8);
|
||||
|
||||
for (var i = 0; i < _points.Count; i += step)
|
||||
var pointCount = _points.Count;
|
||||
var cloudLayers = 8;
|
||||
var baseRadius = Math.Clamp(Math.Min(plot.Width, plot.Height) / 45d, 3, 12);
|
||||
|
||||
var sortedPoints = new List<(double X, double Y, NoiseDistributionLevel Level)>();
|
||||
for (var i = 0; i < pointCount; i++)
|
||||
{
|
||||
var point = _points[i];
|
||||
var level = ResolveLevel(point.DisplayDb, _baselineDb);
|
||||
var x = MapX(plot, point.Timestamp, start, totalTicks);
|
||||
var y = MapY(plot, level, point.Timestamp);
|
||||
context.DrawEllipse(GetLevelBrush(level), pen: null, center: new Point(x, y), radiusX: radius, radiusY: radius);
|
||||
var y = MapYContinuous(plot, point.DisplayDb);
|
||||
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 latestLevel = ResolveLevel(latest.DisplayDb, _baselineDb);
|
||||
var latestX = MapX(plot, latest.Timestamp, start, totalTicks);
|
||||
var latestY = MapY(plot, latestLevel, latest.Timestamp);
|
||||
context.DrawEllipse(GetLevelBrush(latestLevel), pen: new Pen(Brushes.White, 1), center: new Point(latestX, latestY), radiusX: radius + 0.8, radiusY: radius + 0.8);
|
||||
var latestY = MapYContinuous(plot, latest.DisplayDb);
|
||||
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)
|
||||
@@ -103,34 +171,28 @@ public sealed class StudyNoiseDistributionScatterChartControl : Control
|
||||
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 bandHeight = plot.Height / 4d;
|
||||
var levelIndex = level switch
|
||||
{
|
||||
NoiseDistributionLevel.Quiet => 0,
|
||||
NoiseDistributionLevel.Normal => 1,
|
||||
NoiseDistributionLevel.Noisy => 2,
|
||||
NoiseDistributionLevel.Extreme => 3,
|
||||
_ => 1
|
||||
};
|
||||
var minDb = _baselineDb - 5;
|
||||
var maxDb = _baselineDb + 25;
|
||||
var dbRange = maxDb - minDb;
|
||||
if (dbRange <= 0) dbRange = 30;
|
||||
|
||||
var centerY = plot.Bottom - ((levelIndex + 0.5) * bandHeight);
|
||||
var jitter = ComputeJitter(timestamp.Ticks) * bandHeight * 0.26;
|
||||
return Math.Clamp(centerY + jitter, plot.Top + 1.5, plot.Bottom - 1.5);
|
||||
var normalizedDb = (displayDb - minDb) / dbRange;
|
||||
normalizedDb = Math.Clamp(normalizedDb, 0, 1);
|
||||
|
||||
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 value = (ulong)ticks;
|
||||
value ^= value >> 33;
|
||||
value *= 0xff51afd7ed558ccdUL;
|
||||
value ^= value >> 33;
|
||||
value *= 0xc4ceb9fe1a85ec53UL;
|
||||
value ^= value >> 33;
|
||||
var normalized = (value & 0xFFFF) / 65535d;
|
||||
var hash = (ulong)(value * 1000000);
|
||||
hash ^= hash >> 33;
|
||||
hash *= 0xff51afd7ed558ccdUL;
|
||||
hash ^= hash >> 33;
|
||||
hash *= 0xc4ceb9fe1a85ec53UL;
|
||||
hash ^= hash >> 33;
|
||||
var normalized = (hash & 0xFFFF) / 65535d;
|
||||
return (normalized * 2d) - 1d;
|
||||
}
|
||||
|
||||
@@ -162,11 +224,23 @@ public sealed class StudyNoiseDistributionScatterChartControl : Control
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
NoiseDistributionLevel.Quiet => QuietPointBrush,
|
||||
NoiseDistributionLevel.Normal => NormalPointBrush,
|
||||
NoiseDistributionLevel.Noisy => NoisyPointBrush,
|
||||
NoiseDistributionLevel.Extreme => ExtremePointBrush,
|
||||
_ => NormalPointBrush
|
||||
NoiseDistributionLevel.Quiet => QuietBrush,
|
||||
NoiseDistributionLevel.Normal => NormalBrush,
|
||||
NoiseDistributionLevel.Noisy => NoisyBrush,
|
||||
NoiseDistributionLevel.Extreme => ExtremeBrush,
|
||||
_ => 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.Globalization;
|
||||
using System.Linq;
|
||||
@@ -12,7 +12,7 @@ using LanMountainDesktop.Theme;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class StudyNoiseDistributionWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
||||
public partial class StudyNoiseDistributionWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||
{
|
||||
private static readonly Color[] ValueColorCandidates =
|
||||
{
|
||||
@@ -46,13 +46,14 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly DispatcherTimer _uiTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(250)
|
||||
Interval = TimeSpan.FromMilliseconds(100)
|
||||
};
|
||||
|
||||
private double _currentCellSize = 48;
|
||||
private string _languageCode = "zh-CN";
|
||||
private bool _isAttached;
|
||||
private bool _isOnActivePage = true;
|
||||
private bool _isDisposed;
|
||||
private bool _isCompactMode;
|
||||
private bool _isUltraCompactMode;
|
||||
private IDisposable? _monitoringLease;
|
||||
@@ -604,6 +605,25 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
||||
{
|
||||
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;
|
||||
|
||||
public partial class StudySessionControlWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
||||
public partial class StudySessionControlWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||
{
|
||||
private static readonly Color[] PrimaryColorCandidates =
|
||||
{
|
||||
@@ -61,6 +61,7 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
|
||||
private string _languageCode = "zh-CN";
|
||||
private bool _isAttached;
|
||||
private bool _isOnActivePage = true;
|
||||
private bool _isDisposed;
|
||||
private bool _isCompactMode;
|
||||
private bool _isUltraCompactMode;
|
||||
private IDisposable? _monitoringLease;
|
||||
@@ -468,4 +469,20 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
|
||||
{
|
||||
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.Globalization;
|
||||
using System.Linq;
|
||||
@@ -69,6 +69,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
InitializeDialIfNeeded();
|
||||
InitializeHandsIfNeeded();
|
||||
@@ -238,6 +239,12 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
_isNightModeApplied = null;
|
||||
ApplyModeVisualIfNeeded();
|
||||
}
|
||||
|
||||
private void OnClockTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateClockVisual();
|
||||
|
||||
@@ -11,6 +11,74 @@ ICONS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/256x256/apps"
|
||||
DESKTOP_TARGET="$APPLICATIONS_DIR/LanMountainDesktop.desktop"
|
||||
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"
|
||||
|
||||
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。
|
||||
- 桌面端视频相关能力异常:优先在 Windows 环境下验证。
|
||||
- 配置重置:删除 `%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