_workspaces = new();
private bool _verticalTabs;
private TabItem? _splitTab;
@@ -101,6 +110,9 @@ public partial class MainWindow : Window
_resolver = new SearchEngineResolver(_config.SearchEngine);
_zoomStore = new SiteZoomStore(Path.Combine(AppDataDir, "sitezoom.json"));
_boostStore = new BoostStore(Path.Combine(AppDataDir, "boost.json"));
+ _routingRules = new WorkspaceRoutingRules(Path.Combine(AppDataDir, "routing.json")); // T3-1
+ _tabArchive = new TabArchive(Path.Combine(AppDataDir, "archive.json")); // T3-2
+ _notes = new NotesStore(Path.Combine(AppDataDir, "notes.json")); // T3-12
try
{
var wsPath = Path.Combine(AppDataDir, "workspaces.json");
@@ -434,16 +446,22 @@ public partial class MainWindow : Window
private void OnTabRemoved(TabItem tab)
{
if (ReferenceEquals(_splitTab, tab)) ExitSplit();
+ _tabArchive.Add(tab.Title, tab.Url); // T3-2 닫힘 → 아카이브(영속, 500 상한)
_views?.RemoveView(tab);
}
private void UpdateNavState()
{
- var cv = Cv;
Dispatcher.BeginInvoke(new Action(() =>
{
- NavBack.IsEnabled = cv?.CanGoBack == true;
- NavForward.IsEnabled = cv?.CanGoForward == true;
+ try
+ {
+ var cv = Cv; // dispose 이후 이벤트 경로 방어
+ NavBack.IsEnabled = cv?.CanGoBack == true;
+ NavForward.IsEnabled = cv?.CanGoForward == true;
+ }
+ catch (ObjectDisposedException) { }
+ catch (InvalidOperationException) { }
}));
}
@@ -717,6 +735,16 @@ public partial class MainWindow : Window
// T1-4: URL/검색어 판별 + 기본 엔진·키워드 전환(g, yt, ...)은 리졸버가 담당
var intent = _resolver.Resolve(url);
if (intent == null) return;
+
+ // T3-1: 도메인 라우팅 규칙 — 대상 워크스페이스가 다르면 자동 전환 후 이동(Arc ATC 패턴)
+ var routeTo = _routingRules.Match(intent.Target);
+ if (routeTo != null && routeTo != _currentWorkspaceName &&
+ _workspaces.FirstOrDefault(w => w.Name == routeTo) is { } wsTarget)
+ {
+ SwitchWorkspace(wsTarget);
+ _currentWorkspaceName = routeTo;
+ Log($"라우팅 규칙 → 워크스페이스 '{routeTo}' 전환");
+ }
Cv?.Navigate(intent.Target);
}
@@ -778,6 +806,8 @@ public partial class MainWindow : Window
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.D) { ToggleBoostDark(); e.Handled = true; }
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.S) { _ = TakeScreenshot(); e.Handled = true; }
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.N) { OpenInPrivateTab(); e.Handled = true; }
+ else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.M) { ToggleCompactMode(); e.Handled = true; } // T3-13
+ else if (mods == (ModifierKeys.Control | ModifierKeys.Alt) && e.Key == Key.N) { ToggleNotes(); e.Handled = true; } // T3-12
else if (mods == (ModifierKeys.Alt | ModifierKeys.Control) && e.Key == Key.V) { ToggleVerticalTabs(); e.Handled = true; }
else if (mods == (ModifierKeys.Alt | ModifierKeys.Control) && e.Key == Key.H) { ToggleSplit(); e.Handled = true; }
else if (mods == ModifierKeys.Control && e.Key == Key.U) { ViewSource(); e.Handled = true; }
@@ -1243,6 +1273,20 @@ public partial class MainWindow : Window
_palette.Register("미디어 재생목록", "playlist play media 음악 영상", () => { PlaylistPopup.IsOpen = true; });
_palette.Register("재생목록 재생", "playlist play start", OpenPlayer);
_palette.Register("기기 연결 (QR 페어링)", "remote pair qr mobile phone", ShowPairing);
+ // ---- T3 잔여(데스크톱) ----
+ _palette.Register("이 사이트 흑백", "grayscale filter page action 흑백", () => ApplyPageFilter(PageFilter.Grayscale)); // T3-6
+ _palette.Register("이 사이트 세피아", "sepia filter page action 세피아", () => ApplyPageFilter(PageFilter.Sepia));
+ _palette.Register("페이지 액션 끄기", "filter off reset", () => ApplyPageFilter(PageFilter.None));
+ _palette.Register("노트 패널", "notes note 메모", ToggleNotes); // T3-12
+ _palette.Register("노트: 현재 페이지 추가", "note add", () => NotesAddPage_Click(this, new RoutedEventArgs()));
+ _palette.Register("컴팩트 모드 토글", "compact chrome hide ui", ToggleCompactMode); // T3-13
+ _palette.RegisterDynamic(() => _workspaces // T3-1: 사이트→워크스페이스 규칙
+ .Select(w => new PaletteCommand($"라우팅 규칙: 이 사이트 → '{w.Name}'", "routing rule workspace",
+ () => AddRoutingRuleForCurrentSite(w.Name))).ToList());
+ _palette.RegisterDynamic(() => _tabArchive.Search("") // T3-2: 아카이브 복구
+ .Take(8)
+ .Select(a => new PaletteCommand($"아카이브에서 열기: {a.Title}", $"archive reopen {a.Url}",
+ () => { var t = _tabs.NewTab(); Cv2Navigate(t, a.Url); })).ToList());
_palette.Register("원격 접근 켜기/끄기", "remote server toggle lan", async () => await ToggleRemoteAccess());
_palette.RegisterSeparator();
_palette.RegisterDynamic(() => _tabs.Tabs
@@ -1304,6 +1348,112 @@ public partial class MainWindow : Window
Log(!cur.ForceDark ? "이 사이트 강제 다크 켜짐" : "이 사이트 강제 다크 꺼짐");
}
+ // ---- T3-6 페이지 액션(흑백/세피아 — Boost 통합) ----
+ internal void ApplyPageFilter(PageFilter filter)
+ {
+ var url = Cv?.Source;
+ if (string.IsNullOrEmpty(url)) { Log("페이지가 없습니다"); return; }
+ var cur = _boostStore.Get(url);
+ _boostStore.Set(url, cur.ForceDark, cur.CustomCss, filter);
+ _views?.ApplyBoostNow(_tabs.ActiveTab!);
+ Log(filter switch
+ {
+ PageFilter.Grayscale => "이 사이트 흑백 적용",
+ PageFilter.Sepia => "이 사이트 세피아 적용",
+ _ => "페이지 액션 해제",
+ });
+ }
+
+ // ---- T3-1 도메인 라우팅 규칙 ----
+ private void AddRoutingRuleForCurrentSite(string workspaceName)
+ {
+ var url = Cv?.Source;
+ if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out var u))
+ { Log("페이지가 없습니다"); return; }
+ var host = u.Host;
+ if (host.StartsWith("www.", StringComparison.OrdinalIgnoreCase)) host = host[4..];
+ _routingRules.Add(host, workspaceName);
+ Log($"라우팅 규칙 추가: {host} → '{workspaceName}' (다음 이동부터 자동 전환)");
+ }
+
+ // ---- T3-2 아카이브 복구 보조 ----
+ private static void Cv2Navigate(TabItem tab, string url)
+ {
+ try { tab.View?.CoreWebView2?.Navigate(url); } catch { }
+ }
+
+ // ---- T3-12 노트 패널 ----
+ internal void ToggleNotes()
+ {
+ RefreshNotesList();
+ NotesPopup.IsOpen = true;
+ }
+
+ private void RefreshNotesList()
+ {
+ NotesList.ItemsSource = _notes.List()
+ .Select(n => $"[{n.CreatedAt:MM-dd HH:mm}] {n.Title} — {n.Body}")
+ .ToList();
+ NotesList.DisplayMemberPath = null;
+ }
+
+ internal void NotesAddPage_Click(object sender, RoutedEventArgs e)
+ {
+ var url = Cv?.Source ?? "";
+ var title = _tabs.ActiveTab?.Title ?? url;
+ _notes.Add(title, url);
+ Log("노트에 페이지 추가됨");
+ RefreshNotesList();
+ }
+
+ private void NotesDelete_Click(object sender, RoutedEventArgs e)
+ {
+ var idx = NotesList.SelectedIndex;
+ var list = _notes.List();
+ if (idx >= 0 && idx < list.Count)
+ {
+ _notes.Delete(list[idx].Id);
+ RefreshNotesList();
+ }
+ }
+
+ // ---- T3-13 컴팩트 모드(크롬 숨김 + 상단 엣지 호버 복원) ----
+ internal void ToggleCompactMode()
+ {
+ _compactMode = !_compactMode;
+ _chromeRevealed = false;
+ ApplyChromeVisibility();
+ Log(_compactMode ? "컴팩트 모드: 크롤 숨김 (상단 가장자리에 마우스를 올리면 복원)" : "컴팩트 모드 해제");
+ }
+
+ private void ApplyChromeVisibility()
+ {
+ var chromeRows = new[] { 0, 1, 2 }; // 툴바/북마크바/탭스트립
+ foreach (var i in chromeRows)
+ MainGrid.RowDefinitions[i].Height = (_compactMode && !_chromeRevealed) ? new GridLength(0) : GridLength.Auto;
+ if (!_compactMode) _chromeRevealed = false;
+ }
+
+ protected override void OnPreviewMouseMove(System.Windows.Input.MouseEventArgs e)
+ {
+ if (_compactMode)
+ {
+ var p = e.GetPosition(this);
+ var wantReveal = p.Y <= 4; // 상단 엣지 4px
+ if (wantReveal != _chromeRevealed)
+ {
+ _chromeRevealed = wantReveal;
+ ApplyChromeVisibility();
+ }
+ else if (_chromeRevealed && p.Y > 120) // 크롬 영역 벗어나면 다시 숨김
+ {
+ _chromeRevealed = false;
+ ApplyChromeVisibility();
+ }
+ }
+ base.OnPreviewMouseMove(e);
+ }
+
// ---- T2-9 스크린샷 ----
private async Task TakeScreenshot()
{
@@ -1490,7 +1640,7 @@ public partial class MainWindow : Window
if (cv == null) return;
var tiles = _bookmarks.Take(18).Select(b =>
$"🌐
{System.Net.WebUtility.HtmlEncode(b.Title)}
").ToList();
- var dial = "새 탭
" + string.Join("", tiles) + $"
주소창에 검색어나 URL 을 입력하세요 · 즐겨찾기 {_bookmarks.Count}개 중 {tiles.Count}개 표시
";
+ var dial = "새 탭
" + string.Join("", tiles) + $"
주소창에 검색어나 URL 을 입력하세요 · 즐겨찾기 {_bookmarks.Count}개 중 {tiles.Count}개 표시
";
cv.NavigateToString(dial);
}
@@ -1575,6 +1725,7 @@ public partial class MainWindow : Window
{
ExitSplit();
_tabs.SwitchWorkspace(ws);
+ _currentWorkspaceName = ws.Name; // T3-1 라우팅 기준
Log($"워크스페이스 '{ws.Name}' 전환 ({ws.Urls.Count} 탭)");
}
diff --git a/VideoDownloader.App/Themes/Dark.xaml b/VideoDownloader.App/Themes/Dark.xaml
index 8a4d0b3..c8eb2f4 100644
--- a/VideoDownloader.App/Themes/Dark.xaml
+++ b/VideoDownloader.App/Themes/Dark.xaml
@@ -12,11 +12,11 @@
-
+
-
-
+
+
diff --git a/VideoDownloader.App/Themes/Light.xaml b/VideoDownloader.App/Themes/Light.xaml
index af93ede..002f125 100644
--- a/VideoDownloader.App/Themes/Light.xaml
+++ b/VideoDownloader.App/Themes/Light.xaml
@@ -15,8 +15,8 @@
-
-
+
+
diff --git a/VideoDownloader.App/Themes/Shared.xaml b/VideoDownloader.App/Themes/Shared.xaml
index b77a402..cc36fc3 100644
--- a/VideoDownloader.App/Themes/Shared.xaml
+++ b/VideoDownloader.App/Themes/Shared.xaml
@@ -1,10 +1,23 @@
-
- Segoe UI Variable Text, Segoe UI
+
+ Segoe UI Variable Text, Segoe UI, Malgun Gothic
Segoe Fluent Icons
+
+
+
+