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;
|
||||||
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);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ 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;
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
|||||||
private bool _isOnActivePage = true;
|
private bool _isOnActivePage = true;
|
||||||
private bool _pausedStudyMonitoringForRecording;
|
private bool _pausedStudyMonitoringForRecording;
|
||||||
private bool _isNightVisual = true;
|
private bool _isNightVisual = true;
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
public RecordingWidget()
|
public RecordingWidget()
|
||||||
{
|
{
|
||||||
@@ -612,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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -329,4 +330,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -604,6 +605,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 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();
|
||||||
|
|||||||
@@ -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