Temp install extension
This commit is contained in:
1 parent
256bb084c5
commit
809474eac4
5 files changed
+138
-15
No files matched your search
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
@@ -102,9 +131,29 @@ public partial class ExtensionsSettingsWindow : Window
|
||||
}
|
||||
|
||||
private async void Store_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user