데스크톱 T3 잔여 완성 + 스타일 폴리시: 도메인 라우팅(WorkspaceRoutingRules 자동 워크스페이스 전환), 탭 아카이브(TabArchive 영속·팔레트 복구), 페이지 액션(흑백/세피아 필터), 노트 패널(NotesStore+플라이아웃), 컴팩트 모드(크롬 숨김+엣지 호버). 스타일: 한국어 폰트 폴백, 암시 폰트 통일, 다크 툴팁, 텍스트 대비 상향. UpdateNavState dispose 가드. 164/164 GREEN + 실창 스크린샷 검증
This commit is contained in:
parent
04ff0ff93d
commit
f380518919
12 changed files with 594 additions and 24 deletions
|
|
@ -50,6 +50,15 @@ public partial class MainWindow : Window
|
|||
// Phase C: 팔레트/부스트/워크스페이스/레이아웃
|
||||
private readonly CommandPalette _palette = new();
|
||||
private BoostStore _boostStore = null!;
|
||||
private WorkspaceRoutingRules _routingRules = null!; // T3-1
|
||||
private TabArchive _tabArchive = null!; // T3-2
|
||||
internal TabArchive Archive => _tabArchive; // E2E 검증용
|
||||
internal BoostStore BoostStore => _boostStore; // E2E 검증용
|
||||
internal NotesStore Notes => _notes; // E2E 검증용
|
||||
private NotesStore _notes = null!; // T3-12
|
||||
private string? _currentWorkspaceName; // T3-1 라우팅 기준
|
||||
private bool _compactMode; // T3-13
|
||||
private bool _chromeRevealed; // T3-13 호버 복원 상태
|
||||
private List<WorkspaceSnapshot> _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 =>
|
||||
$"<a href='{System.Net.WebUtility.HtmlEncode(b.Url)}' style='display:block;text-decoration:none;background:#262736;color:#e6e6ec;border-radius:12px;padding:18px 12px;text-align:center;font-size:12.5px;min-width:110px' onmouseover='this.style.background=#2f3042' onmouseout='this.style.background=#262736'><div style='font-size:26px;margin-bottom:8px'>🌐</div><div style='overflow:hidden;text-overflow:ellipsis;white-space:nowrap'>{System.Net.WebUtility.HtmlEncode(b.Title)}</div></a>").ToList();
|
||||
var dial = "<!DOCTYPE html><html><head><meta charset='utf-8'><style>body{background:#14151d;color:#e6e6ec;font-family:'Segoe UI',sans-serif;margin:0;padding:48px 24px}h1{font-size:20px;text-align:center;margin-bottom:28px;font-weight:600}.grid{display:flex;flex-wrap:wrap;gap:14px;justify-content:center;max-width:760px;margin:0 auto}.hint{text-align:center;color:#8a8aa0;font-size:12px;margin-top:36px}a{color:inherit}</style></head><body><h1>새 탭</h1><div class='grid'>" + string.Join("", tiles) + $"</div><div class='hint'>주소창에 검색어나 URL 을 입력하세요 · 즐겨찾기 {_bookmarks.Count}개 중 {tiles.Count}개 표시</div></body></html>";
|
||||
var dial = "<!DOCTYPE html><html><head><meta charset='utf-8'><style>body{background:#14151d;color:#e6e6ec;font-family:'Segoe UI Variable Text','Segoe UI','Malgun Gothic',sans-serif;margin:0;padding:48px 24px}h1{font-size:20px;text-align:center;margin-bottom:28px;font-weight:600}.grid{display:flex;flex-wrap:wrap;gap:14px;justify-content:center;max-width:760px;margin:0 auto}.hint{text-align:center;color:#9aa0b4;font-size:12.5px;margin-top:36px}a{color:inherit}</style></head><body><h1>새 탭</h1><div class='grid'>" + string.Join("", tiles) + $"</div><div class='hint'>주소창에 검색어나 URL 을 입력하세요 · 즐겨찾기 {_bookmarks.Count}개 중 {tiles.Count}개 표시</div></body></html>";
|
||||
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} 탭)");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue