UI 복잡 버전 234 RED→GREEN 완료(docs/UI_USECASES.md §A~§J): 라이브 테마 전환(DynamicResource 전환+ThemeResolver+HighContrast 테마, 재시작 없음), 디자인 토큰 체계·WCAG 대비 결함 5건 수정, 설정창 카테고리 내비/검색 재구성+AppConfig 40여 속성, 다운로드 관리자(속도/ETA/세그먼트맵/전체 일시정지-이어받기/CSV 내보내기/예약 게이트/중복 확인), 라이브러리·아카이브·작업공간·재생목록·즐겨찾기 편집 뷰, 접근성(자동화 속성 30여 개·커스텀 피어·docs/ACCESSIBILITY.md), 인터랙션(휠 클릭 새 탭·XButton 탐색·F12·영역 캡처·프로필 전환), 상태/알림(토스트·알림센터·온보딩·CrashReportDialog). 보강 테스트 35건+실창 E2E 4건(StaDispatcher 기반 — 프록시 설정이 기시작 핸들러를 건드리는 크래시 실결함 수정 포함) — 453/453 GREEN. 이전 미커밋 작업(CI 워크플로·Mdns·QueueHub·UpdateChecker·AGENTS/CLAUDE·빌드 스크립트) 일괄 포함
This commit is contained in:
parent
5a0ce730b2
commit
17f2f91152
88 changed files with 9463 additions and 1127 deletions
156
VideoDownloader.App/BookmarkManagerWindow.xaml.cs
Normal file
156
VideoDownloader.App/BookmarkManagerWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using VideoDownloader.Browser;
|
||||
|
||||
namespace VideoDownloader.App;
|
||||
|
||||
/// <summary>UC-K16 즐겨찾기 관리 창 — 검색·편집(M17)·삭제·HTML 내보내기/가져오기(M19).</summary>
|
||||
public partial class BookmarkManagerWindow : Window
|
||||
{
|
||||
private readonly ObservableCollection<Bookmark> _source;
|
||||
private readonly Action _persist;
|
||||
|
||||
public BookmarkManagerWindow(ObservableCollection<Bookmark> bookmarks, Action persist)
|
||||
{
|
||||
InitializeComponent();
|
||||
_source = bookmarks;
|
||||
_persist = persist;
|
||||
List.ItemsSource = _source;
|
||||
}
|
||||
|
||||
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
var q = SearchBox.Text?.Trim() ?? "";
|
||||
var view = System.Windows.Data.CollectionViewSource.GetDefaultView(_source);
|
||||
view.Filter = q.Length == 0
|
||||
? null
|
||||
: o => o is Bookmark b &&
|
||||
(b.Title.Contains(q, StringComparison.OrdinalIgnoreCase) ||
|
||||
b.Url.Contains(q, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private void Edit_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is not Bookmark b) return;
|
||||
var title = InputBox("즐겨찾기 편집", "이름", b.Title);
|
||||
if (title == null) return;
|
||||
var url = InputBox("즐겨찾기 편집", "URL", b.Url);
|
||||
if (url == null) return;
|
||||
var idx = _source.IndexOf(b);
|
||||
if (idx >= 0) _source[idx] = new Bookmark(title.Trim(), url.Trim());
|
||||
_persist();
|
||||
}
|
||||
|
||||
private void Delete_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is not Bookmark b) return;
|
||||
_source.Remove(b);
|
||||
_persist();
|
||||
}
|
||||
|
||||
private void List_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Delete && List.SelectedItem is Bookmark b)
|
||||
{
|
||||
_source.Remove(b);
|
||||
_persist();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>UC-M19 브라우저 호환 Netscape 북마크 HTML 로 내보내기.</summary>
|
||||
private void Export_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.SaveFileDialog { Filter = "북마크 HTML|*.html", FileName = "bookmarks.html" };
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("<!DOCTYPE NETSCAPE-Bookmark-file-1>");
|
||||
sb.AppendLine("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">");
|
||||
sb.AppendLine("<TITLE>Bookmarks</TITLE><H1>Bookmarks</H1><DL><p>");
|
||||
foreach (var b in _source)
|
||||
sb.AppendLine($" <DT><A HREF=\"{System.Net.WebUtility.HtmlEncode(b.Url)}\">{System.Net.WebUtility.HtmlEncode(b.Title)}</A>");
|
||||
sb.AppendLine("</DL><p>");
|
||||
File.WriteAllText(dlg.FileName, sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Netscape HTML 에서 HREF/제목 쌍을 추출해 추가 가져오기.</summary>
|
||||
private void Import_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog { Filter = "북마크 HTML|*.html;*.htm" };
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
try
|
||||
{
|
||||
var html = File.ReadAllText(dlg.FileName);
|
||||
var added = 0;
|
||||
foreach (var (url, title) in ParseNetscapeBookmarks(html))
|
||||
{
|
||||
if (_source.Any(b => b.Url.Equals(url, StringComparison.OrdinalIgnoreCase))) continue;
|
||||
_source.Add(new Bookmark(title, url));
|
||||
added++;
|
||||
}
|
||||
_persist();
|
||||
MessageBox.Show(this, $"{added}개 항목을 가져왔습니다", "가져오기");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, $"가져오기 실패: {ex.Message}", "가져오기", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
internal static IEnumerable<(string Url, string Title)> ParseNetscapeBookmarks(string html)
|
||||
{
|
||||
var matches = System.Text.RegularExpressions.Regex.Matches(html,
|
||||
@"<A\s[^>]*HREF=""(?<url>[^""]+)""[^>]*>(?<title>.*?)</A>",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
foreach (System.Text.RegularExpressions.Match m in matches)
|
||||
{
|
||||
var url = System.Net.WebUtility.HtmlDecode(m.Groups["url"].Value);
|
||||
var title = System.Text.RegularExpressions.Regex.Replace(m.Groups["title"].Value, "<[^>]+>", "");
|
||||
yield return (url, System.Net.WebUtility.HtmlDecode(title).Trim());
|
||||
}
|
||||
}
|
||||
|
||||
private string? InputBox(string title, string label, string initial)
|
||||
{
|
||||
var box = new TextBox { Text = initial, Style = (Style)FindResource("DarkTextBox"), Margin = new Thickness(0, 6, 0, 12) };
|
||||
var win = new Window
|
||||
{
|
||||
Title = title,
|
||||
Width = 440,
|
||||
SizeToContent = SizeToContent.Height,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
Owner = this,
|
||||
Background = (System.Windows.Media.Brush)FindResource("WindowBg"),
|
||||
};
|
||||
var cancel = new Button { Content = "취소", Style = (Style)FindResource("GhostButton"), Width = 80, Margin = new Thickness(0, 0, 8, 0) };
|
||||
var ok = new Button { Content = "확인", Style = (Style)FindResource("PrimaryButton"), Width = 80, IsDefault = true };
|
||||
cancel.Click += (_, _) => win.DialogResult = false;
|
||||
ok.Click += (_, _) => win.DialogResult = true;
|
||||
win.Content = new StackPanel { Margin = new Thickness(16) }.Also(p =>
|
||||
{
|
||||
p.Children.Add(new TextBlock { Text = label, Foreground = (System.Windows.Media.Brush)FindResource("TextSecondary") });
|
||||
p.Children.Add(box);
|
||||
p.Children.Add(new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Children = { cancel, ok },
|
||||
});
|
||||
});
|
||||
return win.ShowDialog() == true ? box.Text : null;
|
||||
}
|
||||
|
||||
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
|
||||
/// <summary>람다식 객체 초기화 헬퍼(BookmarkManagerWindow 입력 다이얼로그용).</summary>
|
||||
internal static class UiExtensions
|
||||
{
|
||||
public static T Also<T>(this T self, Action<T> init)
|
||||
{
|
||||
init(self);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue