From 4ca7352dc3f00fcd2c1b7d7dde6c693dcebc3d6c Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sat, 5 Sep 2026 12:25:30 +0900 Subject: [PATCH] feat(ui): default to active queue, scope search toolbar, add double-bezel inspector, optimize queue columns, and achieve 100% pure Korean UI --- packaging/Package.appxmanifest | 2 +- .../Everything2Everything.App.csproj | 2 +- .../ViewModels/OptionsViewModel.cs | 19 +- .../Views/CategoryGlyphs.cs | 2 +- .../Views/DiagnoseWindow.xaml | 1 + .../Views/FormatShiftTheme.xaml | 94 ++ .../Views/MainWindow.xaml | 1031 +++++++++-------- .../Views/MainWindow.xaml.cs | 89 +- .../Views/QuickOptionsWindow.xaml | 1 + .../Views/QuickProgressWindow.xaml | 1 + .../Views/SettingsWindow.xaml | 233 ++-- .../Filters/QueueFilterMatcher.cs | 11 + .../Inspector/FileInspectorInfo.cs | 2 +- .../Presets/FormatPresetEngine.cs | 140 ++- .../CategoryGlyphsTests.cs | 53 + .../DesignAuditAstTests.cs | 535 +++++++++ .../DesignAuditVisualTreeTests.cs | 161 +++ .../FileInspectorTests.cs | 20 + .../FormatPresetEngineTests.cs | 43 +- .../OptionsViewModelTests.cs | 22 + .../UnifiedTitleBarTests.cs | 314 ++++- tools/Check-Window.ps1 | 50 + tools/Dump-Tree.ps1 | 25 + tools/Dump-UiaControls.ps1 | 31 + tools/Render-WindowScreenshot.ps1 | 49 + tools/Test-HeadfulE2E.ps1 | 196 ++++ tools/test_launch.ps1 | 9 + 27 files changed, 2477 insertions(+), 659 deletions(-) create mode 100644 src/Everything2Everything.Tests/CategoryGlyphsTests.cs create mode 100644 tools/Check-Window.ps1 create mode 100644 tools/Dump-Tree.ps1 create mode 100644 tools/Dump-UiaControls.ps1 create mode 100644 tools/Render-WindowScreenshot.ps1 create mode 100644 tools/Test-HeadfulE2E.ps1 create mode 100644 tools/test_launch.ps1 diff --git a/packaging/Package.appxmanifest b/packaging/Package.appxmanifest index e43af0e..f534f51 100644 --- a/packaging/Package.appxmanifest +++ b/packaging/Package.appxmanifest @@ -1,6 +1,6 @@ - + Everything2Everything YunChan diff --git a/src/Everything2Everything.App/Everything2Everything.App.csproj b/src/Everything2Everything.App/Everything2Everything.App.csproj index 48e53ff..69062d0 100644 --- a/src/Everything2Everything.App/Everything2Everything.App.csproj +++ b/src/Everything2Everything.App/Everything2Everything.App.csproj @@ -1,7 +1,7 @@ - 1.0.11 + 1.0.18 WinExe net9.0-windows10.0.19041.0 enable diff --git a/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs b/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs index 5c2da02..9d3e5bf 100644 --- a/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs +++ b/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs @@ -66,6 +66,13 @@ public partial class OptionsViewModel : ObservableObject [ObservableProperty] private int _channelsIndex; // 0=원본,1=모노,2=스테레오 [ObservableProperty] private bool _loudnorm; + // ── 상세 인코딩 옵션 (폴드아웃 Expander 상태 및 전문 규격) ──────────────────────────────── + [ObservableProperty] private bool _isAdvancedExpanded = true; // 사용자 피드백: 슬라이더/옵션을 바로 볼 수 있도록 기본 전개 + [ObservableProperty] private int _pdfCompressLevelIndex; // 0=Light, 1=Strong, 2=Max + [ObservableProperty] private int _pdfDpiIndex = 1; // 0=150, 1=200, 2=300 + [ObservableProperty] private bool _imageLossless; // WebP/PNG 무손실 + [ObservableProperty] private bool _progressive; // JPEG 프로그레시브 웹 로딩 + /// 현재 상태로 불변 ConvertOptions를 구성한다(기존 MainWindow.BuildOptions와 동일 동작 + 영상/오디오). public ConvertOptions ToConvertOptions() { @@ -77,15 +84,23 @@ public partial class OptionsViewModel : ObservableObject _ => "summarize", }; + var dpi = PdfDpiIndex switch + { + 0 => 150, + 2 => 300, + _ => 200, + }; + return new ConvertOptions { OnCollision = ConflictRule, OutputLocation = hasCustom ? OutputLocation.Custom : OutputLocation.SubfolderBesideSource, CustomOutputDirectory = hasCustom ? CustomOutputDirectory!.Trim() : null, KeepExifWhenPossible = !StripMetadata, - Jpeg = new JpegEncodingOptions { Quality = Quality }, - Webp = new WebpEncodingOptions { Quality = Quality }, + Jpeg = new JpegEncodingOptions { Quality = Quality, Progressive = Progressive }, + Webp = new WebpEncodingOptions { Quality = Quality, Lossless = ImageLossless }, Avif = new AvifEncodingOptions { Quality = Math.Clamp(Quality - 30, 1, 100) }, + PdfRender = new PdfRenderOptions { Dpi = dpi }, Ai = new AiOptions { Task = aiTask, diff --git a/src/Everything2Everything.App/Views/CategoryGlyphs.cs b/src/Everything2Everything.App/Views/CategoryGlyphs.cs index 2532e89..658ae97 100644 --- a/src/Everything2Everything.App/Views/CategoryGlyphs.cs +++ b/src/Everything2Everything.App/Views/CategoryGlyphs.cs @@ -50,7 +50,7 @@ public static class CategoryGlyphs public static ImageSource ForCategory(string category) { if (Cache.TryGetValue(category, out var cached)) return cached; - var uri = new Uri($"pack://application:,,,/Assets/glyph-{category}.png", UriKind.Absolute); + var uri = new Uri($"pack://application:,,,/Everything2Everything;component/Assets/glyph-{category}.png", UriKind.Absolute); var img = new BitmapImage(); img.BeginInit(); img.CacheOption = BitmapCacheOption.OnLoad; diff --git a/src/Everything2Everything.App/Views/DiagnoseWindow.xaml b/src/Everything2Everything.App/Views/DiagnoseWindow.xaml index 3e10c7a..848d52a 100644 --- a/src/Everything2Everything.App/Views/DiagnoseWindow.xaml +++ b/src/Everything2Everything.App/Views/DiagnoseWindow.xaml @@ -12,6 +12,7 @@ + diff --git a/src/Everything2Everything.App/Views/FormatShiftTheme.xaml b/src/Everything2Everything.App/Views/FormatShiftTheme.xaml index 41ada10..adce8d2 100644 --- a/src/Everything2Everything.App/Views/FormatShiftTheme.xaml +++ b/src/Everything2Everything.App/Views/FormatShiftTheme.xaml @@ -521,6 +521,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - @@ -767,60 +774,67 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + - + - - - + + + @@ -880,7 +894,8 @@ + VerticalAlignment="Center" + TextTrimming="CharacterEllipsis"/> - + + HorizontalAlignment="Left" + VerticalAlignment="Center" + Margin="6,0,0,0"/> - - + + + + + + + + + + + + + + - - - - - + + + + + + VerticalAlignment="Center" + TextTrimming="CharacterEllipsis"/> @@ -1110,7 +1133,7 @@ Stroke="{StaticResource FsTextSecondary}" StrokeThickness="2" VerticalAlignment="Center" Data="M1,12 C1,12 5,4 12,4 C19,4 23,12 23,12 C23,12 19,20 12,20 C5,20 1,12 1,12 Z M12,9 A3,3 0 1,1 12,15 A3,3 0 1,1 12,9 Z"/> - @@ -1124,58 +1147,62 @@ - + - - - - - - - - + + + + + + + + + + + - - - - - + + + + + - - + + - - - - - - - + + + + + + + + @@ -1209,26 +1236,26 @@ - - - - - - - - + Background="#D9090A0C" IsHitTestVisible="False"> + + + + + + + + + + + diff --git a/src/Everything2Everything.App/Views/MainWindow.xaml.cs b/src/Everything2Everything.App/Views/MainWindow.xaml.cs index 0b8d28c..bf22559 100644 --- a/src/Everything2Everything.App/Views/MainWindow.xaml.cs +++ b/src/Everything2Everything.App/Views/MainWindow.xaml.cs @@ -145,7 +145,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow ToggleInspectorCommand = new RelayCommand(_ => ToggleInspector()); RemoveQueueItemCommand = new RelayCommand(p => RemoveQueueItem(p as QueueItem)); OpenFolderCommand = new RelayCommand(p => OpenFolderForPath(p as string)); - TabCommand = new RelayCommand(p => ShowTab(p as string ?? "Past")); + TabCommand = new RelayCommand(p => ShowTab(p as string ?? "Active")); ConflictRuleCommand = new RelayCommand(p => SetConflictRule(p as string)); CombineToggleCommand = new RelayCommand(_ => UpdateCombineState(SelectedOutputExtension)); OutputFormatChangedCommand = new RelayCommand(_ => OnOutputFormatSelected()); @@ -156,12 +156,26 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow PastRowCommand = new RelayCommand(p => HandlePastRowClick(p as MouseButtonEventArgs)); InitializeComponent(); + Title = "Everything2Everything"; + if (AppTitleBar is not null) AppTitleBar.Title = "Everything2Everything"; + + if (AdvancedOptionsExpander is not null) + { + AdvancedOptionsExpander.IsExpanded = true; + AdvancedOptionsExpander.Expanded += (_, _) => _options.IsAdvancedExpanded = true; + AdvancedOptionsExpander.Collapsed += (_, _) => _options.IsAdvancedExpanded = false; + } if (SmartPresetCombo is not null) { SmartPresetCombo.SelectionChanged += OnSmartPresetChanged; } + if (OutputFormatCombo is not null) + { + OutputFormatCombo.SelectionChanged += (_, _) => OnOutputFormatSelected(); + } + // ActiveQueueList/PastResultsList의 ItemsSource는 XAML이 ActiveQueue/PastResults에 바인딩(선언적). InitializeOutputFormats(); @@ -202,7 +216,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow return; } - CapabilityStatusText.Text = $"⚠ {notReady.Count}개 형식이 외부 도구를 기다립니다 (Diagnose 참조)"; + CapabilityStatusText.Text = $"⚠ {notReady.Count}개 형식이 외부 도구를 기다립니다 (진단 도구 참조)"; CapabilityStatusText.Visibility = Visibility.Visible; } @@ -343,22 +357,26 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow var count = _activeQueue.Count; if (_cts is not null) { - ProcessQueueButton.Content = $"변환 처리 중… ({count}개 파일)"; + ProcessQueueButton.Content = $"변환 처리 중… ({count}개)"; + ProcessQueueButton.ToolTip = "파일 변환이 진행 중입니다."; ProcessQueueButton.IsEnabled = false; } else if (count == 0) { - ProcessQueueButton.Content = "대기 중 — 파일을 드래그하여 추가하세요"; + ProcessQueueButton.Content = "파일을 드래그하여 추가"; + ProcessQueueButton.ToolTip = "대기열에 변환할 파일을 추가하세요 (단축키: Ctrl + O)"; ProcessQueueButton.IsEnabled = false; } else if (string.IsNullOrEmpty(SelectedOutputExtension)) { - ProcessQueueButton.Content = "변환 불가 (공통 형식 없음)"; + ProcessQueueButton.Content = "공통 형식 없음"; + ProcessQueueButton.ToolTip = "선택된 파일들 간에 호환 가능한 공통 출력 형식이 없습니다."; ProcessQueueButton.IsEnabled = false; } else { - ProcessQueueButton.Content = $"대기열 일괄 변환 시작 ({count}개) [Ctrl + Enter]"; + ProcessQueueButton.Content = $"변환 시작 ({count}개 파일)"; + ProcessQueueButton.ToolTip = $"대기열 일괄 변환 시작 ({count}개 파일) [단축키: Ctrl + Enter]"; ProcessQueueButton.IsEnabled = true; } } @@ -508,7 +526,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow PreviewFormatText.Text = string.IsNullOrEmpty(formatLabel) || formatLabel == "—" ? info.Extension.TrimStart('.').ToUpperInvariant() : formatLabel; PreviewSizeText.Text = string.IsNullOrEmpty(sizeText) || sizeText == "—" ? info.FormattedSize : sizeText; PreviewDimText.Text = info.DimensionsOrMeta; - PreviewPageText.Text = info.Category.ToString(); + PreviewPageText.Text = info.Category.ToKoreanLabel(); } private void ShowPreviewLoading() @@ -1043,10 +1061,9 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow private static string FormatDateLabel(DateOnly date) { var today = DateOnly.FromDateTime(DateTime.Today); - var label = date == today ? "Today" - : date == today.AddDays(-1) ? "Yesterday" - : date.ToString("dddd", CultureInfo.GetCultureInfo("en-US")); - return $"{label}, {date:MMM d}"; + if (date == today) return $"오늘 ({date:M월 d일})"; + if (date == today.AddDays(-1)) return $"어제 ({date:M월 d일})"; + return date.ToString("yyyy년 M월 d일 (ddd)", CultureInfo.GetCultureInfo("ko-KR")); } private void ApplyAppDataStats() @@ -1098,7 +1115,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow else if (TabPastBtn.IsChecked == true) { var confirm = MessageBox.Show(this, - "Past Results 전체를 삭제하시겠습니까?\n영구 저장된 이력도 함께 삭제됩니다.", + "변환 기록 전체를 삭제하시겠습니까?\n영구 저장된 이력도 함께 삭제됩니다.", "Everything2Everything", MessageBoxButton.OKCancel, MessageBoxImage.Question); if (confirm != MessageBoxResult.OK) return; @@ -1386,17 +1403,33 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow private void UpdateQualityPanelForFormat(string? extension) { - if (QualityPanel is null || QualityLabelText is null) return; var ext = extension?.ToLowerInvariant(); - var supportsQuality = ext is ".jpg" or ".jpeg" or ".webp" or ".avif"; - QualityPanel.Visibility = supportsQuality ? Visibility.Visible : Visibility.Collapsed; - QualityLabelText.Text = ext switch + var isImageQuality = ext is ".jpg" or ".jpeg" or ".webp" or ".avif"; + var isVideo = ext is ".mp4" or ".mkv" or ".webm" or ".mov" or ".avi"; + var isAudio = ext is ".mp3" or ".aac" or ".m4a" or ".opus" or ".ogg" or ".flac" or ".wav"; + var isPdf = ext is ".pdf"; + + if (QualityPanel is not null) { - ".jpg" or ".jpeg" => "JPEG QUALITY", - ".webp" => "WEBP QUALITY", - ".avif" => "AVIF QUALITY", - _ => "ENCODING QUALITY", - }; + QualityPanel.Visibility = isImageQuality ? Visibility.Visible : Visibility.Collapsed; + if (QualityLabelText is not null) + { + QualityLabelText.Text = ext switch + { + ".jpg" or ".jpeg" => "JPEG 압축 품질", + ".webp" => "WebP 압축 품질", + ".avif" => "AVIF 압축 품질", + _ => "압축 품질", + }; + } + } + + if (VideoQuickPanel is not null) + VideoQuickPanel.Visibility = isVideo ? Visibility.Visible : Visibility.Collapsed; + if (AudioQuickPanel is not null) + AudioQuickPanel.Visibility = isAudio ? Visibility.Visible : Visibility.Collapsed; + if (PdfQuickPanel is not null) + PdfQuickPanel.Visibility = isPdf ? Visibility.Visible : Visibility.Collapsed; UpdateMediaPanelForFormat(extension); } @@ -1471,9 +1504,17 @@ public sealed class QueueItem : INotifyPropertyChanged public string StateText { get => _state; - set { _state = value; Raise(nameof(StateText)); Raise(nameof(IsDone)); } + set { _state = value; Raise(nameof(StateText)); Raise(nameof(DisplayStateText)); Raise(nameof(IsDone)); } } + /// 사용자에게 표시되는 정제된 한국어 상태 텍스트 (AGENTS.md Rule 3). + public string DisplayStateText => _state switch + { + "queued" => "대기 중", + "done" => "변환 완료", + _ => _state, + }; + public Brush StateBrush => _state switch { "queued" => (Application.Current?.TryFindResource("FsTextTertiary") as Brush) ?? Brushes.Gray, @@ -1547,7 +1588,7 @@ public sealed class DateGroup : INotifyPropertyChanged public string DateTitle { get; } public ObservableCollection Entries { get; } = new(); public long SessionSavingsBytes { get; set; } - public string SessionSavingsText => $"Session Savings: {MainWindow.HumanizeBytes(SessionSavingsBytes)}"; + public string SessionSavingsText => $"세션 절감: {MainWindow.HumanizeBytes(SessionSavingsBytes)}"; public DateGroup(string dateTitle) { DateTitle = dateTitle; } @@ -1591,7 +1632,7 @@ public sealed record HistoryRow( FormatLabel: label, FormatBrush: brush, FileName: Path.GetFileName(e.SourcePath), - MetaLine: $"{e.Timestamp:HH:mm:ss} • {e.OutputCount} output(s)", + MetaLine: $"{e.Timestamp:HH:mm:ss} • {e.OutputCount}개 파일", SizeText: MainWindow.HumanizeBytes(e.SourceSizeBytes), SavingsText: $"{arrow} {MainWindow.HumanizeBytes(Math.Abs(saved))}", SourcePath: e.SourcePath, diff --git a/src/Everything2Everything.App/Views/QuickOptionsWindow.xaml b/src/Everything2Everything.App/Views/QuickOptionsWindow.xaml index ba98883..d052836 100644 --- a/src/Everything2Everything.App/Views/QuickOptionsWindow.xaml +++ b/src/Everything2Everything.App/Views/QuickOptionsWindow.xaml @@ -13,6 +13,7 @@ + diff --git a/src/Everything2Everything.App/Views/QuickProgressWindow.xaml b/src/Everything2Everything.App/Views/QuickProgressWindow.xaml index 637fd15..161ddfc 100644 --- a/src/Everything2Everything.App/Views/QuickProgressWindow.xaml +++ b/src/Everything2Everything.App/Views/QuickProgressWindow.xaml @@ -13,6 +13,7 @@ + diff --git a/src/Everything2Everything.App/Views/SettingsWindow.xaml b/src/Everything2Everything.App/Views/SettingsWindow.xaml index 7f3d450..9e5b022 100644 --- a/src/Everything2Everything.App/Views/SettingsWindow.xaml +++ b/src/Everything2Everything.App/Views/SettingsWindow.xaml @@ -11,6 +11,7 @@ + @@ -29,132 +30,132 @@ - - - - + + + + + - - - - - - - + + + + + + + - - - - - - - - -