126 lines
4 KiB
C#
126 lines
4 KiB
C#
using Microsoft.Web.WebView2.Core;
|
|
|
|
namespace VideoDownloader.App.Browser;
|
|
|
|
/// <summary>네이티브 Find API 의 최소 추상화(단위 테스트용 fake 교체 가능).</summary>
|
|
public interface IFindApi
|
|
{
|
|
Task StartAsync(string term, bool caseSensitive, bool matchWord, bool highlightAll);
|
|
void FindNext();
|
|
void FindPrevious();
|
|
void Stop();
|
|
event EventHandler<int>? MatchCountChanged;
|
|
event EventHandler<int>? ActiveMatchIndexChanged;
|
|
}
|
|
|
|
/// <summary>WebView2 네이티브 Find(CoreWebView2.Find) 를 IFindApi 로 감싼 어댑터.
|
|
/// 옵션 객체는 public ctor 가 없어 CoreWebView2Environment.CreateFindOptions() 로 생성.</summary>
|
|
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<int>? MatchCountChanged
|
|
{
|
|
add => _cv.Find.MatchCountChanged += (s, _) => value?.Invoke(s, _cv.Find.MatchCount);
|
|
remove { }
|
|
}
|
|
|
|
public event EventHandler<int>? ActiveMatchIndexChanged
|
|
{
|
|
add => _cv.Find.ActiveMatchIndexChanged += (s, _) => value?.Invoke(s, _cv.Find.ActiveMatchIndex);
|
|
remove { }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 페이지 내 검색 세션 상태 머신(T1-3). 용어·매치 수·활성 인덱스를 유지하고
|
|
/// UI(검색 바)가 표시할 "3 / 17" 텍스트를 제공한다.
|
|
/// </summary>
|
|
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();
|
|
}
|