Phase B (T1-5/T1-6/T2-2/T2-8/T2-12): download manager flyout w/ speed+pause/resume/cancel, bookmark bar, tab sleeping, tab search (open+closed), context menu w/ 'download video from this link' (79 tests GREEN)
This commit is contained in:
parent
af19dba8f4
commit
0caa1111e7
9 changed files with 763 additions and 3 deletions
|
|
@ -10,6 +10,7 @@ using System.Windows.Interop;
|
|||
using System.Windows.Media;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using VideoDownloader.Browser;
|
||||
using VideoDownloader.App.Browser.Features;
|
||||
using VideoDownloader.Browser.Tabs;
|
||||
using VideoDownloader.App.Core;
|
||||
using VideoDownloader.Core.Detection;
|
||||
|
|
@ -38,6 +39,13 @@ public partial class MainWindow : Window
|
|||
private readonly ObservableCollection<HistoryEntry> _history = new();
|
||||
private bool _suggesting; // 주소창 인라인 자동완성 도중 (TextChanged 재귀 방지)
|
||||
|
||||
// Phase B: 다운로드 매니저(T1-5) / 탭 절전(T2-2)
|
||||
private readonly PageDownloadTracker _pageDownloads = new();
|
||||
private System.Windows.Threading.DispatcherTimer? _speedTimer;
|
||||
private System.Windows.Threading.DispatcherTimer? _suspendTimer;
|
||||
private TabSuspendService? _suspendService;
|
||||
private readonly System.Diagnostics.Stopwatch _speedWatch = new();
|
||||
|
||||
// T1-4 검색엔진 / T1-7 사이트별 줌 / T1-3 페이지 내 검색 / T1-11 전체화면
|
||||
private SearchEngineResolver _resolver = null!;
|
||||
private SiteZoomStore _zoomStore = null!;
|
||||
|
|
@ -175,6 +183,10 @@ public partial class MainWindow : Window
|
|||
if (tab.FindSession != null) FindStatus.Text = tab.FindSession.StatusText;
|
||||
};
|
||||
|
||||
_views.ContextMenuBuild += OnContextMenuBuild;
|
||||
|
||||
_views.ContextMenuBuild += OnContextMenuBuild;
|
||||
|
||||
_tabs = new TabManager(() => new TabItem(), _views.AttachView);
|
||||
_tabs.ActiveChanged += OnActiveTabChanged;
|
||||
_tabs.TabRemoved += OnTabRemoved;
|
||||
|
|
@ -188,6 +200,34 @@ public partial class MainWindow : Window
|
|||
else
|
||||
_tabs.NewTab(_config.HomeUrl);
|
||||
UpdateNavState();
|
||||
|
||||
// T1-5 다운로드 속도 갱신 타이머
|
||||
DownloadsList.ItemsSource = _pageDownloads.Items;
|
||||
_speedTimer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
_speedTimer.Tick += (_, _) =>
|
||||
{
|
||||
foreach (var item in _pageDownloads.Items)
|
||||
item.UpdateSpeed(item.Op.BytesReceived, TimeSpan.FromSeconds(1));
|
||||
DownloadsEmptyHint.Visibility = _pageDownloads.Items.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
};
|
||||
_speedTimer.Start();
|
||||
|
||||
// T2-2 탭 절전: 1분마다 유휴(10분+) 비활성 탭 서스펜드
|
||||
_suspendService = new TabSuspendService(TimeSpan.FromMinutes(10), tab =>
|
||||
{
|
||||
var cv = tab.Core;
|
||||
if (cv == null || cv.IsSuspended) return false;
|
||||
if (tab.View?.Visibility != Visibility.Collapsed) return false; // 숨겨진 탭만 서스펜드 가능
|
||||
_ = cv.TrySuspendAsync();
|
||||
return true;
|
||||
}, tab => tab.Core != null);
|
||||
_suspendTimer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromMinutes(1) };
|
||||
_suspendTimer.Tick += (_, _) =>
|
||||
{
|
||||
if (_tabs.ActiveTab != null)
|
||||
_suspendService.SuspendIdleTabs(_tabs.Tabs, _tabs.ActiveTab, DateTime.UtcNow);
|
||||
};
|
||||
_suspendTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>탭별 WebView2 생명주기·배선 전담 서비스(재구성 ④).</summary>
|
||||
|
|
@ -227,15 +267,46 @@ public partial class MainWindow : Window
|
|||
if (string.IsNullOrWhiteSpace(fn)) fn = $"download_{DateTime.Now:yyyyMMdd_HHmmss}";
|
||||
de.ResultFilePath = MakeUniqueDownloadName(folder, fn);
|
||||
_manager.LogPanel($"📥 페이지 다운로드 → 저장 폴더: {fn}");
|
||||
|
||||
var item = _pageDownloads.Add(new DownloadOperationAdapter(de.DownloadOperation), fn); // T1-5
|
||||
item.UpdateSpeed(0, TimeSpan.FromSeconds(1));
|
||||
DownloadsEmptyHint.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// ---- T1-5 다운로드 플라이아웃 ----
|
||||
private void ToggleDownloads(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DownloadsPopup.IsOpen = !DownloadsPopup.IsOpen;
|
||||
}
|
||||
|
||||
private void DownloadsClear_Click(object sender, RoutedEventArgs e) => _pageDownloads.ClearCompleted();
|
||||
|
||||
private void DlPause_Click(object sender, RoutedEventArgs e)
|
||||
=> IfDownloadItem(sender, i => _pageDownloads.Pause(i));
|
||||
|
||||
private void DlResume_Click(object sender, RoutedEventArgs e)
|
||||
=> IfDownloadItem(sender, i => _pageDownloads.Resume(i));
|
||||
|
||||
private void DlCancel_Click(object sender, RoutedEventArgs e)
|
||||
=> IfDownloadItem(sender, i => _pageDownloads.Cancel(i));
|
||||
|
||||
private static void IfDownloadItem(object sender, Action<PageDownloadItem> act)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is PageDownloadItem item) act(item);
|
||||
}
|
||||
|
||||
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;
|
||||
if (tab.IsSuspended) // T2-2: 절전 탭 활성화 시 재개
|
||||
{
|
||||
try { tab.Core?.Resume(); } catch { }
|
||||
tab.IsSuspended = false;
|
||||
}
|
||||
UpdateNavState();
|
||||
}
|
||||
|
||||
|
|
@ -575,6 +646,9 @@ public partial class MainWindow : Window
|
|||
|
||||
// Phase A 탭 단축키
|
||||
if (mods == ModifierKeys.Control && e.Key == Key.T) { _tabs.NewTab(_config.HomeUrl); e.Handled = true; }
|
||||
else if (mods == ModifierKeys.Control && e.Key == Key.J) { DownloadsPopup.IsOpen = !DownloadsPopup.IsOpen; e.Handled = true; }
|
||||
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.A) { ToggleTabSearch(this, e); e.Handled = true; }
|
||||
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.B) { ToggleBookmarkBar(); 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; }
|
||||
|
|
@ -819,6 +893,7 @@ public partial class MainWindow : Window
|
|||
var title = Trunc(string.IsNullOrWhiteSpace(cv!.DocumentTitle) ? url : cv.DocumentTitle, 80);
|
||||
_bookmarks.Insert(0, new Bookmark(title, url));
|
||||
BrowserStore.SaveBookmarks(_bookmarks.ToList());
|
||||
if (BookmarkBar.Visibility == Visibility.Visible) RefreshBookmarkBar();
|
||||
Log($"즐겨찾기 추가: {title}");
|
||||
}
|
||||
|
||||
|
|
@ -873,6 +948,137 @@ public partial class MainWindow : Window
|
|||
BrowserStore.SaveHistory(_history.ToList());
|
||||
}
|
||||
|
||||
// ---- T2-8 탭 검색 (Ctrl+Shift+A) ----
|
||||
private void ToggleTabSearch(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var open = !TabSearchPopup.IsOpen;
|
||||
TabSearchPopup.IsOpen = open;
|
||||
if (open)
|
||||
{
|
||||
TabSearchBox.Clear();
|
||||
TabSearchBox.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
private void TabSearchBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
var results = TabSearchFilter.Search(_tabs.Tabs, _tabs.ClosedStack, TabSearchBox.Text);
|
||||
TabSearchResults.ItemsSource = results;
|
||||
TabSearchResults.SelectedIndex = results.Count > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
private void TabSearchBox_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape) { TabSearchPopup.IsOpen = false; e.Handled = true; }
|
||||
else if (e.Key == Key.Down && TabSearchResults.HasItems) { TabSearchResults.SelectedIndex = Math.Min(TabSearchResults.SelectedIndex + 1, TabSearchResults.Items.Count - 1); e.Handled = true; }
|
||||
else if (e.Key == Key.Up && TabSearchResults.HasItems) { TabSearchResults.SelectedIndex = Math.Max(TabSearchResults.SelectedIndex - 1, 0); e.Handled = true; }
|
||||
else if (e.Key == Key.Enter)
|
||||
{
|
||||
if (TabSearchResults.SelectedItem is TabSearchResult sel) ActivateSearchResult(sel);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void TabSearchResult_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (TabSearchResults.SelectedItem is TabSearchResult sel) ActivateSearchResult(sel);
|
||||
}
|
||||
|
||||
private void ActivateSearchResult(TabSearchResult sel)
|
||||
{
|
||||
TabSearchPopup.IsOpen = false;
|
||||
if (sel.IsClosed)
|
||||
{
|
||||
var reopened = _tabs.ReopenClosedTab();
|
||||
if (reopened == null) _tabs.NewTab(sel.Url);
|
||||
}
|
||||
else _tabs.Activate(sel.Tab);
|
||||
}
|
||||
|
||||
// ---- T1-6 북마크바 (Ctrl+Shift+B) ----
|
||||
private void ToggleBookmarkBar()
|
||||
{
|
||||
BookmarkBar.Visibility = BookmarkBar.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
|
||||
if (BookmarkBar.Visibility == Visibility.Visible) RefreshBookmarkBar();
|
||||
}
|
||||
|
||||
private void RefreshBookmarkBar() => BookmarkBarList.ItemsSource = _bookmarks.Take(30).ToList();
|
||||
|
||||
private void BookmarkBar_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is Bookmark b) NavigateTo(b.Url);
|
||||
}
|
||||
|
||||
// ---- T2-12 컨텍스트 메뉴 ----
|
||||
private void OnContextMenuBuild(CoreWebView2 cv, CoreWebView2ContextMenuRequestedEventArgs ce)
|
||||
{
|
||||
try
|
||||
{
|
||||
var menu = new ContextMenu();
|
||||
var target = ce.ContextMenuTarget;
|
||||
|
||||
var link = target?.LinkUri;
|
||||
if (!string.IsNullOrEmpty(link))
|
||||
{
|
||||
var download = new MenuItem { Header = "이 링크의 영상 받기", FontWeight = FontWeights.Medium };
|
||||
download.Click += (_, _) => QueueLinkForDownload(link);
|
||||
menu.Items.Add(download);
|
||||
|
||||
var copy = new MenuItem { Header = "링크 주소 복사" };
|
||||
copy.Click += (_, _) => { try { Clipboard.SetText(link); } catch { } };
|
||||
menu.Items.Add(copy);
|
||||
menu.Items.Add(new Separator());
|
||||
}
|
||||
|
||||
foreach (var builtin in ce.MenuItems)
|
||||
{
|
||||
if (builtin.Kind == CoreWebView2ContextMenuItemKind.Separator)
|
||||
{
|
||||
if (menu.Items.Count > 0 && menu.Items[^1] is not Separator) menu.Items.Add(new Separator());
|
||||
continue;
|
||||
}
|
||||
if (builtin.Kind != CoreWebView2ContextMenuItemKind.Command) continue;
|
||||
var mi = new MenuItem { Header = builtin.Label, IsEnabled = builtin.IsEnabled };
|
||||
var commandId = builtin.CommandId;
|
||||
mi.Click += (_, _) =>
|
||||
{
|
||||
try { ce.SelectedCommandId = commandId; ce.Handled = true; } catch { }
|
||||
};
|
||||
menu.Items.Add(mi);
|
||||
}
|
||||
while (menu.Items.Count > 0 && menu.Items[^1] is Separator) menu.Items.RemoveAt(menu.Items.Count - 1);
|
||||
|
||||
var view = ActiveView;
|
||||
if (view != null)
|
||||
{
|
||||
var pt = view.PointToScreen(new Point(ce.Location.X, ce.Location.Y));
|
||||
menu.PlacementTarget = view;
|
||||
menu.Placement = System.Windows.Controls.Primitives.PlacementMode.AbsolutePoint;
|
||||
menu.HorizontalOffset = pt.X;
|
||||
menu.VerticalOffset = pt.Y;
|
||||
}
|
||||
menu.IsOpen = true;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void QueueLinkForDownload(string? link)
|
||||
{
|
||||
if (string.IsNullOrEmpty(link)) return;
|
||||
if (!Uri.TryCreate(link, UriKind.Absolute, out var uri)) return;
|
||||
var referer = Cv?.Source ?? link;
|
||||
|
||||
if (SocialSiteDetector.TryMatch(uri, out var normalized, out var label))
|
||||
{
|
||||
_manager.LogPanel($"컨텍스트 메뉴 소셜 감지 [{label}]: {normalized}");
|
||||
_manager.OnDetected(new DetectionContext(normalized, normalized, MediaKind.Social, Tag: label));
|
||||
return;
|
||||
}
|
||||
var kind = MediaDetector.Classify(link, "", -1, _config) ?? MediaKind.Direct;
|
||||
_manager.LogPanel($"컨텍스트 메뉴 감지 [{kind}]: {link}");
|
||||
_manager.OnDetected(new DetectionContext(link, referer, kind, ""));
|
||||
}
|
||||
|
||||
// ---- 카드 액션 ----
|
||||
private void CardDownload_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue