Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3445ab15d | ||
|
|
4b80e75a0e | ||
|
|
d1337b4d8f | ||
|
|
fc96f5a927 | ||
|
|
4d916207a4 | ||
|
|
79c1d88b58 | ||
|
|
81d1ed8379 | ||
|
|
9e684ec067 | ||
|
|
c3e1117b97 | ||
|
|
a602512ca3 |
No files matched your search
@@ -138,5 +138,64 @@
|
|||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</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.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace FoxBrowser.Core;
|
||||||
|
|
||||||
|
public sealed class AppSettings
|
||||||
|
{
|
||||||
|
public string AppDisplayName { get; set; } = "FoxBrowser";
|
||||||
|
public string? AppIconPath { get; set; }
|
||||||
|
public Dictionary<string, string> Shortcuts { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AppSettingsStore
|
||||||
|
{
|
||||||
|
public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "settings.json");
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.Never
|
||||||
|
};
|
||||||
|
|
||||||
|
public static AppSettings Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(SettingsPath)) return new AppSettings();
|
||||||
|
var settings = JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(SettingsPath), JsonOptions);
|
||||||
|
if (settings is null) return new AppSettings();
|
||||||
|
if (string.IsNullOrWhiteSpace(settings.AppDisplayName)) settings.AppDisplayName = "FoxBrowser";
|
||||||
|
if (!string.IsNullOrWhiteSpace(settings.AppIconPath) && !File.Exists(settings.AppIconPath))
|
||||||
|
settings.AppIconPath = null;
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new AppSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Save(AppSettings settings)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(AppContext.BaseDirectory);
|
||||||
|
var tempPath = SettingsPath + ".tmp";
|
||||||
|
File.WriteAllText(tempPath, JsonSerializer.Serialize(settings, JsonOptions));
|
||||||
|
File.Move(tempPath, SettingsPath, overwrite: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,23 +13,32 @@ public sealed class AudioSessionLabeler : IDisposable
|
|||||||
{
|
{
|
||||||
private const uint SnapshotProcess = 0x00000002;
|
private const uint SnapshotProcess = 0x00000002;
|
||||||
private readonly uint _rootProcessId;
|
private readonly uint _rootProcessId;
|
||||||
private readonly string _iconPath;
|
private string _displayName;
|
||||||
|
private string _iconPath;
|
||||||
private readonly MMDeviceEnumerator _devices = new();
|
private readonly MMDeviceEnumerator _devices = new();
|
||||||
private readonly AudioSessionManager _sessions;
|
private readonly AudioSessionManager _sessions;
|
||||||
private readonly System.Threading.Timer _refreshTimer;
|
private readonly System.Threading.Timer _refreshTimer;
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public AudioSessionLabeler(uint rootProcessId, string iconPath)
|
public AudioSessionLabeler(uint rootProcessId, string iconPath, string displayName = "FoxBrowser")
|
||||||
{
|
{
|
||||||
_rootProcessId = rootProcessId;
|
_rootProcessId = rootProcessId;
|
||||||
_iconPath = iconPath;
|
_iconPath = iconPath;
|
||||||
|
_displayName = string.IsNullOrWhiteSpace(displayName) ? "FoxBrowser" : displayName;
|
||||||
var device = _devices.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
var device = _devices.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||||
_sessions = device.AudioSessionManager;
|
_sessions = device.AudioSessionManager;
|
||||||
_sessions.OnSessionCreated += OnSessionCreated;
|
_sessions.OnSessionCreated += OnSessionCreated;
|
||||||
_refreshTimer = new System.Threading.Timer(_ => RefreshAndLabel(), null, TimeSpan.Zero, TimeSpan.FromSeconds(2));
|
_refreshTimer = new System.Threading.Timer(_ => RefreshAndLabel(), null, TimeSpan.Zero, TimeSpan.FromSeconds(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UpdateIdentity(string displayName, string iconPath)
|
||||||
|
{
|
||||||
|
_displayName = string.IsNullOrWhiteSpace(displayName) ? "FoxBrowser" : displayName;
|
||||||
|
_iconPath = iconPath;
|
||||||
|
RefreshAndLabel();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnSessionCreated(object sender, IAudioSessionControl nativeSession)
|
private void OnSessionCreated(object sender, IAudioSessionControl nativeSession)
|
||||||
{
|
{
|
||||||
LabelSession(new AudioSessionControl(nativeSession), disposeAfter: true);
|
LabelSession(new AudioSessionControl(nativeSession), disposeAfter: true);
|
||||||
@@ -64,7 +73,7 @@ public sealed class AudioSessionLabeler : IDisposable
|
|||||||
{
|
{
|
||||||
var ownedPids = knownOwnedPids ?? GetFoxBrowserWebViewPids();
|
var ownedPids = knownOwnedPids ?? GetFoxBrowserWebViewPids();
|
||||||
if (!ownedPids.Contains(session.GetProcessID)) return;
|
if (!ownedPids.Contains(session.GetProcessID)) return;
|
||||||
session.DisplayName = "FoxBrowser";
|
session.DisplayName = _displayName;
|
||||||
if (File.Exists(_iconPath)) session.IconPath = _iconPath;
|
if (File.Exists(_iconPath)) session.IconPath = _iconPath;
|
||||||
}
|
}
|
||||||
catch { /* A session may close between PID lookup and property update. */ }
|
catch { /* A session may close between PID lookup and property update. */ }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Web.WebView2.Core;
|
using Microsoft.Web.WebView2.Core;
|
||||||
@@ -18,13 +19,15 @@ public sealed class PinnedExtensionItem
|
|||||||
public sealed class ExtensionManager
|
public sealed class ExtensionManager
|
||||||
{
|
{
|
||||||
private readonly CoreWebView2Profile _profile;
|
private readonly CoreWebView2Profile _profile;
|
||||||
private readonly string _metadataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FoxBrowser", "extension-options.json");
|
private readonly string _metadataPath = Path.Combine(AppContext.BaseDirectory, "extension-options.json");
|
||||||
private readonly string _uiMetadataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FoxBrowser", "extension-ui.json");
|
private readonly string _uiMetadataPath = Path.Combine(AppContext.BaseDirectory, "extension-ui.json");
|
||||||
private readonly string _pinnedPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FoxBrowser", "pinned-extensions.json");
|
private readonly string _pinnedPath = Path.Combine(AppContext.BaseDirectory, "pinned-extensions.json");
|
||||||
private readonly Dictionary<string, string?> _optionsPages;
|
private readonly Dictionary<string, string?> _optionsPages;
|
||||||
private readonly Dictionary<string, ExtensionUiMetadata> _uiMetadata;
|
private readonly Dictionary<string, ExtensionUiMetadata> _uiMetadata;
|
||||||
private readonly HashSet<string> _pinnedIds;
|
private readonly HashSet<string> _pinnedIds;
|
||||||
|
|
||||||
|
public static string ExtensionsRootPath => WebView2EnvironmentFactory.PortableExtensionsPath;
|
||||||
|
|
||||||
public ExtensionManager(CoreWebView2Profile profile)
|
public ExtensionManager(CoreWebView2Profile profile)
|
||||||
{
|
{
|
||||||
_profile = profile;
|
_profile = profile;
|
||||||
@@ -63,6 +66,75 @@ public sealed class ExtensionManager
|
|||||||
return extension;
|
return extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<CoreWebView2BrowserExtension> AddArchiveAsync(string archivePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(archivePath)) throw new FileNotFoundException("Файл extension не знайдено.", archivePath);
|
||||||
|
var extensionName = Path.GetFileNameWithoutExtension(archivePath);
|
||||||
|
var safeName = string.Concat(extensionName.Select(ch => char.IsLetterOrDigit(ch) || ch is '-' or '_' ? ch : '_'));
|
||||||
|
if (string.IsNullOrWhiteSpace(safeName)) safeName = "extension";
|
||||||
|
var destination = Path.Combine(ExtensionsRootPath, $"{safeName}_{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(destination);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var input = File.OpenRead(archivePath);
|
||||||
|
using var archive = await OpenZipPayloadAsync(input, archivePath);
|
||||||
|
archive.ExtractToDirectory(destination);
|
||||||
|
|
||||||
|
var manifestFolder = FindManifestFolder(destination);
|
||||||
|
return await AddExtensionAsync(manifestFolder);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try { Directory.Delete(destination, recursive: true); } catch { }
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void OpenExtensionsFolder()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ExtensionsRootPath);
|
||||||
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "explorer.exe",
|
||||||
|
Arguments = $"\"{ExtensionsRootPath}\"",
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindManifestFolder(string destination)
|
||||||
|
{
|
||||||
|
var rootManifest = Path.Combine(destination, "manifest.json");
|
||||||
|
if (File.Exists(rootManifest)) return destination;
|
||||||
|
var nested = Directory.EnumerateFiles(destination, "manifest.json", SearchOption.AllDirectories).FirstOrDefault();
|
||||||
|
if (nested is null) throw new InvalidDataException("В архіві не знайдено manifest.json.");
|
||||||
|
return Path.GetDirectoryName(nested)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<ZipArchive> OpenZipPayloadAsync(FileStream input, string archivePath)
|
||||||
|
{
|
||||||
|
if (!archivePath.EndsWith(".crx", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return new ZipArchive(input, ZipArchiveMode.Read, leaveOpen: false);
|
||||||
|
|
||||||
|
var header = new byte[16];
|
||||||
|
var read = await input.ReadAsync(header);
|
||||||
|
if (read < 16 || header[0] != (byte)'C' || header[1] != (byte)'r' || header[2] != (byte)'2' || header[3] != (byte)'4')
|
||||||
|
throw new InvalidDataException("Файл CRX має некоректний заголовок.");
|
||||||
|
|
||||||
|
var version = BitConverter.ToInt32(header, 4);
|
||||||
|
var zipOffset = version switch
|
||||||
|
{
|
||||||
|
2 => 16 + BitConverter.ToInt32(header, 8) + BitConverter.ToInt32(header, 12),
|
||||||
|
3 => 12 + BitConverter.ToInt32(header, 8),
|
||||||
|
_ => throw new InvalidDataException($"Непідтримувана версія CRX: {version}.")
|
||||||
|
};
|
||||||
|
input.Position = zipOffset;
|
||||||
|
var payload = new MemoryStream();
|
||||||
|
await input.CopyToAsync(payload);
|
||||||
|
payload.Position = 0;
|
||||||
|
return new ZipArchive(payload, ZipArchiveMode.Read, leaveOpen: false);
|
||||||
|
}
|
||||||
|
|
||||||
public string? GetOptionsPage(string extensionId) => _optionsPages.TryGetValue(extensionId, out var page) ? page : null;
|
public string? GetOptionsPage(string extensionId) => _optionsPages.TryGetValue(extensionId, out var page) ? page : null;
|
||||||
public string? GetPopupPage(string extensionId) => _uiMetadata.TryGetValue(extensionId, out var metadata) ? metadata.PopupPage : null;
|
public string? GetPopupPage(string extensionId) => _uiMetadata.TryGetValue(extensionId, out var metadata) ? metadata.PopupPage : null;
|
||||||
public string? GetIconPath(string extensionId) => _uiMetadata.TryGetValue(extensionId, out var metadata) ? metadata.IconPath : null;
|
public string? GetIconPath(string extensionId) => _uiMetadata.TryGetValue(extensionId, out var metadata) ? metadata.IconPath : null;
|
||||||
|
|||||||
+16
-4
@@ -13,6 +13,10 @@ public sealed class TabSnapshot
|
|||||||
public string Address { get; init; } = "https://www.google.com";
|
public string Address { get; init; } = "https://www.google.com";
|
||||||
public string Title { get; init; } = "Нова вкладка";
|
public string Title { get; init; } = "Нова вкладка";
|
||||||
public bool IsAudioOnly { 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 ScrollY { get; init; }
|
||||||
public double MediaPosition { get; init; }
|
public double MediaPosition { get; init; }
|
||||||
public bool WasPlaying { get; init; }
|
public bool WasPlaying { get; init; }
|
||||||
@@ -26,16 +30,15 @@ public sealed class Session
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves and loads the browser session (open tabs + per-tab state) to a JSON
|
/// Saves and loads the browser session (open tabs + per-tab state) to a JSON
|
||||||
/// file in LocalApplicationData\FoxBrowser\session.json.
|
/// file in the portable FoxBrowser folder next to the executable.
|
||||||
///
|
///
|
||||||
/// Crash-recovery strategy: the file is re-written after every successful
|
/// Crash-recovery strategy: the file is re-written after every successful
|
||||||
/// NavigationCompleted so a hard crash never loses more than one page load.
|
/// NavigationCompleted so a hard crash never loses more than one page load.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SessionStore
|
public static class SessionStore
|
||||||
{
|
{
|
||||||
private static readonly string SessionPath = Path.Combine(
|
private static readonly string SessionPath = Path.Combine(AppContext.BaseDirectory, "session.json");
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
private static readonly SemaphoreSlim SaveGate = new(1, 1);
|
||||||
"FoxBrowser", "session.json");
|
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
@@ -45,6 +48,7 @@ public static class SessionStore
|
|||||||
|
|
||||||
public static async Task SaveAsync(IEnumerable<BrowserTab> tabs, int activeIndex)
|
public static async Task SaveAsync(IEnumerable<BrowserTab> tabs, int activeIndex)
|
||||||
{
|
{
|
||||||
|
await SaveGate.WaitAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var snapshots = tabs.Select(t => new TabSnapshot
|
var snapshots = tabs.Select(t => new TabSnapshot
|
||||||
@@ -52,6 +56,10 @@ public static class SessionStore
|
|||||||
Address = t.Address,
|
Address = t.Address,
|
||||||
Title = t.Title,
|
Title = t.Title,
|
||||||
IsAudioOnly = t.IsAudioOnly,
|
IsAudioOnly = t.IsAudioOnly,
|
||||||
|
IsDarkMode = t.IsDarkMode,
|
||||||
|
DarkBrightness = t.DarkBrightness,
|
||||||
|
DarkContrast = t.DarkContrast,
|
||||||
|
DarkSaturation = t.DarkSaturation,
|
||||||
ScrollY = t.ScrollY,
|
ScrollY = t.ScrollY,
|
||||||
MediaPosition = t.MediaPosition,
|
MediaPosition = t.MediaPosition,
|
||||||
WasPlaying = t.WasPlaying,
|
WasPlaying = t.WasPlaying,
|
||||||
@@ -77,6 +85,10 @@ public static class SessionStore
|
|||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine($"[SessionStore] Save failed: {ex.Message}");
|
System.Diagnostics.Debug.WriteLine($"[SessionStore] Save failed: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SaveGate.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<Session?> LoadAsync()
|
public static async Task<Session?> LoadAsync()
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace FoxBrowser.Core;
|
||||||
|
|
||||||
|
public sealed class ShortcutManager
|
||||||
|
{
|
||||||
|
public const string CloseTab = "CloseTab";
|
||||||
|
public const string NewTab = "NewTab";
|
||||||
|
public const string ToggleTopPanel = "ToggleTopPanel";
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, KeyGesture> Defaults =
|
||||||
|
new Dictionary<string, KeyGesture>
|
||||||
|
{
|
||||||
|
[CloseTab] = new(Key.W, ModifierKeys.Control),
|
||||||
|
[NewTab] = new(Key.N, ModifierKeys.Control),
|
||||||
|
[ToggleTopPanel] = new(Key.F, ModifierKeys.Control | ModifierKeys.Shift),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Lazy<ShortcutManager> LazyInstance = new(() => new ShortcutManager());
|
||||||
|
private readonly Dictionary<string, KeyGesture> _shortcuts = new();
|
||||||
|
|
||||||
|
private ShortcutManager()
|
||||||
|
{
|
||||||
|
ResetToDefaults(save: false);
|
||||||
|
Load(AppSettingsStore.Load());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ShortcutManager Instance => LazyInstance.Value;
|
||||||
|
public IReadOnlyDictionary<string, KeyGesture> Shortcuts => _shortcuts;
|
||||||
|
public event EventHandler? Changed;
|
||||||
|
|
||||||
|
public KeyGesture Get(string action) => _shortcuts[action];
|
||||||
|
|
||||||
|
public string? FindAction(Key key, ModifierKeys modifiers)
|
||||||
|
{
|
||||||
|
return _shortcuts.FirstOrDefault(pair => pair.Value.Key == key && pair.Value.Modifiers == modifiers).Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TrySet(string action, KeyGesture gesture, out string? conflictAction)
|
||||||
|
{
|
||||||
|
conflictAction = _shortcuts.FirstOrDefault(pair => pair.Key != action && GesturesEqual(pair.Value, gesture)).Key;
|
||||||
|
if (!string.IsNullOrEmpty(conflictAction)) return false;
|
||||||
|
_shortcuts[action] = gesture;
|
||||||
|
Save();
|
||||||
|
Changed?.Invoke(this, EventArgs.Empty);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetToDefaults()
|
||||||
|
{
|
||||||
|
ResetToDefaults(save: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetActionLabel(string action) => action switch
|
||||||
|
{
|
||||||
|
CloseTab => "Закрити вкладку",
|
||||||
|
NewTab => "Нова вкладка",
|
||||||
|
ToggleTopPanel => "Показати/сховати верхню панель",
|
||||||
|
_ => action,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string Format(KeyGesture gesture)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
if (gesture.Modifiers.HasFlag(ModifierKeys.Control)) parts.Add("Ctrl");
|
||||||
|
if (gesture.Modifiers.HasFlag(ModifierKeys.Shift)) parts.Add("Shift");
|
||||||
|
if (gesture.Modifiers.HasFlag(ModifierKeys.Alt)) parts.Add("Alt");
|
||||||
|
if (gesture.Modifiers.HasFlag(ModifierKeys.Windows)) parts.Add("Win");
|
||||||
|
parts.Add(gesture.Key.ToString());
|
||||||
|
return string.Join("+", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryCreateGesture(Key key, ModifierKeys modifiers, out KeyGesture? gesture)
|
||||||
|
{
|
||||||
|
gesture = null;
|
||||||
|
if (key is Key.LeftCtrl or Key.RightCtrl or Key.LeftShift or Key.RightShift or Key.LeftAlt or Key.RightAlt or Key.LWin or Key.RWin)
|
||||||
|
return false;
|
||||||
|
if (key == Key.None || modifiers == ModifierKeys.None)
|
||||||
|
return false;
|
||||||
|
gesture = new KeyGesture(key, modifiers);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Load(AppSettings settings)
|
||||||
|
{
|
||||||
|
if (settings.Shortcuts is null) return;
|
||||||
|
foreach (var pair in settings.Shortcuts)
|
||||||
|
{
|
||||||
|
if (!Defaults.ContainsKey(pair.Key)) continue;
|
||||||
|
if (TryParse(pair.Value, out var gesture)) _shortcuts[pair.Key] = gesture!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
var settings = AppSettingsStore.Load();
|
||||||
|
settings.Shortcuts = _shortcuts.ToDictionary(pair => pair.Key, pair => Format(pair.Value));
|
||||||
|
AppSettingsStore.Save(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetToDefaults(bool save)
|
||||||
|
{
|
||||||
|
_shortcuts.Clear();
|
||||||
|
foreach (var pair in Defaults) _shortcuts[pair.Key] = pair.Value;
|
||||||
|
if (save) Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GesturesEqual(KeyGesture left, KeyGesture right) => left.Key == right.Key && left.Modifiers == right.Modifiers;
|
||||||
|
|
||||||
|
private static bool TryParse(string? value, out KeyGesture? gesture)
|
||||||
|
{
|
||||||
|
gesture = null;
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) return false;
|
||||||
|
var parts = value.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
if (parts.Length < 2 || !Enum.TryParse(parts[^1], true, out Key key)) return false;
|
||||||
|
var modifiers = ModifierKeys.None;
|
||||||
|
foreach (var part in parts[..^1])
|
||||||
|
{
|
||||||
|
if (part.Equals("Ctrl", StringComparison.OrdinalIgnoreCase) || part.Equals("Control", StringComparison.OrdinalIgnoreCase)) modifiers |= ModifierKeys.Control;
|
||||||
|
else if (part.Equals("Shift", StringComparison.OrdinalIgnoreCase)) modifiers |= ModifierKeys.Shift;
|
||||||
|
else if (part.Equals("Alt", StringComparison.OrdinalIgnoreCase)) modifiers |= ModifierKeys.Alt;
|
||||||
|
else if (part.Equals("Win", StringComparison.OrdinalIgnoreCase) || part.Equals("Windows", StringComparison.OrdinalIgnoreCase)) modifiers |= ModifierKeys.Windows;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
if (!TryCreateGesture(key, modifiers, out gesture)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+273
-4
@@ -26,6 +26,9 @@ public sealed class TabManager : IAsyncDisposable
|
|||||||
private bool _minimizedMode;
|
private bool _minimizedMode;
|
||||||
private bool _navigationInProgress;
|
private bool _navigationInProgress;
|
||||||
private bool _switchInProgress;
|
private bool _switchInProgress;
|
||||||
|
private bool _restoringSession;
|
||||||
|
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)
|
public TabManager(Grid host, CoreWebView2Environment environment, bool extensionsAvailable = true)
|
||||||
{
|
{
|
||||||
@@ -34,12 +37,15 @@ public sealed class TabManager : IAsyncDisposable
|
|||||||
ExtensionsAvailable = extensionsAvailable;
|
ExtensionsAvailable = extensionsAvailable;
|
||||||
_view = new WebView2 { Visibility = Visibility.Visible };
|
_view = new WebView2 { Visibility = Visibility.Visible };
|
||||||
_host.Children.Add(_view);
|
_host.Children.Add(_view);
|
||||||
|
_blockedElements = LoadBlockedElements();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<BrowserTab> Tabs { get; } = new();
|
public ObservableCollection<BrowserTab> Tabs { get; } = new();
|
||||||
public BrowserTab? ActiveTab { get; private set; }
|
public BrowserTab? ActiveTab { get; private set; }
|
||||||
public bool IsMinimizedMode => _minimizedMode;
|
public bool IsMinimizedMode => _minimizedMode;
|
||||||
public bool ExtensionsAvailable { get; private set; }
|
public bool ExtensionsAvailable { get; private set; }
|
||||||
|
public bool SessionReady { get; private set; }
|
||||||
|
public bool IsElementPickerActive { get; private set; }
|
||||||
public CoreWebView2Environment Environment => _environment;
|
public CoreWebView2Environment Environment => _environment;
|
||||||
public WebView2 View => _view;
|
public WebView2 View => _view;
|
||||||
public ExtensionManager? ExtensionManager { get; private set; }
|
public ExtensionManager? ExtensionManager { get; private set; }
|
||||||
@@ -64,8 +70,19 @@ public sealed class TabManager : IAsyncDisposable
|
|||||||
_view.CoreWebView2.DocumentTitleChanged += OnDocumentTitleChanged;
|
_view.CoreWebView2.DocumentTitleChanged += OnDocumentTitleChanged;
|
||||||
_view.CoreWebView2.IsDocumentPlayingAudioChanged += OnAudioStateChanged;
|
_view.CoreWebView2.IsDocumentPlayingAudioChanged += OnAudioStateChanged;
|
||||||
_view.CoreWebView2.HistoryChanged += OnHistoryChanged;
|
_view.CoreWebView2.HistoryChanged += OnHistoryChanged;
|
||||||
|
_view.CoreWebView2.WebMessageReceived += OnWebMessageReceived;
|
||||||
|
_restoringSession = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
await RestoreSessionOrDefaultAsync();
|
await RestoreSessionOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_restoringSession = false;
|
||||||
|
SessionReady = true;
|
||||||
|
}
|
||||||
|
await SaveSessionAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SaveSessionAsync()
|
public async Task SaveSessionAsync()
|
||||||
{
|
{
|
||||||
@@ -90,6 +107,10 @@ public sealed class TabManager : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
Title = snap.Title,
|
Title = snap.Title,
|
||||||
IsAudioOnly = snap.IsAudioOnly,
|
IsAudioOnly = snap.IsAudioOnly,
|
||||||
|
IsDarkMode = snap.IsDarkMode,
|
||||||
|
DarkBrightness = snap.DarkBrightness,
|
||||||
|
DarkContrast = snap.DarkContrast,
|
||||||
|
DarkSaturation = snap.DarkSaturation,
|
||||||
ScrollY = snap.ScrollY,
|
ScrollY = snap.ScrollY,
|
||||||
MediaPosition = snap.MediaPosition,
|
MediaPosition = snap.MediaPosition,
|
||||||
WasPlaying = snap.WasPlaying,
|
WasPlaying = snap.WasPlaying,
|
||||||
@@ -117,11 +138,14 @@ public sealed class TabManager : IAsyncDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (ActiveTab is not null) await CaptureActiveStateAsync();
|
if (ActiveTab is not null) await CaptureActiveStateAsync();
|
||||||
|
IsElementPickerActive = false;
|
||||||
ActiveTab = tab;
|
ActiveTab = tab;
|
||||||
_audioOnly = tab.IsAudioOnly;
|
_audioOnly = tab.IsAudioOnly;
|
||||||
ActiveTabStateChanged?.Invoke(this, EventArgs.Empty);
|
ActiveTabStateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
_navigationInProgress = true;
|
_navigationInProgress = true;
|
||||||
|
await UpdateDocumentCreatedScriptAsync();
|
||||||
core.Navigate(tab.Address);
|
core.Navigate(tab.Address);
|
||||||
|
if (!_restoringSession) await SaveSessionAsync();
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
}
|
}
|
||||||
finally { _switchInProgress = false; }
|
finally { _switchInProgress = false; }
|
||||||
@@ -135,6 +159,7 @@ public sealed class TabManager : IAsyncDisposable
|
|||||||
tab.Address = NormalizeAddress(address);
|
tab.Address = NormalizeAddress(address);
|
||||||
if (ActiveTab != tab) { await SwitchToTabAsync(tab); return; }
|
if (ActiveTab != tab) { await SwitchToTabAsync(tab); return; }
|
||||||
_navigationInProgress = true;
|
_navigationInProgress = true;
|
||||||
|
await UpdateDocumentCreatedScriptAsync();
|
||||||
core.Navigate(tab.Address);
|
core.Navigate(tab.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +193,96 @@ public sealed class TabManager : IAsyncDisposable
|
|||||||
await ApplyLightweightPageModeAsync(_audioOnly || _minimizedMode);
|
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;
|
||||||
|
if (IsElementPickerActive)
|
||||||
|
{
|
||||||
|
await _view.CoreWebView2.ExecuteScriptAsync("window.__foxElementPicker?.();");
|
||||||
|
IsElementPickerActive = false;
|
||||||
|
ActiveTabStateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IsElementPickerActive = true;
|
||||||
|
ActiveTabStateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
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)
|
public async Task SetMinimizedModeAsync(bool enabled)
|
||||||
{
|
{
|
||||||
_minimizedMode = enabled;
|
_minimizedMode = enabled;
|
||||||
@@ -187,13 +302,30 @@ public sealed class TabManager : IAsyncDisposable
|
|||||||
public void GoForward() { var core = _view.CoreWebView2; if (core?.CanGoForward == true) core.GoForward(); }
|
public void GoForward() { var core = _view.CoreWebView2; if (core?.CanGoForward == true) core.GoForward(); }
|
||||||
public void Reload() { var core = _view.CoreWebView2; if (core is null) return; _navigationInProgress = true; core.Reload(); }
|
public void Reload() { var core = _view.CoreWebView2; if (core is null) return; _navigationInProgress = true; core.Reload(); }
|
||||||
|
|
||||||
public void CloseTab(BrowserTab tab)
|
public async Task CloseTabAsync(BrowserTab tab)
|
||||||
{
|
{
|
||||||
var index = Tabs.IndexOf(tab);
|
var index = Tabs.IndexOf(tab);
|
||||||
|
if (index < 0) return;
|
||||||
Tabs.Remove(tab);
|
Tabs.Remove(tab);
|
||||||
if (Tabs.Count == 0) { _ = AddTabAsync(); return; }
|
if (Tabs.Count == 0)
|
||||||
if (ActiveTab == tab) _ = SwitchToTabAsync(Tabs[Math.Min(index, Tabs.Count - 1)]);
|
{
|
||||||
_ = SaveSessionAsync();
|
await AddTabAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ActiveTab == tab)
|
||||||
|
await SwitchToTabAsync(Tabs[Math.Min(index, Tabs.Count - 1)]);
|
||||||
|
else
|
||||||
|
await SaveSessionAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MoveTabAsync(BrowserTab tab, int targetIndex)
|
||||||
|
{
|
||||||
|
var sourceIndex = Tabs.IndexOf(tab);
|
||||||
|
if (sourceIndex < 0 || Tabs.Count < 2) return;
|
||||||
|
targetIndex = Math.Clamp(targetIndex, 0, Tabs.Count - 1);
|
||||||
|
if (sourceIndex == targetIndex) return;
|
||||||
|
Tabs.Move(sourceIndex, targetIndex);
|
||||||
|
await SaveSessionAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
@@ -218,6 +350,8 @@ public sealed class TabManager : IAsyncDisposable
|
|||||||
ActiveTab.Title = string.IsNullOrWhiteSpace(_view.CoreWebView2.DocumentTitle) ? "Нова вкладка" : _view.CoreWebView2.DocumentTitle;
|
ActiveTab.Title = string.IsNullOrWhiteSpace(_view.CoreWebView2.DocumentTitle) ? "Нова вкладка" : _view.CoreWebView2.DocumentTitle;
|
||||||
ActiveTabStateChanged?.Invoke(this, EventArgs.Empty);
|
ActiveTabStateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
await ApplyLightweightPageModeAsync(_audioOnly || _minimizedMode);
|
await ApplyLightweightPageModeAsync(_audioOnly || _minimizedMode);
|
||||||
|
await ApplyDarkModeAsync(ActiveTab.IsDarkMode);
|
||||||
|
await ApplyBlockedElementsAsync();
|
||||||
await RestoreActiveStateAsync();
|
await RestoreActiveStateAsync();
|
||||||
await SaveSessionAsync();
|
await SaveSessionAsync();
|
||||||
}
|
}
|
||||||
@@ -242,6 +376,112 @@ public sealed class TabManager : IAsyncDisposable
|
|||||||
_ = SaveSessionAsync();
|
_ = 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);
|
||||||
|
IsElementPickerActive = false;
|
||||||
|
SaveBlockedElements();
|
||||||
|
ActiveTabStateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
_ = 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 async Task UpdateDocumentCreatedScriptAsync()
|
||||||
|
{
|
||||||
|
var core = _view.CoreWebView2;
|
||||||
|
if (core is null) return;
|
||||||
|
|
||||||
|
var host = GetCurrentHost();
|
||||||
|
var selectors = host is not null && _blockedElements.TryGetValue(host, out var savedSelectors)
|
||||||
|
? savedSelectors
|
||||||
|
: new List<string>();
|
||||||
|
var darkEnabled = ActiveTab?.IsDarkMode == true &&
|
||||||
|
Uri.TryCreate(ActiveTab.Address, UriKind.Absolute, out var address) &&
|
||||||
|
(address.Scheme == Uri.UriSchemeHttp || address.Scheme == Uri.UriSchemeHttps);
|
||||||
|
var brightness = (ActiveTab?.DarkBrightness ?? 0.72).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
var contrast = (ActiveTab?.DarkContrast ?? 1.08).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
var saturation = (ActiveTab?.DarkSaturation ?? 0.88).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
var script = """
|
||||||
|
(()=>{
|
||||||
|
const selectors=__SELECTORS__, dark=__DARK__, brightness=__BRIGHTNESS__, contrast=__CONTRAST__, saturation=__SATURATION__;
|
||||||
|
const apply=()=>{
|
||||||
|
const root=document.documentElement;
|
||||||
|
if(!root){setTimeout(apply,0);return;}
|
||||||
|
let darkStyle=document.getElementById('fox-dark-mode');
|
||||||
|
if(!darkStyle){darkStyle=document.createElement('style');darkStyle.id='fox-dark-mode';root.appendChild(darkStyle);}
|
||||||
|
let blockedStyle=document.getElementById('fox-blocked-elements');
|
||||||
|
if(!blockedStyle){blockedStyle=document.createElement('style');blockedStyle.id='fox-blocked-elements';root.appendChild(blockedStyle);}
|
||||||
|
const darkCss=dark?'html{background:#111!important;color-scheme:dark!important}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}':'';
|
||||||
|
darkStyle.textContent=darkCss;
|
||||||
|
blockedStyle.textContent=selectors.map(x=>x+'{display:none!important;}').join('');
|
||||||
|
if(dark) root.classList.add('fox-dark-mode'); else root.classList.remove('fox-dark-mode');
|
||||||
|
};
|
||||||
|
apply();
|
||||||
|
})();
|
||||||
|
"""
|
||||||
|
.Replace("__SELECTORS__", JsonSerializer.Serialize(selectors), StringComparison.Ordinal)
|
||||||
|
.Replace("__DARK__", darkEnabled ? "true" : "false", StringComparison.Ordinal)
|
||||||
|
.Replace("__BRIGHTNESS__", brightness, StringComparison.Ordinal)
|
||||||
|
.Replace("__CONTRAST__", contrast, StringComparison.Ordinal)
|
||||||
|
.Replace("__SATURATION__", saturation, StringComparison.Ordinal);
|
||||||
|
try { await core.AddScriptToExecuteOnDocumentCreatedAsync(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()
|
private async Task CaptureActiveStateAsync()
|
||||||
{
|
{
|
||||||
if (ActiveTab is null || _view.CoreWebView2 is null) return;
|
if (ActiveTab is null || _view.CoreWebView2 is null) return;
|
||||||
@@ -301,6 +541,35 @@ public sealed class TabManager : IAsyncDisposable
|
|||||||
await _view.CoreWebView2.ExecuteScriptAsync(script);
|
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');
|
||||||
|
let s=document.getElementById('fox-dark-mode');
|
||||||
|
if(!s){s=document.createElement('style');s.id='fox-dark-mode';(document.head||root).appendChild(s);}
|
||||||
|
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.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)
|
private static string NormalizeAddress(string value)
|
||||||
{
|
{
|
||||||
var text = value.Trim();
|
var text = value.Trim();
|
||||||
|
|||||||
+1
-3
@@ -27,9 +27,7 @@ public sealed class VpnProfile
|
|||||||
|
|
||||||
public sealed class VpnManager
|
public sealed class VpnManager
|
||||||
{
|
{
|
||||||
private readonly string _profileDirectory = Path.Combine(
|
private readonly string _profileDirectory = Path.Combine(AppContext.BaseDirectory, "VPN");
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
||||||
"FoxBrowser", "VPN");
|
|
||||||
private string? _activeTunnel;
|
private string? _activeTunnel;
|
||||||
|
|
||||||
public VpnManager()
|
public VpnManager()
|
||||||
|
|||||||
@@ -1,36 +1,28 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security;
|
|
||||||
using Microsoft.Web.WebView2.Core;
|
using Microsoft.Web.WebView2.Core;
|
||||||
|
|
||||||
namespace FoxBrowser.Core;
|
namespace FoxBrowser.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates every WebView2 environment with one consistent set of options and
|
/// Creates WebView2 using only the portable profile next to FoxBrowser.exe.
|
||||||
/// chooses the portable profile when the application directory is writable.
|
/// No AppData/LocalAppData fallback is used.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WebView2EnvironmentFactory
|
public sealed class WebView2EnvironmentFactory
|
||||||
{
|
{
|
||||||
private const string ProductFolderName = "FoxBrowser";
|
|
||||||
private const string ProfileFolderName = "Profile";
|
private const string ProfileFolderName = "Profile";
|
||||||
private const string LegacyProfileFolderName = "WebView2";
|
private const string ExtensionsFolderName = "Extensions";
|
||||||
private const string ProtectedFolderNoticeMarker = "portable-profile-fallback-notified";
|
|
||||||
|
|
||||||
private readonly string _applicationDataRoot = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ProductFolderName);
|
|
||||||
private bool _shouldShowProtectedFolderNotice;
|
|
||||||
|
|
||||||
public string PortableProfilePath => Path.Combine(AppContext.BaseDirectory, ProfileFolderName);
|
public string PortableProfilePath => Path.Combine(AppContext.BaseDirectory, ProfileFolderName);
|
||||||
public string LegacyProfilePath => Path.Combine(_applicationDataRoot, LegacyProfileFolderName);
|
public static string PortableExtensionsPath => Path.Combine(AppContext.BaseDirectory, ExtensionsFolderName);
|
||||||
public bool UsingLegacyProfile { get; private set; }
|
|
||||||
public string? MigrationError { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a WebView2 environment. An override is used only by startup
|
/// Kept for startup compatibility. Portable mode never switches to a user profile.
|
||||||
/// recovery, which must always receive its own temporary profile.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public string? TakeStartupNotice() => null;
|
||||||
|
|
||||||
public async Task<CoreWebView2Environment> CreateAsync(string? userDataFolderOverride = null)
|
public async Task<CoreWebView2Environment> CreateAsync(string? userDataFolderOverride = null)
|
||||||
{
|
{
|
||||||
var userDataFolder = userDataFolderOverride ?? SelectPersistentProfile();
|
var userDataFolder = userDataFolderOverride ?? PortableProfilePath;
|
||||||
Directory.CreateDirectory(userDataFolder);
|
Directory.CreateDirectory(userDataFolder);
|
||||||
|
|
||||||
return await CoreWebView2Environment.CreateAsync(
|
return await CoreWebView2Environment.CreateAsync(
|
||||||
@@ -39,152 +31,8 @@ public sealed class WebView2EnvironmentFactory
|
|||||||
options: CreateWebViewOptions());
|
options: CreateWebViewOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a startup notice once for the protected-installation fallback,
|
|
||||||
/// and returns a migration error when one occurred during this startup.
|
|
||||||
/// </summary>
|
|
||||||
public string? TakeStartupNotice()
|
|
||||||
{
|
|
||||||
if (_shouldShowProtectedFolderNotice)
|
|
||||||
{
|
|
||||||
_shouldShowProtectedFolderNotice = false;
|
|
||||||
return "FoxBrowser встановлено в захищену папку — профіль зберігається " +
|
|
||||||
"у стандартному місці Windows, а не поруч із застосунком. Для " +
|
|
||||||
"портативного профілю перенесіть FoxBrowser у папку з правами запису " +
|
|
||||||
"(наприклад, Робочий стіл або Документи).";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MigrationError is not null)
|
|
||||||
return $"Не вдалося перенести профіль FoxBrowser у папку поруч із застосунком: {MigrationError}\n\n" +
|
|
||||||
"Запуск продовжено зі стандартним профілем Windows.";
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string SelectPersistentProfile()
|
|
||||||
{
|
|
||||||
UsingLegacyProfile = false;
|
|
||||||
MigrationError = null;
|
|
||||||
|
|
||||||
if (!CanWriteToApplicationDirectory())
|
|
||||||
{
|
|
||||||
UsingLegacyProfile = true;
|
|
||||||
MarkProtectedFolderNotice();
|
|
||||||
return LegacyProfilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists(PortableProfilePath) && Directory.Exists(LegacyProfilePath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CopyDirectoryContents(LegacyProfilePath, PortableProfilePath);
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SecurityException)
|
|
||||||
{
|
|
||||||
MigrationError = ex.Message;
|
|
||||||
TryDeleteDirectory(PortableProfilePath);
|
|
||||||
UsingLegacyProfile = true;
|
|
||||||
return LegacyProfilePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PortableProfilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CoreWebView2EnvironmentOptions CreateWebViewOptions() => new()
|
private static CoreWebView2EnvironmentOptions CreateWebViewOptions() => new()
|
||||||
{
|
{
|
||||||
AreBrowserExtensionsEnabled = true
|
AreBrowserExtensionsEnabled = true
|
||||||
};
|
};
|
||||||
|
|
||||||
private static bool CanWriteToApplicationDirectory()
|
|
||||||
{
|
|
||||||
var testFile = Path.Combine(AppContext.BaseDirectory, $".foxbrowser-write-test-{Guid.NewGuid():N}.tmp");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var stream = new FileStream(testFile, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
|
||||||
stream.WriteByte(0);
|
|
||||||
File.Delete(testFile);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
TryDeleteFile(testFile);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
TryDeleteFile(testFile);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (SecurityException)
|
|
||||||
{
|
|
||||||
TryDeleteFile(testFile);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MarkProtectedFolderNotice()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(_applicationDataRoot);
|
|
||||||
var marker = ProtectedFolderNoticeMarkerPath();
|
|
||||||
if (!File.Exists(marker))
|
|
||||||
{
|
|
||||||
File.WriteAllText(marker, DateTimeOffset.UtcNow.ToString("O"));
|
|
||||||
_shouldShowProtectedFolderNotice = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// The fallback profile remains valid even if the notice marker cannot be saved.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ProtectedFolderNoticeMarkerPath() => Path.Combine(_applicationDataRoot, ProtectedFolderNoticeMarker);
|
|
||||||
|
|
||||||
private static void CopyDirectoryContents(string sourceDirectory, string destinationDirectory)
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(destinationDirectory);
|
|
||||||
|
|
||||||
foreach (var directory in Directory.EnumerateDirectories(sourceDirectory, "*", SearchOption.AllDirectories))
|
|
||||||
{
|
|
||||||
var relativePath = Path.GetRelativePath(sourceDirectory, directory);
|
|
||||||
Directory.CreateDirectory(Path.Combine(destinationDirectory, relativePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var file in Directory.EnumerateFiles(sourceDirectory, "*", SearchOption.AllDirectories))
|
|
||||||
{
|
|
||||||
var relativePath = Path.GetRelativePath(sourceDirectory, file);
|
|
||||||
var destinationFile = Path.Combine(destinationDirectory, relativePath);
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!);
|
|
||||||
File.Copy(file, destinationFile, overwrite: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TryDeleteDirectory(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Directory.Exists(path))
|
|
||||||
Directory.Delete(path, recursive: true);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// A partial migration must not prevent fallback to the legacy profile.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TryDeleteFile(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(path))
|
|
||||||
File.Delete(path);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Best effort cleanup only.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+128
-18
@@ -4,9 +4,11 @@
|
|||||||
Title="FoxBrowser" Width="1180" Height="760" MinWidth="760" MinHeight="480"
|
Title="FoxBrowser" Width="1180" Height="760" MinWidth="760" MinHeight="480"
|
||||||
Background="{StaticResource WindowBg}" Foreground="{StaticResource TextPrimary}"
|
Background="{StaticResource WindowBg}" Foreground="{StaticResource TextPrimary}"
|
||||||
Closing="Window_Closing" StateChanged="Window_StateChanged"
|
Closing="Window_Closing" StateChanged="Window_StateChanged"
|
||||||
|
UseLayoutRounding="True" SnapsToDevicePixels="True"
|
||||||
TextOptions.TextFormattingMode="Display"
|
TextOptions.TextFormattingMode="Display"
|
||||||
RenderOptions.ClearTypeHint="Enabled">
|
RenderOptions.ClearTypeHint="Enabled">
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
|
<QuadraticEase x:Key="TabEase" EasingMode="EaseOut" />
|
||||||
<DataTemplate x:Key="TabTemplate">
|
<DataTemplate x:Key="TabTemplate">
|
||||||
<Grid MinWidth="126" MaxWidth="250" Height="34">
|
<Grid MinWidth="126" MaxWidth="250" Height="34">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@@ -34,7 +36,33 @@
|
|||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
BorderBrush="Transparent"
|
BorderBrush="Transparent"
|
||||||
Foreground="{StaticResource TextMuted}">
|
Foreground="{StaticResource TextMuted}"
|
||||||
|
Opacity="0">
|
||||||
|
<Button.Style>
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="Opacity" Value="0" />
|
||||||
|
<Setter Property="IsHitTestVisible" Value="False" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TabItem}, Path=IsMouseOver}" Value="True">
|
||||||
|
<Setter Property="IsHitTestVisible" Value="True" />
|
||||||
|
<DataTrigger.EnterActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.12" EasingFunction="{StaticResource TabEase}" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</DataTrigger.EnterActions>
|
||||||
|
<DataTrigger.ExitActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.12" EasingFunction="{StaticResource TabEase}" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</DataTrigger.ExitActions>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Style>
|
||||||
<Button.Template>
|
<Button.Template>
|
||||||
<ControlTemplate TargetType="Button">
|
<ControlTemplate TargetType="Button">
|
||||||
<Border x:Name="CloseBorder" Background="{TemplateBinding Background}" CornerRadius="5">
|
<Border x:Name="CloseBorder" Background="{TemplateBinding Background}" CornerRadius="5">
|
||||||
@@ -57,29 +85,67 @@
|
|||||||
<Setter Property="BorderBrush" Value="{StaticResource BorderDark}" />
|
<Setter Property="BorderBrush" Value="{StaticResource BorderDark}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextSecondary}" />
|
<Setter Property="Foreground" Value="{StaticResource TextSecondary}" />
|
||||||
<Setter Property="Margin" Value="0,0,6,0" />
|
<Setter Property="Margin" Value="0" />
|
||||||
<Setter Property="Padding" Value="12,0" />
|
<Setter Property="Padding" Value="12,0" />
|
||||||
<Setter Property="Cursor" Value="Hand" />
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="TabItem">
|
<ControlTemplate TargetType="TabItem">
|
||||||
<Border x:Name="TabBorder"
|
<Border x:Name="TabBorder"
|
||||||
Background="{TemplateBinding Background}"
|
Background="#191F28"
|
||||||
BorderBrush="{TemplateBinding BorderBrush}"
|
BorderBrush="#2B3441"
|
||||||
BorderThickness="{TemplateBinding BorderThickness}"
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
CornerRadius="8"
|
CornerRadius="8,8,0,0"
|
||||||
Padding="{TemplateBinding Padding}"
|
Padding="{TemplateBinding Padding}"
|
||||||
Margin="{TemplateBinding Margin}">
|
Margin="0,0,6,2"
|
||||||
|
RenderTransformOrigin="0.5,0.5">
|
||||||
|
<Border.RenderTransform>
|
||||||
|
<ScaleTransform ScaleX="1" ScaleY="1" />
|
||||||
|
</Border.RenderTransform>
|
||||||
<ContentPresenter ContentSource="Header" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
<ContentPresenter ContentSource="Header" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
</Border>
|
</Border>
|
||||||
<ControlTemplate.Triggers>
|
<ControlTemplate.Triggers>
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
<Setter TargetName="TabBorder" Property="Background" Value="#222A35" />
|
<Trigger.EnterActions>
|
||||||
<Setter TargetName="TabBorder" Property="BorderBrush" Value="{StaticResource BorderMedium}" />
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<ColorAnimation Storyboard.TargetName="TabBorder" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" To="#222A35" Duration="0:0:0.13" EasingFunction="{StaticResource TabEase}" />
|
||||||
|
<ColorAnimation Storyboard.TargetName="TabBorder" Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" To="#46566C" Duration="0:0:0.13" EasingFunction="{StaticResource TabEase}" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</Trigger.EnterActions>
|
||||||
|
<Trigger.ExitActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<ColorAnimation Storyboard.TargetName="TabBorder" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" To="#191F28" Duration="0:0:0.13" EasingFunction="{StaticResource TabEase}" />
|
||||||
|
<ColorAnimation Storyboard.TargetName="TabBorder" Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" To="#2B3441" Duration="0:0:0.13" EasingFunction="{StaticResource TabEase}" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</Trigger.ExitActions>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsSelected" Value="True">
|
<Trigger Property="IsSelected" Value="True">
|
||||||
<Setter TargetName="TabBorder" Property="Background" Value="#26354B" />
|
<Setter TargetName="TabBorder" Property="Effect">
|
||||||
<Setter TargetName="TabBorder" Property="BorderBrush" Value="{StaticResource AurBlue}" />
|
<Setter.Value>
|
||||||
|
<DropShadowEffect BlurRadius="7" ShadowDepth="1" Opacity="0.28" Color="#000000" />
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Trigger.EnterActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<ColorAnimation Storyboard.TargetName="TabBorder" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" To="#26354B" Duration="0:0:0.14" EasingFunction="{StaticResource TabEase}" />
|
||||||
|
<ColorAnimation Storyboard.TargetName="TabBorder" Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" To="#78AFFF" Duration="0:0:0.14" EasingFunction="{StaticResource TabEase}" />
|
||||||
|
<DoubleAnimation Storyboard.TargetName="TabBorder" Storyboard.TargetProperty="(UIElement.Opacity)" To="1" Duration="0:0:0.14" EasingFunction="{StaticResource TabEase}" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</Trigger.EnterActions>
|
||||||
|
<Trigger.ExitActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<ColorAnimation Storyboard.TargetName="TabBorder" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" To="#191F28" Duration="0:0:0.14" EasingFunction="{StaticResource TabEase}" />
|
||||||
|
<ColorAnimation Storyboard.TargetName="TabBorder" Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" To="#2B3441" Duration="0:0:0.14" EasingFunction="{StaticResource TabEase}" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</Trigger.ExitActions>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</ControlTemplate.Triggers>
|
</ControlTemplate.Triggers>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
@@ -95,23 +161,25 @@
|
|||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
|
<StackPanel x:Name="TopPanel" DockPanel.Dock="Top">
|
||||||
<!-- Tab strip -->
|
<!-- Tab strip -->
|
||||||
<Border DockPanel.Dock="Top" Background="{StaticResource HeaderBg}" Padding="12,10,12,8" BorderBrush="{StaticResource BorderDark}" BorderThickness="0,0,0,1">
|
<Border Background="{StaticResource HeaderBg}" Padding="12,10,12,8" BorderBrush="{StaticResource BorderDark}" BorderThickness="0,0,0,1">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="2,0,18,0">
|
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="2,0,18,0">
|
||||||
<Border Width="28" Height="28" CornerRadius="8" Background="{StaticResource AurBlueSoft}" Margin="0,0,9,0">
|
<Border Width="32" Height="32" CornerRadius="9" Background="{StaticResource AurBlueSoft}">
|
||||||
<TextBlock Text="🦊" FontSize="16" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
<TextBlock Text="🦊" FontSize="16" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
</Border>
|
</Border>
|
||||||
<StackPanel VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="FoxBrowser" Foreground="{StaticResource TextPrimary}" FontSize="13" FontWeight="Bold" />
|
|
||||||
<TextBlock Text="Focused browsing" Foreground="{StaticResource TextMuted}" FontSize="10" Margin="0,1,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button DockPanel.Dock="Right" Content="+" ToolTip="Нова вкладка" Click="NewTab_Click" Style="{StaticResource IconButton}" Margin="8,0,0,0" />
|
<Button DockPanel.Dock="Right" Content="+" ToolTip="Нова вкладка" Click="NewTab_Click" Style="{StaticResource IconButton}" Margin="8,0,0,0" />
|
||||||
<TabControl x:Name="TabStrip"
|
<TabControl x:Name="TabStrip"
|
||||||
ItemsSource="{Binding Tabs}"
|
ItemsSource="{Binding Tabs}"
|
||||||
ItemTemplate="{StaticResource TabTemplate}"
|
ItemTemplate="{StaticResource TabTemplate}"
|
||||||
SelectionChanged="TabStrip_SelectionChanged"
|
SelectionChanged="TabStrip_SelectionChanged"
|
||||||
|
AllowDrop="True"
|
||||||
|
PreviewMouseLeftButtonDown="TabStrip_PreviewMouseLeftButtonDown"
|
||||||
|
PreviewMouseMove="TabStrip_PreviewMouseMove"
|
||||||
|
DragOver="TabStrip_DragOver"
|
||||||
|
Drop="TabStrip_Drop"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
Padding="0"
|
Padding="0"
|
||||||
@@ -124,7 +192,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Browser controls -->
|
<!-- Browser controls -->
|
||||||
<Border DockPanel.Dock="Top" Background="{StaticResource HeaderBgElevated}" Padding="12,9" BorderBrush="{StaticResource BorderDark}" BorderThickness="0,0,0,1">
|
<Border Background="{StaticResource HeaderBgElevated}" Padding="12,9" BorderBrush="{StaticResource BorderDark}" BorderThickness="0,0,0,1">
|
||||||
<DockPanel LastChildFill="True">
|
<DockPanel LastChildFill="True">
|
||||||
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0">
|
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0">
|
||||||
<Button Content="←" ToolTip="Назад" Click="Back_Click" Style="{StaticResource IconButton}" />
|
<Button Content="←" ToolTip="Назад" Click="Back_Click" Style="{StaticResource IconButton}" />
|
||||||
@@ -142,14 +210,55 @@
|
|||||||
<Button Tag="{Binding}" Click="PinnedExtension_Click" Width="32" Height="32" Padding="0" Margin="0,0,4,0" ToolTip="{Binding Name}" Style="{StaticResource IconButton}">
|
<Button Tag="{Binding}" Click="PinnedExtension_Click" Width="32" Height="32" Padding="0" Margin="0,0,4,0" ToolTip="{Binding Name}" Style="{StaticResource IconButton}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Image Source="{Binding IconPath}" Width="18" Height="18" Stretch="Uniform" />
|
<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>
|
</Grid>
|
||||||
</Button>
|
</Button>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</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="Режим лише аудіо" 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" />
|
<Button Content="Розширення" ToolTip="Керування unpacked extensions" Click="Extensions_Click" Height="32" Padding="11,0" />
|
||||||
|
<Button Content="⌨" ToolTip="Клавіатурні скорочення" Click="Shortcuts_Click" Height="32" Width="36" Padding="0" Margin="4,0,0,0" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Grid VerticalAlignment="Center">
|
<Grid VerticalAlignment="Center">
|
||||||
@@ -162,6 +271,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Web content -->
|
<!-- Web content -->
|
||||||
<Grid x:Name="WebViewHost" Background="#0B0E12">
|
<Grid x:Name="WebViewHost" Background="#0B0E12">
|
||||||
|
|||||||
+405
-19
@@ -3,13 +3,22 @@ using System.Diagnostics;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
using Microsoft.Web.WebView2.Core;
|
using Microsoft.Web.WebView2.Core;
|
||||||
using FoxBrowser.Core;
|
using FoxBrowser.Core;
|
||||||
using FoxBrowser.Native;
|
using FoxBrowser.Native;
|
||||||
using FoxBrowser.UI;
|
using FoxBrowser.UI;
|
||||||
|
using WpfDragDropEffects = System.Windows.DragDropEffects;
|
||||||
|
using WpfDragEventArgs = System.Windows.DragEventArgs;
|
||||||
|
using WpfKeyEventArgs = System.Windows.Input.KeyEventArgs;
|
||||||
|
using WpfMouseEventArgs = System.Windows.Input.MouseEventArgs;
|
||||||
|
using WpfPoint = System.Windows.Point;
|
||||||
|
using WpfScaleTransform = System.Windows.Media.ScaleTransform;
|
||||||
|
using WpfTranslateTransform = System.Windows.Media.TranslateTransform;
|
||||||
|
|
||||||
namespace FoxBrowser;
|
namespace FoxBrowser;
|
||||||
|
|
||||||
@@ -18,21 +27,124 @@ public partial class MainWindow : Window
|
|||||||
private TabManager? _tabManager;
|
private TabManager? _tabManager;
|
||||||
private TrayIconManager? _tray;
|
private TrayIconManager? _tray;
|
||||||
private bool _allowClose;
|
private bool _allowClose;
|
||||||
|
private bool _closingAfterSessionSave;
|
||||||
private readonly DispatcherTimer _watchdogTimer = new() { Interval = TimeSpan.FromSeconds(30) };
|
private readonly DispatcherTimer _watchdogTimer = new() { Interval = TimeSpan.FromSeconds(30) };
|
||||||
private AudioSessionLabeler? _audioLabeler;
|
private AudioSessionLabeler? _audioLabeler;
|
||||||
|
private bool _updatingDarkModeMenu;
|
||||||
|
private AppSettings _appSettings;
|
||||||
|
private WpfPoint _tabDragStartPoint;
|
||||||
|
private BrowserTab? _tabDragItem;
|
||||||
|
private TabItem? _draggedTabContainer;
|
||||||
|
private TabItem? _dragPreviewTarget;
|
||||||
|
private bool _topPanelVisible = true;
|
||||||
|
|
||||||
|
private const int WmNcRButtonDown = 0x00A4;
|
||||||
|
private const int WmNcRButtonUp = 0x00A5;
|
||||||
|
private const int HtSysMenu = 3;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
_appSettings = AppSettingsStore.Load();
|
||||||
|
ApplyAppIdentity(_appSettings);
|
||||||
|
PreviewKeyDown += MainWindow_PreviewKeyDown;
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
var shortcutsItem = new System.Windows.Controls.Button
|
||||||
|
{
|
||||||
|
Content = "Клавіатурні скорочення…",
|
||||||
|
Style = (Style)FindResource("FoxMenuButton")
|
||||||
|
};
|
||||||
|
shortcutsItem.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
popup.IsOpen = false;
|
||||||
|
Shortcuts_Click(this, new RoutedEventArgs());
|
||||||
|
};
|
||||||
|
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 = new StackPanel
|
||||||
|
{
|
||||||
|
Children = { item, shortcutsItem }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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
|
try
|
||||||
{
|
{
|
||||||
var icoPath = Path.Combine(AppContext.BaseDirectory, "FoxBrowser.ico");
|
var image = new System.Windows.Media.Imaging.BitmapImage();
|
||||||
if (File.Exists(icoPath))
|
image.BeginInit();
|
||||||
Icon = new System.Windows.Media.Imaging.BitmapImage(new Uri(icoPath, UriKind.Absolute));
|
image.UriSource = new Uri(iconPath, UriKind.Absolute);
|
||||||
|
image.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
|
||||||
|
image.EndInit();
|
||||||
|
image.Freeze();
|
||||||
|
Icon = image;
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
SourceInitialized += (_, _) => ApplyDarkTitleBar(true);
|
_tray?.UpdateIdentity(Title, iconPath);
|
||||||
Loaded += MainWindow_Loaded;
|
_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)
|
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
||||||
@@ -40,6 +152,7 @@ public partial class MainWindow : Window
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_tray = new TrayIconManager(this, ExitApplication);
|
_tray = new TrayIconManager(this, ExitApplication);
|
||||||
|
_tray.UpdateIdentity(Title, ResolveAppIconPath(_appSettings));
|
||||||
var environmentFactory = new WebView2EnvironmentFactory();
|
var environmentFactory = new WebView2EnvironmentFactory();
|
||||||
var recoveryMode = false;
|
var recoveryMode = false;
|
||||||
CoreWebView2Environment environment;
|
CoreWebView2Environment environment;
|
||||||
@@ -53,7 +166,7 @@ public partial class MainWindow : Window
|
|||||||
// Use a disposable recovery profile so the browser can still start.
|
// Use a disposable recovery profile so the browser can still start.
|
||||||
recoveryMode = true;
|
recoveryMode = true;
|
||||||
var recoveryData = Path.Combine(
|
var recoveryData = Path.Combine(
|
||||||
Path.GetTempPath(), $"FoxBrowser_Recovery_{Environment.ProcessId}_{Guid.NewGuid():N}");
|
AppContext.BaseDirectory, $"Profile_Recovery_{Environment.ProcessId}_{Guid.NewGuid():N}");
|
||||||
Directory.CreateDirectory(recoveryData);
|
Directory.CreateDirectory(recoveryData);
|
||||||
environment = await environmentFactory.CreateAsync(recoveryData);
|
environment = await environmentFactory.CreateAsync(recoveryData);
|
||||||
}
|
}
|
||||||
@@ -61,13 +174,19 @@ public partial class MainWindow : Window
|
|||||||
if (startupNotice is not null)
|
if (startupNotice is not null)
|
||||||
System.Windows.MessageBox.Show(this, startupNotice, "FoxBrowser", MessageBoxButton.OK, MessageBoxImage.Information);
|
System.Windows.MessageBox.Show(this, startupNotice, "FoxBrowser", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
_tabManager = new TabManager(WebViewHost, environment, !recoveryMode);
|
_tabManager = new TabManager(WebViewHost, environment, !recoveryMode);
|
||||||
_tabManager.ActiveTabStateChanged += (_, _) => Dispatcher.Invoke(RefreshAddressBar);
|
_tabManager.ActiveTabStateChanged += (_, _) => Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
RefreshAddressBar();
|
||||||
|
RefreshModeIndicators();
|
||||||
|
});
|
||||||
DataContext = _tabManager;
|
DataContext = _tabManager;
|
||||||
await _tabManager.InitializeAsync();
|
await _tabManager.InitializeAsync();
|
||||||
var iconPath = Path.Combine(AppContext.BaseDirectory, "FoxBrowser.ico");
|
TabStrip.SelectedItem = _tabManager.ActiveTab;
|
||||||
|
RefreshModeIndicators();
|
||||||
|
var iconPath = ResolveAppIconPath(_appSettings);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_audioLabeler = new AudioSessionLabeler((uint)Process.GetCurrentProcess().Id, iconPath);
|
_audioLabeler = new AudioSessionLabeler((uint)Process.GetCurrentProcess().Id, iconPath, Title);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -109,28 +228,247 @@ public partial class MainWindow : Window
|
|||||||
TabStrip.Items.Refresh();
|
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();
|
||||||
|
RefreshModeIndicators();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
RefreshModeIndicators();
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
private async Task NavigateFromAddressBarAsync()
|
||||||
{
|
{
|
||||||
if (_tabManager?.ActiveTab is null) return;
|
if (_tabManager?.ActiveTab is null) return;
|
||||||
await _tabManager.NavigateAsync(_tabManager.ActiveTab, AddressBox.Text);
|
await _tabManager.NavigateAsync(_tabManager.ActiveTab, AddressBox.Text);
|
||||||
AddressBox.Text = _tabManager.ActiveTab.Address;
|
AddressBox.Text = _tabManager.ActiveTab.Address;
|
||||||
|
AnimateWebViewTransition();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Back_Click(object sender, RoutedEventArgs e) => _tabManager?.GoBack();
|
private void Back_Click(object sender, RoutedEventArgs e) { AnimateWebViewTransition(); _tabManager?.GoBack(); }
|
||||||
private void Forward_Click(object sender, RoutedEventArgs e) => _tabManager?.GoForward();
|
private void Forward_Click(object sender, RoutedEventArgs e) { AnimateWebViewTransition(); _tabManager?.GoForward(); }
|
||||||
private void Reload_Click(object sender, RoutedEventArgs e) => _tabManager?.Reload();
|
private void Reload_Click(object sender, RoutedEventArgs e) { AnimateWebViewTransition(); _tabManager?.Reload(); }
|
||||||
private void Extensions_Click(object sender, RoutedEventArgs e)
|
private void Extensions_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_tabManager?.ExtensionManager is null) return;
|
if (_tabManager?.ExtensionManager is null) return;
|
||||||
var window = new ExtensionsSettingsWindow(
|
var window = new ExtensionsSettingsWindow(
|
||||||
_tabManager.ExtensionManager,
|
_tabManager.ExtensionManager,
|
||||||
_tabManager.Environment,
|
_tabManager.Environment,
|
||||||
() => _tabManager.OpenUrlAsync("https://chromewebstore.google.com/category/extensions"),
|
address => _tabManager!.OpenUrlAsync(address),
|
||||||
_tabManager.ExtensionsAvailable)
|
_tabManager.ExtensionsAvailable)
|
||||||
{ Owner = this };
|
{ Owner = this };
|
||||||
window.ShowDialog();
|
window.ShowDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Shortcuts_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var window = new ShortcutSettingsWindow { Owner = this };
|
||||||
|
window.ShowDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void MainWindow_PreviewKeyDown(object sender, WpfKeyEventArgs e)
|
||||||
|
{
|
||||||
|
var key = e.Key == Key.System ? e.SystemKey : e.Key;
|
||||||
|
var action = ShortcutManager.Instance.FindAction(key, Keyboard.Modifiers);
|
||||||
|
if (action is null) return;
|
||||||
|
e.Handled = true;
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case ShortcutManager.CloseTab when _tabManager?.ActiveTab is not null:
|
||||||
|
await _tabManager.CloseTabAsync(_tabManager.ActiveTab);
|
||||||
|
RefreshAddressBar();
|
||||||
|
break;
|
||||||
|
case ShortcutManager.NewTab when _tabManager is not null:
|
||||||
|
await _tabManager.AddTabAsync();
|
||||||
|
RefreshAddressBar();
|
||||||
|
break;
|
||||||
|
case ShortcutManager.ToggleTopPanel:
|
||||||
|
_topPanelVisible = !_topPanelVisible;
|
||||||
|
TopPanel.Visibility = _topPanelVisible ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TabStrip_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
_tabDragStartPoint = e.GetPosition(TabStrip);
|
||||||
|
_tabDragItem = FindTabItem(e.OriginalSource as DependencyObject)?.DataContext as BrowserTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TabStrip_PreviewMouseMove(object sender, WpfMouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.LeftButton != MouseButtonState.Pressed || _tabDragItem is null) return;
|
||||||
|
var position = e.GetPosition(TabStrip);
|
||||||
|
if (Math.Abs(position.X - _tabDragStartPoint.X) < SystemParameters.MinimumHorizontalDragDistance &&
|
||||||
|
Math.Abs(position.Y - _tabDragStartPoint.Y) < SystemParameters.MinimumVerticalDragDistance) return;
|
||||||
|
var item = _tabDragItem;
|
||||||
|
_tabDragItem = null;
|
||||||
|
_draggedTabContainer = FindTabItem(e.OriginalSource as DependencyObject);
|
||||||
|
if (_draggedTabContainer is not null)
|
||||||
|
{
|
||||||
|
_draggedTabContainer.Opacity = 0.7;
|
||||||
|
_draggedTabContainer.RenderTransformOrigin = new System.Windows.Point(0.5, 0.5);
|
||||||
|
_draggedTabContainer.RenderTransform = new System.Windows.Media.ScaleTransform(0.97, 0.97);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Windows.DragDrop.DoDragDrop(TabStrip, item, WpfDragDropEffects.Move);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
RestoreDraggedTabVisual();
|
||||||
|
ResetDragPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TabStrip_DragOver(object sender, WpfDragEventArgs e)
|
||||||
|
{
|
||||||
|
e.Effects = e.Data.GetDataPresent(typeof(BrowserTab)) ? WpfDragDropEffects.Move : WpfDragDropEffects.None;
|
||||||
|
if (e.Data.GetDataPresent(typeof(BrowserTab)))
|
||||||
|
{
|
||||||
|
var target = FindTabItem(e.OriginalSource as DependencyObject);
|
||||||
|
if (target is not null && !ReferenceEquals(target, _draggedTabContainer) && !ReferenceEquals(target, _dragPreviewTarget))
|
||||||
|
{
|
||||||
|
ResetDragPreview();
|
||||||
|
_dragPreviewTarget = target;
|
||||||
|
var source = e.Data.GetData(typeof(BrowserTab)) as BrowserTab;
|
||||||
|
var targetTab = target.DataContext as BrowserTab;
|
||||||
|
var sourceIndex = source is null ? -1 : _tabManager?.Tabs.IndexOf(source) ?? -1;
|
||||||
|
var targetIndex = targetTab is null ? -1 : _tabManager?.Tabs.IndexOf(targetTab) ?? -1;
|
||||||
|
var offset = sourceIndex >= 0 && targetIndex >= 0 && sourceIndex < targetIndex ? -6 : 6;
|
||||||
|
var translation = new System.Windows.Media.TranslateTransform();
|
||||||
|
target.RenderTransform = translation;
|
||||||
|
translation.BeginAnimation(System.Windows.Media.TranslateTransform.XProperty,
|
||||||
|
new DoubleAnimation(offset, new Duration(TimeSpan.FromMilliseconds(120))) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void TabStrip_Drop(object sender, WpfDragEventArgs e)
|
||||||
|
{
|
||||||
|
if (_tabManager is null || !e.Data.GetDataPresent(typeof(BrowserTab))) return;
|
||||||
|
var source = e.Data.GetData(typeof(BrowserTab)) as BrowserTab;
|
||||||
|
var target = FindTabItem(e.OriginalSource as DependencyObject)?.DataContext as BrowserTab;
|
||||||
|
if (source is null || target is null || ReferenceEquals(source, target)) return;
|
||||||
|
var targetIndex = _tabManager.Tabs.IndexOf(target);
|
||||||
|
if (targetIndex >= 0) await _tabManager.MoveTabAsync(source, targetIndex);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestoreDraggedTabVisual()
|
||||||
|
{
|
||||||
|
if (_draggedTabContainer is null) return;
|
||||||
|
_draggedTabContainer.BeginAnimation(UIElement.OpacityProperty,
|
||||||
|
new DoubleAnimation(0.7, 1, new Duration(TimeSpan.FromMilliseconds(140))) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } });
|
||||||
|
if (_draggedTabContainer.RenderTransform is WpfScaleTransform scale)
|
||||||
|
{
|
||||||
|
scale.BeginAnimation(WpfScaleTransform.ScaleXProperty,
|
||||||
|
new DoubleAnimation(0.97, 1, new Duration(TimeSpan.FromMilliseconds(140))) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } });
|
||||||
|
scale.BeginAnimation(WpfScaleTransform.ScaleYProperty,
|
||||||
|
new DoubleAnimation(0.97, 1, new Duration(TimeSpan.FromMilliseconds(140))) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } });
|
||||||
|
}
|
||||||
|
_draggedTabContainer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetDragPreview()
|
||||||
|
{
|
||||||
|
if (_dragPreviewTarget?.RenderTransform is WpfTranslateTransform translation)
|
||||||
|
{
|
||||||
|
translation.BeginAnimation(WpfTranslateTransform.XProperty,
|
||||||
|
new DoubleAnimation(translation.X, 0, new Duration(TimeSpan.FromMilliseconds(120))) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } });
|
||||||
|
}
|
||||||
|
_dragPreviewTarget = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AnimateWebViewTransition()
|
||||||
|
{
|
||||||
|
WebViewHost.BeginAnimation(UIElement.OpacityProperty, null);
|
||||||
|
WebViewHost.Opacity = 0.85;
|
||||||
|
WebViewHost.BeginAnimation(UIElement.OpacityProperty,
|
||||||
|
new DoubleAnimation(0.85, 1, new Duration(TimeSpan.FromMilliseconds(130))) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TabItem? FindTabItem(DependencyObject? element)
|
||||||
|
{
|
||||||
|
while (element is not null)
|
||||||
|
{
|
||||||
|
if (element is TabItem tabItem) return tabItem;
|
||||||
|
element = System.Windows.Media.VisualTreeHelper.GetParent(element);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void PinnedExtension_Click(object sender, RoutedEventArgs e)
|
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)
|
if (_tabManager?.ExtensionManager is null || sender is not System.Windows.Controls.Button button || button.DataContext is not PinnedExtensionItem item)
|
||||||
@@ -138,14 +476,28 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(item.PopupPage))
|
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);
|
Extensions_Click(sender, e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var screenPositionWpf = button.PointToScreen(new System.Windows.Point(0, button.ActualHeight));
|
var screenPositionDevice = button.PointToScreen(new System.Windows.Point(0, button.ActualHeight));
|
||||||
var screenPosition = new System.Drawing.Point((int)screenPositionWpf.X, (int)screenPositionWpf.Y);
|
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
|
Owner = this
|
||||||
};
|
};
|
||||||
@@ -154,15 +506,18 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private async void TabStrip_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
private async void TabStrip_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Source != TabStrip || TabStrip.SelectedItem is not BrowserTab tab || _tabManager is null) return;
|
if (e.Source != TabStrip || TabStrip.SelectedItem is not BrowserTab tab || _tabManager is null || !_tabManager.SessionReady) return;
|
||||||
AddressBox.Text = tab.Address;
|
AddressBox.Text = tab.Address;
|
||||||
await _tabManager.SwitchToTabAsync(tab);
|
await _tabManager.SwitchToTabAsync(tab);
|
||||||
|
AnimateWebViewTransition();
|
||||||
RefreshAddressBar();
|
RefreshAddressBar();
|
||||||
|
RefreshModeIndicators();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CloseTab_Click(object sender, RoutedEventArgs e)
|
private async void CloseTab_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if ((sender as System.Windows.Controls.Button)?.Tag is BrowserTab tab) _tabManager?.CloseTab(tab);
|
if ((sender as System.Windows.Controls.Button)?.Tag is BrowserTab tab && _tabManager is not null)
|
||||||
|
await _tabManager.CloseTabAsync(tab);
|
||||||
RefreshAddressBar();
|
RefreshAddressBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +539,14 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (!_closingAfterSessionSave)
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
_closingAfterSessionSave = true;
|
||||||
|
if (_tabManager is not null) await _tabManager.SaveSessionAsync();
|
||||||
|
Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
_watchdogTimer.Stop();
|
_watchdogTimer.Stop();
|
||||||
_tray?.Dispose();
|
_tray?.Dispose();
|
||||||
_audioLabeler?.Dispose();
|
_audioLabeler?.Dispose();
|
||||||
@@ -211,4 +574,27 @@ public partial class MainWindow : Window
|
|||||||
AddressBox.Text = _tabManager.ActiveTab.Address;
|
AddressBox.Text = _tabManager.ActiveTab.Address;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RefreshModeIndicators()
|
||||||
|
{
|
||||||
|
if (!IsInitialized) return;
|
||||||
|
var darkActive = _tabManager?.ActiveTab?.IsDarkMode == true;
|
||||||
|
var pickerActive = _tabManager?.IsElementPickerActive == true;
|
||||||
|
var activeBackground = (System.Windows.Media.Brush)FindResource("AurBlueSoft");
|
||||||
|
var activeBorder = (System.Windows.Media.Brush)FindResource("AurBlue");
|
||||||
|
var inactiveBackground = (System.Windows.Media.Brush)FindResource("CardBg");
|
||||||
|
var inactiveBorder = (System.Windows.Media.Brush)FindResource("BorderMedium");
|
||||||
|
|
||||||
|
DarkModeButton.Background = darkActive ? activeBackground : inactiveBackground;
|
||||||
|
DarkModeButton.BorderBrush = darkActive ? activeBorder : inactiveBorder;
|
||||||
|
DarkModeButton.Foreground = darkActive
|
||||||
|
? (System.Windows.Media.Brush)FindResource("TextPrimary")
|
||||||
|
: (System.Windows.Media.Brush)FindResource("TextSecondary");
|
||||||
|
|
||||||
|
ElementPickerButton.Background = pickerActive ? activeBackground : inactiveBackground;
|
||||||
|
ElementPickerButton.BorderBrush = pickerActive ? activeBorder : inactiveBorder;
|
||||||
|
ElementPickerButton.Foreground = pickerActive
|
||||||
|
? (System.Windows.Media.Brush)FindResource("TextPrimary")
|
||||||
|
: (System.Windows.Media.Brush)FindResource("TextSecondary");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,23 @@ internal static class Win32Interop
|
|||||||
[DllImport("dwmapi.dll", PreserveSig = true)]
|
[DllImport("dwmapi.dll", PreserveSig = true)]
|
||||||
private static extern int DwmSetWindowAttribute(nint hwnd, int attribute, ref int value, int size);
|
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)
|
public static void ApplyDarkTitleBar(nint hwnd, bool enabled)
|
||||||
{
|
{
|
||||||
if (hwnd == 0) return;
|
if (hwnd == 0) return;
|
||||||
@@ -17,4 +34,14 @@ internal static class Win32Interop
|
|||||||
var result = DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkMode, ref value, sizeof(int));
|
var result = DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkMode, ref value, sizeof(int));
|
||||||
if (result != 0) DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkModeLegacy, 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-ів.
|
FoxBrowser — Windows-first браузерний shell на **C# / .NET 8 WPF / WebView2**, оптимізований під фонову роботу з мінімальною кількістю живих renderer-ів.
|
||||||
|
|
||||||
|
Правила прихованих елементів зберігаються у `blocked-elements.json` поруч із `FoxBrowser.exe`, щоб portable-версія не створювала для них окремі файли в профілі користувача.
|
||||||
|
|
||||||
## Поточна архітектура пам'яті
|
## Поточна архітектура пам'яті
|
||||||
|
|
||||||
FoxBrowser використовує **один WebView2 на весь застосунок**. Логічні вкладки є лише metadata-об'єктами: URL, назва, scroll position, позиція медіа і прапор Audio Mode.
|
FoxBrowser використовує **один WebView2 на весь застосунок**. Логічні вкладки є лише metadata-об'єктами: URL, назва, scroll position, позиція медіа і прапор Audio Mode.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public sealed class BrowserTab : INotifyPropertyChanged
|
|||||||
private string _title;
|
private string _title;
|
||||||
private string _address;
|
private string _address;
|
||||||
private bool _isAudioOnly;
|
private bool _isAudioOnly;
|
||||||
|
private bool _isDarkMode;
|
||||||
private bool _isPlayingAudio;
|
private bool _isPlayingAudio;
|
||||||
private bool _isLoading;
|
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 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 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 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 bool IsPlayingAudio { get => _isPlayingAudio; set { if (_isPlayingAudio == value) return; _isPlayingAudio = value; OnPropertyChanged(); OnPropertyChanged(nameof(DisplayTitle)); } }
|
||||||
public double ScrollY { get; set; }
|
public double ScrollY { get; set; }
|
||||||
public double MediaPosition { get; set; }
|
public double MediaPosition { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<Window x:Class="FoxBrowser.UI.AppSettingsWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Назва та іконка FoxBrowser" Width="540" Height="270"
|
||||||
|
MinWidth="480" Background="#15171A" Foreground="#F2F4F7"
|
||||||
|
WindowStartupLocation="CenterOwner" ResizeMode="NoResize">
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Text="Назва та іконка застосунку" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,18" />
|
||||||
|
<Grid Grid.Row="1" Margin="0,0,0,12">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="140" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="Назва:" VerticalAlignment="Center" />
|
||||||
|
<TextBox x:Name="NameBox" Grid.Column="1" />
|
||||||
|
</Grid>
|
||||||
|
<Grid Grid.Row="2" Margin="0,0,0,8">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="140" /><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="Іконка:" VerticalAlignment="Center" />
|
||||||
|
<TextBlock x:Name="IconPathText" Grid.Column="1" VerticalAlignment="Center" Foreground="#AEB4BE" TextTrimming="CharacterEllipsis" />
|
||||||
|
<Button Grid.Column="2" Content="Обрати іконку…" Click="ChooseIcon_Click" Padding="10,5" Margin="10,0,0,0" />
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Grid.Row="3" Text="PNG і JPG автоматично конвертуються у custom_icon.ico поруч із FoxBrowser.exe." Foreground="#AEB4BE" TextWrapping="Wrap" Margin="0,4,0,0" />
|
||||||
|
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,18,0,0">
|
||||||
|
<Button Content="Скинути до стандартного" Click="Reset_Click" Padding="10,6" Margin="0,0,8,0" />
|
||||||
|
<Button Content="Скасувати" Click="Cancel_Click" Padding="10,6" Margin="0,0,8,0" />
|
||||||
|
<Button Content="Зберегти" Click="Save_Click" Padding="14,6" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows;
|
||||||
|
using FoxBrowser.Core;
|
||||||
|
|
||||||
|
namespace FoxBrowser.UI;
|
||||||
|
|
||||||
|
public partial class AppSettingsWindow : Window
|
||||||
|
{
|
||||||
|
private readonly Action<AppSettings> _apply;
|
||||||
|
private string? _iconPath;
|
||||||
|
|
||||||
|
public AppSettingsWindow(AppSettings current, Action<AppSettings> apply)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_apply = apply;
|
||||||
|
NameBox.Text = current.AppDisplayName;
|
||||||
|
_iconPath = current.AppIconPath;
|
||||||
|
UpdateIconPathText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ChooseIcon_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dialog = new Microsoft.Win32.OpenFileDialog
|
||||||
|
{
|
||||||
|
Title = "Оберіть іконку застосунку",
|
||||||
|
Filter = "Зображення та іконки (*.ico;*.png;*.jpg;*.jpeg)|*.ico;*.png;*.jpg;*.jpeg|Усі файли (*.*)|*.*",
|
||||||
|
CheckFileExists = true
|
||||||
|
};
|
||||||
|
if (dialog.ShowDialog(this) != true) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_iconPath = AppIconConverter.CopyAsPortableIcon(dialog.FileName);
|
||||||
|
UpdateIconPathText();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Windows.MessageBox.Show(this, $"Не вдалося підготувати іконку: {ex.Message}", "Іконка", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var name = NameBox.Text.Trim();
|
||||||
|
if (name.Length == 0) name = "FoxBrowser";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = new AppSettings
|
||||||
|
{
|
||||||
|
AppDisplayName = name,
|
||||||
|
AppIconPath = _iconPath,
|
||||||
|
Shortcuts = ShortcutManager.Instance.Shortcuts.ToDictionary(pair => pair.Key, pair => ShortcutManager.Format(pair.Value))
|
||||||
|
};
|
||||||
|
AppSettingsStore.Save(settings);
|
||||||
|
_apply(settings);
|
||||||
|
DialogResult = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Windows.MessageBox.Show(this, $"Не вдалося зберегти налаштування: {ex.Message}", "Налаштування", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reset_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
NameBox.Text = "FoxBrowser";
|
||||||
|
_iconPath = null;
|
||||||
|
try { File.Delete(Path.Combine(AppContext.BaseDirectory, "custom_icon.ico")); } catch { }
|
||||||
|
UpdateIconPathText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel_Click(object sender, RoutedEventArgs e) => Close();
|
||||||
|
|
||||||
|
private void UpdateIconPathText() => IconPathText.Text = string.IsNullOrWhiteSpace(_iconPath) ? "Вбудована іконка" : Path.GetFileName(_iconPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class AppIconConverter
|
||||||
|
{
|
||||||
|
private const string PortableIconName = "custom_icon.ico";
|
||||||
|
|
||||||
|
public static string CopyAsPortableIcon(string sourcePath)
|
||||||
|
{
|
||||||
|
var destination = Path.Combine(AppContext.BaseDirectory, PortableIconName);
|
||||||
|
var extension = Path.GetExtension(sourcePath);
|
||||||
|
if (extension.Equals(".ico", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (string.Equals(Path.GetFullPath(sourcePath), Path.GetFullPath(destination), StringComparison.OrdinalIgnoreCase))
|
||||||
|
return destination;
|
||||||
|
File.Copy(sourcePath, destination, overwrite: true);
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var bitmap = new Bitmap(sourcePath);
|
||||||
|
var handle = bitmap.GetHicon();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var icon = Icon.FromHandle(handle);
|
||||||
|
using var stream = File.Create(destination);
|
||||||
|
icon.Save(stream);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DestroyIcon(handle);
|
||||||
|
}
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
private static extern bool DestroyIcon(nint handle);
|
||||||
|
}
|
||||||
@@ -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"
|
<Window x:Class="FoxBrowser.UI.ExtensionPopupWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
||||||
WindowStyle="None" ResizeMode="NoResize" ShowInTaskbar="False"
|
WindowStyle="None" ResizeMode="NoResize" ShowInTaskbar="False"
|
||||||
Width="360" Height="520" MaxHeight="600"
|
Width="360" Height="520" MaxHeight="600"
|
||||||
Background="#202328" AllowsTransparency="False">
|
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">
|
<Border BorderBrush="#59616D" BorderThickness="1" Background="#202328">
|
||||||
<Grid x:Name="PopupHost" />
|
<Grid x:Name="PopupHost" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Interop;
|
||||||
using Microsoft.Web.WebView2.Core;
|
using Microsoft.Web.WebView2.Core;
|
||||||
using Microsoft.Web.WebView2.Wpf;
|
using Microsoft.Web.WebView2.Wpf;
|
||||||
|
using FoxBrowser.Native;
|
||||||
|
|
||||||
namespace FoxBrowser.UI;
|
namespace FoxBrowser.UI;
|
||||||
|
|
||||||
@@ -9,15 +11,35 @@ public partial class ExtensionPopupWindow : Window
|
|||||||
private readonly CoreWebView2Environment _environment;
|
private readonly CoreWebView2Environment _environment;
|
||||||
private readonly string _extensionId;
|
private readonly string _extensionId;
|
||||||
private readonly string _popupPage;
|
private readonly string _popupPage;
|
||||||
|
private WebView2? _view;
|
||||||
|
|
||||||
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();
|
InitializeComponent();
|
||||||
_environment = environment;
|
_environment = environment;
|
||||||
_extensionId = extensionId;
|
_extensionId = extensionId;
|
||||||
_popupPage = popupPage;
|
_popupPage = popupPage;
|
||||||
Left = screenPosition.X;
|
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||||
Top = screenPosition.Y;
|
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();
|
Deactivated += (_, _) => Close();
|
||||||
Loaded += async (_, _) => await InitializeAsync();
|
Loaded += async (_, _) => await InitializeAsync();
|
||||||
}
|
}
|
||||||
@@ -26,14 +48,21 @@ public partial class ExtensionPopupWindow : Window
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var view = new WebView2();
|
_view = new WebView2();
|
||||||
PopupHost.Children.Add(view);
|
PopupHost.Children.Add(_view);
|
||||||
await view.EnsureCoreWebView2Async(_environment);
|
await _view.EnsureCoreWebView2Async(_environment);
|
||||||
view.CoreWebView2.Navigate($"chrome-extension://{_extensionId}/{_popupPage}");
|
_view.CoreWebView2.Navigate($"chrome-extension://{_extensionId}/{_popupPage}");
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnClosed(EventArgs e)
|
||||||
|
{
|
||||||
|
_view?.Dispose();
|
||||||
|
_view = null;
|
||||||
|
base.OnClosed(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,13 +5,17 @@
|
|||||||
Background="#15171A" Foreground="#F2F4F7">
|
Background="#15171A" Foreground="#F2F4F7">
|
||||||
<DockPanel Margin="14">
|
<DockPanel Margin="14">
|
||||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
||||||
<Button Content="Магазин розширень" ToolTip="Відкрити Chrome Web Store; встановлення з магазину не підтримується API WebView2" Click="Store_Click" Padding="12,6" />
|
<Button Content="Магазин у FoxBrowser" ToolTip="Відкрити Chrome Web Store у поточній вкладці" Click="Store_Click" Padding="12,6" />
|
||||||
<Button Content="Імпортувати розширення…" Click="Add_Click" Padding="12,6" Margin="8,0,0,0" />
|
<Button Content="CRX Downloader" ToolTip="Відкрити завантажувач CRX у FoxBrowser" Click="Downloader_Click" Padding="12,6" Margin="8,0,0,0" />
|
||||||
|
<Button Content="Папка розширень" ToolTip="Відкрити папку, куди можна розпакувати extension" Click="OpenFolder_Click" Padding="12,6" Margin="8,0,0,0" />
|
||||||
|
<Button Content="Імпортувати ZIP/CRX…" Click="AddArchive_Click" Padding="12,6" Margin="8,0,0,0" />
|
||||||
|
<Button Content="Імпортувати папку…" Click="Add_Click" Padding="12,6" Margin="8,0,0,0" />
|
||||||
<Button Content="Закрити" Click="Close_Click" Padding="12,6" Margin="8,0,0,0" />
|
<Button Content="Закрити" Click="Close_Click" Padding="12,6" Margin="8,0,0,0" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock DockPanel.Dock="Top" Text="Chrome-сумісні unpacked extensions" FontSize="16" FontWeight="SemiBold" Margin="0,0,0,4" />
|
<TextBlock DockPanel.Dock="Top" Text="Chrome-сумісні unpacked extensions" FontSize="16" FontWeight="SemiBold" Margin="0,0,0,4" />
|
||||||
<TextBlock DockPanel.Dock="Top" Text="Кнопка «Налаштування» відкриває сторінку, вказану в manifest.json: options_ui.page або options_page." Foreground="#AEB4BE" TextWrapping="Wrap" Margin="0,0,0,4" />
|
<TextBlock DockPanel.Dock="Top" Text="Кнопка «Налаштування» відкриває сторінку, вказану в manifest.json: options_ui.page або options_page." Foreground="#AEB4BE" TextWrapping="Wrap" Margin="0,0,0,4" />
|
||||||
<TextBlock DockPanel.Dock="Top" Text="Профіль WebView2: папка Profile поруч із FoxBrowser.exe (у захищеній папці Windows використовується стандартний профіль %LOCALAPPDATA%\FoxBrowser\WebView2). WebView2 зберігає реєстрацію розширення, а файли unpacked залишаються у вибраній тобою папці." Foreground="#AEB4BE" TextWrapping="Wrap" Margin="0,0,0,10" />
|
<TextBlock DockPanel.Dock="Top" Text="WebView2 не підтримує Chrome-only встановлення кнопкою Install. Завантаж ZIP/CRX, імпортуй його тут — FoxBrowser сам розпакує файл у папку профілю та підключить extension." Foreground="#FFCC66" TextWrapping="Wrap" Margin="0,0,0,4" />
|
||||||
|
<TextBlock DockPanel.Dock="Top" Text="Імпортовані файли, профіль WebView2 і налаштування extension зберігаються тільки поруч із FoxBrowser.exe у portable-папках." Foreground="#AEB4BE" TextWrapping="Wrap" Margin="0,0,0,10" />
|
||||||
<TextBlock x:Name="ExtensionsStatus" DockPanel.Dock="Top" Foreground="#FFCC66" TextWrapping="Wrap" Margin="0,0,0,8" />
|
<TextBlock x:Name="ExtensionsStatus" DockPanel.Dock="Top" Foreground="#FFCC66" TextWrapping="Wrap" Margin="0,0,0,8" />
|
||||||
<Grid>
|
<Grid>
|
||||||
<ListBox x:Name="ExtensionList" Background="#202328" BorderBrush="#454C57" Foreground="#F2F4F7">
|
<ListBox x:Name="ExtensionList" Background="#202328" BorderBrush="#454C57" Foreground="#F2F4F7">
|
||||||
|
|||||||
@@ -15,15 +15,15 @@ public partial class ExtensionsSettingsWindow : Window
|
|||||||
{
|
{
|
||||||
private readonly ExtensionManager _manager;
|
private readonly ExtensionManager _manager;
|
||||||
private readonly CoreWebView2Environment _environment;
|
private readonly CoreWebView2Environment _environment;
|
||||||
private readonly Func<Task> _openStore;
|
private readonly Func<string, Task> _openUrl;
|
||||||
private WebView2? _optionsView;
|
private WebView2? _optionsView;
|
||||||
|
|
||||||
public ExtensionsSettingsWindow(ExtensionManager manager, CoreWebView2Environment environment, Func<Task> openStore, bool extensionsAvailable)
|
public ExtensionsSettingsWindow(ExtensionManager manager, CoreWebView2Environment environment, Func<string, Task> openUrl, bool extensionsAvailable)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_manager = manager;
|
_manager = manager;
|
||||||
_environment = environment;
|
_environment = environment;
|
||||||
_openStore = openStore;
|
_openUrl = openUrl;
|
||||||
ExtensionList.ItemsSource = _manager.Extensions;
|
ExtensionList.ItemsSource = _manager.Extensions;
|
||||||
if (!extensionsAvailable)
|
if (!extensionsAvailable)
|
||||||
{
|
{
|
||||||
@@ -33,7 +33,7 @@ public partial class ExtensionsSettingsWindow : Window
|
|||||||
Loaded += async (_, _) =>
|
Loaded += async (_, _) =>
|
||||||
{
|
{
|
||||||
await _manager.RefreshAsync();
|
await _manager.RefreshAsync();
|
||||||
Dispatcher.BeginInvoke(RefreshPinCheckboxes);
|
Dispatcher.Invoke(RefreshPinCheckboxes);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,11 +45,40 @@ public partial class ExtensionsSettingsWindow : Window
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _manager.AddExtensionAsync(dialog.SelectedPath);
|
await _manager.AddExtensionAsync(dialog.SelectedPath);
|
||||||
Dispatcher.BeginInvoke(RefreshPinCheckboxes);
|
Dispatcher.Invoke(RefreshPinCheckboxes);
|
||||||
}
|
}
|
||||||
catch (Exception ex) { ShowError(ex.Message); }
|
catch (Exception ex) { ShowError(ex.Message); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void AddArchive_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
using var dialog = new Forms.OpenFileDialog
|
||||||
|
{
|
||||||
|
Title = "Виберіть архів extension",
|
||||||
|
Filter = "Extension archive (*.zip;*.crx)|*.zip;*.crx|Усі файли (*.*)|*.*",
|
||||||
|
CheckFileExists = true
|
||||||
|
};
|
||||||
|
var owner = new WindowInteropHelper(this).Handle;
|
||||||
|
if (dialog.ShowDialog(new NativeWindowWrapper(owner)) != Forms.DialogResult.OK) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _manager.AddArchiveAsync(dialog.FileName);
|
||||||
|
ExtensionsStatus.Text = "Розширення розпаковано у профіль FoxBrowser та підключено.";
|
||||||
|
Dispatcher.Invoke(RefreshPinCheckboxes);
|
||||||
|
}
|
||||||
|
catch (Exception ex) { ShowError($"Не вдалося імпортувати архів: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenFolder_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ExtensionManager.OpenExtensionsFolder();
|
||||||
|
ExtensionsStatus.Text = $"Папка розширень: {ExtensionManager.ExtensionsRootPath}";
|
||||||
|
}
|
||||||
|
catch (Exception ex) { ShowError($"Не вдалося відкрити папку розширень: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
private async void Toggle_Click(object sender, RoutedEventArgs e)
|
private async void Toggle_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if ((sender as WpfCheckBox)?.Tag is not CoreWebView2BrowserExtension extension) return;
|
if ((sender as WpfCheckBox)?.Tag is not CoreWebView2BrowserExtension extension) return;
|
||||||
@@ -102,9 +131,29 @@ public partial class ExtensionsSettingsWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async void Store_Click(object sender, RoutedEventArgs e)
|
private async void Store_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
Close();
|
Close();
|
||||||
await _openStore();
|
await _openUrl("https://chromewebstore.google.com/category/extensions");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ShowError($"Не вдалося відкрити Chrome Web Store: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Downloader_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
await _openUrl("https://www.crx4chrome.com/crx-downloader/");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ShowError($"Не вдалося відкрити CRX Downloader: {ex.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Remove_Click(object sender, RoutedEventArgs e)
|
private async void Remove_Click(object sender, RoutedEventArgs e)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<Window x:Class="FoxBrowser.UI.ShortcutSettingsWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Клавіатурні скорочення" Width="580" Height="330"
|
||||||
|
MinWidth="520" Background="#15171A" Foreground="#F2F4F7"
|
||||||
|
WindowStartupLocation="CenterOwner" ResizeMode="NoResize">
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Text="Клавіатурні скорочення" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,8" />
|
||||||
|
<TextBlock Grid.Row="0" Text="Натисніть «Змінити», потім потрібну комбінацію Ctrl/Shift/Alt + клавіша." Foreground="#AEB4BE" Margin="0,30,0,12" />
|
||||||
|
<StackPanel x:Name="ShortcutList" Grid.Row="1" />
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,14,0,0">
|
||||||
|
<Button Content="Скинути до стандартних" Click="Reset_Click" Padding="10,6" Margin="0,0,8,0" />
|
||||||
|
<Button Content="Закрити" Click="Close_Click" Padding="14,6" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using FoxBrowser.Core;
|
||||||
|
using WpfButton = System.Windows.Controls.Button;
|
||||||
|
using WpfColor = System.Windows.Media.Color;
|
||||||
|
using WpfKeyEventArgs = System.Windows.Input.KeyEventArgs;
|
||||||
|
using WpfTextBox = System.Windows.Controls.TextBox;
|
||||||
|
|
||||||
|
namespace FoxBrowser.UI;
|
||||||
|
|
||||||
|
public partial class ShortcutSettingsWindow : Window
|
||||||
|
{
|
||||||
|
private readonly ShortcutManager _shortcuts = ShortcutManager.Instance;
|
||||||
|
private readonly Dictionary<string, WpfTextBox> _boxes = new();
|
||||||
|
private string? _capturingAction;
|
||||||
|
private KeyGesture? _pendingGesture;
|
||||||
|
|
||||||
|
public ShortcutSettingsWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_shortcuts.Changed += Shortcuts_Changed;
|
||||||
|
BuildRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildRows()
|
||||||
|
{
|
||||||
|
ShortcutList.Children.Clear();
|
||||||
|
_boxes.Clear();
|
||||||
|
foreach (var action in new[] { ShortcutManager.CloseTab, ShortcutManager.NewTab, ShortcutManager.ToggleTopPanel })
|
||||||
|
{
|
||||||
|
var box = new WpfTextBox
|
||||||
|
{
|
||||||
|
Text = ShortcutManager.Format(_shortcuts.Get(action)),
|
||||||
|
IsReadOnly = true,
|
||||||
|
IsTabStop = false,
|
||||||
|
Width = 150,
|
||||||
|
Padding = new Thickness(7, 4, 7, 4),
|
||||||
|
Background = new SolidColorBrush(WpfColor.FromRgb(32, 35, 40)),
|
||||||
|
Foreground = new SolidColorBrush(WpfColor.FromRgb(242, 244, 247)),
|
||||||
|
BorderBrush = new SolidColorBrush(WpfColor.FromRgb(69, 76, 87)),
|
||||||
|
HorizontalContentAlignment = System.Windows.HorizontalAlignment.Center,
|
||||||
|
Tag = action,
|
||||||
|
};
|
||||||
|
var change = new WpfButton { Content = "Змінити", Padding = new Thickness(10, 5, 10, 5), Tag = action };
|
||||||
|
change.Click += Change_Click;
|
||||||
|
var row = new Grid { Margin = new Thickness(0, 0, 0, 8) };
|
||||||
|
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
row.Children.Add(new TextBlock { Text = ShortcutManager.GetActionLabel(action), VerticalAlignment = VerticalAlignment.Center });
|
||||||
|
Grid.SetColumn(box, 1);
|
||||||
|
Grid.SetColumn(change, 2);
|
||||||
|
change.Margin = new Thickness(8, 0, 0, 0);
|
||||||
|
row.Children.Add(box);
|
||||||
|
row.Children.Add(change);
|
||||||
|
ShortcutList.Children.Add(row);
|
||||||
|
_boxes[action] = box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Change_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not WpfButton button || button.Tag is not string action || !_boxes.TryGetValue(action, out var box)) return;
|
||||||
|
_capturingAction = action;
|
||||||
|
_pendingGesture = null;
|
||||||
|
box.Text = "Натисніть комбінацію…";
|
||||||
|
box.Focus();
|
||||||
|
box.PreviewKeyDown -= CaptureBox_PreviewKeyDown;
|
||||||
|
box.PreviewKeyDown += CaptureBox_PreviewKeyDown;
|
||||||
|
box.PreviewKeyUp -= CaptureBox_PreviewKeyUp;
|
||||||
|
box.PreviewKeyUp += CaptureBox_PreviewKeyUp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CaptureBox_PreviewKeyDown(object sender, WpfKeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (_capturingAction is null || sender is not WpfTextBox box) return;
|
||||||
|
var key = e.Key == Key.System ? e.SystemKey : e.Key;
|
||||||
|
if (key == Key.Escape)
|
||||||
|
{
|
||||||
|
CancelCapture(box);
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var modifiers = Keyboard.Modifiers & (ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt);
|
||||||
|
if (!ShortcutManager.TryCreateGesture(key, modifiers, out var gesture)) return;
|
||||||
|
_pendingGesture = gesture;
|
||||||
|
box.Text = ShortcutManager.Format(gesture);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CaptureBox_PreviewKeyUp(object sender, WpfKeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (_capturingAction is null || _pendingGesture is null || sender is not WpfTextBox box) return;
|
||||||
|
var action = _capturingAction;
|
||||||
|
if (!_shortcuts.TrySet(action, _pendingGesture, out var conflictAction))
|
||||||
|
{
|
||||||
|
System.Windows.MessageBox.Show(this, $"Комбінація вже призначена для «{ShortcutManager.GetActionLabel(conflictAction!)}». Оберіть іншу.", "Конфлікт шорткатів", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
box.Text = ShortcutManager.Format(_shortcuts.Get(action));
|
||||||
|
}
|
||||||
|
StopCapture(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelCapture(WpfTextBox box)
|
||||||
|
{
|
||||||
|
box.Text = ShortcutManager.Format(_shortcuts.Get(_capturingAction!));
|
||||||
|
StopCapture(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopCapture(WpfTextBox box)
|
||||||
|
{
|
||||||
|
box.PreviewKeyDown -= CaptureBox_PreviewKeyDown;
|
||||||
|
box.PreviewKeyUp -= CaptureBox_PreviewKeyUp;
|
||||||
|
_capturingAction = null;
|
||||||
|
_pendingGesture = null;
|
||||||
|
Keyboard.ClearFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Shortcuts_Changed(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (!Dispatcher.CheckAccess())
|
||||||
|
{
|
||||||
|
Dispatcher.BeginInvoke(BuildRows);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BuildRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reset_Click(object sender, RoutedEventArgs e) => _shortcuts.ResetToDefaults();
|
||||||
|
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||||
|
|
||||||
|
protected override void OnClosed(EventArgs e)
|
||||||
|
{
|
||||||
|
_shortcuts.Changed -= Shortcuts_Changed;
|
||||||
|
base.OnClosed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-10
@@ -9,28 +9,22 @@ public sealed class TrayIconManager : IDisposable
|
|||||||
private readonly Forms.NotifyIcon _icon;
|
private readonly Forms.NotifyIcon _icon;
|
||||||
private readonly Window _window;
|
private readonly Window _window;
|
||||||
private readonly Action _exit;
|
private readonly Action _exit;
|
||||||
|
private Icon? _ownedIcon;
|
||||||
|
|
||||||
public TrayIconManager(Window window, Action exit)
|
public TrayIconManager(Window window, Action exit)
|
||||||
{
|
{
|
||||||
_window = window;
|
_window = window;
|
||||||
_exit = exit;
|
_exit = exit;
|
||||||
|
|
||||||
Icon? icon = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var icoPath = System.IO.Path.Combine(AppContext.BaseDirectory, "FoxBrowser.ico");
|
|
||||||
if (System.IO.File.Exists(icoPath)) icon = new Icon(icoPath);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
_icon = new Forms.NotifyIcon
|
_icon = new Forms.NotifyIcon
|
||||||
{
|
{
|
||||||
Icon = icon ?? SystemIcons.Application,
|
Icon = SystemIcons.Application,
|
||||||
Text = "FoxBrowser",
|
Text = window.Title,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
ContextMenuStrip = BuildMenu()
|
ContextMenuStrip = BuildMenu()
|
||||||
};
|
};
|
||||||
_icon.MouseClick += OnMouseClick;
|
_icon.MouseClick += OnMouseClick;
|
||||||
|
UpdateIdentity(window.Title, System.IO.Path.Combine(AppContext.BaseDirectory, "FoxBrowser.ico"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MinimizeToTray()
|
public void MinimizeToTray()
|
||||||
@@ -58,6 +52,23 @@ public sealed class TrayIconManager : IDisposable
|
|||||||
_icon.MouseClick -= OnMouseClick;
|
_icon.MouseClick -= OnMouseClick;
|
||||||
_icon.Visible = false;
|
_icon.Visible = false;
|
||||||
_icon.Dispose();
|
_icon.Dispose();
|
||||||
|
_ownedIcon?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateIdentity(string displayName, string iconPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Icon? next = null;
|
||||||
|
if (System.IO.File.Exists(iconPath)) next = new Icon(iconPath);
|
||||||
|
var old = _ownedIcon;
|
||||||
|
_ownedIcon = next;
|
||||||
|
_icon.Icon = next ?? SystemIcons.Application;
|
||||||
|
old?.Dispose();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
_icon.Text = string.IsNullOrWhiteSpace(displayName) ? "FoxBrowser" : displayName[..Math.Min(63, displayName.Length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMouseClick(object? sender, Forms.MouseEventArgs e)
|
private void OnMouseClick(object? sender, Forms.MouseEventArgs e)
|
||||||
|
|||||||
Reference in new issue
Block a user