Files
FoxBrowser/MainWindow.xaml.cs
T
2026-10-01 22:18:11 +03:00

391 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Threading;
using Microsoft.Web.WebView2.Core;
using FoxBrowser.Core;
using FoxBrowser.Native;
using FoxBrowser.UI;
namespace FoxBrowser;
public partial class MainWindow : Window
{
private TabManager? _tabManager;
private TrayIconManager? _tray;
private bool _allowClose;
private readonly DispatcherTimer _watchdogTimer = new() { Interval = TimeSpan.FromSeconds(30) };
private AudioSessionLabeler? _audioLabeler;
private bool _updatingDarkModeMenu;
private AppSettings _appSettings;
private const int WmNcRButtonDown = 0x00A4;
private const int WmNcRButtonUp = 0x00A5;
private const int HtSysMenu = 3;
public MainWindow()
{
InitializeComponent();
_appSettings = AppSettingsStore.Load();
ApplyAppIdentity(_appSettings);
SourceInitialized += MainWindow_SourceInitialized;
Loaded += MainWindow_Loaded;
}
private void MainWindow_SourceInitialized(object? sender, EventArgs e)
{
ApplyDarkTitleBar(true);
var source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
source?.AddHook(MainWindowWndProc);
}
private IntPtr MainWindowWndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if ((msg == WmNcRButtonDown || msg == WmNcRButtonUp) && wParam.ToInt32() == HtSysMenu)
{
handled = true;
if (msg == WmNcRButtonUp)
Dispatcher.BeginInvoke(OpenAppIdentityMenu);
}
return IntPtr.Zero;
}
private void OpenAppIdentityMenu()
{
var popup = new Popup
{
Placement = PlacementMode.MousePoint,
StaysOpen = false,
AllowsTransparency = true,
PopupAnimation = PopupAnimation.Fade
};
var item = new System.Windows.Controls.Button
{
Content = "Змінити назву й іконку…",
Style = (Style)FindResource("FoxMenuButton")
};
item.Click += (_, _) =>
{
popup.IsOpen = false;
OpenAppSettings();
};
popup.Child = new Border
{
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(23, 27, 34)),
BorderBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(51, 64, 78)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(8),
Child = item
};
popup.IsOpen = true;
}
private void OpenAppSettings()
{
var window = new AppSettingsWindow(_appSettings, ApplyAppIdentity) { Owner = this };
window.ShowDialog();
}
private void ApplyAppIdentity(AppSettings settings)
{
_appSettings = settings;
var iconPath = ResolveAppIconPath(settings);
Title = string.IsNullOrWhiteSpace(settings.AppDisplayName) ? "FoxBrowser" : settings.AppDisplayName;
try
{
var image = new System.Windows.Media.Imaging.BitmapImage();
image.BeginInit();
image.UriSource = new Uri(iconPath, UriKind.Absolute);
image.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
image.EndInit();
image.Freeze();
Icon = image;
}
catch { }
_tray?.UpdateIdentity(Title, iconPath);
_audioLabeler?.UpdateIdentity(Title, iconPath);
}
private static string ResolveAppIconPath(AppSettings settings)
{
if (!string.IsNullOrWhiteSpace(settings.AppIconPath) && File.Exists(settings.AppIconPath))
return settings.AppIconPath;
return Path.Combine(AppContext.BaseDirectory, "FoxBrowser.ico");
}
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
try
{
_tray = new TrayIconManager(this, ExitApplication);
_tray.UpdateIdentity(Title, ResolveAppIconPath(_appSettings));
var environmentFactory = new WebView2EnvironmentFactory();
var recoveryMode = false;
CoreWebView2Environment environment;
try
{
environment = await environmentFactory.CreateAsync();
}
catch (Exception ex) when ((uint)ex.HResult == 0x8007139Fu)
{
// Another FoxBrowser/WebView2 process may hold the persistent profile lock.
// Use a disposable recovery profile so the browser can still start.
recoveryMode = true;
var recoveryData = Path.Combine(
AppContext.BaseDirectory, $"Profile_Recovery_{Environment.ProcessId}_{Guid.NewGuid():N}");
Directory.CreateDirectory(recoveryData);
environment = await environmentFactory.CreateAsync(recoveryData);
}
var startupNotice = environmentFactory.TakeStartupNotice();
if (startupNotice is not null)
System.Windows.MessageBox.Show(this, startupNotice, "FoxBrowser", MessageBoxButton.OK, MessageBoxImage.Information);
_tabManager = new TabManager(WebViewHost, environment, !recoveryMode);
_tabManager.ActiveTabStateChanged += (_, _) => Dispatcher.Invoke(RefreshAddressBar);
DataContext = _tabManager;
await _tabManager.InitializeAsync();
var iconPath = ResolveAppIconPath(_appSettings);
try
{
_audioLabeler = new AudioSessionLabeler((uint)Process.GetCurrentProcess().Id, iconPath, Title);
}
catch
{
// Audio labeling is optional; a missing endpoint must not break browsing.
_audioLabeler = null;
}
PinnedExtensionStrip.ItemsSource = _tabManager.ExtensionManager?.PinnedExtensions;
_watchdogTimer.Tick += async (_, _) => await _tabManager.SoftReloadIfNeededAsync();
_watchdogTimer.Start();
BrowserStatus.Visibility = Visibility.Collapsed;
RefreshAddressBar();
}
catch (Exception ex)
{
BrowserStatus.Text = $"WebView2 не запустився: {ex.Message}";
BrowserStatus.Visibility = Visibility.Visible;
}
}
private async void NewTab_Click(object sender, RoutedEventArgs e)
{
if (_tabManager is not null) await _tabManager.AddTabAsync();
RefreshAddressBar();
}
private async void AddressBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key != Key.Enter) return;
await NavigateFromAddressBarAsync();
Keyboard.ClearFocus();
}
private async void Go_Click(object sender, RoutedEventArgs e) => await NavigateFromAddressBarAsync();
private async void AudioOnly_Click(object sender, RoutedEventArgs e)
{
if (_tabManager is null) return;
await _tabManager.ToggleAudioOnlyAsync();
TabStrip.Items.Refresh();
}
private async void DarkMode_Click(object sender, RoutedEventArgs e)
{
DarkModePopup.IsOpen = false;
if (_tabManager is null) return;
await _tabManager.ToggleDarkModeAsync();
TabStrip.Items.Refresh();
}
private void DarkMode_RightClick(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
DarkModePopup.IsOpen = true;
}
private async void ElementPicker_Click(object sender, RoutedEventArgs e)
{
BlockedElementsPopup.IsOpen = false;
if (_tabManager is null) return;
await _tabManager.StartElementPickerAsync();
}
private void ElementPicker_RightClick(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
BlockedElementsPopup.IsOpen = true;
}
private void ManageBlockedElements_Click(object sender, RoutedEventArgs e)
{
BlockedElementsPopup.IsOpen = false;
if (_tabManager is null) return;
var window = new BlockedElementsWindow(_tabManager) { Owner = this };
window.ShowDialog();
}
private async void ClearBlockedSite_Click(object sender, RoutedEventArgs e)
{
BlockedElementsPopup.IsOpen = false;
if (_tabManager is not null) await _tabManager.ClearBlockedForCurrentHostAsync();
}
private async void ClearAllBlocked_Click(object sender, RoutedEventArgs e)
{
BlockedElementsPopup.IsOpen = false;
if (_tabManager is not null) await _tabManager.ClearAllBlockedAsync();
}
private void DarkModeMenu_Opened(object sender, EventArgs e)
{
if (_tabManager?.ActiveTab is not BrowserTab tab) return;
_updatingDarkModeMenu = true;
DarkBrightnessSlider.Value = tab.DarkBrightness;
DarkContrastSlider.Value = tab.DarkContrast;
DarkSaturationSlider.Value = tab.DarkSaturation;
_updatingDarkModeMenu = false;
}
private async void DarkModeParameter_Changed(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (_updatingDarkModeMenu || _tabManager is null) return;
await _tabManager.SetDarkModeParametersAsync(
DarkBrightnessSlider.Value,
DarkContrastSlider.Value,
DarkSaturationSlider.Value);
}
private async void ResetDarkMode_Click(object sender, RoutedEventArgs e)
{
_updatingDarkModeMenu = true;
DarkBrightnessSlider.Value = 0.72;
DarkContrastSlider.Value = 1.08;
DarkSaturationSlider.Value = 0.88;
_updatingDarkModeMenu = false;
if (_tabManager is not null)
await _tabManager.SetDarkModeParametersAsync(0.72, 1.08, 0.88);
}
private async Task NavigateFromAddressBarAsync()
{
if (_tabManager?.ActiveTab is null) return;
await _tabManager.NavigateAsync(_tabManager.ActiveTab, AddressBox.Text);
AddressBox.Text = _tabManager.ActiveTab.Address;
}
private void Back_Click(object sender, RoutedEventArgs e) => _tabManager?.GoBack();
private void Forward_Click(object sender, RoutedEventArgs e) => _tabManager?.GoForward();
private void Reload_Click(object sender, RoutedEventArgs e) => _tabManager?.Reload();
private void Extensions_Click(object sender, RoutedEventArgs e)
{
if (_tabManager?.ExtensionManager is null) return;
var window = new ExtensionsSettingsWindow(
_tabManager.ExtensionManager,
_tabManager.Environment,
address => _tabManager!.OpenUrlAsync(address),
_tabManager.ExtensionsAvailable)
{ Owner = this };
window.ShowDialog();
}
private void PinnedExtension_Click(object sender, RoutedEventArgs e)
{
if (_tabManager?.ExtensionManager is null || sender is not System.Windows.Controls.Button button || button.DataContext is not PinnedExtensionItem item)
return;
if (string.IsNullOrWhiteSpace(item.PopupPage))
{
System.Windows.MessageBox.Show(
this,
$"Розширення «{item.Name}» не має popup-вікна у manifest.json. Це нормально для частини extension (наприклад, Shazam). Відкриваю список розширень, де можна перевірити його налаштування.",
"Popup розширення",
MessageBoxButton.OK,
MessageBoxImage.Information);
Extensions_Click(sender, e);
return;
}
var screenPositionDevice = button.PointToScreen(new System.Windows.Point(0, button.ActualHeight));
var source = PresentationSource.FromVisual(button);
var fromDevice = source?.CompositionTarget?.TransformFromDevice ?? System.Windows.Media.Matrix.Identity;
var screenPosition = fromDevice.Transform(screenPositionDevice);
var screen = System.Windows.Forms.Screen.FromPoint(new System.Drawing.Point(
(int)screenPositionDevice.X, (int)screenPositionDevice.Y));
var workAreaTopLeft = fromDevice.Transform(new System.Windows.Point(screen.WorkingArea.Left, screen.WorkingArea.Top));
var workAreaBottomRight = fromDevice.Transform(new System.Windows.Point(screen.WorkingArea.Right, screen.WorkingArea.Bottom));
var workingArea = new System.Windows.Rect(workAreaTopLeft, workAreaBottomRight);
var popup = new ExtensionPopupWindow(_tabManager.Environment, item.Extension.Id, item.PopupPage, screenPosition, workingArea)
{
Owner = this
};
popup.Show();
}
private async void TabStrip_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.Source != TabStrip || TabStrip.SelectedItem is not BrowserTab tab || _tabManager is null) return;
AddressBox.Text = tab.Address;
await _tabManager.SwitchToTabAsync(tab);
RefreshAddressBar();
}
private void CloseTab_Click(object sender, RoutedEventArgs e)
{
if ((sender as System.Windows.Controls.Button)?.Tag is BrowserTab tab) _tabManager?.CloseTab(tab);
RefreshAddressBar();
}
private async void Window_StateChanged(object? sender, EventArgs e)
{
if (_tabManager is null) return;
var minimized = WindowState == WindowState.Minimized;
if (minimized) _tray?.MinimizeToTray();
await _tabManager.SetMinimizedModeAsync(minimized);
}
private async void Window_Closing(object? sender, CancelEventArgs e)
{
if (!_allowClose)
{
e.Cancel = true;
_tray?.MinimizeToTray();
if (_tabManager is not null) await _tabManager.SetMinimizedModeAsync(true);
}
else
{
_watchdogTimer.Stop();
_tray?.Dispose();
_audioLabeler?.Dispose();
if (_tabManager is not null) await _tabManager.DisposeAsync();
}
}
private void ExitApplication()
{
_allowClose = true;
Close();
}
private void ApplyDarkTitleBar(bool enabled)
{
var hwnd = new WindowInteropHelper(this).Handle;
Win32Interop.ApplyDarkTitleBar(hwnd, enabled);
}
private void RefreshAddressBar()
{
if (_tabManager?.ActiveTab is not null)
{
if (!ReferenceEquals(TabStrip.SelectedItem, _tabManager.ActiveTab)) TabStrip.SelectedItem = _tabManager.ActiveTab;
AddressBox.Text = _tabManager.ActiveTab.Address;
}
}
}