refactor(3): VideoDownloader.Browser library - tabs/find/search-engine/zoom/bookmark-history stores extracted from App (66 GREEN)
This commit is contained in:
parent
6df9f81b4a
commit
e8ec1350b3
20 changed files with 63 additions and 31 deletions
|
|
@ -1,164 +0,0 @@
|
|||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace VideoDownloader.App.Browser.Tabs;
|
||||
|
||||
/// <summary>
|
||||
/// 탭 관리 순수 로직(Phase A). WebView2 생성은 주입된 팩토리/어태처가 담당해
|
||||
/// 단위 테스트에서 UI 없이 검증 가능. 닫힌 탭 스택(25) · 핀 좌측 정렬 · 세션 캡처/복원 포함.
|
||||
/// </summary>
|
||||
public sealed class TabManager
|
||||
{
|
||||
private const int MaxClosed = 25;
|
||||
|
||||
private readonly Func<TabItem> _createTab;
|
||||
private readonly Action<TabItem>? _attachView;
|
||||
private readonly List<(TabItem Tab, int Index)> _closed = new();
|
||||
|
||||
public ObservableCollection<TabItem> Tabs { get; } = new();
|
||||
public TabItem? ActiveTab { get; private set; }
|
||||
|
||||
public event Action<TabItem>? ActiveChanged;
|
||||
public event Action<TabItem>? TabAdded;
|
||||
public event Action<TabItem>? TabRemoved;
|
||||
|
||||
/// <summary>닫힌 탭(LIFO — 최신 우선). Ctrl+Shift+T 복원 대상.</summary>
|
||||
public IReadOnlyList<TabItem> ClosedStack => _closed.Select(e => e.Tab).ToList();
|
||||
|
||||
public TabManager(Func<TabItem> createTab, Action<TabItem>? attachView = null)
|
||||
{
|
||||
_createTab = createTab;
|
||||
_attachView = attachView;
|
||||
}
|
||||
|
||||
public TabItem NewTab(string? url = null, bool activate = true)
|
||||
{
|
||||
var tab = _createTab();
|
||||
if (!string.IsNullOrEmpty(url)) tab.Url = url;
|
||||
_attachView?.Invoke(tab);
|
||||
Tabs.Add(tab); // 신규 탭은 핀 여부와 무관하게 끝에(핀 상태로 만들면 SetPinned 이동)
|
||||
TabAdded?.Invoke(tab);
|
||||
if (activate) Activate(tab);
|
||||
else tab.IsActive = false;
|
||||
return tab;
|
||||
}
|
||||
|
||||
public void Activate(TabItem tab)
|
||||
{
|
||||
if (!Tabs.Contains(tab)) return;
|
||||
foreach (var t in Tabs) t.IsActive = ReferenceEquals(t, tab);
|
||||
ActiveTab = tab;
|
||||
tab.LastActivated = DateTime.UtcNow;
|
||||
ActiveChanged?.Invoke(tab);
|
||||
}
|
||||
|
||||
/// <summary>탭 닫기. 활성 탭이면 오른쪽 이웃(없으면 왼쪽) 활성화. 마지막 탭이면 새 빈 탭으로 대체.</summary>
|
||||
public bool CloseTab(TabItem tab)
|
||||
{
|
||||
var index = Tabs.IndexOf(tab);
|
||||
if (index < 0) return false;
|
||||
|
||||
_closed.Insert(0, (tab, index));
|
||||
while (_closed.Count > MaxClosed) _closed.RemoveAt(_closed.Count - 1);
|
||||
tab.ClosedIndex = index;
|
||||
|
||||
var wasActive = ReferenceEquals(ActiveTab, tab);
|
||||
Tabs.RemoveAt(index);
|
||||
TabRemoved?.Invoke(tab);
|
||||
|
||||
if (Tabs.Count == 0)
|
||||
{
|
||||
NewTab(); // 창 유지 — 마지막 탭은 새 빈 탭으로 대체
|
||||
}
|
||||
else if (wasActive)
|
||||
{
|
||||
var next = Tabs[Math.Min(index, Tabs.Count - 1)]; // 오른쪽 우선, 끝이면 왼쪽
|
||||
Activate(next);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Ctrl+Shift+T: 닫힌 탭을 닫힌 위치에 복원.</summary>
|
||||
public TabItem? ReopenClosedTab()
|
||||
{
|
||||
if (_closed.Count == 0) return null;
|
||||
var (tab, index) = _closed[0];
|
||||
_closed.RemoveAt(0);
|
||||
|
||||
if (tab.View == null) _attachView?.Invoke(tab); // 뷰가 정리된 경우 재생성
|
||||
Tabs.Insert(Math.Min(index, Tabs.Count), tab);
|
||||
TabAdded?.Invoke(tab);
|
||||
Activate(tab);
|
||||
return tab;
|
||||
}
|
||||
|
||||
public void ActivateNext()
|
||||
{
|
||||
if (ActiveTab == null || Tabs.Count < 2) return;
|
||||
var i = Tabs.IndexOf(ActiveTab);
|
||||
Activate(Tabs[(i + 1) % Tabs.Count]);
|
||||
}
|
||||
|
||||
public void ActivatePrevious()
|
||||
{
|
||||
if (ActiveTab == null || Tabs.Count < 2) return;
|
||||
var i = Tabs.IndexOf(ActiveTab);
|
||||
Activate(Tabs[(i - 1 + Tabs.Count) % Tabs.Count]);
|
||||
}
|
||||
|
||||
public void ActivateAt(int index)
|
||||
{
|
||||
if (index >= 0 && index < Tabs.Count) Activate(Tabs[index]);
|
||||
}
|
||||
|
||||
public void ActivateLast() => ActivateAt(Tabs.Count - 1);
|
||||
|
||||
public void Move(TabItem tab, int newIndex)
|
||||
{
|
||||
var old = Tabs.IndexOf(tab);
|
||||
if (old < 0) return;
|
||||
newIndex = Math.Clamp(newIndex, 0, Tabs.Count - 1);
|
||||
if (newIndex == old) return;
|
||||
Tabs.Move(old, newIndex);
|
||||
}
|
||||
|
||||
/// <summary>핀 설정. 핀 탭은 좌측 끝에 정렬(Chrome 동작).</summary>
|
||||
public void SetPinned(TabItem tab, bool pinned)
|
||||
{
|
||||
if (!Tabs.Contains(tab)) return;
|
||||
tab.IsPinned = pinned;
|
||||
if (pinned)
|
||||
{
|
||||
var target = Tabs.Count(t => t.IsPinned) - 1; // 자신 포함 핀 탭 수 - 1
|
||||
Move(tab, Math.Max(0, target));
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = Tabs.Count(t => t.IsPinned); // 핀 탭 블록 바로 뒤
|
||||
Move(tab, target);
|
||||
}
|
||||
}
|
||||
|
||||
public SessionSnapshot CaptureSession()
|
||||
{
|
||||
var active = ActiveTab == null ? 0 : Math.Max(0, Tabs.IndexOf(ActiveTab));
|
||||
return new SessionSnapshot(
|
||||
Tabs.Select(t => t.Url).ToList(),
|
||||
active,
|
||||
Tabs.Select(t => t.IsPinned).ToList());
|
||||
}
|
||||
|
||||
public void RestoreSession(SessionSnapshot snap)
|
||||
{
|
||||
foreach (var t in Tabs.ToList())
|
||||
{
|
||||
Tabs.Remove(t);
|
||||
TabRemoved?.Invoke(t);
|
||||
}
|
||||
for (var i = 0; i < snap.Urls.Count; i++)
|
||||
{
|
||||
var tab = NewTab(snap.Urls[i], activate: false);
|
||||
if (i < snap.Pinned.Count && snap.Pinned[i]) tab.IsPinned = true; // 순서 유지(핀 우선 저장됨)
|
||||
}
|
||||
ActivateAt(Math.Clamp(snap.ActiveIndex, 0, Math.Max(0, Tabs.Count - 1)));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue