DictHandler.cs (신규, 300줄+):
- prefix=dict, 오프라인 국어·영한 사전
- 국어: 혼동어·업무용어·한자어 48개 (가르치다/가리키다, 품의/결재/결제 등)
- 영한: 업무 영어 25개 (agenda, align, approve, deploy 등) + 예문
- dict {단어} → 뜻풀이·유의어·반의어·주의사항
- dict en {word} → 영한 검색, Enter: 클립보드 복사
FlowHandler.cs (신규, 237줄):
- prefix=flow, 명령 체인 워크플로우
- flow add {이름} {cmd1} > {cmd2} > ... 텍스트 기반 워크플로우 저장
- %APPDATA%\AxCopilot\flows.json 로컬 JSON 저장
- flow {이름} → 명령 목록 클립보드 복사, flow del 삭제
- Alfred 워크플로우 경량 대응
SpellHandler.cs (수정, +144줄):
- spell add {틀린} {올바른} [설명] 사용자 항목 추가
- spell del {틀린} 삭제, spell custom 사용자 항목만 보기
- %APPDATA%\AxCopilot\spell_custom.json 저장
- AllEntries() 제너레이터로 내장+사용자 통합 검색
BookmarkHandler.cs (수정, +2줄):
- 검색 결과에 Group="📑 북마크" 카테고리 헤더 설정
App.xaml.cs: DictHandler, FlowHandler 등록 (L29 블록)
LauncherWindow.ShortcutHelp.cs: F3 빠른 미리보기 도움말 추가
LAUNCHER_ROADMAP.md: L29 ✅ 완료, 123개 핸들러
- 빌드: 경고 0, 오류 0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
182 lines
6.4 KiB
C#
182 lines
6.4 KiB
C#
using System.IO;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Services;
|
|
using AxCopilot.Themes;
|
|
|
|
namespace AxCopilot.Handlers;
|
|
|
|
/// <summary>
|
|
/// Chrome / Edge 브라우저 북마크를 검색합니다.
|
|
/// 프리픽스 없음 — 일반 퍼지 검색에 통합됩니다.
|
|
/// 지원 브라우저: Google Chrome, Microsoft Edge
|
|
/// </summary>
|
|
public class BookmarkHandler : IActionHandler
|
|
{
|
|
public string? Prefix => null; // 퍼지 검색에 통합
|
|
|
|
public PluginMetadata Metadata => new(
|
|
"Bookmarks",
|
|
"Chrome / Edge 북마크 검색",
|
|
"1.0",
|
|
"AX");
|
|
|
|
// 캐시 (앱 세션 중 북마크가 자주 바뀌지 않으므로 한 번 로드 후 재사용)
|
|
private List<BookmarkEntry>? _cache;
|
|
private DateTime _cacheTime = DateTime.MinValue;
|
|
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(5);
|
|
|
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(query))
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(Array.Empty<LauncherItem>());
|
|
|
|
RefreshCacheIfNeeded();
|
|
|
|
if (_cache == null || _cache.Count == 0)
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(Array.Empty<LauncherItem>());
|
|
|
|
var q = query.Trim().ToLowerInvariant();
|
|
var results = _cache
|
|
.Where(b => b.Name.ToLowerInvariant().Contains(q)
|
|
|| (b.Url?.ToLowerInvariant().Contains(q) ?? false))
|
|
.Take(8)
|
|
.Select(b => new LauncherItem(
|
|
b.Name,
|
|
b.Url ?? "",
|
|
null,
|
|
b.Url,
|
|
Symbol: Symbols.Globe,
|
|
Group: "📑 북마크"))
|
|
.ToList();
|
|
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(results);
|
|
}
|
|
|
|
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
|
{
|
|
if (item.Data is string url && !string.IsNullOrWhiteSpace(url))
|
|
{
|
|
try
|
|
{
|
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url)
|
|
{ UseShellExecute = true });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogService.Warn($"북마크 열기 실패: {ex.Message}");
|
|
}
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// ─── 캐시 ────────────────────────────────────────────────────────────────
|
|
|
|
private void RefreshCacheIfNeeded()
|
|
{
|
|
if (_cache != null && DateTime.Now - _cacheTime < CacheTtl) return;
|
|
|
|
var bookmarks = new List<BookmarkEntry>();
|
|
|
|
foreach (var file in GetBookmarkFiles())
|
|
{
|
|
try
|
|
{
|
|
var json = File.ReadAllText(file);
|
|
ParseChromeBookmarks(json, bookmarks);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogService.Warn($"북마크 파일 읽기 실패: {file} — {ex.Message}");
|
|
}
|
|
}
|
|
|
|
_cache = bookmarks;
|
|
_cacheTime = DateTime.Now;
|
|
LogService.Info($"북마크 로드 완료: {bookmarks.Count}개");
|
|
}
|
|
|
|
// ─── 북마크 파일 경로 ─────────────────────────────────────────────────────
|
|
|
|
private static IEnumerable<string> GetBookmarkFiles()
|
|
{
|
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
|
|
|
// Chrome (안정, 베타, 개발, 카나리아)
|
|
var chromePaths = new[]
|
|
{
|
|
Path.Combine(localAppData, "Google", "Chrome", "User Data"),
|
|
Path.Combine(localAppData, "Google", "Chrome Beta", "User Data"),
|
|
Path.Combine(localAppData, "Google", "Chrome Dev", "User Data"),
|
|
Path.Combine(localAppData, "Google", "Chrome SxS", "User Data"),
|
|
};
|
|
|
|
// Edge
|
|
var edgePaths = new[]
|
|
{
|
|
Path.Combine(localAppData, "Microsoft", "Edge", "User Data"),
|
|
Path.Combine(localAppData, "Microsoft", "Edge Beta", "User Data"),
|
|
Path.Combine(localAppData, "Microsoft", "Edge Dev", "User Data"),
|
|
Path.Combine(localAppData, "Microsoft", "Edge Canary", "User Data"),
|
|
};
|
|
|
|
foreach (var profileRoot in chromePaths.Concat(edgePaths))
|
|
{
|
|
if (!Directory.Exists(profileRoot)) continue;
|
|
|
|
// Default 프로파일
|
|
var defaultBookmark = Path.Combine(profileRoot, "Default", "Bookmarks");
|
|
if (File.Exists(defaultBookmark)) yield return defaultBookmark;
|
|
|
|
// Profile 1, 2, ... 프로파일
|
|
foreach (var dir in Directory.GetDirectories(profileRoot, "Profile *"))
|
|
{
|
|
var f = Path.Combine(dir, "Bookmarks");
|
|
if (File.Exists(f)) yield return f;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Chrome/Edge JSON 파싱 ───────────────────────────────────────────────
|
|
|
|
private static void ParseChromeBookmarks(string json, List<BookmarkEntry> result)
|
|
{
|
|
var doc = JsonNode.Parse(json);
|
|
var roots = doc?["roots"];
|
|
if (roots == null) return;
|
|
|
|
foreach (var rootKey in new[] { "bookmark_bar", "other", "synced" })
|
|
{
|
|
var node = roots[rootKey];
|
|
if (node != null) WalkNode(node, result);
|
|
}
|
|
}
|
|
|
|
private static void WalkNode(JsonNode node, List<BookmarkEntry> result)
|
|
{
|
|
var type = node["type"]?.GetValue<string>();
|
|
|
|
if (type == "url")
|
|
{
|
|
var name = node["name"]?.GetValue<string>() ?? "";
|
|
var url = node["url"]?.GetValue<string>() ?? "";
|
|
if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(url))
|
|
result.Add(new BookmarkEntry(name, url));
|
|
return;
|
|
}
|
|
|
|
if (type == "folder")
|
|
{
|
|
var children = node["children"]?.AsArray();
|
|
if (children == null) return;
|
|
foreach (var child in children)
|
|
{
|
|
if (child != null) WalkNode(child, result);
|
|
}
|
|
}
|
|
}
|
|
|
|
private record BookmarkEntry(string Name, string? Url);
|
|
}
|