Phase C (T2-1..T3-10): command palette, reader mode (real Readability.js, E2E), boost force-dark, screenshot, split view, vertical tabs, Glance (Alt+click), tab groups (6 colors), workspaces, speed dial, InPrivate tabs, quick find, source view, print, PiP, TTS (94 tests GREEN)
This commit is contained in:
parent
0caa1111e7
commit
406957088f
14 changed files with 3496 additions and 7 deletions
|
|
@ -46,6 +46,18 @@ public sealed class TabViewService
|
|||
/// <summary>우클릭 메뉴 요청 — 호스트가 WPF ContextMenu 로 재구성해 표시(Handled=true).</summary>
|
||||
public event Action<CoreWebView2, CoreWebView2ContextMenuRequestedEventArgs>? ContextMenuBuild;
|
||||
|
||||
/// <summary>T2-7 부스트: URL → 주입할 JS(강제 다크/커스텀 CSS). 빈 문자열이면 미적용.</summary>
|
||||
public Func<string?, string>? BoostScriptProvider { get; set; }
|
||||
|
||||
/// <summary>T2-6 Glance: Alt+클릭한 링크 URL 요청.</summary>
|
||||
public event Action<CoreWebView2, string>? GlanceRequested;
|
||||
|
||||
/// <summary>T2-4 리더 내 낭독(TTS) 요청 — 리더 본문 텍스트.</summary>
|
||||
public event Action<CoreWebView2, string>? SpeakRequested;
|
||||
|
||||
/// <summary>T3-3 InPrivate 탭: 다음에 만들어지는 탭에 적용(1회성).</summary>
|
||||
public bool NextTabIsPrivate { get; set; }
|
||||
|
||||
public TabViewService(CoreWebView2Environment env, AppConfig config, Panel host,
|
||||
SiteZoomStore zoomStore, Dispatcher dispatcher)
|
||||
{
|
||||
|
|
@ -68,6 +80,16 @@ public sealed class TabViewService
|
|||
_init[tab] = InitCoreAsync(tab);
|
||||
}
|
||||
|
||||
/// <summary>부스트 즉시 재적용(토글 직후). 비동기 fire-and-forget.</summary>
|
||||
public void ApplyBoostNow(TabItem tab)
|
||||
{
|
||||
var cv = tab.Core;
|
||||
if (cv == null) return;
|
||||
var js = BoostScriptProvider?.Invoke(cv.Source) ?? "";
|
||||
if (js.Length > 0 && !tab.IsReader) _ = cv.ExecuteScriptAsync(js);
|
||||
else if (js.Length == 0) _ = cv.ExecuteScriptAsync("(function(){try{document.getElementById('vd-boost')?.remove();}catch(e){}})();");
|
||||
}
|
||||
|
||||
/// <summary>탭의 CoreWebView2 초기화 완료 대기 (뉴윈도우 deferral 용).</summary>
|
||||
public async Task WaitTabReady(TabItem tab)
|
||||
{
|
||||
|
|
@ -94,7 +116,9 @@ public sealed class TabViewService
|
|||
try
|
||||
{
|
||||
var options = _env.CreateCoreWebView2ControllerOptions();
|
||||
if (_config.PrivateMode) options.IsInPrivateModeEnabled = true; // InPrivate: 디스크에 아무것도 저장 안 함
|
||||
if (_config.PrivateMode || tab.IsPrivate || NextTabIsPrivate) options.IsInPrivateModeEnabled = true; // InPrivate: 디스크에 아무것도 저장 안 함
|
||||
if (!tab.IsPrivate && NextTabIsPrivate) tab.IsPrivate = true;
|
||||
NextTabIsPrivate = false;
|
||||
await tab.View!.EnsureCoreWebView2Async(_env, options);
|
||||
}
|
||||
catch (Exception)
|
||||
|
|
@ -105,6 +129,35 @@ public sealed class TabViewService
|
|||
var cv = tab.Core!;
|
||||
cv.Settings.AreDevToolsEnabled = true;
|
||||
|
||||
// T2-6 Glance: Alt+클릭 → 링크를 postMessage 로 전달 (캡처 단계에서 preventDefault)
|
||||
const string glanceScript = """
|
||||
(function(){ try { document.addEventListener('click', function(e){
|
||||
if (e.altKey) { var a = e.target && e.target.closest ? e.target.closest('a') : null;
|
||||
if (a && a.href) { e.preventDefault(); e.stopPropagation(); window.chrome.webview.postMessage({ vdGlance: a.href }); } }
|
||||
}, true); } catch(err){} })();
|
||||
""";
|
||||
try { await cv.AddScriptToExecuteOnDocumentCreatedAsync(glanceScript); } catch { }
|
||||
|
||||
// WebMessage 라우팅 (Glance / 리더 낭독)
|
||||
try
|
||||
{
|
||||
cv.WebMessageReceived += (_, we) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var msg = we.WebMessageAsJson;
|
||||
if (string.IsNullOrEmpty(msg)) return;
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(msg);
|
||||
if (doc.RootElement.TryGetProperty("vdGlance", out var g) && g.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
GlanceRequested?.Invoke(cv, g.GetString() ?? "");
|
||||
if (doc.RootElement.TryGetProperty("vdSpeak", out var sp) && sp.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
SpeakRequested?.Invoke(cv, sp.GetString() ?? "");
|
||||
}
|
||||
catch { }
|
||||
};
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 탭별 검색 세션 (네이티브 Find API)
|
||||
tab.FindSession = new FindSession(new WebView2FindApi(cv, _env));
|
||||
tab.FindSession.StateChanged += () => _dispatcher.BeginInvoke(new Action(() =>
|
||||
|
|
@ -118,6 +171,12 @@ public sealed class TabViewService
|
|||
{
|
||||
tab.IsLoading = false;
|
||||
try { if (tab.View != null) tab.View.ZoomFactor = _zoomStore.Get(cv.Source); } catch { } // T1-7 사이트별 줌
|
||||
try // T2-7 부스트(강제 다크/커스텀 CSS) 적용
|
||||
{
|
||||
var boostJs = BoostScriptProvider?.Invoke(cv.Source) ?? "";
|
||||
if (boostJs.Length > 0 && !tab.IsReader) _ = cv.ExecuteScriptAsync(boostJs);
|
||||
}
|
||||
catch { }
|
||||
}));
|
||||
cv.HistoryChanged += (_, _) => HistoryChanged?.Invoke();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue