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