refactor(4): TabViewService extracts WebView2 lifecycle/wiring from MainWindow (god-class dissolved) - all 66 tests incl. tab E2E stay GREEN
This commit is contained in:
parent
e8ec1350b3
commit
af19dba8f4
2 changed files with 227 additions and 140 deletions
|
|
@ -30,7 +30,6 @@ public partial class MainWindow : Window
|
|||
// ---- 탭 아키텍처 (Phase A) ----
|
||||
private CoreWebView2Environment? _env;
|
||||
private TabManager _tabs = null!;
|
||||
private readonly Dictionary<TabItem, Task> _tabInit = new();
|
||||
internal TabManager Tabs => _tabs;
|
||||
internal Wv2.WebView2? ActiveView => _tabs?.ActiveTab?.View;
|
||||
private CoreWebView2? Cv => _tabs?.ActiveTab?.Core;
|
||||
|
|
@ -162,7 +161,21 @@ public partial class MainWindow : Window
|
|||
}
|
||||
catch (Exception ex) { Log($"WebView2 초기화 실패: {ex.Message}"); return; }
|
||||
|
||||
_tabs = new TabManager(CreateTabItem, AttachTabView);
|
||||
_views = new Services.TabViewService(_env!, _config, TabHost, _zoomStore, Dispatcher)
|
||||
{
|
||||
NewTabProvider = () => Task.FromResult(_tabs.NewTab(activate: false)),
|
||||
};
|
||||
_views.TabCoreReady += (_, cv) => cv.WebResourceResponseReceived += OnResponseReceived; // 미디어 감지
|
||||
_views.DownloadStarting += OnDownloadStarting;
|
||||
_views.UrlChanged += OnUrlChanged;
|
||||
_views.TitleChanged += OnTitleChanged;
|
||||
_views.HistoryChanged += UpdateNavState;
|
||||
_views.FindStateChanged += tab =>
|
||||
{
|
||||
if (tab.FindSession != null) FindStatus.Text = tab.FindSession.StatusText;
|
||||
};
|
||||
|
||||
_tabs = new TabManager(() => new TabItem(), _views.AttachView);
|
||||
_tabs.ActiveChanged += OnActiveTabChanged;
|
||||
_tabs.TabRemoved += OnTabRemoved;
|
||||
TabList.ItemsSource = _tabs.Tabs;
|
||||
|
|
@ -177,147 +190,45 @@ public partial class MainWindow : Window
|
|||
UpdateNavState();
|
||||
}
|
||||
|
||||
private TabItem CreateTabItem() => new();
|
||||
/// <summary>탭별 WebView2 생명주기·배선 전담 서비스(재구성 ④).</summary>
|
||||
private Services.TabViewService? _views;
|
||||
|
||||
/// <summary>탭에 WebView2 뷰를 붙이고 비동기 초기화를 시작한다. (UI 스레드)</summary>
|
||||
private void AttachTabView(TabItem tab)
|
||||
private void OnUrlChanged(TabItem tab, string url)
|
||||
{
|
||||
var view = new Wv2.WebView2 { DefaultBackgroundColor = System.Drawing.Color.Black };
|
||||
tab.View = view;
|
||||
TabHost.Children.Add(view);
|
||||
view.Visibility = tab.IsActive ? Visibility.Visible : Visibility.Collapsed;
|
||||
_tabInit[tab] = InitTabCoreAsync(tab);
|
||||
if (tab.PendingHistoryUrl != null) RecordHistory(tab.PendingHistoryUrl, tab.PendingHistoryUrl);
|
||||
if (tab.IsActive && !AddressBox.IsKeyboardFocused) AddressBox.Text = url;
|
||||
tab.PendingHistoryUrl = url.StartsWith("about:", StringComparison.OrdinalIgnoreCase) ? null : url;
|
||||
DetectSocial(tab, url);
|
||||
}
|
||||
|
||||
/// <summary>탭의 CoreWebView2 초기화 + 탭별 이벤트 배선 + 초기 URL 탐색.</summary>
|
||||
private async Task InitTabCoreAsync(TabItem tab)
|
||||
private void OnTitleChanged(TabItem tab, string title)
|
||||
{
|
||||
try
|
||||
tab.Title = string.IsNullOrWhiteSpace(title) ? (tab.Url.StartsWith("http") ? tab.Url : "새 탭") : title;
|
||||
if (tab.PendingHistoryUrl != null)
|
||||
{
|
||||
var options = _env!.CreateCoreWebView2ControllerOptions();
|
||||
if (_config.PrivateMode) options.IsInPrivateModeEnabled = true; // InPrivate: 디스크에 아무것도 저장 안 함
|
||||
await tab.View!.EnsureCoreWebView2Async(_env, options);
|
||||
RecordHistory(string.IsNullOrWhiteSpace(title) ? tab.PendingHistoryUrl : title, tab.PendingHistoryUrl);
|
||||
tab.PendingHistoryUrl = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
// 소셜 카드 제목을 페이지 타이틀로 갱신(출력 파일명 포함, 시작 전일 때)
|
||||
if (tab.PendingSocialUrl != null && !string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
Log($"탭 초기화 실패: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
var cv = tab.Core!;
|
||||
cv.Settings.AreDevToolsEnabled = true;
|
||||
|
||||
// T1-3 탭별 검색 세션 (네이티브 Find API)
|
||||
tab.FindSession = new FindSession(new WebView2FindApi(cv, _env!));
|
||||
tab.FindSession.StateChanged += () => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
if (tab.IsActive && tab.FindSession != null) FindStatus.Text = tab.FindSession.StatusText;
|
||||
}));
|
||||
|
||||
// 탭별 네비게이션 상태
|
||||
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 += (_, _) => UpdateNavState();
|
||||
|
||||
// 소셜/주소창/기록: 탭별 상태로 관리
|
||||
cv.SourceChanged += (_, _) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
var url = cv.Source ?? "";
|
||||
tab.Url = url;
|
||||
if (tab.PendingHistoryUrl != null) RecordHistory(tab.PendingHistoryUrl, tab.PendingHistoryUrl);
|
||||
if (tab.IsActive && !AddressBox.IsKeyboardFocused) AddressBox.Text = url;
|
||||
tab.PendingHistoryUrl = url.StartsWith("about:", StringComparison.OrdinalIgnoreCase) ? null : url;
|
||||
DetectSocial(tab, url);
|
||||
}));
|
||||
cv.DocumentTitleChanged += (_, _) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
var title = cv.DocumentTitle ?? "";
|
||||
tab.Title = string.IsNullOrWhiteSpace(title) ? (tab.Url.StartsWith("http") ? tab.Url : "새 탭") : title;
|
||||
if (tab.PendingHistoryUrl != null)
|
||||
{
|
||||
RecordHistory(string.IsNullOrWhiteSpace(title) ? tab.PendingHistoryUrl : title, tab.PendingHistoryUrl);
|
||||
tab.PendingHistoryUrl = null;
|
||||
}
|
||||
// 소셜 카드 제목을 페이지 타이틀로 갱신(출력 파일명 포함, 시작 전일 때)
|
||||
if (tab.PendingSocialUrl != null && !string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
_manager.RenameSocial(new Uri(tab.PendingSocialUrl), title);
|
||||
tab.PendingSocialUrl = null;
|
||||
}
|
||||
}));
|
||||
|
||||
// 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 { } // 구현체/타이밍 이슈 대비
|
||||
|
||||
// 미디어 감지(탭 무관 전역)
|
||||
cv.WebResourceResponseReceived += OnResponseReceived;
|
||||
|
||||
// Phase A: 팝업/새창 → 새 탭 (deferral 필수 — 탭 생성이 비동기)
|
||||
cv.NewWindowRequested += async (_, ne) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var deferral = ne.GetDeferral();
|
||||
try
|
||||
{
|
||||
var newTab = _tabs.NewTab(activate: false); // URL 없이 생성 → 팝업 내용이 새 창으로 흘러들어옴
|
||||
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 { }
|
||||
}
|
||||
};
|
||||
|
||||
// #13 페이지 내 다운로드(a[download])를 저장 폴더로 라우팅
|
||||
cv.DownloadStarting += (_, de) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var folder = Path.GetFullPath(_config.OutputFolder);
|
||||
Directory.CreateDirectory(folder);
|
||||
var fn = Path.GetFileName(de.ResultFilePath);
|
||||
if (string.IsNullOrWhiteSpace(fn)) fn = $"download_{DateTime.Now:yyyyMMdd_HHmmss}";
|
||||
de.ResultFilePath = MakeUniqueDownloadName(folder, fn);
|
||||
_manager.LogPanel($"📥 페이지 다운로드 → 저장 폴더: {fn}");
|
||||
}
|
||||
catch { }
|
||||
};
|
||||
|
||||
// 초기 URL (세션 복원/빈 탭 제외)
|
||||
if (!string.IsNullOrEmpty(tab.Url) && (cv.Source == null || cv.Source.Length == 0))
|
||||
{
|
||||
try { cv.Navigate(tab.Url); } catch { }
|
||||
_manager.RenameSocial(new Uri(tab.PendingSocialUrl), title);
|
||||
tab.PendingSocialUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>탭의 CoreWebView2 초기화 완료 대기 (뉴윈도우 deferral 용).</summary>
|
||||
private async Task WaitTabReady(TabItem tab)
|
||||
private void OnDownloadStarting(CoreWebView2 cv, CoreWebView2DownloadStartingEventArgs de)
|
||||
{
|
||||
if (tab.Core != null) return;
|
||||
if (_tabInit.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);
|
||||
try
|
||||
{
|
||||
var folder = Path.GetFullPath(_config.OutputFolder);
|
||||
Directory.CreateDirectory(folder);
|
||||
var fn = Path.GetFileName(de.ResultFilePath);
|
||||
if (string.IsNullOrWhiteSpace(fn)) fn = $"download_{DateTime.Now:yyyyMMdd_HHmmss}";
|
||||
de.ResultFilePath = MakeUniqueDownloadName(folder, fn);
|
||||
_manager.LogPanel($"📥 페이지 다운로드 → 저장 폴더: {fn}");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void OnActiveTabChanged(TabItem tab)
|
||||
|
|
@ -328,15 +239,7 @@ public partial class MainWindow : Window
|
|||
UpdateNavState();
|
||||
}
|
||||
|
||||
private void OnTabRemoved(TabItem tab)
|
||||
{
|
||||
if (tab.View != null)
|
||||
{
|
||||
TabHost.Children.Remove(tab.View);
|
||||
tab.View = null; // 재오픈 시 새 뷰로 재생성(ReopenClosedTab → AttachTabView)
|
||||
}
|
||||
_tabInit.Remove(tab);
|
||||
}
|
||||
private void OnTabRemoved(TabItem tab) => _views?.RemoveView(tab);
|
||||
|
||||
private void UpdateNavState()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue