BASE changed interface

This commit is contained in:
AlastorFox committed 2026-10-01 01:46:18 +03:00
commit 256bb084c5
30 files changed
+2836

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
<Window x:Class="FoxBrowser.UI.ExtensionPopupWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStyle="None" ResizeMode="NoResize" ShowInTaskbar="False"
Width="360" Height="520" MaxHeight="600"
Background="#202328" AllowsTransparency="False">
<Border BorderBrush="#59616D" BorderThickness="1" Background="#202328">
<Grid x:Name="PopupHost" />
</Border>
</Window>
+39
View File
@@ -0,0 +1,39 @@
using System.Windows;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.Wpf;
namespace FoxBrowser.UI;
public partial class ExtensionPopupWindow : Window
{
private readonly CoreWebView2Environment _environment;
private readonly string _extensionId;
private readonly string _popupPage;
public ExtensionPopupWindow(CoreWebView2Environment environment, string extensionId, string popupPage, System.Drawing.Point screenPosition)
{
InitializeComponent();
_environment = environment;
_extensionId = extensionId;
_popupPage = popupPage;
Left = screenPosition.X;
Top = screenPosition.Y;
Deactivated += (_, _) => Close();
Loaded += async (_, _) => await InitializeAsync();
}
private async Task InitializeAsync()
{
try
{
var view = new WebView2();
PopupHost.Children.Add(view);
await view.EnsureCoreWebView2Async(_environment);
view.CoreWebView2.Navigate($"chrome-extension://{_extensionId}/{_popupPage}");
}
catch
{
Close();
}
}
}
+44
View File
@@ -0,0 +1,44 @@
<Window x:Class="FoxBrowser.UI.ExtensionsSettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Розширення" Width="860" Height="620" MinWidth="560" MinHeight="360"
Background="#15171A" Foreground="#F2F4F7">
<DockPanel Margin="14">
<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="Імпортувати розширення…" Click="Add_Click" Padding="12,6" Margin="8,0,0,0" />
<Button Content="Закрити" Click="Close_Click" Padding="12,6" Margin="8,0,0,0" />
</StackPanel>
<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="Профіль WebView2: папка Profile поруч із FoxBrowser.exe (у захищеній папці Windows використовується стандартний профіль %LOCALAPPDATA%\FoxBrowser\WebView2). WebView2 зберігає реєстрацію розширення, а файли unpacked залишаються у вибраній тобою папці." 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" />
<Grid>
<ListBox x:Name="ExtensionList" Background="#202328" BorderBrush="#454C57" Foreground="#F2F4F7">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="8">
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /><ColumnDefinition Width="Auto" /><ColumnDefinition Width="Auto" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" />
<TextBlock Text="{Binding Id}" FontSize="11" Foreground="#AEB4BE" />
</StackPanel>
<CheckBox Grid.Column="1" IsChecked="{Binding IsEnabled, Mode=OneWay}" Tag="{Binding}" Click="Toggle_Click" Content="Увімкнено" VerticalAlignment="Center" Margin="8,0" />
<CheckBox x:Name="PinCheckBox" Grid.Column="2" Content="Закріпити" Tag="{Binding}" Click="Pin_Click" VerticalAlignment="Center" Margin="8,0" />
<Button Grid.Column="3" Content="Налаштування" Tag="{Binding}" Click="Options_Click" Padding="8,4" Margin="0,0,6,0" />
<Button Grid.Column="4" Content="Видалити" Tag="{Binding}" Click="Remove_Click" Padding="8,4" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Grid x:Name="OptionsHost" Visibility="Collapsed" Background="#111317">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button Grid.Row="0" Content="← Назад до списку розширень" Click="BackToList_Click" HorizontalAlignment="Left" Padding="10,6" Margin="8" Panel.ZIndex="2" />
<Grid x:Name="OptionsWebViewHost" Grid.Row="1" />
</Grid>
</Grid>
</DockPanel>
</Window>
+155
View File
@@ -0,0 +1,155 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using FoxBrowser.Core;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.Wpf;
using Forms = System.Windows.Forms;
using WpfButton = System.Windows.Controls.Button;
using WpfCheckBox = System.Windows.Controls.CheckBox;
namespace FoxBrowser.UI;
public partial class ExtensionsSettingsWindow : Window
{
private readonly ExtensionManager _manager;
private readonly CoreWebView2Environment _environment;
private readonly Func<Task> _openStore;
private WebView2? _optionsView;
public ExtensionsSettingsWindow(ExtensionManager manager, CoreWebView2Environment environment, Func<Task> openStore, bool extensionsAvailable)
{
InitializeComponent();
_manager = manager;
_environment = environment;
_openStore = openStore;
ExtensionList.ItemsSource = _manager.Extensions;
if (!extensionsAvailable)
{
ExtensionsStatus.Text = "Розширення недоступні в цьому сеансі: FoxBrowser запустився у режимі відновлення профілю. Перезапусти FoxBrowser для повернення до звичайного профілю.";
ExtensionList.IsEnabled = false;
}
Loaded += async (_, _) =>
{
await _manager.RefreshAsync();
Dispatcher.BeginInvoke(RefreshPinCheckboxes);
};
}
private async void Add_Click(object sender, RoutedEventArgs e)
{
using var dialog = new Forms.FolderBrowserDialog { Description = "Виберіть розпаковану папку extension з manifest.json" };
var owner = new WindowInteropHelper(this).Handle;
if (dialog.ShowDialog(new NativeWindowWrapper(owner)) != Forms.DialogResult.OK) return;
try
{
await _manager.AddExtensionAsync(dialog.SelectedPath);
Dispatcher.BeginInvoke(RefreshPinCheckboxes);
}
catch (Exception ex) { ShowError(ex.Message); }
}
private async void Toggle_Click(object sender, RoutedEventArgs e)
{
if ((sender as WpfCheckBox)?.Tag is not CoreWebView2BrowserExtension extension) return;
var checkBox = (WpfCheckBox)sender;
try { await _manager.SetEnabledAsync(extension, checkBox.IsChecked == true); }
catch (Exception ex)
{
checkBox.IsChecked = extension.IsEnabled;
ShowError(ex.Message);
}
}
private void Pin_Click(object sender, RoutedEventArgs e)
{
if ((sender as WpfCheckBox)?.Tag is CoreWebView2BrowserExtension extension && sender is WpfCheckBox checkBox)
_manager.SetPinned(extension, checkBox.IsChecked == true);
}
private async void Options_Click(object sender, RoutedEventArgs e)
{
if ((sender as WpfButton)?.Tag is not CoreWebView2BrowserExtension extension) return;
var optionsPage = _manager.GetOptionsPage(extension.Id);
if (string.IsNullOrWhiteSpace(optionsPage))
{
ShowError("Для цього extension не знайдено options_ui.page або options_page. Видали його та імпортуй папку ще раз, щоб FoxBrowser прочитав manifest.json.");
return;
}
try
{
_optionsView ??= await CreateOptionsViewAsync();
ExtensionList.Visibility = Visibility.Collapsed;
OptionsHost.Visibility = Visibility.Visible;
_optionsView.CoreWebView2.Navigate($"chrome-extension://{extension.Id}/{optionsPage}");
}
catch (Exception ex) { ShowError($"Не вдалося відкрити налаштування extension: {ex.Message}"); }
}
private void BackToList_Click(object sender, RoutedEventArgs e)
{
OptionsHost.Visibility = Visibility.Collapsed;
ExtensionList.Visibility = Visibility.Visible;
}
private async Task<WebView2> CreateOptionsViewAsync()
{
var view = new WebView2 { Visibility = Visibility.Visible };
OptionsWebViewHost.Children.Add(view);
await view.EnsureCoreWebView2Async(_environment);
return view;
}
private async void Store_Click(object sender, RoutedEventArgs e)
{
Close();
await _openStore();
}
private async void Remove_Click(object sender, RoutedEventArgs e)
{
if ((sender as WpfButton)?.Tag is CoreWebView2BrowserExtension extension)
{
try { await _manager.RemoveAsync(extension); }
catch (Exception ex) { ShowError(ex.Message); }
}
}
private void RefreshPinCheckboxes()
{
foreach (var extension in _manager.Extensions)
{
if (ExtensionList.ItemContainerGenerator.ContainerFromItem(extension) is not ListBoxItem item) continue;
var pin = FindVisualChild<WpfCheckBox>(item, "PinCheckBox");
if (pin is not null) pin.IsChecked = _manager.IsPinned(extension);
}
}
private static T? FindVisualChild<T>(DependencyObject parent, string name) where T : FrameworkElement
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is T match && match.Name == name) return match;
var nested = FindVisualChild<T>(child, name);
if (nested is not null) return nested;
}
return null;
}
private void ShowError(string message) => System.Windows.MessageBox.Show(this, message, "Розширення", MessageBoxButton.OK, MessageBoxImage.Error);
private void Close_Click(object sender, RoutedEventArgs e) => Close();
protected override void OnClosed(EventArgs e)
{
_optionsView?.Dispose();
base.OnClosed(e);
}
private sealed class NativeWindowWrapper : System.Windows.Forms.IWin32Window
{
public NativeWindowWrapper(nint handle) => Handle = handle;
public nint Handle { get; }
}
}
+75
View File
@@ -0,0 +1,75 @@
using System.Drawing;
using System.Windows;
using Forms = System.Windows.Forms;
namespace FoxBrowser.UI;
public sealed class TrayIconManager : IDisposable
{
private readonly Forms.NotifyIcon _icon;
private readonly Window _window;
private readonly Action _exit;
public TrayIconManager(Window window, Action exit)
{
_window = window;
_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 = icon ?? SystemIcons.Application,
Text = "FoxBrowser",
Visible = true,
ContextMenuStrip = BuildMenu()
};
_icon.MouseClick += OnMouseClick;
}
public void MinimizeToTray()
{
_window.ShowInTaskbar = false;
_window.Hide();
}
public void ToggleWindow()
{
if (_window.IsVisible && _window.WindowState != WindowState.Minimized)
{
MinimizeToTray();
return;
}
_window.ShowInTaskbar = true;
_window.Show();
_window.WindowState = WindowState.Normal;
_window.Activate();
}
public void Dispose()
{
_icon.MouseClick -= OnMouseClick;
_icon.Visible = false;
_icon.Dispose();
}
private void OnMouseClick(object? sender, Forms.MouseEventArgs e)
{
if (e.Button == Forms.MouseButtons.Left) ToggleWindow();
}
private Forms.ContextMenuStrip BuildMenu()
{
var menu = new Forms.ContextMenuStrip();
menu.Items.Add("Показати / сховати", null, (_, _) => ToggleWindow());
menu.Items.Add("Вийти", null, (_, _) => _exit());
return menu;
}
}
+22
View File
@@ -0,0 +1,22 @@
<Window x:Class="FoxBrowser.UI.VpnSettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="VPN — WireGuard for Windows" Width="620" Height="430" MinWidth="500" MinHeight="320"
Background="#15171A" Foreground="#F2F4F7">
<DockPanel Margin="14">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
<Button Content="Імпортувати .conf…" Click="Import_Click" Padding="12,6" />
<Button Content="Connect" Click="Connect_Click" Padding="12,6" Margin="8,0,0,0" />
<Button Content="Disconnect" Click="Disconnect_Click" Padding="12,6" Margin="8,0,0,0" />
<Button Content="Закрити" Click="Close_Click" Padding="12,6" Margin="8,0,0,0" />
</StackPanel>
<TextBlock DockPanel.Dock="Top" Text="VPN через зовнішній WireGuard for Windows" FontSize="16" FontWeight="SemiBold" Margin="0,0,0,5" />
<TextBlock DockPanel.Dock="Top" Text="FoxBrowser не вбудовує WireGuard: потрібна окрема офіційна інсталяція WireGuard for Windows." Foreground="#AEB4BE" TextWrapping="Wrap" Margin="0,0,0,12" />
<StackPanel DockPanel.Dock="Bottom" Margin="0,12,0,0">
<TextBlock Text="Статус" Foreground="#AEB4BE" />
<TextBlock x:Name="StatusText" Text="Disconnected" FontSize="18" FontWeight="SemiBold" Margin="0,2,0,0" />
<TextBlock x:Name="ErrorText" Foreground="#FF8C8C" TextWrapping="Wrap" Margin="0,6,0,0" />
</StackPanel>
<ListBox x:Name="ProfileList" Background="#202328" BorderBrush="#454C57" Foreground="#F2F4F7" DisplayMemberPath="Name" />
</DockPanel>
</Window>
+72
View File
@@ -0,0 +1,72 @@
using System.Windows;
using FoxBrowser.Core;
using Forms = System.Windows.Forms;
namespace FoxBrowser.UI;
public partial class VpnSettingsWindow : Window
{
private readonly VpnManager _vpn;
public VpnSettingsWindow(VpnManager vpn)
{
InitializeComponent();
_vpn = vpn;
ProfileList.ItemsSource = _vpn.Profiles;
_vpn.StateChanged += OnStateChanged;
RefreshState();
Closed += (_, _) => _vpn.StateChanged -= OnStateChanged;
}
private void Import_Click(object sender, RoutedEventArgs e)
{
using var dialog = new Forms.OpenFileDialog
{
Filter = "WireGuard config (*.conf)|*.conf|All files (*.*)|*.*",
Title = "Імпортувати WireGuard конфігурацію"
};
if (dialog.ShowDialog() != Forms.DialogResult.OK) return;
try
{
var profile = _vpn.ImportTunnelConfig(dialog.FileName);
ProfileList.SelectedItem = profile;
RefreshState();
}
catch (Exception ex) { ShowError(ex.Message); }
}
private async void Connect_Click(object sender, RoutedEventArgs e)
{
if (ProfileList.SelectedItem is not VpnProfile profile)
{
ShowError("Спочатку імпортуйте або виберіть профіль .conf.");
return;
}
try { await _vpn.ConnectAsync(profile); }
catch (Exception ex) { ShowError(ex.Message); }
finally { RefreshState(); }
}
private async void Disconnect_Click(object sender, RoutedEventArgs e)
{
try { await _vpn.DisconnectAsync(); }
catch (Exception ex) { ShowError(ex.Message); }
finally { RefreshState(); }
}
private void OnStateChanged(object? sender, EventArgs e) => Dispatcher.Invoke(RefreshState);
private void RefreshState()
{
StatusText.Text = _vpn.State.ToString();
ErrorText.Text = _vpn.LastError ?? string.Empty;
}
private void ShowError(string message)
{
ErrorText.Text = message;
System.Windows.MessageBox.Show(this, message, "VPN", MessageBoxButton.OK, MessageBoxImage.Error);
}
private void Close_Click(object sender, RoutedEventArgs e) => Close();
}