diff --git a/App.xaml b/App.xaml
index 38b2985..c2d60a1 100644
--- a/App.xaml
+++ b/App.xaml
@@ -138,5 +138,64 @@
+
+
+
+
diff --git a/Core/SessionStore.cs b/Core/SessionStore.cs
index 33503dc..55f45a8 100644
--- a/Core/SessionStore.cs
+++ b/Core/SessionStore.cs
@@ -13,6 +13,10 @@ public sealed class TabSnapshot
public string Address { get; init; } = "https://www.google.com";
public string Title { get; init; } = "Нова вкладка";
public bool IsAudioOnly { get; init; }
+ public bool IsDarkMode { get; init; }
+ public double DarkBrightness { get; init; } = 0.72;
+ public double DarkContrast { get; init; } = 1.08;
+ public double DarkSaturation { get; init; } = 0.88;
public double ScrollY { get; init; }
public double MediaPosition { get; init; }
public bool WasPlaying { get; init; }
@@ -52,6 +56,10 @@ public static class SessionStore
Address = t.Address,
Title = t.Title,
IsAudioOnly = t.IsAudioOnly,
+ IsDarkMode = t.IsDarkMode,
+ DarkBrightness = t.DarkBrightness,
+ DarkContrast = t.DarkContrast,
+ DarkSaturation = t.DarkSaturation,
ScrollY = t.ScrollY,
MediaPosition = t.MediaPosition,
WasPlaying = t.WasPlaying,
diff --git a/Core/TabManager.cs b/Core/TabManager.cs
index e92474d..0824fc7 100644
--- a/Core/TabManager.cs
+++ b/Core/TabManager.cs
@@ -26,6 +26,8 @@ public sealed class TabManager : IAsyncDisposable
private bool _minimizedMode;
private bool _navigationInProgress;
private bool _switchInProgress;
+ private readonly string _blockedElementsPath = Path.Combine(AppContext.BaseDirectory, "blocked-elements.json");
+ private readonly Dictionary> _blockedElements;
public TabManager(Grid host, CoreWebView2Environment environment, bool extensionsAvailable = true)
{
@@ -34,6 +36,7 @@ public sealed class TabManager : IAsyncDisposable
ExtensionsAvailable = extensionsAvailable;
_view = new WebView2 { Visibility = Visibility.Visible };
_host.Children.Add(_view);
+ _blockedElements = LoadBlockedElements();
}
public ObservableCollection Tabs { get; } = new();
@@ -64,6 +67,7 @@ public sealed class TabManager : IAsyncDisposable
_view.CoreWebView2.DocumentTitleChanged += OnDocumentTitleChanged;
_view.CoreWebView2.IsDocumentPlayingAudioChanged += OnAudioStateChanged;
_view.CoreWebView2.HistoryChanged += OnHistoryChanged;
+ _view.CoreWebView2.WebMessageReceived += OnWebMessageReceived;
await RestoreSessionOrDefaultAsync();
}
@@ -90,6 +94,10 @@ public sealed class TabManager : IAsyncDisposable
{
Title = snap.Title,
IsAudioOnly = snap.IsAudioOnly,
+ IsDarkMode = snap.IsDarkMode,
+ DarkBrightness = snap.DarkBrightness,
+ DarkContrast = snap.DarkContrast,
+ DarkSaturation = snap.DarkSaturation,
ScrollY = snap.ScrollY,
MediaPosition = snap.MediaPosition,
WasPlaying = snap.WasPlaying,
@@ -168,6 +176,87 @@ public sealed class TabManager : IAsyncDisposable
await ApplyLightweightPageModeAsync(_audioOnly || _minimizedMode);
}
+ public async Task ToggleDarkModeAsync()
+ {
+ if (ActiveTab is null) return;
+ ActiveTab.IsDarkMode = !ActiveTab.IsDarkMode;
+ await ApplyDarkModeAsync(ActiveTab.IsDarkMode);
+ await SaveSessionAsync();
+ }
+
+ public async Task SetDarkModeParametersAsync(double brightness, double contrast, double saturation)
+ {
+ if (ActiveTab is null) return;
+ ActiveTab.DarkBrightness = Math.Clamp(brightness, 0.45, 1.0);
+ ActiveTab.DarkContrast = Math.Clamp(contrast, 0.8, 1.4);
+ ActiveTab.DarkSaturation = Math.Clamp(saturation, 0.0, 1.2);
+ if (ActiveTab.IsDarkMode) await ApplyDarkModeAsync(true);
+ await SaveSessionAsync();
+ }
+
+ public async Task StartElementPickerAsync()
+ {
+ if (_view.CoreWebView2 is null || ActiveTab is null) return;
+ await _view.CoreWebView2.ExecuteScriptAsync("""
+ (()=>{
+ if(window.__foxElementPicker){window.__foxElementPicker();return;}
+ const previousCursor=document.documentElement.style.cursor;
+ const getSelector=(el)=>{
+ if(el.id)return '#'+CSS.escape(el.id);
+ const parts=[];
+ while(el&&el.nodeType===1&&el!==document.body){
+ let part=el.tagName.toLowerCase();
+ if(el.classList.length)part+='.'+[...el.classList].slice(0,2).map(CSS.escape).join('.');
+ const siblings=[...el.parentElement.children].filter(x=>x.tagName===el.tagName);
+ if(siblings.length>1)part+=`:nth-of-type(${siblings.indexOf(el)+1})`;
+ parts.unshift(part);el=el.parentElement;
+ }
+ return parts.join(' > ');
+ };
+ let last=null;
+ const over=(event)=>{event.stopPropagation();if(last)last.style.outline='';last=event.target;last.style.outline='2px solid #78AFFF';};
+ const click=(event)=>{event.preventDefault();event.stopPropagation();const selector=getSelector(event.target);cleanup();window.chrome?.webview?.postMessage(JSON.stringify({type:'fox-block-element',selector}));};
+ const cleanup=()=>{document.removeEventListener('mouseover',over,true);document.removeEventListener('click',click,true);if(last)last.style.outline='';document.documentElement.style.cursor=previousCursor;window.__foxElementPicker=null;};
+ window.__foxElementPicker=cleanup;document.documentElement.style.cursor='crosshair';document.addEventListener('mouseover',over,true);document.addEventListener('click',click,true);
+ })();
+ """);
+ }
+
+ public string? CurrentHost => GetCurrentHost();
+
+ public IReadOnlyList GetBlockedSelectors()
+ {
+ var host = GetCurrentHost();
+ return host is not null && _blockedElements.TryGetValue(host, out var selectors)
+ ? selectors.ToArray()
+ : Array.Empty();
+ }
+
+ public async Task RemoveBlockedSelectorAsync(string selector)
+ {
+ var host = GetCurrentHost();
+ if (host is null || !_blockedElements.TryGetValue(host, out var selectors)) return;
+ selectors.RemoveAll(x => string.Equals(x, selector, StringComparison.Ordinal));
+ SaveBlockedElements();
+ await ApplyBlockedElementsAsync();
+ }
+
+ public async Task ClearBlockedForCurrentHostAsync()
+ {
+ var host = GetCurrentHost();
+ if (host is null) return;
+ _blockedElements.Remove(host);
+ SaveBlockedElements();
+ await ApplyBlockedElementsAsync();
+ }
+
+ public async Task ClearAllBlockedAsync()
+ {
+ _blockedElements.Clear();
+ SaveBlockedElements();
+ await ApplyBlockedElementsAsync();
+ }
+
public async Task SetMinimizedModeAsync(bool enabled)
{
_minimizedMode = enabled;
@@ -218,6 +307,8 @@ public sealed class TabManager : IAsyncDisposable
ActiveTab.Title = string.IsNullOrWhiteSpace(_view.CoreWebView2.DocumentTitle) ? "Нова вкладка" : _view.CoreWebView2.DocumentTitle;
ActiveTabStateChanged?.Invoke(this, EventArgs.Empty);
await ApplyLightweightPageModeAsync(_audioOnly || _minimizedMode);
+ await ApplyDarkModeAsync(ActiveTab.IsDarkMode);
+ await ApplyBlockedElementsAsync();
await RestoreActiveStateAsync();
await SaveSessionAsync();
}
@@ -242,6 +333,69 @@ public sealed class TabManager : IAsyncDisposable
_ = SaveSessionAsync();
}
+ private void OnWebMessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs e)
+ {
+ try
+ {
+ var payload = JsonSerializer.Deserialize(e.WebMessageAsJson);
+ if (string.IsNullOrWhiteSpace(payload)) return;
+ using var document = JsonDocument.Parse(payload);
+ if (!document.RootElement.TryGetProperty("type", out var type) || type.GetString() != "fox-block-element") return;
+ var selector = document.RootElement.GetProperty("selector").GetString();
+ var host = GetCurrentHost();
+ if (string.IsNullOrWhiteSpace(selector) || string.IsNullOrWhiteSpace(host)) return;
+ if (!_blockedElements.TryGetValue(host, out var selectors))
+ _blockedElements[host] = selectors = new List();
+ if (!selectors.Contains(selector, StringComparer.Ordinal)) selectors.Add(selector);
+ SaveBlockedElements();
+ _ = ApplyBlockedElementsAsync();
+ }
+ catch (JsonException) { }
+ }
+
+ private async Task ApplyBlockedElementsAsync()
+ {
+ if (_view.CoreWebView2 is null) return;
+ var host = GetCurrentHost();
+ var selectors = host is not null && _blockedElements.TryGetValue(host, out var savedSelectors)
+ ? savedSelectors
+ : new List();
+ var json = JsonSerializer.Serialize(selectors);
+ var script = """
+ (()=>{const selectors=__SELECTORS__;let s=document.getElementById('fox-blocked-elements');if(!selectors.length){s?.remove();return;}if(!s){s=document.createElement('style');s.id='fox-blocked-elements';(document.head||document.documentElement).appendChild(s);}s.textContent=selectors.map(x=>x+'{display:none!important;}').join('');})();
+ """.Replace("__SELECTORS__", json, StringComparison.Ordinal);
+ try { await _view.CoreWebView2.ExecuteScriptAsync(script); } catch { }
+ }
+
+ private string? GetCurrentHost()
+ {
+ return Uri.TryCreate(ActiveTab?.Address, UriKind.Absolute, out var uri) &&
+ (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
+ ? uri.Host.ToLowerInvariant()
+ : null;
+ }
+
+ private Dictionary> LoadBlockedElements()
+ {
+ try
+ {
+ if (!File.Exists(_blockedElementsPath)) return new(StringComparer.OrdinalIgnoreCase);
+ return JsonSerializer.Deserialize>>(File.ReadAllText(_blockedElementsPath))
+ ?? new(StringComparer.OrdinalIgnoreCase);
+ }
+ catch { return new(StringComparer.OrdinalIgnoreCase); }
+ }
+
+ private void SaveBlockedElements()
+ {
+ try
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(_blockedElementsPath)!);
+ File.WriteAllText(_blockedElementsPath, JsonSerializer.Serialize(_blockedElements, new JsonSerializerOptions { WriteIndented = true }));
+ }
+ catch { }
+ }
+
private async Task CaptureActiveStateAsync()
{
if (ActiveTab is null || _view.CoreWebView2 is null) return;
@@ -301,6 +455,37 @@ public sealed class TabManager : IAsyncDisposable
await _view.CoreWebView2.ExecuteScriptAsync(script);
}
+ private async Task ApplyDarkModeAsync(bool enabled)
+ {
+ if (_view.CoreWebView2 is null || ActiveTab is null) return;
+ if (!Uri.TryCreate(ActiveTab.Address, UriKind.Absolute, out var uri) ||
+ (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) return;
+
+ var brightness = ActiveTab.DarkBrightness.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ var contrast = ActiveTab.DarkContrast.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ var saturation = ActiveTab.DarkSaturation.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ var script = enabled
+ ? """
+ (()=>{
+ const root=document.documentElement;
+ if(!root)return;
+ root.classList.add('fox-dark-mode');
+ document.getElementById('fox-dark-mode')?.remove();
+ const s=document.createElement('style');
+ s.id='fox-dark-mode';
+ s.textContent=`html.fox-dark-mode{background:#111!important;color-scheme:dark!important}html.fox-dark-mode body{background-color:#15171b!important;filter:brightness(__BRIGHTNESS__) contrast(__CONTRAST__) saturate(__SATURATION__)!important}html.fox-dark-mode input,html.fox-dark-mode textarea,html.fox-dark-mode select,html.fox-dark-mode button{color-scheme:dark!important}`;
+ (document.head||root).appendChild(s);
+ })();
+ """
+ : """
+ (()=>{document.documentElement?.classList.remove('fox-dark-mode');document.getElementById('fox-dark-mode')?.remove();})();
+ """;
+ script = script.Replace("__BRIGHTNESS__", brightness, StringComparison.Ordinal)
+ .Replace("__CONTRAST__", contrast, StringComparison.Ordinal)
+ .Replace("__SATURATION__", saturation, StringComparison.Ordinal);
+ try { await _view.CoreWebView2.ExecuteScriptAsync(script); } catch { }
+ }
+
private static string NormalizeAddress(string value)
{
var text = value.Trim();
diff --git a/MainWindow.xaml b/MainWindow.xaml
index d43e862..2683168 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -139,12 +139,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index b8d5985..cd7a774 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -20,6 +20,7 @@ public partial class MainWindow : Window
private bool _allowClose;
private readonly DispatcherTimer _watchdogTimer = new() { Interval = TimeSpan.FromSeconds(30) };
private AudioSessionLabeler? _audioLabeler;
+ private bool _updatingDarkModeMenu;
public MainWindow()
{
@@ -109,6 +110,83 @@ public partial class MainWindow : Window
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;
@@ -138,14 +216,28 @@ public partial class MainWindow : Window
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 screenPositionWpf = button.PointToScreen(new System.Windows.Point(0, button.ActualHeight));
- var screenPosition = new System.Drawing.Point((int)screenPositionWpf.X, (int)screenPositionWpf.Y);
+ 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 popup = new ExtensionPopupWindow(_tabManager.Environment, item.Extension.Id, item.PopupPage, screenPosition)
+ 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
};
diff --git a/Native/Win32Interop.cs b/Native/Win32Interop.cs
index 794b45f..e327df9 100644
--- a/Native/Win32Interop.cs
+++ b/Native/Win32Interop.cs
@@ -10,6 +10,23 @@ internal static class Win32Interop
[DllImport("dwmapi.dll", PreserveSig = true)]
private static extern int DwmSetWindowAttribute(nint hwnd, int attribute, ref int value, int size);
+ private const int GwlStyle = -16;
+ private const nint WsBorder = 0x00800000;
+ private const nint WsDlgFrame = 0x00400000;
+ private const uint SwpNoMove = 0x0002;
+ private const uint SwpNoSize = 0x0001;
+ private const uint SwpNoZOrder = 0x0004;
+ private const uint SwpFrameChanged = 0x0020;
+
+ [DllImport("user32.dll", EntryPoint = "GetWindowLongPtrW", SetLastError = true)]
+ private static extern nint GetWindowLongPtr(nint hwnd, int index);
+
+ [DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)]
+ private static extern nint SetWindowLongPtr(nint hwnd, int index, nint value);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern bool SetWindowPos(nint hwnd, nint insertAfter, int x, int y, int width, int height, uint flags);
+
public static void ApplyDarkTitleBar(nint hwnd, bool enabled)
{
if (hwnd == 0) return;
@@ -17,4 +34,14 @@ internal static class Win32Interop
var result = DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkMode, ref value, sizeof(int));
if (result != 0) DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkModeLegacy, ref value, sizeof(int));
}
+
+ public static void RemovePopupFrame(nint hwnd)
+ {
+ if (hwnd == 0) return;
+ var style = GetWindowLongPtr(hwnd, GwlStyle);
+ var withoutFrame = style & ~WsBorder & ~WsDlgFrame;
+ if (style == withoutFrame) return;
+ SetWindowLongPtr(hwnd, GwlStyle, withoutFrame);
+ SetWindowPos(hwnd, 0, 0, 0, 0, 0, SwpNoMove | SwpNoSize | SwpNoZOrder | SwpFrameChanged);
+ }
}
diff --git a/README.md b/README.md
index 200a6c9..e0fe0e0 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
FoxBrowser — Windows-first браузерний shell на **C# / .NET 8 WPF / WebView2**, оптимізований під фонову роботу з мінімальною кількістю живих renderer-ів.
+Правила прихованих елементів зберігаються у `blocked-elements.json` поруч із `FoxBrowser.exe`, щоб portable-версія не створювала для них окремі файли в профілі користувача.
+
## Поточна архітектура пам'яті
FoxBrowser використовує **один WebView2 на весь застосунок**. Логічні вкладки є лише metadata-об'єктами: URL, назва, scroll position, позиція медіа і прапор Audio Mode.
diff --git a/Tab.cs b/Tab.cs
index 5b7b72b..a6d33da 100644
--- a/Tab.cs
+++ b/Tab.cs
@@ -8,6 +8,7 @@ public sealed class BrowserTab : INotifyPropertyChanged
private string _title;
private string _address;
private bool _isAudioOnly;
+ private bool _isDarkMode;
private bool _isPlayingAudio;
private bool _isLoading;
@@ -23,6 +24,10 @@ public sealed class BrowserTab : INotifyPropertyChanged
public string Title { get => _title; set { if (_title == value) return; _title = value; OnPropertyChanged(); OnPropertyChanged(nameof(DisplayTitle)); } }
public bool IsLoading { get => _isLoading; set { if (_isLoading == value) return; _isLoading = value; OnPropertyChanged(); } }
public bool IsAudioOnly { get => _isAudioOnly; set { if (_isAudioOnly == value) return; _isAudioOnly = value; OnPropertyChanged(); OnPropertyChanged(nameof(DisplayTitle)); } }
+ public bool IsDarkMode { get => _isDarkMode; set { if (_isDarkMode == value) return; _isDarkMode = value; OnPropertyChanged(); } }
+ public double DarkBrightness { get; set; } = 0.72;
+ public double DarkContrast { get; set; } = 1.08;
+ public double DarkSaturation { get; set; } = 0.88;
public bool IsPlayingAudio { get => _isPlayingAudio; set { if (_isPlayingAudio == value) return; _isPlayingAudio = value; OnPropertyChanged(); OnPropertyChanged(nameof(DisplayTitle)); } }
public double ScrollY { get; set; }
public double MediaPosition { get; set; }
diff --git a/UI/BlockedElementsWindow.xaml b/UI/BlockedElementsWindow.xaml
new file mode 100644
index 0000000..9976b94
--- /dev/null
+++ b/UI/BlockedElementsWindow.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UI/BlockedElementsWindow.xaml.cs b/UI/BlockedElementsWindow.xaml.cs
new file mode 100644
index 0000000..80ba9d2
--- /dev/null
+++ b/UI/BlockedElementsWindow.xaml.cs
@@ -0,0 +1,36 @@
+using System.Windows;
+using FoxBrowser.Core;
+
+namespace FoxBrowser.UI;
+
+public partial class BlockedElementsWindow : Window
+{
+ private readonly TabManager _tabManager;
+
+ public BlockedElementsWindow(TabManager tabManager)
+ {
+ InitializeComponent();
+ _tabManager = tabManager;
+ RefreshList();
+ }
+
+ private void RefreshList()
+ {
+ SelectorList.ItemsSource = _tabManager.GetBlockedSelectors();
+ }
+
+ private async void RemoveSelected_Click(object sender, RoutedEventArgs e)
+ {
+ if (SelectorList.SelectedItem is not string selector) return;
+ await _tabManager.RemoveBlockedSelectorAsync(selector);
+ RefreshList();
+ }
+
+ private async void ClearSite_Click(object sender, RoutedEventArgs e)
+ {
+ await _tabManager.ClearBlockedForCurrentHostAsync();
+ RefreshList();
+ }
+
+ private void Close_Click(object sender, RoutedEventArgs e) => Close();
+}
diff --git a/UI/ExtensionPopupWindow.xaml b/UI/ExtensionPopupWindow.xaml
index 127693c..410d8d6 100644
--- a/UI/ExtensionPopupWindow.xaml
+++ b/UI/ExtensionPopupWindow.xaml
@@ -1,9 +1,17 @@
+
+
+
diff --git a/UI/ExtensionPopupWindow.xaml.cs b/UI/ExtensionPopupWindow.xaml.cs
index 708a115..52ffb24 100644
--- a/UI/ExtensionPopupWindow.xaml.cs
+++ b/UI/ExtensionPopupWindow.xaml.cs
@@ -1,6 +1,8 @@
using System.Windows;
+using System.Windows.Interop;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.Wpf;
+using FoxBrowser.Native;
namespace FoxBrowser.UI;
@@ -10,14 +12,33 @@ public partial class ExtensionPopupWindow : Window
private readonly string _extensionId;
private readonly string _popupPage;
- public ExtensionPopupWindow(CoreWebView2Environment environment, string extensionId, string popupPage, System.Drawing.Point screenPosition)
+ public ExtensionPopupWindow(
+ CoreWebView2Environment environment,
+ string extensionId,
+ string popupPage,
+ System.Windows.Point screenPosition,
+ System.Windows.Rect workingArea)
{
InitializeComponent();
_environment = environment;
_extensionId = extensionId;
_popupPage = popupPage;
- Left = screenPosition.X;
- Top = screenPosition.Y;
+ WindowStartupLocation = WindowStartupLocation.Manual;
+ SourceInitialized += (_, _) => Win32Interop.RemovePopupFrame(new WindowInteropHelper(this).Handle);
+
+ Width = Math.Min(Width, Math.Max(220, workingArea.Width - 8));
+ Height = Math.Min(Height, Math.Max(160, workingArea.Height - 8));
+
+ // Keep the popup on the monitor where the extension button lives.
+ // If there is no room below the toolbar, open it above the button.
+ var left = Math.Min(screenPosition.X, workingArea.Right - Width - 4);
+ left = Math.Max(left, workingArea.Left + 4);
+ var top = screenPosition.Y + 4;
+ if (top + Height > workingArea.Bottom - 4)
+ top = screenPosition.Y - Height - 4;
+ top = Math.Max(top, workingArea.Top + 4);
+ Left = left;
+ Top = top;
Deactivated += (_, _) => Close();
Loaded += async (_, _) => await InitializeAsync();
}