This commit is contained in:
AlastorFox committed 2026-10-01 04:38:11 +03:00
1 parent 54a3e81109
commit e1ef325b3a
11 files changed
+505 -7

No files matched your search

+59
View File
@@ -138,5 +138,64 @@
</Setter.Value>
</Setter>
</Style>
<Style x:Key="FoxContextMenu" TargetType="ContextMenu">
<Setter Property="Background" Value="#171B22" />
<Setter Property="Foreground" Value="{StaticResource TextPrimary}" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="8" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="Focusable" Value="False" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContextMenu">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8"
Padding="{TemplateBinding Padding}">
<ItemsPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="FoxMenuButton" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource TextPrimary}" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Padding" Value="9,7" />
<Setter Property="Margin" Value="0,1" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="Focusable" Value="False" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="MenuButtonBorder"
Background="{TemplateBinding Background}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="MenuButtonBorder" Property="Background" Value="{StaticResource CardBgHover}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="MenuButtonBorder" Property="Background" Value="{StaticResource AurBlueSoft}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources>
</Application>
+8
View File
@@ -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,
+185
View File
@@ -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<string, List<string>> _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<BrowserTab> 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<string> GetBlockedSelectors()
{
var host = GetCurrentHost();
return host is not null && _blockedElements.TryGetValue(host, out var selectors)
? selectors.ToArray()
: Array.Empty<string>();
}
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<string>(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<string>();
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<string>();
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<string, List<string>> LoadBlockedElements()
{
try
{
if (!File.Exists(_blockedElementsPath)) return new(StringComparer.OrdinalIgnoreCase);
return JsonSerializer.Deserialize<Dictionary<string, List<string>>>(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();
+41 -1
View File
@@ -139,12 +139,52 @@
<Button Tag="{Binding}" Click="PinnedExtension_Click" Width="32" Height="32" Padding="0" Margin="0,0,4,0" ToolTip="{Binding Name}" Style="{StaticResource IconButton}">
<Grid>
<Image Source="{Binding IconPath}" Width="18" Height="18" Stretch="Uniform" />
<TextBlock Text="{Binding Glyph}" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" />
<TextBlock Text="{Binding Glyph}"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding HasIcon}" Value="True">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button x:Name="DarkModeButton" Content="◐" ToolTip="ЛКМ: увімкнути Dark Mode · ПКМ: налаштування" Click="DarkMode_Click" PreviewMouseRightButtonUp="DarkMode_RightClick" Height="32" Width="36" Padding="0" Margin="0,0,4,0" />
<Popup x:Name="DarkModePopup" PlacementTarget="{Binding ElementName=DarkModeButton}" Placement="Bottom" StaysOpen="False" AllowsTransparency="True" PopupAnimation="Fade" Opened="DarkModeMenu_Opened">
<Border Background="#171B22" BorderBrush="#33404E" BorderThickness="1" CornerRadius="8" Padding="14" Width="250">
<StackPanel>
<TextBlock Text="Dark Mode" Foreground="{StaticResource TextPrimary}" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,8" />
<TextBlock Text="Яскравість" Foreground="{StaticResource TextSecondary}" FontSize="11" />
<Slider x:Name="DarkBrightnessSlider" Minimum="0.45" Maximum="1.00" Value="0.72" TickFrequency="0.05" IsSnapToTickEnabled="False" ValueChanged="DarkModeParameter_Changed" Margin="0,2,0,6" />
<TextBlock Text="Контраст" Foreground="{StaticResource TextSecondary}" FontSize="11" />
<Slider x:Name="DarkContrastSlider" Minimum="0.80" Maximum="1.40" Value="1.08" TickFrequency="0.05" IsSnapToTickEnabled="False" ValueChanged="DarkModeParameter_Changed" Margin="0,2,0,6" />
<TextBlock Text="Насиченість" Foreground="{StaticResource TextSecondary}" FontSize="11" />
<Slider x:Name="DarkSaturationSlider" Minimum="0.00" Maximum="1.20" Value="0.88" TickFrequency="0.05" IsSnapToTickEnabled="False" ValueChanged="DarkModeParameter_Changed" Margin="0,2,0,8" />
<Button Content="Скинути параметри" Style="{StaticResource FoxMenuButton}" Click="ResetDarkMode_Click" />
</StackPanel>
</Border>
</Popup>
<Button x:Name="ElementPickerButton" Content="✂" ToolTip="ЛКМ: вибрати елемент · ПКМ: керування вирізаними елементами" Click="ElementPicker_Click" PreviewMouseRightButtonUp="ElementPicker_RightClick" Height="32" Width="36" Padding="0" Margin="0,0,4,0" />
<Popup x:Name="BlockedElementsPopup" PlacementTarget="{Binding ElementName=ElementPickerButton}" Placement="Bottom" StaysOpen="False" AllowsTransparency="True" PopupAnimation="Fade">
<Border Background="#171B22" BorderBrush="#33404E" BorderThickness="1" CornerRadius="8" Padding="14" Width="260">
<StackPanel>
<TextBlock Text="Вирізані елементи" Foreground="{StaticResource TextPrimary}" FontWeight="SemiBold" Margin="0,0,0,8" />
<Button Content="Керувати правилами сайту…" Style="{StaticResource FoxMenuButton}" Click="ManageBlockedElements_Click" />
<Button Content="Очистити правила цього сайту" Style="{StaticResource FoxMenuButton}" Click="ClearBlockedSite_Click" />
<Button Content="Очистити всі правила" Style="{StaticResource FoxMenuButton}" Click="ClearAllBlocked_Click" />
</StackPanel>
</Border>
</Popup>
<Button Content="♫" ToolTip="Режим лише аудіо" Click="AudioOnly_Click" Height="32" Width="36" Padding="0" Margin="0,0,4,0" />
<Button Content="Розширення" ToolTip="Керування unpacked extensions" Click="Extensions_Click" Height="32" Padding="11,0" />
</StackPanel>
+95 -3
View File
@@ -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<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;
@@ -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
};
+27
View File
@@ -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);
}
}
+5
View File
@@ -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; }
+17
View File
@@ -0,0 +1,17 @@
<Window x:Class="FoxBrowser.UI.BlockedElementsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Вирізані елементи" Width="620" Height="430" MinWidth="480" MinHeight="300"
WindowStartupLocation="CenterOwner"
Background="{StaticResource HeaderBg}" Foreground="{StaticResource TextPrimary}">
<DockPanel Margin="16">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
<Button Content="Видалити вибране правило" Click="RemoveSelected_Click" Padding="10,6" />
<Button Content="Очистити сайт" Click="ClearSite_Click" Padding="10,6" Margin="8,0,0,0" />
<Button Content="Закрити" Click="Close_Click" Padding="10,6" Margin="8,0,0,0" />
</StackPanel>
<TextBlock DockPanel.Dock="Top" Text="Заблоковані елементи" FontSize="16" FontWeight="SemiBold" />
<TextBlock DockPanel.Dock="Top" Text="Вибери конкретне правило, щоб повернути один елемент. Правила діють лише для поточного сайту." Foreground="{StaticResource TextSecondary}" TextWrapping="Wrap" Margin="0,4,0,10" />
<ListBox x:Name="SelectorList" Background="{StaticResource CardBg}" BorderBrush="{StaticResource BorderMedium}" Foreground="{StaticResource TextPrimary}" />
</DockPanel>
</Window>
+36
View File
@@ -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();
}
+8
View File
@@ -1,9 +1,17 @@
<Window x:Class="FoxBrowser.UI.ExtensionPopupWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
WindowStyle="None" ResizeMode="NoResize" ShowInTaskbar="False"
Width="360" Height="520" MaxHeight="600"
Background="#202328" AllowsTransparency="False">
<shell:WindowChrome.WindowChrome>
<shell:WindowChrome
GlassFrameThickness="0"
CaptionHeight="0"
ResizeBorderThickness="0"
UseAeroCaptionButtons="False" />
</shell:WindowChrome.WindowChrome>
<Border BorderBrush="#59616D" BorderThickness="1" Background="#202328">
<Grid x:Name="PopupHost" />
</Border>
+24 -3
View File
@@ -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();
}