using Microsoft.Web.WebView2.Core; namespace VideoDownloader.Browser; /// 네이티브 Find API 의 최소 추상화(단위 테스트용 fake 교체 가능). public interface IFindApi { Task StartAsync(string term, bool caseSensitive, bool matchWord, bool highlightAll); void FindNext(); void FindPrevious(); void Stop(); event EventHandler? MatchCountChanged; event EventHandler? ActiveMatchIndexChanged; } /// WebView2 네이티브 Find(CoreWebView2.Find) 를 IFindApi 로 감싼 어댑터. /// 옵션 객체는 public ctor 가 없어 CoreWebView2Environment.CreateFindOptions() 로 생성. public sealed class WebView2FindApi : IFindApi { private readonly CoreWebView2 _cv; private readonly CoreWebView2Environment _env; public WebView2FindApi(CoreWebView2 cv, CoreWebView2Environment env) { _cv = cv; _env = env; } public Task StartAsync(string term, bool caseSensitive, bool matchWord, bool highlightAll) { var o = _env.CreateFindOptions(); o.FindTerm = term; o.IsCaseSensitive = caseSensitive; o.ShouldMatchWord = matchWord; o.ShouldHighlightAllMatches = highlightAll; o.SuppressDefaultFindDialog = true; // 자체 WPF 검색 바 사용 return _cv.Find.StartAsync(o); } public void FindNext() => _cv.Find.FindNext(); public void FindPrevious() => _cv.Find.FindPrevious(); public void Stop() => _cv.Find.Stop(); // 이벤트 인자가 object 로 전달되므로 매치 수는 Find 프로퍼티에서 직접 읽는다. public event EventHandler? MatchCountChanged { add => _cv.Find.MatchCountChanged += (s, _) => value?.Invoke(s, _cv.Find.MatchCount); remove { } } public event EventHandler? ActiveMatchIndexChanged { add => _cv.Find.ActiveMatchIndexChanged += (s, _) => value?.Invoke(s, _cv.Find.ActiveMatchIndex); remove { } } } /// /// 페이지 내 검색 세션 상태 머신(T1-3). 용어·매치 수·활성 인덱스를 유지하고 /// UI(검색 바)가 표시할 "3 / 17" 텍스트를 제공한다. /// public sealed class FindSession : IDisposable { private readonly IFindApi _api; public string Term { get; private set; } = string.Empty; public int MatchCount { get; private set; } public int ActiveMatchIndex { get; private set; } public bool IsCaseSensitive { get; private set; } public bool MatchWholeWord { get; private set; } public event Action? StateChanged; public FindSession(IFindApi api) { _api = api; _api.MatchCountChanged += (_, n) => { MatchCount = n; StateChanged?.Invoke(); }; _api.ActiveMatchIndexChanged += (_, n) => { ActiveMatchIndex = n; StateChanged?.Invoke(); }; } public string StatusText { get { if (MatchCount <= 0) return "결과 없음"; var oneBased = ActiveMatchIndex >= 0 ? ActiveMatchIndex + 1 : 0; return $"{oneBased} / {MatchCount}"; } } public async Task StartAsync(string term) { Term = term ?? ""; if (string.IsNullOrWhiteSpace(term)) { Close(); return; } MatchCount = 0; ActiveMatchIndex = -1; await _api.StartAsync(Term, IsCaseSensitive, MatchWholeWord, highlightAll: true); StateChanged?.Invoke(); } public void SetOptions(bool caseSensitive, bool matchWholeWord) { IsCaseSensitive = caseSensitive; MatchWholeWord = matchWholeWord; if (!string.IsNullOrWhiteSpace(Term)) _ = StartAsync(Term); // 옵션 변경은 재시작 필요(네이티브 제약) } public void Next() => _api.FindNext(); public void Previous() => _api.FindPrevious(); public void Close() { _api.Stop(); Term = string.Empty; MatchCount = 0; ActiveMatchIndex = -1; StateChanged?.Invoke(); } public void Dispose() => Close(); }