1
0
Fork 0

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:
Yun Chan 2026-09-05 12:25:30 +09:00
parent 455c8b1324
commit 4ca7352dc3
27 changed files with 2477 additions and 659 deletions

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.11</Version>
<Version>1.0.18</Version>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
<Nullable>enable</Nullable>

View file

@ -66,6 +66,13 @@ public partial class OptionsViewModel : ObservableObject
[ObservableProperty] private int _channelsIndex; // 0=원본,1=모노,2=스테레오
[ObservableProperty] private bool _loudnorm;
// ── 상세 인코딩 옵션 (폴드아웃 Expander 상태 및 전문 규격) ────────────────────────────────
[ObservableProperty] private bool _isAdvancedExpanded = true; // 사용자 피드백: 슬라이더/옵션을 바로 볼 수 있도록 기본 전개
[ObservableProperty] private int _pdfCompressLevelIndex; // 0=Light, 1=Strong, 2=Max
[ObservableProperty] private int _pdfDpiIndex = 1; // 0=150, 1=200, 2=300
[ObservableProperty] private bool _imageLossless; // WebP/PNG 무손실
[ObservableProperty] private bool _progressive; // JPEG 프로그레시브 웹 로딩
/// <summary>현재 상태로 불변 ConvertOptions를 구성한다(기존 MainWindow.BuildOptions와 동일 동작 + 영상/오디오).</summary>
public ConvertOptions ToConvertOptions()
{
@ -77,15 +84,23 @@ public partial class OptionsViewModel : ObservableObject
_ => "summarize",
};
var dpi = PdfDpiIndex switch
{
0 => 150,
2 => 300,
_ => 200,
};
return new ConvertOptions
{
OnCollision = ConflictRule,
OutputLocation = hasCustom ? OutputLocation.Custom : OutputLocation.SubfolderBesideSource,
CustomOutputDirectory = hasCustom ? CustomOutputDirectory!.Trim() : null,
KeepExifWhenPossible = !StripMetadata,
Jpeg = new JpegEncodingOptions { Quality = Quality },
Webp = new WebpEncodingOptions { Quality = Quality },
Jpeg = new JpegEncodingOptions { Quality = Quality, Progressive = Progressive },
Webp = new WebpEncodingOptions { Quality = Quality, Lossless = ImageLossless },
Avif = new AvifEncodingOptions { Quality = Math.Clamp(Quality - 30, 1, 100) },
PdfRender = new PdfRenderOptions { Dpi = dpi },
Ai = new AiOptions
{
Task = aiTask,

View file

@ -50,7 +50,7 @@ public static class CategoryGlyphs
public static ImageSource ForCategory(string category)
{
if (Cache.TryGetValue(category, out var cached)) return cached;
var uri = new Uri($"pack://application:,,,/Assets/glyph-{category}.png", UriKind.Absolute);
var uri = new Uri($"pack://application:,,,/Everything2Everything;component/Assets/glyph-{category}.png", UriKind.Absolute);
var img = new BitmapImage();
img.BeginInit();
img.CacheOption = BitmapCacheOption.OnLoad;

View file

@ -12,6 +12,7 @@
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ControlsDictionary/>
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View file

@ -521,6 +521,100 @@
<Setter Property="Margin" Value="0,0,0,16"/>
</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) 캡슐 스타일 -->
<Style x:Key="FsSpecChipStyle" TargetType="Border">
<Setter Property="Background" Value="{StaticResource FsAccentCyanBg}"/>

File diff suppressed because it is too large Load diff

View file

@ -145,7 +145,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
ToggleInspectorCommand = new RelayCommand(_ => ToggleInspector());
RemoveQueueItemCommand = new RelayCommand(p => RemoveQueueItem(p as QueueItem));
OpenFolderCommand = new RelayCommand(p => OpenFolderForPath(p as string));
TabCommand = new RelayCommand(p => ShowTab(p as string ?? "Past"));
TabCommand = new RelayCommand(p => ShowTab(p as string ?? "Active"));
ConflictRuleCommand = new RelayCommand(p => SetConflictRule(p as string));
CombineToggleCommand = new RelayCommand(_ => UpdateCombineState(SelectedOutputExtension));
OutputFormatChangedCommand = new RelayCommand(_ => OnOutputFormatSelected());
@ -156,12 +156,26 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
PastRowCommand = new RelayCommand(p => HandlePastRowClick(p as MouseButtonEventArgs));
InitializeComponent();
Title = "Everything2Everything";
if (AppTitleBar is not null) AppTitleBar.Title = "Everything2Everything";
if (AdvancedOptionsExpander is not null)
{
AdvancedOptionsExpander.IsExpanded = true;
AdvancedOptionsExpander.Expanded += (_, _) => _options.IsAdvancedExpanded = true;
AdvancedOptionsExpander.Collapsed += (_, _) => _options.IsAdvancedExpanded = false;
}
if (SmartPresetCombo is not null)
{
SmartPresetCombo.SelectionChanged += OnSmartPresetChanged;
}
if (OutputFormatCombo is not null)
{
OutputFormatCombo.SelectionChanged += (_, _) => OnOutputFormatSelected();
}
// ActiveQueueList/PastResultsList의 ItemsSource는 XAML이 ActiveQueue/PastResults에 바인딩(선언적).
InitializeOutputFormats();
@ -202,7 +216,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
return;
}
CapabilityStatusText.Text = $"⚠ {notReady.Count}개 형식이 외부 도구를 기다립니다 (Diagnose 참조)";
CapabilityStatusText.Text = $"⚠ {notReady.Count}개 형식이 외부 도구를 기다립니다 (진단 도구 참조)";
CapabilityStatusText.Visibility = Visibility.Visible;
}
@ -343,22 +357,26 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
var count = _activeQueue.Count;
if (_cts is not null)
{
ProcessQueueButton.Content = $"변환 처리 중… ({count}개 파일)";
ProcessQueueButton.Content = $"변환 처리 중… ({count}개)";
ProcessQueueButton.ToolTip = "파일 변환이 진행 중입니다.";
ProcessQueueButton.IsEnabled = false;
}
else if (count == 0)
{
ProcessQueueButton.Content = "대기 중 — 파일을 드래그하여 추가하세요";
ProcessQueueButton.Content = "파일을 드래그하여 추가";
ProcessQueueButton.ToolTip = "대기열에 변환할 파일을 추가하세요 (단축키: Ctrl + O)";
ProcessQueueButton.IsEnabled = false;
}
else if (string.IsNullOrEmpty(SelectedOutputExtension))
{
ProcessQueueButton.Content = "변환 불가 (공통 형식 없음)";
ProcessQueueButton.Content = "공통 형식 없음";
ProcessQueueButton.ToolTip = "선택된 파일들 간에 호환 가능한 공통 출력 형식이 없습니다.";
ProcessQueueButton.IsEnabled = false;
}
else
{
ProcessQueueButton.Content = $"대기열 일괄 변환 시작 ({count}개) [Ctrl + Enter]";
ProcessQueueButton.Content = $"변환 시작 ({count}개 파일)";
ProcessQueueButton.ToolTip = $"대기열 일괄 변환 시작 ({count}개 파일) [단축키: Ctrl + Enter]";
ProcessQueueButton.IsEnabled = true;
}
}
@ -508,7 +526,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
PreviewFormatText.Text = string.IsNullOrEmpty(formatLabel) || formatLabel == "—" ? info.Extension.TrimStart('.').ToUpperInvariant() : formatLabel;
PreviewSizeText.Text = string.IsNullOrEmpty(sizeText) || sizeText == "—" ? info.FormattedSize : sizeText;
PreviewDimText.Text = info.DimensionsOrMeta;
PreviewPageText.Text = info.Category.ToString();
PreviewPageText.Text = info.Category.ToKoreanLabel();
}
private void ShowPreviewLoading()
@ -1043,10 +1061,9 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private static string FormatDateLabel(DateOnly date)
{
var today = DateOnly.FromDateTime(DateTime.Today);
var label = date == today ? "Today"
: date == today.AddDays(-1) ? "Yesterday"
: date.ToString("dddd", CultureInfo.GetCultureInfo("en-US"));
return $"{label}, {date:MMM d}";
if (date == today) return $"오늘 ({date:M월 d일})";
if (date == today.AddDays(-1)) return $"어제 ({date:M월 d일})";
return date.ToString("yyyy년 M월 d일 (ddd)", CultureInfo.GetCultureInfo("ko-KR"));
}
private void ApplyAppDataStats()
@ -1098,7 +1115,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
else if (TabPastBtn.IsChecked == true)
{
var confirm = MessageBox.Show(this,
"Past Results 전체를 삭제하시겠습니까?\n영구 저장된 이력도 함께 삭제됩니다.",
"변환 기록 전체를 삭제하시겠습니까?\n영구 저장된 이력도 함께 삭제됩니다.",
"Everything2Everything",
MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (confirm != MessageBoxResult.OK) return;
@ -1386,17 +1403,33 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private void UpdateQualityPanelForFormat(string? extension)
{
if (QualityPanel is null || QualityLabelText is null) return;
var ext = extension?.ToLowerInvariant();
var supportsQuality = ext is ".jpg" or ".jpeg" or ".webp" or ".avif";
QualityPanel.Visibility = supportsQuality ? Visibility.Visible : Visibility.Collapsed;
QualityLabelText.Text = ext switch
var isImageQuality = ext is ".jpg" or ".jpeg" or ".webp" or ".avif";
var isVideo = ext is ".mp4" or ".mkv" or ".webm" or ".mov" or ".avi";
var isAudio = ext is ".mp3" or ".aac" or ".m4a" or ".opus" or ".ogg" or ".flac" or ".wav";
var isPdf = ext is ".pdf";
if (QualityPanel is not null)
{
".jpg" or ".jpeg" => "JPEG QUALITY",
".webp" => "WEBP QUALITY",
".avif" => "AVIF QUALITY",
_ => "ENCODING QUALITY",
};
QualityPanel.Visibility = isImageQuality ? Visibility.Visible : Visibility.Collapsed;
if (QualityLabelText is not null)
{
QualityLabelText.Text = ext switch
{
".jpg" or ".jpeg" => "JPEG 압축 품질",
".webp" => "WebP 압축 품질",
".avif" => "AVIF 압축 품질",
_ => "압축 품질",
};
}
}
if (VideoQuickPanel is not null)
VideoQuickPanel.Visibility = isVideo ? Visibility.Visible : Visibility.Collapsed;
if (AudioQuickPanel is not null)
AudioQuickPanel.Visibility = isAudio ? Visibility.Visible : Visibility.Collapsed;
if (PdfQuickPanel is not null)
PdfQuickPanel.Visibility = isPdf ? Visibility.Visible : Visibility.Collapsed;
UpdateMediaPanelForFormat(extension);
}
@ -1471,9 +1504,17 @@ public sealed class QueueItem : INotifyPropertyChanged
public string StateText
{
get => _state;
set { _state = value; Raise(nameof(StateText)); Raise(nameof(IsDone)); }
set { _state = value; Raise(nameof(StateText)); Raise(nameof(DisplayStateText)); Raise(nameof(IsDone)); }
}
/// <summary>사용자에게 표시되는 정제된 한국어 상태 텍스트 (AGENTS.md Rule 3).</summary>
public string DisplayStateText => _state switch
{
"queued" => "대기 중",
"done" => "변환 완료",
_ => _state,
};
public Brush StateBrush => _state switch
{
"queued" => (Application.Current?.TryFindResource("FsTextTertiary") as Brush) ?? Brushes.Gray,
@ -1547,7 +1588,7 @@ public sealed class DateGroup : INotifyPropertyChanged
public string DateTitle { get; }
public ObservableCollection<HistoryRow> Entries { get; } = new();
public long SessionSavingsBytes { get; set; }
public string SessionSavingsText => $"Session Savings: {MainWindow.HumanizeBytes(SessionSavingsBytes)}";
public string SessionSavingsText => $"세션 절감: {MainWindow.HumanizeBytes(SessionSavingsBytes)}";
public DateGroup(string dateTitle) { DateTitle = dateTitle; }
@ -1591,7 +1632,7 @@ public sealed record HistoryRow(
FormatLabel: label,
FormatBrush: brush,
FileName: Path.GetFileName(e.SourcePath),
MetaLine: $"{e.Timestamp:HH:mm:ss} • {e.OutputCount} output(s)",
MetaLine: $"{e.Timestamp:HH:mm:ss} • {e.OutputCount}개 파일",
SizeText: MainWindow.HumanizeBytes(e.SourceSizeBytes),
SavingsText: $"{arrow} {MainWindow.HumanizeBytes(Math.Abs(saved))}",
SourcePath: e.SourcePath,

View file

@ -13,6 +13,7 @@
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ControlsDictionary/>
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View file

@ -13,6 +13,7 @@
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ControlsDictionary/>
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View file

@ -11,6 +11,7 @@
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ControlsDictionary/>
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
@ -29,132 +30,132 @@
<StackPanel>
<!-- ===== AI 카드 ===== -->
<Border Background="{StaticResource FsBgSurface}"
BorderBrush="{StaticResource FsBorderHairline}" BorderThickness="1"
CornerRadius="10" Padding="20" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="AI 텍스트 변환" Style="{StaticResource FsBodyStyle}" FontWeight="SemiBold" FontSize="15"/>
<TextBlock Text="요약 · 번역 · 교정에 사용됩니다 (종량 과금 · 네트워크 필요). 키가 없으면 AI 변환만 비활성됩니다."
Style="{StaticResource FsCaptionStyle}" TextWrapping="Wrap" Margin="0,4,0,16"/>
<Border Style="{StaticResource FsDoubleBezelShellStyle}" Margin="0,0,0,16">
<Border Style="{StaticResource FsDoubleBezelCoreStyle}" Padding="20">
<StackPanel>
<TextBlock Text="AI 텍스트 변환" Style="{StaticResource FsBodyStyle}" FontWeight="SemiBold" FontSize="15"/>
<TextBlock Text="요약 · 번역 · 교정에 사용됩니다 (종량 과금 · 네트워크 필요). 키가 없으면 AI 변환만 비활성됩니다."
Style="{StaticResource FsCaptionStyle}" TextWrapping="Wrap" Margin="0,4,0,16"/>
<TextBlock Text="기본 백엔드" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
<ComboBox x:Name="BackendCombo" Margin="0,0,0,16">
<ComboBoxItem Content="자동 (API 키 우선, 없으면 Codex)"/>
<ComboBoxItem Content="OpenAI (API 키)"/>
<ComboBoxItem Content="Anthropic (API 키)"/>
<ComboBoxItem Content="Codex CLI (ChatGPT 구독 OAuth)"/>
</ComboBox>
<TextBlock Text="기본 백엔드" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
<ComboBox x:Name="BackendCombo" Margin="0,0,0,16">
<ComboBoxItem Content="자동 (API 키 우선, 없으면 Codex)"/>
<ComboBoxItem Content="OpenAI (API 키)"/>
<ComboBoxItem Content="Anthropic (API 키)"/>
<ComboBoxItem Content="Codex CLI (ChatGPT 구독 OAuth)"/>
</ComboBox>
<!-- OpenAI -->
<TextBlock Text="OpenAI API Key" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
<Grid Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<PasswordBox x:Name="OpenAiKeyBox" Grid.Column="0"
Style="{StaticResource FsPasswordInputStyle}"
PasswordChanged="OnOpenAiKeyChanged"/>
<Button x:Name="OpenAiVerifyBtn" Grid.Column="1" Content="확인"
Style="{StaticResource FsSecondaryButtonStyle}" Margin="8,0,0,0"
IsEnabled="False" Click="OnVerifyOpenAi"/>
</Grid>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,16">
<Ellipse x:Name="OpenAiDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock x:Name="OpenAiStatus" Text="키 미설정" Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
<!-- OpenAI -->
<TextBlock Text="OpenAI API Key" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
<Grid Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<PasswordBox x:Name="OpenAiKeyBox" Grid.Column="0"
Style="{StaticResource FsPasswordInputStyle}"
PasswordChanged="OnOpenAiKeyChanged"/>
<Button x:Name="OpenAiVerifyBtn" Grid.Column="1" Content="확인"
Style="{StaticResource FsSecondaryButtonStyle}" Margin="8,0,0,0"
IsEnabled="False" Click="OnVerifyOpenAi"/>
</Grid>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,16">
<Ellipse x:Name="OpenAiDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock x:Name="OpenAiStatus" Text="키 미설정" Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
</StackPanel>
<!-- Anthropic -->
<TextBlock Text="Anthropic API Key" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
<Grid Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<PasswordBox x:Name="AnthropicKeyBox" Grid.Column="0"
Style="{StaticResource FsPasswordInputStyle}"
PasswordChanged="OnAnthropicKeyChanged"/>
<Button x:Name="AnthropicVerifyBtn" Grid.Column="1" Content="확인"
Style="{StaticResource FsSecondaryButtonStyle}" Margin="8,0,0,0"
IsEnabled="False" Click="OnVerifyAnthropic"/>
</Grid>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,16">
<Ellipse x:Name="AnthropicDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock x:Name="AnthropicStatus" Text="키 미설정" Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
</StackPanel>
<!-- Codex CLI (OAuth) -->
<TextBlock Text="Codex CLI (OAuth · API 키 불필요)" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,16">
<Ellipse x:Name="CodexDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock x:Name="CodexStatus" Text="확인 중…" Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
<Button x:Name="CodexVerifyBtn" Content="테스트" Style="{StaticResource FsSecondaryButtonStyle}"
Margin="12,0,0,0" Padding="10,4" IsEnabled="False" Click="OnVerifyCodex" VerticalAlignment="Center"/>
</StackPanel>
<!-- 모델 -->
<TextBlock Text="모델 (선택)" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
<TextBox x:Name="ModelBox" Style="{StaticResource FsPathInputStyle}"/>
<TextBlock Text="비워두면 기본 모델 (OpenAI: gpt-4o-mini, Anthropic: claude-3-5-sonnet-latest)"
Style="{StaticResource FsCaptionStyle}" Margin="0,6,0,0"/>
</StackPanel>
<!-- Anthropic -->
<TextBlock Text="Anthropic API Key" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
<Grid Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<PasswordBox x:Name="AnthropicKeyBox" Grid.Column="0"
Style="{StaticResource FsPasswordInputStyle}"
PasswordChanged="OnAnthropicKeyChanged"/>
<Button x:Name="AnthropicVerifyBtn" Grid.Column="1" Content="확인"
Style="{StaticResource FsSecondaryButtonStyle}" Margin="8,0,0,0"
IsEnabled="False" Click="OnVerifyAnthropic"/>
</Grid>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,16">
<Ellipse x:Name="AnthropicDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock x:Name="AnthropicStatus" Text="키 미설정" Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
</StackPanel>
<!-- Codex CLI (OAuth) -->
<TextBlock Text="Codex CLI (OAuth · API 키 불필요)" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,16">
<Ellipse x:Name="CodexDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock x:Name="CodexStatus" Text="확인 중…" Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
<Button x:Name="CodexVerifyBtn" Content="테스트" Style="{StaticResource FsSecondaryButtonStyle}"
Margin="12,0,0,0" Padding="10,4" IsEnabled="False" Click="OnVerifyCodex" VerticalAlignment="Center"/>
</StackPanel>
<!-- 모델 -->
<TextBlock Text="모델 (선택)" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
<TextBox x:Name="ModelBox" Style="{StaticResource FsPathInputStyle}"/>
<TextBlock Text="비워두면 기본 모델 (OpenAI: gpt-4o-mini, Anthropic: claude-3-5-sonnet-latest)"
Style="{StaticResource FsCaptionStyle}" Margin="0,6,0,0"/>
</StackPanel>
</Border>
</Border>
<!-- ===== 외부 도구 카드 ===== -->
<Border Background="{StaticResource FsBgSurface}"
BorderBrush="{StaticResource FsBorderHairline}" BorderThickness="1"
CornerRadius="10" Padding="20">
<StackPanel>
<TextBlock Text="외부 도구" Style="{StaticResource FsBodyStyle}" FontWeight="SemiBold" FontSize="15"/>
<TextBlock Text="설치하면 영상/오디오·한글/Word 변환이 자동 활성화됩니다."
Style="{StaticResource FsCaptionStyle}" TextWrapping="Wrap" Margin="0,4,0,12"/>
<CheckBox x:Name="GpuToggle" Content="영상 변환 시 GPU 가속(NVENC) 시도 — 없으면 CPU 자동 전환"
Foreground="{StaticResource FsTextSecondary}" IsChecked="True" Margin="0,0,0,16"/>
<Border Style="{StaticResource FsDoubleBezelShellStyle}">
<Border Style="{StaticResource FsDoubleBezelCoreStyle}" Padding="20">
<StackPanel>
<TextBlock Text="외부 도구" Style="{StaticResource FsBodyStyle}" FontWeight="SemiBold" FontSize="15"/>
<TextBlock Text="설치하면 영상/오디오·한글/Word 변환이 자동 활성화됩니다."
Style="{StaticResource FsCaptionStyle}" TextWrapping="Wrap" Margin="0,4,0,12"/>
<CheckBox x:Name="GpuToggle" Content="영상 변환 시 GPU 가속(NVENC) 시도 — 없으면 CPU 자동 전환"
Foreground="{StaticResource FsTextSecondary}" IsChecked="True" Margin="0,0,0,16"/>
<!-- FFmpeg -->
<Grid Margin="0,0,0,14">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse x:Name="FfmpegDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsStatusWarn}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock Text="FFmpeg" Style="{StaticResource FsBodyStyle}" FontWeight="Medium" VerticalAlignment="Center"/>
<TextBlock x:Name="FfmpegStatus" Text="미설치" Style="{StaticResource FsCaptionStyle}" Margin="8,0,0,0" VerticalAlignment="Center"/>
<!-- FFmpeg -->
<Grid Margin="0,0,0,14">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse x:Name="FfmpegDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsStatusWarn}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock Text="FFmpeg" Style="{StaticResource FsBodyStyle}" FontWeight="Medium" VerticalAlignment="Center"/>
<TextBlock x:Name="FfmpegStatus" Text="미설치" Style="{StaticResource FsCaptionStyle}" Margin="8,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="영상/오디오 변환 (mp4·webm·mp3·flac…)" Style="{StaticResource FsCaptionStyle}" Margin="15,3,0,0"/>
</StackPanel>
<TextBlock Text="영상/오디오 변환 (mp4·webm·mp3·flac…)" Style="{StaticResource FsCaptionStyle}" Margin="15,3,0,0"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Button Content="다운로드" Style="{StaticResource FsSecondaryButtonStyle}" Click="OnDownloadFfmpeg"/>
<Button Content="폴더 열기" Style="{StaticResource FsSecondaryButtonStyle}" Margin="6,0,0,0" Click="OnOpenFfmpegFolder"/>
</StackPanel>
</Grid>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Button Content="다운로드" Style="{StaticResource FsSecondaryButtonStyle}" Click="OnDownloadFfmpeg"/>
<Button Content="폴더 열기" Style="{StaticResource FsSecondaryButtonStyle}" Margin="6,0,0,0" Click="OnOpenFfmpegFolder"/>
</StackPanel>
</Grid>
<!-- LibreOffice -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse x:Name="LibreDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsStatusWarn}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock Text="LibreOffice" Style="{StaticResource FsBodyStyle}" FontWeight="Medium" VerticalAlignment="Center"/>
<TextBlock x:Name="LibreStatus" Text="미설치" Style="{StaticResource FsCaptionStyle}" Margin="8,0,0,0" VerticalAlignment="Center"/>
<!-- LibreOffice -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse x:Name="LibreDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsStatusWarn}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock Text="LibreOffice" Style="{StaticResource FsBodyStyle}" FontWeight="Medium" VerticalAlignment="Center"/>
<TextBlock x:Name="LibreStatus" Text="미설치" Style="{StaticResource FsCaptionStyle}" Margin="8,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="한글/Word/문서 변환 (HWP·DOCX→PDF/이미지). 한글은 H2Orestart 확장 필요." Style="{StaticResource FsCaptionStyle}" Margin="15,3,0,0" TextWrapping="Wrap"/>
</StackPanel>
<TextBlock Text="한글/Word/문서 변환 (HWP·DOCX→PDF/이미지). 한글은 H2Orestart 확장 필요." Style="{StaticResource FsCaptionStyle}" Margin="15,3,0,0" TextWrapping="Wrap"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Button Content="다운로드" Style="{StaticResource FsSecondaryButtonStyle}" Click="OnDownloadLibre"/>
</StackPanel>
</Grid>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Button Content="다운로드" Style="{StaticResource FsSecondaryButtonStyle}" Click="OnDownloadLibre"/>
</StackPanel>
</Grid>
</StackPanel>
</Border>
</Border>
</StackPanel>