데스크톱 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
63
VideoDownloader.Browser/NotesStore.cs
Normal file
63
VideoDownloader.Browser/NotesStore.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace VideoDownloader.Browser;
|
||||
|
||||
public sealed record NoteItem(string Id, string Title, string Body, DateTime CreatedAt);
|
||||
|
||||
/// <summary>T3-12: 노트 패널 저장소 — 마크다운 텍스트 노트 추가·검색·삭제, JSON 영속.</summary>
|
||||
public sealed class NotesStore
|
||||
{
|
||||
private readonly string _path;
|
||||
private readonly object _gate = new();
|
||||
private List<NoteItem> _items;
|
||||
|
||||
public NotesStore(string path)
|
||||
{
|
||||
_path = path;
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
_items = JsonSerializer.Deserialize<List<NoteItem>>(File.ReadAllText(path)) ?? new();
|
||||
}
|
||||
catch { }
|
||||
_items ??= new List<NoteItem>();
|
||||
}
|
||||
|
||||
public string Add(string title, string body)
|
||||
{
|
||||
var note = new NoteItem(Guid.NewGuid().ToString("N"), title, body, DateTime.Now);
|
||||
lock (_gate)
|
||||
{
|
||||
_items.Insert(0, note);
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_path)!);
|
||||
File.WriteAllText(_path, JsonSerializer.Serialize(_items));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return note.Id;
|
||||
}
|
||||
|
||||
public List<NoteItem> List()
|
||||
{
|
||||
lock (_gate) return _items.ToList();
|
||||
}
|
||||
|
||||
public List<NoteItem> Search(string query)
|
||||
{
|
||||
var q = query?.Trim() ?? "";
|
||||
return List().Where(n => q.Length == 0 ||
|
||||
n.Title.Contains(q, StringComparison.OrdinalIgnoreCase) ||
|
||||
n.Body.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
public void Delete(string id)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_items.RemoveAll(n => n.Id == id);
|
||||
try { File.WriteAllText(_path, JsonSerializer.Serialize(_items)); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
63
VideoDownloader.Browser/TabArchive.cs
Normal file
63
VideoDownloader.Browser/TabArchive.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace VideoDownloader.Browser;
|
||||
|
||||
public sealed record ArchivedTab(string Title, string Url, DateTime ClosedAt);
|
||||
|
||||
/// <summary>T3-2: 탭 아카이브 — 닫힘 이력 영속(최대 500, 같은 URL 연속은 제목 갱신), 검색·복구 지원.</summary>
|
||||
public sealed class TabArchive
|
||||
{
|
||||
private const int Cap = 500;
|
||||
private readonly string _path;
|
||||
private readonly object _gate = new();
|
||||
private List<ArchivedTab> _items;
|
||||
|
||||
public TabArchive(string path)
|
||||
{
|
||||
_path = path;
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
_items = JsonSerializer.Deserialize<List<ArchivedTab>>(File.ReadAllText(path)) ?? new();
|
||||
}
|
||||
catch { }
|
||||
_items ??= new List<ArchivedTab>();
|
||||
}
|
||||
|
||||
public void Add(string title, string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return;
|
||||
lock (_gate)
|
||||
{
|
||||
_items.RemoveAll(t => t.Url.Equals(url, StringComparison.OrdinalIgnoreCase));
|
||||
_items.Insert(0, new ArchivedTab(title, url, DateTime.Now));
|
||||
if (_items.Count > Cap) _items.RemoveRange(Cap, _items.Count - Cap);
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_path)!);
|
||||
File.WriteAllText(_path, JsonSerializer.Serialize(_items));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public List<ArchivedTab> Search(string query)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var q = query?.Trim() ?? "";
|
||||
return _items.Where(t => q.Length == 0 ||
|
||||
t.Title.Contains(q, StringComparison.OrdinalIgnoreCase) ||
|
||||
t.Url.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(string url)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_items.RemoveAll(t => t.Url.Equals(url, StringComparison.OrdinalIgnoreCase));
|
||||
try { File.WriteAllText(_path, JsonSerializer.Serialize(_items)); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
65
VideoDownloader.Browser/WorkspaceRoutingRules.cs
Normal file
65
VideoDownloader.Browser/WorkspaceRoutingRules.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace VideoDownloader.Browser;
|
||||
|
||||
/// <summary>T3-1: 도메인 → 워크스페이스 자동 라우팅 규칙(Vivaldi 규칙/Arc ATC).
|
||||
/// 주소창 이동 시 매칭되면 해당 워크스페이스로 자동 전환한다. JSON 영속.</summary>
|
||||
public sealed class WorkspaceRoutingRules
|
||||
{
|
||||
private readonly string _path;
|
||||
private Dictionary<string, string> _map = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public WorkspaceRoutingRules(string path)
|
||||
{
|
||||
_path = path;
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
_map = JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(path)) ?? new();
|
||||
}
|
||||
catch { }
|
||||
_map ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, string> Rules => _map;
|
||||
|
||||
public void Add(string domain, string workspaceName)
|
||||
{
|
||||
var d = domain.Trim().TrimStart('.', 'w', 'w', 'w', '.');
|
||||
_map[d] = workspaceName;
|
||||
Save();
|
||||
}
|
||||
|
||||
public void Remove(string domain)
|
||||
{
|
||||
_map.Remove(domain.Trim());
|
||||
Save();
|
||||
}
|
||||
|
||||
/// <summary>URL 이 매칭되는 워크스페이스명(없으면 null). www/서브도메인 허용.</summary>
|
||||
public string? Match(string? url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return null;
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var u) ||
|
||||
(u.Scheme != Uri.UriSchemeHttp && u.Scheme != Uri.UriSchemeHttps)) return null;
|
||||
var host = u.Host;
|
||||
if (host.StartsWith("www.", StringComparison.OrdinalIgnoreCase)) host = host[4..];
|
||||
foreach (var (domain, ws) in _map)
|
||||
{
|
||||
if (host.Equals(domain, StringComparison.OrdinalIgnoreCase) ||
|
||||
host.EndsWith("." + domain, StringComparison.OrdinalIgnoreCase))
|
||||
return ws;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_path)!);
|
||||
File.WriteAllText(_path, JsonSerializer.Serialize(_map));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue