Files
LanMountainDesktop/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs
lincube abfa64b3d7 Avalonia12 (#7)
* 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.修复合并产生的问题。
2026-04-29 12:14:29 +08:00

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;
}
}