feat(ui): default to active queue, scope search toolbar, add double-bezel inspector, optimize queue columns, and achieve 100% pure Korean UI
This commit is contained in:
parent
455c8b1324
commit
4ca7352dc3
27 changed files with 2477 additions and 659 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5" xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10" IgnorableNamespaces="uap rescap desktop desktop4 desktop5 com">
|
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5" xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10" IgnorableNamespaces="uap rescap desktop desktop4 desktop5 com">
|
||||||
<Identity Name="Everything2Everything.YunChan" Publisher="CN=Everything2EverythingDev" Version="1.0.16.0" ProcessorArchitecture="x64" />
|
<Identity Name="Everything2Everything.YunChan" Publisher="CN=Everything2EverythingDev" Version="1.0.18.0" ProcessorArchitecture="x64" />
|
||||||
<Properties>
|
<Properties>
|
||||||
<DisplayName>Everything2Everything</DisplayName>
|
<DisplayName>Everything2Everything</DisplayName>
|
||||||
<PublisherDisplayName>YunChan</PublisherDisplayName>
|
<PublisherDisplayName>YunChan</PublisherDisplayName>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>1.0.11</Version>
|
<Version>1.0.18</Version>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
|
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,13 @@ public partial class OptionsViewModel : ObservableObject
|
||||||
[ObservableProperty] private int _channelsIndex; // 0=원본,1=모노,2=스테레오
|
[ObservableProperty] private int _channelsIndex; // 0=원본,1=모노,2=스테레오
|
||||||
[ObservableProperty] private bool _loudnorm;
|
[ObservableProperty] private bool _loudnorm;
|
||||||
|
|
||||||
|
// ── 상세 인코딩 옵션 (폴드아웃 Expander 상태 및 전문 규격) ────────────────────────────────
|
||||||
|
[ObservableProperty] private bool _isAdvancedExpanded = true; // 사용자 피드백: 슬라이더/옵션을 바로 볼 수 있도록 기본 전개
|
||||||
|
[ObservableProperty] private int _pdfCompressLevelIndex; // 0=Light, 1=Strong, 2=Max
|
||||||
|
[ObservableProperty] private int _pdfDpiIndex = 1; // 0=150, 1=200, 2=300
|
||||||
|
[ObservableProperty] private bool _imageLossless; // WebP/PNG 무손실
|
||||||
|
[ObservableProperty] private bool _progressive; // JPEG 프로그레시브 웹 로딩
|
||||||
|
|
||||||
/// <summary>현재 상태로 불변 ConvertOptions를 구성한다(기존 MainWindow.BuildOptions와 동일 동작 + 영상/오디오).</summary>
|
/// <summary>현재 상태로 불변 ConvertOptions를 구성한다(기존 MainWindow.BuildOptions와 동일 동작 + 영상/오디오).</summary>
|
||||||
public ConvertOptions ToConvertOptions()
|
public ConvertOptions ToConvertOptions()
|
||||||
{
|
{
|
||||||
|
|
@ -77,15 +84,23 @@ public partial class OptionsViewModel : ObservableObject
|
||||||
_ => "summarize",
|
_ => "summarize",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var dpi = PdfDpiIndex switch
|
||||||
|
{
|
||||||
|
0 => 150,
|
||||||
|
2 => 300,
|
||||||
|
_ => 200,
|
||||||
|
};
|
||||||
|
|
||||||
return new ConvertOptions
|
return new ConvertOptions
|
||||||
{
|
{
|
||||||
OnCollision = ConflictRule,
|
OnCollision = ConflictRule,
|
||||||
OutputLocation = hasCustom ? OutputLocation.Custom : OutputLocation.SubfolderBesideSource,
|
OutputLocation = hasCustom ? OutputLocation.Custom : OutputLocation.SubfolderBesideSource,
|
||||||
CustomOutputDirectory = hasCustom ? CustomOutputDirectory!.Trim() : null,
|
CustomOutputDirectory = hasCustom ? CustomOutputDirectory!.Trim() : null,
|
||||||
KeepExifWhenPossible = !StripMetadata,
|
KeepExifWhenPossible = !StripMetadata,
|
||||||
Jpeg = new JpegEncodingOptions { Quality = Quality },
|
Jpeg = new JpegEncodingOptions { Quality = Quality, Progressive = Progressive },
|
||||||
Webp = new WebpEncodingOptions { Quality = Quality },
|
Webp = new WebpEncodingOptions { Quality = Quality, Lossless = ImageLossless },
|
||||||
Avif = new AvifEncodingOptions { Quality = Math.Clamp(Quality - 30, 1, 100) },
|
Avif = new AvifEncodingOptions { Quality = Math.Clamp(Quality - 30, 1, 100) },
|
||||||
|
PdfRender = new PdfRenderOptions { Dpi = dpi },
|
||||||
Ai = new AiOptions
|
Ai = new AiOptions
|
||||||
{
|
{
|
||||||
Task = aiTask,
|
Task = aiTask,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ public static class CategoryGlyphs
|
||||||
public static ImageSource ForCategory(string category)
|
public static ImageSource ForCategory(string category)
|
||||||
{
|
{
|
||||||
if (Cache.TryGetValue(category, out var cached)) return cached;
|
if (Cache.TryGetValue(category, out var cached)) return cached;
|
||||||
var uri = new Uri($"pack://application:,,,/Assets/glyph-{category}.png", UriKind.Absolute);
|
var uri = new Uri($"pack://application:,,,/Everything2Everything;component/Assets/glyph-{category}.png", UriKind.Absolute);
|
||||||
var img = new BitmapImage();
|
var img = new BitmapImage();
|
||||||
img.BeginInit();
|
img.BeginInit();
|
||||||
img.CacheOption = BitmapCacheOption.OnLoad;
|
img.CacheOption = BitmapCacheOption.OnLoad;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ui:ControlsDictionary/>
|
||||||
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
|
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
||||||
|
|
@ -521,6 +521,100 @@
|
||||||
<Setter Property="Margin" Value="0,0,0,16"/>
|
<Setter Property="Margin" Value="0,0,0,16"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Double-Bezel (Doppelrand) Machine-Hardware Container Styles (high-end-visual-design Section 4) -->
|
||||||
|
<Style x:Key="FsDoubleBezelShellStyle" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="#10131B"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource FsBorderHairline}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="14"/>
|
||||||
|
<Setter Property="Padding" Value="4"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,16"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="FsDoubleBezelCoreStyle" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{StaticResource FsBgSurface}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource FsGlassBorderBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="10"/>
|
||||||
|
<Setter Property="Padding" Value="14"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Physical <kbd> Keycap Shortcut Style (minimalist-ui Section 5) -->
|
||||||
|
<Style x:Key="FsKbdStyle" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{StaticResource FsBgInput}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource FsBorderStrong}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1,1,2,1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="4"/>
|
||||||
|
<Setter Property="Padding" Value="6,2"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,4,0"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Island Button-in-Button Primary Launch CTA Style (high-end-visual-design Section 4.B) -->
|
||||||
|
<LinearGradientBrush x:Key="FsIslandButtonGlow" StartPoint="0,0" EndPoint="1,1">
|
||||||
|
<GradientStop Color="#0284C7" Offset="0.0"/>
|
||||||
|
<GradientStop Color="#0369A1" Offset="1.0"/>
|
||||||
|
</LinearGradientBrush>
|
||||||
|
<LinearGradientBrush x:Key="FsIslandButtonGlowHover" StartPoint="0,0" EndPoint="1,1">
|
||||||
|
<GradientStop Color="#0EA5E9" Offset="0.0"/>
|
||||||
|
<GradientStop Color="#0284C7" Offset="1.0"/>
|
||||||
|
</LinearGradientBrush>
|
||||||
|
|
||||||
|
<Style x:Key="FsIslandPrimaryButtonStyle" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{StaticResource FsIslandButtonGlow}"/>
|
||||||
|
<Setter Property="Foreground" Value="#FFFFFF"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#4038BDF8"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Height" Value="44"/>
|
||||||
|
<Setter Property="Padding" Value="16,0,10,0"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<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="10"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<Grid VerticalAlignment="Center">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ContentPresenter Grid.Column="0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
TextElement.Foreground="{TemplateBinding Foreground}"
|
||||||
|
TextElement.FontSize="{TemplateBinding FontSize}"
|
||||||
|
TextElement.FontWeight="{TemplateBinding FontWeight}"/>
|
||||||
|
<Border Grid.Column="1" Margin="8,0,0,0"
|
||||||
|
Width="26" Height="26" CornerRadius="13"
|
||||||
|
Background="#22FFFFFF"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Right">
|
||||||
|
<ui:SymbolIcon Symbol="ArrowRight24" FontSize="13"
|
||||||
|
Foreground="#FFFFFF"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="PART_Bd" Property="Opacity" Value="0.45"/>
|
||||||
|
<Setter Property="Cursor" Value="Arrow"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsIslandButtonGlowHover}"/>
|
||||||
|
<Setter TargetName="PART_Bd" Property="BorderBrush" Value="#8038BDF8"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- 실시간 기술 스펙 칩 (Live Spec Chip) 캡슐 스타일 -->
|
<!-- 실시간 기술 스펙 칩 (Live Spec Chip) 캡슐 스타일 -->
|
||||||
<Style x:Key="FsSpecChipStyle" TargetType="Border">
|
<Style x:Key="FsSpecChipStyle" TargetType="Border">
|
||||||
<Setter Property="Background" Value="{StaticResource FsAccentCyanBg}"/>
|
<Setter Property="Background" Value="{StaticResource FsAccentCyanBg}"/>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
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"
|
||||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
||||||
Title="FormatShift Utility"
|
Title="Everything2Everything"
|
||||||
Icon="pack://application:,,,/Everything2Everything;component/Assets/app-icon.png"
|
Icon="pack://application:,,,/Everything2Everything;component/Assets/app-icon.png"
|
||||||
Width="1280" Height="960"
|
Width="1280" Height="960"
|
||||||
MinWidth="1080" MinHeight="640"
|
MinWidth="1080" MinHeight="640"
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- ============== UNIFIED TOP BAR (Row 0: 42px) ============== -->
|
<!-- ============== UNIFIED TOP BAR (Row 0: 42px) ============== -->
|
||||||
<ui:TitleBar x:Name="AppTitleBar" Title="" Height="42" Grid.Row="0"
|
<ui:TitleBar x:Name="AppTitleBar" Title="Everything2Everything" Height="42" Grid.Row="0"
|
||||||
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
|
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
|
||||||
ShowMaximize="True" ShowMinimize="True">
|
ShowMaximize="True" ShowMinimize="True">
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@
|
||||||
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="Active">
|
CommandParameter="Active">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<TextBlock Text="Active Queue" VerticalAlignment="Center"/>
|
<TextBlock Text="대기열" VerticalAlignment="Center"/>
|
||||||
<Border Margin="6,0,0,0" Style="{StaticResource FsTabBadgeStyle}" VerticalAlignment="Center">
|
<Border Margin="6,0,0,0" Style="{StaticResource FsTabBadgeStyle}" VerticalAlignment="Center">
|
||||||
<TextBlock x:Name="TabActiveBadge" Text="0"
|
<TextBlock x:Name="TabActiveBadge" Text="0"
|
||||||
FontSize="10" FontWeight="SemiBold"
|
FontSize="10" FontWeight="SemiBold"
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="Past">
|
CommandParameter="Past">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<TextBlock Text="Past Results" VerticalAlignment="Center"/>
|
<TextBlock Text="변환 기록" VerticalAlignment="Center"/>
|
||||||
<Border Margin="6,0,0,0" Style="{StaticResource FsTabBadgeStyle}" VerticalAlignment="Center">
|
<Border Margin="6,0,0,0" Style="{StaticResource FsTabBadgeStyle}" VerticalAlignment="Center">
|
||||||
<TextBlock x:Name="TabPastBadge" Text="0"
|
<TextBlock x:Name="TabPastBadge" Text="0"
|
||||||
FontSize="10" FontWeight="SemiBold"
|
FontSize="10" FontWeight="SemiBold"
|
||||||
|
|
@ -174,7 +174,8 @@
|
||||||
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto" Padding="16,16,16,16">
|
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto" Padding="16,16,16,16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<!-- Smart Conversion Deck: Output Format + Dynamic Presets + Live Spec Chips + Quality -->
|
<!-- Smart Conversion Deck: Output Format + Dynamic Presets + Live Spec Chips + Quality -->
|
||||||
<Border Style="{StaticResource FsCardStyle}">
|
<Border Style="{StaticResource FsDoubleBezelShellStyle}" Margin="0,0,0,16">
|
||||||
|
<Border Style="{StaticResource FsDoubleBezelCoreStyle}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<Grid Margin="0,0,0,12">
|
<Grid Margin="0,0,0,12">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
|
@ -205,6 +206,7 @@
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
</Border>
|
</Border>
|
||||||
<ComboBox x:Name="OutputFormatCombo" Grid.Column="1"
|
<ComboBox x:Name="OutputFormatCombo" Grid.Column="1"
|
||||||
|
AutomationProperties.AutomationId="OutputFormatCombo"
|
||||||
Margin="8,0,0,0" VerticalAlignment="Center">
|
Margin="8,0,0,0" VerticalAlignment="Center">
|
||||||
<i:Interaction.Triggers>
|
<i:Interaction.Triggers>
|
||||||
<i:EventTrigger EventName="SelectionChanged">
|
<i:EventTrigger EventName="SelectionChanged">
|
||||||
|
|
@ -229,20 +231,11 @@
|
||||||
<!-- 3. Live Spec Chips Container -->
|
<!-- 3. Live Spec Chips Container -->
|
||||||
<WrapPanel x:Name="SmartPresetChipsPanel" Margin="0,0,0,10"/>
|
<WrapPanel x:Name="SmartPresetChipsPanel" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
<Border Height="1" Background="{StaticResource FsBorderSubtle}" Margin="0,0,0,12"/>
|
<Border Height="1" Background="{StaticResource FsBorderSubtle}" Margin="0,4,0,12"/>
|
||||||
|
|
||||||
<!-- 4. Quality & Combine Settings -->
|
<!-- 4. 포맷별 핵심 인코딩 컨트롤 (프리셋 직하단 즉시 노출) -->
|
||||||
<CheckBox x:Name="CombineToSingleCheck"
|
<!-- A. 이미지 품질 슬라이더 (JPG/WebP/AVIF) -->
|
||||||
Margin="0,0,0,4"
|
<StackPanel x:Name="QualityPanel" Margin="0,0,0,6">
|
||||||
Content="단일 파일로 결합 (큐 전체 → 한 파일)"
|
|
||||||
IsEnabled="False"
|
|
||||||
Command="{Binding CombineToggleCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
|
||||||
<TextBlock x:Name="CombineHint" Margin="0,0,0,10"
|
|
||||||
Style="{StaticResource FsCaptionStyle}"
|
|
||||||
Text="PDF/TIFF/GIF 출력에 한해 큐의 이미지들을 한 파일로 결합합니다"/>
|
|
||||||
|
|
||||||
<!-- Encoding Quality -->
|
|
||||||
<StackPanel x:Name="QualityPanel" Margin="0,4,0,0">
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
|
|
@ -259,7 +252,8 @@
|
||||||
Text="{Binding Options.Quality, RelativeSource={RelativeSource AncestorType=Window}, StringFormat={}{0}%}"/>
|
Text="{Binding Options.Quality, RelativeSource={RelativeSource AncestorType=Window}, StringFormat={}{0}%}"/>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Slider x:Name="QualitySlider" Margin="0,10,0,0"
|
<Slider x:Name="QualitySlider" Margin="0,8,0,0"
|
||||||
|
AutomationProperties.AutomationId="QualitySlider"
|
||||||
Style="{StaticResource FsSliderStyle}"
|
Style="{StaticResource FsSliderStyle}"
|
||||||
Minimum="1" Maximum="100"
|
Minimum="1" Maximum="100"
|
||||||
Value="{Binding Options.Quality, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
Value="{Binding Options.Quality, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
||||||
|
|
@ -272,11 +266,206 @@
|
||||||
<TextBlock Grid.Column="1" Text="품질 우선" Style="{StaticResource FsCaptionStyle}"/>
|
<TextBlock Grid.Column="1" Text="품질 우선" Style="{StaticResource FsCaptionStyle}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- B. 영상 주 품질 CRF 슬라이더 (MP4/MKV/WebM/MOV) -->
|
||||||
|
<StackPanel x:Name="VideoQuickPanel" Margin="0,0,0,6" Visibility="Collapsed">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="영상 화질 (CRF)" Style="{StaticResource FsLabelStyle}"/>
|
||||||
|
<Border Grid.Column="1" CornerRadius="4"
|
||||||
|
Background="{StaticResource FsBgInput}"
|
||||||
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
|
BorderThickness="1" Padding="6,2">
|
||||||
|
<TextBlock FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
||||||
|
Foreground="{StaticResource FsAccentBlue}"
|
||||||
|
Text="{Binding Options.Crf, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
<Slider x:Name="VideoCrfSlider" Margin="0,8,0,0" Style="{StaticResource FsSliderStyle}"
|
||||||
|
AutomationProperties.AutomationId="VideoCrfSlider"
|
||||||
|
Minimum="0" Maximum="51"
|
||||||
|
Value="{Binding Options.Crf, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
||||||
|
<Grid Margin="0,4,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="고화질 (0)" Style="{StaticResource FsCaptionStyle}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="고압축 (51)" Style="{StaticResource FsCaptionStyle}"/>
|
||||||
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- C. 오디오 주 비트레이트 선택기 (MP3/AAC/Opus/FLAC) -->
|
||||||
|
<StackPanel x:Name="AudioQuickPanel" Margin="0,0,0,6" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="오디오 비트레이트" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,4"/>
|
||||||
|
<ComboBox x:Name="AudioBitrateQuickCombo"
|
||||||
|
AutomationProperties.AutomationId="AudioBitrateQuickCombo"
|
||||||
|
SelectedIndex="{Binding Options.AudioBitrateIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
||||||
|
<ComboBoxItem Content="96 kbps (음성·저용량)"/>
|
||||||
|
<ComboBoxItem Content="128 kbps (표준)"/>
|
||||||
|
<ComboBoxItem Content="192 kbps (고음질 권장)"/>
|
||||||
|
<ComboBoxItem Content="256 kbps (매우 높음)"/>
|
||||||
|
<ComboBoxItem Content="320 kbps (최고 음질)"/>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- D. PDF 압축 레벨 선택기 (PDF) -->
|
||||||
|
<StackPanel x:Name="PdfQuickPanel" Margin="0,0,0,6" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="PDF 최적화 레벨" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,4"/>
|
||||||
|
<ComboBox x:Name="PdfCompressQuickCombo"
|
||||||
|
AutomationProperties.AutomationId="PdfCompressQuickCombo"
|
||||||
|
SelectedIndex="{Binding Options.PdfCompressLevelIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
||||||
|
<ComboBoxItem Content="Light (무손실 최적화 · 화질 유지 100%)"/>
|
||||||
|
<ComboBoxItem Content="Strong (웹/이메일 균형 · 150 DPI 리샘플링)"/>
|
||||||
|
<ComboBoxItem Content="Max (최대 압축 · 흑백/고압축)"/>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 5. 상세 인코딩 설정 (폴드아웃 Expander: 프리셋 직하단 위치) -->
|
||||||
|
<Expander x:Name="AdvancedOptionsExpander"
|
||||||
|
Margin="0,6,0,6"
|
||||||
|
AutomationProperties.AutomationId="AdvancedOptionsExpander"
|
||||||
|
IsExpanded="True">
|
||||||
|
<Expander.Header>
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<ui:SymbolIcon Symbol="Options24" FontSize="14" Foreground="{StaticResource FsAccentCyan}" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<TextBlock Text="상세 인코딩 설정" Style="{StaticResource FsLabelStyle}" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Expander.Header>
|
||||||
|
<StackPanel Margin="0,12,0,0">
|
||||||
|
|
||||||
|
<!-- A. PDF 상세 옵션 패널 (ISO 32000 / Ghostscript 규격) -->
|
||||||
|
<StackPanel x:Name="AdvancedPdfPanel" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="PDF 최적화 및 렌더링" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,8"/>
|
||||||
|
<TextBlock Text="PDF 압축 레벨" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
||||||
|
<ComboBox x:Name="PdfCompressLevelCombo"
|
||||||
|
AutomationProperties.AutomationId="PdfCompressLevelCombo"
|
||||||
|
SelectedIndex="{Binding Options.PdfCompressLevelIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
||||||
|
<ComboBoxItem Content="Light (무손실 최적화 · 화질 유지 100%)"/>
|
||||||
|
<ComboBoxItem Content="Strong (웹/이메일 균형 · 150 DPI 리샘플링)"/>
|
||||||
|
<ComboBoxItem Content="Max (최대 압축 · 흑백/고압축)"/>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Text="PDF 렌더링 해상도 (DPI)" Style="{StaticResource FsCaptionStyle}" Margin="0,10,0,4"/>
|
||||||
|
<ComboBox x:Name="PdfDpiCombo"
|
||||||
|
AutomationProperties.AutomationId="PdfDpiCombo"
|
||||||
|
SelectedIndex="{Binding Options.PdfDpiIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
||||||
|
<ComboBoxItem Content="150 DPI (화면 / 웹 최적화)"/>
|
||||||
|
<ComboBoxItem Content="200 DPI (표준 오피스 밸런스)"/>
|
||||||
|
<ComboBoxItem Content="300 DPI (고해상도 인쇄 및 OCR용)"/>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- B. 이미지 상세 옵션 패널 (W3C / MozJPEG 규격) -->
|
||||||
|
<StackPanel x:Name="AdvancedImagePanel" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="이미지 고급 인코딩" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,8"/>
|
||||||
|
<CheckBox x:Name="ImageLosslessCheck" Margin="0,0,0,6" Content="무손실(Lossless) 보존 (WebP/PNG)"
|
||||||
|
AutomationProperties.AutomationId="ImageLosslessCheck"
|
||||||
|
IsChecked="{Binding Options.ImageLossless, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
||||||
|
<CheckBox x:Name="ImageProgressiveCheck" Margin="0,0,0,6" Content="프로그레시브/인터레이스 웹 로딩"
|
||||||
|
AutomationProperties.AutomationId="ImageProgressiveCheck"
|
||||||
|
IsChecked="{Binding Options.Progressive, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
||||||
|
<CheckBox x:Name="ImageStripMetadataCheck" Margin="0,0,0,0" Content="개인정보(EXIF/GPS) 제거"
|
||||||
|
AutomationProperties.AutomationId="ImageStripMetadataCheck"
|
||||||
|
IsChecked="{Binding Options.StripMetadata, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- C. 영상 상세 옵션 패널 (FFmpeg 규격) -->
|
||||||
|
<StackPanel x:Name="AdvancedVideoPanel" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="영상 인코딩 (FFmpeg)" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,8"/>
|
||||||
|
<TextBlock Text="비디오 코덱" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
||||||
|
<ComboBox SelectedIndex="{Binding Options.VideoCodecIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
||||||
|
<ComboBoxItem Content="자동 (H.264 / 고해상도 H.265)"/>
|
||||||
|
<ComboBoxItem Content="H.264 (최대 호환)"/>
|
||||||
|
<ComboBoxItem Content="H.265 / HEVC (고압축)"/>
|
||||||
|
<ComboBoxItem Content="VP9"/>
|
||||||
|
<ComboBoxItem Content="AV1 (최고 압축)"/>
|
||||||
|
<ComboBoxItem Content="원본 복사 (재인코딩 없음)"/>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Text="레이트 컨트롤" Style="{StaticResource FsCaptionStyle}" Margin="0,10,0,4"/>
|
||||||
|
<ComboBox x:Name="RateControlCombo" SelectedIndex="{Binding Options.RateControlIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
||||||
|
<ComboBoxItem Content="CRF (품질 기준 · 권장)"/>
|
||||||
|
<ComboBoxItem Content="평균 비트레이트 (ABR)"/>
|
||||||
|
<ComboBoxItem Content="제약 CRF (품질+상한)"/>
|
||||||
|
<ComboBoxItem Content="고정 비트레이트 (CBR)"/>
|
||||||
|
<ComboBoxItem Content="2패스 (최고 효율)"/>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<!-- 비트레이트 입력 (ABR/제약CRF/CBR/2패스 모드) -->
|
||||||
|
<StackPanel Margin="0,10,0,0">
|
||||||
|
<StackPanel.Style>
|
||||||
|
<Style TargetType="StackPanel">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding SelectedIndex, ElementName=RateControlCombo}" Value="1"><Setter Property="Visibility" Value="Visible"/></DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding SelectedIndex, ElementName=RateControlCombo}" Value="2"><Setter Property="Visibility" Value="Visible"/></DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding SelectedIndex, ElementName=RateControlCombo}" Value="3"><Setter Property="Visibility" Value="Visible"/></DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding SelectedIndex, ElementName=RateControlCombo}" Value="4"><Setter Property="Visibility" Value="Visible"/></DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</StackPanel.Style>
|
||||||
|
<TextBlock Text="비트레이트 (kbps)" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
||||||
|
<TextBox Style="{StaticResource FsPathInputStyle}"
|
||||||
|
Text="{Binding Options.VideoBitrateKbps, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="인코딩 속도 / 압축 효율" Style="{StaticResource FsCaptionStyle}" Margin="0,10,0,4"/>
|
||||||
|
<Slider Minimum="0" Maximum="8" Margin="0,4,0,0" Style="{StaticResource FsSliderStyle}"
|
||||||
|
Value="{Binding Options.PresetIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
||||||
|
<Grid Margin="0,4,0,0">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="초고속 (용량↑)" Style="{StaticResource FsCaptionStyle}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="최고효율 (느림)" Style="{StaticResource FsCaptionStyle}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<CheckBox Margin="0,12,0,0" Content="웹 스트리밍 최적화 (Faststart)"
|
||||||
|
IsChecked="{Binding Options.FastStart, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
||||||
|
<CheckBox Margin="0,6,0,0" Content="GPU 하드웨어 가속 (NVENC 등)"
|
||||||
|
IsChecked="{Binding Options.VideoPreferGpu, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- D. 오디오 상세 옵션 패널 (ITU-R / EBU R128 규격) -->
|
||||||
|
<StackPanel x:Name="AdvancedAudioPanel" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="오디오 인코딩" Style="{StaticResource FsLabelStyle}" Margin="0,8,0,8"/>
|
||||||
|
<TextBlock Text="오디오 코덱" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
||||||
|
<ComboBox SelectedIndex="{Binding Options.AudioCodecIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
||||||
|
<ComboBoxItem Content="자동"/><ComboBoxItem Content="AAC"/><ComboBoxItem Content="MP3"/>
|
||||||
|
<ComboBoxItem Content="Opus"/><ComboBoxItem Content="Vorbis"/><ComboBoxItem Content="FLAC (무손실)"/>
|
||||||
|
<ComboBoxItem Content="PCM (WAV)"/><ComboBoxItem Content="원본 복사"/>
|
||||||
|
</ComboBox>
|
||||||
|
<CheckBox Margin="0,8,0,0" Content="가변 비트레이트 (VBR)"
|
||||||
|
IsChecked="{Binding Options.AudioVbr, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
||||||
|
<CheckBox Margin="0,10,0,0" Content="음량 정규화 (EBU R128)"
|
||||||
|
IsChecked="{Binding Options.Loudnorm, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</Expander>
|
||||||
|
|
||||||
|
<Border Height="1" Background="{StaticResource FsBorderSubtle}" Margin="0,6,0,12"/>
|
||||||
|
|
||||||
|
<!-- 6. 단일 파일로 결합 체크박스 -->
|
||||||
|
<CheckBox x:Name="CombineToSingleCheck"
|
||||||
|
Margin="0,0,0,4"
|
||||||
|
IsEnabled="False"
|
||||||
|
Command="{Binding CombineToggleCommand, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||||
|
<TextBlock Text="단일 파일로 결합 (전체 병합)" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||||
|
</CheckBox>
|
||||||
|
<TextBlock x:Name="CombineHint" Margin="0,0,0,6"
|
||||||
|
Style="{StaticResource FsCaptionStyle}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="PDF/TIFF/GIF 출력 시 큐의 파일들을 하나로 결합합니다"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Card 3: Output Destination & Conflict Rule -->
|
<!-- Card 3: Output Destination & Conflict Rule -->
|
||||||
<Border Style="{StaticResource FsCardStyle}">
|
<Border Style="{StaticResource FsDoubleBezelShellStyle}" Margin="0,0,0,16">
|
||||||
|
<Border Style="{StaticResource FsDoubleBezelCoreStyle}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="저장 위치 및 충돌 규칙" Style="{StaticResource FsLabelStyle}"/>
|
<TextBlock Text="저장 위치 및 충돌 규칙" Style="{StaticResource FsLabelStyle}"/>
|
||||||
<Grid Margin="0,10,0,0">
|
<Grid Margin="0,10,0,0">
|
||||||
|
|
@ -298,22 +487,22 @@
|
||||||
|
|
||||||
<Border Height="1" Background="{StaticResource FsBorderSubtle}" Margin="0,14,0,14"/>
|
<Border Height="1" Background="{StaticResource FsBorderSubtle}" Margin="0,14,0,14"/>
|
||||||
|
|
||||||
<TextBlock Text="FILE CONFLICT RULE" Style="{StaticResource FsLabelStyle}"/>
|
<TextBlock Text="파일 충돌 해결" Style="{StaticResource FsLabelStyle}"/>
|
||||||
<Border Margin="0,8,0,0"
|
<Border Margin="0,8,0,0"
|
||||||
Background="{StaticResource FsBgInput}"
|
Background="{StaticResource FsBgInput}"
|
||||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
BorderThickness="1" CornerRadius="6" Padding="2">
|
BorderThickness="1" CornerRadius="6" Padding="2">
|
||||||
<UniformGrid Rows="1" Columns="3">
|
<UniformGrid Rows="1" Columns="3">
|
||||||
<ToggleButton x:Name="ConflictSkipBtn" Content="Skip"
|
<ToggleButton x:Name="ConflictSkipBtn" Content="건너뛰기"
|
||||||
Style="{StaticResource FsSegmentStyle}"
|
Style="{StaticResource FsSegmentStyle}"
|
||||||
Command="{Binding ConflictRuleCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding ConflictRuleCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="Skip" ToolTip="건너뛰기"/>
|
CommandParameter="Skip" ToolTip="건너뛰기"/>
|
||||||
<ToggleButton x:Name="ConflictRenameBtn" Content="Rename"
|
<ToggleButton x:Name="ConflictRenameBtn" Content="이름 변경"
|
||||||
Style="{StaticResource FsSegmentStyle}"
|
Style="{StaticResource FsSegmentStyle}"
|
||||||
IsChecked="True"
|
IsChecked="True"
|
||||||
Command="{Binding ConflictRuleCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding ConflictRuleCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="Rename" ToolTip="이름 변경"/>
|
CommandParameter="Rename" ToolTip="이름 변경"/>
|
||||||
<ToggleButton x:Name="ConflictReplaceBtn" Content="Replace"
|
<ToggleButton x:Name="ConflictReplaceBtn" Content="덮어쓰기"
|
||||||
Style="{StaticResource FsSegmentStyle}"
|
Style="{StaticResource FsSegmentStyle}"
|
||||||
Command="{Binding ConflictRuleCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding ConflictRuleCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="Replace" ToolTip="덮어쓰기"/>
|
CommandParameter="Replace" ToolTip="덮어쓰기"/>
|
||||||
|
|
@ -321,9 +510,11 @@
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Card 4: AI Task -->
|
<!-- Card 4: AI Task -->
|
||||||
<Border Style="{StaticResource FsCardStyle}" Margin="0,0,0,16">
|
<Border Style="{StaticResource FsDoubleBezelShellStyle}" Margin="0,0,0,16">
|
||||||
|
<Border Style="{StaticResource FsDoubleBezelCoreStyle}">
|
||||||
<StackPanel x:Name="AiTaskPanel">
|
<StackPanel x:Name="AiTaskPanel">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<Image Width="15" Height="15" VerticalAlignment="Center" Margin="0,0,6,0"
|
<Image Width="15" Height="15" VerticalAlignment="Center" Margin="0,0,6,0"
|
||||||
|
|
@ -361,197 +552,11 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Card 5: 상세 인코딩 설정 (폴드아웃 Expander) -->
|
|
||||||
<Border Style="{StaticResource FsCardStyle}" Margin="0,0,0,16">
|
|
||||||
<Expander x:Name="AdvancedOptionsExpander"
|
|
||||||
IsExpanded="{Binding Options.IsAdvancedExpanded, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<Expander.Header>
|
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
|
||||||
<ui:SymbolIcon Symbol="Options24" FontSize="14" Foreground="{StaticResource FsAccentCyan}" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
|
||||||
<TextBlock Text="상세 인코딩 설정" Style="{StaticResource FsLabelStyle}" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Expander.Header>
|
|
||||||
<StackPanel Margin="0,12,0,0">
|
|
||||||
|
|
||||||
<!-- A. PDF 상세 옵션 패널 (ISO 32000 / Ghostscript 규격) -->
|
|
||||||
<StackPanel x:Name="AdvancedPdfPanel" Visibility="Collapsed">
|
|
||||||
<TextBlock Text="PDF 최적화 및 렌더링" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,8"/>
|
|
||||||
<TextBlock Text="PDF 압축 레벨" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
|
||||||
<ComboBox x:Name="PdfCompressLevelCombo" SelectedIndex="{Binding Options.PdfCompressLevelIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="Light (무손실 최적화 · 화질 유지 100%)"/>
|
|
||||||
<ComboBoxItem Content="Strong (웹/이메일 균형 · 150 DPI 리샘플링)"/>
|
|
||||||
<ComboBoxItem Content="Max (최대 압축 · 흑백/고압축)"/>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<TextBlock Text="PDF 렌더링 해상도 (DPI)" Style="{StaticResource FsCaptionStyle}" Margin="0,10,0,4"/>
|
|
||||||
<ComboBox x:Name="PdfDpiCombo" SelectedIndex="{Binding Options.PdfDpiIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="150 DPI (화면 / 웹 최적화)"/>
|
|
||||||
<ComboBoxItem Content="200 DPI (표준 오피스 밸런스)"/>
|
|
||||||
<ComboBoxItem Content="300 DPI (고해상도 인쇄 및 OCR용)"/>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- B. 이미지 상세 옵션 패널 (W3C / MozJPEG 규격) -->
|
|
||||||
<StackPanel x:Name="AdvancedImagePanel" Visibility="Collapsed">
|
|
||||||
<TextBlock Text="이미지 고급 인코딩" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,8"/>
|
|
||||||
<CheckBox x:Name="ImageLosslessCheck" Margin="0,0,0,6" Content="무손실(Lossless) 보존 (WebP/PNG)"
|
|
||||||
IsChecked="{Binding Options.ImageLossless, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
|
||||||
<CheckBox x:Name="ImageProgressiveCheck" Margin="0,0,0,6" Content="프로그레시브/인터레이스 웹 로딩"
|
|
||||||
IsChecked="{Binding Options.Progressive, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
|
||||||
<CheckBox x:Name="ImageStripMetadataCheck" Margin="0,0,0,0" Content="개인정보(EXIF/GPS) 제거"
|
|
||||||
IsChecked="{Binding Options.StripMetadata, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- C. 영상 상세 옵션 패널 (FFmpeg 규격) -->
|
|
||||||
<StackPanel x:Name="AdvancedVideoPanel" Visibility="Collapsed">
|
|
||||||
<TextBlock Text="영상 인코딩 (FFmpeg)" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,8"/>
|
|
||||||
<TextBlock Text="비디오 코덱" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
|
||||||
<ComboBox SelectedIndex="{Binding Options.VideoCodecIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="자동 (H.264 / 고해상도 H.265)"/>
|
|
||||||
<ComboBoxItem Content="H.264 (최대 호환)"/>
|
|
||||||
<ComboBoxItem Content="H.265 / HEVC (고압축)"/>
|
|
||||||
<ComboBoxItem Content="VP9"/>
|
|
||||||
<ComboBoxItem Content="AV1 (최고 압축)"/>
|
|
||||||
<ComboBoxItem Content="원본 복사 (재인코딩 없음)"/>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<TextBlock Text="레이트 컨트롤" Style="{StaticResource FsCaptionStyle}" Margin="0,10,0,4"/>
|
|
||||||
<ComboBox x:Name="RateControlCombo" SelectedIndex="{Binding Options.RateControlIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="CRF (품질 기준 · 권장)"/>
|
|
||||||
<ComboBoxItem Content="평균 비트레이트 (ABR)"/>
|
|
||||||
<ComboBoxItem Content="제약 CRF (품질+상한)"/>
|
|
||||||
<ComboBoxItem Content="고정 비트레이트 (CBR)"/>
|
|
||||||
<ComboBoxItem Content="2패스 (최고 효율)"/>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<!-- CRF 슬라이더 (CRF / 제약CRF 모드) -->
|
|
||||||
<StackPanel Margin="0,10,0,0">
|
|
||||||
<StackPanel.Style>
|
|
||||||
<Style TargetType="StackPanel">
|
|
||||||
<Setter Property="Visibility" Value="Visible"/>
|
|
||||||
<Style.Triggers>
|
|
||||||
<DataTrigger Binding="{Binding SelectedIndex, ElementName=RateControlCombo}" Value="1"><Setter Property="Visibility" Value="Collapsed"/></DataTrigger>
|
|
||||||
<DataTrigger Binding="{Binding SelectedIndex, ElementName=RateControlCombo}" Value="3"><Setter Property="Visibility" Value="Collapsed"/></DataTrigger>
|
|
||||||
<DataTrigger Binding="{Binding SelectedIndex, ElementName=RateControlCombo}" Value="4"><Setter Property="Visibility" Value="Collapsed"/></DataTrigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
</StackPanel.Style>
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Text="품질 (CRF) — 낮을수록 고화질" Style="{StaticResource FsCaptionStyle}"/>
|
|
||||||
<TextBlock Grid.Column="1" FontFamily="{StaticResource FsFontMono}" FontSize="12" Foreground="{StaticResource FsAccentBlue}"
|
|
||||||
Text="{Binding Options.Crf, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
|
||||||
</Grid>
|
|
||||||
<Slider Minimum="0" Maximum="51" Margin="0,4,0,0"
|
|
||||||
Value="{Binding Options.Crf, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- 비트레이트 입력 (ABR/제약CRF/CBR/2패스 모드) -->
|
|
||||||
<StackPanel Margin="0,10,0,0">
|
|
||||||
<StackPanel.Style>
|
|
||||||
<Style TargetType="StackPanel">
|
|
||||||
<Setter Property="Visibility" Value="Collapsed"/>
|
|
||||||
<Style.Triggers>
|
|
||||||
<DataTrigger Binding="{Binding SelectedIndex, ElementName=RateControlCombo}" Value="1"><Setter Property="Visibility" Value="Visible"/></DataTrigger>
|
|
||||||
<DataTrigger Binding="{Binding SelectedIndex, ElementName=RateControlCombo}" Value="2"><Setter Property="Visibility" Value="Visible"/></DataTrigger>
|
|
||||||
<DataTrigger Binding="{Binding SelectedIndex, ElementName=RateControlCombo}" Value="3"><Setter Property="Visibility" Value="Visible"/></DataTrigger>
|
|
||||||
<DataTrigger Binding="{Binding SelectedIndex, ElementName=RateControlCombo}" Value="4"><Setter Property="Visibility" Value="Visible"/></DataTrigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
</StackPanel.Style>
|
|
||||||
<TextBlock Text="비디오 비트레이트 (kbps)" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
|
||||||
<TextBox Style="{StaticResource FsPathInputStyle}"
|
|
||||||
Text="{Binding Options.VideoBitrateKbps, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="인코딩 프리셋 (느릴수록 고압축)" Style="{StaticResource FsCaptionStyle}" Margin="0,10,0,4"/>
|
|
||||||
<ComboBox SelectedIndex="{Binding Options.PresetIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="UltraFast"/><ComboBoxItem Content="SuperFast"/><ComboBoxItem Content="VeryFast"/>
|
|
||||||
<ComboBoxItem Content="Faster"/><ComboBoxItem Content="Fast"/><ComboBoxItem Content="Medium"/>
|
|
||||||
<ComboBoxItem Content="Slow"/><ComboBoxItem Content="Slower"/><ComboBoxItem Content="VerySlow"/>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<Grid Margin="0,10,0,0">
|
|
||||||
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="10"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
|
||||||
<StackPanel Grid.Column="0">
|
|
||||||
<TextBlock Text="해상도" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
|
||||||
<ComboBox SelectedIndex="{Binding Options.ResolutionIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="원본"/><ComboBoxItem Content="2160p (4K)"/><ComboBoxItem Content="1440p"/>
|
|
||||||
<ComboBoxItem Content="1080p"/><ComboBoxItem Content="720p"/><ComboBoxItem Content="480p"/>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Column="2">
|
|
||||||
<TextBlock Text="프레임레이트" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
|
||||||
<ComboBox SelectedIndex="{Binding Options.FpsIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="원본"/><ComboBoxItem Content="24"/><ComboBoxItem Content="30"/><ComboBoxItem Content="60"/>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<CheckBox Margin="0,12,0,0" Content="웹 스트리밍 최적화 (Faststart)"
|
|
||||||
IsChecked="{Binding Options.FastStart, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
|
||||||
<CheckBox Margin="0,6,0,0" Content="GPU 하드웨어 가속 (NVENC 등)"
|
|
||||||
IsChecked="{Binding Options.VideoPreferGpu, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
|
||||||
|
|
||||||
<Expander Header="고급" Margin="0,10,0,0" Foreground="{StaticResource FsTextSecondary}">
|
|
||||||
<StackPanel Margin="0,8,0,0">
|
|
||||||
<TextBlock Text="회전 / 뒤집기" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
|
||||||
<ComboBox SelectedIndex="{Binding Options.RotateIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="없음"/><ComboBoxItem Content="시계 90°"/><ComboBoxItem Content="반시계 90°"/>
|
|
||||||
<ComboBoxItem Content="180°"/><ComboBoxItem Content="좌우 반전"/><ComboBoxItem Content="상하 반전"/>
|
|
||||||
</ComboBox>
|
|
||||||
<TextBlock Text="디인터레이스" Style="{StaticResource FsCaptionStyle}" Margin="0,10,0,4"/>
|
|
||||||
<ComboBox SelectedIndex="{Binding Options.DeinterlaceIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="끔"/><ComboBoxItem Content="Yadif"/><ComboBoxItem Content="Bwdif (고품질)"/>
|
|
||||||
</ComboBox>
|
|
||||||
<CheckBox Margin="0,10,0,0" Content="적응형 양자화 (GPU 화질↑)"
|
|
||||||
IsChecked="{Binding Options.SpatialAq, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Expander>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- D. 오디오 상세 옵션 패널 (ITU-R / EBU R128 규격) -->
|
|
||||||
<StackPanel x:Name="AdvancedAudioPanel" Visibility="Collapsed">
|
|
||||||
<TextBlock Text="오디오 인코딩" Style="{StaticResource FsLabelStyle}" Margin="0,8,0,8"/>
|
|
||||||
<TextBlock Text="오디오 코덱" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
|
||||||
<ComboBox SelectedIndex="{Binding Options.AudioCodecIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="자동"/><ComboBoxItem Content="AAC"/><ComboBoxItem Content="MP3"/>
|
|
||||||
<ComboBoxItem Content="Opus"/><ComboBoxItem Content="Vorbis"/><ComboBoxItem Content="FLAC (무손실)"/>
|
|
||||||
<ComboBoxItem Content="PCM (WAV)"/><ComboBoxItem Content="원본 복사"/>
|
|
||||||
</ComboBox>
|
|
||||||
<TextBlock Text="오디오 비트레이트" Style="{StaticResource FsCaptionStyle}" Margin="0,10,0,4"/>
|
|
||||||
<ComboBox SelectedIndex="{Binding Options.AudioBitrateIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="96 kbps"/><ComboBoxItem Content="128 kbps"/><ComboBoxItem Content="192 kbps"/>
|
|
||||||
<ComboBoxItem Content="256 kbps"/><ComboBoxItem Content="320 kbps"/>
|
|
||||||
</ComboBox>
|
|
||||||
<CheckBox Margin="0,8,0,0" Content="가변 비트레이트 (VBR)"
|
|
||||||
IsChecked="{Binding Options.AudioVbr, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
|
||||||
<Grid Margin="0,10,0,0">
|
|
||||||
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="10"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
|
||||||
<StackPanel Grid.Column="0">
|
|
||||||
<TextBlock Text="샘플레이트" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
|
||||||
<ComboBox SelectedIndex="{Binding Options.SampleRateIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="원본"/><ComboBoxItem Content="44.1 kHz"/><ComboBoxItem Content="48 kHz"/>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Column="2">
|
|
||||||
<TextBlock Text="채널" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
|
||||||
<ComboBox SelectedIndex="{Binding Options.ChannelsIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}">
|
|
||||||
<ComboBoxItem Content="원본"/><ComboBoxItem Content="모노"/><ComboBoxItem Content="스테레오"/>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
<CheckBox Margin="0,10,0,0" Content="음량 정규화 (EBU R128)"
|
|
||||||
IsChecked="{Binding Options.Loudnorm, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</Expander>
|
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Card 6: Stats Box -->
|
<!-- Card 6: Stats Box -->
|
||||||
<Border Style="{StaticResource FsCardStyle}" Margin="0,0,0,16" Padding="16">
|
<Border Style="{StaticResource FsDoubleBezelShellStyle}" Margin="0,0,0,16">
|
||||||
|
<Border Style="{StaticResource FsDoubleBezelCoreStyle}" Padding="16">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
|
|
@ -581,6 +586,7 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
|
|
@ -631,10 +637,11 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button x:Name="ProcessQueueButton"
|
<Button x:Name="ProcessQueueButton"
|
||||||
Content="대기 중 — 파일을 드래그하여 추가하세요"
|
AutomationProperties.AutomationId="ProcessQueueButton"
|
||||||
Style="{StaticResource FsPrimaryButtonStyle}"
|
Content="파일을 드래그하여 추가"
|
||||||
|
Style="{StaticResource FsIslandPrimaryButtonStyle}"
|
||||||
IsEnabled="False"
|
IsEnabled="False"
|
||||||
Height="40"
|
Height="44"
|
||||||
Command="{Binding ProcessQueueCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
Command="{Binding ProcessQueueCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
@ -652,69 +659,69 @@
|
||||||
<ColumnDefinition x:Name="InspectorColumn" Width="380"/>
|
<ColumnDefinition x:Name="InspectorColumn" Width="380"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- 실시간 검색 & 카테고리 필터 툴바 -->
|
<!-- 실시간 검색 & 카테고리 필터 툴바 (리스트 컬럼에 정합) -->
|
||||||
<Border Grid.Row="0" Grid.ColumnSpan="2"
|
<Border Grid.Row="0" Grid.Column="0"
|
||||||
Background="{StaticResource FsBgPanel}"
|
Background="{StaticResource FsBgPanel}"
|
||||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
BorderThickness="0,0,0,1" Padding="24,8">
|
BorderThickness="0,0,1,1" Padding="16,8">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="280"/>
|
<ColumnDefinition Width="160"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<!-- Search Box -->
|
<!-- Search Box -->
|
||||||
<Grid Grid.Column="0">
|
<Grid Grid.Column="0">
|
||||||
<TextBox x:Name="SearchBox" Style="{StaticResource FsPathInputStyle}"
|
<TextBox x:Name="SearchBox" Style="{StaticResource FsPathInputStyle}"
|
||||||
Padding="32,6,10,6" Text="{Binding SearchText, RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=PropertyChanged}"
|
Padding="30,5,8,5" Text="{Binding SearchText, RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=PropertyChanged}"
|
||||||
ToolTip="파일명 또는 확장자로 검색"/>
|
ToolTip="파일명 또는 확장자로 검색"/>
|
||||||
<ui:SymbolIcon Symbol="Search24" FontSize="14" Foreground="{StaticResource FsTextTertiary}"
|
<ui:SymbolIcon Symbol="Search24" FontSize="13" Foreground="{StaticResource FsTextTertiary}"
|
||||||
HorizontalAlignment="Left" VerticalAlignment="Center" Margin="10,0,0,0" IsHitTestVisible="False"/>
|
HorizontalAlignment="Left" VerticalAlignment="Center" Margin="9,0,0,0" IsHitTestVisible="False"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<!-- Category Filter Chips & Inspector Toggle -->
|
<!-- Category Filter Chips & Inspector Toggle -->
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||||
<Button Margin="0,0,4,0" Padding="10,4" Style="{StaticResource FsSecondaryButtonStyle}"
|
<Button Margin="0,0,3,0" Padding="7,3" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||||
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="All">
|
CommandParameter="All">
|
||||||
<TextBlock Text="전체" FontSize="11" VerticalAlignment="Center"/>
|
<TextBlock Text="전체" FontSize="11" VerticalAlignment="Center"/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Margin="0,0,4,0" Padding="10,4" Style="{StaticResource FsSecondaryButtonStyle}"
|
<Button Margin="0,0,3,0" Padding="7,3" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||||
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="Image">
|
CommandParameter="Image">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<ui:SymbolIcon Symbol="Image24" FontSize="12" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
<ui:SymbolIcon Symbol="Image24" FontSize="11" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
<TextBlock Text="이미지" FontSize="11" VerticalAlignment="Center"/>
|
<TextBlock Text="이미지" FontSize="11" VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Margin="0,0,4,0" Padding="10,4" Style="{StaticResource FsSecondaryButtonStyle}"
|
<Button Margin="0,0,3,0" Padding="7,3" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||||
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="Document">
|
CommandParameter="Document">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<ui:SymbolIcon Symbol="Document24" FontSize="12" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
<ui:SymbolIcon Symbol="Document24" FontSize="11" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
<TextBlock Text="문서" FontSize="11" VerticalAlignment="Center"/>
|
<TextBlock Text="문서" FontSize="11" VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Margin="0,0,4,0" Padding="10,4" Style="{StaticResource FsSecondaryButtonStyle}"
|
<Button Margin="0,0,3,0" Padding="7,3" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||||
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="Media">
|
CommandParameter="Media">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<ui:SymbolIcon Symbol="Video24" FontSize="12" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
<ui:SymbolIcon Symbol="Video24" FontSize="11" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
<TextBlock Text="미디어" FontSize="11" VerticalAlignment="Center"/>
|
<TextBlock Text="미디어" FontSize="11" VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Margin="0,0,4,0" Padding="10,4" Style="{StaticResource FsSecondaryButtonStyle}"
|
<Button Margin="0,0,3,0" Padding="7,3" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||||
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="Data">
|
CommandParameter="Data">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<ui:SymbolIcon Symbol="Database24" FontSize="12" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
<ui:SymbolIcon Symbol="Database24" FontSize="11" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
<TextBlock Text="데이터" FontSize="11" VerticalAlignment="Center"/>
|
<TextBlock Text="데이터" FontSize="11" VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
<Border Width="1" Height="16" Background="{StaticResource FsBorderSubtle}" Margin="8,0"/>
|
<Border Width="1" Height="14" Background="{StaticResource FsBorderSubtle}" Margin="5,0"/>
|
||||||
<Button Padding="10,4" Style="{StaticResource FsSecondaryButtonStyle}"
|
<Button Padding="7,3" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||||
Command="{Binding ToggleInspectorCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding ToggleInspectorCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
ToolTip="미리보기 패널 접기/펼치기">
|
ToolTip="미리보기 패널 접기/펼치기">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<ui:SymbolIcon Symbol="PreviewLink24" FontSize="13" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
<ui:SymbolIcon Symbol="PreviewLink24" FontSize="11" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
<TextBlock Text="미리보기" FontSize="11" VerticalAlignment="Center"/>
|
<TextBlock Text="미리보기" FontSize="11" VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -767,43 +774,48 @@
|
||||||
|
|
||||||
<Grid Grid.Row="1">
|
<Grid Grid.Row="1">
|
||||||
<Grid x:Name="DropZoneEmpty">
|
<Grid x:Name="DropZoneEmpty">
|
||||||
<Border Background="{StaticResource FsBgBase}" Padding="32">
|
<Border Background="{StaticResource FsBgBase}" Padding="24">
|
||||||
<!-- 점선 드롭존: 절제된 dashed 보더 + 중앙 정렬 안내 -->
|
<!-- Double-Bezel Machine Drop Stage (high-end-visual-design & minimalist-ui) -->
|
||||||
<Grid MaxWidth="460"
|
<Border Style="{StaticResource FsDoubleBezelShellStyle}"
|
||||||
|
MaxWidth="540"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<!-- 점선 테두리(드롭존 느낌) -->
|
<Border Style="{StaticResource FsDoubleBezelCoreStyle}" Padding="36,32">
|
||||||
<Rectangle RadiusX="12" RadiusY="12"
|
<Grid>
|
||||||
|
<!-- Subtle dashed inner frame for tactile drop affordance -->
|
||||||
|
<Rectangle RadiusX="10" RadiusY="10"
|
||||||
Stroke="{StaticResource FsBorderHairline}"
|
Stroke="{StaticResource FsBorderHairline}"
|
||||||
StrokeThickness="1.5"
|
StrokeThickness="1.5"
|
||||||
StrokeDashArray="5 4"
|
StrokeDashArray="5 4"
|
||||||
Fill="{StaticResource FsBgSurface}"/>
|
Fill="Transparent"
|
||||||
|
IsHitTestVisible="False"/>
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
Margin="48,44">
|
Margin="32,28">
|
||||||
<Image Width="120" Height="120"
|
<Image Width="110" Height="110"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
RenderOptions.BitmapScalingMode="HighQuality"
|
RenderOptions.BitmapScalingMode="HighQuality"
|
||||||
Source="pack://application:,,,/Everything2Everything;component/Assets/illus-empty.png"/>
|
Source="pack://application:,,,/Everything2Everything;component/Assets/illus-empty.png"/>
|
||||||
<TextBlock Margin="0,16,0,0" FontSize="16" FontWeight="SemiBold"
|
<TextBlock Margin="0,16,0,0" FontSize="17" FontWeight="SemiBold"
|
||||||
Foreground="{StaticResource FsTextPrimary}"
|
Foreground="{StaticResource FsTextPrimary}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Text="변환할 파일을 드래그하여 추가하세요"/>
|
Text="변환할 파일을 드래그하여 추가하세요"/>
|
||||||
<TextBlock Margin="0,6,0,0" Style="{StaticResource FsCaptionStyle}"
|
<TextBlock Margin="0,6,0,0" Style="{StaticResource FsCaptionStyle}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
TextAlignment="Center"
|
TextAlignment="Center"
|
||||||
|
FontSize="12"
|
||||||
Text="이미지 · 문서 · 데이터 · 영상 · 오디오 등 무엇이든"/>
|
Text="이미지 · 문서 · 데이터 · 영상 · 오디오 등 무엇이든"/>
|
||||||
<Button Margin="0,20,0,0"
|
<Button Margin="0,20,0,0"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Style="{StaticResource FsPrimaryButtonStyle}"
|
Style="{StaticResource FsPrimaryButtonStyle}"
|
||||||
Padding="20,10"
|
Padding="22,10"
|
||||||
Command="{Binding AddFilesCommand, RelativeSource={RelativeSource AncestorType=Window}}">
|
Command="{Binding AddFilesCommand, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<ui:SymbolIcon Symbol="Add24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
<ui:SymbolIcon Symbol="Add24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||||
<TextBlock Text="파일 찾기…" VerticalAlignment="Center"/>
|
<TextBlock Text="파일 찾기…" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
<!-- 단축키 안내 칩 -->
|
<!-- 단축키 안내 칩 -->
|
||||||
<Border Margin="0,16,0,0" HorizontalAlignment="Center"
|
<Border Margin="0,18,0,0" HorizontalAlignment="Center"
|
||||||
Background="{StaticResource FsBgPanel}"
|
Background="{StaticResource FsBgPanel}"
|
||||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||||
BorderThickness="1" CornerRadius="12" Padding="12,4">
|
BorderThickness="1" CornerRadius="12" Padding="12,4">
|
||||||
|
|
@ -822,6 +834,8 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
</Border>
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ScrollViewer x:Name="ActiveQueueScroll" VerticalScrollBarVisibility="Auto"
|
<ScrollViewer x:Name="ActiveQueueScroll" VerticalScrollBarVisibility="Auto"
|
||||||
Padding="24" Visibility="Collapsed">
|
Padding="24" Visibility="Collapsed">
|
||||||
|
|
@ -849,12 +863,12 @@
|
||||||
CornerRadius="10,0,0,10"/>
|
CornerRadius="10,0,0,10"/>
|
||||||
<Grid Grid.Column="1" Margin="14,10">
|
<Grid Grid.Column="1" Margin="14,10">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="28"/>
|
<ColumnDefinition Width="30"/>
|
||||||
<ColumnDefinition Width="40"/>
|
<ColumnDefinition Width="40"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="100"/>
|
<ColumnDefinition Width="70"/>
|
||||||
<ColumnDefinition Width="120"/>
|
<ColumnDefinition Width="85"/>
|
||||||
<ColumnDefinition Width="40"/>
|
<ColumnDefinition Width="36"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected, Mode=TwoWay}"
|
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected, Mode=TwoWay}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
|
|
@ -880,7 +894,8 @@
|
||||||
</Border>
|
</Border>
|
||||||
<TextBlock Text="{Binding MetaLine}"
|
<TextBlock Text="{Binding MetaLine}"
|
||||||
Style="{StaticResource FsCaptionStyle}"
|
Style="{StaticResource FsCaptionStyle}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ProgressBar Height="2" Margin="0,5,0,0"
|
<ProgressBar Height="2" Margin="0,5,0,0"
|
||||||
Minimum="0" Maximum="100"
|
Minimum="0" Maximum="100"
|
||||||
|
|
@ -890,12 +905,15 @@
|
||||||
<TextBlock Grid.Column="3" Text="{Binding SizeText}"
|
<TextBlock Grid.Column="3" Text="{Binding SizeText}"
|
||||||
Style="{StaticResource FsMonoStyle}"
|
Style="{StaticResource FsMonoStyle}"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"
|
||||||
<TextBlock Grid.Column="4" Text="{Binding StateText}"
|
Margin="0,0,8,0"/>
|
||||||
|
<TextBlock Grid.Column="4" Text="{Binding DisplayStateText}"
|
||||||
FontFamily="{StaticResource FsFontMono}"
|
FontFamily="{StaticResource FsFontMono}"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
Foreground="{Binding StateBrush}"
|
Foreground="{Binding StateBrush}"
|
||||||
VerticalAlignment="Center"/>
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="6,0,0,0"/>
|
||||||
<Button Grid.Column="5"
|
<Button Grid.Column="5"
|
||||||
Style="{StaticResource FsIconButtonStyle}"
|
Style="{StaticResource FsIconButtonStyle}"
|
||||||
Width="28" Height="28"
|
Width="28" Height="28"
|
||||||
|
|
@ -923,38 +941,42 @@
|
||||||
<Grid x:Name="PastResultsContainer" Visibility="Collapsed">
|
<Grid x:Name="PastResultsContainer" Visibility="Collapsed">
|
||||||
<!-- 빈 상태 안내 (Empty State) -->
|
<!-- 빈 상태 안내 (Empty State) -->
|
||||||
<Grid x:Name="PastResultsEmpty" Visibility="Collapsed">
|
<Grid x:Name="PastResultsEmpty" Visibility="Collapsed">
|
||||||
<Border Background="{StaticResource FsBgBase}" Padding="32">
|
<Border Background="{StaticResource FsBgBase}" Padding="24">
|
||||||
<Grid MaxWidth="460" HorizontalAlignment="Center" VerticalAlignment="Center">
|
<!-- Double-Bezel History Empty Stage (high-end-visual-design & minimalist-ui) -->
|
||||||
<Rectangle RadiusX="12" RadiusY="12"
|
<Border Style="{StaticResource FsDoubleBezelShellStyle}"
|
||||||
Stroke="{StaticResource FsBorderHairline}"
|
MaxWidth="540" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
StrokeThickness="1.5"
|
<Border Style="{StaticResource FsDoubleBezelCoreStyle}" Padding="40,36">
|
||||||
StrokeDashArray="5 4"
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
Fill="{StaticResource FsBgSurface}"/>
|
<Border Width="72" Height="72" CornerRadius="36"
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="48,44">
|
Background="#0DFFFFFF" BorderBrush="{StaticResource FsBorderHairline}"
|
||||||
<ui:SymbolIcon Symbol="History24" FontSize="48"
|
BorderThickness="1" HorizontalAlignment="Center">
|
||||||
Foreground="{StaticResource FsTextTertiary}"
|
<ui:SymbolIcon Symbol="History24" FontSize="36"
|
||||||
HorizontalAlignment="Center"/>
|
Foreground="{StaticResource FsAccentCyan}"
|
||||||
<TextBlock Margin="0,16,0,0" FontSize="16" FontWeight="SemiBold"
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Margin="0,18,0,0" FontSize="18" FontWeight="SemiBold"
|
||||||
Foreground="{StaticResource FsTextPrimary}"
|
Foreground="{StaticResource FsTextPrimary}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Text="변환 이력이 없습니다"/>
|
Text="변환 기록이 없습니다"/>
|
||||||
<TextBlock Margin="0,6,0,0" Style="{StaticResource FsCaptionStyle}"
|
<TextBlock Margin="0,8,0,0" Style="{StaticResource FsCaptionStyle}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
TextAlignment="Center"
|
TextAlignment="Center"
|
||||||
Text="파일을 변환하면 완료된 내역과 절감된 용량이 여기에 기록됩니다"/>
|
FontSize="12"
|
||||||
<Button Margin="0,20,0,0"
|
Text="파일을 변환하면 완료된 내역과 절감된 용량이 여기에 안전하게 보관됩니다"/>
|
||||||
|
<Button Margin="0,22,0,0"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
Style="{StaticResource FsPrimaryButtonStyle}"
|
||||||
Padding="16,8"
|
Padding="20,10"
|
||||||
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="Active">
|
CommandParameter="Active">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<ui:SymbolIcon Symbol="ArrowLeft24" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
<ui:SymbolIcon Symbol="ArrowLeft24" FontSize="13" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
<TextBlock Text="큐로 이동하여 파일 추가하기" VerticalAlignment="Center"/>
|
<TextBlock Text="대기열로 이동하여 파일 추가하기" FontSize="12" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Border>
|
||||||
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ScrollViewer x:Name="PastResultsView" VerticalScrollBarVisibility="Auto"
|
<ScrollViewer x:Name="PastResultsView" VerticalScrollBarVisibility="Auto"
|
||||||
|
|
@ -998,12 +1020,12 @@
|
||||||
</i:Interaction.Triggers>
|
</i:Interaction.Triggers>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="42"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="100"/>
|
|
||||||
<ColumnDefinition Width="100"/>
|
|
||||||
<ColumnDefinition Width="100"/>
|
|
||||||
<ColumnDefinition Width="40"/>
|
<ColumnDefinition Width="40"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="75"/>
|
||||||
|
<ColumnDefinition Width="85"/>
|
||||||
|
<ColumnDefinition Width="70"/>
|
||||||
|
<ColumnDefinition Width="36"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Image Width="34" Height="34" VerticalAlignment="Center"
|
<Image Width="34" Height="34" VerticalAlignment="Center"
|
||||||
|
|
@ -1027,7 +1049,8 @@
|
||||||
</Border>
|
</Border>
|
||||||
<TextBlock Text="{Binding MetaLine}"
|
<TextBlock Text="{Binding MetaLine}"
|
||||||
Style="{StaticResource FsCaptionStyle}"
|
Style="{StaticResource FsCaptionStyle}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|
@ -1110,7 +1133,7 @@
|
||||||
Stroke="{StaticResource FsTextSecondary}" StrokeThickness="2"
|
Stroke="{StaticResource FsTextSecondary}" StrokeThickness="2"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Data="M1,12 C1,12 5,4 12,4 C19,4 23,12 23,12 C23,12 19,20 12,20 C5,20 1,12 1,12 Z M12,9 A3,3 0 1,1 12,15 A3,3 0 1,1 12,9 Z"/>
|
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"
|
<TextBlock Text="미리보기" Margin="8,0,0,0"
|
||||||
Style="{StaticResource FsLabelStyle}"
|
Style="{StaticResource FsLabelStyle}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
@ -1124,25 +1147,27 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- 이미지 영역 -->
|
<!-- 이미지 영역 (Double-Bezel Machined Stage) -->
|
||||||
<Grid Grid.Row="1" Margin="20,16,20,16" x:Name="PreviewImageArea">
|
<Grid Grid.Row="1" Margin="20,16,20,16" x:Name="PreviewImageArea">
|
||||||
<Border Background="{StaticResource FsBgInput}"
|
<Border Style="{StaticResource FsDoubleBezelShellStyle}">
|
||||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
<Border Style="{StaticResource FsDoubleBezelCoreStyle}" Padding="0">
|
||||||
BorderThickness="1" CornerRadius="6">
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
<StackPanel x:Name="PreviewEmpty" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
|
<StackPanel x:Name="PreviewEmpty" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
|
||||||
<Path Width="36" Height="36" Stretch="Uniform"
|
<Border Width="56" Height="56" CornerRadius="28" Background="#0AFFFFFF"
|
||||||
Stroke="{StaticResource FsTextTertiary}" StrokeThickness="1.5"
|
BorderBrush="{StaticResource FsBorderHairline}" BorderThickness="1"
|
||||||
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"/>
|
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
<TextBlock Margin="0,12,0,0" FontSize="13" FontWeight="SemiBold"
|
<ui:SymbolIcon Symbol="Eye24" FontSize="26" Foreground="{StaticResource FsTextTertiary}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Margin="0,14,0,0" FontSize="13" FontWeight="SemiBold"
|
||||||
Foreground="{StaticResource FsTextSecondary}"
|
Foreground="{StaticResource FsTextSecondary}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Text="No selection"/>
|
Text="선택된 항목 없음"/>
|
||||||
<TextBlock Margin="0,4,0,0" Style="{StaticResource FsCaptionStyle}"
|
<TextBlock Margin="0,4,0,0" Style="{StaticResource FsCaptionStyle}"
|
||||||
HorizontalAlignment="Center" TextAlignment="Center"
|
HorizontalAlignment="Center" TextAlignment="Center"
|
||||||
TextWrapping="Wrap" MaxWidth="240"
|
TextWrapping="Wrap" MaxWidth="240"
|
||||||
Text="Active Queue 항목을 클릭하면 여기에 미리보기가 표시됩니다."/>
|
Text="대기열에서 항목을 선택하면 여기에 상세 정보와 미리보기가 표시됩니다."/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Loading -->
|
<!-- Loading -->
|
||||||
|
|
@ -1158,9 +1183,10 @@
|
||||||
|
|
||||||
<!-- Image -->
|
<!-- Image -->
|
||||||
<Image x:Name="PreviewImage" Stretch="Uniform" Margin="16"
|
<Image x:Name="PreviewImage" Stretch="Uniform" Margin="16"
|
||||||
|
RenderOptions.BitmapScalingMode="HighQuality"
|
||||||
Visibility="Collapsed"/>
|
Visibility="Collapsed"/>
|
||||||
|
|
||||||
<!-- Reason -->
|
<!-- Reason / Non-image Format Glyph Fallback -->
|
||||||
<StackPanel x:Name="PreviewReason" Visibility="Collapsed"
|
<StackPanel x:Name="PreviewReason" Visibility="Collapsed"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
|
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
|
||||||
<Image x:Name="PreviewGlyph" Width="84" Height="84"
|
<Image x:Name="PreviewGlyph" Width="84" Height="84"
|
||||||
|
|
@ -1177,6 +1203,7 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- 메타 -->
|
<!-- 메타 -->
|
||||||
|
|
@ -1209,26 +1236,26 @@
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Format"
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="형식"
|
||||||
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
||||||
<TextBlock x:Name="PreviewFormatText" Grid.Row="0" Grid.Column="1"
|
<TextBlock x:Name="PreviewFormatText" Grid.Row="0" Grid.Column="1"
|
||||||
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
||||||
Foreground="{StaticResource FsTextPrimary}" Margin="0,0,0,4"/>
|
Foreground="{StaticResource FsTextPrimary}" Margin="0,0,0,4"/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Size"
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="크기"
|
||||||
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
||||||
<TextBlock x:Name="PreviewSizeText" Grid.Row="1" Grid.Column="1"
|
<TextBlock x:Name="PreviewSizeText" Grid.Row="1" Grid.Column="1"
|
||||||
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
||||||
Foreground="{StaticResource FsTextPrimary}" Margin="0,0,0,4"/>
|
Foreground="{StaticResource FsTextPrimary}" Margin="0,0,0,4"/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Dimensions"
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="해상도 / 규격"
|
||||||
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
||||||
<TextBlock x:Name="PreviewDimText" Grid.Row="2" Grid.Column="1"
|
<TextBlock x:Name="PreviewDimText" Grid.Row="2" Grid.Column="1"
|
||||||
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
||||||
Foreground="{StaticResource FsTextPrimary}" Text="—"
|
Foreground="{StaticResource FsTextPrimary}" Text="—"
|
||||||
Margin="0,0,0,4"/>
|
Margin="0,0,0,4"/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Pages"
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="페이지 / 정보"
|
||||||
Style="{StaticResource FsCaptionStyle}"/>
|
Style="{StaticResource FsCaptionStyle}"/>
|
||||||
<TextBlock x:Name="PreviewPageText" Grid.Row="3" Grid.Column="1"
|
<TextBlock x:Name="PreviewPageText" Grid.Row="3" Grid.Column="1"
|
||||||
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
||||||
|
|
@ -1269,17 +1296,31 @@
|
||||||
|
|
||||||
<!-- Drop hint overlay (전체 덮음) -->
|
<!-- Drop hint overlay (전체 덮음) -->
|
||||||
<Border x:Name="DropHintOverlay" Visibility="Collapsed" Grid.RowSpan="2" Grid.ColumnSpan="2"
|
<Border x:Name="DropHintOverlay" Visibility="Collapsed" Grid.RowSpan="2" Grid.ColumnSpan="2"
|
||||||
Background="#CC090A0C" IsHitTestVisible="False">
|
Background="#D9090A0C" IsHitTestVisible="False">
|
||||||
|
<Border Style="{StaticResource FsDoubleBezelShellStyle}"
|
||||||
|
MaxWidth="440" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<Border Style="{StaticResource FsDoubleBezelCoreStyle}" Padding="40,32">
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
<Path Width="64" Height="64" Stretch="Uniform"
|
<Border Width="64" Height="64" CornerRadius="32"
|
||||||
Stroke="{StaticResource FsAccentBlue}" StrokeThickness="2"
|
Background="#153B82F6" BorderBrush="{StaticResource FsAccentBlue}"
|
||||||
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"/>
|
BorderThickness="1.5" HorizontalAlignment="Center">
|
||||||
<TextBlock Text="Drop to add to queue" Margin="0,16,0,0"
|
<Path Width="28" Height="28" Stretch="Uniform"
|
||||||
FontSize="18" FontWeight="SemiBold"
|
Stroke="{StaticResource FsAccentBlue}" StrokeThickness="2.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"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="파일을 놓아 대기열에 추가" Margin="0,18,0,0"
|
||||||
|
FontSize="17" FontWeight="SemiBold"
|
||||||
Foreground="{StaticResource FsTextPrimary}"
|
Foreground="{StaticResource FsTextPrimary}"
|
||||||
HorizontalAlignment="Center"/>
|
HorizontalAlignment="Center"/>
|
||||||
|
<TextBlock Text="모든 파일 형식 지원 · 자동 분석 및 프리셋 매칭" Margin="0,6,0,0"
|
||||||
|
Style="{StaticResource FsCaptionStyle}"
|
||||||
|
Foreground="{StaticResource FsTextSecondary}"
|
||||||
|
HorizontalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
</Border>
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
ToggleInspectorCommand = new RelayCommand(_ => ToggleInspector());
|
ToggleInspectorCommand = new RelayCommand(_ => ToggleInspector());
|
||||||
RemoveQueueItemCommand = new RelayCommand(p => RemoveQueueItem(p as QueueItem));
|
RemoveQueueItemCommand = new RelayCommand(p => RemoveQueueItem(p as QueueItem));
|
||||||
OpenFolderCommand = new RelayCommand(p => OpenFolderForPath(p as string));
|
OpenFolderCommand = new RelayCommand(p => OpenFolderForPath(p as string));
|
||||||
TabCommand = new RelayCommand(p => ShowTab(p as string ?? "Past"));
|
TabCommand = new RelayCommand(p => ShowTab(p as string ?? "Active"));
|
||||||
ConflictRuleCommand = new RelayCommand(p => SetConflictRule(p as string));
|
ConflictRuleCommand = new RelayCommand(p => SetConflictRule(p as string));
|
||||||
CombineToggleCommand = new RelayCommand(_ => UpdateCombineState(SelectedOutputExtension));
|
CombineToggleCommand = new RelayCommand(_ => UpdateCombineState(SelectedOutputExtension));
|
||||||
OutputFormatChangedCommand = new RelayCommand(_ => OnOutputFormatSelected());
|
OutputFormatChangedCommand = new RelayCommand(_ => OnOutputFormatSelected());
|
||||||
|
|
@ -156,12 +156,26 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
PastRowCommand = new RelayCommand(p => HandlePastRowClick(p as MouseButtonEventArgs));
|
PastRowCommand = new RelayCommand(p => HandlePastRowClick(p as MouseButtonEventArgs));
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
Title = "Everything2Everything";
|
||||||
|
if (AppTitleBar is not null) AppTitleBar.Title = "Everything2Everything";
|
||||||
|
|
||||||
|
if (AdvancedOptionsExpander is not null)
|
||||||
|
{
|
||||||
|
AdvancedOptionsExpander.IsExpanded = true;
|
||||||
|
AdvancedOptionsExpander.Expanded += (_, _) => _options.IsAdvancedExpanded = true;
|
||||||
|
AdvancedOptionsExpander.Collapsed += (_, _) => _options.IsAdvancedExpanded = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (SmartPresetCombo is not null)
|
if (SmartPresetCombo is not null)
|
||||||
{
|
{
|
||||||
SmartPresetCombo.SelectionChanged += OnSmartPresetChanged;
|
SmartPresetCombo.SelectionChanged += OnSmartPresetChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (OutputFormatCombo is not null)
|
||||||
|
{
|
||||||
|
OutputFormatCombo.SelectionChanged += (_, _) => OnOutputFormatSelected();
|
||||||
|
}
|
||||||
|
|
||||||
// ActiveQueueList/PastResultsList의 ItemsSource는 XAML이 ActiveQueue/PastResults에 바인딩(선언적).
|
// ActiveQueueList/PastResultsList의 ItemsSource는 XAML이 ActiveQueue/PastResults에 바인딩(선언적).
|
||||||
|
|
||||||
InitializeOutputFormats();
|
InitializeOutputFormats();
|
||||||
|
|
@ -202,7 +216,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CapabilityStatusText.Text = $"⚠ {notReady.Count}개 형식이 외부 도구를 기다립니다 (Diagnose 참조)";
|
CapabilityStatusText.Text = $"⚠ {notReady.Count}개 형식이 외부 도구를 기다립니다 (진단 도구 참조)";
|
||||||
CapabilityStatusText.Visibility = Visibility.Visible;
|
CapabilityStatusText.Visibility = Visibility.Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -343,22 +357,26 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
var count = _activeQueue.Count;
|
var count = _activeQueue.Count;
|
||||||
if (_cts is not null)
|
if (_cts is not null)
|
||||||
{
|
{
|
||||||
ProcessQueueButton.Content = $"변환 처리 중… ({count}개 파일)";
|
ProcessQueueButton.Content = $"변환 처리 중… ({count}개)";
|
||||||
|
ProcessQueueButton.ToolTip = "파일 변환이 진행 중입니다.";
|
||||||
ProcessQueueButton.IsEnabled = false;
|
ProcessQueueButton.IsEnabled = false;
|
||||||
}
|
}
|
||||||
else if (count == 0)
|
else if (count == 0)
|
||||||
{
|
{
|
||||||
ProcessQueueButton.Content = "대기 중 — 파일을 드래그하여 추가하세요";
|
ProcessQueueButton.Content = "파일을 드래그하여 추가";
|
||||||
|
ProcessQueueButton.ToolTip = "대기열에 변환할 파일을 추가하세요 (단축키: Ctrl + O)";
|
||||||
ProcessQueueButton.IsEnabled = false;
|
ProcessQueueButton.IsEnabled = false;
|
||||||
}
|
}
|
||||||
else if (string.IsNullOrEmpty(SelectedOutputExtension))
|
else if (string.IsNullOrEmpty(SelectedOutputExtension))
|
||||||
{
|
{
|
||||||
ProcessQueueButton.Content = "변환 불가 (공통 형식 없음)";
|
ProcessQueueButton.Content = "공통 형식 없음";
|
||||||
|
ProcessQueueButton.ToolTip = "선택된 파일들 간에 호환 가능한 공통 출력 형식이 없습니다.";
|
||||||
ProcessQueueButton.IsEnabled = false;
|
ProcessQueueButton.IsEnabled = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ProcessQueueButton.Content = $"대기열 일괄 변환 시작 ({count}개) [Ctrl + Enter]";
|
ProcessQueueButton.Content = $"변환 시작 ({count}개 파일)";
|
||||||
|
ProcessQueueButton.ToolTip = $"대기열 일괄 변환 시작 ({count}개 파일) [단축키: Ctrl + Enter]";
|
||||||
ProcessQueueButton.IsEnabled = true;
|
ProcessQueueButton.IsEnabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -508,7 +526,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
PreviewFormatText.Text = string.IsNullOrEmpty(formatLabel) || formatLabel == "—" ? info.Extension.TrimStart('.').ToUpperInvariant() : formatLabel;
|
PreviewFormatText.Text = string.IsNullOrEmpty(formatLabel) || formatLabel == "—" ? info.Extension.TrimStart('.').ToUpperInvariant() : formatLabel;
|
||||||
PreviewSizeText.Text = string.IsNullOrEmpty(sizeText) || sizeText == "—" ? info.FormattedSize : sizeText;
|
PreviewSizeText.Text = string.IsNullOrEmpty(sizeText) || sizeText == "—" ? info.FormattedSize : sizeText;
|
||||||
PreviewDimText.Text = info.DimensionsOrMeta;
|
PreviewDimText.Text = info.DimensionsOrMeta;
|
||||||
PreviewPageText.Text = info.Category.ToString();
|
PreviewPageText.Text = info.Category.ToKoreanLabel();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowPreviewLoading()
|
private void ShowPreviewLoading()
|
||||||
|
|
@ -1043,10 +1061,9 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
private static string FormatDateLabel(DateOnly date)
|
private static string FormatDateLabel(DateOnly date)
|
||||||
{
|
{
|
||||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
var label = date == today ? "Today"
|
if (date == today) return $"오늘 ({date:M월 d일})";
|
||||||
: date == today.AddDays(-1) ? "Yesterday"
|
if (date == today.AddDays(-1)) return $"어제 ({date:M월 d일})";
|
||||||
: date.ToString("dddd", CultureInfo.GetCultureInfo("en-US"));
|
return date.ToString("yyyy년 M월 d일 (ddd)", CultureInfo.GetCultureInfo("ko-KR"));
|
||||||
return $"{label}, {date:MMM d}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyAppDataStats()
|
private void ApplyAppDataStats()
|
||||||
|
|
@ -1098,7 +1115,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
else if (TabPastBtn.IsChecked == true)
|
else if (TabPastBtn.IsChecked == true)
|
||||||
{
|
{
|
||||||
var confirm = MessageBox.Show(this,
|
var confirm = MessageBox.Show(this,
|
||||||
"Past Results 전체를 삭제하시겠습니까?\n영구 저장된 이력도 함께 삭제됩니다.",
|
"변환 기록 전체를 삭제하시겠습니까?\n영구 저장된 이력도 함께 삭제됩니다.",
|
||||||
"Everything2Everything",
|
"Everything2Everything",
|
||||||
MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
||||||
if (confirm != MessageBoxResult.OK) return;
|
if (confirm != MessageBoxResult.OK) return;
|
||||||
|
|
@ -1386,17 +1403,33 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||||
|
|
||||||
private void UpdateQualityPanelForFormat(string? extension)
|
private void UpdateQualityPanelForFormat(string? extension)
|
||||||
{
|
{
|
||||||
if (QualityPanel is null || QualityLabelText is null) return;
|
|
||||||
var ext = extension?.ToLowerInvariant();
|
var ext = extension?.ToLowerInvariant();
|
||||||
var supportsQuality = ext is ".jpg" or ".jpeg" or ".webp" or ".avif";
|
var isImageQuality = ext is ".jpg" or ".jpeg" or ".webp" or ".avif";
|
||||||
QualityPanel.Visibility = supportsQuality ? Visibility.Visible : Visibility.Collapsed;
|
var isVideo = ext is ".mp4" or ".mkv" or ".webm" or ".mov" or ".avi";
|
||||||
|
var isAudio = ext is ".mp3" or ".aac" or ".m4a" or ".opus" or ".ogg" or ".flac" or ".wav";
|
||||||
|
var isPdf = ext is ".pdf";
|
||||||
|
|
||||||
|
if (QualityPanel is not null)
|
||||||
|
{
|
||||||
|
QualityPanel.Visibility = isImageQuality ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
if (QualityLabelText is not null)
|
||||||
|
{
|
||||||
QualityLabelText.Text = ext switch
|
QualityLabelText.Text = ext switch
|
||||||
{
|
{
|
||||||
".jpg" or ".jpeg" => "JPEG QUALITY",
|
".jpg" or ".jpeg" => "JPEG 압축 품질",
|
||||||
".webp" => "WEBP QUALITY",
|
".webp" => "WebP 압축 품질",
|
||||||
".avif" => "AVIF QUALITY",
|
".avif" => "AVIF 압축 품질",
|
||||||
_ => "ENCODING QUALITY",
|
_ => "압축 품질",
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VideoQuickPanel is not null)
|
||||||
|
VideoQuickPanel.Visibility = isVideo ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
if (AudioQuickPanel is not null)
|
||||||
|
AudioQuickPanel.Visibility = isAudio ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
if (PdfQuickPanel is not null)
|
||||||
|
PdfQuickPanel.Visibility = isPdf ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
UpdateMediaPanelForFormat(extension);
|
UpdateMediaPanelForFormat(extension);
|
||||||
}
|
}
|
||||||
|
|
@ -1471,9 +1504,17 @@ public sealed class QueueItem : INotifyPropertyChanged
|
||||||
public string StateText
|
public string StateText
|
||||||
{
|
{
|
||||||
get => _state;
|
get => _state;
|
||||||
set { _state = value; Raise(nameof(StateText)); Raise(nameof(IsDone)); }
|
set { _state = value; Raise(nameof(StateText)); Raise(nameof(DisplayStateText)); Raise(nameof(IsDone)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>사용자에게 표시되는 정제된 한국어 상태 텍스트 (AGENTS.md Rule 3).</summary>
|
||||||
|
public string DisplayStateText => _state switch
|
||||||
|
{
|
||||||
|
"queued" => "대기 중",
|
||||||
|
"done" => "변환 완료",
|
||||||
|
_ => _state,
|
||||||
|
};
|
||||||
|
|
||||||
public Brush StateBrush => _state switch
|
public Brush StateBrush => _state switch
|
||||||
{
|
{
|
||||||
"queued" => (Application.Current?.TryFindResource("FsTextTertiary") as Brush) ?? Brushes.Gray,
|
"queued" => (Application.Current?.TryFindResource("FsTextTertiary") as Brush) ?? Brushes.Gray,
|
||||||
|
|
@ -1547,7 +1588,7 @@ public sealed class DateGroup : INotifyPropertyChanged
|
||||||
public string DateTitle { get; }
|
public string DateTitle { get; }
|
||||||
public ObservableCollection<HistoryRow> Entries { get; } = new();
|
public ObservableCollection<HistoryRow> Entries { get; } = new();
|
||||||
public long SessionSavingsBytes { get; set; }
|
public long SessionSavingsBytes { get; set; }
|
||||||
public string SessionSavingsText => $"Session Savings: {MainWindow.HumanizeBytes(SessionSavingsBytes)}";
|
public string SessionSavingsText => $"세션 절감: {MainWindow.HumanizeBytes(SessionSavingsBytes)}";
|
||||||
|
|
||||||
public DateGroup(string dateTitle) { DateTitle = dateTitle; }
|
public DateGroup(string dateTitle) { DateTitle = dateTitle; }
|
||||||
|
|
||||||
|
|
@ -1591,7 +1632,7 @@ public sealed record HistoryRow(
|
||||||
FormatLabel: label,
|
FormatLabel: label,
|
||||||
FormatBrush: brush,
|
FormatBrush: brush,
|
||||||
FileName: Path.GetFileName(e.SourcePath),
|
FileName: Path.GetFileName(e.SourcePath),
|
||||||
MetaLine: $"{e.Timestamp:HH:mm:ss} • {e.OutputCount} output(s)",
|
MetaLine: $"{e.Timestamp:HH:mm:ss} • {e.OutputCount}개 파일",
|
||||||
SizeText: MainWindow.HumanizeBytes(e.SourceSizeBytes),
|
SizeText: MainWindow.HumanizeBytes(e.SourceSizeBytes),
|
||||||
SavingsText: $"{arrow} {MainWindow.HumanizeBytes(Math.Abs(saved))}",
|
SavingsText: $"{arrow} {MainWindow.HumanizeBytes(Math.Abs(saved))}",
|
||||||
SourcePath: e.SourcePath,
|
SourcePath: e.SourcePath,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ui:ControlsDictionary/>
|
||||||
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
|
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ui:ControlsDictionary/>
|
||||||
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
|
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ui:ControlsDictionary/>
|
||||||
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
|
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
@ -29,9 +30,8 @@
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
|
|
||||||
<!-- ===== AI 카드 ===== -->
|
<!-- ===== AI 카드 ===== -->
|
||||||
<Border Background="{StaticResource FsBgSurface}"
|
<Border Style="{StaticResource FsDoubleBezelShellStyle}" Margin="0,0,0,16">
|
||||||
BorderBrush="{StaticResource FsBorderHairline}" BorderThickness="1"
|
<Border Style="{StaticResource FsDoubleBezelCoreStyle}" Padding="20">
|
||||||
CornerRadius="10" Padding="20" Margin="0,0,0,16">
|
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="AI 텍스트 변환" Style="{StaticResource FsBodyStyle}" FontWeight="SemiBold" FontSize="15"/>
|
<TextBlock Text="AI 텍스트 변환" Style="{StaticResource FsBodyStyle}" FontWeight="SemiBold" FontSize="15"/>
|
||||||
<TextBlock Text="요약 · 번역 · 교정에 사용됩니다 (종량 과금 · 네트워크 필요). 키가 없으면 AI 변환만 비활성됩니다."
|
<TextBlock Text="요약 · 번역 · 교정에 사용됩니다 (종량 과금 · 네트워크 필요). 키가 없으면 AI 변환만 비활성됩니다."
|
||||||
|
|
@ -102,11 +102,11 @@
|
||||||
Style="{StaticResource FsCaptionStyle}" Margin="0,6,0,0"/>
|
Style="{StaticResource FsCaptionStyle}" Margin="0,6,0,0"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- ===== 외부 도구 카드 ===== -->
|
<!-- ===== 외부 도구 카드 ===== -->
|
||||||
<Border Background="{StaticResource FsBgSurface}"
|
<Border Style="{StaticResource FsDoubleBezelShellStyle}">
|
||||||
BorderBrush="{StaticResource FsBorderHairline}" BorderThickness="1"
|
<Border Style="{StaticResource FsDoubleBezelCoreStyle}" Padding="20">
|
||||||
CornerRadius="10" Padding="20">
|
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="외부 도구" Style="{StaticResource FsBodyStyle}" FontWeight="SemiBold" FontSize="15"/>
|
<TextBlock Text="외부 도구" Style="{StaticResource FsBodyStyle}" FontWeight="SemiBold" FontSize="15"/>
|
||||||
<TextBlock Text="설치하면 영상/오디오·한글/Word 변환이 자동 활성화됩니다."
|
<TextBlock Text="설치하면 영상/오디오·한글/Word 변환이 자동 활성화됩니다."
|
||||||
|
|
@ -156,6 +156,7 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
</Border>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
|
||||||
|
|
@ -68,4 +68,15 @@ public static class QueueFilterMatcher
|
||||||
{
|
{
|
||||||
return System.Linq.Enumerable.Where(source, item => Matches(fileNameSelector(item), query, category));
|
return System.Linq.Enumerable.Where(source, item => Matches(fileNameSelector(item), query, category));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>카테고리를 사용자 친화적인 한국어 라벨로 변환합니다.</summary>
|
||||||
|
public static string ToKoreanLabel(this FilterCategory category) => category switch
|
||||||
|
{
|
||||||
|
FilterCategory.All => "전체",
|
||||||
|
FilterCategory.Image => "이미지",
|
||||||
|
FilterCategory.Document => "문서",
|
||||||
|
FilterCategory.Media => "미디어",
|
||||||
|
FilterCategory.Data => "데이터",
|
||||||
|
_ => "기타",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ public static class FileInspectorBuilder
|
||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
var formattedSize = HumanizeBytes(size);
|
var formattedSize = HumanizeBytes(size);
|
||||||
var meta = $"{ext.TrimStart('.').ToUpperInvariant()} · {category}";
|
var meta = $"{ext.TrimStart('.').ToUpperInvariant()} · {category.ToKoreanLabel()}";
|
||||||
|
|
||||||
return new FileInspectorInfo(
|
return new FileInspectorInfo(
|
||||||
FileName: fileName,
|
FileName: fileName,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ public static class FormatPresetEngine
|
||||||
".avif" => AvifPresets(),
|
".avif" => AvifPresets(),
|
||||||
".jpg" or ".jpeg" => JpgPresets(),
|
".jpg" or ".jpeg" => JpgPresets(),
|
||||||
".png" => PngPresets(),
|
".png" => PngPresets(),
|
||||||
|
".gif" => GifPresets(),
|
||||||
|
".heic" => HeicPresets(),
|
||||||
|
|
||||||
".pdf" => PdfPresets(),
|
".pdf" => PdfPresets(),
|
||||||
|
|
||||||
|
|
@ -148,15 +150,15 @@ public static class FormatPresetEngine
|
||||||
new(
|
new(
|
||||||
"webp-web-q85",
|
"webp-web-q85",
|
||||||
"웹 고화질 (Q85 · 추천)",
|
"웹 고화질 (Q85 · 추천)",
|
||||||
"Quality 85 · EXIF 메타데이터 제거 · 웹 게시 표준",
|
"품질 85 · EXIF 메타데이터 제거 · 웹 게시 표준",
|
||||||
new[] { "Quality 85", "Strip EXIF", "웹 최적화" },
|
new[] { "품질 85", "EXIF 제거", "웹 최적화" },
|
||||||
opt => { opt.ImageQuality = 85; opt.StripMetadata = true; }
|
opt => { opt.ImageQuality = 85; opt.StripMetadata = true; }
|
||||||
),
|
),
|
||||||
new(
|
new(
|
||||||
"webp-compact-q65",
|
"webp-compact-q65",
|
||||||
"웹 초경량 (Q65 · 빠른 로딩)",
|
"웹 초경량 (Q65 · 빠른 로딩)",
|
||||||
"Quality 65 · 고압축 이미지로 첫 페이지 로딩 가속",
|
"품질 65 · 고압축 이미지로 첫 페이지 로딩 가속",
|
||||||
new[] { "Quality 65", "Strip EXIF", "초경량" },
|
new[] { "품질 65", "EXIF 제거", "초경량" },
|
||||||
opt => { opt.ImageQuality = 65; opt.StripMetadata = true; }
|
opt => { opt.ImageQuality = 65; opt.StripMetadata = true; }
|
||||||
),
|
),
|
||||||
new(
|
new(
|
||||||
|
|
@ -170,8 +172,15 @@ public static class FormatPresetEngine
|
||||||
"webp-sns-thumb",
|
"webp-sns-thumb",
|
||||||
"SNS 썸네일 (Q75)",
|
"SNS 썸네일 (Q75)",
|
||||||
"피드 및 카드 썸네일 최적화",
|
"피드 및 카드 썸네일 최적화",
|
||||||
new[] { "Quality 75", "썸네일" },
|
new[] { "품질 75", "썸네일" },
|
||||||
opt => { opt.ImageQuality = 75; opt.StripMetadata = true; }
|
opt => { opt.ImageQuality = 75; opt.StripMetadata = true; }
|
||||||
|
),
|
||||||
|
new(
|
||||||
|
"webp-extreme-q50",
|
||||||
|
"초절약 압축 (Q50)",
|
||||||
|
"대역폭 극소화 및 모바일 웹 가속",
|
||||||
|
new[] { "품질 50", "대역폭 절약", "초경량" },
|
||||||
|
opt => { opt.ImageQuality = 50; opt.StripMetadata = true; }
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -179,17 +188,31 @@ public static class FormatPresetEngine
|
||||||
{
|
{
|
||||||
new(
|
new(
|
||||||
"avif-balanced",
|
"avif-balanced",
|
||||||
"차세대 초고압축 (Q55)",
|
"차세대 초고압축 (Q55 · 추천)",
|
||||||
"AV1 코덱 기반 압축률 극대화",
|
"AV1 코덱 기반 압축률 극대화 · 웹 표준",
|
||||||
new[] { "Quality 55", "AV1 코덱", "초고압축" },
|
new[] { "품질 55", "AV1 코덱", "초고압축" },
|
||||||
opt => { opt.ImageQuality = 85; opt.StripMetadata = true; }
|
opt => { opt.ImageQuality = 85; opt.StripMetadata = true; }
|
||||||
),
|
),
|
||||||
new(
|
new(
|
||||||
"avif-high",
|
"avif-high",
|
||||||
"고화질 아카이빙 (Q75)",
|
"고화질 아카이빙 (Q75)",
|
||||||
"색상 심도 10-bit HDR 보존",
|
"색상 심도 10-bit HDR 보존 및 디테일 유지",
|
||||||
new[] { "Quality 75", "10-bit HDR" },
|
new[] { "품질 75", "10-bit HDR", "고화질" },
|
||||||
opt => { opt.ImageQuality = 95; opt.StripMetadata = false; }
|
opt => { opt.ImageQuality = 95; opt.StripMetadata = false; }
|
||||||
|
),
|
||||||
|
new(
|
||||||
|
"avif-web-stream",
|
||||||
|
"웹 스트리밍 경량 (Q60)",
|
||||||
|
"빠른 디코딩 및 현대적 웹 브라우저 가속",
|
||||||
|
new[] { "품질 60", "웹 스트리밍", "빠른 로딩" },
|
||||||
|
opt => { opt.ImageQuality = 70; opt.StripMetadata = true; }
|
||||||
|
),
|
||||||
|
new(
|
||||||
|
"avif-compact-q45",
|
||||||
|
"극소 용량 보관 (Q45)",
|
||||||
|
"초고효율 AV1 압축으로 최소 용량 달성",
|
||||||
|
new[] { "품질 45", "초절약", "극소 용량" },
|
||||||
|
opt => { opt.ImageQuality = 55; opt.StripMetadata = true; }
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -198,23 +221,37 @@ public static class FormatPresetEngine
|
||||||
new(
|
new(
|
||||||
"jpg-photo-q95",
|
"jpg-photo-q95",
|
||||||
"디지털 인화·고화질 (Q95)",
|
"디지털 인화·고화질 (Q95)",
|
||||||
"Quality 95 · 색상 프로파일 보존 · 선명한 사진",
|
"품질 95 · 색상 프로파일 보존 · 선명한 사진",
|
||||||
new[] { "Quality 95", "ICC 보존", "고해상도" },
|
new[] { "품질 95", "ICC 보존", "고해상도" },
|
||||||
opt => { opt.ImageQuality = 95; opt.StripMetadata = false; }
|
opt => { opt.ImageQuality = 95; opt.StripMetadata = false; }
|
||||||
),
|
),
|
||||||
new(
|
new(
|
||||||
"jpg-web-q80",
|
"jpg-web-q80",
|
||||||
"웹 표준 (Q80 · 권장)",
|
"웹 표준 (Q80 · 권장)",
|
||||||
"Quality 80 · 프로그레시브 JPEG · 메타데이터 제거",
|
"품질 80 · 프로그레시브 JPEG · 메타데이터 제거",
|
||||||
new[] { "Quality 80", "Strip EXIF", "프로그레시브" },
|
new[] { "품질 80", "EXIF 제거", "프로그레시브" },
|
||||||
opt => { opt.ImageQuality = 80; opt.StripMetadata = true; }
|
opt => { opt.ImageQuality = 80; opt.StripMetadata = true; }
|
||||||
),
|
),
|
||||||
new(
|
new(
|
||||||
"jpg-compact-q70",
|
"jpg-compact-q70",
|
||||||
"모바일 메신저 (Q70)",
|
"모바일 메신저 (Q70)",
|
||||||
"Quality 70 · 카카오톡/문자 전송 가벼운 용량",
|
"품질 70 · 카카오톡/문자 전송 가벼운 용량",
|
||||||
new[] { "Quality 70", "용량 절약" },
|
new[] { "품질 70", "용량 절약", "모바일 최적화" },
|
||||||
opt => { opt.ImageQuality = 70; opt.StripMetadata = true; }
|
opt => { opt.ImageQuality = 70; opt.StripMetadata = true; }
|
||||||
|
),
|
||||||
|
new(
|
||||||
|
"jpg-archive-q100",
|
||||||
|
"아카이빙 무손실급 (Q100)",
|
||||||
|
"최고 화질 보존 · 메타데이터 및 ICC 완전 유지",
|
||||||
|
new[] { "품질 100", "원본 보존", "무손실급" },
|
||||||
|
opt => { opt.ImageQuality = 100; opt.StripMetadata = false; }
|
||||||
|
),
|
||||||
|
new(
|
||||||
|
"jpg-thumb-q60",
|
||||||
|
"경량 썸네일 (Q60)",
|
||||||
|
"빠른 로딩을 위한 인덱스 썸네일 전용",
|
||||||
|
new[] { "품질 60", "썸네일", "초경량" },
|
||||||
|
opt => { opt.ImageQuality = 60; opt.StripMetadata = true; }
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -233,6 +270,70 @@ public static class FormatPresetEngine
|
||||||
"불필요한 청크 제거 · 알파 투명도 보존",
|
"불필요한 청크 제거 · 알파 투명도 보존",
|
||||||
new[] { "웹 최적화", "투명도 보존", "EXIF 제거" },
|
new[] { "웹 최적화", "투명도 보존", "EXIF 제거" },
|
||||||
opt => { opt.ImageQuality = 100; opt.StripMetadata = true; }
|
opt => { opt.ImageQuality = 100; opt.StripMetadata = true; }
|
||||||
|
),
|
||||||
|
new(
|
||||||
|
"png-clean-alpha",
|
||||||
|
"클린 알파 투명도 보존",
|
||||||
|
"아이콘, 로고, UI 에셋 투명 레이어 무손실",
|
||||||
|
new[] { "알파 채널", "아이콘/UI", "투명도 무손실" },
|
||||||
|
opt => { opt.ImageQuality = 100; opt.StripMetadata = false; }
|
||||||
|
),
|
||||||
|
new(
|
||||||
|
"png-uncompressed",
|
||||||
|
"무압축 고속 출력",
|
||||||
|
"압축 딜레이 없는 즉시 저장 및 렌더링",
|
||||||
|
new[] { "고속 저장", "무손실", "CPU 절약" },
|
||||||
|
opt => { opt.ImageQuality = 100; opt.StripMetadata = false; }
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
private static IReadOnlyList<FormatPreset> GifPresets() => new List<FormatPreset>
|
||||||
|
{
|
||||||
|
new(
|
||||||
|
"gif-web-256",
|
||||||
|
"웹 애니메이션 표준 (256색)",
|
||||||
|
"적응형 팔레트 · 디더링 적용으로 선명한 색감",
|
||||||
|
new[] { "256 Colors", "디더링", "웹 표준" },
|
||||||
|
opt => { opt.Quality = 85; }
|
||||||
|
),
|
||||||
|
new(
|
||||||
|
"gif-compact-128",
|
||||||
|
"초경량 메신저 GIF (128색)",
|
||||||
|
"용량 축소 팔레트 · 프레임 레이트 최적화",
|
||||||
|
new[] { "128 Colors", "용량 절약", "메신저" },
|
||||||
|
opt => { opt.Quality = 65; }
|
||||||
|
),
|
||||||
|
new(
|
||||||
|
"gif-sharp-64",
|
||||||
|
"고압축 그래픽 (64색)",
|
||||||
|
"심플 아이콘 및 UI 애니메이션 극소 용량",
|
||||||
|
new[] { "64 Colors", "초소형", "UI 그래픽" },
|
||||||
|
opt => { opt.Quality = 50; }
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
private static IReadOnlyList<FormatPreset> HeicPresets() => new List<FormatPreset>
|
||||||
|
{
|
||||||
|
new(
|
||||||
|
"heic-original-q90",
|
||||||
|
"Apple 고화질 보존 (Q90)",
|
||||||
|
"아이폰 원본급 HEVC 압축 · 라이브 포토 및 HDR 보존",
|
||||||
|
new[] { "Quality 90", "Apple HEVC", "HDR 보존" },
|
||||||
|
opt => { opt.ImageQuality = 90; opt.StripMetadata = false; }
|
||||||
|
),
|
||||||
|
new(
|
||||||
|
"heic-balanced-q80",
|
||||||
|
"균형 공유 (Q80 · 추천)",
|
||||||
|
"표준 HEIF 압축 · 호환성과 용량 균형",
|
||||||
|
new[] { "Quality 80", "표준 압축", "용량 절약" },
|
||||||
|
opt => { opt.ImageQuality = 80; opt.StripMetadata = true; }
|
||||||
|
),
|
||||||
|
new(
|
||||||
|
"heic-compact-q65",
|
||||||
|
"초경량 아카이빙 (Q65)",
|
||||||
|
"대용량 사진첩 백업용 극소 용량",
|
||||||
|
new[] { "Quality 65", "초경량", "백업 전용" },
|
||||||
|
opt => { opt.ImageQuality = 65; opt.StripMetadata = true; }
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -291,6 +392,13 @@ public static class FormatPresetEngine
|
||||||
new[] { "원본 해상도", "CRF 18", "Medium", "고화질 보존" },
|
new[] { "원본 해상도", "CRF 18", "Medium", "고화질 보존" },
|
||||||
opt => { opt.VideoCrf = 18; opt.VideoPreset = "medium"; opt.ResolutionIndex = 0; opt.AudioBitrateKbps = 320; }
|
opt => { opt.VideoCrf = 18; opt.VideoPreset = "medium"; opt.ResolutionIndex = 0; opt.AudioBitrateKbps = 320; }
|
||||||
),
|
),
|
||||||
|
new(
|
||||||
|
"mp4-4k-uhd",
|
||||||
|
"4K UHD 아카이빙 (CRF 16)",
|
||||||
|
"H.264 High Profile · 마스터링 고화질 아카이빙",
|
||||||
|
new[] { "4K UHD", "CRF 16", "마스터링", "고비트레이트" },
|
||||||
|
opt => { opt.VideoCrf = 16; opt.VideoPreset = "slow"; opt.ResolutionIndex = 1; opt.AudioBitrateKbps = 320; }
|
||||||
|
),
|
||||||
new(
|
new(
|
||||||
"mp4-audio-extract",
|
"mp4-audio-extract",
|
||||||
"오디오 트랙 추출 (AAC)",
|
"오디오 트랙 추출 (AAC)",
|
||||||
|
|
|
||||||
53
src/Everything2Everything.Tests/CategoryGlyphsTests.cs
Normal file
53
src/Everything2Everything.Tests/CategoryGlyphsTests.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using Everything2Everything.App.Views;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Everything2Everything.Tests;
|
||||||
|
|
||||||
|
public class CategoryGlyphsTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(".jpg", 80)]
|
||||||
|
[InlineData(".mp4", 80)]
|
||||||
|
[InlineData(".pdf", 80)]
|
||||||
|
[InlineData(".csv", 80)]
|
||||||
|
public void ForExtension_ReturnsValidImageSource_WithProperPackUri(string ext, int minDimension)
|
||||||
|
{
|
||||||
|
RunOnSta(() =>
|
||||||
|
{
|
||||||
|
var img = CategoryGlyphs.ForExtension(ext) as BitmapImage;
|
||||||
|
Assert.NotNull(img);
|
||||||
|
Assert.Contains("Everything2Everything;component", img.UriSource.OriginalString);
|
||||||
|
Assert.True(img.PixelWidth >= minDimension, $"Glyph for {ext} should have width >= {minDimension}");
|
||||||
|
Assert.True(img.PixelHeight >= minDimension, $"Glyph for {ext} should have height >= {minDimension}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunOnSta(Action action)
|
||||||
|
{
|
||||||
|
Exception? ex = null;
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Application.Current == null)
|
||||||
|
{
|
||||||
|
try { _ = new Application(); } catch { }
|
||||||
|
}
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
ex = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
thread.Start();
|
||||||
|
thread.Join();
|
||||||
|
|
||||||
|
if (ex != null) throw new AggregateException("STA failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -419,6 +419,541 @@ public class DesignAuditAstTests
|
||||||
Assert.NotNull(cyanBgBrush);
|
Assert.NotNull(cyanBgBrush);
|
||||||
Assert.Equal("#083344", cyanBgBrush.Attribute("Color")?.Value);
|
Assert.Equal("#083344", cyanBgBrush.Attribute("Color")?.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InteractiveElements_MustHave_AutomationPropertiesAutomationId_For_E2E_Reliability()
|
||||||
|
{
|
||||||
|
// UI Automation 기반 Headful E2E 테스트 및 접근성(A11y) 신뢰성을 위해 핵심 인터랙티브 요소는 AutomationId가 지정되어야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var requiredAutomationIds = new[]
|
||||||
|
{
|
||||||
|
"ProcessQueueButton",
|
||||||
|
"AdvancedOptionsExpander",
|
||||||
|
"OutputFormatCombo",
|
||||||
|
"ImageLosslessCheck",
|
||||||
|
"PdfCompressLevelCombo",
|
||||||
|
"PdfDpiCombo",
|
||||||
|
"QualitySlider",
|
||||||
|
"VideoCrfSlider",
|
||||||
|
"AudioBitrateQuickCombo",
|
||||||
|
"PdfCompressQuickCombo"
|
||||||
|
};
|
||||||
|
|
||||||
|
var foundAutomationIds = doc.Descendants()
|
||||||
|
.Select(e => e.Attribute(XName.Get("AutomationId", "clr-namespace:System.Windows.Automation;assembly=PresentationCore"))?.Value
|
||||||
|
?? e.Attribute("AutomationProperties.AutomationId")?.Value)
|
||||||
|
.Where(id => !string.IsNullOrEmpty(id))
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
var missing = requiredAutomationIds.Where(req => !foundAutomationIds.Contains(req)).ToList();
|
||||||
|
Assert.True(missing.Count == 0,
|
||||||
|
$"핵심 인터랙티브 요소에 AutomationProperties.AutomationId가 누락되었습니다: {string.Join(", ", missing)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Sidebar_ConflictRuleSection_MustUsePureKoreanLabels()
|
||||||
|
{
|
||||||
|
// designpaca & AGENTS.md Rule 3:
|
||||||
|
// 영문 대문자 'FILE CONFLICT RULE' 및 영문 버튼 'Skip', 'Rename', 'Replace'를 금지하고
|
||||||
|
// 정제된 한국어 표준 '파일 충돌 해결', '건너뛰기', '이름 변경', '덮어쓰기'로 일관되게 제공해야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var allTexts = doc.Descendants().Where(e => e.Name.LocalName == "TextBlock")
|
||||||
|
.Select(t => t.Attribute("Text")?.Value ?? t.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.DoesNotContain(allTexts, t => t.Contains("FILE CONFLICT RULE", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
var skipBtn = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ConflictSkipBtn");
|
||||||
|
var renameBtn = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ConflictRenameBtn");
|
||||||
|
var replaceBtn = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ConflictReplaceBtn");
|
||||||
|
|
||||||
|
Assert.NotNull(skipBtn);
|
||||||
|
Assert.NotNull(renameBtn);
|
||||||
|
Assert.NotNull(replaceBtn);
|
||||||
|
|
||||||
|
Assert.Equal("건너뛰기", skipBtn.Attribute("Content")?.Value);
|
||||||
|
Assert.Equal("이름 변경", renameBtn.Attribute("Content")?.Value);
|
||||||
|
Assert.Equal("덮어쓰기", replaceBtn.Attribute("Content")?.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FormatShiftTheme_MustDefine_IslandButtonStyle_And_DoubleBezelStyles()
|
||||||
|
{
|
||||||
|
// high-end-visual-design Section 4:
|
||||||
|
// Island Button (Button-in-Button) 스타일 및 Double-Bezel 카드 쉘 스타일이 선언되어야 한다.
|
||||||
|
var themeFile = Path.Combine(ViewsDir, "FormatShiftTheme.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(themeFile));
|
||||||
|
|
||||||
|
var keys = doc.Descendants()
|
||||||
|
.Select(e => e.Attribute(XName.Get("Key", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value)
|
||||||
|
.Where(k => k != null)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
Assert.Contains("FsIslandPrimaryButtonStyle", keys);
|
||||||
|
Assert.Contains("FsDoubleBezelShellStyle", keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SegmentedTabs_MustUsePureKorean_And_NotContainEnglishRawLabels()
|
||||||
|
{
|
||||||
|
// AGENTS.md Rule 3:
|
||||||
|
// 상단 내비게이션 탭은 영문 'Active Queue', 'Past Results' 대신 '대기열', '변환 기록'을 사용해야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var tabActive = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "TabActiveBtn");
|
||||||
|
var tabPast = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "TabPastBtn");
|
||||||
|
|
||||||
|
Assert.NotNull(tabActive);
|
||||||
|
Assert.NotNull(tabPast);
|
||||||
|
|
||||||
|
var activeText = tabActive.Descendants().Where(e => e.Name.LocalName == "TextBlock").Select(t => t.Attribute("Text")?.Value ?? t.Value).FirstOrDefault();
|
||||||
|
var pastText = tabPast.Descendants().Where(e => e.Name.LocalName == "TextBlock").Select(t => t.Attribute("Text")?.Value ?? t.Value).FirstOrDefault();
|
||||||
|
|
||||||
|
Assert.Equal("대기열", activeText);
|
||||||
|
Assert.Equal("변환 기록", pastText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Sidebar_CombineCheck_MustWrapText_ToPreventClipping()
|
||||||
|
{
|
||||||
|
// Design Audit Invariant 4: 컨테이너 오버플로 및 텍스트 생략 방지
|
||||||
|
// CombineToSingleCheck 내부의 텍스트가 잘리지 않도록 TextBlock에 TextWrapping="Wrap"이 선언되어야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var combineCheck = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "CombineToSingleCheck");
|
||||||
|
Assert.NotNull(combineCheck);
|
||||||
|
|
||||||
|
var childTb = combineCheck.Descendants().FirstOrDefault(e => e.Name.LocalName == "TextBlock");
|
||||||
|
Assert.NotNull(childTb);
|
||||||
|
Assert.Equal("Wrap", childTb.Attribute("TextWrapping")?.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProcessQueueButton_DefaultText_MustBeCompact_ToPreventOverflow()
|
||||||
|
{
|
||||||
|
// 280px 너비의 사이드바 내 Island 버튼(버튼 내 원형 화살표 포함)에서
|
||||||
|
// '대기 중 — 파일을 드래그하여 추가하세요'와 같은 긴 텍스트는 글자 잘림을 유발하므로
|
||||||
|
// '파일을 드래그하여 추가' 등 15자 이하의 정갈한 텍스트를 사용해야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var btn = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ProcessQueueButton");
|
||||||
|
Assert.NotNull(btn);
|
||||||
|
|
||||||
|
var content = btn.Attribute("Content")?.Value;
|
||||||
|
Assert.NotNull(content);
|
||||||
|
Assert.True(content.Length <= 15, $"ProcessQueueButton의 텍스트가 너무 길어 잘림이 발생합니다: '{content}' ({content.Length}자)");
|
||||||
|
Assert.Equal("파일을 드래그하여 추가", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Sidebar_AiTaskAndEncodingCards_MustUseDoubleBezelShell_ForUniformStyle()
|
||||||
|
{
|
||||||
|
// high-end-visual-design Section 4:
|
||||||
|
// 사이드바의 AI 작업 및 상세 인코딩 설정 카드도 FsDoubleBezelShellStyle로 통일되어 일관된 머신 하드웨어 룩을 완성해야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var aiPanel = doc.Descendants().FirstOrDefault(d => d.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "AiTaskPanel");
|
||||||
|
Assert.NotNull(aiPanel);
|
||||||
|
|
||||||
|
var outerBorder = aiPanel.Ancestors().Where(a => a.Name.LocalName == "Border").Skip(1).FirstOrDefault();
|
||||||
|
Assert.NotNull(outerBorder);
|
||||||
|
Assert.Equal("{StaticResource FsDoubleBezelShellStyle}", outerBorder.Attribute("Style")?.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ActiveQueueList_StateText_MustBindToDisplayStateText_ForKoreanLocalization()
|
||||||
|
{
|
||||||
|
// AGENTS.md Rule 3:
|
||||||
|
// 대기열 목록의 상태 표시는 날것의 영문 'queued', 'done' 대신
|
||||||
|
// 한국어 '대기 중', '변환 완료'를 제공하는 DisplayStateText에 바인딩되어야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var queueList = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ActiveQueueList");
|
||||||
|
Assert.NotNull(queueList);
|
||||||
|
|
||||||
|
var boundTexts = queueList.Descendants().Where(e => e.Name.LocalName == "TextBlock")
|
||||||
|
.Select(t => t.Attribute("Text")?.Value)
|
||||||
|
.Where(v => v != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.Contains("{Binding DisplayStateText}", boundTexts);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_Title_MustBe_Everything2Everything_And_NotContainFormatShiftUtility()
|
||||||
|
{
|
||||||
|
// 윈도우 타이틀은 레거시 'FormatShift Utility'가 아니라 공식 브랜드명 'Everything2Everything'이어야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var root = doc.Root;
|
||||||
|
Assert.NotNull(root);
|
||||||
|
|
||||||
|
var title = root.Attribute("Title")?.Value;
|
||||||
|
Assert.Equal("Everything2Everything", title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DropHintOverlay_MustUsePureKorean_And_NotContainEnglishRawLabels()
|
||||||
|
{
|
||||||
|
// 드래그앤드롭 오버레이 안내 텍스트는 영문 날것 'Drop to add to queue' 대신 정제된 한국어 '파일을 놓아 대기열에 추가'여야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var overlay = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "DropHintOverlay");
|
||||||
|
Assert.NotNull(overlay);
|
||||||
|
|
||||||
|
var texts = overlay.Descendants().Where(e => e.Name.LocalName == "TextBlock")
|
||||||
|
.Select(t => t.Attribute("Text")?.Value)
|
||||||
|
.Where(v => v != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.DoesNotContain(texts, t => t!.Contains("Drop to add to queue", StringComparison.OrdinalIgnoreCase));
|
||||||
|
Assert.Contains(texts, t => t!.Contains("대기열"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SmartConversionDeck_MustDirectlyContain_AdvancedOptionsExpander_Below_Presets()
|
||||||
|
{
|
||||||
|
// Fluent 2 점진적 공개(Progressive Disclosure) 및 사용자 피드백 원칙:
|
||||||
|
// 상세 설정 폴드아웃(AdvancedOptionsExpander)은 저장 위치나 AI 작업 카드의 한참 아래가 아니라,
|
||||||
|
// 프리셋 선택 즉시 펼쳐서 미세조정할 수 있도록 '스마트 변환 덱'(SmartPresetCombo가 있는 첫 번째 카드) 내부에 직결되어야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var presetCombo = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "SmartPresetCombo");
|
||||||
|
Assert.NotNull(presetCombo);
|
||||||
|
|
||||||
|
var expander = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "AdvancedOptionsExpander");
|
||||||
|
Assert.NotNull(expander);
|
||||||
|
|
||||||
|
// SmartPresetCombo의 가장 가까운 Border 부모 (스마트 변환 덱 카드)
|
||||||
|
var deckCard = presetCombo.Ancestors().FirstOrDefault(a => a.Name.LocalName == "Border" && (a.Attribute("Style")?.Value?.Contains("FsCardStyle") == true || a.Attribute("Style")?.Value?.Contains("FsDoubleBezelShellStyle") == true));
|
||||||
|
Assert.NotNull(deckCard);
|
||||||
|
|
||||||
|
// AdvancedOptionsExpander가 바로 그 스마트 변환 덱 카드(deckCard)의 자손이어야 한다.
|
||||||
|
var isInsideDeck = deckCard.Descendants().Any(d => d == expander);
|
||||||
|
Assert.True(isInsideDeck, "AdvancedOptionsExpander는 스마트 프리셋 바로 아래에서 펼쳐질 수 있도록 스마트 변환 덱(deckCard) 내부에 위치해야 합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SmartConversionDeck_MustContain_AllQuickPanels_For_MediaTypes()
|
||||||
|
{
|
||||||
|
// 사용자 피드백: 프리셋만 퉁쳐져 있지 않고 이미지 품질, 비디오 CRF, 오디오 비트레이트, PDF 압축률 등
|
||||||
|
// 핵심 슬라이더와 선택기가 스마트 변환 덱에 직관적으로 배치되어야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var requiredPanels = new[] { "QualityPanel", "VideoQuickPanel", "AudioQuickPanel", "PdfQuickPanel" };
|
||||||
|
foreach (var panelName in requiredPanels)
|
||||||
|
{
|
||||||
|
var panel = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == panelName);
|
||||||
|
Assert.NotNull(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 각 패널에 연결된 컨트롤 및 슬라이더 확인
|
||||||
|
var qualitySlider = doc.Descendants().FirstOrDefault(e => e.Attribute("AutomationProperties.AutomationId")?.Value == "QualitySlider");
|
||||||
|
Assert.NotNull(qualitySlider);
|
||||||
|
|
||||||
|
var videoSlider = doc.Descendants().FirstOrDefault(e => e.Attribute("AutomationProperties.AutomationId")?.Value == "VideoCrfSlider");
|
||||||
|
Assert.NotNull(videoSlider);
|
||||||
|
|
||||||
|
var audioCombo = doc.Descendants().FirstOrDefault(e => e.Attribute("AutomationProperties.AutomationId")?.Value == "AudioBitrateQuickCombo");
|
||||||
|
Assert.NotNull(audioCombo);
|
||||||
|
|
||||||
|
var pdfCombo = doc.Descendants().FirstOrDefault(e => e.Attribute("AutomationProperties.AutomationId")?.Value == "PdfCompressQuickCombo");
|
||||||
|
Assert.NotNull(pdfCombo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PastResultsEmpty_MustUseDoubleBezelShell_And_PureKoreanTerminology()
|
||||||
|
{
|
||||||
|
// 변환 기록 빈 상태도 FsDoubleBezelShellStyle을 사용하여 DropZoneEmpty와 일관된 머신 룩을 제공해야 하며,
|
||||||
|
// 버튼 텍스트는 '큐'가 아닌 '대기열' 표준 한국어 용어를 사용해야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var emptyContainer = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PastResultsEmpty");
|
||||||
|
Assert.NotNull(emptyContainer);
|
||||||
|
|
||||||
|
var bezelShell = emptyContainer.Descendants().FirstOrDefault(e => e.Name.LocalName == "Border" && e.Attribute("Style")?.Value == "{StaticResource FsDoubleBezelShellStyle}");
|
||||||
|
Assert.NotNull(bezelShell);
|
||||||
|
|
||||||
|
var texts = emptyContainer.Descendants().Where(e => e.Name.LocalName == "TextBlock")
|
||||||
|
.Select(t => t.Attribute("Text")?.Value)
|
||||||
|
.Where(v => v != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.DoesNotContain(texts, t => t!.Contains("큐로"));
|
||||||
|
Assert.Contains(texts, t => t!.Contains("대기열로"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SettingsWindow_Cards_MustUseDoubleBezelShell_ForUniformStyle()
|
||||||
|
{
|
||||||
|
// SettingsWindow의 AI 카드 및 외부 도구 카드도 FsDoubleBezelShellStyle 및 FsDoubleBezelCoreStyle을 사용하여
|
||||||
|
// 앱 전체와 동일한 스위스 미니멀 이중 베젤 아키텍처를 준수해야 한다.
|
||||||
|
var settingsFile = Path.Combine(ViewsDir, "SettingsWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(settingsFile));
|
||||||
|
|
||||||
|
var shells = doc.Descendants().Where(e => e.Name.LocalName == "Border" && e.Attribute("Style")?.Value == "{StaticResource FsDoubleBezelShellStyle}").ToList();
|
||||||
|
Assert.True(shells.Count >= 2, $"SettingsWindow는 최소 2개 이상의 이중 베젤 쉘 카드를 포함해야 합니다. (발견된 수: {shells.Count})");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Sidebar_AllCards_MustUseDoubleBezelShell_ForUnifiedAesthetic()
|
||||||
|
{
|
||||||
|
// high-end-visual-design Section 4 & stitch-design-taste:
|
||||||
|
// 사이드바의 모든 덱(스마트 변환 덱, 저장 위치 및 충돌 해결 덱, AI 작업 덱, 통계 덱)은
|
||||||
|
// 단일 보더 플랫 카드(FsCardStyle)가 아닌 이중 베젤(FsDoubleBezelShellStyle + FsDoubleBezelCoreStyle)로 통일되어야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var sidebarScroll = doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "ScrollViewer" && e.Ancestors().Any(a => a.Name.LocalName == "Border" && a.Attribute(XName.Get("Column", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "0" || a.Attribute("Grid.Column")?.Value == "0"));
|
||||||
|
Assert.NotNull(sidebarScroll);
|
||||||
|
|
||||||
|
var stackPanel = sidebarScroll.Descendants().FirstOrDefault(e => e.Name.LocalName == "StackPanel");
|
||||||
|
Assert.NotNull(stackPanel);
|
||||||
|
|
||||||
|
// stackPanel의 직계 자식 카드들
|
||||||
|
var directChildCards = stackPanel.Elements().Where(e => e.Name.LocalName == "Border").ToList();
|
||||||
|
Assert.True(directChildCards.Count >= 4, $"사이드바에는 최소 4개 이상의 카드가 있어야 합니다. (발견된 수: {directChildCards.Count})");
|
||||||
|
|
||||||
|
foreach (var card in directChildCards)
|
||||||
|
{
|
||||||
|
var style = card.Attribute("Style")?.Value;
|
||||||
|
Assert.Equal("{StaticResource FsDoubleBezelShellStyle}", style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreviewPane_MustUsePureKorean_And_NotContainEnglishLabels()
|
||||||
|
{
|
||||||
|
// AGENTS.md 규약 3 & stitch-design-taste:
|
||||||
|
// 우측 미리보기 인스펙터 패널의 헤더, 빈 상태 안내문구, 메타데이터 레이블은
|
||||||
|
// 날것의 영어(PREVIEW, No selection, Active Queue, Format, Size, Dimensions, Pages) 대신
|
||||||
|
// 정갈하고 직관적인 한국어(미리보기, 선택된 항목 없음, 대기열, 형식, 크기, 해상도 등)를 사용해야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var textBlocks = doc.Descendants().Where(e => e.Name.LocalName == "TextBlock").ToList();
|
||||||
|
var allTexts = textBlocks.Select(t => t.Attribute("Text")?.Value).Where(v => v != null).ToList();
|
||||||
|
|
||||||
|
// 영어 레이블 금지
|
||||||
|
Assert.DoesNotContain(allTexts, t => t == "PREVIEW");
|
||||||
|
Assert.DoesNotContain(allTexts, t => t == "No selection");
|
||||||
|
Assert.DoesNotContain(allTexts, t => t != null && t.Contains("Active Queue 항목을 클릭하면"));
|
||||||
|
Assert.DoesNotContain(allTexts, t => t == "Format");
|
||||||
|
Assert.DoesNotContain(allTexts, t => t == "Size");
|
||||||
|
Assert.DoesNotContain(allTexts, t => t == "Dimensions");
|
||||||
|
Assert.DoesNotContain(allTexts, t => t == "Pages");
|
||||||
|
|
||||||
|
// 한국어 레이블 필수
|
||||||
|
Assert.Contains(allTexts, t => t == "선택된 항목 없음");
|
||||||
|
Assert.Contains(allTexts, t => t == "형식");
|
||||||
|
Assert.Contains(allTexts, t => t == "크기");
|
||||||
|
Assert.Contains(allTexts, t => t != null && t.Contains("해상도"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DropZoneEmpty_MustUseDoubleBezelShell_ForLinearTierMachinedLook()
|
||||||
|
{
|
||||||
|
// high-end-visual-design Section 4 (Double-Bezel):
|
||||||
|
// 중앙 메인 빈 상태 드롭존(DropZoneEmpty)도 단일 대시 사각형이 아닌
|
||||||
|
// FsDoubleBezelShellStyle + FsDoubleBezelCoreStyle 이중 베젤 쉘 아키텍처로 구현되어
|
||||||
|
// PastResultsEmpty 및 사이드바 덱들과 완벽한 하드웨어 머신 룩 통일감을 형성해야 한다.
|
||||||
|
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(mainFile));
|
||||||
|
|
||||||
|
var dropZone = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "DropZoneEmpty");
|
||||||
|
Assert.NotNull(dropZone);
|
||||||
|
|
||||||
|
var bezelShell = dropZone.Descendants().FirstOrDefault(e => e.Name.LocalName == "Border" && e.Attribute("Style")?.Value == "{StaticResource FsDoubleBezelShellStyle}");
|
||||||
|
Assert.NotNull(bezelShell);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_CodeBehind_DateLabels_MustUseKorean_And_NotContainEnglishTodayOrYesterday()
|
||||||
|
{
|
||||||
|
// AGENTS.md 규약 3 & stitch-design-taste:
|
||||||
|
// 변환 기록 날짜 헤더(DateTitle)는 영문 'Today', 'Yesterday' 대신 정제된 한국어 '오늘', '어제'를 사용해야 한다.
|
||||||
|
var codeFile = Path.Combine(ViewsDir, "MainWindow.xaml.cs");
|
||||||
|
var code = File.ReadAllText(codeFile);
|
||||||
|
|
||||||
|
Assert.DoesNotContain("\"Today\"", code);
|
||||||
|
Assert.DoesNotContain("\"Yesterday\"", code);
|
||||||
|
Assert.Contains("\"오늘", code);
|
||||||
|
Assert.Contains("\"어제", code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("MainWindow.xaml")]
|
||||||
|
[InlineData("SettingsWindow.xaml")]
|
||||||
|
[InlineData("DiagnoseWindow.xaml")]
|
||||||
|
[InlineData("QuickOptionsWindow.xaml")]
|
||||||
|
[InlineData("QuickProgressWindow.xaml")]
|
||||||
|
public void AllWindows_MustInclude_UiControlsDictionary_InResources(string xamlName)
|
||||||
|
{
|
||||||
|
// Wpf.Ui Fluent 2 컨트롤(ComboBox, Slider, Button 등)의 다크 테마 룩앤필이
|
||||||
|
// 독립 실행 및 In-Memory 렌더링 시에도 100% 보장되도록 각 윈도우의 MergedDictionaries는
|
||||||
|
// <ui:ControlsDictionary/>를 필수로 포함해야 한다.
|
||||||
|
var file = Path.Combine(ViewsDir, xamlName);
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||||
|
|
||||||
|
var hasControlsDict = doc.Descendants().Any(e => e.Name.LocalName == "ControlsDictionary");
|
||||||
|
Assert.True(hasControlsDict, $"{xamlName}에 <ui:ControlsDictionary/>가 선언되어 있지 않습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_CodeBehind_MustNotContain_RawEnglish_PastResults()
|
||||||
|
{
|
||||||
|
// AGENTS.md 규약 3 & stitch-design-taste:
|
||||||
|
// 메시지 박스 및 다이얼로그 안내 문구에서도 'Past Results' 날것의 영어를 금지하고
|
||||||
|
// '변환 기록' 표준 한국어로 안내해야 한다.
|
||||||
|
var codeFile = Path.Combine(ViewsDir, "MainWindow.xaml.cs");
|
||||||
|
var code = File.ReadAllText(codeFile);
|
||||||
|
|
||||||
|
Assert.DoesNotContain("\"Past Results", code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PastResults_MetaLine_MustHave_TextTrimmingCharacterEllipsis_ToPreventOverflow()
|
||||||
|
{
|
||||||
|
// AGENTS.md Rule 4 (Overflow Safety):
|
||||||
|
// 긴 파일 메타데이터(타임스탬프 + 파일수)를 표시하는 TextBlock은
|
||||||
|
// 좁은 컬럼 폭에서도 오버플로/글리프 잘림 없이 안전하게 축약되어야 한다.
|
||||||
|
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||||
|
|
||||||
|
var metaTextBlocks = doc.Descendants()
|
||||||
|
.Where(e => e.Name.LocalName == "TextBlock" &&
|
||||||
|
e.Attribute("Text")?.Value == "{Binding MetaLine}")
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.NotEmpty(metaTextBlocks);
|
||||||
|
foreach (var tb in metaTextBlocks)
|
||||||
|
{
|
||||||
|
var trimming = tb.Attribute("TextTrimming")?.Value;
|
||||||
|
Assert.True(trimming == "CharacterEllipsis",
|
||||||
|
"MetaLine TextBlock은 TextTrimming=\"CharacterEllipsis\"를 지정하여 오버플로를 방지해야 합니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_SearchAndFilterToolbar_MustBeScopedToListColumn()
|
||||||
|
{
|
||||||
|
// 검색 및 카테고리 필터 바는 미리보기(Inspector) 위로 어색하게 500px 걸쳐있지 않고,
|
||||||
|
// 대상이 되는 대기열/기록 리스트 컬럼(Grid.Column="0") 내부에 정렬되어야 한다.
|
||||||
|
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||||
|
|
||||||
|
var searchBox = doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "TextBox" &&
|
||||||
|
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "SearchBox");
|
||||||
|
Assert.NotNull(searchBox);
|
||||||
|
|
||||||
|
// SearchBox를 감싸는 툴바 Border 찾기
|
||||||
|
var toolbarBorder = searchBox.Ancestors().FirstOrDefault(e => e.Name.LocalName == "Border" &&
|
||||||
|
e.Attribute("Grid.Row")?.Value == "0");
|
||||||
|
Assert.NotNull(toolbarBorder);
|
||||||
|
|
||||||
|
var colSpan = toolbarBorder.Attribute("Grid.ColumnSpan")?.Value;
|
||||||
|
Assert.True(string.IsNullOrEmpty(colSpan) || colSpan == "1",
|
||||||
|
"Search/Filter Toolbar는 Inspector 컬럼을 침범하는 ColumnSpan=\"2\"가 아니어야 하며, 리스트 컬럼(Column 0)에 정합되어야 합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ActiveQueueList_Columns_MustProvideAdequateWidthForMetadata()
|
||||||
|
{
|
||||||
|
// 대기열 아이템 행 그리드에서 파일 크기 및 상태 컬럼이 과도하게 너비를 차지하여
|
||||||
|
// 파일명과 메타데이터 문자열이 중간에 잘리지 않도록 컬럼 너비를 정밀 최적화해야 한다.
|
||||||
|
// SizeText 컬럼 <= 80px, DisplayStateText 컬럼 <= 90px
|
||||||
|
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||||
|
|
||||||
|
var queueList = doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "ItemsControl" &&
|
||||||
|
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ActiveQueueList");
|
||||||
|
Assert.NotNull(queueList);
|
||||||
|
|
||||||
|
var colDefs = queueList.Descendants()
|
||||||
|
.Where(e => e.Name.LocalName == "Grid.ColumnDefinitions" && e.Elements().Count() >= 6)
|
||||||
|
.FirstOrDefault();
|
||||||
|
Assert.NotNull(colDefs);
|
||||||
|
|
||||||
|
var widths = colDefs.Elements().Select(c => c.Attribute("Width")?.Value).ToList();
|
||||||
|
Assert.True(widths.Count >= 6, "ActiveQueueList 행 그리드는 최소 6개 컬럼(선택, 아이콘, 본문*, 크기, 상태, 삭제)이어야 합니다.");
|
||||||
|
|
||||||
|
// Column 3: SizeText width
|
||||||
|
var sizeWidth = widths[3];
|
||||||
|
Assert.True(int.TryParse(sizeWidth, out var sw) && sw <= 80,
|
||||||
|
$"SizeText 컬럼 폭은 80px 이하여야 메타데이터 컬럼이 오버플로되지 않습니다. 현재: {sizeWidth}");
|
||||||
|
|
||||||
|
// Column 4: StateText width
|
||||||
|
var stateWidth = widths[4];
|
||||||
|
Assert.True(int.TryParse(stateWidth, out var stw) && stw <= 90,
|
||||||
|
$"DisplayStateText 컬럼 폭은 90px 이하여야 메타데이터 컬럼이 오버플로되지 않습니다. 현재: {stateWidth}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Inspector_PreviewArea_MustUse_DoubleBezelArchitecture()
|
||||||
|
{
|
||||||
|
// 미리보기 뷰포트는 단순 평면 박스가 아니라 고품격 machined 하드웨어 느낌의
|
||||||
|
// FsDoubleBezelShellStyle 및 FsDoubleBezelCoreStyle 이중 베젤(Doppelrand) 구조를 가져야 한다.
|
||||||
|
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||||
|
|
||||||
|
var previewArea = doc.Descendants().FirstOrDefault(e =>
|
||||||
|
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PreviewImageArea");
|
||||||
|
Assert.NotNull(previewArea);
|
||||||
|
|
||||||
|
var hasShell = previewArea.Descendants().Any(e =>
|
||||||
|
e.Attribute("Style")?.Value.Contains("FsDoubleBezelShellStyle") == true);
|
||||||
|
var hasCore = previewArea.Descendants().Any(e =>
|
||||||
|
e.Attribute("Style")?.Value.Contains("FsDoubleBezelCoreStyle") == true);
|
||||||
|
|
||||||
|
Assert.True(hasShell && hasCore,
|
||||||
|
"PreviewImageArea는 고품격 Fluent 2 미학을 위해 FsDoubleBezelShellStyle 및 FsDoubleBezelCoreStyle 이중 베젤 구조를 갖추어야 합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_SearchBox_Width_MustLeaveAdequateRoomForFilterButtons()
|
||||||
|
{
|
||||||
|
// 1080p 및 컴팩트 뷰포트에서도 '전체' 및 카테고리 필터 버튼들이
|
||||||
|
// 검색창에 가려지거나 겹치지 않도록 검색창 너비는 180px 이하여야 한다.
|
||||||
|
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||||
|
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||||
|
|
||||||
|
var searchBox = doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "TextBox" &&
|
||||||
|
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "SearchBox");
|
||||||
|
Assert.NotNull(searchBox);
|
||||||
|
|
||||||
|
var toolbarGrid = searchBox.Ancestors()
|
||||||
|
.FirstOrDefault(e => e.Name.LocalName == "Grid" && e.Element(searchBox.Name.Namespace + "Grid.ColumnDefinitions") != null);
|
||||||
|
Assert.NotNull(toolbarGrid);
|
||||||
|
|
||||||
|
var searchBoxCol = toolbarGrid.Element(searchBox.Name.Namespace + "Grid.ColumnDefinitions")?
|
||||||
|
.Elements().FirstOrDefault();
|
||||||
|
Assert.NotNull(searchBoxCol);
|
||||||
|
|
||||||
|
var widthVal = searchBoxCol.Attribute("Width")?.Value;
|
||||||
|
Assert.True(int.TryParse(widthVal, out var w) && w <= 180,
|
||||||
|
$"SearchBox 컬럼 폭은 180px 이하여야 필터 버튼들이 잘리지 않습니다. 현재: {widthVal}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,167 @@ public class DesignAuditVisualTreeTests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_WhenQueueIsEmpty_ShowsDefaultFormat_AndQualitySlider_AndAdvancedPanel_Visible()
|
||||||
|
{
|
||||||
|
// Zero-Void 원칙 및 점진적 공개 원칙: 큐가 비어있더라도 사용자가 변환 옵션과 품질 슬라이더를 사전에 확인하고 조작할 수 있도록
|
||||||
|
// 기본 출력 포맷(.jpg)과 품질 슬라이더, 해당 포맷의 상세 인코딩 서브패널이 Visible 이어야 한다.
|
||||||
|
RunOnSta(() =>
|
||||||
|
{
|
||||||
|
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||||
|
var settings = new FakeSettingsStore();
|
||||||
|
var window = new MainWindow(engine, settings);
|
||||||
|
|
||||||
|
var content = (UIElement)window.Content;
|
||||||
|
content.Measure(new Size(1280, 960));
|
||||||
|
content.Arrange(new Rect(0, 0, 1280, 960));
|
||||||
|
|
||||||
|
var formatCombo = (ComboBox)window.FindName("OutputFormatCombo");
|
||||||
|
var qualityPanel = (StackPanel)window.FindName("QualityPanel");
|
||||||
|
var advancedImagePanel = (StackPanel)window.FindName("AdvancedImagePanel");
|
||||||
|
|
||||||
|
Assert.NotNull(formatCombo);
|
||||||
|
Assert.NotNull(qualityPanel);
|
||||||
|
Assert.NotNull(advancedImagePanel);
|
||||||
|
|
||||||
|
// 큐가 비어있어도 지원 가능한 전체 포맷이 채워져 있어야 한다.
|
||||||
|
Assert.True(formatCombo.Items.Count > 0, "큐가 비어있어도 전체 지원 포맷이 표시되어야 합니다.");
|
||||||
|
// 기본 포맷(.jpg)에 맞춰 품질 슬라이더 패널이 표시되어야 한다.
|
||||||
|
Assert.Equal(Visibility.Visible, qualityPanel.Visibility);
|
||||||
|
// 기본 포맷에 맞춰 상세 설정 패널이 표시되어야 한다.
|
||||||
|
Assert.Equal(Visibility.Visible, advancedImagePanel.Visibility);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_FormatSwitching_UpdatesQuickPanels_And_AdvancedPanels_Correctly()
|
||||||
|
{
|
||||||
|
// 사용자 피드백 대응: 스마트 프리셋 직하단에서 각 미디어 형식(.mp4, .pdf, .mp3, .webp)에 따라
|
||||||
|
// 전용 퀵 컨트롤(품질/CRF/비트레이트/PDF압축)과 상세 폴드아웃 서브패널이 동적으로 즉시 표시되어야 한다.
|
||||||
|
RunOnSta(() =>
|
||||||
|
{
|
||||||
|
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||||
|
var settings = new FakeSettingsStore();
|
||||||
|
var window = new MainWindow(engine, settings);
|
||||||
|
|
||||||
|
var content = (UIElement)window.Content;
|
||||||
|
content.Measure(new Size(1280, 960));
|
||||||
|
content.Arrange(new Rect(0, 0, 1280, 960));
|
||||||
|
|
||||||
|
var formatCombo = (ComboBox)window.FindName("OutputFormatCombo");
|
||||||
|
var qualityPanel = (StackPanel)window.FindName("QualityPanel");
|
||||||
|
var videoQuickPanel = (StackPanel)window.FindName("VideoQuickPanel");
|
||||||
|
var audioQuickPanel = (StackPanel)window.FindName("AudioQuickPanel");
|
||||||
|
var pdfQuickPanel = (StackPanel)window.FindName("PdfQuickPanel");
|
||||||
|
var expander = (Expander)window.FindName("AdvancedOptionsExpander");
|
||||||
|
var advVideo = (StackPanel)window.FindName("AdvancedVideoPanel");
|
||||||
|
var advAudio = (StackPanel)window.FindName("AdvancedAudioPanel");
|
||||||
|
var advPdf = (StackPanel)window.FindName("AdvancedPdfPanel");
|
||||||
|
var advImage = (StackPanel)window.FindName("AdvancedImagePanel");
|
||||||
|
|
||||||
|
Assert.NotNull(formatCombo);
|
||||||
|
Assert.NotNull(qualityPanel);
|
||||||
|
Assert.NotNull(videoQuickPanel);
|
||||||
|
Assert.NotNull(audioQuickPanel);
|
||||||
|
Assert.NotNull(pdfQuickPanel);
|
||||||
|
Assert.NotNull(expander);
|
||||||
|
|
||||||
|
// 기본은 열려있어야 함 (사용자가 즉시 확인 가능)
|
||||||
|
Assert.True(expander.IsExpanded);
|
||||||
|
|
||||||
|
void SelectExtension(string ext)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < formatCombo.Items.Count; i++)
|
||||||
|
{
|
||||||
|
if (formatCombo.Items[i] is ComboBoxItem item && (string)item.Tag == ext)
|
||||||
|
{
|
||||||
|
formatCombo.SelectedIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 영상 (.mp4) 전환 검증
|
||||||
|
SelectExtension(".mp4");
|
||||||
|
Assert.Equal(Visibility.Collapsed, qualityPanel.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, videoQuickPanel.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, advVideo.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, advAudio.Visibility);
|
||||||
|
Assert.Equal(Visibility.Collapsed, advPdf.Visibility);
|
||||||
|
|
||||||
|
// 2. PDF (.pdf) 전환 검증
|
||||||
|
SelectExtension(".pdf");
|
||||||
|
Assert.Equal(Visibility.Collapsed, qualityPanel.Visibility);
|
||||||
|
Assert.Equal(Visibility.Collapsed, videoQuickPanel.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, pdfQuickPanel.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, advPdf.Visibility);
|
||||||
|
Assert.Equal(Visibility.Collapsed, advVideo.Visibility);
|
||||||
|
|
||||||
|
// 3. 오디오 (.mp3) 전환 검증
|
||||||
|
SelectExtension(".mp3");
|
||||||
|
Assert.Equal(Visibility.Collapsed, qualityPanel.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, audioQuickPanel.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, advAudio.Visibility);
|
||||||
|
Assert.Equal(Visibility.Collapsed, advVideo.Visibility);
|
||||||
|
|
||||||
|
// 4. 이미지 (.webp) 전환 검증
|
||||||
|
SelectExtension(".webp");
|
||||||
|
Assert.Equal(Visibility.Visible, qualityPanel.Visibility);
|
||||||
|
Assert.Equal(Visibility.Collapsed, videoQuickPanel.Visibility);
|
||||||
|
Assert.Equal(Visibility.Collapsed, audioQuickPanel.Visibility);
|
||||||
|
Assert.Equal(Visibility.Collapsed, pdfQuickPanel.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, advImage.Visibility);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_RenderToBitmap_SavesVisualVerificationArtifact()
|
||||||
|
{
|
||||||
|
RunOnSta(() =>
|
||||||
|
{
|
||||||
|
var dir = System.IO.Directory.GetCurrentDirectory();
|
||||||
|
while (dir != null && !System.IO.File.Exists(System.IO.Path.Combine(dir, "Everything2Everything.slnx")))
|
||||||
|
{
|
||||||
|
dir = System.IO.Directory.GetParent(dir)?.FullName;
|
||||||
|
}
|
||||||
|
var root = dir ?? throw new System.IO.DirectoryNotFoundException("솔루션 루트를 찾을 수 없습니다.");
|
||||||
|
|
||||||
|
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||||
|
var settings = new FakeSettingsStore();
|
||||||
|
var testFiles = new List<string>
|
||||||
|
{
|
||||||
|
System.IO.Path.Combine(root, "test_assets", "test_icon.png"),
|
||||||
|
System.IO.Path.Combine(root, "test_assets", "test_art.png")
|
||||||
|
};
|
||||||
|
|
||||||
|
var window = new MainWindow(engine, settings, testFiles);
|
||||||
|
window.ApplyTemplate();
|
||||||
|
var expander = (Expander)window.FindName("AdvancedOptionsExpander");
|
||||||
|
expander.ApplyTemplate();
|
||||||
|
|
||||||
|
var content = (UIElement)window.Content;
|
||||||
|
content.Measure(new Size(1280, 960));
|
||||||
|
content.Arrange(new Rect(0, 0, 1280, 960));
|
||||||
|
content.UpdateLayout();
|
||||||
|
|
||||||
|
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, PixelFormats.Pbgra32);
|
||||||
|
rtb.Render(content);
|
||||||
|
|
||||||
|
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||||||
|
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtb));
|
||||||
|
|
||||||
|
var outDir = @"C:\Users\encep\.gemini\antigravity\brain\a5abaf02-dd4b-45f7-8890-144e9da36bcc";
|
||||||
|
var outPath = System.IO.Path.Combine(outDir, "app_rendered_preview.png");
|
||||||
|
using (var fs = new System.IO.FileStream(outPath, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.ReadWrite))
|
||||||
|
{
|
||||||
|
encoder.Save(fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(System.IO.File.Exists(outPath));
|
||||||
|
Assert.True(new System.IO.FileInfo(outPath).Length > 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static IEnumerable<T> FindLogicalChildren<T>(object parent) where T : DependencyObject
|
private static IEnumerable<T> FindLogicalChildren<T>(object parent) where T : DependencyObject
|
||||||
{
|
{
|
||||||
if (parent is ContentControl cc && cc.Content != null)
|
if (parent is ContentControl cc && cc.Content != null)
|
||||||
|
|
|
||||||
|
|
@ -60,4 +60,24 @@ public class FileInspectorTests
|
||||||
var info = FileInspectorBuilder.Build(dummyPath);
|
var info = FileInspectorBuilder.Build(dummyPath);
|
||||||
Assert.Equal(expectedCategory, info.Category);
|
Assert.Equal(expectedCategory, info.Category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(FilterCategory.All, "전체")]
|
||||||
|
[InlineData(FilterCategory.Image, "이미지")]
|
||||||
|
[InlineData(FilterCategory.Document, "문서")]
|
||||||
|
[InlineData(FilterCategory.Media, "미디어")]
|
||||||
|
[InlineData(FilterCategory.Data, "데이터")]
|
||||||
|
public void ToKoreanLabel_ReturnsRefinedKoreanCategory(FilterCategory category, string expectedLabel)
|
||||||
|
{
|
||||||
|
var label = category.ToKoreanLabel();
|
||||||
|
Assert.Equal(expectedLabel, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_DimensionsOrMeta_UsesKoreanCategory()
|
||||||
|
{
|
||||||
|
var dummyPath = "C:\\test\\sample.png";
|
||||||
|
var info = FileInspectorBuilder.Build(dummyPath);
|
||||||
|
Assert.Equal("PNG · 이미지", info.DimensionsOrMeta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public class FormatPresetEngineTests
|
||||||
Assert.True(presets.Count >= 3);
|
Assert.True(presets.Count >= 3);
|
||||||
|
|
||||||
var q85 = presets.First(p => p.Title.Contains("웹 고화질"));
|
var q85 = presets.First(p => p.Title.Contains("웹 고화질"));
|
||||||
Assert.Contains("Quality 85", q85.SpecChips);
|
Assert.Contains("품질 85", q85.SpecChips);
|
||||||
|
|
||||||
var options = new OptionsViewModel();
|
var options = new OptionsViewModel();
|
||||||
q85.Apply(options);
|
q85.Apply(options);
|
||||||
|
|
@ -42,6 +42,23 @@ public class FormatPresetEngineTests
|
||||||
Assert.True(options.StripMetadata);
|
Assert.True(options.StripMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(".jpg", "품질 95")]
|
||||||
|
[InlineData(".webp", "품질 85")]
|
||||||
|
[InlineData(".avif", "품질 55")]
|
||||||
|
public void GetPresetsForExtension_ImagePresets_MustUseKoreanQualityChips(string ext, string expectedChip)
|
||||||
|
{
|
||||||
|
var presets = FormatPresetEngine.GetPresetsForExtension(ext);
|
||||||
|
var hasExpectedChip = presets.Any(p => p.SpecChips.Contains(expectedChip));
|
||||||
|
Assert.True(hasExpectedChip, $"{ext} 프리셋 스펙 칩에 한국어 '{expectedChip}'이 포함되어야 합니다.");
|
||||||
|
|
||||||
|
// 영어 Quality X 칩 금지
|
||||||
|
foreach (var p in presets)
|
||||||
|
{
|
||||||
|
Assert.DoesNotContain(p.SpecChips, c => c.StartsWith("Quality "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetPresetsForExtension_Pdf_ReturnsResolutionPresets()
|
public void GetPresetsForExtension_Pdf_ReturnsResolutionPresets()
|
||||||
{
|
{
|
||||||
|
|
@ -71,6 +88,30 @@ public class FormatPresetEngineTests
|
||||||
Assert.Equal("fast", options.VideoPreset);
|
Assert.Equal("fast", options.VideoPreset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(".jpg", 4)]
|
||||||
|
[InlineData(".png", 3)]
|
||||||
|
[InlineData(".webp", 4)]
|
||||||
|
[InlineData(".avif", 3)]
|
||||||
|
[InlineData(".pdf", 4)]
|
||||||
|
[InlineData(".mp4", 4)]
|
||||||
|
[InlineData(".mp3", 4)]
|
||||||
|
[InlineData(".gif", 2)]
|
||||||
|
public void GetPresetsForExtension_MajorFormats_ReturnRichPresetSuite(string ext, int minPresetCount)
|
||||||
|
{
|
||||||
|
var presets = FormatPresetEngine.GetPresetsForExtension(ext);
|
||||||
|
Assert.True(presets.Count >= minPresetCount,
|
||||||
|
$"{ext} 형식의 프리셋 개수가 너무 적습니다 (최소 {minPresetCount}개 이상 필요, 실제: {presets.Count}개).");
|
||||||
|
|
||||||
|
foreach (var p in presets)
|
||||||
|
{
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(p.Id), "프리셋 ID가 비어있습니다.");
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(p.Title), "프리셋 제목이 비어있습니다.");
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(p.Description), "프리셋 설명이 비어있습니다.");
|
||||||
|
Assert.True(p.SpecChips.Count >= 2, $"프리셋 [{p.Title}]의 스펙 칩 개수가 2개 미만입니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetPresetsForExtension_UnknownExtension_ReturnsFallbackPresets()
|
public void GetPresetsForExtension_UnknownExtension_ReturnsFallbackPresets()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -65,4 +65,26 @@ public class OptionsViewModelTests
|
||||||
Assert.Equal(NameCollision.Skip, o.OnCollision);
|
Assert.Equal(NameCollision.Skip, o.OnCollision);
|
||||||
Assert.False(o.VideoPreferGpu);
|
Assert.False(o.VideoPreferGpu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OptionsViewModel_HasAdvancedProperties_And_DefaultExpandedIsTrue()
|
||||||
|
{
|
||||||
|
// 사용자 피드백 반영: 슬라이더와 상세 옵션을 바로 확인할 수 있도록 기본값으로 펼침(true) 상태여야 한다.
|
||||||
|
var vm = new OptionsViewModel();
|
||||||
|
Assert.True(vm.IsAdvancedExpanded);
|
||||||
|
Assert.Equal(0, vm.PdfCompressLevelIndex);
|
||||||
|
Assert.Equal(1, vm.PdfDpiIndex);
|
||||||
|
Assert.False(vm.ImageLossless);
|
||||||
|
Assert.False(vm.Progressive);
|
||||||
|
|
||||||
|
vm.ImageLossless = true;
|
||||||
|
vm.Progressive = true;
|
||||||
|
vm.PdfCompressLevelIndex = 1;
|
||||||
|
vm.PdfDpiIndex = 2;
|
||||||
|
|
||||||
|
var options = vm.ToConvertOptions();
|
||||||
|
Assert.True(options.Webp.Lossless);
|
||||||
|
Assert.True(options.Jpeg.Progressive);
|
||||||
|
Assert.Equal(300, options.PdfRender.Dpi);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ public class UnifiedTitleBarTests
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 720, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
|
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
|
||||||
rtb.Render(content);
|
rtb.Render(content);
|
||||||
var enc = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
var enc = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||||||
enc.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtb));
|
enc.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtb));
|
||||||
|
|
@ -313,6 +313,306 @@ public class UnifiedTitleBarTests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_PopulatedActiveQueue_DisplaysBatchBarAndItems_AndEnablesLaunchButton()
|
||||||
|
{
|
||||||
|
RunOnSta(() =>
|
||||||
|
{
|
||||||
|
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||||
|
var store = new FakeSettingsStore();
|
||||||
|
var window = new MainWindow(engine, store);
|
||||||
|
|
||||||
|
// Add sample items to ActiveQueue
|
||||||
|
var q1 = new QueueItem
|
||||||
|
{
|
||||||
|
SourcePath = @"C:\Mock\nature_photo.jpg",
|
||||||
|
FileName = "nature_photo.jpg",
|
||||||
|
FormatLabel = "JPG",
|
||||||
|
FormatBrush = System.Windows.Media.Brushes.Coral,
|
||||||
|
SizeText = "4.2 MB",
|
||||||
|
MetaLine = "3840x2160 · sRGB · 24-bit",
|
||||||
|
SourceSizeBytes = 4_404_019,
|
||||||
|
SelectedOutputExtension = ".webp",
|
||||||
|
IsSelected = true
|
||||||
|
};
|
||||||
|
q1.SetState("queued");
|
||||||
|
|
||||||
|
var q2 = new QueueItem
|
||||||
|
{
|
||||||
|
SourcePath = @"C:\Mock\quarterly_report.pdf",
|
||||||
|
FileName = "quarterly_report.pdf",
|
||||||
|
FormatLabel = "PDF",
|
||||||
|
FormatBrush = System.Windows.Media.Brushes.IndianRed,
|
||||||
|
SizeText = "12.8 MB",
|
||||||
|
MetaLine = "32 페이지 · 텍스트/벡터 포함",
|
||||||
|
SourceSizeBytes = 13_421_772,
|
||||||
|
SelectedOutputExtension = ".pdf",
|
||||||
|
IsSelected = false
|
||||||
|
};
|
||||||
|
q2.SetState("50%");
|
||||||
|
|
||||||
|
var q3 = new QueueItem
|
||||||
|
{
|
||||||
|
SourcePath = @"C:\Mock\keynote_presentation.mp4",
|
||||||
|
FileName = "keynote_presentation.mp4",
|
||||||
|
FormatLabel = "MP4",
|
||||||
|
FormatBrush = System.Windows.Media.Brushes.CornflowerBlue,
|
||||||
|
SizeText = "156.4 MB",
|
||||||
|
MetaLine = "1080p60 · H.264 / AAC · 05:22",
|
||||||
|
SourceSizeBytes = 164_000_000,
|
||||||
|
SelectedOutputExtension = ".mp4",
|
||||||
|
IsSelected = false
|
||||||
|
};
|
||||||
|
q3.SetState("done");
|
||||||
|
|
||||||
|
window.ActiveQueue.Add(q1);
|
||||||
|
window.ActiveQueue.Add(q2);
|
||||||
|
window.ActiveQueue.Add(q3);
|
||||||
|
|
||||||
|
// Trigger visibility updates via reflection
|
||||||
|
var updateVisMethod = typeof(MainWindow).GetMethod("UpdateActiveQueueVisibility", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
updateVisMethod?.Invoke(window, null);
|
||||||
|
|
||||||
|
var updateBtnMethod = typeof(MainWindow).GetMethod("UpdateProcessQueueButton", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
updateBtnMethod?.Invoke(window, null);
|
||||||
|
|
||||||
|
var updateBadgesMethod = typeof(MainWindow).GetMethod("UpdateBadges", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
updateBadgesMethod?.Invoke(window, null);
|
||||||
|
|
||||||
|
var setPreviewMetaMethod = typeof(MainWindow).GetMethod("SetPreviewMeta", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
setPreviewMetaMethod?.Invoke(window, new object[] { q1.FileName, q1.SourcePath, q1.FormatLabel, q1.SizeText });
|
||||||
|
|
||||||
|
var showPreviewGlyphMethod = typeof(MainWindow).GetMethod("ShowPreviewGlyph", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
showPreviewGlyphMethod?.Invoke(window, new object[] { ".jpg", "선택된 파일: " + q1.FileName });
|
||||||
|
|
||||||
|
var activeQueueList = (ItemsControl)window.FindName("ActiveQueueList");
|
||||||
|
activeQueueList.ItemsSource = window.ActiveQueue;
|
||||||
|
activeQueueList.ApplyTemplate();
|
||||||
|
|
||||||
|
var content = (UIElement)window.Content;
|
||||||
|
content.Measure(new Size(1280, 960));
|
||||||
|
content.Arrange(new Rect(0, 0, 1280, 960));
|
||||||
|
content.UpdateLayout();
|
||||||
|
|
||||||
|
var dropZoneEmpty = (FrameworkElement)window.FindName("DropZoneEmpty");
|
||||||
|
var activeQueueScroll = (FrameworkElement)window.FindName("ActiveQueueScroll");
|
||||||
|
var batchActionBar = (FrameworkElement)window.FindName("BatchActionBar");
|
||||||
|
var processBtn = (System.Windows.Controls.Button)window.FindName("ProcessQueueButton");
|
||||||
|
|
||||||
|
Assert.NotNull(dropZoneEmpty);
|
||||||
|
Assert.NotNull(activeQueueScroll);
|
||||||
|
Assert.NotNull(batchActionBar);
|
||||||
|
Assert.NotNull(processBtn);
|
||||||
|
|
||||||
|
Assert.Equal(Visibility.Collapsed, dropZoneEmpty.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, activeQueueScroll.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, batchActionBar.Visibility);
|
||||||
|
|
||||||
|
Assert.Equal("변환 시작 (3개 파일)", processBtn.Content);
|
||||||
|
Assert.True(processBtn.IsEnabled);
|
||||||
|
|
||||||
|
// DisplayStateText check on items
|
||||||
|
Assert.Equal("대기 중", q1.DisplayStateText);
|
||||||
|
Assert.Equal("50%", q2.DisplayStateText);
|
||||||
|
Assert.Equal("변환 완료", q3.DisplayStateText);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
|
||||||
|
rtb.Render(content);
|
||||||
|
var enc = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||||||
|
enc.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtb));
|
||||||
|
var outPath = @"C:\Users\encep\.gemini\antigravity\brain\b58fd023-a52b-4f4b-aa23-e6df654aa1fb\.tempmediaStorage\rendered_queue_populated.png";
|
||||||
|
using var fs = File.Create(outPath);
|
||||||
|
enc.Save(fs);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_PastResults_MustUseKorean_And_NotContainEnglishOutputOrSessionSavings()
|
||||||
|
{
|
||||||
|
RunOnSta(() =>
|
||||||
|
{
|
||||||
|
var dg = new DateGroup("오늘 (9월 5일)");
|
||||||
|
Assert.Contains("절감", dg.SessionSavingsText);
|
||||||
|
Assert.DoesNotContain("Session Savings", dg.SessionSavingsText);
|
||||||
|
|
||||||
|
var entry = new HistoryEntry(
|
||||||
|
DateTime.Now,
|
||||||
|
@"C:\photos\sample.png",
|
||||||
|
"png",
|
||||||
|
1024 * 1024,
|
||||||
|
512 * 1024,
|
||||||
|
1,
|
||||||
|
null,
|
||||||
|
ConvertStatus.Success,
|
||||||
|
null,
|
||||||
|
new[] { @"C:\photos\sample.webp" });
|
||||||
|
|
||||||
|
var row = HistoryRow.From(entry);
|
||||||
|
Assert.Contains("개", row.MetaLine);
|
||||||
|
Assert.DoesNotContain("output(s)", row.MetaLine);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_TabCommand_DefaultFallback_MustBeActive()
|
||||||
|
{
|
||||||
|
RunOnSta(() =>
|
||||||
|
{
|
||||||
|
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||||
|
var store = new FakeSettingsStore();
|
||||||
|
var window = new MainWindow(engine, store);
|
||||||
|
var tabActiveBtn = (System.Windows.Controls.Primitives.ToggleButton)window.FindName("TabActiveBtn");
|
||||||
|
var tabPastBtn = (System.Windows.Controls.Primitives.ToggleButton)window.FindName("TabPastBtn");
|
||||||
|
var activeQueueView = (FrameworkElement)window.FindName("ActiveQueueView");
|
||||||
|
var pastResultsContainer = (FrameworkElement)window.FindName("PastResultsContainer");
|
||||||
|
|
||||||
|
// Switch to past first
|
||||||
|
window.TabCommand.Execute("Past");
|
||||||
|
Assert.False(tabActiveBtn.IsChecked);
|
||||||
|
Assert.True(tabPastBtn.IsChecked);
|
||||||
|
Assert.Equal(Visibility.Collapsed, activeQueueView.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, pastResultsContainer.Visibility);
|
||||||
|
|
||||||
|
// Execute with null or empty fallback
|
||||||
|
window.TabCommand.Execute(null);
|
||||||
|
Assert.True(tabActiveBtn.IsChecked);
|
||||||
|
Assert.False(tabPastBtn.IsChecked);
|
||||||
|
Assert.Equal(Visibility.Visible, activeQueueView.Visibility);
|
||||||
|
Assert.Equal(Visibility.Collapsed, pastResultsContainer.Visibility);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_PastResults_RendersPopulatedAndEmptyState()
|
||||||
|
{
|
||||||
|
RunOnSta(() =>
|
||||||
|
{
|
||||||
|
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||||
|
var store = new FakeSettingsStore();
|
||||||
|
var window = new MainWindow(engine, store);
|
||||||
|
|
||||||
|
var content = (UIElement)window.Content;
|
||||||
|
content.Measure(new Size(1280, 960));
|
||||||
|
content.Arrange(new Rect(0, 0, 1280, 960));
|
||||||
|
content.UpdateLayout();
|
||||||
|
|
||||||
|
var updateBadgesMethod = typeof(MainWindow).GetMethod("UpdateBadges", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
|
||||||
|
// 1. Switch to Past tab when history is empty
|
||||||
|
window.PastResults.Clear();
|
||||||
|
updateBadgesMethod?.Invoke(window, null);
|
||||||
|
window.TabCommand.Execute("Past");
|
||||||
|
|
||||||
|
content.Measure(new Size(1280, 960));
|
||||||
|
content.Arrange(new Rect(0, 0, 1280, 960));
|
||||||
|
content.UpdateLayout();
|
||||||
|
|
||||||
|
var pastResultsContainer = (FrameworkElement)window.FindName("PastResultsContainer");
|
||||||
|
var pastResultsEmpty = (FrameworkElement)window.FindName("PastResultsEmpty");
|
||||||
|
var pastResultsView = (FrameworkElement)window.FindName("PastResultsView");
|
||||||
|
|
||||||
|
Assert.Equal(Visibility.Visible, pastResultsContainer.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, pastResultsEmpty.Visibility);
|
||||||
|
Assert.Equal(Visibility.Collapsed, pastResultsView.Visibility);
|
||||||
|
|
||||||
|
// Render empty state
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rtbEmpty = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
|
||||||
|
rtbEmpty.Render(content);
|
||||||
|
var enc = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||||||
|
enc.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtbEmpty));
|
||||||
|
var outPath = @"C:\Users\encep\.gemini\antigravity\brain\b58fd023-a52b-4f4b-aa23-e6df654aa1fb\.tempmediaStorage\rendered_past_results_empty.png";
|
||||||
|
using var fs = File.Create(outPath);
|
||||||
|
enc.Save(fs);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
// 2. Populate Past Results
|
||||||
|
var dateGroup = new DateGroup("오늘 (9월 5일)");
|
||||||
|
var entry1 = new HistoryEntry(
|
||||||
|
DateTime.Now,
|
||||||
|
@"C:\demo\hero-graphic.png",
|
||||||
|
"png",
|
||||||
|
4_250_000,
|
||||||
|
820_000,
|
||||||
|
1,
|
||||||
|
null,
|
||||||
|
ConvertStatus.Success,
|
||||||
|
null,
|
||||||
|
new[] { @"C:\demo\hero-graphic.webp" });
|
||||||
|
var entry2 = new HistoryEntry(
|
||||||
|
DateTime.Now.AddMinutes(-12),
|
||||||
|
@"C:\demo\annual-report.docx",
|
||||||
|
"docx",
|
||||||
|
12_800_000,
|
||||||
|
3_150_000,
|
||||||
|
1,
|
||||||
|
null,
|
||||||
|
ConvertStatus.Success,
|
||||||
|
null,
|
||||||
|
new[] { @"C:\demo\annual-report.pdf" });
|
||||||
|
|
||||||
|
dateGroup.Add(HistoryRow.From(entry1));
|
||||||
|
dateGroup.Add(HistoryRow.From(entry2));
|
||||||
|
window.PastResults.Add(dateGroup);
|
||||||
|
|
||||||
|
updateBadgesMethod?.Invoke(window, null);
|
||||||
|
|
||||||
|
var pastRow = dateGroup.Entries.FirstOrDefault();
|
||||||
|
if (pastRow != null)
|
||||||
|
{
|
||||||
|
var setPreviewMetaMethod = typeof(MainWindow).GetMethod("SetPreviewMeta", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
setPreviewMetaMethod?.Invoke(window, new object[] { pastRow.FileName, pastRow.SourcePath, pastRow.FormatLabel, pastRow.SizeText });
|
||||||
|
var showPreviewGlyphMethod = typeof(MainWindow).GetMethod("ShowPreviewGlyph", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
showPreviewGlyphMethod?.Invoke(window, new object[] { ".png", "변환 완료: " + pastRow.FileName });
|
||||||
|
}
|
||||||
|
|
||||||
|
var pastResultsList = (ItemsControl)window.FindName("PastResultsList");
|
||||||
|
pastResultsList.ItemsSource = window.PastResults;
|
||||||
|
pastResultsList.ApplyTemplate();
|
||||||
|
|
||||||
|
content.Measure(new Size(1280, 960));
|
||||||
|
content.Arrange(new Rect(0, 0, 1280, 960));
|
||||||
|
content.UpdateLayout();
|
||||||
|
|
||||||
|
Assert.Equal(Visibility.Collapsed, pastResultsEmpty.Visibility);
|
||||||
|
Assert.Equal(Visibility.Visible, pastResultsView.Visibility);
|
||||||
|
|
||||||
|
// Render populated state
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rtbPopulated = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
|
||||||
|
rtbPopulated.Render(content);
|
||||||
|
var enc = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||||||
|
enc.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtbPopulated));
|
||||||
|
var outPath = @"C:\Users\encep\.gemini\antigravity\brain\b58fd023-a52b-4f4b-aa23-e6df654aa1fb\.tempmediaStorage\rendered_past_results.png";
|
||||||
|
using var fs = File.Create(outPath);
|
||||||
|
enc.Save(fs);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MainWindow_EnsureHandle_CreatesValidHwnd()
|
||||||
|
{
|
||||||
|
RunOnSta(() =>
|
||||||
|
{
|
||||||
|
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||||
|
var store = new FakeSettingsStore();
|
||||||
|
var window = new MainWindow(engine, store);
|
||||||
|
var helper = new System.Windows.Interop.WindowInteropHelper(window);
|
||||||
|
var hwnd = helper.EnsureHandle();
|
||||||
|
Assert.NotEqual(IntPtr.Zero, hwnd);
|
||||||
|
window.Close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SettingsWindow_TitleBar_MustDisplayProperlyAlignedHeaderAndCloseButton()
|
public void SettingsWindow_TitleBar_MustDisplayProperlyAlignedHeaderAndCloseButton()
|
||||||
{
|
{
|
||||||
|
|
@ -355,6 +655,18 @@ public class UnifiedTitleBarTests
|
||||||
var closeCenterY = closeBtn.TransformToAncestor(titleBar).Transform(new Point(0, closeBtn.ActualHeight / 2.0)).Y;
|
var closeCenterY = closeBtn.TransformToAncestor(titleBar).Transform(new Point(0, closeBtn.ActualHeight / 2.0)).Y;
|
||||||
Assert.True(Math.Abs(textCenterY - closeCenterY) <= 2.0,
|
Assert.True(Math.Abs(textCenterY - closeCenterY) <= 2.0,
|
||||||
$"Title text CenterY ({textCenterY:F1}px) and CloseButton CenterY ({closeCenterY:F1}px) must match within 2px.");
|
$"Title text CenterY ({textCenterY:F1}px) and CloseButton CenterY ({closeCenterY:F1}px) must match within 2px.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(560, 720, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
|
||||||
|
rtb.Render(content);
|
||||||
|
var enc = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||||||
|
enc.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtb));
|
||||||
|
var outPath = @"C:\Users\encep\.gemini\antigravity\brain\b58fd023-a52b-4f4b-aa23-e6df654aa1fb\.tempmediaStorage\rendered_settings.png";
|
||||||
|
using var fs = File.Create(outPath);
|
||||||
|
enc.Save(fs);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
50
tools/Check-Window.ps1
Normal file
50
tools/Check-Window.ps1
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes, System.Drawing, System.Windows.Forms
|
||||||
|
|
||||||
|
$proc = Get-Process -Name "Everything2Everything" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
if (-not $proc) {
|
||||||
|
Write-Host "Everything2Everything 프로세스가 없습니다."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Process ID: $($proc.Id), MainWindowHandle: $($proc.MainWindowHandle), Title: '$($proc.MainWindowTitle)'"
|
||||||
|
|
||||||
|
# Find UI Automation element
|
||||||
|
$cond = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ProcessIdProperty, $proc.Id)
|
||||||
|
$windowEl = [System.Windows.Automation.AutomationElement]::RootElement.FindFirst([System.Windows.Automation.TreeScope]::Children, $cond)
|
||||||
|
|
||||||
|
if (-not $windowEl) {
|
||||||
|
# Try descendants
|
||||||
|
$windowEl = [System.Windows.Automation.AutomationElement]::RootElement.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $cond)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($windowEl) {
|
||||||
|
Write-Host "UI Automation Window Name: '$($windowEl.Current.Name)'"
|
||||||
|
Write-Host "BoundingRectangle: $($windowEl.Current.BoundingRectangle)"
|
||||||
|
|
||||||
|
# Look for our specific controls:
|
||||||
|
$controls = @("SmartPresetCombo", "QualitySlider", "VideoCrfSlider", "AudioBitrateQuickCombo", "PdfCompressQuickCombo", "AdvancedOptionsExpander", "OutputFormatCombo")
|
||||||
|
foreach ($cid in $controls) {
|
||||||
|
$cCond = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::AutomationIdProperty, $cid)
|
||||||
|
$el = $windowEl.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $cCond)
|
||||||
|
if ($el) {
|
||||||
|
Write-Host " FOUND Control [$cid]: Type=$($el.Current.ControlType.ProgrammaticName), Name='$($el.Current.Name)', IsOffscreen=$($el.Current.IsOffscreen)"
|
||||||
|
} else {
|
||||||
|
Write-Host " MISSING Control [$cid]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Take screenshot of the window
|
||||||
|
$rect = $windowEl.Current.BoundingRectangle
|
||||||
|
if ($rect.Width -gt 50 -and $rect.Height -gt 50) {
|
||||||
|
$bmp = New-Object System.Drawing.Bitmap ([int]$rect.Width), ([int]$rect.Height)
|
||||||
|
$gfx = [System.Drawing.Graphics]::FromImage($bmp)
|
||||||
|
$gfx.CopyFromScreen([int]$rect.Left, [int]$rect.Top, 0, 0, (New-Object System.Drawing.Size([int]$rect.Width, [int]$rect.Height)))
|
||||||
|
$shotPath = "C:\Users\encep\.gemini\antigravity\brain\a5abaf02-dd4b-45f7-8890-144e9da36bcc\window_screenshot.png"
|
||||||
|
$bmp.Save($shotPath, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
$gfx.Dispose()
|
||||||
|
$bmp.Dispose()
|
||||||
|
Write-Host "SCREENSHOT_SAVED: $shotPath"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "AutomationElement를 찾지 못했습니다."
|
||||||
|
}
|
||||||
25
tools/Dump-Tree.ps1
Normal file
25
tools/Dump-Tree.ps1
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes
|
||||||
|
|
||||||
|
$proc = Get-Process -Name "Everything2Everything" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
if (-not $proc) {
|
||||||
|
Write-Host "프로세스를 먼저 실행합니다..."
|
||||||
|
$file1 = "d:\workspace\Everything2Everthing\test_assets\test_icon.png"
|
||||||
|
$proc = Start-Process "d:\workspace\Everything2Everthing\src\Everything2Everything.App\bin\Debug\net9.0-windows10.0.19041.0\Everything2Everything.exe" -ArgumentList "`"$file1`"" -PassThru
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
}
|
||||||
|
|
||||||
|
$p = Get-Process -Id $proc.Id
|
||||||
|
Write-Host "Process Id: $($p.Id), Handle: $($p.MainWindowHandle)"
|
||||||
|
|
||||||
|
$windowEl = [System.Windows.Automation.AutomationElement]::FromHandle([IntPtr]$p.MainWindowHandle)
|
||||||
|
$all = $windowEl.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition)
|
||||||
|
|
||||||
|
Write-Host "총 UIA 컨트롤 수: $($all.Count)"
|
||||||
|
foreach ($el in $all) {
|
||||||
|
$id = $el.Current.AutomationId
|
||||||
|
$name = $el.Current.Name
|
||||||
|
$type = $el.Current.ControlType.ProgrammaticName
|
||||||
|
if ($id -or $name) {
|
||||||
|
Write-Host "[$type] Id='$id' Name='$name'"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
tools/Dump-UiaControls.ps1
Normal file
31
tools/Dump-UiaControls.ps1
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes
|
||||||
|
|
||||||
|
$proc = Get-Process -Name "Everything2Everything" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
if (-not $proc) {
|
||||||
|
Write-Host "프로세스가 없습니다."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "발견된 프로세스 ID: $($proc.Id)"
|
||||||
|
|
||||||
|
$cond = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ProcessIdProperty, $proc.Id)
|
||||||
|
$windowEl = [System.Windows.Automation.AutomationElement]::RootElement.FindFirst([System.Windows.Automation.TreeScope]::Children, $cond)
|
||||||
|
|
||||||
|
if (-not $windowEl) {
|
||||||
|
Write-Host "루트 엘리먼트 아래에서 윈도우를 찾을 수 없습니다."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "윈도우 제목: '$($windowEl.Current.Name)', NativeHandle: $($windowEl.Current.NativeWindowHandle)"
|
||||||
|
|
||||||
|
$all = $windowEl.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition)
|
||||||
|
Write-Host "총 발견된 UI 컨트롤 수: $($all.Count)"
|
||||||
|
|
||||||
|
foreach ($el in $all) {
|
||||||
|
$autoId = $el.Current.AutomationId
|
||||||
|
$name = $el.Current.Name
|
||||||
|
$type = $el.Current.ControlType.ProgrammaticName
|
||||||
|
if ($autoId -or $name) {
|
||||||
|
Write-Host "Type: $type | Id: '$autoId' | Name: '$name'"
|
||||||
|
}
|
||||||
|
}
|
||||||
49
tools/Render-WindowScreenshot.ps1
Normal file
49
tools/Render-WindowScreenshot.ps1
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase
|
||||||
|
|
||||||
|
$binDir = "D:\workspace\Everything2Everthing\src\Everything2Everything.App\bin\Debug\net9.0-windows10.0.19041.0"
|
||||||
|
[System.Reflection.Assembly]::LoadFrom((Join-Path $binDir "Everything2Everything.Core.dll")) | Out-Null
|
||||||
|
[System.Reflection.Assembly]::LoadFrom((Join-Path $binDir "Everything2Everything.dll")) | Out-Null
|
||||||
|
|
||||||
|
$testAssets = "D:\workspace\Everything2Everthing\test_assets"
|
||||||
|
$testFiles = @(Join-Path $testAssets "test_icon.png", Join-Path $testAssets "test_art.png")
|
||||||
|
|
||||||
|
$thread = New-Object System.Threading.Thread([System.Threading.ThreadStart]{
|
||||||
|
try {
|
||||||
|
if ($null -eq [System.Windows.Application]::Current) {
|
||||||
|
$app = New-Object System.Windows.Application
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = [Everything2Everything.Core.Everything2EverythingBootstrap]::CreateDefault()
|
||||||
|
|
||||||
|
# Simple settings store via dynamic object or reflection
|
||||||
|
$settingsType = [System.Type]::GetType("Everything2Everything.Core.ISettingsStore, Everything2Everything.Core")
|
||||||
|
# Use existing memory settings store if available
|
||||||
|
$servicesField = $engine.GetType().GetProperty("Providers")
|
||||||
|
$store = New-Object Everything2Everything.Core.Settings.MemorySettingsStore
|
||||||
|
|
||||||
|
$win = New-Object Everything2Everything.App.Views.MainWindow($engine, $store, [string[]]$testFiles)
|
||||||
|
|
||||||
|
$size = New-Object System.Windows.Size(1280, 960)
|
||||||
|
$win.Measure($size)
|
||||||
|
$win.Arrange((New-Object System.Windows.Rect(0, 0, 1280, 960)))
|
||||||
|
$win.UpdateLayout()
|
||||||
|
|
||||||
|
$rtb = New-Object System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, [System.Windows.Media.PixelFormats]::Pbgra32)
|
||||||
|
$rtb.Render($win)
|
||||||
|
|
||||||
|
$encoder = New-Object System.Windows.Media.Imaging.PngBitmapEncoder
|
||||||
|
$encoder.Frames.Add([System.Windows.Media.Imaging.BitmapFrame]::Create($rtb))
|
||||||
|
|
||||||
|
$outPath = "C:\Users\encep\.gemini\antigravity\brain\a5abaf02-dd4b-45f7-8890-144e9da36bcc\app_rendered_preview.png"
|
||||||
|
$fs = [System.IO.File]::OpenWrite($outPath)
|
||||||
|
$encoder.Save($fs)
|
||||||
|
$fs.Close()
|
||||||
|
Write-Host "RENDER_SUCCESS: $outPath"
|
||||||
|
} catch {
|
||||||
|
Write-Error $_.Exception.ToString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$thread.SetApartmentState([System.Threading.ApartmentState]::STA)
|
||||||
|
$thread.Start()
|
||||||
|
$thread.Join()
|
||||||
196
tools/Test-HeadfulE2E.ps1
Normal file
196
tools/Test-HeadfulE2E.ps1
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
# tools/Test-HeadfulE2E.ps1
|
||||||
|
# Everything2Everything Headful E2E 시각적 상호작용 및 실제 변환 검증 스크립트
|
||||||
|
# WMI 분리 런처(agy-gui-launch.ps1) 경유 규약 준수
|
||||||
|
|
||||||
|
param(
|
||||||
|
[switch]$Release
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$RootDir = Split-Path -Parent $ScriptDir
|
||||||
|
$Config = if ($Release) { "Release" } else { "Debug" }
|
||||||
|
$exePath = Join-Path $RootDir "src/Everything2Everything.App/bin/$Config/net9.0-windows10.0.19041.0/Everything2Everything.exe"
|
||||||
|
|
||||||
|
Write-Host "==========================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Everything2Everything Headful E2E 인터랙션 검증" -ForegroundColor Cyan
|
||||||
|
Write-Host " 실행 바이너리: $exePath" -ForegroundColor Cyan
|
||||||
|
Write-Host "==========================================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# 0. 기존 프로세스 종료
|
||||||
|
Stop-Process -Name "Everything2Everything" -Force -ErrorAction SilentlyContinue
|
||||||
|
Start-Sleep -Milliseconds 500
|
||||||
|
|
||||||
|
# 테스트 파일 준비
|
||||||
|
$testDir = Join-Path $RootDir "test_assets"
|
||||||
|
$file1 = Join-Path $testDir "test_icon.png"
|
||||||
|
$file2 = Join-Path $testDir "test_art.png"
|
||||||
|
|
||||||
|
# 기존 변환 출력 디렉터리 정리
|
||||||
|
Remove-Item (Join-Path $testDir "*_converted*") -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item (Join-Path $testDir "*_webp*") -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item (Join-Path $testDir "*_pdf*") -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# 1. WMI 런처를 통해 사용자 인터랙티브 데스크톱 세션에 실행
|
||||||
|
Write-Host "`n[Step 1] WMI 런처를 통해 Headful 창 실행 중 (입력 파일 2개 탑재)..." -ForegroundColor Yellow
|
||||||
|
$argsStr = "`"$file1`" `"$file2`""
|
||||||
|
|
||||||
|
& "C:\Users\encep\.gemini\agy-gui-launch.ps1" -ExePath $exePath -Arguments $argsStr -ProcessName "Everything2Everything" -WaitSeconds 60
|
||||||
|
|
||||||
|
$p = Get-Process -Name "Everything2Everything" -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1
|
||||||
|
if (-not $p) {
|
||||||
|
Write-Error "MainWindowHandle이 0이 아닌 Everything2Everything 프로세스를 찾지 못했습니다."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = $p.MainWindowHandle
|
||||||
|
Write-Host "✅ Headful 창 활성화 완료! (PID: $($p.Id), Handle: $handle, Title: '$($p.MainWindowTitle)')" -ForegroundColor Green
|
||||||
|
|
||||||
|
# 2. UI Automation 로드
|
||||||
|
Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes
|
||||||
|
$windowEl = [System.Windows.Automation.AutomationElement]::FromHandle([IntPtr]$handle)
|
||||||
|
if (-not $windowEl) {
|
||||||
|
Write-Error "AutomationElement를 찾을 수 없습니다."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "✅ UI Automation 연결 성공: $($windowEl.Current.Name)" -ForegroundColor Green
|
||||||
|
|
||||||
|
function Find-ElementById([string]$autoId) {
|
||||||
|
$cond = New-Object System.Windows.Automation.PropertyCondition ([System.Windows.Automation.AutomationElement]::AutomationIdProperty), $autoId
|
||||||
|
return $windowEl.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $cond)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-ElementByName([string]$name) {
|
||||||
|
$cond = New-Object System.Windows.Automation.PropertyCondition ([System.Windows.Automation.AutomationElement]::NameProperty), $name
|
||||||
|
return $windowEl.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $cond)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. 큐 및 뱃지 상태 탐색
|
||||||
|
Write-Host "`n[Step 2] 큐 및 컨트롤 실시간 탐색..." -ForegroundColor Yellow
|
||||||
|
$badge = Find-ElementById "TabActiveBadge"
|
||||||
|
if ($badge) {
|
||||||
|
Write-Host " - Active Queue 뱃지: '$($badge.Current.Name)'" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. 상세 설정 폴드아웃(AdvancedOptionsExpander) 클릭
|
||||||
|
Write-Host "`n[Step 3] 상세 설정 폴드아웃(AdvancedOptionsExpander) 펼치기 클릭..." -ForegroundColor Yellow
|
||||||
|
$expander = Find-ElementById "AdvancedOptionsExpander"
|
||||||
|
if ($expander) {
|
||||||
|
Write-Host " - AdvancedOptionsExpander 컨트롤 발견!" -ForegroundColor Gray
|
||||||
|
$expPattern = $expander.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern) -as [System.Windows.Automation.ExpandCollapsePattern]
|
||||||
|
if ($expPattern) {
|
||||||
|
$expPattern.Expand()
|
||||||
|
Start-Sleep -Milliseconds 600
|
||||||
|
Write-Host "✅ 상세 설정 폴드아웃 펼침 성공! (상태: $($expPattern.Current.ExpandCollapseState))" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Warning "AdvancedOptionsExpander ID를 찾지 못했습니다."
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4-1. 고급 이미지 인코딩 옵션(ImageLosslessCheck) 클릭 및 토글
|
||||||
|
Write-Host "`n[Step 3-1] 이미지 무손실(Lossless) 옵션 체크박스 클릭..." -ForegroundColor Yellow
|
||||||
|
$losslessCheck = Find-ElementById "ImageLosslessCheck"
|
||||||
|
if ($losslessCheck) {
|
||||||
|
Write-Host " - ImageLosslessCheck 컨트롤 발견!" -ForegroundColor Gray
|
||||||
|
$togglePattern = $losslessCheck.GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern) -as [System.Windows.Automation.TogglePattern]
|
||||||
|
if ($togglePattern) {
|
||||||
|
$togglePattern.Toggle()
|
||||||
|
Start-Sleep -Milliseconds 400
|
||||||
|
Write-Host "✅ 무손실(Lossless) 체크박스 토글 완료! (상태: $($togglePattern.Current.ToggleState))" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " ℹ️ 현재 포맷 패널에 따라 ImageLosslessCheck 표시 여부 확인됨." -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4-2. 윈도우 스크린샷 캡처 (Headful UI 동작 증빙)
|
||||||
|
try {
|
||||||
|
Add-Type -AssemblyName System.Drawing, System.Windows.Forms
|
||||||
|
$rect = $windowEl.Current.BoundingRectangle
|
||||||
|
if ($rect.Width -gt 0 -and $rect.Height -gt 0) {
|
||||||
|
$bmp = New-Object System.Drawing.Bitmap ([int]$rect.Width), ([int]$rect.Height)
|
||||||
|
$gfx = [System.Drawing.Graphics]::FromImage($bmp)
|
||||||
|
$gfx.CopyFromScreen([int]$rect.Left, [int]$rect.Top, 0, 0, (New-Object System.Drawing.Size([int]$rect.Width, [int]$rect.Height)))
|
||||||
|
$shotPath = "C:\Users\encep\.gemini\antigravity\brain\a5abaf02-dd4b-45f7-8890-144e9da36bcc\e2e_headful_ui.png"
|
||||||
|
$bmp.Save($shotPath, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
$gfx.Dispose()
|
||||||
|
$bmp.Dispose()
|
||||||
|
Write-Host "📸 Headful UI 창 스크린샷 캡처 성공: $shotPath" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Warning "스크린샷 캡처 중 경고 발생: $($_.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 5. 변환 시작 버튼 클릭
|
||||||
|
Write-Host "`n[Step 4] '대기열 일괄 변환 시작' 버튼 클릭 트리거..." -ForegroundColor Yellow
|
||||||
|
$convertBtn = Find-ElementById "ProcessQueueButton"
|
||||||
|
if (-not $convertBtn) {
|
||||||
|
$convertBtn = $windowEl.FindFirst([System.Windows.Automation.TreeScope]::Descendants,
|
||||||
|
(New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ControlTypeProperty, [System.Windows.Automation.ControlType]::Button)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($convertBtn) {
|
||||||
|
Write-Host " - 변환 버튼 발견: '$($convertBtn.Current.Name)'" -ForegroundColor Gray
|
||||||
|
$btnPattern = $convertBtn.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
|
||||||
|
if ($btnPattern) {
|
||||||
|
$btnPattern.Invoke()
|
||||||
|
Write-Host "✅ '대기열 일괄 변환 시작' 버튼 클릭 완료! 변환 엔진 가동!" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Error "변환 버튼을 찾지 못했습니다."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 6. 변환 완료 대기 및 실시간 관측
|
||||||
|
Write-Host "`n[Step 5] 실시간 변환 진행 및 완료 대기 중..." -ForegroundColor Yellow
|
||||||
|
$completed = $false
|
||||||
|
for ($i = 0; $i -lt 30; $i++) {
|
||||||
|
Start-Sleep -Milliseconds 600
|
||||||
|
$convertedFiles = Get-ChildItem (Join-Path $testDir "*") -Recurse -File | Where-Object {
|
||||||
|
$_.DirectoryName -ne $testDir -and $_.LastWriteTime -gt (Get-Date).AddMinutes(-2)
|
||||||
|
}
|
||||||
|
if ($convertedFiles.Count -ge 2) {
|
||||||
|
$completed = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Write-Host " ... 변환 진행 중 ($([math]::Round($i * 0.6, 1))초 경과)" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
|
||||||
|
# 7. 실제 결과 파일 검증
|
||||||
|
Write-Host "`n[Step 6] 실제 디스크 출력 결과 파일 전수 검증:" -ForegroundColor Yellow
|
||||||
|
$allOutputs = Get-ChildItem (Join-Path $testDir "*") -Recurse -File | Where-Object {
|
||||||
|
$_.DirectoryName -ne $testDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($allOutputs.Count -eq 0) {
|
||||||
|
Write-Error "❌ 변환 결과 파일이 생성되지 않았습니다!"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($out in $allOutputs) {
|
||||||
|
Write-Host " 📄 생성된 파일: $($out.FullName)" -ForegroundColor Cyan
|
||||||
|
Write-Host " 크기: $([math]::Round($out.Length / 1KB, 2)) KB ($($out.Length) bytes)" -ForegroundColor Gray
|
||||||
|
Write-Host " 생성 시간: $($out.LastWriteTime)" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
# 최종 화면 상태 스크린샷 캡처 (변환 완료 상태)
|
||||||
|
try {
|
||||||
|
Start-Sleep -Milliseconds 500
|
||||||
|
$rect = $windowEl.Current.BoundingRectangle
|
||||||
|
if ($rect.Width -gt 0 -and $rect.Height -gt 0) {
|
||||||
|
$bmp2 = New-Object System.Drawing.Bitmap ([int]$rect.Width), ([int]$rect.Height)
|
||||||
|
$gfx2 = [System.Drawing.Graphics]::FromImage($bmp2)
|
||||||
|
$gfx2.CopyFromScreen([int]$rect.Left, [int]$rect.Top, 0, 0, (New-Object System.Drawing.Size([int]$rect.Width, [int]$rect.Height)))
|
||||||
|
$doneShotPath = "C:\Users\encep\.gemini\antigravity\brain\a5abaf02-dd4b-45f7-8890-144e9da36bcc\e2e_headful_done.png"
|
||||||
|
$bmp2.Save($doneShotPath, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
$gfx2.Dispose()
|
||||||
|
$bmp2.Dispose()
|
||||||
|
Write-Host "📸 변환 완료 Headful UI 스크린샷 저장 완료: $doneShotPath" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Warning "완료 스크린샷 캡처 중 경고 발생: $($_.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n🎉 Headful E2E 실제 클릭 및 변환 전 과정 테스트 성공!" -ForegroundColor Green
|
||||||
|
|
||||||
|
# 8. 테스트 종료 후 프로세스 정리
|
||||||
|
Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
|
||||||
9
tools/test_launch.ps1
Normal file
9
tools/test_launch.ps1
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
$exe = "D:\workspace\Everything2Everthing\src\Everything2Everything.App\bin\Debug\net9.0-windows10.0.19041.0\Everything2Everything.exe"
|
||||||
|
$testFile = "D:\workspace\Everything2Everthing\test_assets\test_icon.png"
|
||||||
|
Write-Host "Starting: $exe"
|
||||||
|
$proc = Start-Process -FilePath $exe -ArgumentList "`"$testFile`"" -PassThru
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
Get-Process -Id $proc.Id | Format-List Id, ProcessName, MainWindowHandle, MainWindowTitle, Responding
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
Stop-Process -Id $proc.Id -Force
|
||||||
|
Write-Host "Done"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue