Temp install extension

This commit is contained in:
AlastorFox committed 2026-10-01 02:41:50 +03:00
1 parent fd6bc3459f
commit a602512ca3
5 files changed
+139 -16

No files matched your search

+73
View File
@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using System.IO.Compression;
using System.IO;
using System.Text.Json;
using Microsoft.Web.WebView2.Core;
@@ -25,6 +26,9 @@ public sealed class ExtensionManager
private readonly Dictionary<string, ExtensionUiMetadata> _uiMetadata;
private readonly HashSet<string> _pinnedIds;
public static string ExtensionsRootPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FoxBrowser", "Extensions");
public ExtensionManager(CoreWebView2Profile profile)
{
_profile = profile;
@@ -63,6 +67,75 @@ public sealed class ExtensionManager
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? 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;
+4 -7
View File
@@ -4,6 +4,7 @@
Title="FoxBrowser" Width="1180" Height="760" MinWidth="760" MinHeight="480"
Background="{StaticResource WindowBg}" Foreground="{StaticResource TextPrimary}"
Closing="Window_Closing" StateChanged="Window_StateChanged"
UseLayoutRounding="True" SnapsToDevicePixels="True"
TextOptions.TextFormattingMode="Display"
RenderOptions.ClearTypeHint="Enabled">
<Window.Resources>
@@ -57,7 +58,7 @@
<Setter Property="BorderBrush" Value="{StaticResource BorderDark}" />
<Setter Property="BorderThickness" Value="1" />
<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="Cursor" Value="Hand" />
<Setter Property="Template">
@@ -69,7 +70,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8"
Padding="{TemplateBinding Padding}"
Margin="{TemplateBinding Margin}">
Margin="0,0,6,2">
<ContentPresenter ContentSource="Header" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
@@ -99,13 +100,9 @@
<Border DockPanel.Dock="Top" Background="{StaticResource HeaderBg}" Padding="12,10,12,8" BorderBrush="{StaticResource BorderDark}" BorderThickness="0,0,0,1">
<DockPanel>
<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" />
</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>
<Button DockPanel.Dock="Right" Content="+" ToolTip="Нова вкладка" Click="NewTab_Click" Style="{StaticResource IconButton}" Margin="8,0,0,0" />
<TabControl x:Name="TabStrip"
+1 -1
View File
@@ -125,7 +125,7 @@ public partial class MainWindow : Window
var window = new ExtensionsSettingsWindow(
_tabManager.ExtensionManager,
_tabManager.Environment,
() => _tabManager.OpenUrlAsync("https://chromewebstore.google.com/category/extensions"),
address => _tabManager!.OpenUrlAsync(address),
_tabManager.ExtensionsAvailable)
{ Owner = this };
window.ShowDialog();
+7 -3
View File
@@ -5,13 +5,17 @@
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="Магазин у FoxBrowser" ToolTip="Відкрити Chrome Web Store у поточній вкладці" Click="Store_Click" Padding="12,6" />
<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" />
</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 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="Імпортовані файли зберігаються у %LOCALAPPDATA%\FoxBrowser\Extensions, а реєстрація extension — у профілі WebView2. Кнопка «Папка розширень» відкриває це місце." 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">
+54 -5
View File
@@ -15,15 +15,15 @@ public partial class ExtensionsSettingsWindow : Window
{
private readonly ExtensionManager _manager;
private readonly CoreWebView2Environment _environment;
private readonly Func<Task> _openStore;
private readonly Func<string, Task> _openUrl;
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();
_manager = manager;
_environment = environment;
_openStore = openStore;
_openUrl = openUrl;
ExtensionList.ItemsSource = _manager.Extensions;
if (!extensionsAvailable)
{
@@ -50,6 +50,35 @@ public partial class ExtensionsSettingsWindow : Window
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.BeginInvoke(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)
{
if ((sender as WpfCheckBox)?.Tag is not CoreWebView2BrowserExtension extension) return;
@@ -103,8 +132,28 @@ public partial class ExtensionsSettingsWindow : Window
private async void Store_Click(object sender, RoutedEventArgs e)
{
Close();
await _openStore();
try
{
Close();
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)