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:
parent
27c91c04dd
commit
46c6c321f2
26 changed files with 1058 additions and 30 deletions
|
|
@ -191,6 +191,14 @@
|
|||
<Button Name="HistoryBtn" Style="{StaticResource IconButton}" Content="" ToolTip="방문 기록" Margin="0,0,2,0" Click="ToggleHistory"/>
|
||||
<Button Name="TabSearchBtn" Style="{StaticResource IconButton}" Content="" ToolTip="탭 검색 (Ctrl+Shift+A)" Margin="0,0,2,0" Click="ToggleTabSearch"/>
|
||||
<Button Name="DownloadsBtn" Style="{StaticResource IconButton}" Content="" ToolTip="다운로드 (Ctrl+J)" Margin="0,0,2,0" Click="ToggleDownloads"/>
|
||||
<!-- T3-14 추적 방지 실드 -->
|
||||
<Grid Margin="0,0,2,0">
|
||||
<Button Name="ShieldBtn" Style="{StaticResource IconButton}" Content="" ToolTip="추적 방지" Click="Shield_Click"/>
|
||||
<Border Name="ShieldBadge" CornerRadius="8" Background="#FF4CAF50" Padding="5,1"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,2,2,0" Visibility="Collapsed">
|
||||
<TextBlock Name="ShieldBadgeText" Text="" FontSize="9" Foreground="White"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
<Button Name="PlaylistBtn" Style="{StaticResource IconButton}" Content="" ToolTip="미디어 재생목록" Margin="0,0,2,0" Click="TogglePlaylistPanel"/>
|
||||
<Button Style="{StaticResource IconButton}" Content="" ToolTip="저장 폴더 열기" Margin="0,0,2,0" Click="OpenFolder_Click"/>
|
||||
<Button Name="LinkBtn" Style="{StaticResource IconButton}" Content="" ToolTip="링크로 받기 (YouTube·Instagram·X)" Margin="0,0,2,0" Click="ToggleLink"/>
|
||||
|
|
@ -208,10 +216,25 @@
|
|||
<!-- 주소창 -->
|
||||
<Border Background="{StaticResource GlassBg}" BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="1" CornerRadius="8" Height="32" VerticalAlignment="Center">
|
||||
<TextBox Name="AddressBox" Style="{StaticResource DarkTextBox}"
|
||||
PreviewKeyDown="AddressBox_PreviewKeyDown" TextChanged="AddressBox_TextChanged"
|
||||
BorderThickness="0" Background="Transparent"
|
||||
Padding="14,0" FontSize="13" VerticalContentAlignment="Center"/>
|
||||
<Grid>
|
||||
<TextBox Name="AddressBox" Style="{StaticResource DarkTextBox}"
|
||||
PreviewKeyDown="AddressBox_PreviewKeyDown" TextChanged="AddressBox_TextChanged"
|
||||
BorderThickness="0" Background="Transparent"
|
||||
Padding="14,0" FontSize="13" VerticalContentAlignment="Center">
|
||||
<TextBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="붙여넣기" Command="ApplicationCommands.Paste"/>
|
||||
<MenuItem Header="복사" Command="ApplicationCommands.Copy"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="파라미터 제거 후 복사" Click="CopyCleanUrl_Click" InputGestureText="T3-15"/>
|
||||
</ContextMenu>
|
||||
</TextBox.ContextMenu>
|
||||
</TextBox>
|
||||
<!-- T3-15 주소창 진행 바 -->
|
||||
<ProgressBar Name="NavProgress" Height="2" VerticalAlignment="Bottom" MinHeight="2"
|
||||
Minimum="0" Maximum="100" IsIndeterminate="True" Visibility="Collapsed"
|
||||
Foreground="{StaticResource AccentBrush}" Background="Transparent" BorderThickness="0"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
|
@ -527,6 +550,21 @@
|
|||
</Popup>
|
||||
|
||||
<!-- ===== 다운로드 플라이아웃 (T1-5) ===== -->
|
||||
<!-- T3-14 추적 방지 실드 팝업 -->
|
||||
<Popup Name="ShieldPopup" PlacementTarget="{Binding ElementName=ShieldBtn}"
|
||||
Placement="Bottom" HorizontalOffset="-10" VerticalOffset="6"
|
||||
StaysOpen="False" AllowsTransparency="True" PopupAnimation="Fade">
|
||||
<Border Background="{StaticResource CardBg}" BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="1" CornerRadius="12" Padding="10" Width="320" MaxHeight="360">
|
||||
<StackPanel>
|
||||
<TextBlock Text="추적 방지 — 차단된 요청" Foreground="{StaticResource TextPrimary}"
|
||||
FontWeight="SemiBold" Margin="6,4,6,8"/>
|
||||
<ListBox Name="ShieldList" Background="Transparent" BorderThickness="0"
|
||||
Foreground="{StaticResource TextSecondary}" FontSize="12"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
|
||||
<Popup Name="DownloadsPopup" PlacementTarget="{Binding ElementName=DownloadsBtn}"
|
||||
Placement="Bottom" HorizontalOffset="-10" VerticalOffset="6"
|
||||
StaysOpen="False" AllowsTransparency="True" PopupAnimation="Fade">
|
||||
|
|
|
|||
|
|
@ -214,10 +214,33 @@ public partial class MainWindow : Window
|
|||
_views.GlanceRequested += (_, link) => Dispatcher.BeginInvoke(new Action(() => OpenGlance(link))); // T2-6
|
||||
_views.SpeakRequested += (_, text) => Dispatcher.BeginInvoke(new Action(() => SpeakText(text))); // T3-9
|
||||
|
||||
_views.ContextMenuBuild += OnContextMenuBuild;
|
||||
_views.BoostScriptProvider = url => BoostCssBuilder.BuildScript(url, _boostStore.Get(url)); // T2-7
|
||||
_views.GlanceRequested += (_, link) => Dispatcher.BeginInvoke(new Action(() => OpenGlance(link))); // T2-6
|
||||
_views.SpeakRequested += (_, text) => Dispatcher.BeginInvoke(new Action(() => SpeakText(text))); // T3-9
|
||||
// T3-15 주소창 진행 바: 전 탭 네비게이션 집계 → 표시
|
||||
_navTracker = new NavProgressTracker();
|
||||
_navTracker.PropertyChanged += (_, e) =>
|
||||
NavProgress.Visibility = e.PropertyName == nameof(NavProgressTracker.IsNavigating) && _navTracker.IsNavigating
|
||||
? Visibility.Visible : Visibility.Collapsed;
|
||||
_views.NavigationStarted += _navTracker.Started;
|
||||
_views.NavigationCompleted += _navTracker.Completed;
|
||||
|
||||
// T3-11 권한 다이얼로그: 예=항상 허용 / 아니요=항상 거절 / 취소=이번만 거절
|
||||
_views.PermissionPrompt += (cv, e) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
var kind = e.PermissionKind.ToString();
|
||||
var origin = Services.TabViewService.OriginOf(e.Uri);
|
||||
var r = System.Windows.MessageBox.Show(this,
|
||||
$"{origin} 사이트가 {KoreanPermission(kind)} 권한을 요청합니다.\n허용하시겠습니까?\n\n예: 항상 허용 · 아니요: 항상 거절 · 취소: 이번만 거절",
|
||||
"권한 요청", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
|
||||
e.State = r == MessageBoxResult.Yes ? Microsoft.Web.WebView2.Core.CoreWebView2PermissionState.Allow
|
||||
: Microsoft.Web.WebView2.Core.CoreWebView2PermissionState.Deny;
|
||||
e.SavesInProfile = r != MessageBoxResult.Cancel; // 항상 선택만 프로필 영속
|
||||
e.Handled = true;
|
||||
if (r != MessageBoxResult.Cancel)
|
||||
_views.Permissions.Remember(kind, origin,
|
||||
r == MessageBoxResult.Yes ? PermissionVerdict.Allow : PermissionVerdict.Deny);
|
||||
}));
|
||||
|
||||
// T3-14 실드 배지 갱신
|
||||
_views.TrackerBlocked += UpdateShield;
|
||||
|
||||
_tabs = new TabManager(() => new TabItem(), _views.AttachView);
|
||||
_tabs.ActiveChanged += OnActiveTabChanged;
|
||||
|
|
@ -284,6 +307,52 @@ public partial class MainWindow : Window
|
|||
/// <summary>탭별 WebView2 생명주기·배선 전담 서비스(재구성 ④).</summary>
|
||||
private Services.TabViewService? _views;
|
||||
|
||||
/// <summary>T3-15 주소창 진행 바 상태(전 탭 네비게이션 집계).</summary>
|
||||
private NavProgressTracker? _navTracker;
|
||||
|
||||
/// <summary>T3-15: 파라미터 제거 후 클립보드 복사(주소창 우클릭).</summary>
|
||||
private void CopyCleanUrl_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var clean = UrlCleaner.StripQuery(AddressBox.Text);
|
||||
if (string.IsNullOrEmpty(clean)) return;
|
||||
try { Clipboard.SetText(clean); Log($"파라미터 제거 URL 복사: {clean}"); } catch { }
|
||||
}
|
||||
|
||||
/// <summary>T3-14: 실드 배지 갱신.</summary>
|
||||
private void UpdateShield()
|
||||
{
|
||||
if (_views == null) return;
|
||||
var n = _views.TrackerCounter.Total;
|
||||
ShieldBadgeText.Text = n > 0 ? n.ToString() : "";
|
||||
ShieldBadge.Visibility = n > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ShieldBtn.ToolTip = n > 0 ? $"추적 요청 {n}건 차단됨" : "추적 방지";
|
||||
}
|
||||
|
||||
/// <summary>T3-14: 실드 클릭 → 차단 도메인 목록 팝업.</summary>
|
||||
private void Shield_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_views == null) return;
|
||||
var byDomain = _views.TrackerCounter.ByDomain;
|
||||
ShieldList.ItemsSource = byDomain.Count == 0
|
||||
? new[] { "차단된 추적 요청이 없습니다" }
|
||||
: byDomain.OrderByDescending(kv => kv.Value).Select(kv => $"{kv.Value,3} × {kv.Key}").ToList();
|
||||
ShieldPopup.IsOpen = true;
|
||||
}
|
||||
|
||||
/// <summary>T3-11: 권한 종류 한글 표기.</summary>
|
||||
private static string KoreanPermission(string kind) => kind switch
|
||||
{
|
||||
"Microphone" => "마이크",
|
||||
"Camera" => "카메라",
|
||||
"Geolocation" => "위치",
|
||||
"Notifications" => "알림",
|
||||
"ClipboardRead" => "클립보드 읽기",
|
||||
"Autoplay" => "자동 재생",
|
||||
"FileReadWrite" => "파일 읽기/쓰기",
|
||||
"PersistentStorage" => "영구 저장소",
|
||||
_ => kind,
|
||||
};
|
||||
|
||||
private void OnUrlChanged(TabItem tab, string url)
|
||||
{
|
||||
if (tab.PendingHistoryUrl != null) RecordHistory(tab.PendingHistoryUrl, tab.PendingHistoryUrl);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue