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:
parent
01467a4d27
commit
00d1eeff96
6 changed files with 595 additions and 373 deletions
|
|
@ -9,6 +9,7 @@
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ui:ThemesDictionary Theme="Dark"/>
|
<ui:ThemesDictionary Theme="Dark"/>
|
||||||
<ui:ControlsDictionary/>
|
<ui:ControlsDictionary/>
|
||||||
|
<ResourceDictionary Source="Views/FormatShiftTheme.xaml"/>
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
<!-- Spacing tokens (Fluent 8pt grid) -->
|
<!-- Spacing tokens (Fluent 8pt grid) -->
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ public partial class App : Application
|
||||||
|
|
||||||
private void ShowConvertDialog(IReadOnlyList<string> files)
|
private void ShowConvertDialog(IReadOnlyList<string> files)
|
||||||
{
|
{
|
||||||
var window = new ConvertWindow(Engine, files);
|
var window = new Views.MainWindow(files);
|
||||||
MainWindow = window;
|
MainWindow = window;
|
||||||
window.Show();
|
window.Show();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,47 +3,66 @@
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||||
Title="JPEG로 변환"
|
Title="JPEG로 변환"
|
||||||
Width="640" Height="780"
|
Width="720" Height="780"
|
||||||
MinWidth="520" MinHeight="560"
|
MinWidth="560" MinHeight="560"
|
||||||
ExtendsContentIntoTitleBar="True"
|
ExtendsContentIntoTitleBar="True"
|
||||||
WindowBackdropType="Mica"
|
WindowBackdropType="None"
|
||||||
WindowCornerPreference="Round"
|
WindowCornerPreference="Round"
|
||||||
WindowStartupLocation="CenterOwner"
|
WindowStartupLocation="CenterOwner"
|
||||||
AllowDrop="True"
|
AllowDrop="True"
|
||||||
Drop="OnFilesDropped"
|
Drop="OnFilesDropped"
|
||||||
DragOver="OnDragOver">
|
DragOver="OnDragOver"
|
||||||
<Grid>
|
TextOptions.TextFormattingMode="Display"
|
||||||
|
UseLayoutRounding="True">
|
||||||
|
<Window.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid Background="{StaticResource FsBgBase}">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="32"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- 0: TitleBar -->
|
<!-- 0: TitleBar (window controls only) -->
|
||||||
<ui:TitleBar Grid.Row="0" Title="JPEG로 변환"/>
|
<ui:TitleBar Grid.Row="0" Title=""/>
|
||||||
|
|
||||||
<!-- 1: 헤더 (제목 + 파일 카운트) -->
|
<!-- 1: 헤더 -->
|
||||||
<Grid Grid.Row="1" Margin="32,8,32,12">
|
<Grid Grid.Row="1" Margin="32,8,32,16">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="JPEG로 변환" Style="{StaticResource TextTitle}"/>
|
<TextBlock Text="JPEG로 변환"
|
||||||
<TextBlock x:Name="FilesSummaryText" Style="{StaticResource TextCaption}" Margin="0,2,0,0"/>
|
FontFamily="{StaticResource FsFontSans}"
|
||||||
|
FontSize="20" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource FsTextPrimary}"/>
|
||||||
|
<TextBlock x:Name="FilesSummaryText" Margin="0,2,0,0"
|
||||||
|
Style="{StaticResource FsCaptionStyle}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<ui:Button Content="추가" Icon="{ui:SymbolIcon Add24}"
|
<Button Content="+ 추가" Margin="0,0,8,0"
|
||||||
Click="OnAddFilesClick" Margin="0,0,8,0"/>
|
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||||
<ui:Button Content="비우기" Icon="{ui:SymbolIcon Delete24}"
|
Click="OnAddFilesClick"/>
|
||||||
|
<Button Content="비우기"
|
||||||
|
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||||
Click="OnClearFilesClick"/>
|
Click="OnClearFilesClick"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- 2: 옵션 바 (고정 핵심 옵션 + 고급 expander) -->
|
<!-- 2: 옵션 카드 -->
|
||||||
<ui:Card Grid.Row="2" Margin="32,0,32,12" Padding="16,12">
|
<Border Grid.Row="2" Margin="32,0,32,16"
|
||||||
|
Background="{StaticResource FsBgPanel}"
|
||||||
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
|
BorderThickness="1" CornerRadius="8" Padding="20">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
|
@ -53,128 +72,194 @@
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- 품질 -->
|
<!-- 품질 -->
|
||||||
<StackPanel Grid.Column="0" Margin="0,0,8,0">
|
<StackPanel Grid.Column="0" Margin="0,0,12,0">
|
||||||
<TextBlock Text="JPEG 품질" Style="{StaticResource TextCaption}"/>
|
<Grid>
|
||||||
<Grid Margin="0,2,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="32"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Slider x:Name="QualitySlider" Minimum="50" Maximum="100" Value="92"
|
<TextBlock Text="JPEG 품질" Style="{StaticResource FsLabelStyle}"/>
|
||||||
SmallChange="1" LargeChange="5" VerticalAlignment="Center"/>
|
<Border Grid.Column="1" CornerRadius="4"
|
||||||
<TextBlock Grid.Column="1" Text="{Binding ElementName=QualitySlider, Path=Value, StringFormat={}{0:0}}"
|
Background="{StaticResource FsBgInput}"
|
||||||
FontWeight="SemiBold" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
|
BorderThickness="1" Padding="6,1">
|
||||||
|
<TextBlock x:Name="QualityValueText"
|
||||||
|
FontFamily="{StaticResource FsFontMono}" FontSize="11"
|
||||||
|
Foreground="{StaticResource FsAccentBlue}"
|
||||||
|
Text="92"/>
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Slider x:Name="QualitySlider" Margin="0,8,0,0"
|
||||||
|
Style="{StaticResource FsSliderStyle}"
|
||||||
|
Minimum="50" Maximum="100" Value="92"
|
||||||
|
ValueChanged="OnQualityChanged"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- 출력 위치 -->
|
<!-- 출력 위치 -->
|
||||||
<StackPanel Grid.Column="1" Margin="8,0,8,0">
|
<StackPanel Grid.Column="1" Margin="6,0,6,0">
|
||||||
<TextBlock Text="출력 위치" Style="{StaticResource TextCaption}"/>
|
<TextBlock Text="출력 위치" Style="{StaticResource FsLabelStyle}"/>
|
||||||
<ComboBox x:Name="OutputModeCombo" SelectedIndex="0" Margin="0,2,0,0">
|
<Border Margin="0,8,0,0"
|
||||||
<ComboBoxItem Content="원본 폴더 안 _jpeg 하위 폴더"/>
|
Background="{StaticResource FsBgInput}"
|
||||||
<ComboBoxItem Content="원본과 같은 폴더"/>
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
<ComboBoxItem Content="사용자 지정 폴더…"/>
|
BorderThickness="1" CornerRadius="6" Padding="2">
|
||||||
</ComboBox>
|
<UniformGrid Rows="1" Columns="3">
|
||||||
|
<ToggleButton x:Name="OutputSubBtn" Content="하위"
|
||||||
|
Style="{StaticResource FsSegmentStyle}"
|
||||||
|
IsChecked="True"
|
||||||
|
Click="OnOutputSegmentClick" Tag="Sub"
|
||||||
|
ToolTip="원본 폴더 안 _jpeg 하위 폴더"/>
|
||||||
|
<ToggleButton x:Name="OutputSameBtn" Content="동일"
|
||||||
|
Style="{StaticResource FsSegmentStyle}"
|
||||||
|
Click="OnOutputSegmentClick" Tag="Same"
|
||||||
|
ToolTip="원본과 같은 폴더"/>
|
||||||
|
<ToggleButton x:Name="OutputCustomBtn" Content="지정"
|
||||||
|
Style="{StaticResource FsSegmentStyle}"
|
||||||
|
Click="OnOutputSegmentClick" Tag="Custom"
|
||||||
|
ToolTip="사용자 지정 폴더"/>
|
||||||
|
</UniformGrid>
|
||||||
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- 충돌 -->
|
<!-- 이름 충돌 -->
|
||||||
<StackPanel Grid.Column="2" Margin="8,0,0,0">
|
<StackPanel Grid.Column="2" Margin="12,0,0,0">
|
||||||
<TextBlock Text="이름 충돌" Style="{StaticResource TextCaption}"/>
|
<TextBlock Text="이름 충돌" Style="{StaticResource FsLabelStyle}"/>
|
||||||
<ComboBox x:Name="CollisionCombo" SelectedIndex="0" Margin="0,2,0,0">
|
<Border Margin="0,8,0,0"
|
||||||
<ComboBoxItem Content="번호 붙여 새 파일"/>
|
Background="{StaticResource FsBgInput}"
|
||||||
<ComboBoxItem Content="덮어쓰기"/>
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
<ComboBoxItem Content="건너뛰기"/>
|
BorderThickness="1" CornerRadius="6" Padding="2">
|
||||||
</ComboBox>
|
<UniformGrid Rows="1" Columns="3">
|
||||||
|
<ToggleButton x:Name="ConflictRenameBtn" Content="번호"
|
||||||
|
Style="{StaticResource FsSegmentStyle}"
|
||||||
|
IsChecked="True"
|
||||||
|
Click="OnConflictSegmentClick" Tag="Rename"/>
|
||||||
|
<ToggleButton x:Name="ConflictReplaceBtn" Content="덮어쓰기"
|
||||||
|
Style="{StaticResource FsSegmentStyle}"
|
||||||
|
Click="OnConflictSegmentClick" Tag="Replace"/>
|
||||||
|
<ToggleButton x:Name="ConflictSkipBtn" Content="건너뛰기"
|
||||||
|
Style="{StaticResource FsSegmentStyle}"
|
||||||
|
Click="OnConflictSegmentClick" Tag="Skip"/>
|
||||||
|
</UniformGrid>
|
||||||
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- 사용자 지정 폴더 (옵션) -->
|
<!-- 사용자 지정 폴더 -->
|
||||||
<Grid Margin="0,8,0,0" x:Name="CustomFolderRow" Visibility="Collapsed">
|
<Grid Margin="0,12,0,0" x:Name="CustomFolderRow" Visibility="Collapsed">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBox x:Name="CustomFolderTextBox"/>
|
<TextBox x:Name="CustomFolderTextBox"
|
||||||
<ui:Button Grid.Column="1" Content="찾아보기" Margin="6,0,0,0" Click="OnBrowseClick"/>
|
Style="{StaticResource FsPathInputStyle}"/>
|
||||||
|
<Button Grid.Column="1" Margin="8,0,0,0" Width="40"
|
||||||
|
Content="…" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||||
|
Click="OnBrowseClick"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- 고급 옵션 expander -->
|
<!-- 고급 옵션 -->
|
||||||
<Expander Header="고급 옵션" Margin="0,8,0,0" IsExpanded="False"
|
<Expander Margin="0,12,0,0" IsExpanded="False"
|
||||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}">
|
Foreground="{StaticResource FsTextSecondary}">
|
||||||
<Grid Margin="0,8,0,0">
|
<Expander.Header>
|
||||||
|
<TextBlock Text="고급 옵션" Style="{StaticResource FsLabelStyle}"/>
|
||||||
|
</Expander.Header>
|
||||||
|
<Grid Margin="0,12,0,0">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<StackPanel Grid.Column="0" Margin="0,0,8,0">
|
<StackPanel Grid.Column="0" Margin="0,0,12,0">
|
||||||
<TextBlock Text="긴 변 최대 픽셀" Style="{StaticResource TextCaption}"
|
<TextBlock Text="긴 변 최대 픽셀" Style="{StaticResource FsLabelStyle}"
|
||||||
ToolTip="비워 두면 원본 크기 유지"/>
|
|
||||||
<TextBox x:Name="MaxLongEdgeTextBox" Margin="0,2,0,0"
|
|
||||||
ToolTip="비워 두면 원본 크기 유지"/>
|
ToolTip="비워 두면 원본 크기 유지"/>
|
||||||
|
<TextBox x:Name="MaxLongEdgeTextBox" Margin="0,8,0,0"
|
||||||
|
Style="{StaticResource FsPathInputStyle}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Margin="8,0,8,0">
|
<StackPanel Grid.Column="1" Margin="6,0,12,0">
|
||||||
<TextBlock Text="PDF 렌더 DPI" Style="{StaticResource TextCaption}"/>
|
<Grid>
|
||||||
<Grid Margin="0,2,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="32"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Slider x:Name="DpiSlider" Minimum="72" Maximum="450" Value="200"
|
<TextBlock Text="PDF 렌더 DPI" Style="{StaticResource FsLabelStyle}"/>
|
||||||
VerticalAlignment="Center"/>
|
<TextBlock Grid.Column="1"
|
||||||
<TextBlock Grid.Column="1" Text="{Binding ElementName=DpiSlider, Path=Value, StringFormat={}{0:0}}"
|
FontFamily="{StaticResource FsFontMono}" FontSize="11"
|
||||||
VerticalAlignment="Center" HorizontalAlignment="Right" FontWeight="SemiBold"/>
|
Foreground="{StaticResource FsAccentBlue}"
|
||||||
|
Text="{Binding ElementName=DpiSlider, Path=Value, StringFormat={}{0:0}}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Slider x:Name="DpiSlider" Margin="0,8,0,0"
|
||||||
|
Style="{StaticResource FsSliderStyle}"
|
||||||
|
Minimum="72" Maximum="450" Value="200"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Grid.Column="2" Margin="8,0,0,0" VerticalAlignment="Bottom">
|
<CheckBox Grid.Column="2" x:Name="FlattenCheckBox"
|
||||||
<CheckBox x:Name="FlattenCheckBox" Content="투명을 흰색으로"
|
Content="투명을 흰색으로"
|
||||||
IsChecked="True" Margin="0,0,0,4"/>
|
IsChecked="True" VerticalAlignment="Bottom"
|
||||||
</StackPanel>
|
Foreground="{StaticResource FsTextSecondary}"
|
||||||
|
FontFamily="{StaticResource FsFontSans}"
|
||||||
|
FontSize="12"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Expander>
|
</Expander>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:Card>
|
|
||||||
|
|
||||||
<!-- 3: 파일 리스트 (메인 — 세로로 끝까지) -->
|
|
||||||
<ui:Card Grid.Row="3" Margin="32,0,32,0" Padding="0">
|
|
||||||
<DockPanel>
|
|
||||||
<Border DockPanel.Dock="Top" Padding="20,12,20,8"
|
|
||||||
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
|
||||||
BorderThickness="0,0,0,1">
|
|
||||||
<TextBlock Text="대상 파일" Style="{StaticResource TextBodyStrong}"/>
|
|
||||||
</Border>
|
</Border>
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="20,8,20,16">
|
|
||||||
|
<!-- 3: 파일 리스트 -->
|
||||||
|
<Border Grid.Row="3" Margin="32,0,32,0"
|
||||||
|
Background="{StaticResource FsBgPanel}"
|
||||||
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
|
BorderThickness="1" CornerRadius="8">
|
||||||
|
<DockPanel>
|
||||||
|
<Border DockPanel.Dock="Top" Padding="20,14,20,10"
|
||||||
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
|
BorderThickness="0,0,0,1">
|
||||||
|
<TextBlock Text="대상 파일" Style="{StaticResource FsLabelStyle}"/>
|
||||||
|
</Border>
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="20,12,20,16">
|
||||||
<ItemsControl x:Name="FilesList">
|
<ItemsControl x:Name="FilesList">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Border Margin="0,0,0,6" Padding="12,8" CornerRadius="6"
|
<Border Margin="0,0,0,8" Padding="12,10" CornerRadius="6"
|
||||||
Background="{DynamicResource SubtleFillColorTransparentBrush}"
|
Background="{StaticResource FsBgSurface}"
|
||||||
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
BorderThickness="1">
|
BorderThickness="1">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="44"/>
|
<ColumnDefinition Width="40"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Border Width="40" Height="40" CornerRadius="4"
|
|
||||||
Background="{DynamicResource ControlFillColorDefaultBrush}">
|
<Border Width="32" Height="32" CornerRadius="4"
|
||||||
|
Background="{Binding FormatBrush}"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Grid>
|
||||||
<Image Source="{Binding Thumbnail}" Stretch="UniformToFill"/>
|
<Image Source="{Binding Thumbnail}" Stretch="UniformToFill"/>
|
||||||
|
<TextBlock Text="{Binding FormatLabel}"
|
||||||
|
Foreground="White" FontSize="9"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Visibility="{Binding ShowFormatLabel}"/>
|
||||||
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<StackPanel Grid.Column="1" Margin="12,0,8,0" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="{Binding FileName}" FontWeight="SemiBold"
|
<StackPanel Grid.Column="1" Margin="14,0,8,0"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding FileName}"
|
||||||
|
FontFamily="{StaticResource FsFontSans}"
|
||||||
|
FontSize="13" FontWeight="Medium"
|
||||||
|
Foreground="{StaticResource FsTextPrimary}"
|
||||||
TextTrimming="CharacterEllipsis"/>
|
TextTrimming="CharacterEllipsis"/>
|
||||||
<TextBlock Text="{Binding SubText}" FontSize="11"
|
<TextBlock Text="{Binding SubText}"
|
||||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
FontSize="11" Margin="0,2,0,0"
|
||||||
|
Foreground="{StaticResource FsTextTertiary}"
|
||||||
TextTrimming="CharacterEllipsis"/>
|
TextTrimming="CharacterEllipsis"/>
|
||||||
<TextBlock Text="{Binding State}" FontSize="11" Margin="0,2,0,0">
|
<TextBlock Text="{Binding State}" FontSize="11" Margin="0,2,0,0">
|
||||||
<TextBlock.Style>
|
<TextBlock.Style>
|
||||||
<Style TargetType="TextBlock">
|
<Style TargetType="TextBlock">
|
||||||
<Setter Property="Foreground"
|
<Setter Property="Foreground"
|
||||||
Value="{DynamicResource TextFillColorTertiaryBrush}"/>
|
Value="{StaticResource FsTextTertiary}"/>
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<DataTrigger Binding="{Binding IsFailed}" Value="True">
|
<DataTrigger Binding="{Binding IsFailed}" Value="True">
|
||||||
<Setter Property="Foreground" Value="#F87171"/>
|
<Setter Property="Foreground" Value="#F87171"/>
|
||||||
|
|
@ -184,9 +269,19 @@
|
||||||
</TextBlock.Style>
|
</TextBlock.Style>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ui:Button Grid.Column="2" Icon="{ui:SymbolIcon Dismiss20}"
|
|
||||||
Appearance="Transparent" Click="OnRemoveEntry"
|
<Button Grid.Column="2"
|
||||||
ToolTip="목록에서 제거"/>
|
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0"
|
||||||
|
Padding="6"
|
||||||
|
Click="OnRemoveEntry"
|
||||||
|
ToolTip="목록에서 제거">
|
||||||
|
<Path Width="14" Height="14" Stretch="Uniform"
|
||||||
|
Stroke="{StaticResource FsTextTertiary}"
|
||||||
|
StrokeThickness="2"
|
||||||
|
Data="M18,6 L6,18 M6,6 L18,18"/>
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
@ -194,31 +289,33 @@
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</ui:Card>
|
</Border>
|
||||||
|
|
||||||
<!-- 4: 액션 바 (하단 고정) -->
|
<!-- 4: 액션바 -->
|
||||||
<Border Grid.Row="4" Background="{DynamicResource LayerOnAcrylicFillColorDefaultBrush}"
|
<Border Grid.Row="4" Padding="32,16,32,20" Margin="0,16,0,0"
|
||||||
BorderThickness="0,1,0,0"
|
BorderBrush="{StaticResource FsBorderSubtle}" BorderThickness="0,1,0,0"
|
||||||
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
Background="{StaticResource FsBgPanel}">
|
||||||
Padding="32,16" Margin="0,16,0,0">
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<StackPanel VerticalAlignment="Center">
|
<StackPanel VerticalAlignment="Center">
|
||||||
<ProgressBar x:Name="OverallProgress" Height="6" Minimum="0" Maximum="1"
|
<ProgressBar x:Name="OverallProgress" Height="3"
|
||||||
IsIndeterminate="False"/>
|
Background="{StaticResource FsBgInput}"
|
||||||
<TextBlock x:Name="ProgressStatusText" Style="{StaticResource TextCaption}"
|
Foreground="{StaticResource FsAccentBlue}"
|
||||||
Margin="0,6,0,0" TextTrimming="CharacterEllipsis"/>
|
Minimum="0" Maximum="1"/>
|
||||||
|
<TextBlock x:Name="ProgressStatusText" Margin="0,8,0,0"
|
||||||
|
Style="{StaticResource FsCaptionStyle}"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Margin="20,0,0,0">
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Margin="20,0,0,0">
|
||||||
<ui:Button x:Name="CancelButton" Content="닫기" Click="OnCancelClick"
|
<Button x:Name="CancelButton" Content="닫기" Click="OnCancelClick"
|
||||||
Margin="0,0,8,0" Padding="20,6"/>
|
Margin="0,0,8,0" MinWidth="80"
|
||||||
<ui:Button x:Name="ConvertButton" Content="변환 시작"
|
Style="{StaticResource FsSecondaryButtonStyle}"/>
|
||||||
Icon="{ui:SymbolIcon ArrowRight24}"
|
<Button x:Name="ConvertButton" Content="→ 변환 시작"
|
||||||
Appearance="Primary" Click="OnConvertClick"
|
Click="OnConvertClick" MinWidth="120"
|
||||||
Padding="24,6"/>
|
Style="{StaticResource FsPrimaryButtonStyle}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,46 @@
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using EverythingToJpeg.Core;
|
using EverythingToJpeg.Core;
|
||||||
using EverythingToJpeg.Core.Providers;
|
|
||||||
using Wpf.Ui.Controls;
|
using Wpf.Ui.Controls;
|
||||||
|
|
||||||
namespace EverythingToJpeg.App.Views;
|
namespace EverythingToJpeg.App.Views;
|
||||||
|
|
||||||
public partial class ConvertWindow : FluentWindow
|
public partial class ConvertWindow : FluentWindow
|
||||||
{
|
{
|
||||||
|
private static readonly string DialogLogPath =
|
||||||
|
Path.Combine(Path.GetTempPath(), "EverythingToJpeg_dialog.log");
|
||||||
|
|
||||||
private readonly ConversionEngine _engine;
|
private readonly ConversionEngine _engine;
|
||||||
private readonly ObservableCollection<FileEntry> _entries = new();
|
private readonly ObservableCollection<ConvertFileEntry> _entries = new();
|
||||||
private CancellationTokenSource? _cts;
|
private CancellationTokenSource? _cts;
|
||||||
|
private OutputLocation _outputMode = OutputLocation.SubfolderBesideSource;
|
||||||
|
private NameCollision _conflictRule = NameCollision.AppendNumber;
|
||||||
|
|
||||||
public ConvertWindow(ConversionEngine engine, IReadOnlyList<string> initialFiles)
|
public ConvertWindow(ConversionEngine engine, IReadOnlyList<string> initialFiles)
|
||||||
{
|
{
|
||||||
_engine = engine;
|
_engine = engine;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
FilesList.ItemsSource = _entries;
|
FilesList.ItemsSource = _entries;
|
||||||
|
|
||||||
AddFiles(initialFiles);
|
AddFiles(initialFiles);
|
||||||
|
|
||||||
OutputModeCombo.SelectionChanged += (_, _) =>
|
|
||||||
CustomFolderRow.Visibility = OutputModeCombo.SelectedIndex == 2
|
|
||||||
? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void DiagLog(string line)
|
||||||
|
{
|
||||||
|
try { File.AppendAllText(DialogLogPath,
|
||||||
|
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {line}{Environment.NewLine}"); }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== Files ==============
|
||||||
|
|
||||||
private void AddFiles(IEnumerable<string> paths)
|
private void AddFiles(IEnumerable<string> paths)
|
||||||
{
|
{
|
||||||
var existing = new HashSet<string>(_entries.Select(e => e.Path), StringComparer.OrdinalIgnoreCase);
|
var existing = new HashSet<string>(_entries.Select(e => e.Path), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
@ -38,7 +49,7 @@ public partial class ConvertWindow : FluentWindow
|
||||||
if (!File.Exists(p)) continue;
|
if (!File.Exists(p)) continue;
|
||||||
if (existing.Contains(p)) continue;
|
if (existing.Contains(p)) continue;
|
||||||
|
|
||||||
var entry = new FileEntry(p, _engine);
|
var entry = ConvertFileEntry.From(p, _engine);
|
||||||
_entries.Add(entry);
|
_entries.Add(entry);
|
||||||
_ = entry.LoadThumbnailAsync();
|
_ = entry.LoadThumbnailAsync();
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +96,7 @@ public partial class ConvertWindow : FluentWindow
|
||||||
|
|
||||||
private void OnRemoveEntry(object sender, RoutedEventArgs e)
|
private void OnRemoveEntry(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is FrameworkElement fe && fe.DataContext is FileEntry entry)
|
if (sender is FrameworkElement fe && fe.DataContext is ConvertFileEntry entry)
|
||||||
{
|
{
|
||||||
_entries.Remove(entry);
|
_entries.Remove(entry);
|
||||||
UpdateSummary();
|
UpdateSummary();
|
||||||
|
|
@ -105,33 +116,74 @@ public partial class ConvertWindow : FluentWindow
|
||||||
{
|
{
|
||||||
if (File.Exists(p)) yield return p;
|
if (File.Exists(p)) yield return p;
|
||||||
else if (Directory.Exists(p))
|
else if (Directory.Exists(p))
|
||||||
{
|
|
||||||
foreach (var f in Directory.EnumerateFiles(p, "*", SearchOption.TopDirectoryOnly))
|
foreach (var f in Directory.EnumerateFiles(p, "*", SearchOption.TopDirectoryOnly))
|
||||||
yield return f;
|
yield return f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============== Segments ==============
|
||||||
|
|
||||||
|
private void OnQualityChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||||
|
{
|
||||||
|
if (QualityValueText is null) return;
|
||||||
|
QualityValueText.Text = ((int)e.NewValue).ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly string DialogLogPath =
|
private void OnOutputSegmentClick(object sender, RoutedEventArgs e)
|
||||||
Path.Combine(Path.GetTempPath(), "EverythingToJpeg_dialog.log");
|
{
|
||||||
|
if (sender is not ToggleButton clicked) return;
|
||||||
|
OutputSubBtn.IsChecked = clicked == OutputSubBtn;
|
||||||
|
OutputSameBtn.IsChecked = clicked == OutputSameBtn;
|
||||||
|
OutputCustomBtn.IsChecked = clicked == OutputCustomBtn;
|
||||||
|
|
||||||
private static void DiagLog(string line)
|
_outputMode = (clicked.Tag as string) switch
|
||||||
{
|
{
|
||||||
try
|
"Same" => OutputLocation.SameFolderAsSource,
|
||||||
|
"Custom" => OutputLocation.Custom,
|
||||||
|
_ => OutputLocation.SubfolderBesideSource,
|
||||||
|
};
|
||||||
|
CustomFolderRow.Visibility = _outputMode == OutputLocation.Custom
|
||||||
|
? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConflictSegmentClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
File.AppendAllText(DialogLogPath,
|
if (sender is not ToggleButton clicked) return;
|
||||||
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {line}{Environment.NewLine}");
|
ConflictRenameBtn.IsChecked = clicked == ConflictRenameBtn;
|
||||||
|
ConflictReplaceBtn.IsChecked = clicked == ConflictReplaceBtn;
|
||||||
|
ConflictSkipBtn.IsChecked = clicked == ConflictSkipBtn;
|
||||||
|
_conflictRule = (clicked.Tag as string) switch
|
||||||
|
{
|
||||||
|
"Skip" => NameCollision.Skip,
|
||||||
|
"Replace" => NameCollision.Overwrite,
|
||||||
|
_ => NameCollision.AppendNumber,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
catch { }
|
|
||||||
|
private ConvertOptions BuildOptions()
|
||||||
|
{
|
||||||
|
var opts = new ConvertOptions
|
||||||
|
{
|
||||||
|
Quality = (int)QualitySlider.Value,
|
||||||
|
PdfDpi = (int)DpiSlider.Value,
|
||||||
|
FlattenTransparency = FlattenCheckBox.IsChecked == true,
|
||||||
|
OutputLocation = _outputMode,
|
||||||
|
OnCollision = _conflictRule,
|
||||||
|
};
|
||||||
|
if (_outputMode == OutputLocation.Custom)
|
||||||
|
opts.CustomOutputDirectory = CustomFolderTextBox.Text;
|
||||||
|
if (int.TryParse(MaxLongEdgeTextBox.Text, out var maxEdge) && maxEdge > 0)
|
||||||
|
opts.MaxLongEdgePixels = maxEdge;
|
||||||
|
return opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============== Convert ==============
|
||||||
|
|
||||||
private async void OnConvertClick(object sender, RoutedEventArgs e)
|
private async void OnConvertClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
DiagLog($"OnConvertClick: entries={_entries.Count}");
|
DiagLog($"OnConvertClick: entries={_entries.Count}");
|
||||||
|
|
||||||
if (_entries.Count == 0)
|
if (_entries.Count == 0)
|
||||||
{
|
{
|
||||||
DiagLog(" → no entries, showing info");
|
|
||||||
ShowInfo("변환할 파일이 없습니다.");
|
ShowInfo("변환할 파일이 없습니다.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +197,7 @@ public partial class ConvertWindow : FluentWindow
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
options = BuildOptions();
|
options = BuildOptions();
|
||||||
DiagLog($" options: Quality={options.Quality} OutputLocation={options.OutputLocation} Custom={options.CustomOutputDirectory} Collision={options.OnCollision} MaxLong={options.MaxLongEdgePixels} PdfDpi={options.PdfDpi}");
|
DiagLog($" options: Quality={options.Quality} OutputLocation={options.OutputLocation} Custom={options.CustomOutputDirectory} Collision={options.OnCollision}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -171,24 +223,19 @@ public partial class ConvertWindow : FluentWindow
|
||||||
DiagLog($" starting ConvertManyAsync, {sources.Count} files");
|
DiagLog($" starting ConvertManyAsync, {sources.Count} files");
|
||||||
var results = await _engine.ConvertManyAsync(sources, options, reporter, _cts.Token);
|
var results = await _engine.ConvertManyAsync(sources, options, reporter, _cts.Token);
|
||||||
DiagLog($" finished, {results.Count} results");
|
DiagLog($" finished, {results.Count} results");
|
||||||
foreach (var r in results)
|
|
||||||
DiagLog($" [{r.Status}] {Path.GetFileName(r.SourcePath)} msg={r.Message}");
|
|
||||||
ApplyResults(results);
|
ApplyResults(results);
|
||||||
ProgressStatusText.Text = SummarizeResults(results);
|
ProgressStatusText.Text = SummarizeResults(results);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
DiagLog(" canceled");
|
|
||||||
ProgressStatusText.Text = "변환이 취소되었습니다.";
|
ProgressStatusText.Text = "변환이 취소되었습니다.";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
DiagLog(" EXCEPTION: " + ex);
|
DiagLog(" EXCEPTION: " + ex);
|
||||||
ProgressStatusText.Text = "오류: " + ex.Message;
|
ProgressStatusText.Text = "오류: " + ex.Message;
|
||||||
MessageBox.Show(this,
|
MessageBox.Show(this, "변환 중 오류:\n\n" + ex.Message + "\n\n로그: " + DialogLogPath,
|
||||||
"변환 중 오류가 발생했습니다:\n\n" + ex.Message + "\n\n로그: " + DialogLogPath,
|
"EverythingToJpeg", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
"EverythingToJpeg",
|
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -241,83 +288,67 @@ public partial class ConvertWindow : FluentWindow
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ConvertOptions BuildOptions()
|
|
||||||
{
|
|
||||||
var opts = new ConvertOptions
|
|
||||||
{
|
|
||||||
Quality = (int)QualitySlider.Value,
|
|
||||||
PdfDpi = (int)DpiSlider.Value,
|
|
||||||
FlattenTransparency = FlattenCheckBox.IsChecked == true,
|
|
||||||
};
|
|
||||||
opts.OutputLocation = OutputModeCombo.SelectedIndex switch
|
|
||||||
{
|
|
||||||
1 => OutputLocation.SameFolderAsSource,
|
|
||||||
2 => OutputLocation.Custom,
|
|
||||||
_ => OutputLocation.SubfolderBesideSource,
|
|
||||||
};
|
|
||||||
if (opts.OutputLocation == OutputLocation.Custom)
|
|
||||||
opts.CustomOutputDirectory = CustomFolderTextBox.Text;
|
|
||||||
|
|
||||||
opts.OnCollision = CollisionCombo.SelectedIndex switch
|
|
||||||
{
|
|
||||||
1 => NameCollision.Overwrite,
|
|
||||||
2 => NameCollision.Skip,
|
|
||||||
_ => NameCollision.AppendNumber,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (int.TryParse(MaxLongEdgeTextBox.Text, out var maxEdge) && maxEdge > 0)
|
|
||||||
opts.MaxLongEdgePixels = maxEdge;
|
|
||||||
|
|
||||||
return opts;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowInfo(string message)
|
private void ShowInfo(string message)
|
||||||
=> MessageBox.Show(this, message, "EverythingToJpeg",
|
=> MessageBox.Show(this, message, "EverythingToJpeg",
|
||||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class FileEntry : System.ComponentModel.INotifyPropertyChanged
|
public sealed class ConvertFileEntry : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
private readonly ConversionEngine _engine;
|
|
||||||
private string _state = "대기";
|
private string _state = "대기";
|
||||||
private bool _isFailed;
|
private bool _isFailed;
|
||||||
private ImageSource? _thumbnail;
|
private ImageSource? _thumbnail;
|
||||||
|
|
||||||
public FileEntry(string path, ConversionEngine engine)
|
public required string Path { get; init; }
|
||||||
|
public required string FileName { get; init; }
|
||||||
|
public required string SubText { get; init; }
|
||||||
|
public required string FormatLabel { get; init; }
|
||||||
|
public required Brush FormatBrush { get; init; }
|
||||||
|
|
||||||
|
public string State
|
||||||
{
|
{
|
||||||
Path = path;
|
get => _state;
|
||||||
_engine = engine;
|
set { _state = value; Raise(nameof(State)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Path { get; }
|
public bool IsFailed
|
||||||
public string FileName => System.IO.Path.GetFileName(Path);
|
{
|
||||||
|
get => _isFailed;
|
||||||
|
set { _isFailed = value; Raise(nameof(IsFailed)); }
|
||||||
|
}
|
||||||
|
|
||||||
public string SubText
|
public ImageSource? Thumbnail
|
||||||
{
|
{
|
||||||
get
|
get => _thumbnail;
|
||||||
|
set { _thumbnail = value; Raise(nameof(Thumbnail)); Raise(nameof(ShowFormatLabel)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Visibility ShowFormatLabel => _thumbnail is null ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
|
public static ConvertFileEntry From(string path, ConversionEngine engine)
|
||||||
{
|
{
|
||||||
var ext = System.IO.Path.GetExtension(Path).ToLowerInvariant();
|
var ext = System.IO.Path.GetExtension(path).TrimStart('.').ToLowerInvariant();
|
||||||
|
var (label, brushKey) = FormatPalette.For(ext);
|
||||||
|
|
||||||
string handler;
|
string handler;
|
||||||
if (_engine.Providers.TryGetForFile(Path, out var provider) && provider is not null)
|
if (engine.Providers.TryGetForFile(path, out var provider) && provider is not null)
|
||||||
handler = provider.Capability.DisplayName;
|
handler = provider.Capability.DisplayName;
|
||||||
else
|
else
|
||||||
handler = "지원되지 않음";
|
handler = "지원되지 않음";
|
||||||
try
|
|
||||||
{
|
|
||||||
var size = new FileInfo(Path).Length;
|
|
||||||
return $"{ext} · {handler} · {FormatBytes(size)}";
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return $"{ext} · {handler}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string State { get => _state; set { _state = value; Raise(nameof(State)); } }
|
long size = 0;
|
||||||
public bool IsFailed { get => _isFailed; set { _isFailed = value; Raise(nameof(IsFailed)); } }
|
try { size = new FileInfo(path).Length; } catch { }
|
||||||
public ImageSource? Thumbnail { get => _thumbnail; set { _thumbnail = value; Raise(nameof(Thumbnail)); } }
|
var sub = $".{ext} · {handler} · {MainWindow.HumanizeBytes(size)}";
|
||||||
|
|
||||||
|
return new ConvertFileEntry
|
||||||
|
{
|
||||||
|
Path = path,
|
||||||
|
FileName = System.IO.Path.GetFileName(path),
|
||||||
|
SubText = sub,
|
||||||
|
FormatLabel = label,
|
||||||
|
FormatBrush = (Brush)Application.Current.FindResource(brushKey),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public Task LoadThumbnailAsync() => Task.Run(() =>
|
public Task LoadThumbnailAsync() => Task.Run(() =>
|
||||||
{
|
{
|
||||||
|
|
@ -333,21 +364,13 @@ public sealed class FileEntry : System.ComponentModel.INotifyPropertyChanged
|
||||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||||
bmp.EndInit();
|
bmp.EndInit();
|
||||||
bmp.Freeze();
|
bmp.Freeze();
|
||||||
System.Windows.Application.Current.Dispatcher.Invoke(() => Thumbnail = bmp);
|
Application.Current.Dispatcher.Invoke(() => Thumbnail = bmp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
});
|
});
|
||||||
|
|
||||||
private static string FormatBytes(long bytes)
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
{
|
private void Raise([CallerMemberName] string? n = null)
|
||||||
string[] units = { "B", "KB", "MB", "GB" };
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
|
||||||
double size = bytes;
|
|
||||||
var unit = 0;
|
|
||||||
while (size >= 1024 && unit < units.Length - 1) { size /= 1024; unit++; }
|
|
||||||
return $"{size:0.#} {units[unit]}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
private void Raise(string n) => PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(n));
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -246,18 +246,6 @@
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton x:Name="TabPreviewBtn" Margin="8,0,0,0"
|
|
||||||
Style="{StaticResource FsTabPillStyle}"
|
|
||||||
Click="OnTabClick" Tag="Preview">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBlock Text="Preview" VerticalAlignment="Center"/>
|
|
||||||
<Border Margin="8,0,0,0" Style="{StaticResource FsTabBadgeStyle}">
|
|
||||||
<TextBlock x:Name="TabPreviewBadge" Text="0"
|
|
||||||
FontSize="10" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource FsTextTertiary}"/>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</ToggleButton>
|
|
||||||
<ToggleButton x:Name="TabPastBtn" Margin="8,0,0,0"
|
<ToggleButton x:Name="TabPastBtn" Margin="8,0,0,0"
|
||||||
IsChecked="True"
|
IsChecked="True"
|
||||||
Style="{StaticResource FsTabPillStyle}"
|
Style="{StaticResource FsTabPillStyle}"
|
||||||
|
|
@ -292,8 +280,15 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- View content -->
|
<!-- View content (좌측: 탭 컨텐츠 / 우측: Preview 컬럼) -->
|
||||||
<Grid Grid.Row="1">
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" MinWidth="360"/>
|
||||||
|
<ColumnDefinition Width="380"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 좌측: Active Queue OR Past Results -->
|
||||||
|
<Grid Grid.Column="0">
|
||||||
<!-- Active Queue view -->
|
<!-- Active Queue view -->
|
||||||
<Grid x:Name="ActiveQueueView" Visibility="Collapsed">
|
<Grid x:Name="ActiveQueueView" Visibility="Collapsed">
|
||||||
<Grid x:Name="DropZoneEmpty">
|
<Grid x:Name="DropZoneEmpty">
|
||||||
|
|
@ -323,7 +318,7 @@
|
||||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
MouseLeftButtonUp="OnQueueRowClick"
|
PreviewMouseLeftButtonUp="OnQueueRowClick"
|
||||||
Tag="{Binding}">
|
Tag="{Binding}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
|
@ -381,129 +376,6 @@
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Preview view -->
|
|
||||||
<Grid x:Name="PreviewView" Visibility="Collapsed" Background="{StaticResource FsBgBase}">
|
|
||||||
<Grid Margin="32">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="320"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- Image area -->
|
|
||||||
<Border Background="{StaticResource FsBgPanel}"
|
|
||||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
|
||||||
BorderThickness="1" CornerRadius="8">
|
|
||||||
<Grid>
|
|
||||||
<!-- Empty state -->
|
|
||||||
<StackPanel x:Name="PreviewEmpty" HorizontalAlignment="Center" VerticalAlignment="Center">
|
|
||||||
<Path Width="40" Height="40" Stretch="Uniform"
|
|
||||||
Stroke="{StaticResource FsTextTertiary}" StrokeThickness="1.5"
|
|
||||||
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"/>
|
|
||||||
<TextBlock Margin="0,12,0,0" FontSize="14" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource FsTextSecondary}"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Text="No preview selected"/>
|
|
||||||
<TextBlock Margin="0,4,0,0" Style="{StaticResource FsCaptionStyle}"
|
|
||||||
HorizontalAlignment="Center" TextAlignment="Center"
|
|
||||||
Text="Active Queue 항목을 클릭하면
여기에 미리보기가 표시됩니다."/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Loading -->
|
|
||||||
<StackPanel x:Name="PreviewLoading" Visibility="Collapsed"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
|
||||||
<ProgressBar IsIndeterminate="True" Width="200" Height="3"
|
|
||||||
Foreground="{StaticResource FsAccentBlue}"
|
|
||||||
Background="{StaticResource FsBgInput}"/>
|
|
||||||
<TextBlock Margin="0,12,0,0" Style="{StaticResource FsCaptionStyle}"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Text="미리보기 생성 중…"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Image -->
|
|
||||||
<Image x:Name="PreviewImage" Stretch="Uniform" Margin="24"
|
|
||||||
Visibility="Collapsed"/>
|
|
||||||
|
|
||||||
<!-- Reason / fallback -->
|
|
||||||
<StackPanel x:Name="PreviewReason" Visibility="Collapsed"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
|
||||||
<Path Width="36" Height="36" Stretch="Uniform"
|
|
||||||
Stroke="{StaticResource FsAccentAmber}" StrokeThickness="1.5"
|
|
||||||
Data="M12,2 L22,20 L2,20 Z M12,9 L12,14 M12,17 L12,17.01"/>
|
|
||||||
<TextBlock x:Name="PreviewReasonText" Margin="0,12,0,0"
|
|
||||||
Style="{StaticResource FsBodyStyle}"
|
|
||||||
HorizontalAlignment="Center" TextAlignment="Center"
|
|
||||||
TextWrapping="Wrap" MaxWidth="360"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Meta panel -->
|
|
||||||
<Border Grid.Column="1" Margin="16,0,0,0"
|
|
||||||
Background="{StaticResource FsBgPanel}"
|
|
||||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
|
||||||
BorderThickness="1" CornerRadius="8" Padding="20">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="FILE DETAIL" Style="{StaticResource FsLabelStyle}"/>
|
|
||||||
<TextBlock x:Name="PreviewFileName" Margin="0,8,0,0"
|
|
||||||
Style="{StaticResource FsBodyStyle}"
|
|
||||||
FontWeight="SemiBold" TextWrapping="Wrap"
|
|
||||||
Text="—"/>
|
|
||||||
<TextBlock x:Name="PreviewFilePath" Margin="0,4,0,0"
|
|
||||||
FontFamily="{StaticResource FsFontMono}" FontSize="11"
|
|
||||||
Foreground="{StaticResource FsTextTertiary}"
|
|
||||||
TextWrapping="Wrap"/>
|
|
||||||
|
|
||||||
<Border Margin="0,16,0,16" Height="1" Background="{StaticResource FsBorderSubtle}"/>
|
|
||||||
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Format"
|
|
||||||
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,6"/>
|
|
||||||
<TextBlock x:Name="PreviewFormatText" Grid.Row="0" Grid.Column="1"
|
|
||||||
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
|
||||||
Foreground="{StaticResource FsTextPrimary}" Margin="0,0,0,6"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Size"
|
|
||||||
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,6"/>
|
|
||||||
<TextBlock x:Name="PreviewSizeText" Grid.Row="1" Grid.Column="1"
|
|
||||||
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
|
||||||
Foreground="{StaticResource FsTextPrimary}" Margin="0,0,0,6"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Dimensions"
|
|
||||||
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,6"/>
|
|
||||||
<TextBlock x:Name="PreviewDimText" Grid.Row="2" Grid.Column="1"
|
|
||||||
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
|
||||||
Foreground="{StaticResource FsTextPrimary}" Text="—"
|
|
||||||
Margin="0,0,0,6"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Pages"
|
|
||||||
Style="{StaticResource FsCaptionStyle}"/>
|
|
||||||
<TextBlock x:Name="PreviewPageText" Grid.Row="3" Grid.Column="1"
|
|
||||||
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
|
||||||
Foreground="{StaticResource FsTextPrimary}" Text="—"/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Border Margin="0,20,0,16" Height="1" Background="{StaticResource FsBorderSubtle}"/>
|
|
||||||
|
|
||||||
<Button Content="Open in Explorer"
|
|
||||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
|
||||||
Click="OnPreviewOpenFolder"
|
|
||||||
HorizontalAlignment="Stretch"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Past Results view (default) -->
|
<!-- Past Results view (default) -->
|
||||||
<ScrollViewer x:Name="PastResultsView" VerticalScrollBarVisibility="Auto"
|
<ScrollViewer x:Name="PastResultsView" VerticalScrollBarVisibility="Auto"
|
||||||
Padding="24">
|
Padding="24">
|
||||||
|
|
@ -537,7 +409,11 @@
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Border BorderBrush="#0DFFFFFF"
|
<Border BorderBrush="#0DFFFFFF"
|
||||||
BorderThickness="0,0,0,1" Padding="0,12">
|
BorderThickness="0,0,0,1" Padding="0,12"
|
||||||
|
Cursor="Hand"
|
||||||
|
Background="Transparent"
|
||||||
|
PreviewMouseLeftButtonUp="OnPastRowClick"
|
||||||
|
Tag="{Binding}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="40"/>
|
<ColumnDefinition Width="40"/>
|
||||||
|
|
@ -617,8 +493,154 @@
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<!-- Drop hint overlay -->
|
</Grid>
|
||||||
<Border x:Name="DropHintOverlay" Visibility="Collapsed"
|
<!-- /좌측 컬럼 끝 -->
|
||||||
|
|
||||||
|
<!-- 우측: Preview 컬럼 (항상 보임) -->
|
||||||
|
<Border Grid.Column="1"
|
||||||
|
Background="{StaticResource FsBgPanel}"
|
||||||
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
|
BorderThickness="1,0,0,0">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 헤더 -->
|
||||||
|
<Border Grid.Row="0" Padding="20,16,20,12"
|
||||||
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
|
BorderThickness="0,0,0,1">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Path Width="14" Height="14" Stretch="Uniform"
|
||||||
|
Stroke="{StaticResource FsTextSecondary}" StrokeThickness="2"
|
||||||
|
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"/>
|
||||||
|
<TextBlock Text="PREVIEW" Margin="8,0,0,0"
|
||||||
|
Style="{StaticResource FsLabelStyle}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 이미지 영역 -->
|
||||||
|
<Grid Grid.Row="1" Margin="20,16,20,16" x:Name="PreviewImageArea">
|
||||||
|
<Border Background="{StaticResource FsBgInput}"
|
||||||
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
|
BorderThickness="1" CornerRadius="6">
|
||||||
|
<Grid>
|
||||||
|
<!-- Empty state -->
|
||||||
|
<StackPanel x:Name="PreviewEmpty" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
|
||||||
|
<Path Width="36" Height="36" Stretch="Uniform"
|
||||||
|
Stroke="{StaticResource FsTextTertiary}" StrokeThickness="1.5"
|
||||||
|
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"/>
|
||||||
|
<TextBlock Margin="0,12,0,0" FontSize="13" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource FsTextSecondary}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Text="No selection"/>
|
||||||
|
<TextBlock Margin="0,4,0,0" Style="{StaticResource FsCaptionStyle}"
|
||||||
|
HorizontalAlignment="Center" TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap" MaxWidth="240"
|
||||||
|
Text="Active Queue 항목을 클릭하면 여기에 미리보기가 표시됩니다."/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<StackPanel x:Name="PreviewLoading" Visibility="Collapsed"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<ProgressBar IsIndeterminate="True" Width="180" Height="3"
|
||||||
|
Foreground="{StaticResource FsAccentBlue}"
|
||||||
|
Background="{StaticResource FsBgSurface}"/>
|
||||||
|
<TextBlock Margin="0,12,0,0" Style="{StaticResource FsCaptionStyle}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Text="미리보기 생성 중…"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Image -->
|
||||||
|
<Image x:Name="PreviewImage" Stretch="Uniform" Margin="16"
|
||||||
|
Visibility="Collapsed"/>
|
||||||
|
|
||||||
|
<!-- Reason -->
|
||||||
|
<StackPanel x:Name="PreviewReason" Visibility="Collapsed"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
|
||||||
|
<Path Width="32" Height="32" Stretch="Uniform"
|
||||||
|
Stroke="{StaticResource FsAccentAmber}" StrokeThickness="1.5"
|
||||||
|
Data="M12,2 L22,20 L2,20 Z M12,9 L12,14 M12,17 L12,17.01"/>
|
||||||
|
<TextBlock x:Name="PreviewReasonText" Margin="0,12,0,0"
|
||||||
|
Style="{StaticResource FsBodyStyle}"
|
||||||
|
FontSize="12"
|
||||||
|
HorizontalAlignment="Center" TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap" MaxWidth="280"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 메타 -->
|
||||||
|
<Border Grid.Row="2" Padding="20,12,20,20"
|
||||||
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
|
BorderThickness="0,1,0,0">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock x:Name="PreviewFileName"
|
||||||
|
Style="{StaticResource FsBodyStyle}"
|
||||||
|
FontWeight="SemiBold" TextWrapping="NoWrap"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
Text="—"/>
|
||||||
|
<TextBlock x:Name="PreviewFilePath" Margin="0,4,0,0"
|
||||||
|
FontFamily="{StaticResource FsFontMono}" FontSize="11"
|
||||||
|
Foreground="{StaticResource FsTextTertiary}"
|
||||||
|
TextTrimming="CharacterEllipsis" MaxHeight="32"/>
|
||||||
|
|
||||||
|
<Border Margin="0,12,0,12" Height="1"
|
||||||
|
Background="{StaticResource FsBorderSubtle}"/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Format"
|
||||||
|
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
||||||
|
<TextBlock x:Name="PreviewFormatText" Grid.Row="0" Grid.Column="1"
|
||||||
|
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
||||||
|
Foreground="{StaticResource FsTextPrimary}" Margin="0,0,0,4"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Size"
|
||||||
|
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
||||||
|
<TextBlock x:Name="PreviewSizeText" Grid.Row="1" Grid.Column="1"
|
||||||
|
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
||||||
|
Foreground="{StaticResource FsTextPrimary}" Margin="0,0,0,4"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="Dimensions"
|
||||||
|
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
||||||
|
<TextBlock x:Name="PreviewDimText" Grid.Row="2" Grid.Column="1"
|
||||||
|
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
||||||
|
Foreground="{StaticResource FsTextPrimary}" Text="—"
|
||||||
|
Margin="0,0,0,4"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="Pages"
|
||||||
|
Style="{StaticResource FsCaptionStyle}"/>
|
||||||
|
<TextBlock x:Name="PreviewPageText" Grid.Row="3" Grid.Column="1"
|
||||||
|
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
||||||
|
Foreground="{StaticResource FsTextPrimary}" Text="—"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Button Content="Open in Explorer" Margin="0,16,0,0"
|
||||||
|
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||||
|
Click="OnPreviewOpenFolder"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Drop hint overlay (전체 덮음) -->
|
||||||
|
<Border x:Name="DropHintOverlay" Visibility="Collapsed" Grid.ColumnSpan="2"
|
||||||
Background="#CC090A0C" IsHitTestVisible="False">
|
Background="#CC090A0C" IsHitTestVisible="False">
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
<Path Width="64" Height="64" Stretch="Uniform"
|
<Path Width="64" Height="64" Stretch="Uniform"
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
private CancellationTokenSource? _cts;
|
private CancellationTokenSource? _cts;
|
||||||
private NameCollision _conflictRule = NameCollision.AppendNumber;
|
private NameCollision _conflictRule = NameCollision.AppendNumber;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow() : this(null) { }
|
||||||
|
|
||||||
|
public MainWindow(IReadOnlyList<string>? initialFiles)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
|
@ -30,9 +32,18 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
SeedDemoHistory();
|
SeedDemoHistory();
|
||||||
UpdateBadges();
|
UpdateBadges();
|
||||||
UpdateProcessQueueButton();
|
UpdateProcessQueueButton();
|
||||||
ShowTab("Past");
|
|
||||||
UpdateActiveQueueVisibility();
|
|
||||||
|
|
||||||
|
if (initialFiles is { Count: > 0 })
|
||||||
|
{
|
||||||
|
AddToQueue(initialFiles);
|
||||||
|
ShowTab("Active");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ShowTab("Past");
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateActiveQueueVisibility();
|
||||||
ApplyAppDataStats();
|
ApplyAppDataStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,11 +60,9 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
private void ShowTab(string tag)
|
private void ShowTab(string tag)
|
||||||
{
|
{
|
||||||
TabActiveBtn.IsChecked = tag == "Active";
|
TabActiveBtn.IsChecked = tag == "Active";
|
||||||
TabPreviewBtn.IsChecked = tag == "Preview";
|
|
||||||
TabPastBtn.IsChecked = tag == "Past";
|
TabPastBtn.IsChecked = tag == "Past";
|
||||||
|
|
||||||
ActiveQueueView.Visibility = tag == "Active" ? Visibility.Visible : Visibility.Collapsed;
|
ActiveQueueView.Visibility = tag == "Active" ? Visibility.Visible : Visibility.Collapsed;
|
||||||
PreviewView.Visibility = tag == "Preview" ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
PastResultsView.Visibility = tag == "Past" ? 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)
|
private void AddToQueue(IEnumerable<string> paths)
|
||||||
{
|
{
|
||||||
|
var wasEmpty = _activeQueue.Count == 0;
|
||||||
var existing = new HashSet<string>(_activeQueue.Select(q => q.SourcePath), StringComparer.OrdinalIgnoreCase);
|
var existing = new HashSet<string>(_activeQueue.Select(q => q.SourcePath), StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var path in paths)
|
foreach (var path in paths)
|
||||||
{
|
{
|
||||||
|
|
@ -107,6 +117,12 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
UpdateBadges();
|
UpdateBadges();
|
||||||
UpdateProcessQueueButton();
|
UpdateProcessQueueButton();
|
||||||
UpdateActiveQueueVisibility();
|
UpdateActiveQueueVisibility();
|
||||||
|
|
||||||
|
if (wasEmpty && _activeQueue.Count > 0 && _selectedPreviewItem is null)
|
||||||
|
{
|
||||||
|
_selectedPreviewItem = _activeQueue[0];
|
||||||
|
_ = LoadPreviewAsync(_selectedPreviewItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnRemoveQueueItem(object sender, RoutedEventArgs e)
|
private void OnRemoveQueueItem(object sender, RoutedEventArgs e)
|
||||||
|
|
@ -130,7 +146,6 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
private void UpdateBadges()
|
private void UpdateBadges()
|
||||||
{
|
{
|
||||||
TabActiveBadge.Text = _activeQueue.Count.ToString(CultureInfo.InvariantCulture);
|
TabActiveBadge.Text = _activeQueue.Count.ToString(CultureInfo.InvariantCulture);
|
||||||
TabPreviewBadge.Text = "0";
|
|
||||||
var count = _pastResults.Sum(g => g.Entries.Count);
|
var count = _pastResults.Sum(g => g.Entries.Count);
|
||||||
TabPastBadge.Text = count.ToString(CultureInfo.InvariantCulture);
|
TabPastBadge.Text = count.ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
@ -211,37 +226,74 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
// ============== Preview ==============
|
// ============== Preview ==============
|
||||||
|
|
||||||
private QueueItem? _selectedPreviewItem;
|
private QueueItem? _selectedPreviewItem;
|
||||||
|
private string? _selectedPreviewPath;
|
||||||
private CancellationTokenSource? _previewCts;
|
private CancellationTokenSource? _previewCts;
|
||||||
|
|
||||||
private async void OnQueueRowClick(object sender, MouseButtonEventArgs e)
|
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;
|
if (sender is not FrameworkElement fe || fe.Tag is not QueueItem item) return;
|
||||||
|
|
||||||
_selectedPreviewItem = item;
|
_selectedPreviewItem = item;
|
||||||
ShowTab("Preview");
|
|
||||||
await LoadPreviewAsync(item);
|
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?.Cancel();
|
||||||
_previewCts = new CancellationTokenSource();
|
_previewCts = new CancellationTokenSource();
|
||||||
var token = _previewCts.Token;
|
var token = _previewCts.Token;
|
||||||
|
|
||||||
PreviewEmpty.Visibility = Visibility.Collapsed;
|
SetPreviewMeta(fileName, sourcePath, formatLabel, sizeText);
|
||||||
PreviewImage.Visibility = Visibility.Collapsed;
|
ShowPreviewLoading();
|
||||||
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 = "—";
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await PreviewService.CreateAsync(item.SourcePath, 720, token);
|
var result = await PreviewService.CreateAsync(sourcePath, 720, token);
|
||||||
if (token.IsCancellationRequested) return;
|
if (token.IsCancellationRequested) return;
|
||||||
|
|
||||||
PreviewLoading.Visibility = Visibility.Collapsed;
|
PreviewLoading.Visibility = Visibility.Collapsed;
|
||||||
|
|
@ -264,20 +316,47 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
PreviewLoading.Visibility = Visibility.Collapsed;
|
PreviewLoading.Visibility = Visibility.Collapsed;
|
||||||
PreviewReasonText.Text = "미리보기 오류: " + ex.Message;
|
ShowPreviewReason("미리보기 오류: " + ex.Message);
|
||||||
PreviewReason.Visibility = Visibility.Visible;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
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
|
try
|
||||||
{
|
{
|
||||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = "explorer.exe",
|
FileName = "explorer.exe",
|
||||||
Arguments = $"/select,\"{_selectedPreviewItem.SourcePath}\"",
|
Arguments = $"/select,\"{path}\"",
|
||||||
UseShellExecute = true,
|
UseShellExecute = true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue