T3-11/14/15 + M2 잔여: 권한 정책 UI(PermissionGate/PermissionRequested 다이얼로그+SavesInProfile), 추적 방지(TrackerBlocklist/WebResourceRequested 차단+실드 배지·팝업), 주소창 진행 바+파라미터 제거 복사(UrlCleaner/NavProgressTracker), U5 완료 알림(Mobile.Core 감지기+Plugin.LocalNotification), 재생중 ongoing 미디어 알림, 썸네일/해상도 API(Thumbnailer 사이드카 캐시+/thumb)+라이브러리 그리드, 외부망 안내(ConnectionTroubleshoot). 147 tests GREEN

This commit is contained in:
Yun Chan 2026-08-15 18:11:49 +09:00
parent 27c91c04dd
commit 46c6c321f2
26 changed files with 1058 additions and 30 deletions

View file

@ -55,6 +55,27 @@ public sealed class TabViewService
/// <summary>T2-4 리더 내 낭독(TTS) 요청 — 리더 본문 텍스트.</summary>
public event Action<CoreWebView2, string>? SpeakRequested;
/// <summary>T3-15 주소창 진행 바: 네비게이션 시작(전 탭 집계).</summary>
public event Action? NavigationStarted;
/// <summary>T3-15 주소창 진행 바: 네비게이션 완료(전 탭 집계).</summary>
public event Action? NavigationCompleted;
/// <summary>T3-11 사이트별 권한 정책(출처×종류 기억).</summary>
public PermissionGate Permissions { get; set; } = new();
/// <summary>T3-11 Ask 판정 시 호스트 다이얼로그 — State/Handled/SavesInProfile 설정은 호스트 책임.</summary>
public event Action<CoreWebView2, CoreWebView2PermissionRequestedEventArgs>? PermissionPrompt;
/// <summary>T3-14 추적 방지 블랙리스트(교체 주입 가능).</summary>
public TrackerBlocklist TrackerBlocklist { get; set; } = new();
/// <summary>T3-14 차단 카운터(실드 배지·팝업 데이터).</summary>
public TrackerCounter TrackerCounter { get; } = new();
/// <summary>T3-14 차단 발생 알림(배지 갱신).</summary>
public event Action? TrackerBlocked;
/// <summary>T3-3 InPrivate 탭: 다음에 만들어지는 탭에 적용(1회성).</summary>
public bool NextTabIsPrivate { get; set; }
@ -166,10 +187,11 @@ public sealed class TabViewService
}));
// 네비게이션 상태
cv.NavigationStarting += (_, _) => _dispatcher.BeginInvoke(new Action(() => tab.IsLoading = true));
cv.NavigationStarting += (_, _) => _dispatcher.BeginInvoke(new Action(() => { tab.IsLoading = true; NavigationStarted?.Invoke(); }));
cv.NavigationCompleted += (_, _) => _dispatcher.BeginInvoke(new Action(() =>
{
tab.IsLoading = false;
NavigationCompleted?.Invoke();
try { if (tab.View != null) tab.View.ZoomFactor = _zoomStore.Get(cv.Source); } catch { } // T1-7 사이트별 줌
try // T2-7 부스트(강제 다크/커스텀 CSS) 적용
{
@ -180,6 +202,37 @@ public sealed class TabViewService
}));
cv.HistoryChanged += (_, _) => HistoryChanged?.Invoke();
// T3-11 사이트별 권한: 정책 Ask 만 프롬프트, Allow/Deny 는 즉시 응답
cv.PermissionRequested += (_, e) =>
{
var kind = e.PermissionKind.ToString();
var origin = OriginOf(e.Uri);
switch (Permissions.Decide(kind, origin))
{
case PermissionVerdict.Allow:
e.State = CoreWebView2PermissionState.Allow; e.Handled = true;
break;
case PermissionVerdict.Deny:
e.State = CoreWebView2PermissionState.Deny; e.Handled = true;
break;
default:
PermissionPrompt?.Invoke(cv, e); // 호스트 다이얼로그가 State/Handled 설정
break;
}
};
// T3-14 추적 방지: 블랙리스트 요청 차단 + 카운터
cv.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All);
cv.WebResourceRequested += (_, e) =>
{
Uri u;
try { u = new Uri(e.Request.Uri); } catch { return; }
if (!TrackerBlocklist.Matches(u)) return;
try { e.Response = _env.CreateWebResourceResponse(null, 204, "Blocked", ""); } catch { }
TrackerCounter.Record(e.Request.Uri);
_dispatcher.BeginInvoke(new Action(() => TrackerBlocked?.Invoke()));
};
cv.SourceChanged += (_, _) => _dispatcher.BeginInvoke(new Action(() =>
{
var url = cv.Source ?? "";
@ -250,4 +303,12 @@ public sealed class TabViewService
/// <summary>활성 탭 검색 상태 표시 요청(FindStatus 텍스트 갱신 — 호스트가 구독).</summary>
public event Action<TabItem>? FindStateChanged;
/// <summary>권한 기준 출처(scheme://host:port) 추출.</summary>
public static string OriginOf(string? uri)
{
if (string.IsNullOrEmpty(uri)) return "";
try { var u = new Uri(uri); return u.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped); }
catch { return uri; }
}
}