Fix BASE 1.1
This commit is contained in:
1 parent
9e684ec067
commit
81d1ed8379
7 files changed
+17
-205
No files matched your search
@@ -19,9 +19,9 @@ 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;
|
||||||
|
|||||||
@@ -30,16 +30,14 @@ 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),
|
|
||||||
"FoxBrowser", "session.json");
|
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
|
|||||||
+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,41 +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 ExtensionsFolderName = "Extensions";
|
||||||
private const string LegacyExtensionsFolderName = "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 static string PortableExtensionsPath => Path.Combine(AppContext.BaseDirectory, ExtensionsFolderName);
|
||||||
public static string LegacyExtensionsPath => Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ProductFolderName, LegacyExtensionsFolderName);
|
|
||||||
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(
|
||||||
@@ -44,179 +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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MigrateLegacyExtensionsIfNeeded();
|
|
||||||
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 MigrateLegacyExtensionsIfNeeded()
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(LegacyExtensionsPath) || !Directory.EnumerateFileSystemEntries(LegacyExtensionsPath).Any())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (Directory.Exists(PortableExtensionsPath) && Directory.EnumerateFileSystemEntries(PortableExtensionsPath).Any())
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CopyDirectoryContents(LegacyExtensionsPath, PortableExtensionsPath);
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
// Keep the legacy folder intact. A later startup can retry the migration.
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
// Keep the legacy folder intact and let the normal profile startup continue.
|
|
||||||
}
|
|
||||||
catch (SecurityException)
|
|
||||||
{
|
|
||||||
// Keep the legacy folder intact and let the normal profile startup continue.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+1
-1
@@ -54,7 +54,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<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 не підтримує Chrome-only встановлення кнопкою Install. Завантаж ZIP/CRX, імпортуй його тут — FoxBrowser сам розпакує файл у папку профілю та підключить extension." Foreground="#FFCC66" TextWrapping="Wrap" Margin="0,0,0,4" />
|
<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="Імпортовані файли зберігаються у папці Extensions поруч із FoxBrowser.exe, а реєстрація extension — у portable-профілі WebView2. Старі extension із %LOCALAPPDATA%\FoxBrowser\Extensions переносяться туди автоматично один раз." Foreground="#AEB4BE" TextWrapping="Wrap" Margin="0,0,0,10" />
|
<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">
|
||||||
|
|||||||
@@ -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,7 +45,7 @@ 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); }
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ public partial class ExtensionsSettingsWindow : Window
|
|||||||
{
|
{
|
||||||
await _manager.AddArchiveAsync(dialog.FileName);
|
await _manager.AddArchiveAsync(dialog.FileName);
|
||||||
ExtensionsStatus.Text = "Розширення розпаковано у профіль FoxBrowser та підключено.";
|
ExtensionsStatus.Text = "Розширення розпаковано у профіль FoxBrowser та підключено.";
|
||||||
Dispatcher.BeginInvoke(RefreshPinCheckboxes);
|
Dispatcher.Invoke(RefreshPinCheckboxes);
|
||||||
}
|
}
|
||||||
catch (Exception ex) { ShowError($"Не вдалося імпортувати архів: {ex.Message}"); }
|
catch (Exception ex) { ShowError($"Не вдалося імпортувати архів: {ex.Message}"); }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user