156 lines
6.4 KiB
C#
156 lines
6.4 KiB
C#
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;
|
|
}
|
|
}
|