ux: FormatShift Utility 디자인을 메인 윈도우에 충실히 재현
새 메인 윈도우 (1280x960, dark minimal): - 320px 사이드바 — Target Format(JPG), Encoding Quality 슬라이더(파란색 fill + 흰 thumb), Output Destination, Skip/Rename/Replace 세그먼트, Stats Box, Processing Queue 버튼(disabled→enabled 상태 전이) - 메인 영역 — pill 탭 3개(Active Queue / Preview / Past Results), 액션 버튼 (Register Menu / Diagnose / Export Log / Clear All) - Past Results — 날짜별 그룹화, "Today/Yesterday" 헤더 + Session Savings 뱃지, 파일 아이콘(PDF/PNG/HEIC/JPG/...), 사이즈/savings/status 컬럼 - Active Queue — 빈 상태 드롭존 + 파일 행, 행 클릭 시 Preview 자동 전환 - Drop hint overlay — 파일 끌어올 때 흐릿한 가이드 표시 핵심 신규 인프라: - FormatShiftTheme.xaml — 디자인 토큰(색/타이포/슬라이더 스타일/탭 pill 스타일/ 세그먼트 스타일/primary·secondary 버튼) 전부 - PreviewService — 이미지/RAW(Magick), HEIC(libheif decode), PDF(PDFium)을 720px 긴변 BitmapSource로 렌더. DOCX/HWP/HTML은 안내 문구 fallback - HistoryStore — 변환 결과 누적(메모리), 날짜별 group + session savings 합산 - Export Log — Past Results를 CSV/JSON으로 저장 기존 기능 통합: - 사이드바 옵션이 ConvertOptions에 매핑 (Quality, Output Path, Conflict) - Process Queue 버튼이 ConvertManyAsync 호출 → 결과를 History에 누적 - Register Menu / Diagnose 액션 유지 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8065bf7373
commit
01467a4d27
5 changed files with 1701 additions and 278 deletions
293
src/EverythingToJpeg.App/Views/FormatShiftTheme.xaml
Normal file
293
src/EverythingToJpeg.App/Views/FormatShiftTheme.xaml
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- Backgrounds -->
|
||||
<SolidColorBrush x:Key="FsBgBase" Color="#090A0C"/>
|
||||
<SolidColorBrush x:Key="FsBgPanel" Color="#131417"/>
|
||||
<SolidColorBrush x:Key="FsBgSurface" Color="#1C1E22"/>
|
||||
<SolidColorBrush x:Key="FsBgSurfaceHover" Color="#23252A"/>
|
||||
<SolidColorBrush x:Key="FsBgInput" Color="#0F1012"/>
|
||||
|
||||
<!-- Borders -->
|
||||
<SolidColorBrush x:Key="FsBorderSubtle" Color="#26282D"/>
|
||||
<SolidColorBrush x:Key="FsBorderStrong" Color="#3A3D45"/>
|
||||
|
||||
<!-- Text -->
|
||||
<SolidColorBrush x:Key="FsTextPrimary" Color="#ECEEFA"/>
|
||||
<SolidColorBrush x:Key="FsTextSecondary" Color="#8B909A"/>
|
||||
<SolidColorBrush x:Key="FsTextTertiary" Color="#5D626C"/>
|
||||
|
||||
<!-- Accents -->
|
||||
<SolidColorBrush x:Key="FsAccentBlue" Color="#3B72FF"/>
|
||||
<SolidColorBrush x:Key="FsAccentGreen" Color="#10B981"/>
|
||||
<SolidColorBrush x:Key="FsAccentAmber" Color="#F59E0B"/>
|
||||
<SolidColorBrush x:Key="FsAccentRed" Color="#EF4444"/>
|
||||
|
||||
<!-- Format pills -->
|
||||
<SolidColorBrush x:Key="FsFmtPdf" Color="#E53E3E"/>
|
||||
<SolidColorBrush x:Key="FsFmtPng" Color="#805AD5"/>
|
||||
<SolidColorBrush x:Key="FsFmtHeic" Color="#D69E2E"/>
|
||||
<SolidColorBrush x:Key="FsFmtJpg" Color="#3182CE"/>
|
||||
<SolidColorBrush x:Key="FsFmtDocx" Color="#2B6CB0"/>
|
||||
<SolidColorBrush x:Key="FsFmtHtml" Color="#DD6B20"/>
|
||||
<SolidColorBrush x:Key="FsFmtRaw" Color="#319795"/>
|
||||
<SolidColorBrush x:Key="FsFmtGif" Color="#9F7AEA"/>
|
||||
<SolidColorBrush x:Key="FsFmtTiff" Color="#38B2AC"/>
|
||||
<SolidColorBrush x:Key="FsFmtWebp" Color="#48BB78"/>
|
||||
<SolidColorBrush x:Key="FsFmtBmp" Color="#718096"/>
|
||||
<SolidColorBrush x:Key="FsFmtHwp" Color="#9B2C2C"/>
|
||||
<SolidColorBrush x:Key="FsFmtOther" Color="#4A5568"/>
|
||||
|
||||
<!-- Font families -->
|
||||
<FontFamily x:Key="FsFontSans">Inter, Segoe UI Variable Text, Segoe UI</FontFamily>
|
||||
<FontFamily x:Key="FsFontMono">JetBrains Mono, Cascadia Mono, Consolas</FontFamily>
|
||||
|
||||
<!-- Typography styles -->
|
||||
<Style x:Key="FsLabelStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource FsFontSans}"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource FsTextSecondary}"/>
|
||||
<Setter Property="TextOptions.TextFormattingMode" Value="Display"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="FsCaptionStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource FsFontSans}"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource FsTextTertiary}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="FsBodyStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource FsFontSans}"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource FsTextPrimary}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="FsMonoStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource FsFontMono}"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource FsTextSecondary}"/>
|
||||
</Style>
|
||||
|
||||
<!-- Slider style: 2px track, 14px round thumb, blue fill up to value -->
|
||||
<Style x:Key="FsSliderStyle" TargetType="Slider">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource FsAccentBlue}"/>
|
||||
<Setter Property="MinHeight" Value="20"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="IsMoveToPointEnabled" Value="True"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Slider">
|
||||
<Grid>
|
||||
<Border Height="2" CornerRadius="1"
|
||||
Background="{StaticResource FsBorderStrong}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Track x:Name="PART_Track">
|
||||
<Track.DecreaseRepeatButton>
|
||||
<RepeatButton Command="Slider.DecreaseLarge">
|
||||
<RepeatButton.Template>
|
||||
<ControlTemplate TargetType="RepeatButton">
|
||||
<Border Height="2" CornerRadius="1"
|
||||
Background="{StaticResource FsAccentBlue}"
|
||||
VerticalAlignment="Center"/>
|
||||
</ControlTemplate>
|
||||
</RepeatButton.Template>
|
||||
</RepeatButton>
|
||||
</Track.DecreaseRepeatButton>
|
||||
<Track.Thumb>
|
||||
<Thumb>
|
||||
<Thumb.Template>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Ellipse Width="14" Height="14"
|
||||
Fill="{StaticResource FsTextPrimary}"/>
|
||||
</ControlTemplate>
|
||||
</Thumb.Template>
|
||||
</Thumb>
|
||||
</Track.Thumb>
|
||||
<Track.IncreaseRepeatButton>
|
||||
<RepeatButton Command="Slider.IncreaseLarge">
|
||||
<RepeatButton.Template>
|
||||
<ControlTemplate TargetType="RepeatButton">
|
||||
<Border Background="Transparent"/>
|
||||
</ControlTemplate>
|
||||
</RepeatButton.Template>
|
||||
</RepeatButton>
|
||||
</Track.IncreaseRepeatButton>
|
||||
</Track>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Tab pill style -->
|
||||
<Style x:Key="FsTabPillStyle" TargetType="ToggleButton">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource FsTextSecondary}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="14,6"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border x:Name="PART_Bd" CornerRadius="20"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextElement.Foreground="{TemplateBinding Foreground}"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsAccentBlue}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsBgSurfaceHover}"/>
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsChecked" Value="True"/>
|
||||
<Condition Property="IsMouseOver" Value="True"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsAccentBlue}"/>
|
||||
</MultiTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Segment button style (Skip / Rename / Replace) -->
|
||||
<Style x:Key="FsSegmentStyle" TargetType="ToggleButton">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource FsTextSecondary}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border x:Name="PART_Bd" CornerRadius="4" Padding="0,6">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextElement.Foreground="{TemplateBinding Foreground}"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsBgSurface}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource FsTextPrimary}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Primary button (Processing Queue / Run) -->
|
||||
<Style x:Key="FsPrimaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource FsAccentBlue}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="12"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="PART_Bd"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="6"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
TextElement.Foreground="{TemplateBinding Foreground}"
|
||||
TextElement.FontSize="{TemplateBinding FontSize}"
|
||||
TextElement.FontWeight="{TemplateBinding FontWeight}"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="PART_Bd" Property="Opacity" Value="0.5"/>
|
||||
<Setter Property="Cursor" Value="Arrow"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="PART_Bd" Property="Background" Value="#2D5DDB"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Secondary button -->
|
||||
<Style x:Key="FsSecondaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource FsBgSurface}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource FsTextPrimary}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource FsBorderSubtle}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="12,6"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="PART_Bd"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="6"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
TextElement.Foreground="{TemplateBinding Foreground}"
|
||||
TextElement.FontSize="{TemplateBinding FontSize}"
|
||||
TextElement.FontWeight="{TemplateBinding FontWeight}"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsBgSurfaceHover}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Tab badge -->
|
||||
<Style x:Key="FsTabBadgeStyle" TargetType="Border">
|
||||
<Setter Property="CornerRadius" Value="10"/>
|
||||
<Setter Property="Padding" Value="6,1"/>
|
||||
<Setter Property="Background" Value="{StaticResource FsBgSurface}"/>
|
||||
</Style>
|
||||
|
||||
<!-- Path input box -->
|
||||
<Style x:Key="FsPathInputStyle" TargetType="TextBox">
|
||||
<Setter Property="Background" Value="{StaticResource FsBgInput}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource FsTextSecondary}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource FsBorderSubtle}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="10,8"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource FsFontMono}"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="CaretBrush" Value="{StaticResource FsTextPrimary}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="6"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ScrollViewer x:Name="PART_ContentHost"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
|
|
@ -2,110 +2,636 @@
|
|||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
Title="EverythingToJpeg"
|
||||
Width="880" Height="640"
|
||||
MinWidth="640" MinHeight="480"
|
||||
Title="FormatShift Utility"
|
||||
Width="1280" Height="960"
|
||||
MinWidth="1080" MinHeight="640"
|
||||
ExtendsContentIntoTitleBar="True"
|
||||
WindowBackdropType="Mica"
|
||||
WindowBackdropType="None"
|
||||
WindowCornerPreference="Round"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
AllowDrop="True"
|
||||
Drop="OnFilesDropped"
|
||||
DragOver="OnDragOver"
|
||||
DragLeave="OnDragLeave">
|
||||
<Grid>
|
||||
DragLeave="OnDragLeave"
|
||||
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>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="32"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:TitleBar Grid.Row="0" Title="EverythingToJpeg">
|
||||
<ui:TitleBar.Icon>
|
||||
<ui:SymbolIcon Symbol="ImageMultiple24"/>
|
||||
</ui:TitleBar.Icon>
|
||||
</ui:TitleBar>
|
||||
<!-- Title bar (window controls only) -->
|
||||
<ui:TitleBar Grid.Row="0" Title="" ShowMaximize="True" ShowMinimize="True"/>
|
||||
|
||||
<Grid Grid.Row="1" Margin="32,16,32,32">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<!-- App body -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="320"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Hero -->
|
||||
<StackPanel Grid.Row="0" Margin="0,0,0,24">
|
||||
<TextBlock Text="모든 것을 JPEG로." Style="{StaticResource TextTitleLarge}"/>
|
||||
<TextBlock Margin="0,4,0,0" Style="{StaticResource TextBody}"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}">
|
||||
파일을 우클릭하면 끝. PNG · GIF · HEIC · RAW · PDF · DOCX 가 모두 한 번에 변환됩니다.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<!-- ============== SIDEBAR ============== -->
|
||||
<Border Grid.Column="0"
|
||||
Background="{StaticResource FsBgPanel}"
|
||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="56"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Drop zone -->
|
||||
<Border x:Name="DropZone" Grid.Row="1"
|
||||
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
||||
BorderThickness="2"
|
||||
Background="{DynamicResource ControlFillColorDefaultBrush}"
|
||||
CornerRadius="12"
|
||||
Padding="32,24"
|
||||
Margin="0,0,0,20">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsDraggingOver, RelativeSource={RelativeSource AncestorType=Window}}" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemFillColorAttentionBrush}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<ui:SymbolIcon Symbol="ImageArrowCounterclockwise24" FontSize="36"
|
||||
Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="여기에 파일이나 폴더를 끌어다 놓으세요"
|
||||
Style="{StaticResource TextSubtitle}"
|
||||
HorizontalAlignment="Center" Margin="0,12,0,4"/>
|
||||
<TextBlock Style="{StaticResource TextCaption}" HorizontalAlignment="Center">
|
||||
또는
|
||||
<Hyperlink Click="OnPickFilesClick">파일 선택</Hyperlink>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<!-- Sidebar header -->
|
||||
<Border Grid.Row="0" BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="0,0,0,1" Padding="24,0">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Path Width="18" Height="18" Stretch="Uniform"
|
||||
Stroke="{StaticResource FsAccentBlue}" StrokeThickness="2"
|
||||
Data="M3,3 L21,3 L21,21 L3,21 Z M12,8 L12,16 M8,12 L16,12"/>
|
||||
<TextBlock Margin="8,0,0,0" FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource FsTextPrimary}"
|
||||
FontFamily="{StaticResource FsFontSans}">
|
||||
FormatShift
|
||||
<Run Foreground="{StaticResource FsTextTertiary}" FontWeight="Medium"
|
||||
Text=" Utility"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Sidebar content -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="24,24,24,24">
|
||||
<StackPanel>
|
||||
<!-- Target Format -->
|
||||
<StackPanel Margin="0,0,0,32">
|
||||
<TextBlock Text="TARGET FORMAT" Style="{StaticResource FsLabelStyle}"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0" VerticalAlignment="Center">
|
||||
<Border Width="24" Height="24" CornerRadius="4"
|
||||
Background="{StaticResource FsFmtJpg}">
|
||||
<TextBlock Text="JPG" Foreground="White" FontSize="8"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="JPEG Image" Margin="8,0,0,0"
|
||||
Style="{StaticResource FsBodyStyle}"
|
||||
FontWeight="Medium" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Encoding Quality -->
|
||||
<StackPanel Margin="0,0,0,32">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="ENCODING QUALITY" Style="{StaticResource FsLabelStyle}"/>
|
||||
<Border Grid.Column="1" CornerRadius="4"
|
||||
Background="{StaticResource FsBgInput}"
|
||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="1" Padding="6,2">
|
||||
<TextBlock x:Name="QualityValueText"
|
||||
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
||||
Foreground="{StaticResource FsAccentBlue}"
|
||||
Text="85%"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
<Slider x:Name="QualitySlider" Margin="0,12,0,0"
|
||||
Style="{StaticResource FsSliderStyle}"
|
||||
Minimum="1" Maximum="100" Value="85"
|
||||
ValueChanged="OnQualityChanged"/>
|
||||
<Grid Margin="0,4,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="Smaller File" Style="{StaticResource FsCaptionStyle}"/>
|
||||
<TextBlock Grid.Column="1" Text="Higher Quality" Style="{StaticResource FsCaptionStyle}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Output Destination -->
|
||||
<StackPanel Margin="0,0,0,32">
|
||||
<TextBlock Text="OUTPUT DESTINATION" Style="{StaticResource FsLabelStyle}"/>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox x:Name="OutputPathTextBox"
|
||||
Style="{StaticResource FsPathInputStyle}"
|
||||
ToolTip="비워두면 원본 폴더 안 _jpeg 하위에 저장"/>
|
||||
<Button Grid.Column="1" Margin="8,0,0,0" Width="36"
|
||||
Content="…" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Click="OnPickOutputFolderClick"/>
|
||||
</Grid>
|
||||
<TextBlock Margin="0,6,0,0"
|
||||
Style="{StaticResource FsCaptionStyle}"
|
||||
Text="비워두면 원본 옆 _jpeg 폴더에 저장됩니다"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- File Conflict Rule -->
|
||||
<StackPanel Margin="0,0,0,32">
|
||||
<TextBlock Text="FILE CONFLICT RULE" Style="{StaticResource FsLabelStyle}"/>
|
||||
<Border Margin="0,8,0,0"
|
||||
Background="{StaticResource FsBgInput}"
|
||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="2">
|
||||
<UniformGrid Rows="1" Columns="3">
|
||||
<ToggleButton x:Name="ConflictSkipBtn" Content="Skip"
|
||||
Style="{StaticResource FsSegmentStyle}"
|
||||
Click="OnConflictSegmentClick"
|
||||
Tag="Skip"/>
|
||||
<ToggleButton x:Name="ConflictRenameBtn" Content="Rename"
|
||||
Style="{StaticResource FsSegmentStyle}"
|
||||
IsChecked="True"
|
||||
Click="OnConflictSegmentClick"
|
||||
Tag="Rename"/>
|
||||
<ToggleButton x:Name="ConflictReplaceBtn" Content="Replace"
|
||||
Style="{StaticResource FsSegmentStyle}"
|
||||
Click="OnConflictSegmentClick"
|
||||
Tag="Replace"/>
|
||||
</UniformGrid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Stats Box -->
|
||||
<Border Background="{StaticResource FsBgInput}"
|
||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="12">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel>
|
||||
<TextBlock Text="PROCESSED TODAY"
|
||||
FontFamily="{StaticResource FsFontSans}"
|
||||
FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource FsTextSecondary}"/>
|
||||
<TextBlock x:Name="ProcessedTodayText"
|
||||
FontFamily="{StaticResource FsFontMono}"
|
||||
FontSize="14"
|
||||
Foreground="{StaticResource FsTextPrimary}"
|
||||
Text="0" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1">
|
||||
<TextBlock Text="EST. SPACE SAVED"
|
||||
FontFamily="{StaticResource FsFontSans}"
|
||||
FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource FsTextSecondary}"/>
|
||||
<TextBlock x:Name="SpaceSavedText"
|
||||
FontFamily="{StaticResource FsFontMono}"
|
||||
FontSize="14"
|
||||
Foreground="{StaticResource FsAccentGreen}"
|
||||
Text="0 B" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Sidebar footer -->
|
||||
<Border Grid.Row="2" BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="0,1,0,0" Padding="24">
|
||||
<Button x:Name="ProcessQueueButton"
|
||||
Content="Idle — drop files to begin"
|
||||
Style="{StaticResource FsPrimaryButtonStyle}"
|
||||
IsEnabled="False"
|
||||
Click="OnProcessQueueClick"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Provider list -->
|
||||
<ui:Card Grid.Row="2" Padding="0">
|
||||
<DockPanel>
|
||||
<Grid DockPanel.Dock="Top" Margin="20,16,20,12">
|
||||
<!-- ============== MAIN ============== -->
|
||||
<Grid Grid.Column="1" Background="{StaticResource FsBgBase}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="56"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Nav bar -->
|
||||
<Border Grid.Row="0" BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="0,0,0,1" Padding="24,0">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="지원 형식" Style="{StaticResource TextSubtitle}"/>
|
||||
<TextBlock Grid.Column="1" Style="{StaticResource TextCaption}"
|
||||
VerticalAlignment="Center">
|
||||
준비된 형식만 우클릭 메뉴에 등록됩니다
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="20,0,20,16">
|
||||
<ItemsControl x:Name="ProvidersList"/>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</ui:Card>
|
||||
|
||||
<!-- Action bar -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal"
|
||||
HorizontalAlignment="Right" Margin="0,20,0,0">
|
||||
<ui:Button Content="진단" Icon="{ui:SymbolIcon Stethoscope24}"
|
||||
Click="OnDiagnoseClick" Margin="0,0,8,0"/>
|
||||
<ui:Button x:Name="UnregisterButton" Content="컨텍스트 메뉴 해제"
|
||||
Icon="{ui:SymbolIcon DismissCircle24}"
|
||||
Click="OnUnregisterClick" Margin="0,0,8,0"/>
|
||||
<ui:Button x:Name="RegisterButton" Content="컨텍스트 메뉴 등록"
|
||||
Icon="{ui:SymbolIcon CheckmarkCircle24}"
|
||||
Appearance="Primary" Click="OnRegisterClick"/>
|
||||
</StackPanel>
|
||||
<!-- Tabs -->
|
||||
<Border HorizontalAlignment="Left"
|
||||
Background="{StaticResource FsBgPanel}"
|
||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="1" CornerRadius="20" Padding="4">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ToggleButton x:Name="TabActiveBtn"
|
||||
Style="{StaticResource FsTabPillStyle}"
|
||||
Click="OnTabClick" Tag="Active">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Active Queue" VerticalAlignment="Center"/>
|
||||
<Border Margin="8,0,0,0" Style="{StaticResource FsTabBadgeStyle}">
|
||||
<TextBlock x:Name="TabActiveBadge" Text="0"
|
||||
FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource FsTextTertiary}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</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"
|
||||
IsChecked="True"
|
||||
Style="{StaticResource FsTabPillStyle}"
|
||||
Click="OnTabClick" Tag="Past">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Past Results" VerticalAlignment="Center"/>
|
||||
<Border Margin="8,0,0,0" Style="{StaticResource FsTabBadgeStyle}">
|
||||
<TextBlock x:Name="TabPastBadge" Text="0"
|
||||
FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource FsTextTertiary}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Actions -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button Content="Register Menu" Margin="0,0,8,0"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Click="OnRegisterClick"/>
|
||||
<Button Content="Diagnose" Margin="0,0,8,0"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Click="OnDiagnoseClick"/>
|
||||
<Button Content="Export Log" Margin="0,0,8,0"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Click="OnExportLogClick"/>
|
||||
<Button x:Name="ClearAllButton" Content="Clear All"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Click="OnClearAllClick"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- View content -->
|
||||
<Grid Grid.Row="1">
|
||||
<!-- Active Queue view -->
|
||||
<Grid x:Name="ActiveQueueView" Visibility="Collapsed">
|
||||
<Grid x:Name="DropZoneEmpty">
|
||||
<Border Background="{StaticResource FsBgBase}">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Margin="48">
|
||||
<Path Width="48" Height="48" Stretch="Uniform"
|
||||
Stroke="{StaticResource FsAccentBlue}" StrokeThickness="1.5"
|
||||
Data="M21,15 L21,19 C21,20.1 20.1,21 19,21 L5,21 C3.9,21 3,20.1 3,19 L3,15 M7,10 L12,15 L17,10 M12,15 L12,3"/>
|
||||
<TextBlock Margin="0,16,0,0" FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource FsTextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
Text="Drop files anywhere to queue"/>
|
||||
<TextBlock Margin="0,4,0,0" Style="{StaticResource FsCaptionStyle}"
|
||||
HorizontalAlignment="Center"
|
||||
Text="PNG · JPG · GIF · HEIC · RAW · PDF · DOCX · HTML · HWP/HWPX"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
<ScrollViewer x:Name="ActiveQueueScroll" VerticalScrollBarVisibility="Auto"
|
||||
Padding="24" Visibility="Collapsed">
|
||||
<ItemsControl x:Name="ActiveQueueList">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Margin="0,0,0,8" Padding="12,10" CornerRadius="6"
|
||||
Background="{StaticResource FsBgPanel}"
|
||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="1"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonUp="OnQueueRowClick"
|
||||
Tag="{Binding}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="40"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="100"/>
|
||||
<ColumnDefinition Width="120"/>
|
||||
<ColumnDefinition Width="40"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Width="32" Height="32" CornerRadius="4"
|
||||
Background="{Binding FormatBrush}"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding FormatLabel}"
|
||||
Foreground="White" FontSize="10"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" Margin="16,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding FileName}"
|
||||
Style="{StaticResource FsBodyStyle}"
|
||||
FontWeight="Medium"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding MetaLine}"
|
||||
Style="{StaticResource FsCaptionStyle}"
|
||||
Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Text="{Binding SizeText}"
|
||||
Style="{StaticResource FsMonoStyle}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding StateText}"
|
||||
FontFamily="{StaticResource FsFontMono}"
|
||||
FontSize="12"
|
||||
Foreground="{Binding StateBrush}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="4"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="6"
|
||||
Click="OnRemoveQueueItem"
|
||||
Tag="{Binding}">
|
||||
<Path Width="14" Height="14" Stretch="Uniform"
|
||||
Stroke="{StaticResource FsTextTertiary}"
|
||||
StrokeThickness="2"
|
||||
Data="M18,6 L6,18 M6,6 L18,18"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</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) -->
|
||||
<ScrollViewer x:Name="PastResultsView" VerticalScrollBarVisibility="Auto"
|
||||
Padding="24">
|
||||
<ItemsControl x:Name="PastResultsList">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="0,0,0,40">
|
||||
<!-- Date header -->
|
||||
<Grid Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{Binding DateTitle}"
|
||||
FontFamily="{StaticResource FsFontSans}"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource FsTextPrimary}"/>
|
||||
<Border Grid.Column="1" CornerRadius="20" Padding="10,4"
|
||||
Background="#10B98122">
|
||||
<TextBlock Text="{Binding SessionSavingsText}"
|
||||
FontFamily="{StaticResource FsFontMono}"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource FsAccentGreen}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
<Border Height="1" Background="{StaticResource FsBorderSubtle}"
|
||||
Margin="0,0,0,12"/>
|
||||
|
||||
<!-- Rows -->
|
||||
<ItemsControl ItemsSource="{Binding Entries}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="#0DFFFFFF"
|
||||
BorderThickness="0,0,0,1" Padding="0,12">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="40"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="100"/>
|
||||
<ColumnDefinition Width="100"/>
|
||||
<ColumnDefinition Width="120"/>
|
||||
<ColumnDefinition Width="40"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Width="32" Height="32" CornerRadius="4"
|
||||
Background="{Binding FormatBrush}"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding FormatLabel}"
|
||||
Foreground="White" FontSize="10"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="16,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding FileName}"
|
||||
Style="{StaticResource FsBodyStyle}"
|
||||
FontWeight="Medium"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding MetaLine}"
|
||||
Style="{StaticResource FsCaptionStyle}"
|
||||
Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="{Binding SizeText}"
|
||||
Style="{StaticResource FsMonoStyle}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Column="3"
|
||||
Text="{Binding SavingsText}"
|
||||
FontFamily="{StaticResource FsFontMono}"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource FsAccentGreen}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<StackPanel Grid.Column="4" Orientation="Horizontal"
|
||||
VerticalAlignment="Center">
|
||||
<Path Width="12" Height="12" Stretch="Uniform"
|
||||
Stroke="{StaticResource FsAccentGreen}"
|
||||
StrokeThickness="3"
|
||||
Data="M20,6 L9,17 L4,12"/>
|
||||
<TextBlock Text="Success" Margin="6,0,0,0"
|
||||
FontSize="11" FontWeight="Medium"
|
||||
Foreground="{StaticResource FsAccentGreen}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="5"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="6"
|
||||
Click="OnOpenFolderClick"
|
||||
Tag="{Binding SourcePath}"
|
||||
ToolTip="원본 폴더 열기">
|
||||
<Path Width="14" Height="14" Stretch="Uniform"
|
||||
Stroke="{StaticResource FsTextTertiary}"
|
||||
StrokeThickness="2"
|
||||
Data="M22,19 C22,20.1 21.1,21 20,21 L4,21 C2.9,21 2,20.1 2,19 L2,5 C2,3.9 2.9,3 4,3 L9,3 L11,6 L20,6 C21.1,6 22,6.9 22,8 Z"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Drop hint overlay -->
|
||||
<Border x:Name="DropHintOverlay" Visibility="Collapsed"
|
||||
Background="#CC090A0C" IsHitTestVisible="False">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Path Width="64" Height="64" Stretch="Uniform"
|
||||
Stroke="{StaticResource FsAccentBlue}" StrokeThickness="2"
|
||||
Data="M21,15 L21,19 C21,20.1 20.1,21 19,21 L5,21 C3.9,21 3,20.1 3,19 L3,15 M7,10 L12,15 L17,10 M12,15 L12,3"/>
|
||||
<TextBlock Text="Drop to add to queue" Margin="0,16,0,0"
|
||||
FontSize="18" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource FsTextPrimary}"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ui:FluentWindow>
|
||||
|
|
|
|||
|
|
@ -1,256 +1,532 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using EverythingToJpeg.App.Shell;
|
||||
using EverythingToJpeg.Core.Providers;
|
||||
using Wpf.Ui.Controls;
|
||||
using EverythingToJpeg.Core;
|
||||
|
||||
namespace EverythingToJpeg.App.Views;
|
||||
|
||||
public partial class MainWindow : FluentWindow, INotifyPropertyChanged
|
||||
public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||
{
|
||||
private bool _isDraggingOver;
|
||||
public bool IsDraggingOver
|
||||
{
|
||||
get => _isDraggingOver;
|
||||
set { _isDraggingOver = value; OnPropertyChanged(); }
|
||||
}
|
||||
private readonly ObservableCollection<QueueItem> _activeQueue = new();
|
||||
private readonly ObservableCollection<DateGroup> _pastResults = new();
|
||||
private CancellationTokenSource? _cts;
|
||||
private NameCollision _conflictRule = NameCollision.AppendNumber;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = this;
|
||||
Loaded += async (_, _) => await PopulateAsync();
|
||||
|
||||
ActiveQueueList.ItemsSource = _activeQueue;
|
||||
PastResultsList.ItemsSource = _pastResults;
|
||||
|
||||
SeedDemoHistory();
|
||||
UpdateBadges();
|
||||
UpdateProcessQueueButton();
|
||||
ShowTab("Past");
|
||||
UpdateActiveQueueVisibility();
|
||||
|
||||
ApplyAppDataStats();
|
||||
}
|
||||
|
||||
private async Task PopulateAsync()
|
||||
// ============== Tabs ==============
|
||||
|
||||
private void OnTabClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var engine = ((App)Application.Current).Engine;
|
||||
ProvidersList.Items.Clear();
|
||||
foreach (var provider in engine.Providers.All)
|
||||
if (sender is ToggleButton tb && tb.Tag is string tag)
|
||||
{
|
||||
var availability = await provider.CheckAvailabilityAsync();
|
||||
ProvidersList.Items.Add(BuildProviderRow(provider.Capability, availability));
|
||||
ShowTab(tag);
|
||||
}
|
||||
}
|
||||
|
||||
private static UIElement BuildProviderRow(ProviderCapability cap, ProviderAvailability availability)
|
||||
private void ShowTab(string tag)
|
||||
{
|
||||
var (badge, badgeKey) = cap.Status switch
|
||||
{
|
||||
ProviderStatus.Available => availability.IsReady
|
||||
? ("준비됨", "BadgeReadyBrush")
|
||||
: ("점검 필요", "BadgeWarnBrush"),
|
||||
ProviderStatus.Preview => ("프리뷰", "BadgeInfoBrush"),
|
||||
ProviderStatus.RequiresExternal => availability.IsReady
|
||||
? ("외부 도구 감지됨", "BadgeReadyBrush")
|
||||
: ("외부 도구 필요", "BadgeWarnBrush"),
|
||||
ProviderStatus.ComingSoon => ("개발 중", "BadgeMutedBrush"),
|
||||
_ => ("비활성", "BadgeMutedBrush"),
|
||||
};
|
||||
TabActiveBtn.IsChecked = tag == "Active";
|
||||
TabPreviewBtn.IsChecked = tag == "Preview";
|
||||
TabPastBtn.IsChecked = tag == "Past";
|
||||
|
||||
var card = new CardControl
|
||||
{
|
||||
Padding = new Thickness(16, 12, 16, 12),
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
};
|
||||
|
||||
var stack = new StackPanel();
|
||||
|
||||
var headerStack = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
headerStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = cap.DisplayName,
|
||||
FontFamily = new FontFamily("Segoe UI Variable Text, Segoe UI"),
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
headerStack.Children.Add(new Border
|
||||
{
|
||||
Background = (Brush)Application.Current.FindResource(badgeKey),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(8, 2, 8, 2),
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
Child = new TextBlock { Text = badge, FontSize = 11, Foreground = Brushes.White },
|
||||
});
|
||||
stack.Children.Add(headerStack);
|
||||
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = cap.Summary,
|
||||
FontFamily = new FontFamily("Segoe UI Variable Text, Segoe UI"),
|
||||
FontSize = 12,
|
||||
Foreground = (Brush)Application.Current.FindResource("TextFillColorSecondaryBrush"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 4, 0, 0),
|
||||
});
|
||||
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "확장자: " + string.Join(", ", cap.Extensions),
|
||||
FontSize = 11,
|
||||
Foreground = (Brush)Application.Current.FindResource("TextFillColorTertiaryBrush"),
|
||||
Margin = new Thickness(0, 4, 0, 0),
|
||||
});
|
||||
|
||||
if (!availability.IsReady && !string.IsNullOrEmpty(availability.Reason))
|
||||
{
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = availability.Reason,
|
||||
FontSize = 11,
|
||||
Foreground = (Brush)Application.Current.FindResource("BadgeWarnBrush"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 4, 0, 0),
|
||||
});
|
||||
}
|
||||
|
||||
if (cap.ExternalDependencies.Count > 0)
|
||||
{
|
||||
foreach (var dep in cap.ExternalDependencies)
|
||||
{
|
||||
var line = new TextBlock
|
||||
{
|
||||
FontSize = 11,
|
||||
Foreground = (Brush)Application.Current.FindResource("TextFillColorSecondaryBrush"),
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
line.Inlines.Add(new Run($"• {dep.Name} — {dep.Description} "));
|
||||
if (!string.IsNullOrEmpty(dep.DownloadUrl))
|
||||
{
|
||||
var hl = new Hyperlink(new Run(dep.DownloadUrl)) { NavigateUri = new Uri(dep.DownloadUrl) };
|
||||
hl.RequestNavigate += (_, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = e.Uri.AbsoluteUri,
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
e.Handled = true;
|
||||
};
|
||||
line.Inlines.Add(hl);
|
||||
}
|
||||
stack.Children.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(cap.RoadmapNote))
|
||||
{
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "로드맵: " + cap.RoadmapNote,
|
||||
FontSize = 11,
|
||||
FontStyle = FontStyles.Italic,
|
||||
Foreground = (Brush)Application.Current.FindResource("TextFillColorTertiaryBrush"),
|
||||
Margin = new Thickness(0, 4, 0, 0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
}
|
||||
|
||||
card.Content = stack;
|
||||
return card;
|
||||
ActiveQueueView.Visibility = tag == "Active" ? Visibility.Visible : Visibility.Collapsed;
|
||||
PreviewView.Visibility = tag == "Preview" ? Visibility.Visible : Visibility.Collapsed;
|
||||
PastResultsView.Visibility = tag == "Past" ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// ============== Drag & Drop ==============
|
||||
|
||||
private void OnDragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
{
|
||||
e.Effects = DragDropEffects.Copy;
|
||||
IsDraggingOver = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Effects = DragDropEffects.None;
|
||||
}
|
||||
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop)
|
||||
? DragDropEffects.Copy : DragDropEffects.None;
|
||||
DropHintOverlay.Visibility = e.Effects == DragDropEffects.Copy
|
||||
? Visibility.Visible : Visibility.Collapsed;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnDragLeave(object sender, DragEventArgs e)
|
||||
{
|
||||
IsDraggingOver = false;
|
||||
DropHintOverlay.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void OnFilesDropped(object sender, DragEventArgs e)
|
||||
{
|
||||
IsDraggingOver = false;
|
||||
DropHintOverlay.Visibility = Visibility.Collapsed;
|
||||
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
|
||||
if (e.Data.GetData(DataFormats.FileDrop) is not string[] paths) return;
|
||||
OpenConvertWindow(paths);
|
||||
|
||||
AddToQueue(ExpandPaths(paths));
|
||||
ShowTab("Active");
|
||||
}
|
||||
|
||||
private void OnPickFilesClick(object sender, RoutedEventArgs e)
|
||||
private static IEnumerable<string> ExpandPaths(IEnumerable<string> paths)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = "변환할 파일 선택",
|
||||
Multiselect = true,
|
||||
Filter = "지원 파일|*.png;*.jpg;*.jpeg;*.gif;*.bmp;*.tif;*.tiff;*.webp;*.avif;*.heic;*.heif;*.psd;*.dng;*.nef;*.cr2;*.cr3;*.arw;*.raf;*.orf;*.rw2;*.srw;*.pef;*.pdf;*.docx;*.doc;*.html;*.htm;*.hwp;*.hwpx|모든 파일|*.*",
|
||||
};
|
||||
if (dlg.ShowDialog(this) == true)
|
||||
{
|
||||
OpenConvertWindow(dlg.FileNames);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenConvertWindow(string[] paths)
|
||||
{
|
||||
var files = ExpandPaths(paths);
|
||||
if (files.Count == 0) return;
|
||||
var window = new ConvertWindow(((App)Application.Current).Engine, files) { Owner = this };
|
||||
window.ShowDialog();
|
||||
}
|
||||
|
||||
private static List<string> ExpandPaths(IEnumerable<string> paths)
|
||||
{
|
||||
var list = new List<string>();
|
||||
foreach (var p in paths)
|
||||
{
|
||||
try
|
||||
if (File.Exists(p)) yield return p;
|
||||
else if (Directory.Exists(p))
|
||||
{
|
||||
if (File.Exists(p)) list.Add(p);
|
||||
else if (Directory.Exists(p))
|
||||
list.AddRange(Directory.EnumerateFiles(p, "*", SearchOption.TopDirectoryOnly));
|
||||
foreach (var f in Directory.EnumerateFiles(p, "*", SearchOption.TopDirectoryOnly))
|
||||
yield return f;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private async void OnRegisterClick(object sender, RoutedEventArgs e)
|
||||
private void AddToQueue(IEnumerable<string> paths)
|
||||
{
|
||||
var existing = new HashSet<string>(_activeQueue.Select(q => q.SourcePath), StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (!File.Exists(path) || existing.Contains(path)) continue;
|
||||
_activeQueue.Add(QueueItem.FromPath(path));
|
||||
}
|
||||
UpdateBadges();
|
||||
UpdateProcessQueueButton();
|
||||
UpdateActiveQueueVisibility();
|
||||
}
|
||||
|
||||
private void OnRemoveQueueItem(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement fe && fe.Tag is QueueItem item)
|
||||
{
|
||||
_activeQueue.Remove(item);
|
||||
UpdateBadges();
|
||||
UpdateProcessQueueButton();
|
||||
UpdateActiveQueueVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateActiveQueueVisibility()
|
||||
{
|
||||
var hasItems = _activeQueue.Count > 0;
|
||||
DropZoneEmpty.Visibility = hasItems ? Visibility.Collapsed : Visibility.Visible;
|
||||
ActiveQueueScroll.Visibility = hasItems ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private void UpdateProcessQueueButton()
|
||||
{
|
||||
var count = _activeQueue.Count;
|
||||
if (_cts is not null)
|
||||
{
|
||||
ProcessQueueButton.Content = $"Processing… ({count} files)";
|
||||
ProcessQueueButton.IsEnabled = false;
|
||||
}
|
||||
else if (count == 0)
|
||||
{
|
||||
ProcessQueueButton.Content = "Idle — drop files to begin";
|
||||
ProcessQueueButton.IsEnabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessQueueButton.Content = $"Process Queue ({count})";
|
||||
ProcessQueueButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Sidebar inputs ==============
|
||||
|
||||
private void OnQualityChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (QualityValueText is null) return;
|
||||
QualityValueText.Text = $"{(int)e.NewValue}%";
|
||||
}
|
||||
|
||||
private void OnConflictSegmentClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not ToggleButton clicked) return;
|
||||
ConflictSkipBtn.IsChecked = clicked == ConflictSkipBtn;
|
||||
ConflictRenameBtn.IsChecked = clicked == ConflictRenameBtn;
|
||||
ConflictReplaceBtn.IsChecked = clicked == ConflictReplaceBtn;
|
||||
_conflictRule = (clicked.Tag as string) switch
|
||||
{
|
||||
"Skip" => NameCollision.Skip,
|
||||
"Replace" => NameCollision.Overwrite,
|
||||
_ => NameCollision.AppendNumber,
|
||||
};
|
||||
}
|
||||
|
||||
private void OnPickOutputFolderClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFolderDialog { Title = "출력 폴더 선택" };
|
||||
if (dlg.ShowDialog(this) == true)
|
||||
OutputPathTextBox.Text = dlg.FolderName;
|
||||
}
|
||||
|
||||
private ConvertOptions BuildOptions()
|
||||
{
|
||||
var opts = new ConvertOptions
|
||||
{
|
||||
Quality = (int)QualitySlider.Value,
|
||||
OnCollision = _conflictRule,
|
||||
};
|
||||
|
||||
var custom = OutputPathTextBox.Text?.Trim();
|
||||
if (!string.IsNullOrEmpty(custom))
|
||||
{
|
||||
opts.OutputLocation = OutputLocation.Custom;
|
||||
opts.CustomOutputDirectory = custom;
|
||||
}
|
||||
else
|
||||
{
|
||||
opts.OutputLocation = OutputLocation.SubfolderBesideSource;
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
// ============== Process queue ==============
|
||||
|
||||
// ============== Preview ==============
|
||||
|
||||
private QueueItem? _selectedPreviewItem;
|
||||
private CancellationTokenSource? _previewCts;
|
||||
|
||||
private async void OnQueueRowClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is not FrameworkElement fe || fe.Tag is not QueueItem item) return;
|
||||
_selectedPreviewItem = item;
|
||||
ShowTab("Preview");
|
||||
await LoadPreviewAsync(item);
|
||||
}
|
||||
|
||||
private async Task LoadPreviewAsync(QueueItem item)
|
||||
{
|
||||
_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 = "—";
|
||||
|
||||
try
|
||||
{
|
||||
ContextMenuRegistrar.Register(((App)Application.Current).Engine);
|
||||
ShowToast("컨텍스트 메뉴를 등록했습니다.\n파일 위에서 우클릭 → \"추가 옵션 표시\"에서 보입니다.");
|
||||
await PopulateAsync();
|
||||
var result = await PreviewService.CreateAsync(item.SourcePath, 720, token);
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
PreviewLoading.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (result.Image is not null)
|
||||
{
|
||||
PreviewImage.Source = result.Image;
|
||||
PreviewImage.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
PreviewReasonText.Text = result.Reason ?? "미리보기를 생성하지 못했습니다.";
|
||||
PreviewReason.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
PreviewDimText.Text = result.Dimensions ?? "—";
|
||||
PreviewPageText.Text = result.PageCount?.ToString() ?? "—";
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
PreviewLoading.Visibility = Visibility.Collapsed;
|
||||
PreviewReasonText.Text = "미리보기 오류: " + ex.Message;
|
||||
PreviewReason.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPreviewOpenFolder(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_selectedPreviewItem is null) return;
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "explorer.exe",
|
||||
Arguments = $"/select,\"{_selectedPreviewItem.SourcePath}\"",
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// ============== Export Log ==============
|
||||
|
||||
private void OnExportLogClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_pastResults.Count == 0)
|
||||
{
|
||||
MessageBox.Show(this, "저장할 이력이 없습니다.", "EverythingToJpeg",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var dlg = new Microsoft.Win32.SaveFileDialog
|
||||
{
|
||||
Title = "Export Log",
|
||||
FileName = $"EverythingToJpeg-log-{DateTime.Now:yyyyMMdd-HHmmss}.csv",
|
||||
DefaultExt = ".csv",
|
||||
Filter = "CSV (*.csv)|*.csv|JSON (*.json)|*.json",
|
||||
};
|
||||
if (dlg.ShowDialog(this) != true) return;
|
||||
|
||||
try
|
||||
{
|
||||
if (dlg.FileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
ExportJson(dlg.FileName);
|
||||
else
|
||||
ExportCsv(dlg.FileName);
|
||||
|
||||
MessageBox.Show(this, "저장되었습니다:\n" + dlg.FileName, "EverythingToJpeg",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("등록 중 오류: " + ex.Message, "EverythingToJpeg",
|
||||
MessageBox.Show(this, "저장 중 오류: " + ex.Message, "EverythingToJpeg",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnUnregisterClick(object sender, RoutedEventArgs e)
|
||||
private void ExportCsv(string path)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("Date,Format,FileName,SourcePath,Size,Savings,Meta");
|
||||
foreach (var group in _pastResults)
|
||||
foreach (var row in group.Entries)
|
||||
sb.AppendLine(string.Join(",",
|
||||
EscapeCsv(group.DateTitle),
|
||||
EscapeCsv(row.FormatLabel),
|
||||
EscapeCsv(row.FileName),
|
||||
EscapeCsv(row.SourcePath),
|
||||
EscapeCsv(row.SizeText),
|
||||
EscapeCsv(row.SavingsText),
|
||||
EscapeCsv(row.MetaLine)));
|
||||
File.WriteAllText(path, sb.ToString(), System.Text.Encoding.UTF8);
|
||||
}
|
||||
|
||||
private void ExportJson(string path)
|
||||
{
|
||||
var data = _pastResults.Select(g => new
|
||||
{
|
||||
date = g.DateTitle,
|
||||
sessionSavings = HumanizeBytes(g.SessionSavingsBytes),
|
||||
entries = g.Entries.Select(r => new
|
||||
{
|
||||
format = r.FormatLabel,
|
||||
fileName = r.FileName,
|
||||
sourcePath = r.SourcePath,
|
||||
size = r.SizeText,
|
||||
savings = r.SavingsText,
|
||||
meta = r.MetaLine,
|
||||
}),
|
||||
});
|
||||
File.WriteAllText(path,
|
||||
System.Text.Json.JsonSerializer.Serialize(data,
|
||||
new System.Text.Json.JsonSerializerOptions { WriteIndented = true }),
|
||||
System.Text.Encoding.UTF8);
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string? s)
|
||||
{
|
||||
s ??= "";
|
||||
if (s.Contains('"') || s.Contains(',') || s.Contains('\n'))
|
||||
return "\"" + s.Replace("\"", "\"\"") + "\"";
|
||||
return s;
|
||||
}
|
||||
|
||||
private async void OnProcessQueueClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_activeQueue.Count == 0) return;
|
||||
|
||||
var snapshot = _activeQueue.ToList();
|
||||
foreach (var item in snapshot) item.SetPending();
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
UpdateProcessQueueButton();
|
||||
|
||||
var engine = ((App)Application.Current).Engine;
|
||||
var options = BuildOptions();
|
||||
|
||||
var reporter = new Progress<ConvertProgress>(p =>
|
||||
{
|
||||
for (var i = 0; i < snapshot.Count; i++)
|
||||
{
|
||||
if (i < p.Index) snapshot[i].SetState("done");
|
||||
else if (i == p.Index) snapshot[i].SetState($"{(int)(p.FileProgress * 100)}%");
|
||||
else snapshot[i].SetState("queued");
|
||||
}
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var sources = snapshot.Select(s => s.SourcePath).ToList();
|
||||
var results = await engine.ConvertManyAsync(sources, options, reporter, _cts.Token);
|
||||
|
||||
foreach (var (item, result) in snapshot.Zip(results))
|
||||
{
|
||||
long outputSize = 0;
|
||||
foreach (var p in result.OutputPaths)
|
||||
{
|
||||
try { outputSize += new FileInfo(p).Length; } catch { }
|
||||
}
|
||||
|
||||
AddToHistory(new HistoryEntry(
|
||||
Timestamp: DateTime.Now,
|
||||
SourcePath: item.SourcePath,
|
||||
SourceFormat: item.FormatLabel,
|
||||
SourceSizeBytes: item.SourceSizeBytes,
|
||||
OutputSizeBytes: outputSize,
|
||||
OutputCount: result.OutputPaths.Count,
|
||||
MetaLine: item.MetaLine,
|
||||
Status: result.Status,
|
||||
Message: result.Message));
|
||||
|
||||
_activeQueue.Remove(item);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, "변환 중 오류: " + ex.Message, "EverythingToJpeg",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cts = null;
|
||||
UpdateBadges();
|
||||
UpdateProcessQueueButton();
|
||||
UpdateActiveQueueVisibility();
|
||||
ApplyAppDataStats();
|
||||
if (_activeQueue.Count == 0) ShowTab("Past");
|
||||
}
|
||||
}
|
||||
|
||||
// ============== History ==============
|
||||
|
||||
private void AddToHistory(HistoryEntry entry)
|
||||
{
|
||||
var label = FormatDateLabel(entry.Date);
|
||||
var group = _pastResults.FirstOrDefault(g => g.DateTitle == label);
|
||||
if (group is null)
|
||||
{
|
||||
group = new DateGroup(label);
|
||||
_pastResults.Insert(0, group);
|
||||
}
|
||||
group.Add(HistoryRow.From(entry));
|
||||
}
|
||||
|
||||
private void SeedDemoHistory()
|
||||
{
|
||||
var today = FormatDateLabel(DateOnly.FromDateTime(DateTime.Today));
|
||||
var todayGroup = new DateGroup(today);
|
||||
todayGroup.Add(new HistoryRow(
|
||||
FormatLabel: "PNG", FormatBrush: (Brush)FindResource("FsFmtPng"),
|
||||
FileName: "hero_background_final_v2.png",
|
||||
MetaLine: "08:42:12 • 3200x1800",
|
||||
SizeText: "14.2 MB",
|
||||
SavingsText: "↓ 1.1 MB",
|
||||
SourcePath: "<demo>"));
|
||||
todayGroup.Add(new HistoryRow(
|
||||
FormatLabel: "HEIC", FormatBrush: (Brush)FindResource("FsFmtHeic"),
|
||||
FileName: "portrait_session_04.heic",
|
||||
MetaLine: "08:35:45 • 4032x3024",
|
||||
SizeText: "6.8 MB",
|
||||
SavingsText: "↓ 2.4 MB",
|
||||
SourcePath: "<demo>"));
|
||||
todayGroup.SessionSavingsBytes = (long)(842.4 * 1024 * 1024);
|
||||
_pastResults.Add(todayGroup);
|
||||
|
||||
var yesterday = FormatDateLabel(DateOnly.FromDateTime(DateTime.Today.AddDays(-1)));
|
||||
var yGroup = new DateGroup(yesterday);
|
||||
yGroup.Add(new HistoryRow(
|
||||
FormatLabel: "PDF", FormatBrush: (Brush)FindResource("FsFmtPdf"),
|
||||
FileName: "Q3_Full_Marketing_Deck_v12.pdf",
|
||||
MetaLine: "17:22:10 • 124 Pages",
|
||||
SizeText: "245.4 MB",
|
||||
SavingsText: "↓ 12.8 MB",
|
||||
SourcePath: "<demo>"));
|
||||
yGroup.Add(new HistoryRow(
|
||||
FormatLabel: "PNG", FormatBrush: (Brush)FindResource("FsFmtPng"),
|
||||
FileName: "asset_bundle_archive_raw.png",
|
||||
MetaLine: "16:45:33 • 8000x8000",
|
||||
SizeText: "82.1 MB",
|
||||
SavingsText: "↓ 4.5 MB",
|
||||
SourcePath: "<demo>"));
|
||||
yGroup.SessionSavingsBytes = (long)(3.1 * 1024 * 1024 * 1024);
|
||||
_pastResults.Add(yGroup);
|
||||
|
||||
UpdateBadges();
|
||||
}
|
||||
|
||||
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}";
|
||||
}
|
||||
|
||||
private void ApplyAppDataStats()
|
||||
{
|
||||
var todayLabel = FormatDateLabel(DateOnly.FromDateTime(DateTime.Today));
|
||||
var todayGroup = _pastResults.FirstOrDefault(g => g.DateTitle == todayLabel);
|
||||
|
||||
var processedToday = todayGroup?.Entries.Count ?? 0;
|
||||
var allSavings = _pastResults.Sum(g => g.SessionSavingsBytes);
|
||||
|
||||
ProcessedTodayText.Text = processedToday.ToString("N0", CultureInfo.InvariantCulture);
|
||||
SpaceSavedText.Text = HumanizeBytes(allSavings);
|
||||
}
|
||||
|
||||
// ============== Top-bar actions ==============
|
||||
|
||||
private void OnRegisterClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
ContextMenuRegistrar.Unregister(((App)Application.Current).Engine);
|
||||
ShowToast("컨텍스트 메뉴를 해제했습니다.");
|
||||
await PopulateAsync();
|
||||
ContextMenuRegistrar.Register(((App)Application.Current).Engine);
|
||||
MessageBox.Show(this,
|
||||
"컨텍스트 메뉴를 등록했습니다.\n파일 우클릭 → \"추가 옵션 표시\" 또는 \"JPEG로 빠른 변환/변환…\".",
|
||||
"EverythingToJpeg", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("해제 중 오류: " + ex.Message, "EverythingToJpeg",
|
||||
MessageBox.Show(this, "등록 중 오류: " + ex.Message, "EverythingToJpeg",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
|
@ -261,13 +537,176 @@ public partial class MainWindow : FluentWindow, INotifyPropertyChanged
|
|||
window.ShowDialog();
|
||||
}
|
||||
|
||||
private void ShowToast(string message)
|
||||
private void OnClearAllClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MessageBox.Show(this, message, "EverythingToJpeg",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
if (TabActiveBtn.IsChecked == true)
|
||||
{
|
||||
_activeQueue.Clear();
|
||||
}
|
||||
else if (TabPastBtn.IsChecked == true)
|
||||
{
|
||||
_pastResults.Clear();
|
||||
}
|
||||
UpdateBadges();
|
||||
UpdateProcessQueueButton();
|
||||
UpdateActiveQueueVisibility();
|
||||
ApplyAppDataStats();
|
||||
}
|
||||
|
||||
private void OnOpenFolderClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement fe && fe.Tag is string path && File.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "explorer.exe",
|
||||
Arguments = $"/select,\"{path}\"",
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public static string HumanizeBytes(long bytes)
|
||||
{
|
||||
if (bytes <= 0) return "0 B";
|
||||
string[] units = { "B", "KB", "MB", "GB", "TB" };
|
||||
double size = bytes;
|
||||
var unit = 0;
|
||||
while (size >= 1024 && unit < units.Length - 1) { size /= 1024; unit++; }
|
||||
return $"{size:0.#} {units[unit]}";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// View models
|
||||
// ============================================================
|
||||
|
||||
public sealed class QueueItem : INotifyPropertyChanged
|
||||
{
|
||||
private string _state = "queued";
|
||||
|
||||
public required string SourcePath { get; init; }
|
||||
public required string FileName { get; init; }
|
||||
public required string FormatLabel { get; init; }
|
||||
public required Brush FormatBrush { get; init; }
|
||||
public required string SizeText { get; init; }
|
||||
public required string MetaLine { get; init; }
|
||||
public required long SourceSizeBytes { get; init; }
|
||||
|
||||
public string StateText
|
||||
{
|
||||
get => _state;
|
||||
set { _state = value; Raise(nameof(StateText)); }
|
||||
}
|
||||
|
||||
public Brush StateBrush => _state switch
|
||||
{
|
||||
"queued" => (Brush)Application.Current.FindResource("FsTextTertiary"),
|
||||
"done" => (Brush)Application.Current.FindResource("FsAccentGreen"),
|
||||
_ => (Brush)Application.Current.FindResource("FsAccentBlue"),
|
||||
};
|
||||
|
||||
public void SetPending() => StateText = "queued";
|
||||
public void SetState(string s)
|
||||
{
|
||||
StateText = s;
|
||||
Raise(nameof(StateBrush));
|
||||
}
|
||||
|
||||
public static QueueItem FromPath(string path)
|
||||
{
|
||||
var ext = Path.GetExtension(path).TrimStart('.').ToLowerInvariant();
|
||||
var (label, brushKey) = FormatPalette.For(ext);
|
||||
long size = 0;
|
||||
try { size = new FileInfo(path).Length; } catch { }
|
||||
|
||||
return new QueueItem
|
||||
{
|
||||
SourcePath = path,
|
||||
FileName = Path.GetFileName(path),
|
||||
FormatLabel = label,
|
||||
FormatBrush = (Brush)Application.Current.FindResource(brushKey),
|
||||
SizeText = MainWindow.HumanizeBytes(size),
|
||||
MetaLine = $"{ext.ToUpperInvariant()} • {MainWindow.HumanizeBytes(size)}",
|
||||
SourceSizeBytes = size,
|
||||
};
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
private void Raise([CallerMemberName] string? n = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
|
||||
}
|
||||
|
||||
public sealed class DateGroup : INotifyPropertyChanged
|
||||
{
|
||||
public string DateTitle { get; }
|
||||
public ObservableCollection<HistoryRow> Entries { get; } = new();
|
||||
public long SessionSavingsBytes { get; set; }
|
||||
public string SessionSavingsText => $"Session Savings: {MainWindow.HumanizeBytes(SessionSavingsBytes)}";
|
||||
|
||||
public DateGroup(string dateTitle) { DateTitle = dateTitle; }
|
||||
|
||||
public void Add(HistoryRow row)
|
||||
{
|
||||
Entries.Insert(0, row);
|
||||
SessionSavingsBytes += row.SavingsBytes;
|
||||
Raise(nameof(SessionSavingsText));
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
private void Raise([CallerMemberName] string? n = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
|
||||
}
|
||||
|
||||
public sealed record HistoryRow(
|
||||
string FormatLabel,
|
||||
Brush FormatBrush,
|
||||
string FileName,
|
||||
string MetaLine,
|
||||
string SizeText,
|
||||
string SavingsText,
|
||||
string SourcePath,
|
||||
long SavingsBytes = 0)
|
||||
{
|
||||
public static HistoryRow From(HistoryEntry e)
|
||||
{
|
||||
var ext = Path.GetExtension(e.SourcePath).TrimStart('.').ToLowerInvariant();
|
||||
var (label, brushKey) = FormatPalette.For(ext);
|
||||
var saved = e.SavingsBytes;
|
||||
var arrow = saved >= 0 ? "↓" : "↑";
|
||||
return new HistoryRow(
|
||||
FormatLabel: label,
|
||||
FormatBrush: (Brush)Application.Current.FindResource(brushKey),
|
||||
FileName: Path.GetFileName(e.SourcePath),
|
||||
MetaLine: $"{e.Timestamp:HH:mm:ss} • {e.OutputCount} output(s)",
|
||||
SizeText: MainWindow.HumanizeBytes(e.SourceSizeBytes),
|
||||
SavingsText: $"{arrow} {MainWindow.HumanizeBytes(Math.Abs(saved))}",
|
||||
SourcePath: e.SourcePath,
|
||||
SavingsBytes: saved);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FormatPalette
|
||||
{
|
||||
public static (string Label, string BrushKey) For(string ext) => ext switch
|
||||
{
|
||||
"pdf" => ("PDF", "FsFmtPdf"),
|
||||
"png" => ("PNG", "FsFmtPng"),
|
||||
"heic" or "heif" => ("HEIC", "FsFmtHeic"),
|
||||
"jpg" or "jpeg" or "jpe" => ("JPG", "FsFmtJpg"),
|
||||
"doc" or "docx" => ("DOCX", "FsFmtDocx"),
|
||||
"html" or "htm" => ("HTML", "FsFmtHtml"),
|
||||
"hwp" or "hwpx" => ("HWP", "FsFmtHwp"),
|
||||
"gif" => ("GIF", "FsFmtGif"),
|
||||
"tif" or "tiff" => ("TIFF", "FsFmtTiff"),
|
||||
"webp" => ("WEBP", "FsFmtWebp"),
|
||||
"bmp" => ("BMP", "FsFmtBmp"),
|
||||
"raw" or "dng" or "nef" or "cr2" or "cr3" or "arw" or "raf" or "orf" or "rw2" or "srw" or "pef"
|
||||
=> ("RAW", "FsFmtRaw"),
|
||||
_ => (ext.ToUpperInvariant(), "FsFmtOther"),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
37
src/EverythingToJpeg.Core/HistoryStore.cs
Normal file
37
src/EverythingToJpeg.Core/HistoryStore.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace EverythingToJpeg.Core;
|
||||
|
||||
public sealed record HistoryEntry(
|
||||
DateTime Timestamp,
|
||||
string SourcePath,
|
||||
string SourceFormat,
|
||||
long SourceSizeBytes,
|
||||
long OutputSizeBytes,
|
||||
int OutputCount,
|
||||
string? MetaLine,
|
||||
ConvertStatus Status,
|
||||
string? Message)
|
||||
{
|
||||
public long SavingsBytes => SourceSizeBytes - OutputSizeBytes;
|
||||
|
||||
public DateOnly Date => DateOnly.FromDateTime(Timestamp);
|
||||
}
|
||||
|
||||
public sealed class HistoryStore
|
||||
{
|
||||
private readonly ObservableCollection<HistoryEntry> _entries = new();
|
||||
|
||||
public ReadOnlyObservableCollection<HistoryEntry> Entries { get; }
|
||||
|
||||
public HistoryStore()
|
||||
{
|
||||
Entries = new ReadOnlyObservableCollection<HistoryEntry>(_entries);
|
||||
}
|
||||
|
||||
public void Add(HistoryEntry entry) => _entries.Insert(0, entry);
|
||||
|
||||
public void Clear() => _entries.Clear();
|
||||
|
||||
public int Count => _entries.Count;
|
||||
}
|
||||
128
src/EverythingToJpeg.Core/PreviewService.cs
Normal file
128
src/EverythingToJpeg.Core/PreviewService.cs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
using System.Windows.Media.Imaging;
|
||||
using ImageMagick;
|
||||
using PDFtoImage;
|
||||
using PhotoSauce.MagicScaler;
|
||||
using PhotoSauce.NativeCodecs.Libheif;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace EverythingToJpeg.Core;
|
||||
|
||||
public sealed record PreviewResult(BitmapSource? Image, string? Reason, string? Dimensions, int? PageCount);
|
||||
|
||||
public static class PreviewService
|
||||
{
|
||||
private static int _heifConfigured;
|
||||
|
||||
public static async Task<PreviewResult> CreateAsync(string path, int maxLongEdge = 720, CancellationToken ct = default)
|
||||
{
|
||||
if (!File.Exists(path)) return new PreviewResult(null, "파일을 찾을 수 없습니다.", null, null);
|
||||
|
||||
var ext = Path.GetExtension(path).ToLowerInvariant();
|
||||
try
|
||||
{
|
||||
return ext switch
|
||||
{
|
||||
".pdf" => await Task.Run(() => RenderPdf(path, maxLongEdge), ct).ConfigureAwait(false),
|
||||
".heic" or ".heif" => await Task.Run(() => RenderHeic(path, maxLongEdge), ct).ConfigureAwait(false),
|
||||
".html" or ".htm" => new PreviewResult(null, "HTML 미리보기는 변환 시점에 렌더됩니다.", null, null),
|
||||
".doc" or ".docx" or ".hwp" or ".hwpx" =>
|
||||
new PreviewResult(null, "문서 미리보기는 다음 업데이트에서 지원합니다.", null, null),
|
||||
_ => await Task.Run(() => RenderViaMagick(path, maxLongEdge), ct).ConfigureAwait(false),
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PreviewResult(null, "미리보기 생성 실패: " + ex.Message, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static PreviewResult RenderViaMagick(string path, int maxLongEdge)
|
||||
{
|
||||
using var image = new MagickImage(path);
|
||||
var w = (int)image.Width;
|
||||
var h = (int)image.Height;
|
||||
try { image.AutoOrient(); } catch { }
|
||||
if (image.HasAlpha)
|
||||
{
|
||||
image.BackgroundColor = MagickColors.White;
|
||||
image.Alpha(AlphaOption.Remove);
|
||||
image.Alpha(AlphaOption.Off);
|
||||
}
|
||||
if (w > maxLongEdge || h > maxLongEdge)
|
||||
{
|
||||
var geom = new MagickGeometry((uint)maxLongEdge, (uint)maxLongEdge) { IgnoreAspectRatio = false };
|
||||
image.Resize(geom);
|
||||
}
|
||||
image.Quality = 88;
|
||||
image.Format = MagickFormat.Jpeg;
|
||||
var bytes = image.ToByteArray();
|
||||
return new PreviewResult(BytesToBitmap(bytes), null, $"{w} × {h}", null);
|
||||
}
|
||||
|
||||
private static PreviewResult RenderHeic(string path, int maxLongEdge)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _heifConfigured, 1) == 0)
|
||||
CodecManager.Configure(c => c.UseLibheif());
|
||||
|
||||
var tempPng = Path.Combine(Path.GetTempPath(), $"e2j_pv_{Guid.NewGuid():N}.png");
|
||||
try
|
||||
{
|
||||
MagicImageProcessor.ProcessImage(path, tempPng, ProcessImageSettings.Default);
|
||||
return RenderViaMagick(tempPng, maxLongEdge);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (File.Exists(tempPng)) File.Delete(tempPng); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static PreviewResult RenderPdf(string path, int maxLongEdge)
|
||||
{
|
||||
int pageCount;
|
||||
using (var probe = File.OpenRead(path))
|
||||
{
|
||||
pageCount = Conversion.GetPageCount(probe);
|
||||
}
|
||||
if (pageCount <= 0) return new PreviewResult(null, "PDF 페이지가 없습니다.", null, 0);
|
||||
|
||||
var ms = new MemoryStream();
|
||||
using (var input = File.OpenRead(path))
|
||||
{
|
||||
var renderOptions = new RenderOptions
|
||||
{
|
||||
Dpi = 144,
|
||||
BackgroundColor = SKColors.White,
|
||||
WithAnnotations = true,
|
||||
WithFormFill = true,
|
||||
};
|
||||
Conversion.SaveJpeg(ms, input, page: 0, leaveOpen: false, password: null, options: renderOptions);
|
||||
}
|
||||
ms.Position = 0;
|
||||
var bytes = ms.ToArray();
|
||||
|
||||
// optional resize via Magick
|
||||
using var image = new MagickImage(bytes);
|
||||
var w = (int)image.Width;
|
||||
var h = (int)image.Height;
|
||||
if (w > maxLongEdge || h > maxLongEdge)
|
||||
{
|
||||
var geom = new MagickGeometry((uint)maxLongEdge, (uint)maxLongEdge) { IgnoreAspectRatio = false };
|
||||
image.Resize(geom);
|
||||
image.Quality = 88;
|
||||
image.Format = MagickFormat.Jpeg;
|
||||
bytes = image.ToByteArray();
|
||||
}
|
||||
return new PreviewResult(BytesToBitmap(bytes), null, $"{w} × {h}", pageCount);
|
||||
}
|
||||
|
||||
private static BitmapSource BytesToBitmap(byte[] bytes)
|
||||
{
|
||||
var bmp = new BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bmp.StreamSource = new MemoryStream(bytes);
|
||||
bmp.EndInit();
|
||||
bmp.Freeze();
|
||||
return bmp;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue