diff --git a/Core/ExtensionManager.cs b/Core/ExtensionManager.cs index 8e2f456..be5896d 100644 --- a/Core/ExtensionManager.cs +++ b/Core/ExtensionManager.cs @@ -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 _uiMetadata; private readonly HashSet _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 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 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; diff --git a/MainWindow.xaml b/MainWindow.xaml index 41bdfb1..d43e862 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -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"> @@ -57,7 +58,7 @@ - + @@ -69,7 +70,7 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="8" Padding="{TemplateBinding Padding}" - Margin="{TemplateBinding Margin}"> + Margin="0,0,6,2"> @@ -99,13 +100,9 @@ - + - - - -