1
0
Fork 0

ux: 우클릭 → 메인 윈도우 통합 + Preview를 우측 컬럼으로 승격

우클릭 진입 통합
- App.OnStartup의 dialog 분기에서 ConvertWindow 대신 MainWindow를 띄우면서
  파일을 Active Queue에 자동 추가, 첫 항목 자동 Preview 로드.
- 사용자 시점에서 우클릭 → "JPEG로 변환…" 클릭 시 작은 다이얼로그가
  아니라 풀스크린 FormatShift 메인 화면이 그대로 노출.
- ConvertWindow 자체는 FormatShift 토큰으로 통일했지만 더 이상 호출되지
  않음 (코드는 향후 활용 위해 보존).

Preview를 탭 → 우측 메인 컬럼으로 승격
- 메인 영역 그리드를 1 column → 2 column 으로 분할: 좌측 *(min 360),
  우측 380px 고정 Preview 컬럼.
- 탭은 Active Queue / Past Results 두 개만 (Preview 탭과 무의미한 0 뱃지
  제거). Preview는 항상 보임.
- Active Queue 행 클릭 → 우측 Preview에 즉시 표시 (탭 전환 없음).
- Past Results 행도 클릭 가능: 실제 파일이면 미리보기, 데모/소실 파일이면
  안내 문구.
- Drop hint overlay는 ColumnSpan=2로 양쪽 컬럼 모두 덮음.

이벤트 처리 개선
- 행 Border에 PreviewMouseLeftButtonUp(tunnel) 사용 → 자식 컨트롤이 클릭을
  가로채는 이슈 해소.
- IsInsideButton 헬퍼로 Remove/Open 버튼이 발화시킨 클릭은 행 핸들러에서
  무시하여 의도 충돌 방지.

App.xaml 정리
- FormatShiftTheme.xaml 을 App-level resources에 머지 → 모든 윈도우에서
  Application.Current.FindResource("Fs*") 조회 가능 (이전엔 Window-level
  머지만 해서 ResourceReferenceKeyNotFoundException 발생).

LoadPreview 통합
- LoadPreviewByPathAsync(path, fileName, format, size) 단일 메서드로 통합.
- _selectedPreviewPath 로 Active/Past 두 케이스 모두 Open in Explorer 일관
  처리.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yun Chan 2026-05-06 19:26:27 +09:00
parent 01467a4d27
commit 00d1eeff96
6 changed files with 595 additions and 373 deletions

View file

@ -20,7 +20,9 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private CancellationTokenSource? _cts;
private NameCollision _conflictRule = NameCollision.AppendNumber;
public MainWindow()
public MainWindow() : this(null) { }
public MainWindow(IReadOnlyList<string>? initialFiles)
{
InitializeComponent();
@ -30,9 +32,18 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
SeedDemoHistory();
UpdateBadges();
UpdateProcessQueueButton();
ShowTab("Past");
UpdateActiveQueueVisibility();
if (initialFiles is { Count: > 0 })
{
AddToQueue(initialFiles);
ShowTab("Active");
}
else
{
ShowTab("Past");
}
UpdateActiveQueueVisibility();
ApplyAppDataStats();
}
@ -49,11 +60,9 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private void ShowTab(string tag)
{
TabActiveBtn.IsChecked = tag == "Active";
TabPreviewBtn.IsChecked = tag == "Preview";
TabPastBtn.IsChecked = tag == "Past";
ActiveQueueView.Visibility = tag == "Active" ? Visibility.Visible : Visibility.Collapsed;
PreviewView.Visibility = tag == "Preview" ? Visibility.Visible : Visibility.Collapsed;
PastResultsView.Visibility = tag == "Past" ? Visibility.Visible : Visibility.Collapsed;
}
@ -98,6 +107,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private void AddToQueue(IEnumerable<string> paths)
{
var wasEmpty = _activeQueue.Count == 0;
var existing = new HashSet<string>(_activeQueue.Select(q => q.SourcePath), StringComparer.OrdinalIgnoreCase);
foreach (var path in paths)
{
@ -107,6 +117,12 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
UpdateBadges();
UpdateProcessQueueButton();
UpdateActiveQueueVisibility();
if (wasEmpty && _activeQueue.Count > 0 && _selectedPreviewItem is null)
{
_selectedPreviewItem = _activeQueue[0];
_ = LoadPreviewAsync(_selectedPreviewItem);
}
}
private void OnRemoveQueueItem(object sender, RoutedEventArgs e)
@ -130,7 +146,6 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private void UpdateBadges()
{
TabActiveBadge.Text = _activeQueue.Count.ToString(CultureInfo.InvariantCulture);
TabPreviewBadge.Text = "0";
var count = _pastResults.Sum(g => g.Entries.Count);
TabPastBadge.Text = count.ToString(CultureInfo.InvariantCulture);
}
@ -211,37 +226,74 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
// ============== Preview ==============
private QueueItem? _selectedPreviewItem;
private string? _selectedPreviewPath;
private CancellationTokenSource? _previewCts;
private async void OnQueueRowClick(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is DependencyObject src && IsInsideButton(src)) return;
if (sender is not FrameworkElement fe || fe.Tag is not QueueItem item) return;
_selectedPreviewItem = item;
ShowTab("Preview");
await LoadPreviewAsync(item);
e.Handled = true;
}
private async Task LoadPreviewAsync(QueueItem item)
private async void OnPastRowClick(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is DependencyObject src && IsInsideButton(src)) return;
if (sender is not FrameworkElement fe || fe.Tag is not HistoryRow row) return;
if (row.SourcePath == "<demo>")
{
SetPreviewMeta(row.FileName, row.SourcePath, row.FormatLabel, row.SizeText);
ShowPreviewReason("샘플 데모 항목입니다. 원본 파일이 없으므로 미리보기를 만들 수 없습니다.");
_selectedPreviewPath = null;
}
else if (!File.Exists(row.SourcePath))
{
SetPreviewMeta(row.FileName, row.SourcePath, row.FormatLabel, row.SizeText);
ShowPreviewReason("원본 파일을 찾을 수 없습니다. 파일이 이동·삭제되었을 수 있습니다.");
_selectedPreviewPath = null;
}
else
{
_selectedPreviewItem = null;
_selectedPreviewPath = row.SourcePath;
await LoadPreviewByPathAsync(row.SourcePath, row.FileName, row.FormatLabel, row.SizeText);
}
e.Handled = true;
}
private static bool IsInsideButton(DependencyObject? node)
{
while (node is not null)
{
if (node is Button) return true;
node = System.Windows.Media.VisualTreeHelper.GetParent(node)
?? (node is FrameworkElement fe ? fe.Parent : null);
}
return false;
}
private Task LoadPreviewAsync(QueueItem item)
{
_selectedPreviewPath = item.SourcePath;
return LoadPreviewByPathAsync(item.SourcePath, item.FileName, item.FormatLabel, item.SizeText);
}
private async Task LoadPreviewByPathAsync(string sourcePath, string fileName, string formatLabel, string sizeText)
{
_previewCts?.Cancel();
_previewCts = new CancellationTokenSource();
var token = _previewCts.Token;
PreviewEmpty.Visibility = Visibility.Collapsed;
PreviewImage.Visibility = Visibility.Collapsed;
PreviewReason.Visibility = Visibility.Collapsed;
PreviewLoading.Visibility = Visibility.Visible;
PreviewFileName.Text = item.FileName;
PreviewFilePath.Text = item.SourcePath;
PreviewFormatText.Text = item.FormatLabel;
PreviewSizeText.Text = item.SizeText;
PreviewDimText.Text = "—";
PreviewPageText.Text = "—";
SetPreviewMeta(fileName, sourcePath, formatLabel, sizeText);
ShowPreviewLoading();
try
{
var result = await PreviewService.CreateAsync(item.SourcePath, 720, token);
var result = await PreviewService.CreateAsync(sourcePath, 720, token);
if (token.IsCancellationRequested) return;
PreviewLoading.Visibility = Visibility.Collapsed;
@ -264,20 +316,47 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
catch (Exception ex)
{
PreviewLoading.Visibility = Visibility.Collapsed;
PreviewReasonText.Text = "미리보기 오류: " + ex.Message;
PreviewReason.Visibility = Visibility.Visible;
ShowPreviewReason("미리보기 오류: " + ex.Message);
}
}
private void SetPreviewMeta(string fileName, string filePath, string formatLabel, string sizeText)
{
PreviewFileName.Text = fileName;
PreviewFilePath.Text = filePath;
PreviewFormatText.Text = formatLabel;
PreviewSizeText.Text = sizeText;
PreviewDimText.Text = "—";
PreviewPageText.Text = "—";
}
private void ShowPreviewLoading()
{
PreviewEmpty.Visibility = Visibility.Collapsed;
PreviewImage.Visibility = Visibility.Collapsed;
PreviewReason.Visibility = Visibility.Collapsed;
PreviewLoading.Visibility = Visibility.Visible;
}
private void ShowPreviewReason(string reason)
{
PreviewEmpty.Visibility = Visibility.Collapsed;
PreviewImage.Visibility = Visibility.Collapsed;
PreviewLoading.Visibility = Visibility.Collapsed;
PreviewReasonText.Text = reason;
PreviewReason.Visibility = Visibility.Visible;
}
private void OnPreviewOpenFolder(object sender, RoutedEventArgs e)
{
if (_selectedPreviewItem is null) return;
var path = _selectedPreviewPath ?? _selectedPreviewItem?.SourcePath;
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return;
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"/select,\"{_selectedPreviewItem.SourcePath}\"",
Arguments = $"/select,\"{path}\"",
UseShellExecute = true,
});
}