mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-21 16:14:28 +08:00
* ava12升级 * Enable centralized package versioning Add <Project> and <PropertyGroup> with <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> to Directory.Packages.props to enable centralized package version management across the repository. This allows package versions to be controlled from this single file instead of individual project files. * Migrate codebase to Avalonia 12 APIs Apply Avalonia 12 migration changes: replace SystemDecorations with WindowDecorations and remove ExtendClientAreaChromeHints/ExtendClientAreaTitleBarHeightHint usages; update BindingPlugins removal logic (no-op); switch clipboard usage to ClipboardExtensions.SetTextAsync; update Bitmap.CopyPixels calls to the new signature. Replace TextBox.Watermark with PlaceholderText, convert NumberBox styles to FANumberBox and adjust templates, change Checked/Unchecked handlers to IsCheckedChanged, and adapt FluentIcons usages (SymbolIconSource -> FASymbol/FAFont/FluentIcon equivalents). Fix MainWindow partial classes to inherit Window and correct missing variables/fields/usings. Add migration docs/specs/tasks under .trae and include a small TestFluentIcons project for icon testing. * Migrate to Avalonia 12 and Plugin SDK v5 Upgrade project to the Avalonia 12 baseline and Plugin SDK v5: centralize Avalonia packages, remove legacy WebView.Avalonia usage (use NativeWebView/WebView2 EnvironmentRequested), and update Fluent/Material icon/package usages. Bump multiple package/project versions to 5.0.0 and Avalonia 12.0.1, update plugin template and README/docs to SDK v5, and add PLUGIN_SDK_V5_MIGRATION.md. Also fix runtime/behavior bugs: make DataLocationResolver use a fixed bootstrap launcher data path and avoid recursive ResolveDataRoot; add legacy-state handling and extraction in OobeStateService; and update component settings tests to reflect migrated storage (DB/backup) and reset cache for test reloads. Various csproj, tests, and docs updated to reflect the migration and ensure build/test compatibility. * Update icon glyphs and symbol mappings Replace and refine icon sources across settings pages and controls: many FAFontIconSource glyphs were updated to specific Seagull Fluent Icons codepoints, some FASymbolIconSource usages were replaced with FAFontIconSource, and a number of symbol-to-Symbol enum mappings were adjusted (e.g. "Bell" -> AlertOn, "Shield" -> ShieldLock). Also clarified a comment in SettingsWindow and fixed a trailing newline in StudySettingsPage. Changes standardize icon visuals and bridge FluentIcons glyphs into FluentAvalonia icon sources. * fix.修复合并产生的问题。
533 lines
16 KiB
C#
533 lines
16 KiB
C#
using System;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Media;
|
|
using Avalonia.Platform;
|
|
using Avalonia.Styling;
|
|
using LanMountainDesktop.ComponentSystem;
|
|
using LanMountainDesktop.Services;
|
|
|
|
namespace LanMountainDesktop.Views.Components;
|
|
|
|
public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
|
IDesktopPageVisibilityAwareComponentWidget, IComponentPlacementContextAware, IDisposable
|
|
{
|
|
private static readonly Uri DefaultHomeUri = new("https://www.bing.com");
|
|
|
|
private readonly bool _isDesignModePreview = Design.IsDesignMode;
|
|
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 NativeWebView? _browserWebView;
|
|
private readonly WebView2RuntimeAvailability _runtimeAvailability;
|
|
private bool _isDisposed;
|
|
|
|
public BrowserWidget()
|
|
{
|
|
InitializeComponent();
|
|
SizeChanged += OnSizeChanged;
|
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
|
|
|
ApplyCellSize(_currentCellSize);
|
|
ApplyTheme(force: true);
|
|
|
|
_runtimeAvailability = _isDesignModePreview
|
|
? new WebView2RuntimeAvailability(
|
|
IsAvailable: false,
|
|
Version: null,
|
|
Message: "WebView preview is disabled in Avalonia design mode.")
|
|
: WebView2RuntimeProbe.GetAvailability();
|
|
if (_runtimeAvailability.IsAvailable)
|
|
{
|
|
EnsureWebViewCreated();
|
|
}
|
|
else
|
|
{
|
|
ApplyRuntimeUnavailableState();
|
|
}
|
|
|
|
AddressTextBox.Text = DefaultHomeUri.ToString();
|
|
UpdateWebViewActiveState();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_isDisposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isDisposed = true;
|
|
|
|
SizeChanged -= OnSizeChanged;
|
|
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
|
|
AttachedToVisualTree -= OnAttachedToVisualTree;
|
|
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
|
|
|
if (_browserWebView is not null)
|
|
{
|
|
_browserWebView.NavigationStarted -= OnBrowserWebViewNavigationStarting;
|
|
_browserWebView.EnvironmentRequested -= OnBrowserWebViewEnvironmentRequested;
|
|
}
|
|
}
|
|
|
|
public void ApplyCellSize(double cellSize)
|
|
{
|
|
_currentCellSize = Math.Max(1, cellSize);
|
|
|
|
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
|
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
|
RootBorder.Padding = new Thickness(Math.Clamp(_currentCellSize * 0.20, 8, 18));
|
|
|
|
WebViewHostBorder.CornerRadius = mainRectangleCornerRadius;
|
|
AddressBarBorder.CornerRadius = mainRectangleCornerRadius;
|
|
AddressBarBorder.Padding = new Thickness(8, 6);
|
|
|
|
if (RootBorder.Child is Grid rootGrid)
|
|
{
|
|
rootGrid.RowSpacing = 8d;
|
|
}
|
|
|
|
var buttonSize = Math.Clamp(_currentCellSize * 0.72, 30, 36);
|
|
var buttonCorner = buttonSize * 0.5;
|
|
var iconSize = Math.Clamp(buttonSize * 0.44, 14, 16);
|
|
foreach (var button in new[] { RefreshButton, GoButton })
|
|
{
|
|
button.Width = buttonSize;
|
|
button.Height = buttonSize;
|
|
button.CornerRadius = new CornerRadius(buttonCorner);
|
|
}
|
|
|
|
if (RefreshButton.Content is FluentIcons.Avalonia.SymbolIcon refreshIcon)
|
|
{
|
|
refreshIcon.FontSize = iconSize;
|
|
}
|
|
|
|
if (GoButton.Content is FluentIcons.Avalonia.SymbolIcon goIcon)
|
|
{
|
|
goIcon.FontSize = iconSize;
|
|
}
|
|
|
|
AddressTextBox.FontSize = Math.Clamp(_currentCellSize * 0.30, 12, 15);
|
|
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;
|
|
DeactivateWebView(clearUrl: false);
|
|
}
|
|
|
|
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
|
{
|
|
ApplyCellSize(_currentCellSize);
|
|
}
|
|
|
|
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
|
{
|
|
ApplyTheme(force: false);
|
|
}
|
|
|
|
private void ApplyTheme(bool force)
|
|
{
|
|
var isNightMode = ResolveIsNightMode();
|
|
if (!force && _isNightModeApplied.HasValue && _isNightModeApplied.Value == isNightMode)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isNightModeApplied = isNightMode;
|
|
RootBorder.Background = new SolidColorBrush(isNightMode ? Color.Parse("#FF141A24") : Color.Parse("#FFF4F7FC"));
|
|
WebViewHostBorder.Background = new SolidColorBrush(isNightMode ? Color.Parse("#FF0A0E15") : Color.Parse("#FFFFFFFF"));
|
|
WebViewHostBorder.BorderBrush = new SolidColorBrush(isNightMode ? Color.Parse("#33FFFFFF") : Color.Parse("#22000000"));
|
|
AddressBarBorder.Background = new SolidColorBrush(isNightMode ? Color.Parse("#1BFFFFFF") : Color.Parse("#ECF2FA"));
|
|
AddressBarBorder.BorderBrush = new SolidColorBrush(isNightMode ? Color.Parse("#26FFFFFF") : Color.Parse("#22000000"));
|
|
|
|
var idleBackground = new SolidColorBrush(isNightMode ? Color.Parse("#24FFFFFF") : Color.Parse("#DCE6F5"));
|
|
var idleForeground = new SolidColorBrush(isNightMode ? Color.Parse("#FFE5E7EB") : Color.Parse("#FF1E293B"));
|
|
|
|
foreach (var button in new[] { RefreshButton, GoButton })
|
|
{
|
|
button.Background = idleBackground;
|
|
button.Foreground = idleForeground;
|
|
button.BorderThickness = new Thickness(0);
|
|
}
|
|
|
|
AddressTextBox.Background = new SolidColorBrush(isNightMode ? Color.Parse("#1F000000") : Color.Parse("#FFFFFFFF"));
|
|
AddressTextBox.BorderBrush = new SolidColorBrush(isNightMode ? Color.Parse("#2FFFFFFF") : Color.Parse("#22000000"));
|
|
AddressTextBox.Foreground = idleForeground;
|
|
AddressTextBox.CaretBrush = idleForeground;
|
|
}
|
|
|
|
private bool ResolveIsNightMode()
|
|
{
|
|
if (ActualThemeVariant == ThemeVariant.Dark)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (ActualThemeVariant == ThemeVariant.Light)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
|
|
value is ISolidColorBrush brush)
|
|
{
|
|
return CalculateRelativeLuminance(brush.Color) < 0.45;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static double CalculateRelativeLuminance(Color color)
|
|
{
|
|
static double ToLinear(double channel)
|
|
{
|
|
return channel <= 0.03928
|
|
? channel / 12.92
|
|
: Math.Pow((channel + 0.055) / 1.055, 2.4);
|
|
}
|
|
|
|
var red = ToLinear(color.R / 255d);
|
|
var green = ToLinear(color.G / 255d);
|
|
var blue = ToLinear(color.B / 255d);
|
|
return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue);
|
|
}
|
|
|
|
private void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (!CanUseWebView())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryReloadWebView("Refresh"))
|
|
{
|
|
TryNavigate(DefaultHomeUri, "RefreshFallback");
|
|
}
|
|
}
|
|
|
|
private void OnGoButtonClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (!CanUseWebView())
|
|
{
|
|
return;
|
|
}
|
|
|
|
NavigateFromAddressBar();
|
|
}
|
|
|
|
private void OnAddressTextBoxKeyDown(object? sender, KeyEventArgs e)
|
|
{
|
|
if (!CanUseWebView())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (e.Key != Key.Enter)
|
|
{
|
|
return;
|
|
}
|
|
|
|
NavigateFromAddressBar();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void NavigateFromAddressBar()
|
|
{
|
|
if (!CanUseWebView())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var target = TryNormalizeUri(AddressTextBox.Text);
|
|
if (target is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
NavigateTo(target);
|
|
}
|
|
|
|
private void NavigateTo(Uri uri)
|
|
{
|
|
_lastKnownUri = uri;
|
|
AddressTextBox.Text = uri.ToString();
|
|
if (_isWebViewActive)
|
|
{
|
|
TryNavigate(uri, "NavigateTo");
|
|
}
|
|
}
|
|
|
|
private void OnBrowserWebViewNavigationStarting(object? sender, WebViewNavigationStartingEventArgs e)
|
|
{
|
|
if (e.Request is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_lastKnownUri = e.Request;
|
|
AddressTextBox.Text = e.Request.ToString();
|
|
}
|
|
|
|
private void OnBrowserWebViewEnvironmentRequested(object? sender, WebViewEnvironmentRequestedEventArgs e)
|
|
{
|
|
if (e is not WindowsWebView2EnvironmentRequestedEventArgs windowsArgs)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
windowsArgs.UserDataFolder = WebView2RuntimeProbe.ResolveUserDataFolder();
|
|
}
|
|
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
|
{
|
|
AppLogger.Warn("WebView2", "Failed to configure the WebView2 user data folder for BrowserWidget.", ex);
|
|
}
|
|
}
|
|
|
|
private void UpdateWebViewActiveState()
|
|
{
|
|
if (_isDesignModePreview)
|
|
{
|
|
_isWebViewActive = false;
|
|
ApplyRuntimeUnavailableState();
|
|
return;
|
|
}
|
|
|
|
if (!_runtimeAvailability.IsAvailable || _isWebViewFaulted)
|
|
{
|
|
_isWebViewActive = false;
|
|
ApplyRuntimeUnavailableState();
|
|
return;
|
|
}
|
|
|
|
var shouldBeActive = _isAttachedToVisualTree && _isOnActiveDesktopPage && !_isEditMode && IsVisible;
|
|
if (_isWebViewActive == shouldBeActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isWebViewActive = shouldBeActive;
|
|
if (!_isWebViewActive)
|
|
{
|
|
DeactivateWebView(clearUrl: false);
|
|
return;
|
|
}
|
|
|
|
ActivateWebView();
|
|
}
|
|
|
|
private void ActivateWebView()
|
|
{
|
|
EnsureWebViewCreated();
|
|
if (_isWebViewFaulted || !_runtimeAvailability.IsAvailable)
|
|
{
|
|
ApplyRuntimeUnavailableState();
|
|
return;
|
|
}
|
|
|
|
if (_browserWebView is null)
|
|
{
|
|
ApplyRuntimeUnavailableState();
|
|
return;
|
|
}
|
|
|
|
_browserWebView.IsVisible = true;
|
|
_browserWebView.IsHitTestVisible = true;
|
|
RefreshButton.IsEnabled = true;
|
|
GoButton.IsEnabled = true;
|
|
AddressTextBox.IsEnabled = true;
|
|
UnavailableOverlay.IsVisible = false;
|
|
TryNavigate(_lastKnownUri, "ActivateWebView");
|
|
}
|
|
|
|
private void DeactivateWebView(bool clearUrl)
|
|
{
|
|
if (_browserWebView is not null)
|
|
{
|
|
_browserWebView.IsHitTestVisible = false;
|
|
_browserWebView.IsVisible = false;
|
|
}
|
|
|
|
if (clearUrl)
|
|
{
|
|
TryClearWebViewUrl();
|
|
}
|
|
}
|
|
|
|
private bool TryReloadWebView(string action)
|
|
{
|
|
if (_browserWebView is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
return _browserWebView.Refresh();
|
|
}
|
|
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
|
{
|
|
EnterFaultedState(action, ex);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private bool TryNavigate(Uri uri, string action)
|
|
{
|
|
if (_browserWebView is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
_browserWebView.Navigate(uri);
|
|
return true;
|
|
}
|
|
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
|
{
|
|
EnterFaultedState(action, ex);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void TryClearWebViewUrl()
|
|
{
|
|
if (_browserWebView is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_browserWebView.Navigate(new Uri("about:blank"));
|
|
}
|
|
catch
|
|
{
|
|
// Best-effort cleanup only.
|
|
}
|
|
}
|
|
|
|
private bool CanUseWebView()
|
|
{
|
|
return _runtimeAvailability.IsAvailable &&
|
|
!_isWebViewFaulted &&
|
|
_isWebViewActive &&
|
|
_browserWebView is not null;
|
|
}
|
|
|
|
private void ApplyRuntimeUnavailableState()
|
|
{
|
|
_isWebViewActive = false;
|
|
if (_browserWebView is not null)
|
|
{
|
|
_browserWebView.IsVisible = false;
|
|
_browserWebView.IsHitTestVisible = false;
|
|
}
|
|
|
|
RefreshButton.IsEnabled = false;
|
|
GoButton.IsEnabled = false;
|
|
AddressTextBox.IsEnabled = false;
|
|
AddressTextBox.Text = _lastKnownUri.ToString();
|
|
|
|
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 EnsureWebViewCreated()
|
|
{
|
|
if (_browserWebView is not null || _isDesignModePreview || !_runtimeAvailability.IsAvailable)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_browserWebView = new NativeWebView
|
|
{
|
|
Source = new Uri("about:blank"),
|
|
IsVisible = false,
|
|
IsHitTestVisible = false
|
|
};
|
|
_browserWebView.NavigationStarted += OnBrowserWebViewNavigationStarting;
|
|
_browserWebView.EnvironmentRequested += OnBrowserWebViewEnvironmentRequested;
|
|
WebViewPresenter.Children.Insert(0, _browserWebView);
|
|
}
|
|
|
|
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))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var candidate = rawText.Trim();
|
|
if (!candidate.Contains("://", StringComparison.Ordinal))
|
|
{
|
|
candidate = $"https://{candidate}";
|
|
}
|
|
|
|
if (!Uri.TryCreate(candidate, UriKind.Absolute, out var uri))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
|
|
uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
|
|
? uri
|
|
: null;
|
|
}
|
|
}
|