BASE 1.1
This commit is contained in:
1 parent
c3e1117b97
commit
9e684ec067
12 files changed
+507
-7
No files matched your search
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user