using System.Collections.ObjectModel; using System.IO; using System.Text.Json; namespace VideoDownloader.App.Browser.Features; /// 팔레트 명령(T2-1, Arc Command Bar / Vivaldi Quick Commands 패턴). public sealed record PaletteCommand(string Title, string Keywords, Action Execute, string? Category = null); /// 명령 등록·검색·실행. 동적 소스(탭/기록/북마크)는 검색 시점에 평가. public sealed class CommandPalette { private readonly List _static = new(); private readonly List>> _dynamic = new(); private readonly List _separators = new(); // 빈질의 표시용 구분자(Title="\u2015") public IReadOnlyList Register(string title, string keywords, Action execute, string? category = null) { var cmd = new PaletteCommand(title, keywords, execute, category); _static.Add(cmd); return new[] { cmd }; } public void RegisterDynamic(Func> source) => _dynamic.Add(source); public void RegisterSeparator() => _separators.Add(new PaletteCommand("\u2015", "", () => { })); /// 검색: 정적+동적 통합, 시작-일치 우선, 대소문자 무시. 빈 질의는 정적 전체(등록순). public List Search(string? query) { query = query?.Trim(); if (string.IsNullOrEmpty(query)) return _static.ToList(); var low = query.ToLowerInvariant(); var pool = _static.Concat(_dynamic.SelectMany(d => d())).ToList(); return pool .Select(c => new { Cmd = c, Hay = (c.Title + " " + c.Keywords).ToLowerInvariant() }) .Where(x => x.Hay.Contains(low)) .OrderByDescending(x => x.Hay.StartsWith(low) ? 1 : 0) .ThenBy(_ => 0) // 안정 정렬 유지(등록순) .Select(x => x.Cmd) .Take(12) .ToList(); } public void Execute(PaletteCommand command) => command.Execute(); } /// 사이트별 부스트 설정(T2-7, Zen/Arc Boosts 패턴). public sealed record BoostSettings(bool ForceDark, string CustomCss); /// 도메인별 부스트 영속 저장. public sealed class BoostStore { private readonly string _path; private Dictionary _map = new(StringComparer.OrdinalIgnoreCase); public BoostStore(string path) { _path = path; try { if (File.Exists(path)) { var loaded = JsonSerializer.Deserialize>(File.ReadAllText(path)); if (loaded != null) _map = loaded; } } catch { _map = new(); } } public BoostSettings Get(string? url) { var host = Normalize(url); return host != null && _map.TryGetValue(host, out var v) ? v : new BoostSettings(false, ""); } public void Set(string? url, bool forceDark, string css) { var host = Normalize(url); if (host == null) return; _map[host] = new BoostSettings(forceDark, css); try { Directory.CreateDirectory(Path.GetDirectoryName(_path)!); var tmp = _path + ".tmp"; File.WriteAllText(tmp, JsonSerializer.Serialize(_map)); File.Move(tmp, _path, overwrite: true); } catch { } } private static string? Normalize(string? url) { if (string.IsNullOrWhiteSpace(url)) return null; var raw = url.Trim(); Uri? uri = null; if (Uri.TryCreate(raw, UriKind.Absolute, out var parsed) && !string.IsNullOrEmpty(parsed.Host)) uri = parsed; else Uri.TryCreate("https://" + raw, UriKind.Absolute, out uri); // 스킴 없는 호스트 허용 if (uri == null) return null; var host = uri.Host; return host.StartsWith("www.", StringComparison.OrdinalIgnoreCase) ? host[4..] : host; } } /// 부스트 → 문서 생성 시점 주입 JS 생성(강제 다크: 반전 필터 + 미디어 보호). public static class BoostCssBuilder { public static string BuildScript(string? url, BoostSettings settings) { if (!settings.ForceDark && string.IsNullOrWhiteSpace(settings.CustomCss)) return ""; var css = ""; if (settings.ForceDark) { css += """ html { filter: invert(1) hue-rotate(180deg); background: #fff; } img, video, picture, canvas, svg, iframe, embed, object { filter: invert(1) hue-rotate(180deg); } """; } if (!string.IsNullOrWhiteSpace(settings.CustomCss)) css += settings.CustomCss + "\n"; var js = $$""" (function(){ try { var s = document.createElement('style'); s.id = 'vd-boost'; s.textContent = {{JsonSerializer.Serialize(css)}}; (document.head || document.documentElement).appendChild(s); } catch(e){} })(); """; return js; } }