using System.Windows; using System.Windows.Controls; using System.Windows.Threading; using Microsoft.Web.WebView2.Core; using VideoDownloader.Browser; using VideoDownloader.Browser.Tabs; using VideoDownloader.Core.Config; using Wv2 = Microsoft.Web.WebView2.Wpf; using TabItem = VideoDownloader.Browser.Tabs.TabItem; namespace VideoDownloader.App.Services; /// /// 탭별 WebView2 생명주기·이벤트 배선 전담(재구성 ④ — MainWindow 갓-클래스 해체). /// 일반 브라우저 동작(제목/URL/로딩/오디오/검색세션/줌 적용/초기 탐색/뉴윈도우→탭)은 /// 이 서비스가, 앱 고유 기능(미디어 감지/다운로드 라우팅/소셜/기록/주소창 반영)은 /// 이벤트 훅으로 호스트(MainWindow)가 주입한다. /// public sealed class TabViewService { private readonly CoreWebView2Environment _env; private readonly AppConfig _config; private readonly Panel _host; private readonly SiteZoomStore _zoomStore; private readonly Dispatcher _dispatcher; private readonly Dictionary _init = new(); /// 뉴윈도우(팝업) 요청 시 새 탭을 만들어 반환하는 공급자(호스트가 TabManager 에 연결). public Func>? NewTabProvider { get; set; } /// 탭 CoreWebView2 초기화 완료(호스트가 감지 등 탭별 구독 부착). public event Action? TabCoreReady; /// 페이지 내 a[download] 등 다운로드 시작(호스트가 저장 폴더로 라우팅). public event Action? DownloadStarting; /// URL 확정(SourceChanged). 인자: (탭, url). public event Action? UrlChanged; /// 문서 타이틀 확정(DocumentTitleChanged). 인자: (탭, title). public event Action? TitleChanged; /// 탭 뒤로/앞으로 가능 상태 변화(네비 버튼 갱신용). public event Action? HistoryChanged; public TabViewService(CoreWebView2Environment env, AppConfig config, Panel host, SiteZoomStore zoomStore, Dispatcher dispatcher) { _env = env; _config = config; _host = host; _zoomStore = zoomStore; _dispatcher = dispatcher; } public TabItem CreateTabItem() => new(); /// 탭에 WebView2 뷰를 붙이고 비동기 초기화를 시작한다. (UI 스레드) public void AttachView(TabItem tab) { var view = new Wv2.WebView2 { DefaultBackgroundColor = System.Drawing.Color.Black }; tab.View = view; _host.Children.Add(view); view.Visibility = tab.IsActive ? Visibility.Visible : Visibility.Collapsed; _init[tab] = InitCoreAsync(tab); } /// 탭의 CoreWebView2 초기화 완료 대기 (뉴윈도우 deferral 용). public async Task WaitTabReady(TabItem tab) { if (tab.Core != null) return; if (_init.TryGetValue(tab, out var t)) { try { await t; } catch { } return; } var sw = System.Diagnostics.Stopwatch.StartNew(); while (tab.Core == null && sw.ElapsedMilliseconds < 15000) await Task.Delay(50); } /// 탭 제거 시 뷰를 호스트에서 떼고 재오픈 가능 상태로(null) 만든다. public void RemoveView(TabItem tab) { if (tab.View != null) { _host.Children.Remove(tab.View); tab.View = null; // 재오픈 시 새 뷰로 재생성(ReopenClosedTab → AttachView) } _init.Remove(tab); } private async Task InitCoreAsync(TabItem tab) { try { var options = _env.CreateCoreWebView2ControllerOptions(); if (_config.PrivateMode) options.IsInPrivateModeEnabled = true; // InPrivate: 디스크에 아무것도 저장 안 함 await tab.View!.EnsureCoreWebView2Async(_env, options); } catch (Exception) { return; // 초기화 실패 탭 — 호스트가 로그 표시는 TitleChanged 로 알림 없이 둔다(기존 동작) } var cv = tab.Core!; cv.Settings.AreDevToolsEnabled = true; // 탭별 검색 세션 (네이티브 Find API) tab.FindSession = new FindSession(new WebView2FindApi(cv, _env)); tab.FindSession.StateChanged += () => _dispatcher.BeginInvoke(new Action(() => { if (tab.IsActive && tab.FindSession != null) FindStateChanged?.Invoke(tab); })); // 네비게이션 상태 cv.NavigationStarting += (_, _) => _dispatcher.BeginInvoke(new Action(() => tab.IsLoading = true)); cv.NavigationCompleted += (_, _) => _dispatcher.BeginInvoke(new Action(() => { tab.IsLoading = false; try { if (tab.View != null) tab.View.ZoomFactor = _zoomStore.Get(cv.Source); } catch { } // T1-7 사이트별 줌 })); cv.HistoryChanged += (_, _) => HistoryChanged?.Invoke(); cv.SourceChanged += (_, _) => _dispatcher.BeginInvoke(new Action(() => { var url = cv.Source ?? ""; tab.Url = url; UrlChanged?.Invoke(tab, url); })); cv.DocumentTitleChanged += (_, _) => _dispatcher.BeginInvoke(new Action(() => { TitleChanged?.Invoke(tab, cv.DocumentTitle ?? ""); })); // T1-8 오디오 표시/뮤트 try { cv.IsDocumentPlayingAudioChanged += (_, _) => _dispatcher.BeginInvoke(new Action(() => { try { tab.IsAudioPlaying = cv.IsDocumentPlayingAudio; } catch { } })); cv.IsMutedChanged += (_, _) => _dispatcher.BeginInvoke(new Action(() => { try { tab.IsMuted = cv.IsMuted; } catch { } })); } catch { } // 뉴윈도우(팝업) → 새 탭 (deferral 필수 — 탭 생성이 비동기) cv.NewWindowRequested += async (_, ne) => { try { var deferral = ne.GetDeferral(); try { var newTab = NewTabProvider != null ? await NewTabProvider() : null; if (newTab != null) { await WaitTabReady(newTab); ne.NewWindow = newTab.Core; } ne.Handled = true; } finally { deferral.Complete(); } } catch { try { ne.Handled = true; if (ne.Uri != null) cv.Navigate(ne.Uri); } catch { } } }; // 페이지 내 다운로드 → 호스트 훅 cv.DownloadStarting += (_, de) => DownloadStarting?.Invoke(cv, de); // 초기 URL (세션 복원) if (!string.IsNullOrEmpty(tab.Url) && string.IsNullOrEmpty(cv.Source)) { try { cv.Navigate(tab.Url); } catch { } } TabCoreReady?.Invoke(tab, cv); } /// 활성 탭 검색 상태 표시 요청(FindStatus 텍스트 갱신 — 호스트가 구독). public event Action? FindStateChanged; }