Phase A: tab architecture integrated into MainWindow - tab strip UI, per-tab WebView2 lifecycle, new-window->tab w/ deferral, audio indicator/mute, session save/restore, shortcuts; E2E on real window (56 tests GREEN)
This commit is contained in:
parent
be01a116ca
commit
b6b7fc341b
7 changed files with 389 additions and 106 deletions
|
|
@ -10,10 +10,13 @@ using System.Windows.Interop;
|
|||
using System.Windows.Media;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using VideoDownloader.App.Browser;
|
||||
using VideoDownloader.App.Browser.Tabs;
|
||||
using VideoDownloader.App.Core;
|
||||
using VideoDownloader.App.Detection;
|
||||
using VideoDownloader.App.ViewModels;
|
||||
using VideoDownloader.Core.Config;
|
||||
using Wv2 = Microsoft.Web.WebView2.Wpf;
|
||||
using TabItem = VideoDownloader.App.Browser.Tabs.TabItem;
|
||||
|
||||
namespace VideoDownloader.App;
|
||||
|
||||
|
|
@ -23,18 +26,22 @@ public partial class MainWindow : Window
|
|||
private AppConfig _config;
|
||||
private int _totalRequests;
|
||||
private string _appLogPath = "";
|
||||
private string _currentTitle = "";
|
||||
|
||||
// ---- 탭 아키텍처 (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;
|
||||
|
||||
private readonly ObservableCollection<Bookmark> _bookmarks = new();
|
||||
private readonly ObservableCollection<HistoryEntry> _history = new();
|
||||
private string? _pendingHistoryUrl;
|
||||
private string? _pendingSocialUrl; // 타이틀 확정 시 소셜 카드 제목 갱신용
|
||||
private bool _suggesting; // 주소창 인라인 자동완성 도중 (TextChanged 재귀 방지)
|
||||
|
||||
// T1-4 검색엔진 / T1-7 사이트별 줌 / T1-3 페이지 내 검색 / T1-11 전체화면
|
||||
private SearchEngineResolver _resolver = null!;
|
||||
private SiteZoomStore _zoomStore = null!;
|
||||
private FindSession? _findSession;
|
||||
private bool _fullscreen;
|
||||
private WindowState _stateBeforeFullscreen = WindowState.Normal;
|
||||
|
||||
|
|
@ -51,6 +58,8 @@ public partial class MainWindow : Window
|
|||
private const double MinExpandedWidth = 280;
|
||||
private const double MaxExpandedWidth = 900;
|
||||
|
||||
private static string SessionPath => Path.Combine(AppDataDir, "session.json");
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
|
@ -142,44 +151,144 @@ public partial class MainWindow : Window
|
|||
|
||||
internal static string ProfileFolder => Path.Combine(AppDataDir, "webview");
|
||||
|
||||
// ==================== 탭 생명주기 (Phase A) ====================
|
||||
|
||||
private async Task InitializeBrowserAsync()
|
||||
{
|
||||
CoreWebView2Environment? env = null;
|
||||
try
|
||||
{
|
||||
// 명시적 영속 프로필: 기본(exe 디렉터리) 폴더는 빌드/재배포 시 날아가 로그인이 풀림 → %AppData% 로 고정.
|
||||
Directory.CreateDirectory(ProfileFolder);
|
||||
env = await CoreWebView2Environment.CreateAsync(browserExecutableFolder: null, userDataFolder: ProfileFolder);
|
||||
var options = env.CreateCoreWebView2ControllerOptions();
|
||||
if (_config.PrivateMode) options.IsInPrivateModeEnabled = true; // InPrivate: 디스크에 아무것도 저장 안 함
|
||||
await Browser.EnsureCoreWebView2Async(env, options);
|
||||
_env = await CoreWebView2Environment.CreateAsync(browserExecutableFolder: null, userDataFolder: ProfileFolder);
|
||||
}
|
||||
catch (Exception ex) { Log($"WebView2 초기화 실패: {ex.Message}"); return; }
|
||||
|
||||
var cv = Browser.CoreWebView2;
|
||||
_tabs = new TabManager(CreateTabItem, AttachTabView);
|
||||
_tabs.ActiveChanged += OnActiveTabChanged;
|
||||
_tabs.TabRemoved += OnTabRemoved;
|
||||
TabList.ItemsSource = _tabs.Tabs;
|
||||
UpdatePrivateButton();
|
||||
|
||||
// T1-10 세션 복원: 저장된 탭이 있으면 복원, 아니면 홈 탭
|
||||
var snap = _config.RestoreSession ? TabSessionStore.Load(SessionPath) : null;
|
||||
if (snap != null && snap.Urls.Count > 0)
|
||||
_tabs.RestoreSession(snap);
|
||||
else
|
||||
_tabs.NewTab(_config.HomeUrl);
|
||||
UpdateNavState();
|
||||
}
|
||||
|
||||
private TabItem CreateTabItem() => new();
|
||||
|
||||
/// <summary>탭에 WebView2 뷰를 붙이고 비동기 초기화를 시작한다. (UI 스레드)</summary>
|
||||
private void AttachTabView(TabItem tab)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>탭의 CoreWebView2 초기화 + 탭별 이벤트 배선 + 초기 URL 탐색.</summary>
|
||||
private async Task InitTabCoreAsync(TabItem tab)
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = _env!.CreateCoreWebView2ControllerOptions();
|
||||
if (_config.PrivateMode) options.IsInPrivateModeEnabled = true; // InPrivate: 디스크에 아무것도 저장 안 함
|
||||
await tab.View!.EnsureCoreWebView2Async(_env, options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"탭 초기화 실패: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
var cv = tab.Core!;
|
||||
cv.Settings.AreDevToolsEnabled = true;
|
||||
|
||||
// T1-3 네이티브 Find API 세션 (자체 검색 바)
|
||||
_findSession = new FindSession(new WebView2FindApi(cv, env!));
|
||||
_findSession.StateChanged += () => Dispatcher.BeginInvoke(new Action(() =>
|
||||
// T1-3 탭별 검색 세션 (네이티브 Find API)
|
||||
tab.FindSession = new FindSession(new WebView2FindApi(cv, _env!));
|
||||
tab.FindSession.StateChanged += () => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
if (_findSession != null) FindStatus.Text = _findSession.StatusText;
|
||||
if (tab.IsActive && tab.FindSession != null) FindStatus.Text = tab.FindSession.StatusText;
|
||||
}));
|
||||
|
||||
// T1-7 사이트별 줌: 네비 완료 시 저장된 배율 적용 (WebView2 는 사이트별 영속 미보장)
|
||||
// 탭별 네비게이션 상태
|
||||
cv.NavigationStarting += (_, _) => Dispatcher.BeginInvoke(new Action(() => tab.IsLoading = true));
|
||||
cv.NavigationCompleted += (_, _) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try { Browser.ZoomFactor = _zoomStore.Get(cv.Source); } catch { }
|
||||
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;
|
||||
}
|
||||
}));
|
||||
|
||||
cv.WebResourceResponseReceived += OnResponseReceived;
|
||||
// #4 팝업/새창을 현재 WebView2 로 강제 라우팅
|
||||
cv.NewWindowRequested += (_, ne) =>
|
||||
// T1-8 오디오 표시/뮤트
|
||||
try
|
||||
{
|
||||
try { if (ne.Uri != null) { ne.Handled = true; cv.Navigate(ne.Uri); } }
|
||||
catch { }
|
||||
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) =>
|
||||
{
|
||||
|
|
@ -194,46 +303,90 @@ public partial class MainWindow : Window
|
|||
}
|
||||
catch { }
|
||||
};
|
||||
cv.HistoryChanged += (_, _) => UpdateNavState();
|
||||
cv.SourceChanged += (_, _) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
if (_pendingHistoryUrl != null) RecordHistory(_pendingHistoryUrl, _pendingHistoryUrl);
|
||||
var url = cv.Source ?? "";
|
||||
AddressBox.Text = url;
|
||||
_pendingHistoryUrl = url.StartsWith("about:", StringComparison.OrdinalIgnoreCase) ? null : url;
|
||||
DetectSocial(url);
|
||||
}));
|
||||
cv.DocumentTitleChanged += (_, _) => Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
_currentTitle = cv.DocumentTitle ?? "";
|
||||
if (_pendingHistoryUrl != null)
|
||||
{
|
||||
RecordHistory(string.IsNullOrWhiteSpace(_currentTitle) ? _pendingHistoryUrl : _currentTitle, _pendingHistoryUrl);
|
||||
_pendingHistoryUrl = null;
|
||||
}
|
||||
// 소셜 카드 제목을 페이지 타이틀로 갱신(출력 파일명 포함, 시작 전일 때)
|
||||
if (_pendingSocialUrl != null && !string.IsNullOrWhiteSpace(_currentTitle))
|
||||
{
|
||||
_manager.RenameSocial(new Uri(_pendingSocialUrl), _currentTitle);
|
||||
_pendingSocialUrl = null;
|
||||
}
|
||||
}));
|
||||
|
||||
NavHome_Click(this, null!);
|
||||
// 초기 URL (세션 복원/빈 탭 제외)
|
||||
if (!string.IsNullOrEmpty(tab.Url) && (cv.Source == null || cv.Source.Length == 0))
|
||||
{
|
||||
try { cv.Navigate(tab.Url); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>탭의 CoreWebView2 초기화 완료 대기 (뉴윈도우 deferral 용).</summary>
|
||||
private async Task WaitTabReady(TabItem tab)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
private void OnActiveTabChanged(TabItem tab)
|
||||
{
|
||||
foreach (var t in _tabs.Tabs)
|
||||
if (t.View != null) t.View.Visibility = ReferenceEquals(t, tab) ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (!AddressBox.IsKeyboardFocused) AddressBox.Text = tab.Url;
|
||||
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 UpdateNavState()
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
if (cv == null) return;
|
||||
var cv = Cv;
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
NavBack.IsEnabled = cv.CanGoBack;
|
||||
NavForward.IsEnabled = cv.CanGoForward;
|
||||
NavBack.IsEnabled = cv?.CanGoBack == true;
|
||||
NavForward.IsEnabled = cv?.CanGoForward == true;
|
||||
}));
|
||||
}
|
||||
|
||||
// ---- 탭 스트립 UI 이벤트 ----
|
||||
private void NewTab_Click(object sender, RoutedEventArgs e) => _tabs.NewTab(_config.HomeUrl);
|
||||
|
||||
private void Tab_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is TabItem tab)
|
||||
_tabs.Activate(tab);
|
||||
}
|
||||
|
||||
private void Tab_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 휠 클릭(미들) → 탭 닫기 (브라우저 관례)
|
||||
if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Pressed)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is TabItem tab)
|
||||
{
|
||||
_tabs.CloseTab(tab);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TabClose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is TabItem tab)
|
||||
_tabs.CloseTab(tab);
|
||||
}
|
||||
|
||||
// T1-8 스피커 아이콘 클릭 → 음소거 토글
|
||||
private void TabAudio_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is TabItem { Core: { } cv } tab)
|
||||
{
|
||||
try { cv.IsMuted = !cv.IsMuted; tab.IsMuted = cv.IsMuted; } catch { }
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnResponseReceived(object? sender, CoreWebView2WebResourceResponseReceivedEventArgs e)
|
||||
{
|
||||
try
|
||||
|
|
@ -273,7 +426,7 @@ public partial class MainWindow : Window
|
|||
}
|
||||
catch { }
|
||||
|
||||
string referer = Browser.CoreWebView2?.Source ?? "";
|
||||
string referer = (sender as CoreWebView2)?.Source ?? Cv?.Source ?? "";
|
||||
try { var r = e.Request.Headers.GetHeader("Referer"); if (!string.IsNullOrEmpty(r)) referer = r; } catch { }
|
||||
|
||||
var kind = MediaDetector.Classify(uri, ct, len, _config);
|
||||
|
|
@ -348,7 +501,7 @@ public partial class MainWindow : Window
|
|||
|
||||
private async Task<List<CoreWebView2Cookie>> GetCookiesAsync(Uri url)
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
var cv = Cv;
|
||||
if (cv == null) return new();
|
||||
try { return (await cv.CookieManager.GetCookiesAsync(url.ToString())).ToList(); }
|
||||
catch { return new(); }
|
||||
|
|
@ -455,9 +608,9 @@ public partial class MainWindow : Window
|
|||
}
|
||||
}
|
||||
|
||||
private void NavBack_Click(object sender, RoutedEventArgs e) => Browser.CoreWebView2?.GoBack();
|
||||
private void NavForward_Click(object sender, RoutedEventArgs e) => Browser.CoreWebView2?.GoForward();
|
||||
private void NavRefresh_Click(object sender, RoutedEventArgs e) => Browser.CoreWebView2?.Reload();
|
||||
private void NavBack_Click(object sender, RoutedEventArgs e) => Cv?.GoBack();
|
||||
private void NavForward_Click(object sender, RoutedEventArgs e) => Cv?.GoForward();
|
||||
private void NavRefresh_Click(object sender, RoutedEventArgs e) => Cv?.Reload();
|
||||
private void NavHome_Click(object sender, RoutedEventArgs e) => NavigateTo(_config.HomeUrl);
|
||||
|
||||
internal void NavigateTo(string? url)
|
||||
|
|
@ -465,21 +618,21 @@ public partial class MainWindow : Window
|
|||
// T1-4: URL/검색어 판별 + 기본 엔진·키워드 전환(g, yt, ...)은 리졸버가 담당
|
||||
var intent = _resolver.Resolve(url);
|
||||
if (intent == null) return;
|
||||
Browser.CoreWebView2?.Navigate(intent.Target);
|
||||
Cv?.Navigate(intent.Target);
|
||||
}
|
||||
|
||||
// 소셜 영상 페이지(YouTube/Instagram/X/...) 감지 → yt-dlp 카드 등록.
|
||||
private void DetectSocial(string? url)
|
||||
// 소셜 영상 페이지(YouTube/Instagram/X/...) 감지 → yt-dlp 카드 등록. (탭별 상태)
|
||||
private void DetectSocial(TabItem tab, string? url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url)) { _pendingSocialUrl = null; return; }
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) { _pendingSocialUrl = null; return; }
|
||||
if (string.IsNullOrEmpty(url)) { tab.PendingSocialUrl = null; return; }
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) { tab.PendingSocialUrl = null; return; }
|
||||
if (SocialSiteDetector.TryMatch(uri, out var normalized, out var label))
|
||||
{
|
||||
_manager.LogPanel($"★ 소셜 감지 [{label}]: {normalized}");
|
||||
_manager.OnDetected(new DetectionContext(normalized, normalized, MediaKind.Social, Tag: label));
|
||||
_pendingSocialUrl = normalized;
|
||||
tab.PendingSocialUrl = normalized;
|
||||
}
|
||||
else _pendingSocialUrl = null;
|
||||
else tab.PendingSocialUrl = null;
|
||||
}
|
||||
|
||||
// ---- 링크로 받기 (URL 붙여넣기) ----
|
||||
|
|
@ -506,27 +659,40 @@ public partial class MainWindow : Window
|
|||
{
|
||||
_manager.LogPanel($"★ 소셜 감지 [{label}]: {normalized}");
|
||||
_manager.OnDetected(new DetectionContext(normalized, normalized, MediaKind.Social, Tag: label));
|
||||
_pendingSocialUrl = normalized;
|
||||
LinkPopup.IsOpen = false;
|
||||
Log("카드에 추가했습니다. 화질을 선택하고 받기를 누르세요");
|
||||
}
|
||||
else Log("지원하는 소셜 영상 링크가 아닙니다 (YouTube·Instagram·X·TikTok·Facebook)");
|
||||
}
|
||||
|
||||
// #7 전역 키보드 단축키 (+T1-3/T1-9/T1-11)
|
||||
// #7 전역 키보드 단축키 (+Phase A 탭/T1-3/T1-9/T1-11)
|
||||
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
var cv = Cv;
|
||||
var mods = Keyboard.Modifiers;
|
||||
|
||||
// Phase A 탭 단축키
|
||||
if (mods == ModifierKeys.Control && e.Key == Key.T) { _tabs.NewTab(_config.HomeUrl); e.Handled = true; }
|
||||
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.T) { _tabs.ReopenClosedTab(); e.Handled = true; }
|
||||
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.W) { _tabs.CloseTab(_tabs.ActiveTab!); e.Handled = true; }
|
||||
else if (mods == ModifierKeys.Control && e.Key == Key.W) { if (_tabs.ActiveTab != null) _tabs.CloseTab(_tabs.ActiveTab); e.Handled = true; }
|
||||
else if (mods == ModifierKeys.Control && e.Key == Key.Tab) { _tabs.ActivateNext(); e.Handled = true; }
|
||||
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.Tab) { _tabs.ActivatePrevious(); e.Handled = true; }
|
||||
else if (mods == ModifierKeys.Control && e.Key >= Key.D1 && e.Key <= Key.D9)
|
||||
{
|
||||
var idx = e.Key - Key.D1;
|
||||
if (idx == 8) _tabs.ActivateLast(); else _tabs.ActivateAt(idx); // Ctrl+9 = 마지막 탭
|
||||
e.Handled = true;
|
||||
}
|
||||
// T1-3 검색 바가 열려 있으면 Esc 는 닫기가 우선
|
||||
if (e.Key == Key.Escape && FindBar.Visibility == Visibility.Visible) { CloseFind(); e.Handled = true; }
|
||||
else if (e.Key == Key.Escape && FindBar.Visibility == Visibility.Visible) { CloseFind(); e.Handled = true; }
|
||||
else if (mods == ModifierKeys.Control && e.Key == Key.F) { OpenFind(); e.Handled = true; }
|
||||
else if (e.Key == Key.F3)
|
||||
{
|
||||
if (FindBar.Visibility == Visibility.Visible)
|
||||
{
|
||||
if (mods == ModifierKeys.Shift) _findSession?.Previous(); else _findSession?.Next();
|
||||
if (mods == ModifierKeys.Shift) _tabs.ActiveTab?.FindSession?.Previous();
|
||||
else _tabs.ActiveTab?.FindSession?.Next();
|
||||
}
|
||||
else OpenFind();
|
||||
e.Handled = true;
|
||||
|
|
@ -540,66 +706,80 @@ public partial class MainWindow : Window
|
|||
else if (mods == ModifierKeys.Alt && e.Key == Key.Right) { try { if (cv?.CanGoForward == true) cv.GoForward(); } catch { } e.Handled = true; }
|
||||
else if (mods == ModifierKeys.Control)
|
||||
{
|
||||
if (e.Key == Key.OemPlus || e.Key == Key.Add) { try { Browser.ZoomFactor = Math.Min(Browser.ZoomFactor + 0.1, 5); PersistZoom(); } catch { } e.Handled = true; }
|
||||
else if (e.Key == Key.OemMinus || e.Key == Key.Subtract) { try { Browser.ZoomFactor = Math.Max(Browser.ZoomFactor - 0.1, 0.1); PersistZoom(); } catch { } e.Handled = true; }
|
||||
else if (e.Key == Key.D0) { try { Browser.ZoomFactor = 1.0; PersistZoom(); } catch { } e.Handled = true; }
|
||||
if (e.Key == Key.OemPlus || e.Key == Key.Add) { try { ZoomBy(+0.1); } catch { } e.Handled = true; }
|
||||
else if (e.Key == Key.OemMinus || e.Key == Key.Subtract) { try { ZoomBy(-0.1); } catch { } e.Handled = true; }
|
||||
else if (e.Key == Key.D0) { try { ZoomSet(1.0); } catch { } e.Handled = true; }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- T1-3 페이지 내 검색 바 ----
|
||||
private void ZoomBy(double delta) => ZoomSet(Math.Clamp((ActiveView?.ZoomFactor ?? 1.0) + delta, 0.1, 5.0));
|
||||
|
||||
private void ZoomSet(double zoom)
|
||||
{
|
||||
if (ActiveView == null) return;
|
||||
ActiveView.ZoomFactor = zoom;
|
||||
PersistZoom();
|
||||
}
|
||||
|
||||
// ---- T1-3 페이지 내 검색 바 (활성 탭 세션) ----
|
||||
internal void OpenFind()
|
||||
{
|
||||
FindBar.Visibility = Visibility.Visible;
|
||||
FindStatus.Text = "";
|
||||
FindBox.Focus();
|
||||
FindBox.SelectAll();
|
||||
if (FindBox.Text.Length > 0) _ = _findSession?.StartAsync(FindBox.Text);
|
||||
if (FindBox.Text.Length > 0) _ = _tabs.ActiveTab?.FindSession?.StartAsync(FindBox.Text);
|
||||
}
|
||||
|
||||
internal void CloseFind()
|
||||
{
|
||||
FindBar.Visibility = Visibility.Collapsed;
|
||||
_findSession?.Close();
|
||||
_tabs.ActiveTab?.FindSession?.Close();
|
||||
FindStatus.Text = "";
|
||||
}
|
||||
|
||||
private void FindBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (FindBar.Visibility == Visibility.Visible)
|
||||
_ = _findSession?.StartAsync(FindBox.Text);
|
||||
_ = _tabs.ActiveTab?.FindSession?.StartAsync(FindBox.Text);
|
||||
}
|
||||
|
||||
private void FindBox_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter) { if (Keyboard.Modifiers == ModifierKeys.Shift) _findSession?.Previous(); else _findSession?.Next(); e.Handled = true; }
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
if (Keyboard.Modifiers == ModifierKeys.Shift) _tabs.ActiveTab?.FindSession?.Previous();
|
||||
else _tabs.ActiveTab?.FindSession?.Next();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Escape) { CloseFind(); e.Handled = true; }
|
||||
}
|
||||
|
||||
private void FindNext_Click(object sender, RoutedEventArgs e) => _findSession?.Next();
|
||||
private void FindPrev_Click(object sender, RoutedEventArgs e) => _findSession?.Previous();
|
||||
private void FindNext_Click(object sender, RoutedEventArgs e) => _tabs.ActiveTab?.FindSession?.Next();
|
||||
private void FindPrev_Click(object sender, RoutedEventArgs e) => _tabs.ActiveTab?.FindSession?.Previous();
|
||||
private void FindClose_Click(object sender, RoutedEventArgs e) => CloseFind();
|
||||
|
||||
private void FindOption_Changed(object sender, RoutedEventArgs e)
|
||||
=> _findSession?.SetOptions(FindCaseBox.IsChecked == true, FindWordBox.IsChecked == true);
|
||||
=> _tabs.ActiveTab?.FindSession?.SetOptions(FindCaseBox.IsChecked == true, FindWordBox.IsChecked == true);
|
||||
|
||||
// T1-7 줌 변경을 사이트별 저장소에 영속
|
||||
private void PersistZoom()
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
if (cv != null) _zoomStore.Set(cv.Source, Browser.ZoomFactor);
|
||||
var cv = Cv;
|
||||
if (cv != null && ActiveView != null) _zoomStore.Set(cv.Source, ActiveView.ZoomFactor);
|
||||
}
|
||||
|
||||
// T1-9 강제 새로고침: 디스크 캐시 비우고 다시 로드
|
||||
private async void ForceReload()
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
var cv = Cv;
|
||||
if (cv == null) return;
|
||||
try { await cv.Profile.ClearBrowsingDataAsync(CoreWebView2BrowsingDataKinds.DiskCache); }
|
||||
catch { }
|
||||
try { cv.Reload(); } catch { }
|
||||
}
|
||||
|
||||
// T1-11 전체화면(F11): 툴바/상태바 숨기고 최대화. 재누르면 복원.
|
||||
// T1-11 전체화면(F11): 툴바/탭/상태바 숨기고 최대화. 재누르면 복원.
|
||||
internal void ToggleFullscreen()
|
||||
{
|
||||
_fullscreen = !_fullscreen;
|
||||
|
|
@ -613,6 +793,7 @@ public partial class MainWindow : Window
|
|||
WindowState = _stateBeforeFullscreen;
|
||||
}
|
||||
ToolbarRow.Visibility = _fullscreen ? Visibility.Collapsed : Visibility.Visible;
|
||||
TabStripRow.Visibility = _fullscreen ? Visibility.Collapsed : Visibility.Visible;
|
||||
StatusBar.Visibility = _fullscreen ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
|
|
@ -672,6 +853,8 @@ public partial class MainWindow : Window
|
|||
_config.RightPanelCollapsed = _panelCollapsed;
|
||||
if (!_panelCollapsed) _config.RightPanelWidth = ClampPanelWidth(RightCol.Width.Value);
|
||||
ConfigStore.Save(_config);
|
||||
// T1-10 세션 저장: 탭 URL/활성/핀 보존
|
||||
try { if (_tabs != null) TabSessionStore.Save(SessionPath, _tabs.CaptureSession()); } catch { }
|
||||
}
|
||||
|
||||
// ---- 개인정보 보호 모드 + 브라우저 데이터 관리 ----
|
||||
|
|
@ -707,7 +890,7 @@ public partial class MainWindow : Window
|
|||
/// <summary>현재 프로필의 브라우저 데이터(쿠키·DOM 저장소·캐시) 삭제. 일반 모드: 영속 프로필 정리.</summary>
|
||||
public async Task ClearBrowsingDataAsync()
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
var cv = Cv;
|
||||
if (cv == null) { Log("브라우저가 아직 준비되지 않았습니다"); return; }
|
||||
try
|
||||
{
|
||||
|
|
@ -727,7 +910,7 @@ public partial class MainWindow : Window
|
|||
|
||||
private void AddBookmark_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var cv = Browser.CoreWebView2;
|
||||
var cv = Cv;
|
||||
var url = cv?.Source ?? "";
|
||||
if (string.IsNullOrEmpty(url) || url.StartsWith("about:", StringComparison.OrdinalIgnoreCase)) return;
|
||||
if (_bookmarks.Any(b => b.Url == url)) { Log("이미 즐겨찾기에 있습니다"); return; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue