This commit is contained in:
AlastorFox committed 2026-10-12 02:06:41 +03:00
1 parent a1fff09097
commit d03aacf042
6 files changed
+1 -624

No files matched your search

+1
View File
@@ -3,3 +3,4 @@ bin/
obj/ obj/
*.user *.user
*.suo *.suo
AI/
-96
View File
@@ -1,96 +0,0 @@
Реалізуй чотири незалежні покращення в FoxBrowser. Роби їх послідовно,
кожне — окремим комітом, з коротким тестовим сценарієм на кінці.
Не чіпай TabManager/watchdog-логіку, якщо це не потрібно напряму для задачі.
## 1. Підтримка розширень (Chrome-сумісні extensions)
WebView2 має нативну підтримку unpacked-розширень через CoreWebView2Profile:
- При створенні CoreWebView2Environment виставити
`CoreWebView2EnvironmentOptions.AreBrowserExtensionsEnabled = true`
(без цього прапорця AddBrowserExtensionAsync повертає ERROR_NOT_SUPPORTED).
- Додати `Core/ExtensionManager.cs`:
- `AddExtensionAsync(string folderPath)` → `profile.AddBrowserExtensionAsync(folderPath)`.
Важливо: приймається тільки шлях до **розпакованої** папки з manifest.json
(той самий формат, що й Chrome "Load unpacked" у режимі розробника) —
прямого встановлення з Chrome Web Store за URL немає в API, це нормально,
не намагайся емулювати завантаження з магазину.
- `GetInstalledExtensionsAsync()` → `profile.GetBrowserExtensionsAsync()`.
- Enable/disable через `CoreWebView2BrowserExtension.IsEnabled`, видалення — `.Remove()`.
- UI: нове вікно/секція в налаштуваннях `ExtensionsSettingsWindow.xaml` — список
встановлених розширень (назва, іконка якщо є, перемикач увімк/вимк, кнопка
видалити) + кнопка "Додати" з `System.Windows.Forms.FolderBrowserDialog` для
вибору папки розширення.
- Розширення персистяться в профілі (user data folder) автоматично —
не треба вручну зберігати список між запусками, просто перечитувати
GetBrowserExtensionsAsync() при старті.
- ⚠️ Це API позначене в документації Microsoft як таке, що ще може змінюватись
("prerelease"/experimental) — перевір актуальну версію WebView2 SDK
в csproj, і якщо AddBrowserExtensionAsync недоступний у поточній версії,
онови NuGet-пакет Microsoft.Web.WebView2 до версії, де метод є.
## 2. Темна смужка заголовка вікна (title bar)
Стандартний WPF Window малює системну (світлу) title bar незалежно від теми
застосунку. Потрібен P/Invoke до DWM:
- У `Native/Win32Interop.cs` додати сигнатуру:
`DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int size)`.
- Атрибут `DWMWA_USE_IMMERSIVE_DARK_MODE` = 20 (Windows 10 build 19041+ і всі
Windows 11); для старіших білдів Windows 10 (до 19041) значення атрибута — 19.
Визнач білд через `Environment.OSVersion` і підстав правильне число, або
просто пробуй 20 і фолбек на 19, якщо виклик повернув помилку.
- Викликати з value=1 (true) у `MainWindow.xaml.cs` в обробнику
`SourceInitialized` (handle вікна вже створений на цьому етапі,
раніше — hwnd ще нульовий і виклик не подіє).
- Якщо застосунок надалі планує світлу/темну тему як перемикач — винести
виклик у окремий метод `ApplyDarkTitleBar(bool enabled)` і викликати його
і при старті, і при зміні теми в налаштуваннях, а не хардкодити один раз.
## 3. Прибрати кнопку "у трей" з головного вікна
- У `MainWindow.xaml` знайти і видалити XAML-кнопку, що ховає вікно в трей
(і відповідний Click-обробник у code-behind, якщо він не використовується
більше ніде).
- Функціональність згортання в трей НЕ прибирати — вона має лишитись
доступною через стандартну поведінку: клік на "згорнути" (мінімізувати)
вікна або на "закрити" (якщо зараз close перехоплюється і ховає замість
завершення застосунку — залиш як є). Перевір `TrayIconManager.cs` —
контекстне меню трею (Show/Hide, Exit) лишається без змін.
- Ціль — прибрати саме зайвий UI-елемент з панелі інструментів вікна,
не ламаючи існуючий цикл згортання/розгортання.
## 4. Виправити подвійний клік по іконці трею
Зараз: перший клік на іконку в треї лише "проявляє" застосунок на панелі
задач, і тільки другий клік по цій новій іконці на панелі задач реально
відкриває вікно. Це означає, що десь у `TrayIconManager.cs` логіка розбита
на два окремі кроки замість одного atomic-виклику, або що обробник висить
на `MouseDoubleClick` замість одинарного кліку лівою кнопкою, а проміжний
стан (ShowInTaskbar=true) відбувається окремо від Show()/Activate().
Виправлення:
- Знайти обробник кліку по NotifyIcon (ймовірно `MouseClick`/`MouseDoubleClick`
на `System.Windows.Forms.NotifyIcon` або аналог з бібліотеки трею, якщо
використовується інша, напр. Hardcodet.NotifyIcon.Wpf).
- Прибрати залежність на подвійний клік — підписатись на одинарний клік
лівою кнопкою (`MouseClick` з перевіркою `e.Button == MouseButtons.Left`,
або відповідний однократний eventArgs у використовуваній бібліотеці).
- В одному обробнику, синхронно й без проміжних станів, виконати весь ланцюжок:
```csharp
mainWindow.ShowInTaskbar = true;
mainWindow.Show();
mainWindow.WindowState = WindowState.Normal;
mainWindow.Activate();
```
Порядок важливий: ShowInTaskbar виставляється ДО Show(), інакше вікно
може з'явитись без іконки на панелі задач до наступного перемальовування.
- Протестувати сценарій: застосунок згорнутий у трей (ShowInTaskbar=false,
Hide()) → один клік лівою кнопкою по іконці трею → вікно одразу видиме,
активне, і з'являється на панелі задач за один крок, без другого кліка.
- Right-click (контекстне меню Show/Hide/Exit) не займати — воно вже працює
окремо і його логіку не чіпати.
Після кожного пункту напиши короткий підсумок, що саме змінено і як це
перевірити вручну на Windows-машині (агент у Linux sandbox без dotnet/WPF
runtime, тому фінальну компіляцію й ручний тест виконує користувач).
-280
View File
@@ -1,280 +0,0 @@
# FoxBrowser — інструкція для наступного агента
## Мета проєкту
FoxBrowser — Windows-first легкий фоновий браузер для музики/відео. Мета — не зробити ще один Chromium-клон, а зменшити кількість одночасно живого web-коду, renderer-ів, мережевих ресурсів і візуального навантаження.
Ключовий сценарій: користувач відкриває багато логічних вкладок, але у фоні реально працює одна або кілька медіа-вкладок. Важливо не ламати звук і повертати стан вкладки після перемикання.
## Стек і запуск
- C# / .NET 8;
- WPF;
- Microsoft.Web.WebView2 `1.0.2903.40`;
- Windows 10 1809+ і Microsoft Edge WebView2 Runtime.
Поточний агент працює в Linux sandbox без `dotnet`, Windows SDK та WebView2 Runtime, тому перед релізом потрібно запускати:
```powershell
dotnet restore
dotnet build -c Debug
dotnet run --project .\FoxBrowser.csproj
```
## Що вже зроблено
### Shell і UI
- WPF shell, адресний рядок, назад/вперед/оновити;
- логічні вкладки;
- системний трей;
- кнопка `♫ Audio`;
- явна кнопка `Перейти`;
- address bar синхронізується з активною вкладкою і оновлюється після навігації по посиланнях;
- закриття вкладок виправлено на WPF `Button`.
### Один WebView2
`Core/TabManager.cs` використовує **один спільний WebView2**. `BrowserTab` у `Tab.cs` містить тільки metadata: URL, title, scroll position, media position, прапори Audio/playing/loading.
При перемиканні:
1. поточний URL/scroll/media state зберігається через JS;
2. активною стає інша логічна вкладка;
3. єдиний WebView2 переходить на її URL;
4. після `NavigationCompleted` відновлюються scroll position, currentTime і playback, якщо попереднє медіа грало.
### Resource firewall
Через `WebResourceRequested` блокуються відомі рекламні й аналітичні host-и. У lightweight/minimized режимі блокуються images і fonts. Окремого `CoreWebView2WebResourceContext.Iframe` **не існує**, тому iframe/embed фільтруються тільки консервативно за URL (`/embed/`, `livechat`). Не повертати enum `Iframe` у код.
### Audio / minimal YouTube
Кнопка Audio:
- приховує картинки, SVG, canvas і відеовізуалізацію;
- прибирає частину YouTube UI, рекомендації, comments, chat, Shorts/shelves;
- прибирає preload/prefetch/next;
- залишає media element для звуку.
CSS/JS не може звільнити базову пам’ять JavaScript runtime YouTube. Це оптимізація трафіку/DOM/GPU, а не повна заміна renderer.
### Minimized і watchdog
- при згортанні у трей встановлюється minimized mode;
- нові навігації блокуються, візуальний шар стискається, звук не зупиняється;
- watchdog кожні 30 секунд перевіряє Working Set;
- поріг soft-reload — 850 MB;
- перед reload зберігаються URL, scrollY, currentTime, playing;
- після reload стан відновлюється з повторними спробами через 300 і 1000 мс.
### Native audio boundary
`Core/NativeAudioSession.cs` містить `INativeAudioSession` і WPF `MediaPlayer` для **direct audio URL**. Автоматичного YouTube extractor немає. Це навмисно: extractor нестабільний через зміни YouTube, авторизацію, DRM та правила сервісу.
## Що ще не зроблено
1. Фінальна компіляція на Windows і тест WebView2 Runtime.
2. Реальний вимір RAM/CPU/GPU у старій і новій архітектурі.
3. Надійне збереження cookies/session state між переходами — профіль WebView2 існує, але стан вкладки поки metadata-only.
4. Повне відновлення плейлистів, form state, history та відкритих popup/window.
5. Відокремлений native YouTube audio backend із optional extractor.
6. Win32 always-on-bottom (`SetWinEventHook`) ще не реалізований.
7. Не можна блокувати всі iframe, scripts або media: YouTube зламається. Firewall має бути allow-by-default, deny тільки для перевірених ресурсів.
## Відомі ризики
- `ExecuteScriptAsync` повертає JSON-encoded string; для state parsing використовувати `JsonSerializer.Deserialize<string>(result)`, а не простий `Trim('"')`.
- Перемикання вкладки не повинно повторно запускати navigation через `SelectionChanged`; у `TabManager` є guard для вже активної вкладки.
- Після переходу посиланням address bar оновлюється через `ActiveTabStateChanged`.
- `WasPlaying` не гарантує autoplay: сайт може блокувати `play()` після navigation.
- Сховати `<video>` не означає гарантовано вимкнути video decode.
- SSD може зберігати metadata, але не може замінити RAM для живого JS/audio renderer.
## Що з власним браузерним engine
Повний власний engine HTML/CSS/JS не є практичним наступним кроком. Потрібні DOM/layout, CSS engine, JS engine, network/security sandbox, cookies/CORS, media codecs, WebRTC, DRM і web compatibility. YouTube-сумісний engine — це багаторічний проєкт рівня Chromium/WebKit.
Реалістичні альтернативи:
- власний **browser shell** над системним WebView2;
- один живий WebView2 + metadata tabs;
- власний resource firewall;
- окремий native audio backend для direct streams;
- у далекій перспективі — власний вузький media client, але не власний універсальний HTML engine.
Критерій переходу до іншої технології: спочатку виміряти shared WebView2. Якщо одна активна YouTube-вкладка все одно споживає неприйнятну RAM, не писати engine з нуля; робити окремий lightweight media client/native audio path.
## FEATURE_REQUEST_2026-09 — виконано
### 1. Chrome-сумісні unpacked extensions
У `Core/ExtensionManager.cs` додано роботу з `CoreWebView2Profile`: перечитування встановлених розширень, додавання папки з `manifest.json`, enable/disable через `EnableAsync(bool)` і видалення через `RemoveAsync()`. Важливо: у .NET WebView2 `IsEnabled` — read-only property; його не можна присвоювати напряму.
`MainWindow` створює WebView2 environment з `AreBrowserExtensionsEnabled = true`. У `UI/ExtensionsSettingsWindow.xaml` додано список розширень, перемикач стану, видалення і вибір папки через `FolderBrowserDialog`. WebView2 SDK `1.0.2903.40` містить потрібний API.
Ручний тест Windows:
1. Підготувати unpacked extension folder із `manifest.json`.
2. Запустити FoxBrowser і натиснути **Розширення**.
3. Натиснути **Додати розширення…**, вибрати папку.
4. Перевірити, що extension з’явився у списку.
5. Вимкнути/увімкнути checkbox і перевірити поведінку extension у новій навігації.
6. Видалити extension, перезапустити FoxBrowser і переконатися, що він не повернувся.
### 2. Темна title bar
У `Native/Win32Interop.cs` додано `DwmSetWindowAttribute` з атрибутом 20 і fallback на 19. `MainWindow` викликає `ApplyDarkTitleBar(true)` у `SourceInitialized`, коли HWND уже створений.
Ручний тест Windows:
1. Запустити FoxBrowser на Windows 10 19041+ або Windows 11.
2. Перевірити, що системна title bar темна, а не стандартна світла.
3. Перевірити, що вікно можна згортати, розгортати й закривати без зміни поведінки.
### 3. Прибрана кнопка «Трей» з головного вікна
Кнопка `Трей` видалена з `MainWindow.xaml`. Сам `TrayIconManager` і згортання через системні кнопки вікна залишені. Невикористаний `HideToTray_Click` прибраний із code-behind.
Ручний тест Windows:
1. Перевірити, що у toolbar немає кнопки `Трей`.
2. Натиснути стандартну кнопку minimize.
3. Переконатися, що застосунок ховається у tray.
4. Через tray відкрити його назад.
### 4. Одинарний клік по tray icon
`TrayIconManager` більше не використовує `DoubleClick`. Обробляється `MouseClick` з перевіркою `MouseButtons.Left`. В одному atomic-ланцюжку виставляються `ShowInTaskbar = true`, `Show()`, `WindowState = Normal`, `Activate()`. При приховуванні `ShowInTaskbar = false` виставляється до `Hide()`.
Ручний тест Windows:
1. Запустити FoxBrowser.
2. Згорнути його в tray.
3. Натиснути лівою кнопкою один раз на tray icon.
4. Перевірити, що вікно одразу видиме, активне і має taskbar entry.
5. Перевірити, що правий клік і меню `Показати / сховати` та `Вийти` працюють окремо.
Окремі git commits не створені через I/O-помилку файлової системи змонтованого каталогу; зміни залишені в робочих файлах і описані тут по незалежних етапах.
## Startup recovery fix — 0x8007139F
`MainWindow_Loaded` now retries WebView2 with a disposable recovery user-data folder when the persistent profile returns HRESULT `0x8007139F` (often caused by another WebView2/FoxBrowser process holding the profile lock or by a profile in an invalid state). The normal profile remains the first choice so extensions and cookies persist normally; recovery mode is only a startup fallback.
`TabManager.InitializeAsync` also treats extension-profile enumeration as optional: if extension APIs fail, normal browsing still starts and the extension manager is disabled for that session.
All navigation methods (`GoBack`, `GoForward`, `Reload`, `NavigateAsync`, `SwitchToTabAsync`) now return safely while `CoreWebView2` is not initialized. This prevents the previous `NullReferenceException` when a user clicked a tab or Reload while initialization had failed or was still in progress.
Manual test:
1. Close all FoxBrowser and WebView2 processes in Task Manager and start normally.
2. Start a second instance or leave an old WebView2 profile locked; the new instance should use recovery data instead of showing only an initialization error.
3. Click Reload, Back, Forward and tabs before/while startup completes; no `NullReferenceException` should appear.
4. After normal startup, close the recovery/duplicate process and start again to return to the persistent profile.
## FEATURE_REQUEST_2026-09-25 — частково виконано без запуску команд
### 1. WireGuard VPN — базова CLI-фаза
Додано `Core/VpnManager.cs` і `UI/VpnSettingsWindow.xaml(.cs)`. FoxBrowser:
- шукає зовнішній `wireguard.exe` у Program Files та App Paths registry;
- імпортує `.conf` у `%LOCALAPPDATA%\\FoxBrowser\\VPN`;
- запускає `/installtunnelservice <config>`;
- зупиняє `/uninstalltunnelservice <TunnelName>`;
- читає exit code, stdout/stderr і показує помилку;
- не встановлює WireGuard автоматично;
- показує статус VPN у головній панелі.
Це навмисно не вбудований VPN. Потрібен офіційний WireGuard for Windows.
Ручна перевірка:
1. Встановити WireGuard for Windows з https://www.wireguard.com/install/.
2. Запустити FoxBrowser і натиснути `VPN: Вимкнено`.
3. Імпортувати коректний `.conf`.
4. Вибрати профіль і натиснути Connect.
5. Перевірити статус `Connected` і мережевий маршрут.
6. Натиснути Disconnect і перевірити, що статус повернувся до `Disconnected`.
7. Повторити тест без WireGuard — має бути зрозуміле повідомлення, що зовнішній клієнт не встановлено.
### 2. Extensions settings
`ExtensionsSettingsWindow` уже існує і кнопка `Розширення` залишається в головному toolbar. Воно відкривається модально через `ShowDialog()`, так само як VPN settings window.
### 3a. Нативний плеєр прибрано з UI та code-behind
Кнопку `Native`, нижню панель плеєра, `NativeAudio_Click`, pause/stop handlers і поля `NativeAudioSession`/`yt-dlp` прибрано з `MainWindow`. JS-based Audio mode і state capture у `TabManager` не змінювалися.
`Core/NativeAudioSession.cs` і `Core/YtDlpExtractor.cs` залишені як невикористані source-файли, оскільки поточне обмеження задачі забороняє файлові shell-команди, а видалення не підтримується файловим patch API. Вони більше ніде не підключені й не впливають на runtime.
### 3b. «Тримати вкладки активними» — свідомо НЕ реалізовано напівмірою
Поточний `TabManager` має один shared `WebView2`; тому такий toggle потребує справжнього multi-WebView2 refactor. Простий bool був би брехливим: сторінки все одно перезавантажувалися б при перемиканні. Повний режим має винести resource firewall, navigation events, title/audio tracking, state capture і minimized-mode застосування з жорстко зашитого `_view` у per-tab controller abstraction.
Цей пункт зупинено до окремого рішення/етапу, як і вимагав feature request: не приховувати конфлікт архітектури під виглядом працюючого перемикача. Дефолтний shared режим і всі існуючі memory optimizations залишилися без змін.
Фінальну компіляцію та ручний тест потрібно виконати на Windows вручну.
## Extensions follow-up
Each extension row now has `Налаштування`. FoxBrowser opens the conventional `chrome-extension://<id>/options.html` page. This works only for extensions that actually declare an options page; extensions without one may show a blank/error page.
A `Магазин розширень` button opens the Chrome Web Store in the active FoxBrowser tab. WebView2's supported .NET API installs unpacked extensions from a local folder via `AddBrowserExtensionAsync`; it does not provide a Chrome Web Store/CRX installer API. Therefore the store button is a catalog/browser shortcut, not a direct one-click installer. The user must download/unpack an extension and use `Імпортувати розширення…`.
The WebView2 profile is `%LOCALAPPDATA%\\FoxBrowser\\WebView2`. The source files of an unpacked extension remain in the folder selected during import; WebView2 persists the registration in the profile but does not copy the extension source into the profile.
## Audio identity in Windows
Windows Volume Mixer usually attributes WebView2 page audio to the child process `msedgewebview2.exe`, because that renderer process owns the Chromium audio session. The parent executable name `FoxBrowser.exe` cannot reliably rename that session through ordinary WebView2 APIs. A native WASAPI session-label workaround would need to enumerate child audio sessions and repeatedly set display names; it is version-sensitive and may affect unrelated WebView2 sessions, so it was not added as a fake promise.
A guaranteed `FoxBrowser` audio identity would require a separate native audio pipeline that owns playback itself, which is exactly the native player path removed from the current product because it was not reliable for the intended YouTube scenario.
## 25.09.2026 — extension diagnostics and Windows audio labeler
### Extension diagnostics
The recovery branch now uses the shared `CreateWebViewOptions()` method, so both normal and recovery environments receive `AreBrowserExtensionsEnabled = true`. `TabManager.ExtensionsAvailable` is false when the app had to use the disposable recovery profile or when extension profile enumeration fails. The settings window displays an explicit recovery-mode message instead of silently showing an empty list.
The checkbox remains `Mode=OneWay`; `Toggle_Click` calls `EnableAsync(bool)` and restores the old visual state on failure. `FolderBrowserDialog` now receives an `IWin32Window` owner wrapper created from `WindowInteropHelper(this).Handle`.
Extension options no longer navigate the main shared WebView2. `ExtensionsSettingsWindow` creates its own lazy `WebView2` using the same `CoreWebView2Environment`, displays it inside the settings window, and navigates that isolated control to `chrome-extension://<id>/options.html`. The main tab is not replaced. The Web Store button remains a catalog shortcut; WebView2 still cannot install directly from the store.
### Audio session labeler
Added `Core/AudioSessionLabeler.cs` and the `NAudio 2.2.1` package reference. The labeler enumerates the process tree from the FoxBrowser PID, keeps only descendant `msedgewebview2.exe` PIDs, and changes `DisplayName`/`IconPath` only for matching WASAPI sessions. It subscribes to NAudio `OnSessionCreated` and also rescans every two seconds because session labels live only for the lifetime of each WASAPI session. It does not touch unrelated Edge, Teams, Outlook, or other WebView2 sessions.
The existing `FoxBrowser.ico` is already present and is used for the audio session icon. The labeler starts after WebView2 initialization and is disposed during application shutdown.
Final build and runtime verification remain Windows-only.
## Fix: extension options reported «file not found»
The previous implementation always navigated to `chrome-extension://<id>/options.html`. That is not valid for many extensions: Manifest V3 commonly declares `options_ui.page`, and older extensions may use `options_page`, with arbitrary relative paths.
`ExtensionManager` now reads `manifest.json` during import, extracts `options_ui.page` first and `options_page` second, normalizes the relative path, and persists the mapping in `%LOCALAPPDATA%\\FoxBrowser\\extension-options.json`. The isolated options WebView2 navigates to the stored manifest path instead of a hard-coded `options.html`.
Extensions imported before this fix have no stored manifest mapping. For those, remove the extension from FoxBrowser and import its folder again once. If the manifest has no `options_ui.page` or `options_page`, the extension simply has no options page and the button reports that clearly instead of showing a misleading file-not-found navigation.
## 26.09.2026 — extension UX and VPN removal
- Added `Назад до списку розширень` inside the isolated options WebView2 view. It hides the options view and restores the extension list without closing the settings window.
- Added persisted pinning via `ExtensionManager`: IDs are stored in `%LOCALAPPDATA%\\FoxBrowser\\pinned-extensions.json`, and `PinnedExtensions` updates live.
- Added a puzzle-piece icon strip near the address bar. Clicking a pinned icon opens the extensions window; the pin state itself is controlled per extension in the settings list.
- Removed the VPN button, field, startup subscription, and VPN handlers from the main browser code. The old VPN source files remain as unreferenced files because this agent session's file API does not support physical deletion; they are no longer used by the application. They can be deleted manually from the project in Visual Studio.
## 26.09.2026 — pinned extension popups and back-button layout
Pinned extension metadata now reads `action.default_popup` / `browser_action.default_popup` and `action.default_icon` / `browser_action.default_icon` from the unpacked extension manifest. It is persisted in `%LOCALAPPDATA%\\FoxBrowser\\extension-ui.json`. The pinned buttons are now on the right side of the address bar, immediately before the general `Розширення` button. An icon file is rendered when available; otherwise the first character of the extension name is used as a fallback glyph.
A pinned extension with `default_popup` opens `ExtensionPopupWindow`, a borderless 360x520 WebView2 window positioned below the clicked button, using the same WebView2 environment and closing on `Deactivated`. Extensions without a popup use the general extension list as the documented fallback. Disabling an extension immediately removes its icon from the pinned strip; re-enabling restores it.
The options view now uses a two-row Grid: a dedicated top row contains the back button and a separate bottom row contains the isolated WebView2. This removes the previous overlay/z-order ambiguity and keeps `Назад до списку розширень` visible whenever options are open.
-70
View File
@@ -1,70 +0,0 @@
# FoxBrowser — гайд з переходу на Ultralight (замість/поруч з WebView2)
## Контекст і чому це взагалі розглядається
Поточна архітектура (`TabManager.cs`, `NativeAudioSession.cs`) побудована на Microsoft Edge WebView2 — тобто по суті на Chromium/Blink рушії, який вже стоїть у системі. Виграш там — тільки по диску (немає власної копії Chromium в інсталяторі) і по shell-архітектурі (один спільний WebView2, suspend вкладок, audio-only CSS/JS). Базове споживання RAM на сторінку залишається "chromium-рівня", бо рушій той самий.
Диск користувача не хвилює. Хвилює тільки RAM. Тому єдиний спосіб реально зменшити базове споживання пам'яті — це взяти **інший, не Chromium-похідний рушій**. Ultralight — головний кандидат: це форк WebKit (WebCore) зі своїм легким рендерером і JavaScriptCore замість V8, зроблений спеціально для embedding в apps/games, а не для повноцінного браузингу.
**Це не заміна архітектури "один shell — багато логічних вкладок" — це заміна рушія під капотом `TabManager`.** Wrapper-рівень (metadata tabs, watchdog, resource firewall) залишається концептуально тим самим.
## ⚠️ Критичне обмеження — прочитати перед тим, як писати хоч рядок коду
Офіційний FAQ Ultralight прямо каже: **рушій не підтримує WebGL, WebRTC, а підтримка HTML5 Video/Audio — "currently experimental"**. DRM (Widevine) не підтримується взагалі — це навіть не обговорюється в їхніх матеріалах, бо Ultralight не є Chromium-похідним і не має ліцензованого DRM-модуля.
Це напряму б'є по головному сценарію проєкту — YouTube/музичні сервіси:
- Звичайний `<video>`/`<audio>` на YouTube може просто не програватись, або програватися нестабільно, бо аудіо/відео декодування в рушії — експериментальна фіча, а не production-ready шлях.
- Будь-який контент з DRM (YouTube Premium, Spotify Web Player з захищеними треками тощо) гарантовано не запрацює.
- YouTube — важкий сучасний SPA (React-подібний фронтенд, WASM-компоненти). WebCore/JavaScriptCore може мати проблеми сумісності, яких в Blink просто немає, бо весь сучасний веб тестується проти Chromium.
**Практичний висновок:** якщо є ідея "просто підмінити WebView2 на Ultralight і воно так само програє YouTube-звук" — це, ймовірно, не спрацює з коробки. Реалістичний шлях — той, що вже закладений у проєкті через `NativeAudioSession.cs`: не покладатись на рушій узагалі для аудіо-декодування, а брати **direct audio URL** і програвати його нативним `MediaPlayer`/WASAPI в обхід HTML5 `<audio>` елемента. Ultralight в такому разі відповідає тільки за легкий рендер сторінки (UI, DOM, JS-логіка сайту), а не за сам звук.
## Крок 0 — Go/No-Go тест (зробити ДО будь-якого рефакторингу коду)
Не чіпати `TabManager.cs` одразу. Спочатку окремим ізольованим експериментом перевірити, чи Ultralight взагалі відкриває YouTube прийнятно:
1. Завантажити офіційний Ultralight SDK, знайти в ньому Sample #8 ("Browser") — це вже готовий мінімальний браузер на чистому Ultralight.
2. Відкрити в ньому youtube.com, спробувати програти будь-яке відео.
3. Зафіксувати чесно: чи взагалі вантажиться сторінка, чи є звук, чи є відео, наскільки стабільно (не падає/не висне при скролі списку рекомендацій).
4. Порівняти RAM одного відкритого таба в Sample #8 з тим, що зараз показує watchdog для WebView2 на тій самій сторінці.
Якщо на кроці 3 YouTube взагалі не вантажиться або аудіо не програється — не переходити до Кроку 1. У такому разі єдиний life-виправданий сценарій — Ultralight як рушій **тільки для легких некомплексних сторінок** (власний UI, налаштування, прості сайти), а YouTube/аудіо йде повністю через `NativeAudioSession` (direct URL, без рендеру самої сторінки взагалі, або з окремим легким HTML-плеєром власної розробки). Це суттєво звужує роль Ultralight у проєкті — задокументувати це чесно, а не підганяти висновок під бажаний результат.
## Стек і бібліотеки для .NET
Офіційний API Ultralight — тільки C/C++. Готових офіційних .NET-бібліотек від самої Ultralight Inc немає — є community-підтримувані байндинги:
- **UltralightNet** / **UltralightNet.Bundle** (NuGet) — керовані байндинги до C API Ultralight + AppCore (windowing, font loader, filesystem, logger), з нативними бінарниками для `win-x64` вже в пакеті.
```
dotnet add package UltralightNet.Bundle
```
- ⚠️ На момент цього гайду `UltralightNet.Bundle` таргетить **.NET 10**, а поточний проєкт — **.NET 8**. Перед стартом перевірити актуальну версію пакета на nuget.org і чи є білд під .NET 8, або чи є сенс піднімати проєкт до .NET 10 заради цього (окреме рішення, не робити мовчки за замовчуванням).
- Пакет — тонка обгортка над нативним C API, тобто код виглядає не по-WPF-івськи (`renderer.CreateView()`, ручний update/render loop), а не як `CoreWebView2` — буде помітно більше низькорівневого коду, ніж зараз з WebView2.
## Ліцензування — важливо для будь-якого комерційного релізу
Ultralight розповсюджується за "ULTRALIGHT FREE LICENSE AGREEMENT — V1":
- Free/Limited Commercial Distribution дозволена **тільки якщо** юрособа-ліцензіат не мала обороту понад **US$100,000** за останній фінансовий рік.
- Якщо оборот перевищує цей поріг у будь-якому наступному році — усі права на комерційну дистрибуцію автоматично анулюються, і потрібна окрема платна ліцензія напряму з Ultralight Inc.
- Для приватного/некомерційного use case (як зараз, судячи з проєкту) це не проблема, але задокументувати цей поріг, щоб не зʼясувати це post-factum при спробі щось випустити комерційно.
- Заборонено reverse engineering, декомпіляція, спроби дістатись сирців пропрієтарної частини (`UltralightCore`, `Ultralight` view/compositor) — сама `WebCore` (layout engine, форк WebKit) відкрита під LGPL/BSD.
## Архітектурні відмінності від WebView2, які треба врахувати в коді
- **Single process, без IPC** — на відміну від WebView2/Chromium, де кожен renderer — окремий процес (`msedgewebview2.exe` per tab/GPU), Ultralight працює в одному процесі без міжпроцесної ізоляції. Це означає: (а) менше overhead per-tab у теорії, (б) **немає** природної sandbox-ізоляції — крашнута/зависла вкладка може завалити весь застосунок, а не тільки один child-процес. Поточний watchdog (`MemoryWatchdog.cs`) писався в припущенні окремих WebView2-процесів — цю логіку треба переосмислити під single-process модель.
- **Рендер:** GPU-текстура або CPU pixel buffer, керується вручну через `renderer.Update()` / `renderer.Render()` у власному циклі — на відміну від WebView2, де WPF-хостинг рендер-циклу ховає це від вас. Треба буде інтегрувати цей update loop у WPF `CompositionTarget.Rendering` або окремий таймер, і подавати результат як `WriteableBitmap`/текстуру у WPF `Image`-контрол.
- **`WebResourceRequested`-подібного firewall** у тому вигляді, що є зараз (перехоплення мережевих запитів по host/URL) — треба перевірити, чи є еквівалент у Ultralight API (AppCore filesystem/network hooks), задокументована частина цього не гарантована так само детально, як у WebView2.
## Рекомендований план дій для агента
1. **Крок 0 (Go/No-Go тест вище) — обов'язково перший**, і чесно задокументувати результат у цьому файлі чи окремому `ULTRALIGHT_EVAL_RESULTS.md`, включно з цифрами RAM.
2. Якщо результат прийнятний — не переписувати `TabManager.cs` одразу поверх існуючого. Зробити паралельний `Core/UltralightTabManager.cs` (або аналог) як окрему гілку/фіче-флаг, щоб WebView2-шлях лишався робочим fallback, поки Ultralight-шлях не доведено стабільним на реальних сценаріях (не тільки YouTube — плейлисти, перемикання вкладок, тривала фонова робота годинами).
3. Портувати спочатку тільки non-media сторінки (власний UI, прості сайти) на Ultralight, лишаючи медіа-плеєр окремо через `NativeAudioSession` (direct URL) незалежно від того, який рушій рендерить решту.
4. Watchdog і suspend-логіку переписати під single-process модель Ultralight — старі пороги (850 MB soft-reload) писались під конкретну поведінку WebView2/Chromium і, ймовірно, невалідні напряму.
5. Повторно виміряти реальний виграш по RAM (не по відчуттю) на тому самому сценарії, що і в NEXT_AGENT_GUIDE.md — одна довгограюча медіа-вкладка годинами — і чесно порівняти з поточними цифрами WebView2-версії.
## Головний ризик, який не можна замовчати
Якщо Крок 0 покаже, що YouTube на Ultralight банально не працює (немає звуку, ламається UI, падає рушій) — це не привід "трохи допрацювати", а сигнал, що для головного use case проєкту (легкий фоновий плеєр для YouTube/музики) Ultralight як **основний** рушій не підходить, і чесний наступний крок — залишити WebView2 для медіа-сторінок, а Ultralight розглядати хіба що для легких допоміжних вікон (налаштування, non-media UI), де сумісність з сучасним вебом не критична.
-82
View File
@@ -1,82 +0,0 @@
# Результати тестування Ultralight (Крок 0 / Go-No-Go)
Тестування виконано згідно з вимогами `ULTRALIGHT_AGENT_GUIDE.md` на Windows 10 x64, .NET 8, Ultralight v1.3.0 (WebKit 610.4.3.1.4, JavaScriptCore).
---
## 1. Конфігурація середовища тестування
- **Платформа:** Windows 10 (10.0.19045), x64
- **Runtime:** .NET 8.0 (`net8.0-windows`)
- **Бібліотеки:** `UltralightNet` 1.3.0, `UltralightNet.AppCore` 1.3.0, `UltralightNet.Binaries` 1.3.0
- **Нативні ресурси:** `AppCore.dll`, `Ultralight.dll`, `UltralightCore.dll`, `WebCore.dll`, `icudt67l.dat`, `cacert.pem`
- **Тестові цілі:**
1. `about:blank` (перевірка наявності Web/Media API)
2. `https://www.youtube.com/` (головна сторінка YouTube SPA)
3. `https://www.youtube.com/watch?v=dQw4w9WgXcQ` (пряме відео)
---
## 2. Результати тестів
### 2.1. Перевірка медіа API (`canPlayType`, `HTMLVideoElement`, кодеки)
Спроба виклику `canPlayType` завершилася фатальною помилкою:
```
TypeError: document.createElement('video').canPlayType is not a function.
(In 'document.createElement('video').canPlayType(...)', 'document.createElement('video').canPlayType' is undefined)
```
- `HTMLVideoElement`: **відсутній** у global scope (`ReferenceError: Can't find variable: HTMLVideoElement`)
- `HTMLAudioElement`: **відсутній**
- `MediaSource` (MSE): **відсутній**
- `requestMediaKeySystemAccess` (EME / DRM / Widevine): **відсутній**
- `AudioContext` / Web Audio: відсутній
### 2.2. Завантаження головної сторінки YouTube (`https://www.youtube.com/`)
- **DOM:** завантажився частково (1224 елементи), контейнер `<ytd-app>` створений.
- **Помилки JS:** падіння скрипта `kevlar_base` на рядку 5444 через відсутність `HTMLVideoElement`:
```
ReferenceError: Can't find variable: HTMLVideoElement (kevlar_base_module:5444)
```
- **Відеокартки / прев'ю:** 0 знайдених рендерерів через збій у JS-ініціалізації YouTube.
- **Споживання RAM:** **~320 MB** (Single Process).
### 2.3. Завантаження сторінки з відео (`watch?v=dQw4w9WgXcQ`)
- **Заголовок сторінки:** розпізнано коректно:
`Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster) - YouTube`
- **Плеєр YouTube:** плеєр відображає системне повідомлення про несумісність:
> **«Воспроизвести видео в этом браузере нельзя. Подробнее…»**
- **Відтворення звуку:** **0 / відсутнє**. Звук не грає, потік не декодується.
- **Споживання RAM:** **441.8 MB** (для одного відкритого таба в одному процесі).
---
## 3. Порівняння: Ultralight vs WebView2
| Критерій | WebView2 (Chromium) | Ultralight v1.3.0 |
| :--- | :--- | :--- |
| **HTML5 Video / Audio** | Повноцінна підтримка | **Не працює** (`canPlayType is undefined`) |
| **YouTube HTML5 Player** | Працює ідеально | **«Не можна відтворити відео в цьому браузері»** |
| **DRM / Widevine** | Працює | **Не підтримується взагалі** |
| **Сучасні JS API** | Повна підтримка Blink | Падає на `HTMLVideoElement`, Trusted Types |
| **RAM (одна вкладка YouTube)** | ~350–450 MB (сумарно з renderer/GPU) | **441.8 MB** (один процес, навіть без відео) |
| **Виграш по RAM** | — | **0 MB** (споживання практично однакове) |
---
## 4. Висновок за правилом Go/No-Go (Крок 0)
Згідно з інструкцією `ULTRALIGHT_AGENT_GUIDE.md`:
> *«Якщо на кроці 3 YouTube взагалі не вантажиться або аудіо не програється — не переходити до Кроку 1. ...це не привід "трохи допрацювати", а сигнал, що для головного use case проєкту Ultralight як основний рушій не підходить.»*
### Вердикт: **NO-GO для заміни WebView2 на YouTube**
1. **Ultralight не здатен відтворювати YouTube усередині HTML:** рушій не має реалізації HTML5 Video/Audio кодеків, MSE та EME. YouTube видає плашку «Воспроизвести видео в этом браузере нельзя».
2. **Немає виграшу по RAM:** один процес Ultralight на сторінці YouTube зайняв **441.8 MB**, що відповідає або навіть перевищує оптимізований режим WebView2.
3. **Правильний шлях для проєкту:**
- Для веб-браузингу та навігації — **залишати WebView2**.
- Для ультралегкого звуку без навантаження на рендерер — використовувати вже створений **`NativeAudioSession` + `YtDlpExtractor`** (direct audio stream через WASAPI/MediaPlayer).
- Ultralight можна використовувати виключно для внутрішніх легких UI-віджетів/налаштувань (якщо виникне потреба у чистому локальному HTML-інтерфейсі).
-96
View File
@@ -1,96 +0,0 @@
# Промт для агента: FoxBrowser — легкий фоновий браузер
## Задум і чесна оцінка реалістичності
**Три цілі проєкту:**
1. Низьке споживання CPU/RAM порівняно зі звичайними браузерами.
2. Вікно завжди позаду всіх інших вікон (як "приліплений" віджет, а не звичайне вікно застосунку).
3. Не "роздувається" з часом, коли грає фонова музика/відео (напр. плейлист на YouTube з однією вкладкою).
4. (Другий етап) Вбудований VPN через WireGuard-тунель, без потреби в окремому клієнті.
**Важлива чесна засторога перед стартом:** писати власний рушій рендерингу HTML/CSS/JS (як Blink чи Gecko) — це багаторічна робота команд у сотні людей, нереалістично для одного проєкту. Правильний шлях — **не змагатися з Chromium рушієм**, а взяти вже готовий системний рушій (Microsoft Edge WebView2 на Windows) і виграти на рівні: відсутності власної копії Chromium в кожному інсталяторі (Electron тягне ~150+ МБ свого Chromium, WebView2 використовує вже встановлений у системі Edge Runtime), мінімалістичного "chrome" браузера без важкої екосистеми розширень, агресивного керування вкладками/пам'яттю, і унікальних UX-фіч (always-on-bottom, audio-only режим). Це дає реальний, чесний виграш у легкості — але базове споживання пам'яті на сторінку не буде на порядок меншим за сам Edge, бо рушій той самий.
## Рекомендований стек
- **Windows-first, C#/.NET 8 (WPF) + Microsoft.Web.WebView2** — обрано для консистентності з попереднім OCR-проєктом (той самий стек, ті самі патерни Win32-інтеропу для системних фіч), і тому що WebView2 — найлегший шлях отримати сучасний рушій без бандлингу власного Chromium.
- Альтернатива, якщо потрібна кросплатформність з нуля: Rust + Tauri v2 (теж використовує системний WebView, а не власний Chromium) — складніше по стеку, але менший бінарник і нативна швидкість shell-коду.
## Функціональні вимоги
### 1. Базовий shell браузера
- Мультивкладковість: кожна вкладка — окремий `CoreWebView2` контролер (WebView2 підтримує кілька `CoreWebView2Environment`/профілів у одному процесі-хості).
- Мінімалістичний UI: рядок вкладок, адресний рядок, кнопки назад/вперед/оновити — без важкої панелі закладок/історії з великою кількістю віджетів (легкість — і візуальна, і по ресурсах).
- Системний трей: згортання в трей замість закриття, іконка з контекстним меню (Show/Hide, Play/Pause якщо є активна медіа-вкладка, Exit).
### 2. Always-on-bottom (вікно завжди позаду)
- Реалізується через Win32 API, а не WPF-властивості: `SetWindowPos(hwnd, HWND_BOTTOM, ...)` замість звичного `HWND_TOPMOST`.
- **Ключова складність:** Windows постійно "підіймає" активне вікно наверх при фокусі/кліку, тому просто одноразовий `HWND_BOTTOM` не втримає вікно ззаду. Потрібен або: (а) хук на `WM_WINDOWPOSCHANGING`/`WM_ACTIVATE` для повторного примусового `SetWindowPos(HWND_BOTTOM)` щоразу, коли ОС намагається підняти вікно, або (б) `SetWinEventHook` на `EVENT_SYSTEM_FOREGROUND`, який відстежує зміну активного вікна і одразу опускає FoxBrowser назад вниз z-order. Задокументуй обраний підхід і протестуй, що вікно не "мигає" і не краде фокус у користувача під час гри музики.
- Клік по вкладці/елементу керування в самому FoxBrowser (напр. пауза музики) має працювати без переведення вікна на передній план — тобто вікно позаду, але клікабельне (`WS_EX_NOACTIVATE` для сценаріїв, де фокус не потрібен, або контрольоване тимчасове підняття тільки коли користувач свідомо клікає в вікно).
- Гаряча клавіша для "тимчасово підняти на передній план" (напр. коли треба щось вибрати в плеєрі), потім вікно саме повертається вниз.
### 3. Захист від "роздування" пам'яті з часом
- **Моніторинг**: фоновий таймер кожні N хвилин читає RAM/CPU процесу WebView2 (через `Process.WorkingSet64` для дочірніх процесів WebView2 — у них окремі `msedgewebview2.exe` процеси на renderer/GPU).
- **Watchdog для довгограючих вкладок**: якщо пам'ять конкретної вкладки перевищує поріг (наприклад, зросла вдвічі відносно початкового рівня після завантаження) — виконати "м'який" reload сторінки, попередньо зберігши стан програвання через JS injection (`ExecuteScriptAsync`): зчитати з `document` поточний час відео/треку, назву плейлиста/позицію, після reload — виконати JS, який перемотує на збережену позицію і продовжує відтворення. Користувач в ідеалі навіть не помітить короткого "заїкання" звуку.
- **Suspend неактивних вкладок**: вкладки, які не видно і не грають аудіо, вивантажувати (закривати `CoreWebView2Controller`, лишаючи тільки URL+заголовок+favicon у пам'яті), і відновлювати повний рендер лише при переключенні на них — аналогічно "sleeping tabs" в Edge/Chrome, тільки агресивніше за замовчуванням.
- **"Music Mode" (аудіо-only режим)** — головна фіча проти роздування на музичних плейлистах: для розпізнаних медіа-сайтів (YouTube, SoundCloud тощо) пропонувати режим, де відеодоріжка не рендериться взагалі (сторінка завантажується, але відеоелемент приховується/призупиняється у видимості, звук іде далі через Web Audio) — це прибирає найважчу частину навантаження (декодування й рендер відеокадрів), лишаючи тільки аудіодекодування, яке на порядки дешевше по CPU/GPU. Технічно: CSS/JS ін'єкція, яка ховає `<video>` елемент і зупиняє його рендер-цикл, не займаючи паузою сам плеєр (`display: none` на відео не завжди зупиняє декодування в Chromium — треба перевірити практично й, якщо потрібно, додатково `video.pause()` рендер-кадрів через `requestVideoFrameCallback` трюки або обмеження частоти кадрів).
### 4. Вбудований WireGuard VPN (другий етап)
- Не вимагати від користувача встановлювати окремий клієнт WireGuard. Варіанти реалізації, від простішого до "повністю вбудованого":
1. **Найпростіше:** якщо офіційний WireGuard for Windows встановлений, керувати тунелем через його CLI (`wireguard.exe /installtunnelservice config.conf`) з коду застосунку — швидко зробити, але вимагає зовнішньої залежності.
2. **Повністю вбудоване (рекомендовано для фінальної версії):** використати `wintun.dll` (офіційний легкий TUN-драйвер від творців WireGuard, розповсюджується вільно, не вимагає окремої інсталяції — просто кладеш dll поруч з .exe) у парі з **boringtun** (userspace-реалізація WireGuard-протоколу від Cloudflare на Rust, є FFI-бібліотека) або нативним P/Invoke до `wireguard-go`. Це дає тумблер "VPN: Увімкнено/Вимкнено" прямо в налаштуваннях FoxBrowser без жодних зовнішніх застосунків.
- UI: імпорт `.conf`-файлу конфігурації тунелю (стандартний формат WireGuard, який видають VPN-провайдери), список збережених профілів, індикатор стану підключення в тулбарі.
## Архітектура (орієнтовна структура)
```
/FoxBrowser
/Core
TabManager.cs // CoreWebView2 контролери, lifecycle вкладок
WindowStackManager.cs // Win32 always-on-bottom логіка
MemoryWatchdog.cs // моніторинг RAM/CPU, soft-reload довгограючих вкладок
TabSuspender.cs // вивантаження неактивних вкладок
MusicModeInjector.cs // CSS/JS ін'єкція для audio-only режиму
VpnManager.cs // обгортка над wintun/boringtun або wireguard.exe CLI
/UI
MainWindow.xaml(.cs)
TabBar.xaml(.cs)
TrayIconManager.cs
VpnSettingsWindow.xaml(.cs)
/Native
Win32Interop.cs // SetWindowPos, SetWinEventHook, P/Invoke сигнатури
App.xaml(.cs)
```
## Кроки реалізації для агента (фазами — не намагайся зробити все одразу)
**Фаза 1 — робочий shell:**
1. Ініціалізувати WPF-проєкт, додати `Microsoft.Web.WebView2` NuGet.
2. Реалізувати одну вкладку з WebView2, адресний рядок, навігацію.
3. Додати мультивкладковість (`TabManager`, кілька контролерів).
4. Системний трей + згортання замість закриття.
**Фаза 2 — always-on-bottom:**
5. Реалізувати `WindowStackManager` з `SetWinEventHook` на `EVENT_SYSTEM_FOREGROUND`, тестувати на різних сценаріях (Alt+Tab, клік по інших вікнах, повноекранні застосунки).
6. Гаряча клавіша для тимчасового підняття вікна.
**Фаза 3 — керування пам'яттю:**
7. `MemoryWatchdog` — логування споживання по вкладках, поки без автодій, просто щоб бачити цифри.
8. `TabSuspender` для неактивних вкладок.
9. Soft-reload з відновленням позиції відтворення для довгограючих вкладок.
**Фаза 4 — Music Mode:**
10. Детекція медіа-сайтів, UI-перемикач "Audio only", `MusicModeInjector`.
11. Виміряти реальний виграш по CPU/GPU до/після (профайлер Windows Performance Monitor або вбудований Task Manager по процесу `msedgewebview2.exe`) — це треба підтвердити цифрами, а не тільки відчуттями.
**Фаза 5 — VPN:**
12. Почати з найпростішого варіанту (керування зовнішнім `wireguard.exe`), переконатись, що весь флоу працює.
13. Якщо треба повна автономність без зовнішніх застосунків — інтегрувати wintun + boringtun.
## Нефункціональні вимоги
- Кожна фаза має бути окремим працюючим релізом, який можна потестувати — не розтягувати "великий бенг" на весь проєкт одразу.
- Вимірювати реальне споживання пам'яті/CPU до і після кожної оптимізації (Фаза 3-4) — легко обманути себе відчуттям "стало легше", коли насправді змінилось не так уже й багато.
- Задокументувати, які Windows-версії підтримуються (WebView2 Runtime вимагає Windows 10 1809+ або треба бандлити Evergreen-інсталятор для дуже старих систем).
---
Почни з Фази 1 (робочий shell з кількома вкладками і треєм) — саме на цьому етапі одразу стане видно, наскільки WebView2 справді легший за Chrome/Electron у твоєму конкретному сценарії використання, і чи варто взагалі рухатись далі за цим планом.