mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
0.5.20
我认为很稳定了,后面就要开始弄插件不稳定了
This commit is contained in:
@@ -6,21 +6,27 @@ using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using AvaloniaWebView;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Services;
|
||||
using WebViewCore.Events;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||
public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
||||
IDesktopPageVisibilityAwareComponentWidget, IComponentPlacementContextAware, IDisposable
|
||||
{
|
||||
private static readonly Uri DefaultHomeUri = new("https://www.bing.com");
|
||||
|
||||
private double _currentCellSize = 48;
|
||||
private string _componentId = BuiltInComponentIds.DesktopBrowser;
|
||||
private string _placementId = string.Empty;
|
||||
private bool? _isNightModeApplied;
|
||||
private Uri _lastKnownUri = DefaultHomeUri;
|
||||
private bool _isOnActiveDesktopPage;
|
||||
private bool _isAttachedToVisualTree;
|
||||
private bool _isEditMode;
|
||||
private bool _isWebViewActive = true;
|
||||
private bool _isWebViewFaulted;
|
||||
private readonly WebView2RuntimeAvailability _runtimeAvailability;
|
||||
private bool _isDisposed;
|
||||
|
||||
@@ -45,8 +51,8 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
ApplyRuntimeUnavailableState();
|
||||
}
|
||||
|
||||
AddressTextBox.Text = DefaultHomeUri.ToString();
|
||||
UpdateWebViewActiveState();
|
||||
NavigateTo(DefaultHomeUri);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -74,17 +80,15 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
|
||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(_currentCellSize * 0.34, 12, 28));
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(_currentCellSize * 0.20, 8, 18));
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(_currentCellSize * 0.20, 8, 18));
|
||||
|
||||
WebViewHostBorder.CornerRadius = new CornerRadius(Math.Clamp(_currentCellSize * 0.24, 10, 22));
|
||||
AddressBarBorder.CornerRadius = new CornerRadius(Math.Clamp(_currentCellSize * 0.22, 10, 20));
|
||||
AddressBarBorder.Padding = new Thickness(8, 6);
|
||||
|
||||
var rowSpacing = 8d;
|
||||
if (RootBorder.Child is Grid rootGrid)
|
||||
{
|
||||
rootGrid.RowSpacing = rowSpacing;
|
||||
rootGrid.RowSpacing = 8d;
|
||||
}
|
||||
|
||||
var buttonSize = Math.Clamp(_currentCellSize * 0.72, 30, 36);
|
||||
@@ -111,16 +115,33 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
AddressTextBox.Height = buttonSize;
|
||||
}
|
||||
|
||||
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||
{
|
||||
_isOnActiveDesktopPage = isOnActivePage;
|
||||
_isEditMode = isEditMode;
|
||||
UpdateWebViewActiveState();
|
||||
}
|
||||
|
||||
public void SetComponentPlacementContext(string componentId, string? placementId)
|
||||
{
|
||||
_componentId = string.IsNullOrWhiteSpace(componentId)
|
||||
? BuiltInComponentIds.DesktopBrowser
|
||||
: componentId.Trim();
|
||||
_placementId = placementId?.Trim() ?? string.Empty;
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttachedToVisualTree = true;
|
||||
ApplyTheme(force: true);
|
||||
UpdateWebViewActiveState();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttachedToVisualTree = false;
|
||||
_isOnActiveDesktopPage = false;
|
||||
UpdateWebViewActiveState();
|
||||
DeactivateWebView(clearUrl: false);
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
@@ -202,28 +223,20 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
|
||||
private void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!_runtimeAvailability.IsAvailable)
|
||||
if (!CanUseWebView())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isWebViewActive)
|
||||
if (!TryReloadWebView("Refresh"))
|
||||
{
|
||||
return;
|
||||
TryNavigate(DefaultHomeUri, "RefreshFallback");
|
||||
}
|
||||
|
||||
if (BrowserWebView.Url is not null)
|
||||
{
|
||||
BrowserWebView.Reload();
|
||||
return;
|
||||
}
|
||||
|
||||
NavigateTo(DefaultHomeUri);
|
||||
}
|
||||
|
||||
private void OnGoButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!_runtimeAvailability.IsAvailable)
|
||||
if (!CanUseWebView())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -233,7 +246,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
|
||||
private void OnAddressTextBoxKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (!_runtimeAvailability.IsAvailable)
|
||||
if (!CanUseWebView())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -249,7 +262,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
|
||||
private void NavigateFromAddressBar()
|
||||
{
|
||||
if (!_runtimeAvailability.IsAvailable)
|
||||
if (!CanUseWebView())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -269,7 +282,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
AddressTextBox.Text = uri.ToString();
|
||||
if (_isWebViewActive)
|
||||
{
|
||||
BrowserWebView.Url = uri;
|
||||
TryNavigate(uri, "NavigateTo");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,25 +297,16 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
AddressTextBox.Text = e.Url.ToString();
|
||||
}
|
||||
|
||||
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||
{
|
||||
_isOnActiveDesktopPage = isOnActivePage;
|
||||
_isEditMode = isEditMode;
|
||||
UpdateWebViewActiveState();
|
||||
}
|
||||
|
||||
private void UpdateWebViewActiveState()
|
||||
{
|
||||
if (!_runtimeAvailability.IsAvailable)
|
||||
if (!_runtimeAvailability.IsAvailable || _isWebViewFaulted)
|
||||
{
|
||||
_isWebViewActive = false;
|
||||
BrowserWebView.Url = null;
|
||||
BrowserWebView.IsVisible = false;
|
||||
BrowserWebView.IsHitTestVisible = false;
|
||||
ApplyRuntimeUnavailableState();
|
||||
return;
|
||||
}
|
||||
|
||||
var shouldBeActive = _isOnActiveDesktopPage && !_isEditMode && IsVisible;
|
||||
var shouldBeActive = _isAttachedToVisualTree && _isOnActiveDesktopPage && !_isEditMode && IsVisible;
|
||||
if (_isWebViewActive == shouldBeActive)
|
||||
{
|
||||
return;
|
||||
@@ -311,40 +315,118 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
|
||||
_isWebViewActive = shouldBeActive;
|
||||
if (!_isWebViewActive)
|
||||
{
|
||||
if (BrowserWebView.Url is Uri currentUri)
|
||||
{
|
||||
_lastKnownUri = currentUri;
|
||||
}
|
||||
DeactivateWebView(clearUrl: false);
|
||||
return;
|
||||
}
|
||||
|
||||
BrowserWebView.IsHitTestVisible = false;
|
||||
BrowserWebView.IsVisible = false;
|
||||
BrowserWebView.Url = null;
|
||||
ActivateWebView();
|
||||
}
|
||||
|
||||
private void ActivateWebView()
|
||||
{
|
||||
if (_isWebViewFaulted || !_runtimeAvailability.IsAvailable)
|
||||
{
|
||||
ApplyRuntimeUnavailableState();
|
||||
return;
|
||||
}
|
||||
|
||||
BrowserWebView.IsVisible = true;
|
||||
BrowserWebView.IsHitTestVisible = true;
|
||||
BrowserWebView.Url = _lastKnownUri;
|
||||
RefreshButton.IsEnabled = true;
|
||||
GoButton.IsEnabled = true;
|
||||
AddressTextBox.IsEnabled = true;
|
||||
UnavailableOverlay.IsVisible = false;
|
||||
|
||||
TryNavigate(_lastKnownUri, "Activate");
|
||||
}
|
||||
|
||||
private void DeactivateWebView(bool clearUrl)
|
||||
{
|
||||
BrowserWebView.IsHitTestVisible = false;
|
||||
BrowserWebView.IsVisible = false;
|
||||
|
||||
if (clearUrl)
|
||||
{
|
||||
TryClearWebViewUrl();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryReloadWebView(string action)
|
||||
{
|
||||
try
|
||||
{
|
||||
BrowserWebView.Reload();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
||||
{
|
||||
EnterFaultedState(action, ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryNavigate(Uri uri, string action)
|
||||
{
|
||||
try
|
||||
{
|
||||
BrowserWebView.Url = uri;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
||||
{
|
||||
EnterFaultedState(action, ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryClearWebViewUrl()
|
||||
{
|
||||
try
|
||||
{
|
||||
BrowserWebView.Url = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort cleanup only.
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanUseWebView()
|
||||
{
|
||||
return _runtimeAvailability.IsAvailable && !_isWebViewFaulted && _isWebViewActive;
|
||||
}
|
||||
|
||||
private void ApplyRuntimeUnavailableState()
|
||||
{
|
||||
_isWebViewActive = false;
|
||||
BrowserWebView.Url = null;
|
||||
BrowserWebView.IsVisible = false;
|
||||
BrowserWebView.IsHitTestVisible = false;
|
||||
|
||||
RefreshButton.IsEnabled = false;
|
||||
GoButton.IsEnabled = false;
|
||||
AddressTextBox.IsEnabled = false;
|
||||
AddressTextBox.Text = string.Empty;
|
||||
AddressTextBox.Text = _lastKnownUri.ToString();
|
||||
|
||||
UnavailableMessageTextBlock.Text = string.IsNullOrWhiteSpace(_runtimeAvailability.Message)
|
||||
? "WebView runtime unavailable."
|
||||
: _runtimeAvailability.Message;
|
||||
UnavailableMessageTextBlock.Text = _isWebViewFaulted
|
||||
? "The browser component is temporarily unavailable. Restart the app to retry."
|
||||
: string.IsNullOrWhiteSpace(_runtimeAvailability.Message)
|
||||
? "WebView runtime unavailable."
|
||||
: _runtimeAvailability.Message;
|
||||
UnavailableOverlay.IsVisible = true;
|
||||
}
|
||||
|
||||
private void EnterFaultedState(string action, Exception ex)
|
||||
{
|
||||
_isWebViewFaulted = true;
|
||||
_isWebViewActive = false;
|
||||
AppLogger.Warn(
|
||||
"BrowserWidget",
|
||||
$"Browser component faulted. Action={action}; ComponentId={_componentId}; PlacementId={_placementId}; RuntimeAvailability={_runtimeAvailability.IsAvailable}; RuntimeVersion={_runtimeAvailability.Version ?? string.Empty}; CurrentUrl={_lastKnownUri}",
|
||||
ex);
|
||||
TryClearWebViewUrl();
|
||||
ApplyRuntimeUnavailableState();
|
||||
}
|
||||
|
||||
private static Uri? TryNormalizeUri(string? rawText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawText))
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
internal sealed class DesktopComponentFailureView : UserControl, IDesktopComponentWidget
|
||||
{
|
||||
private readonly Border _rootBorder;
|
||||
private readonly TextBlock _titleBlock;
|
||||
private readonly TextBlock _summaryBlock;
|
||||
private readonly TextBlock _statusBlock;
|
||||
private readonly Button _toggleDetailsButton;
|
||||
private readonly Button _copyReportButton;
|
||||
private readonly Border _detailsBorder;
|
||||
private readonly TextBox _reportTextBox;
|
||||
private readonly string _componentId;
|
||||
private readonly string? _placementId;
|
||||
private readonly string _reportText;
|
||||
private bool _detailsVisible;
|
||||
|
||||
public DesktopComponentFailureView(
|
||||
string componentName,
|
||||
string componentId,
|
||||
string? placementId,
|
||||
int? pageIndex,
|
||||
string action,
|
||||
Exception exception)
|
||||
{
|
||||
_componentId = componentId;
|
||||
_placementId = placementId;
|
||||
_reportText = BuildReport(componentName, componentId, placementId, pageIndex, action, exception);
|
||||
|
||||
_titleBlock = new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(componentName) ? "组件暂时不可用" : componentName,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
_summaryBlock = new TextBlock
|
||||
{
|
||||
Text = "该组件已临时停用,并由信息占位保留原位置。你可以展开详情或复制错误报告。",
|
||||
Foreground = CreateBrush("#FFD6DEE9"),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
_statusBlock = new TextBlock
|
||||
{
|
||||
IsVisible = false,
|
||||
Foreground = CreateBrush("#FF93C5FD"),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
_toggleDetailsButton = CreateButton("查看错误信息", OnToggleDetailsClick);
|
||||
_copyReportButton = CreateButton("复制错误报告", OnCopyReportClick);
|
||||
|
||||
_reportTextBox = new TextBox
|
||||
{
|
||||
Text = _reportText,
|
||||
IsReadOnly = true,
|
||||
AcceptsReturn = true,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MinHeight = 96,
|
||||
MaxHeight = 220,
|
||||
Background = CreateBrush("#CC0F172A"),
|
||||
Foreground = CreateBrush("#FFE2E8F0"),
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(8)
|
||||
};
|
||||
|
||||
_detailsBorder = new Border
|
||||
{
|
||||
IsVisible = false,
|
||||
Background = CreateBrush("#660F172A"),
|
||||
BorderBrush = CreateBrush("#33475569"),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Child = _reportTextBox
|
||||
};
|
||||
|
||||
_rootBorder = new Border
|
||||
{
|
||||
Background = CreateBrush("#D91E293B"),
|
||||
BorderBrush = CreateBrush("#336B7280"),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(18),
|
||||
Padding = new Thickness(14),
|
||||
ClipToBounds = true,
|
||||
Child = new StackPanel
|
||||
{
|
||||
Spacing = 8,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
_titleBlock,
|
||||
_summaryBlock,
|
||||
new WrapPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
ItemSpacing = 8,
|
||||
LineSpacing = 8,
|
||||
Children =
|
||||
{
|
||||
_toggleDetailsButton,
|
||||
_copyReportButton
|
||||
}
|
||||
},
|
||||
_statusBlock,
|
||||
_detailsBorder
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Content = _rootBorder;
|
||||
ApplyCellSize(48);
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
var normalized = Math.Max(1, cellSize);
|
||||
_rootBorder.CornerRadius = new CornerRadius(Math.Clamp(normalized * 0.24, 12, 24));
|
||||
_rootBorder.Padding = new Thickness(Math.Clamp(normalized * 0.24, 10, 18));
|
||||
_titleBlock.FontSize = Math.Clamp(normalized * 0.36, 14, 22);
|
||||
_summaryBlock.FontSize = Math.Clamp(normalized * 0.24, 11, 15);
|
||||
_statusBlock.FontSize = Math.Clamp(normalized * 0.22, 10, 13);
|
||||
_toggleDetailsButton.FontSize = Math.Clamp(normalized * 0.22, 10, 14);
|
||||
_copyReportButton.FontSize = Math.Clamp(normalized * 0.22, 10, 14);
|
||||
_toggleDetailsButton.Padding = new Thickness(Math.Clamp(normalized * 0.18, 8, 12), 6);
|
||||
_copyReportButton.Padding = new Thickness(Math.Clamp(normalized * 0.18, 8, 12), 6);
|
||||
_reportTextBox.FontSize = Math.Clamp(normalized * 0.2, 10, 13);
|
||||
_reportTextBox.MaxHeight = Math.Clamp(normalized * 5.2, 120, 260);
|
||||
}
|
||||
|
||||
private static Button CreateButton(string text, EventHandler<RoutedEventArgs> clickHandler)
|
||||
{
|
||||
var button = new Button
|
||||
{
|
||||
Content = text,
|
||||
Background = CreateBrush("#80334155"),
|
||||
Foreground = Brushes.White,
|
||||
BorderBrush = CreateBrush("#335B6575"),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
HorizontalAlignment = HorizontalAlignment.Left
|
||||
};
|
||||
button.Click += clickHandler;
|
||||
return button;
|
||||
}
|
||||
|
||||
private void OnToggleDetailsClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_detailsVisible = !_detailsVisible;
|
||||
_detailsBorder.IsVisible = _detailsVisible;
|
||||
_toggleDetailsButton.Content = _detailsVisible ? "隐藏错误信息" : "查看错误信息";
|
||||
UpdateStatus(null);
|
||||
}
|
||||
|
||||
private void OnCopyReportClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
UiExceptionGuard.FireAndForgetGuarded(
|
||||
CopyReportAsync,
|
||||
"DesktopComponentFailureView.CopyReport",
|
||||
UiExceptionGuard.BuildContext(
|
||||
("ComponentId", _componentId),
|
||||
("PlacementId", _placementId)));
|
||||
}
|
||||
|
||||
private async Task CopyReportAsync()
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
var clipboard = topLevel?.Clipboard;
|
||||
if (clipboard is null)
|
||||
{
|
||||
UpdateStatus("当前环境不支持复制错误报告。");
|
||||
return;
|
||||
}
|
||||
|
||||
await clipboard.SetTextAsync(_reportText);
|
||||
UpdateStatus("错误报告已复制到剪贴板。");
|
||||
}
|
||||
|
||||
private void UpdateStatus(string? message)
|
||||
{
|
||||
_statusBlock.Text = message ?? string.Empty;
|
||||
_statusBlock.IsVisible = !string.IsNullOrWhiteSpace(message);
|
||||
}
|
||||
|
||||
private static string BuildReport(
|
||||
string componentName,
|
||||
string componentId,
|
||||
string? placementId,
|
||||
int? pageIndex,
|
||||
string action,
|
||||
Exception exception)
|
||||
{
|
||||
var version = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "unknown";
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("LanMountainDesktop Component Failure Report");
|
||||
builder.AppendLine($"GeneratedAt: {DateTimeOffset.Now:O}");
|
||||
builder.AppendLine($"AppVersion: {version}");
|
||||
builder.AppendLine($"Action: {action}");
|
||||
builder.AppendLine($"ComponentName: {componentName}");
|
||||
builder.AppendLine($"ComponentId: {componentId}");
|
||||
builder.AppendLine($"PlacementId: {placementId ?? string.Empty}");
|
||||
builder.AppendLine($"PageIndex: {pageIndex?.ToString() ?? string.Empty}");
|
||||
builder.AppendLine($"ExceptionType: {exception.GetType().FullName}");
|
||||
builder.AppendLine($"ExceptionMessage: {exception.Message}");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(exception.ToString());
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static IBrush CreateBrush(string colorHex)
|
||||
{
|
||||
return new SolidColorBrush(Color.Parse(colorHex));
|
||||
}
|
||||
}
|
||||
@@ -1522,7 +1522,7 @@ public partial class MainWindow
|
||||
placement.PlacementId = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
var component = CreateDesktopComponentControl(placement.ComponentId, placement.PlacementId);
|
||||
var component = CreateDesktopComponentControl(placement.ComponentId, placement.PlacementId, placement.PageIndex);
|
||||
if (component is null)
|
||||
{
|
||||
return null;
|
||||
@@ -1956,23 +1956,54 @@ public partial class MainWindow
|
||||
return onLeft || onRight || onTop || onBottom;
|
||||
}
|
||||
|
||||
private Control? CreateDesktopComponentControl(string componentId, string? placementId = null)
|
||||
private Control? CreateDesktopComponentControl(string componentId, string? placementId = null, int? pageIndex = null)
|
||||
{
|
||||
if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var component = runtimeDescriptor.CreateControl(
|
||||
_currentDesktopCellSize,
|
||||
_timeZoneService,
|
||||
_weatherDataService,
|
||||
_recommendationInfoService,
|
||||
_calculatorDataService,
|
||||
_componentSettingsService,
|
||||
placementId);
|
||||
component.Classes.Add(DesktopComponentClass);
|
||||
return component;
|
||||
return CreateDesktopComponentControl(runtimeDescriptor, _currentDesktopCellSize, placementId, pageIndex, "DesktopSurface");
|
||||
}
|
||||
|
||||
private Control? CreateDesktopComponentControl(
|
||||
DesktopComponentRuntimeDescriptor runtimeDescriptor,
|
||||
double cellSize,
|
||||
string? placementId,
|
||||
int? pageIndex,
|
||||
string action)
|
||||
{
|
||||
try
|
||||
{
|
||||
var component = runtimeDescriptor.CreateControl(
|
||||
cellSize,
|
||||
_timeZoneService,
|
||||
_weatherDataService,
|
||||
_recommendationInfoService,
|
||||
_calculatorDataService,
|
||||
_componentSettingsService,
|
||||
placementId);
|
||||
component.Classes.Add(DesktopComponentClass);
|
||||
return component;
|
||||
}
|
||||
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
||||
{
|
||||
AppLogger.Warn(
|
||||
"ComponentRuntime",
|
||||
$"Action={action}; ComponentId={runtimeDescriptor.Definition.Id}; PlacementId={placementId ?? string.Empty}; PageIndex={pageIndex?.ToString() ?? string.Empty}; ExceptionType={ex.GetType().FullName}; IsFatal=false",
|
||||
ex);
|
||||
|
||||
var failureView = new DesktopComponentFailureView(
|
||||
runtimeDescriptor.Definition.DisplayName,
|
||||
runtimeDescriptor.Definition.Id,
|
||||
placementId,
|
||||
pageIndex,
|
||||
action,
|
||||
ex);
|
||||
failureView.ApplyCellSize(cellSize);
|
||||
failureView.Classes.Add(DesktopComponentClass);
|
||||
return failureView;
|
||||
}
|
||||
}
|
||||
|
||||
private void CollapseComponentLibraryPanel()
|
||||
@@ -3113,13 +3144,16 @@ public partial class MainWindow
|
||||
var previewHeight = previewSpan.HeightCells * previewCellSize;
|
||||
var renderCellSize = Math.Clamp(previewCellSize * 1.15, 26, 110);
|
||||
|
||||
var previewControl = descriptor.CreateControl(
|
||||
var previewControl = CreateDesktopComponentControl(
|
||||
descriptor,
|
||||
renderCellSize,
|
||||
_timeZoneService,
|
||||
_weatherDataService,
|
||||
_recommendationInfoService,
|
||||
_calculatorDataService,
|
||||
_componentSettingsService);
|
||||
placementId: null,
|
||||
pageIndex: null,
|
||||
action: "ComponentLibraryPreview");
|
||||
if (previewControl is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Component library previews must stay non-interactive so drag gesture is reliable.
|
||||
previewControl.IsHitTestVisible = false;
|
||||
previewControl.Focusable = false;
|
||||
|
||||
@@ -1,59 +1,58 @@
|
||||
using System;
|
||||
using Avalonia.Interactivity;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class MainWindow
|
||||
{
|
||||
private readonly DispatcherTimer _singleInstanceNoticeTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(6)
|
||||
};
|
||||
private bool _isSingleInstancePromptVisible;
|
||||
|
||||
internal void ShowSingleInstanceNotice()
|
||||
{
|
||||
void ShowPrompt()
|
||||
{
|
||||
UiExceptionGuard.FireAndForgetGuarded(
|
||||
ShowSingleInstanceNoticeCoreAsync,
|
||||
"MainWindow.ShowSingleInstanceNotice");
|
||||
}
|
||||
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
ShowSingleInstanceNoticeCore();
|
||||
ShowPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Post(ShowSingleInstanceNoticeCore, DispatcherPriority.Send);
|
||||
Dispatcher.UIThread.Post(ShowPrompt, DispatcherPriority.Send);
|
||||
}
|
||||
|
||||
private void ShowSingleInstanceNoticeCore()
|
||||
private async Task ShowSingleInstanceNoticeCoreAsync()
|
||||
{
|
||||
SingleInstanceNoticeTitleTextBlock.Text = L(
|
||||
"single_instance.notice.title",
|
||||
"App already open");
|
||||
SingleInstanceNoticeDescriptionTextBlock.Text = L(
|
||||
"single_instance.notice.description",
|
||||
"LanMountainDesktop is already running. Switched back to the active desktop.");
|
||||
SingleInstanceNoticeButtonTextBlock.Text = L(
|
||||
"single_instance.notice.button",
|
||||
"Got it");
|
||||
SingleInstanceNoticeDock.IsVisible = true;
|
||||
if (_isSingleInstancePromptVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_singleInstanceNoticeTimer.Stop();
|
||||
_singleInstanceNoticeTimer.Tick -= OnSingleInstanceNoticeTimerTick;
|
||||
_singleInstanceNoticeTimer.Tick += OnSingleInstanceNoticeTimerTick;
|
||||
_singleInstanceNoticeTimer.Start();
|
||||
}
|
||||
_isSingleInstancePromptVisible = true;
|
||||
|
||||
private void OnSingleInstanceNoticeButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
HideSingleInstanceNotice();
|
||||
}
|
||||
try
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = L("single_instance.notice.title", "应用已经运行"),
|
||||
Content = L(
|
||||
"single_instance.notice.description",
|
||||
"应用已经运行,无需多次点击打开。"),
|
||||
PrimaryButtonText = L("single_instance.notice.button", "确定"),
|
||||
DefaultButton = ContentDialogButton.Primary
|
||||
};
|
||||
|
||||
private void OnSingleInstanceNoticeTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
HideSingleInstanceNotice();
|
||||
}
|
||||
|
||||
private void HideSingleInstanceNotice()
|
||||
{
|
||||
_singleInstanceNoticeTimer.Stop();
|
||||
SingleInstanceNoticeDock.IsVisible = false;
|
||||
await dialog.ShowAsync(this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSingleInstancePromptVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,51 +471,6 @@
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Spacing="12">
|
||||
<Border x:Name="SingleInstanceNoticeDock"
|
||||
IsVisible="False"
|
||||
Classes="glass-panel"
|
||||
CornerRadius="18"
|
||||
Padding="14,12">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="12">
|
||||
<Border Width="34"
|
||||
Height="34"
|
||||
CornerRadius="17"
|
||||
Background="{DynamicResource AdaptiveAccentBrush}">
|
||||
<fi:FluentIcon Icon="Alert"
|
||||
IconVariant="Regular"
|
||||
FontSize="16"
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="2"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="SingleInstanceNoticeTitleTextBlock"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Text="App already open" />
|
||||
<TextBlock x:Name="SingleInstanceNoticeDescriptionTextBlock"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="LanMountainDesktop is already running. Switched back to the active desktop." />
|
||||
</StackPanel>
|
||||
<Button x:Name="SingleInstanceNoticeButton"
|
||||
Grid.Column="2"
|
||||
Padding="14,8"
|
||||
Click="OnSingleInstanceNoticeButtonClick">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<fi:FluentIcon Icon="Checkmark"
|
||||
IconVariant="Regular" />
|
||||
<TextBlock x:Name="SingleInstanceNoticeButtonTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
Text="Got it" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="PendingRestartDock"
|
||||
IsVisible="False"
|
||||
Classes="glass-panel"
|
||||
|
||||
Reference in New Issue
Block a user