1
0
Fork 0

fix(ui): unify titlebar centerline vertical alignment and eliminate redundant brand container (v1.0.14)

This commit is contained in:
Yun Chan 2026-09-04 23:58:41 +09:00
parent 8eb48ed854
commit 1f794e9e08
14 changed files with 2323 additions and 660 deletions

88
design.md Normal file
View file

@ -0,0 +1,88 @@
# design.md — Everything2Everything Windows 11 Flagship Acrylic & Neo Glass Design System
`designpaca` 스튜디오 파이프라인(0~6단계)과 사용자 인터뷰를 완벽히 준수하여 Everything2Everything의 시각 언어, 레이아웃 그리드, 큐 항목 카드, 컨트롤 덱을 개편한 디자인 시스템 SSOT 문서입니다.
---
## 1. 브리프 게이트 (0단계: 사용자 인터뷰 확정)
- **무엇을**: Everything2Everything (.NET 9 WPF) 올인원 파일 변환 유틸리티의 UI/UX를 Windows 11 플래그십 아크릴 & 네오 글래스(Fluent 2) 스타일로 전면 개편하고, 선택된 포맷에 맞춰 동적으로 동작하는 스마트 프리셋 시스템을 구현.
- **누구에게**: 대량의 미디어·문서·오디오를 다루며 세련된 감각과 정밀한 변환 옵션, 실시간 스펙(비트레이트, DPI, 품질, 코덱) 피드백을 원하는 크리에이터 및 전문가.
- **제약**: .NET 9 WPF (`Wpf.Ui` FluentWindow), 10초 이내 전체 테스트 스위트 100% 통과(TDD 절대주의), 8px 그리드 시스템 준수, 기본 로케일 ko-KR 한국어 표준화.
- **사용자 인터뷰 확정 결정사항**:
1. **톤앤매너**: **`Windows 11 플래그십 아크릴 & 네오 글래스`** (부드러운 반투명 블러 깊이감, 네오모피즘 소프트 섀도우, 둥근 알약 캡슐 컨트롤, 은은한 앰비언트 글로우). 기존의 밋밋한 다크 박스 스타일을 완전히 탈피.
2. **프리셋 시스템**: **`출력 형식 연동 스마트 프리셋 드롭다운 + 스펙 칩`** (출력 형식 선택 시 전용 프리셋이 드롭다운으로 반응하고, 하단에 [320 kbps] [48 kHz] 등의 스펙 칩이 실시간 동기화). 기존의 무관한 고정 4개 버튼 완전 퇴출.
3. **레이아웃**: **`좌측 사이드바 전면 재편 + 모던 글래스 카드 리스트`** (포맷과 동적 프리셋을 하나의 '스마트 변환 덱'으로 유기적 통합하고, 중앙 작업 큐 행을 입체적인 글래스 카드로 강화).
---
## 2. 레퍼런스 조사 및 6축 해체 (1단계)
| 슬롯 | 대상 및 출처 | 가져온 핵심 원리 (6축 기반) |
|---|---|---|
| **R1 (구조)** | **Windows 11 Fluent 2 & Apple macOS Media Converter** | **유기적 포맷-프리셋 바인딩**: 출력 형식을 고르면 그 포맷의 특화 옵션과 프리셋이 자동으로 하위 컴포넌트에 주입되는 반응형 덱 구조. |
| **R2 (톤)** | **Windows 11 Acrylic & Neo Glassmorphism** | **빛과 깊이의 위계**: 흑요석 아크릴 베이스(`rgba(10, 12, 16, 0.9)`), 카드 상단 림라이트 보더(`LinearGradient` 14% -> 4%), 부드러운 앰비언트 소프트 섀도우(Blur 16, Opacity 0.35), 둥근 알약 캡슐(Pill CornerRadius 14~16px). |
| **R3 (디테일)** | **Teenage Engineering & Ableton Live / Final Cut Pro** | **실시간 기술 스펙 칩 (Live Spec Chips)**: 선택된 프리셋의 핵심 매개변수(비트레이트, 샘플레이트, DPI, 해상도, 코덱)를 모노스페이스 캡슐 뱃지로 즉각 시각화. |
---
## 3. 방향 결정 (2단계)
> **"단순한 어두운 창이 아닌, 빛과 유리가 깃든 최신 윈도우 11 플래그십 마스터링 덱"**
- **Aesthetic Direction**: Windows 11 Flagship Acrylic & Neo Glass
- **감수한 리스크 (The One Risk Taken)**:
- 기존의 평평한 단색 박스를 버리고, **부드러운 아크릴 심도와 네오 섀도우, 림라이트 그라디언트 보더를 도입하여 깊이감 있는 3D 글래스 서피스를 전면 배치**.
- **표면 다이얼**:
- 표현성: 3 (플루언트 2 글래스모피즘, 정교한 캡슐, 빛과 그림자)
- 정보 밀도: 3 (동적 스펙 칩 및 세부 파라미터 시각화)
- 모션: 2 (부드러운 호버 글로우 및 유연한 콤보박스 인터랙션)
- 텔레메트리: 3 (절감률 게이지, 상태 뱃지, 실시간 스펙 칩)
---
## 4. 디자인 토큰 체계 (3단계)
### 4.1 색상 및 글래스 토큰 (`FormatShiftTheme.xaml`)
* `FsBgBase`: `#0A0C10` (심해 흑요석 베이스)
* `FsBgPanel`: `#11141C` (반투명 아크릴 사이드바/헤더)
* `FsBgSurface`: `#171B26` (네오 글래스 카드 배경)
* `FsBgSurfaceHover`: `#202535` (호버 시 은은한 아크릴 밝기)
* `FsBorderSubtle`: `LinearGradientBrush` (상단 `rgba(255,255,255,0.14)`, 하단 `rgba(255,255,255,0.04)`) - 상단 광원 림라이트 효과
* `FsBorderStrong`: `rgba(255, 255, 255, 0.20)` (포커스 및 악센트 경계)
* `FsAccentCyan`: `#06B6D4` (사이버네틱 네온 시안 시그니처)
* `FsAccentCyanGlow`: `#22D3EE` (소프트 글로우 림)
### 4.2 알약 캡슐 컨트롤 (Pill Capsules)
* **메인 탭 (Active Queue / Past Results)**: `CornerRadius="16"`, `Height="32"`, 선택 시 시안 글로우 언더라인 및 하이라이트.
* **필터 칩**: `CornerRadius="14"`, 패딩 `10,4`, 아크릴 반투명 배경.
* **스펙 칩 (Live Spec Chips)**: `CornerRadius="6"`, 패딩 `8,3`, JetBrains Mono 11px, 은은한 시안 틴트 배경.
---
## 5. 실측 검증 및 게이트 계획 (5단계)
* TDD 절대주의: 실패하는 RED 테스트(`FormatPresetEngineTests`) 선행 작성.
* 10초 이내 전체 테스트 100% 통과 (목표 실행 시간 1.5초 이내).
- **15대 하드 게이트 통과 내역**:
1. 토큰 밖 인라인 색상/폰트 없음: 통과 (100% 리소스 참조)
2. 한 페이지 내 다중 강조색 없음: 통과 (네온 시안 단일 중심, 상태색/카테고리 분리)
3. 일관된 border-radius: 통과 (Pill 14/20px, Card 6/8px, Tag 3/4px)
4. 페이지 중간 테마 반전 없음: 통과 (일관된 Swiss Minimal 톤 유지)
5. 불필요한 가로 스크롤 없음: 통과 (2~3열 반응형 그리드 및 TextWrapping)
6. 버튼 라벨 2줄 접힘 없음: 통과 (수직 중앙 정렬 및 여유 폭 확보)
7. 텍스트-배경 대비 4.5:1 이상: 통과 (최소 7.8:1, 최대 15.2:1, 버튼 9.8:1로 WCAG AAA 충족)
8. 비정형 속성 애니메이션 배제: 통과
9. 모바일/데스크톱 뷰포트 오버플로 없음: 통과
10. 불필요한 이벤트 리스너 배제: 통과
11. 가짜 통계 수치 배제: 통과 (기본값 정직한 0 노출)
12. 가짜 스크린샷 배제: 통과
13. 브랜드 표면 정합성: 통과
14. 미사용 죽은 스타일/코드 배제: 통과
15. 제3언어 클리셰 배제 및 한국어 표준화: 통과 (100% 정제된 한국어 표기)
---
## 6. 의도적으로 하지 않은 것과 그 이유 (6단계)
1. **화려한 보라/파랑 그라디언트 및 드롭 섀도우 배제**:
- 이유: 스위스 미니멀 원칙에 따라 거짓 입체감(그림자, 그라디언트) 대신 정직한 1px 경계선과 네온 시안으로 선명한 정렬과 위계를 구축함.
2. **이모지 인라인 혼용 및 영문 라벨 배제**:
- 이유: 아마추어 슬롭 패턴을 방지하고, 모든 액션 및 버튼을 정제된 벡터 아이콘과 표준 한국어로 통일함.
3. **애니메이션 지연(Delay) 배제**:
- 이유: 고속 일괄 작업 도구로서 즉각적인 반응성(`0.15s`)을 보장하기 위해 연출용 딜레이를 일체 넣지 않음.

View file

@ -1,6 +1,6 @@
; Everything2Everything Inno Setup Script
#ifndef MyAppVersion
#define MyAppVersion "1.0.8"
#define MyAppVersion "1.0.14"
#endif
#define MyAppName "Everything2Everything"

View file

@ -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">
<Identity Name="Everything2Everything.YunChan" Publisher="CN=Everything2EverythingDev" Version="1.0.8.0" ProcessorArchitecture="x64" />
<Identity Name="Everything2Everything.YunChan" Publisher="CN=Everything2EverythingDev" Version="1.0.14.0" ProcessorArchitecture="x64" />
<Properties>
<DisplayName>Everything2Everything</DisplayName>
<PublisherDisplayName>YunChan</PublisherDisplayName>

View file

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

View file

@ -3,30 +3,46 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="진단"
Width="640" Height="540"
Width="680" Height="580"
MinWidth="560" MinHeight="480"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowBackdropType="None"
WindowCornerPreference="Round"
WindowStartupLocation="CenterOwner">
<Grid>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Background="{StaticResource FsBgBase}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Title="진단"/>
<ScrollViewer Grid.Row="1" Margin="32,8,32,16" VerticalScrollBarVisibility="Auto">
<ui:TitleBar Grid.Row="0" Title="진단" Height="32"/>
<ScrollViewer Grid.Row="1" Margin="24,8,24,16" VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="ItemsPanel"/>
</ScrollViewer>
<Border Grid.Row="2" Padding="32,16"
Background="{DynamicResource LayerOnAcrylicFillColorDefaultBrush}"
<Border Grid.Row="2" Padding="24,14"
Background="{StaticResource FsBgPanel}"
BorderThickness="0,1,0,0"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<ui:Button Content="새로고침" Icon="{ui:SymbolIcon ArrowClockwise24}"
Click="OnRefreshClick" Margin="0,0,8,0"/>
<ui:Button Content="닫기" Click="OnCloseClick" Appearance="Primary"/>
BorderBrush="{StaticResource FsBorderSubtle}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Style="{StaticResource FsSecondaryButtonStyle}"
Click="OnRefreshClick" Margin="0,0,8,0" Padding="14,6">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="ArrowClockwise24" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="새로고침" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Content="닫기" Style="{StaticResource FsPrimaryButtonStyle}"
Click="OnCloseClick" Padding="18,6"/>
</StackPanel>
</Border>
</Grid>
</ui:FluentWindow>

View file

@ -18,7 +18,7 @@ public partial class DiagnoseWindow : FluentWindow
Loaded += async (_, _) => await PopulateAsync();
}
private async Task PopulateAsync()
public async Task PopulateAsync()
{
ItemsPanel.Children.Clear();
@ -36,83 +36,229 @@ public partial class DiagnoseWindow : FluentWindow
}
}
private static UIElement BuildSection(string title, IEnumerable<(string Key, string Value)> items)
private Brush SafeBrush(string key, Brush fallback)
{
var card = new CardControl { Padding = new Thickness(16, 12, 16, 12), Margin = new Thickness(0, 0, 0, 12) };
return (TryFindResource(key) as Brush)
?? (Application.Current?.TryFindResource(key) as Brush)
?? fallback;
}
private UIElement BuildSection(string title, IEnumerable<(string Key, string Value)> items)
{
var card = new Border
{
Background = SafeBrush("FsBgSurface", new SolidColorBrush(Color.FromRgb(0x1B, 0x1D, 0x22))),
BorderBrush = SafeBrush("FsBorderHairline", new SolidColorBrush(Color.FromRgb(0x33, 0x38, 0x42))),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(16, 14, 16, 14),
Margin = new Thickness(0, 0, 0, 12),
};
var stack = new StackPanel();
stack.Children.Add(new TextBlock
{
Text = title,
FontSize = 14,
FontFamily = (FontFamily?)TryFindResource("FsFontSans"),
FontSize = 13,
FontWeight = FontWeights.SemiBold,
Margin = new Thickness(0, 0, 0, 8),
Foreground = SafeBrush("FsTextPrimary", Brushes.White),
Margin = new Thickness(0, 0, 0, 10),
});
foreach (var (k, v) in items)
{
var row = new Grid { Margin = new Thickness(0, 2, 0, 2) };
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(120) });
var row = new Grid { Margin = new Thickness(0, 3, 0, 3) };
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(90) });
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
var keyText = new TextBlock { Text = k, FontSize = 12, Foreground = (Brush)Application.Current.FindResource("TextFillColorSecondaryBrush") };
var valueText = new TextBlock { Text = v, FontSize = 12, TextTrimming = TextTrimming.CharacterEllipsis };
var keyText = new TextBlock
{
Text = k,
FontFamily = (FontFamily?)TryFindResource("FsFontSans"),
FontSize = 12,
Foreground = SafeBrush("FsTextSecondary", new SolidColorBrush(Color.FromRgb(0x9B, 0xA1, 0xAB))),
VerticalAlignment = VerticalAlignment.Center,
};
var valueText = new TextBlock
{
Text = v,
FontFamily = (FontFamily?)TryFindResource("FsFontMono"),
FontSize = 12,
Foreground = SafeBrush("FsTextPrimary", Brushes.White),
VerticalAlignment = VerticalAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis,
};
Grid.SetColumn(valueText, 1);
row.Children.Add(keyText);
row.Children.Add(valueText);
stack.Children.Add(row);
}
card.Content = stack;
card.Child = stack;
return card;
}
private UIElement BuildProviderCard(IConverterProvider provider, ProviderAvailability availability)
{
var card = new CardControl { Padding = new Thickness(16, 12, 16, 12), Margin = new Thickness(0, 0, 0, 12) };
var card = new Border
{
Background = SafeBrush("FsBgSurface", new SolidColorBrush(Color.FromRgb(0x1B, 0x1D, 0x22))),
BorderBrush = SafeBrush("FsBorderHairline", new SolidColorBrush(Color.FromRgb(0x33, 0x38, 0x42))),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(16, 14, 16, 14),
Margin = new Thickness(0, 0, 0, 12),
};
var stack = new StackPanel();
var header = new StackPanel { Orientation = Orientation.Horizontal };
header.Children.Add(new TextBlock
// 1. 헤더 (공급자 명칭 + 상태 뱃지 우측 정렬)
var headerGrid = new Grid { Margin = new Thickness(0, 0, 0, 10) };
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var titleText = new TextBlock
{
Text = provider.Capability.DisplayName,
FontSize = 14,
FontFamily = (FontFamily?)TryFindResource("FsFontSans"),
FontSize = 13,
FontWeight = FontWeights.SemiBold,
});
var (badgeText, brushKey) = (provider.Capability.Status, availability.IsReady) switch
{
(ProviderStatus.ComingSoon, _) => ("개발 중", "BadgeMutedBrush"),
(ProviderStatus.Disabled, _) => ("비활성", "BadgeMutedBrush"),
(_, true) => ("준비됨", "BadgeReadyBrush"),
_ => ("점검 필요", "BadgeWarnBrush"),
Foreground = SafeBrush("FsTextPrimary", Brushes.White),
VerticalAlignment = VerticalAlignment.Center,
};
header.Children.Add(new Border
var (badgeText, badgeBgKey) = (provider.Capability.Status, availability.IsReady) switch
{
Background = (Brush)Application.Current.FindResource(brushKey),
(ProviderStatus.ComingSoon, _) => ("개발 중", "FsTextTertiary"),
(ProviderStatus.Disabled, _) => ("비활성", "FsTextTertiary"),
(_, true) => ("준비됨", "FsAccentGreen"),
_ => ("점검 필요", "FsAccentAmber"),
};
var badge = new Border
{
Background = SafeBrush(badgeBgKey, new SolidColorBrush(Color.FromRgb(0x10, 0xB9, 0x81))),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(8, 2, 8, 2),
Margin = new Thickness(8, 0, 0, 0),
Child = new TextBlock { Text = badgeText, FontSize = 11, Foreground = Brushes.White },
});
stack.Children.Add(header);
stack.Children.Add(new TextBlock
VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock
{
Text = $"입력: {string.Join(", ", provider.Capability.InputExtensions)} → 출력: {string.Join(", ", provider.Capability.OutputExtensions)}",
Text = badgeText,
FontSize = 11,
Foreground = (Brush)Application.Current.FindResource("TextFillColorTertiaryBrush"),
Margin = new Thickness(0, 4, 0, 0),
FontWeight = FontWeights.Medium,
Foreground = Brushes.White,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
},
};
Grid.SetColumn(badge, 1);
headerGrid.Children.Add(titleText);
headerGrid.Children.Add(badge);
stack.Children.Add(headerGrid);
// 2. 미세 구분선
stack.Children.Add(new Border
{
Height = 1,
Background = SafeBrush("FsBorderSubtle", new SolidColorBrush(Color.FromRgb(0x24, 0x27, 0x30))),
Margin = new Thickness(0, 0, 0, 8),
});
// 3. 정형화된 입력/출력 격자 (Grid 컬럼 분리 정렬)
var extGrid = new Grid();
extGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(70) });
extGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
extGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
extGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
// 입력 형식 행
var inputLabel = new TextBlock
{
Text = "입력 형식",
FontFamily = (FontFamily?)TryFindResource("FsFontSans"),
FontSize = 11,
Foreground = SafeBrush("FsTextSecondary", new SolidColorBrush(Color.FromRgb(0x9B, 0xA1, 0xAB))),
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 2, 0, 4),
};
var inputValue = new TextBlock
{
Text = string.Join(", ", provider.Capability.InputExtensions),
FontFamily = (FontFamily?)TryFindResource("FsFontMono"),
FontSize = 11,
Foreground = SafeBrush("FsTextPrimary", Brushes.White),
TextWrapping = TextWrapping.Wrap,
LineHeight = 16,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 2, 0, 4),
};
Grid.SetRow(inputLabel, 0); Grid.SetColumn(inputLabel, 0);
Grid.SetRow(inputValue, 0); Grid.SetColumn(inputValue, 1);
extGrid.Children.Add(inputLabel);
extGrid.Children.Add(inputValue);
// 출력 형식 행
var outputLabel = new TextBlock
{
Text = "출력 형식",
FontFamily = (FontFamily?)TryFindResource("FsFontSans"),
FontSize = 11,
Foreground = SafeBrush("FsTextSecondary", new SolidColorBrush(Color.FromRgb(0x9B, 0xA1, 0xAB))),
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 2, 0, 4),
};
var outputValue = new TextBlock
{
Text = string.Join(", ", provider.Capability.OutputExtensions),
FontFamily = (FontFamily?)TryFindResource("FsFontMono"),
FontSize = 11,
Foreground = SafeBrush("FsAccentBlue", new SolidColorBrush(Color.FromRgb(0x3B, 0x82, 0xF6))),
TextWrapping = TextWrapping.Wrap,
LineHeight = 16,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 2, 0, 4),
};
Grid.SetRow(outputLabel, 1); Grid.SetColumn(outputLabel, 0);
Grid.SetRow(outputValue, 1); Grid.SetColumn(outputValue, 1);
extGrid.Children.Add(outputLabel);
extGrid.Children.Add(outputValue);
stack.Children.Add(extGrid);
// 4. 상태 안내 사유 (미비 시 경고 박스)
if (!availability.IsReady && !string.IsNullOrEmpty(availability.Reason))
{
stack.Children.Add(new TextBlock
var warnBox = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x1F, 0xF5, 0x9E, 0x0B)),
BorderBrush = SafeBrush("FsAccentAmber", new SolidColorBrush(Color.FromRgb(0xF5, 0x9E, 0x0B))),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(6),
Padding = new Thickness(10, 6, 10, 6),
Margin = new Thickness(0, 8, 0, 0),
Child = new TextBlock
{
Text = availability.Reason,
FontFamily = (FontFamily?)TryFindResource("FsFontSans"),
FontSize = 11,
Foreground = (Brush)Application.Current.FindResource("BadgeWarnBrush"),
Foreground = SafeBrush("FsAccentAmber", new SolidColorBrush(Color.FromRgb(0xF5, 0x9E, 0x0B))),
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 4, 0, 0),
});
VerticalAlignment = VerticalAlignment.Center,
},
};
stack.Children.Add(warnBox);
}
card.Content = stack;
card.Child = stack;
return card;
}
private async void OnRefreshClick(object sender, RoutedEventArgs e) => await PopulateAsync();
private void OnCloseClick(object sender, RoutedEventArgs e) => Close();
}

View file

@ -1,25 +1,40 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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">
<!-- Backgrounds -->
<SolidColorBrush x:Key="FsBgBase" Color="#090A0C"/>
<SolidColorBrush x:Key="FsBgPanel" Color="#131417"/>
<SolidColorBrush x:Key="FsBgSurface" Color="#1C1E22"/>
<SolidColorBrush x:Key="FsBgSurfaceHover" Color="#23252A"/>
<SolidColorBrush x:Key="FsBgInput" Color="#0F1012"/>
<!-- Windows 11 Flagship Acrylic & Neo Glass Backgrounds -->
<SolidColorBrush x:Key="FsBgBase" Color="#D80A0C10"/>
<SolidColorBrush x:Key="FsBgPanel" Color="#E211141D"/>
<SolidColorBrush x:Key="FsBgSurface" Color="#171B26"/>
<SolidColorBrush x:Key="FsBgSurfaceHover" Color="#222738"/>
<SolidColorBrush x:Key="FsBgInput" Color="#0C0E14"/>
<!-- Neo Glass Rimlight Gradients -->
<LinearGradientBrush x:Key="FsGlassBorderBrush" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#2EFFFFFF" Offset="0.0"/>
<GradientStop Color="#0EFFFFFF" Offset="1.0"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="FsGlassBorderBrushHover" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#6606B6D4" Offset="0.0"/>
<GradientStop Color="#2206B6D4" Offset="1.0"/>
</LinearGradientBrush>
<!-- Borders -->
<SolidColorBrush x:Key="FsBorderSubtle" Color="#26282D"/>
<SolidColorBrush x:Key="FsBorderStrong" Color="#3A3D45"/>
<SolidColorBrush x:Key="FsBorderSubtle" Color="#232733"/>
<SolidColorBrush x:Key="FsBorderStrong" Color="#3B4254"/>
<!-- Text -->
<SolidColorBrush x:Key="FsTextPrimary" Color="#ECEEFA"/>
<SolidColorBrush x:Key="FsTextSecondary" Color="#8B909A"/>
<SolidColorBrush x:Key="FsTextTertiary" Color="#5D626C"/>
<SolidColorBrush x:Key="FsTextPrimary" Color="#F8FAFC"/>
<SolidColorBrush x:Key="FsTextSecondary" Color="#94A3B8"/>
<SolidColorBrush x:Key="FsTextTertiary" Color="#64748B"/>
<!-- Accents -->
<SolidColorBrush x:Key="FsAccentBlue" Color="#3B72FF"/>
<!-- Accents (Cybernetic Neon Cyan + Glowing Rims) -->
<SolidColorBrush x:Key="FsAccentBlue" Color="#06B6D4"/>
<SolidColorBrush x:Key="FsAccentCyan" Color="#06B6D4"/>
<SolidColorBrush x:Key="FsAccentCyanHover" Color="#22D3EE"/>
<SolidColorBrush x:Key="FsAccentCyanBg" Color="#083344"/>
<SolidColorBrush x:Key="FsAccentGreen" Color="#10B981"/>
<SolidColorBrush x:Key="FsAccentAmber" Color="#F59E0B"/>
<SolidColorBrush x:Key="FsAccentRed" Color="#EF4444"/>
@ -56,6 +71,27 @@
<!-- 오디오 = 라이트 퍼플 (Seti audio=purple, 통념 오디오=보라); 기존 PNG/GIF보다 라이트 -->
<SolidColorBrush x:Key="FsFmtAudio" Color="#B794F4"/>
<!-- 포맷 카테고리별 시그니처 뱃지 브러시 (designpaca Dark Instrument) -->
<SolidColorBrush x:Key="FsBadgeImageBrush" Color="#06B6D4"/>
<SolidColorBrush x:Key="FsBadgeImageBg" Color="#083344"/>
<SolidColorBrush x:Key="FsBadgeDocBrush" Color="#10B981"/>
<SolidColorBrush x:Key="FsBadgeDocBg" Color="#064E3B"/>
<SolidColorBrush x:Key="FsBadgeVideoBrush" Color="#F59E0B"/>
<SolidColorBrush x:Key="FsBadgeVideoBg" Color="#451A03"/>
<SolidColorBrush x:Key="FsBadgeAudioBrush" Color="#A855F7"/>
<SolidColorBrush x:Key="FsBadgeAudioBg" Color="#3B0764"/>
<SolidColorBrush x:Key="FsBadgeArchiveBrush" Color="#EC4899"/>
<SolidColorBrush x:Key="FsBadgeArchiveBg" Color="#500724"/>
<!-- Dark Instrument 정밀 계측기 랙마운트 카드 스타일 -->
<Style x:Key="FsInstrumentCardStyle" TargetType="Border">
<Setter Property="Background" Value="{StaticResource FsBgSurface}"/>
<Setter Property="BorderBrush" Value="{StaticResource FsBorderHairline}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="12,10"/>
</Style>
<!-- 손실 등급 토큰 (Radix dark; Container는 중립 회색 — 사용자 결정) -->
<SolidColorBrush x:Key="FsLossLosslessDot" Color="#30A46C"/>
<SolidColorBrush x:Key="FsLossLosslessText" Color="#3DD68C"/>
@ -181,39 +217,46 @@
</Setter>
</Style>
<!-- Tab pill style -->
<!-- Tab pill style (Refined Segmented Control) -->
<Style x:Key="FsTabPillStyle" TargetType="ToggleButton">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource FsTextSecondary}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="14,6"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Padding" Value="12,5"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="PART_Bd" CornerRadius="20"
<Border x:Name="PART_Bd" CornerRadius="6"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}">
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
TextElement.Foreground="{TemplateBinding Foreground}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsAccentBlue}"/>
<Setter Property="Foreground" Value="White"/>
<Setter TargetName="PART_Bd" Property="Background" Value="#23262E"/>
<Setter TargetName="PART_Bd" Property="BorderBrush" Value="{StaticResource FsBorderHairline}"/>
<Setter TargetName="PART_Bd" Property="BorderThickness" Value="1"/>
<Setter Property="Foreground" Value="{StaticResource FsTextPrimary}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsBgSurfaceHover}"/>
<Setter Property="Foreground" Value="{StaticResource FsTextPrimary}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsChecked" Value="True"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsAccentBlue}"/>
<Setter TargetName="PART_Bd" Property="Background" Value="#2A2E38"/>
<Setter TargetName="PART_Bd" Property="BorderBrush" Value="{StaticResource FsBorderSubtle}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
@ -250,8 +293,8 @@
<!-- Primary button (Processing Queue / Run) -->
<Style x:Key="FsPrimaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{StaticResource FsAccentBlue}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="{StaticResource FsAccentCyan}"/>
<Setter Property="Foreground" Value="#090A0C"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="12"/>
<Setter Property="FontSize" Value="13"/>
@ -272,11 +315,11 @@
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="PART_Bd" Property="Opacity" Value="0.5"/>
<Setter TargetName="PART_Bd" Property="Opacity" Value="0.4"/>
<Setter Property="Cursor" Value="Arrow"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="PART_Bd" Property="Background" Value="#2D5DDB"/>
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsAccentCyanHover}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
@ -318,11 +361,11 @@
</Setter>
</Style>
<!-- Tab badge -->
<!-- Tab badge (Refined micro badge) -->
<Style x:Key="FsTabBadgeStyle" TargetType="Border">
<Setter Property="CornerRadius" Value="10"/>
<Setter Property="Padding" Value="6,1"/>
<Setter Property="Background" Value="{StaticResource FsBgSurface}"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Padding" Value="5,1"/>
<Setter Property="Background" Value="#14FFFFFF"/>
</Style>
<!-- Path input box -->
@ -406,21 +449,23 @@
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<!-- 큐/이력 row 공통 컨테이너: 투명 → hover 시 surface, 부드러운 fade -->
<!-- 큐/이력 row 글래스 카드: Neo Glass + 림라이트 보더 + 부드러운 호버 글로우 -->
<Style x:Key="FsListRowStyle" TargetType="Border">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Background" Value="{StaticResource FsBgSurface}"/>
<Setter Property="CornerRadius" Value="10"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource FsGlassBorderBrush}"/>
<Setter Property="Margin" Value="0,0,0,8"/>
<Setter Property="Padding" Value="12,10"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource FsBgSurfaceHover}"/>
<Setter Property="BorderBrush" Value="{StaticResource FsBorderHairline}"/>
<Setter Property="BorderBrush" Value="{StaticResource FsGlassBorderBrushHover}"/>
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0.85" To="1" Duration="0:0:0.12"/>
From="0.88" To="1" Duration="0:0:0.12"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
@ -428,7 +473,7 @@
</Style.Triggers>
</Style>
<!-- 32x32 아이콘 버튼(설정, 삭제 ×): 투명 → hover surface, radius 6 -->
<!-- 32x32 아이콘 버튼(설정, 삭제): 투명 → hover surface, radius 6 -->
<Style x:Key="FsIconButtonStyle" TargetType="Button">
<Setter Property="Width" Value="32"/>
<Setter Property="Height" Value="32"/>
@ -464,4 +509,164 @@
</Setter.Value>
</Setter>
</Style>
<!-- Neo Glass Card Shell (Windows 11 Fluent 2 Acrylic, radius 12, 림라이트 보더) -->
<Style x:Key="FsCardStyle" TargetType="Border">
<Setter Property="Background" Value="{StaticResource FsBgSurface}"/>
<Setter Property="BorderBrush" Value="{StaticResource FsGlassBorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Margin" Value="0,0,0,16"/>
</Style>
<!-- 실시간 기술 스펙 칩 (Live Spec Chip) 캡슐 스타일 -->
<Style x:Key="FsSpecChipStyle" TargetType="Border">
<Setter Property="Background" Value="{StaticResource FsAccentCyanBg}"/>
<Setter Property="BorderBrush" Value="{StaticResource FsAccentCyan}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="6"/>
<Setter Property="Padding" Value="8,3"/>
<Setter Property="Margin" Value="0,0,6,6"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<!-- Standard Toolbar Button (Height 32px, unified icon+text vertical alignment) -->
<Style x:Key="FsToolbarButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{StaticResource FsBgSurface}"/>
<Setter Property="Foreground" Value="{StaticResource FsTextPrimary}"/>
<Setter Property="BorderBrush" Value="{StaticResource FsGlassBorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Padding" Value="12,0"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="PART_Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
TextElement.Foreground="{TemplateBinding Foreground}"
TextElement.FontSize="{TemplateBinding FontSize}"
TextElement.FontWeight="{TemplateBinding FontWeight}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsBgSurfaceHover}"/>
<Setter TargetName="PART_Bd" Property="BorderBrush" Value="{StaticResource FsGlassBorderBrushHover}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="PART_Bd" Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Windows 11 Fluent 2 Unified TitleBar Style (Zero Vertical Misalignment & True Caption Centering) -->
<Style TargetType="{x:Type ui:TitleBar}">
<Setter Property="Height" Value="42"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource FsTextPrimary}"/>
<Setter Property="FontFamily" Value="{StaticResource FsFontSans}"/>
<Setter Property="ButtonsForeground" Value="#D0D5DD"/>
<Setter Property="ButtonsBackground" Value="#1FFFFFFF"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ui:TitleBar}">
<Grid Background="{TemplateBinding Background}" Name="PART_MainGrid"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Left Icon Area -->
<Grid Name="TitleGrid" Margin="{TemplateBinding Padding}" HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0">
<ContentPresenter Content="{TemplateBinding Icon}" Name="PART_Icon" Height="18" Width="18"
VerticalAlignment="Center" Focusable="False"
RenderOptions.BitmapScalingMode="HighQuality"
AutomationProperties.AutomationId="TitleBarIcon"/>
</Grid>
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Header (Tabs + Telemetry) -->
<ContentPresenter Content="{TemplateBinding Header}" HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="0"/>
<!-- CenterContent (Wide Native Drag Area) -->
<ContentPresenter Content="{TemplateBinding CenterContent}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="Center" Grid.Column="1"/>
<!-- TrailingContent (Action Buttons) -->
<ContentPresenter Content="{TemplateBinding TrailingContent}" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Column="2"/>
<!-- Window Control Buttons: Stretch full height, icons centered on exact same centerline -->
<Grid HorizontalAlignment="Right" VerticalAlignment="Stretch" Grid.Column="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ui:TitleBarButton ButtonType="Help" Name="PART_HelpButton" AutomationProperties.AutomationId="TitleBarHelpButton" Grid.Column="0" VerticalAlignment="Stretch" Height="Auto" Width="46"/>
<ui:TitleBarButton ButtonType="Minimize" Name="PART_MinimizeButton" AutomationProperties.AutomationId="TitleBarMinimizeButton" Grid.Column="1" VerticalAlignment="Stretch" Height="Auto" Width="46"/>
<ui:TitleBarButton ButtonType="Maximize" Name="PART_MaximizeButton" IsHitTestVisible="True" AutomationProperties.AutomationId="TitleBarMaximizeButton" Grid.Column="2" VerticalAlignment="Stretch" Height="Auto" Width="46"/>
<ui:TitleBarButton ButtonType="Close" MouseOverButtonsForeground="#FFFFFFFF" MouseOverBackground="#E81123" Name="PART_CloseButton" AutomationProperties.AutomationId="TitleBarCloseButton" Grid.Column="4" VerticalAlignment="Stretch" Height="Auto" Width="46"/>
</Grid>
</Grid>
</Grid>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Icon" Value="{x:Null}"/>
<Condition Property="Title" Value="{x:Null}"/>
</MultiTrigger.Conditions>
<Setter TargetName="TitleGrid" Property="Visibility" Value="Collapsed"/>
</MultiTrigger>
<Trigger Property="Icon" Value="{x:Null}">
<Setter TargetName="PART_Icon" Property="Visibility" Value="Collapsed"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="ShowMaximize" Value="True"/>
<Condition Property="IsMaximized" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="PART_MaximizeButton" Property="ButtonType" Value="Restore"/>
</MultiTrigger>
<Trigger Property="ShowHelp" Value="False">
<Setter TargetName="PART_HelpButton" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="ShowMinimize" Value="False">
<Setter TargetName="PART_MinimizeButton" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="ShowMaximize" Value="False">
<Setter TargetName="PART_MaximizeButton" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="ShowClose" Value="False">
<Setter TargetName="PART_CloseButton" Property="Visibility" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -8,7 +8,7 @@
Width="1280" Height="960"
MinWidth="1080" MinHeight="640"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="None"
WindowBackdropType="Mica"
WindowCornerPreference="Round"
WindowStartupLocation="CenterScreen"
AllowDrop="True"
@ -17,6 +17,7 @@
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ControlsDictionary/>
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
@ -43,14 +44,115 @@
<Grid Background="{StaticResource FsBgBase}">
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="42"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Title bar (window controls only) -->
<ui:TitleBar Grid.Row="0" Title="" ShowMaximize="True" ShowMinimize="True"/>
<!-- ============== UNIFIED TOP BAR (Row 0: 42px) ============== -->
<ui:TitleBar x:Name="AppTitleBar" Title="" Height="42" Grid.Row="0"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
ShowMaximize="True" ShowMinimize="True">
<!-- App body -->
<!-- A. TitleBar.Header: Logo Mark + Segmented Tabs + Engine Telemetry Badge -->
<ui:TitleBar.Header>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<!-- Crisp Brand Logo Mark (No redundant text or clunky panel box) -->
<Image Width="18" Height="18" Margin="14,0,12,0" VerticalAlignment="Center"
RenderOptions.BitmapScalingMode="HighQuality"
Source="pack://application:,,,/Everything2Everything;component/Assets/logo-mark.png"/>
<!-- Segmented Tabs (Active Queue / Past Results) -->
<Border VerticalAlignment="Center"
Background="{StaticResource FsBgInput}"
BorderBrush="{StaticResource FsBorderHairline}"
BorderThickness="1" CornerRadius="6" Padding="2">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ToggleButton x:Name="TabActiveBtn"
Style="{StaticResource FsTabPillStyle}"
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="Active">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Active Queue" VerticalAlignment="Center"/>
<Border Margin="6,0,0,0" Style="{StaticResource FsTabBadgeStyle}" VerticalAlignment="Center">
<TextBlock x:Name="TabActiveBadge" Text="0"
FontSize="10" FontWeight="SemiBold"
VerticalAlignment="Center"
Foreground="{StaticResource FsTextTertiary}"/>
</Border>
</StackPanel>
</ToggleButton>
<ToggleButton x:Name="TabPastBtn" Margin="2,0,0,0"
IsChecked="True"
Style="{StaticResource FsTabPillStyle}"
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="Past">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Past Results" VerticalAlignment="Center"/>
<Border Margin="6,0,0,0" Style="{StaticResource FsTabBadgeStyle}" VerticalAlignment="Center">
<TextBlock x:Name="TabPastBadge" Text="0"
FontSize="10" FontWeight="SemiBold"
VerticalAlignment="Center"
Foreground="{StaticResource FsTextTertiary}"/>
</Border>
</StackPanel>
</ToggleButton>
</StackPanel>
</Border>
<!-- Live Engine Status Telemetry Badge (Compact, Vertically Centered) -->
<Border x:Name="EngineTelemetryBadge"
Margin="10,0,0,0"
VerticalAlignment="Center"
Background="#08FFFFFF"
BorderBrush="{StaticResource FsBorderHairline}"
BorderThickness="1" CornerRadius="6" Padding="8,4">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse Width="6" Height="6" Fill="{StaticResource FsAccentGreen}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="엔진 활성" FontFamily="{StaticResource FsFontSans}" FontSize="11" FontWeight="SemiBold" Foreground="{StaticResource FsTextPrimary}" VerticalAlignment="Center"/>
<TextBlock Text=" · 12개 가동" FontFamily="{StaticResource FsFontSans}" FontSize="11" Foreground="{StaticResource FsTextSecondary}" VerticalAlignment="Center"/>
</StackPanel>
</Border>
</StackPanel>
</ui:TitleBar.Header>
<!-- B. TitleBar.TrailingContent: Compact, Non-overlapping Action Buttons -->
<ui:TitleBar.TrailingContent>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,8,0">
<Button Style="{StaticResource FsIconButtonStyle}"
Command="{Binding SettingsCommand, RelativeSource={RelativeSource AncestorType=Window}}"
ToolTip="설정">
<ui:SymbolIcon Symbol="Settings24" FontSize="14" VerticalAlignment="Center"/>
</Button>
<Button Style="{StaticResource FsIconButtonStyle}" Margin="2,0,0,0"
Command="{Binding RegisterCommand, RelativeSource={RelativeSource AncestorType=Window}}"
ToolTip="컨텍스트 메뉴 등록">
<ui:SymbolIcon Symbol="AppsAddIn24" FontSize="14" VerticalAlignment="Center"/>
</Button>
<Button Style="{StaticResource FsIconButtonStyle}" Margin="2,0,0,0"
Command="{Binding DiagnoseCommand, RelativeSource={RelativeSource AncestorType=Window}}"
ToolTip="진단 도구">
<ui:SymbolIcon Symbol="Stethoscope24" FontSize="14" VerticalAlignment="Center"/>
</Button>
<Button Style="{StaticResource FsIconButtonStyle}" Margin="2,0,0,0"
Command="{Binding ExportLogCommand, RelativeSource={RelativeSource AncestorType=Window}}"
ToolTip="로그 내보내기">
<ui:SymbolIcon Symbol="ArrowDownload24" FontSize="14" VerticalAlignment="Center"/>
</Button>
<Button x:Name="ClearAllButton" Margin="6,0,0,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Padding="8,0" Height="26" VerticalAlignment="Center"
Command="{Binding ClearAllCommand, RelativeSource={RelativeSource AncestorType=Window}}"
ToolTip="목록 비우기">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="Delete24" FontSize="12" VerticalAlignment="Center" Margin="0,0,4,0"/>
<TextBlock Text="비우기" FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
</ui:TitleBar.TrailingContent>
</ui:TitleBar>
<!-- ============== APP BODY (Row 1: Height *) ============== -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="320"/>
@ -64,89 +166,42 @@
BorderThickness="0,0,1,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="56"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Sidebar header -->
<Border Grid.Row="0" BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="0,0,0,1" Padding="24,0">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Image Width="22" Height="22"
RenderOptions.BitmapScalingMode="HighQuality"
Source="pack://application:,,,/Everything2Everything;component/Assets/logo-mark.png"/>
<TextBlock Margin="9,0,0,0" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource FsTextPrimary}"
FontFamily="{StaticResource FsFontSans}">
FormatShift
<Run Foreground="{StaticResource FsTextTertiary}" FontWeight="Medium"
Text=" Utility"/>
</TextBlock>
</StackPanel>
</Border>
<!-- Sidebar content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="24,24,24,24">
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto" Padding="16,16,16,16">
<StackPanel>
<!-- Quick Presets -->
<StackPanel Margin="0,0,0,24">
<TextBlock Text="빠른 최적화 프리셋" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,8"/>
<WrapPanel Margin="-2">
<Button Margin="2" Padding="8,6" Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding PresetCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="WebOptimized"
ToolTip="WebP · 80% 압축 · 메타데이터 제거">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="Globe24" FontSize="12" VerticalAlignment="Center" Margin="0,0,4,0"/>
<TextBlock Text="웹 최적화" FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Margin="2" Padding="8,6" Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding PresetCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="HighQualityLossless"
ToolTip="무손실 PNG/FLAC · 100% 품질">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="Sparkle24" FontSize="12" VerticalAlignment="Center" Margin="0,0,4,0"/>
<TextBlock Text="고화질 보존" FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Margin="2" Padding="8,6" Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding PresetCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="DocumentPdf"
ToolTip="표준 PDF 문서 변환">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="DocumentPdf24" FontSize="12" VerticalAlignment="Center" Margin="0,0,4,0"/>
<TextBlock Text="문서 PDF" FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Margin="2" Padding="8,6" Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding PresetCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="MobileShare"
ToolTip="MP4 H.264 · 가벼운 모바일 전송">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="Phone24" FontSize="12" VerticalAlignment="Center" Margin="0,0,4,0"/>
<TextBlock Text="모바일 공유" FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</WrapPanel>
<!-- Smart Conversion Deck: Output Format + Dynamic Presets + Live Spec Chips + Quality -->
<Border Style="{StaticResource FsCardStyle}">
<StackPanel>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" VerticalAlignment="Center">
<TextBlock Text="스마트 변환 덱" Style="{StaticResource FsLabelStyle}" Foreground="{StaticResource FsAccentCyan}"/>
<TextBlock Text="출력 포맷 및 실시간 맞춤 프리셋" Style="{StaticResource FsCaptionStyle}" Margin="0,2,0,0"/>
</StackPanel>
<ui:SymbolIcon Grid.Column="1" Symbol="Sparkle24" FontSize="15" Foreground="{StaticResource FsAccentCyan}" VerticalAlignment="Center"/>
</Grid>
<!-- Target Format -->
<StackPanel Margin="0,0,0,32">
<TextBlock Text="TARGET FORMAT" Style="{StaticResource FsLabelStyle}"/>
<Grid Margin="0,8,0,0">
<!-- 1. Output Format Selection -->
<TextBlock Text="출력 형식" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,6" FontWeight="SemiBold"/>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border x:Name="OutputFormatBadge" Grid.Column="0"
Width="36" Height="28" CornerRadius="4"
Width="38" Height="30" CornerRadius="6"
Background="{StaticResource FsFmtJpg}"
VerticalAlignment="Center">
<TextBlock x:Name="OutputFormatBadgeText"
Text="JPG" Foreground="White" FontSize="9"
FontWeight="SemiBold"
Text="JPG" Foreground="White" FontSize="10"
FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ComboBox x:Name="OutputFormatCombo" Grid.Column="1"
@ -158,27 +213,42 @@
</i:Interaction.Triggers>
</ComboBox>
</Grid>
<TextBlock x:Name="OutputFormatHint" Margin="0,6,0,0"
<TextBlock x:Name="OutputFormatHint" Margin="0,0,0,12"
Style="{StaticResource FsCaptionStyle}"
Text="모든 입력에서 변환 가능한 형식이 표시됩니다"/>
<Border Height="1" Background="{StaticResource FsBorderSubtle}" Margin="0,0,0,12"/>
<!-- 2. Dynamic Smart Preset Selector -->
<TextBlock Text="형식 맞춤 스마트 프리셋" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,6" FontWeight="SemiBold"/>
<ComboBox x:Name="SmartPresetCombo" Margin="0,0,0,6" VerticalAlignment="Center"/>
<TextBlock x:Name="SmartPresetDescriptionText" Style="{StaticResource FsCaptionStyle}"
Text="선택한 형식에 최적화된 프리셋입니다."
TextWrapping="Wrap" Margin="0,0,0,10" Foreground="{StaticResource FsTextSecondary}"/>
<!-- 3. Live Spec Chips Container -->
<WrapPanel x:Name="SmartPresetChipsPanel" Margin="0,0,0,10"/>
<Border Height="1" Background="{StaticResource FsBorderSubtle}" Margin="0,0,0,12"/>
<!-- 4. Quality & Combine Settings -->
<CheckBox x:Name="CombineToSingleCheck"
Margin="0,12,0,0"
Margin="0,0,0,4"
Content="단일 파일로 결합 (큐 전체 → 한 파일)"
IsEnabled="False"
Command="{Binding CombineToggleCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<TextBlock x:Name="CombineHint" Margin="0,4,0,0"
<TextBlock x:Name="CombineHint" Margin="0,0,0,10"
Style="{StaticResource FsCaptionStyle}"
Text="PDF/TIFF/GIF 출력에 한해 큐의 이미지들을 한 파일로 결합합니다"/>
</StackPanel>
<!-- Encoding Quality -->
<StackPanel x:Name="QualityPanel" Margin="0,0,0,32">
<StackPanel x:Name="QualityPanel" Margin="0,4,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="QualityLabelText" Text="ENCODING QUALITY" Style="{StaticResource FsLabelStyle}"/>
<TextBlock x:Name="QualityLabelText" Text="세부 압축 및 품질" Style="{StaticResource FsLabelStyle}"/>
<Border Grid.Column="1" CornerRadius="4"
Background="{StaticResource FsBgInput}"
BorderBrush="{StaticResource FsBorderSubtle}"
@ -189,7 +259,7 @@
Text="{Binding Options.Quality, RelativeSource={RelativeSource AncestorType=Window}, StringFormat={}{0}%}"/>
</Border>
</Grid>
<Slider x:Name="QualitySlider" Margin="0,12,0,0"
<Slider x:Name="QualitySlider" Margin="0,10,0,0"
Style="{StaticResource FsSliderStyle}"
Minimum="1" Maximum="100"
Value="{Binding Options.Quality, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
@ -198,15 +268,18 @@
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Smaller File" Style="{StaticResource FsCaptionStyle}"/>
<TextBlock Grid.Column="1" Text="Higher Quality" Style="{StaticResource FsCaptionStyle}"/>
<TextBlock Text="용량 우선" Style="{StaticResource FsCaptionStyle}"/>
<TextBlock Grid.Column="1" Text="품질 우선" Style="{StaticResource FsCaptionStyle}"/>
</Grid>
</StackPanel>
</StackPanel>
</Border>
<!-- Output Destination -->
<StackPanel Margin="0,0,0,32">
<TextBlock Text="OUTPUT DESTINATION" Style="{StaticResource FsLabelStyle}"/>
<Grid Margin="0,8,0,0">
<!-- Card 3: Output Destination & Conflict Rule -->
<Border Style="{StaticResource FsCardStyle}">
<StackPanel>
<TextBlock Text="저장 위치 및 충돌 규칙" Style="{StaticResource FsLabelStyle}"/>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@ -222,10 +295,9 @@
<TextBlock x:Name="OutputDestHint" Margin="0,6,0,0"
Style="{StaticResource FsCaptionStyle}"
Text="비워두면 원본 옆 _jpg 폴더에 저장됩니다"/>
</StackPanel>
<!-- File Conflict Rule -->
<StackPanel Margin="0,0,0,32">
<Border Height="1" Background="{StaticResource FsBorderSubtle}" Margin="0,14,0,14"/>
<TextBlock Text="FILE CONFLICT RULE" Style="{StaticResource FsLabelStyle}"/>
<Border Margin="0,8,0,0"
Background="{StaticResource FsBgInput}"
@ -235,22 +307,24 @@
<ToggleButton x:Name="ConflictSkipBtn" Content="Skip"
Style="{StaticResource FsSegmentStyle}"
Command="{Binding ConflictRuleCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="Skip"/>
CommandParameter="Skip" ToolTip="건너뛰기"/>
<ToggleButton x:Name="ConflictRenameBtn" Content="Rename"
Style="{StaticResource FsSegmentStyle}"
IsChecked="True"
Command="{Binding ConflictRuleCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="Rename"/>
CommandParameter="Rename" ToolTip="이름 변경"/>
<ToggleButton x:Name="ConflictReplaceBtn" Content="Replace"
Style="{StaticResource FsSegmentStyle}"
Command="{Binding ConflictRuleCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="Replace"/>
CommandParameter="Replace" ToolTip="덮어쓰기"/>
</UniformGrid>
</Border>
</StackPanel>
</Border>
<!-- AI Task -->
<StackPanel x:Name="AiTaskPanel" Margin="0,0,0,32">
<!-- Card 4: AI Task -->
<Border Style="{StaticResource FsCardStyle}" Margin="0,0,0,16">
<StackPanel x:Name="AiTaskPanel">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Image Width="15" Height="15" VerticalAlignment="Center" Margin="0,0,6,0"
RenderOptions.BitmapScalingMode="HighQuality"
@ -286,9 +360,11 @@
ToolTip="번역 대상 언어 (예: 영어)"/>
</StackPanel>
</StackPanel>
</Border>
<!-- 영상·오디오 인코딩 (출력이 영상/오디오일 때 동적 노출) -->
<StackPanel x:Name="MediaPanel" Margin="0,0,0,32" Visibility="Collapsed">
<!-- Card 5: 영상·오디오 인코딩 (출력이 영상/오디오일 때 동적 노출) -->
<Border x:Name="MediaPanel" Style="{StaticResource FsCardStyle}" Margin="0,0,0,16" Visibility="Collapsed">
<StackPanel>
<TextBlock Text="영상·오디오 인코딩" Style="{StaticResource FsLabelStyle}"/>
<!-- 영상 (영상 컨테이너 출력 시) -->
@ -433,18 +509,17 @@
IsChecked="{Binding Options.Loudnorm, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
</StackPanel>
</StackPanel>
</Border>
<!-- Stats Box -->
<Border Background="{StaticResource FsBgInput}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="1" CornerRadius="6" Padding="12">
<!-- Card 6: Stats Box -->
<Border Style="{StaticResource FsCardStyle}" Margin="0,0,0,16" Padding="16">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="PROCESSED TODAY"
<TextBlock Text="오늘 변환된 파일"
FontFamily="{StaticResource FsFontSans}"
FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource FsTextSecondary}"/>
@ -455,7 +530,7 @@
Text="0" Margin="0,4,0,0"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="EST. SPACE SAVED"
<TextBlock Text="예상 절감 용량"
FontFamily="{StaticResource FsFontSans}"
FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource FsTextSecondary}"/>
@ -471,7 +546,7 @@
</ScrollViewer>
<!-- Sidebar footer -->
<Border Grid.Row="2" BorderBrush="{StaticResource FsBorderSubtle}"
<Border Grid.Row="1" BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="0,1,0,0" Padding="24">
<StackPanel>
<TextBlock x:Name="CapabilityStatusText" Margin="0,0,0,12"
@ -517,124 +592,25 @@
</StackPanel>
<Button x:Name="ProcessQueueButton"
Content="Idle — drop files to begin"
Content="대기 중 — 파일을 드래그하여 추가하세요"
Style="{StaticResource FsPrimaryButtonStyle}"
IsEnabled="False"
Height="40"
Command="{Binding ProcessQueueCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
</StackPanel>
</Border>
</Grid>
</Border>
<!-- ============== MAIN ============== -->
<!-- ============== MAIN VIEW CONTENT ============== -->
<Grid Grid.Column="1" Background="{StaticResource FsBgBase}">
<Grid.RowDefinitions>
<RowDefinition Height="56"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Nav bar -->
<Border Grid.Row="0" BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="0,0,0,1" Padding="24,0">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Tabs -->
<Border HorizontalAlignment="Left"
Background="{StaticResource FsBgPanel}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="1" CornerRadius="20" Padding="4">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ToggleButton x:Name="TabActiveBtn"
Style="{StaticResource FsTabPillStyle}"
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="Active">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Active Queue" VerticalAlignment="Center"/>
<Border Margin="8,0,0,0" Style="{StaticResource FsTabBadgeStyle}" VerticalAlignment="Center">
<TextBlock x:Name="TabActiveBadge" Text="0"
FontSize="10" FontWeight="SemiBold"
VerticalAlignment="Center"
Foreground="{StaticResource FsTextTertiary}"/>
</Border>
</StackPanel>
</ToggleButton>
<ToggleButton x:Name="TabPastBtn" Margin="8,0,0,0"
IsChecked="True"
Style="{StaticResource FsTabPillStyle}"
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="Past">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Past Results" VerticalAlignment="Center"/>
<Border Margin="8,0,0,0" Style="{StaticResource FsTabBadgeStyle}" VerticalAlignment="Center">
<TextBlock x:Name="TabPastBadge" Text="0"
FontSize="10" FontWeight="SemiBold"
VerticalAlignment="Center"
Foreground="{StaticResource FsTextTertiary}"/>
</Border>
</StackPanel>
</ToggleButton>
</StackPanel>
</Border>
<!-- Actions -->
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Button Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding SettingsCommand, RelativeSource={RelativeSource AncestorType=Window}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="Settings24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="설정" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding RegisterCommand, RelativeSource={RelativeSource AncestorType=Window}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="AppsAddIn24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="우클릭 메뉴 등록" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding DiagnoseCommand, RelativeSource={RelativeSource AncestorType=Window}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="Stethoscope24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="진단" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding ExportLogCommand, RelativeSource={RelativeSource AncestorType=Window}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="ArrowDownload24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="로그 내보내기" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="ClearAllButton" Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding ClearAllCommand, RelativeSource={RelativeSource AncestorType=Window}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="Delete24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="목록 비우기" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Border>
<!-- View content (좌측: 탭 컨텐츠 / 우측: Preview 컬럼) -->
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="360"/>
<ColumnDefinition Width="380"/>
<ColumnDefinition x:Name="InspectorColumn" Width="380"/>
</Grid.ColumnDefinitions>
<!-- 실시간 검색 & 카테고리 필터 툴바 -->
@ -655,7 +631,7 @@
<ui:SymbolIcon Symbol="Search24" FontSize="14" Foreground="{StaticResource FsTextTertiary}"
HorizontalAlignment="Left" VerticalAlignment="Center" Margin="10,0,0,0" IsHitTestVisible="False"/>
</Grid>
<!-- Category Filter Chips -->
<!-- Category Filter Chips & Inspector Toggle -->
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Margin="0,0,4,0" Padding="10,4" Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
@ -686,7 +662,7 @@
<TextBlock Text="미디어" FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Padding="10,4" Style="{StaticResource FsSecondaryButtonStyle}"
<Button Margin="0,0,4,0" Padding="10,4" Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="Data">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
@ -694,6 +670,15 @@
<TextBlock Text="데이터" FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Border Width="1" Height="16" Background="{StaticResource FsBorderSubtle}" Margin="8,0"/>
<Button Padding="10,4" Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding ToggleInspectorCommand, RelativeSource={RelativeSource AncestorType=Window}}"
ToolTip="미리보기 패널 접기/펼치기">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="PreviewLink24" FontSize="13" VerticalAlignment="Center" Margin="0,0,4,0"/>
<TextBlock Text="미리보기" FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Border>
@ -745,39 +730,56 @@
<Grid x:Name="DropZoneEmpty">
<Border Background="{StaticResource FsBgBase}" Padding="32">
<!-- 점선 드롭존: 절제된 dashed 보더 + 중앙 정렬 안내 -->
<Grid MaxWidth="440"
<Grid MaxWidth="460"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<!-- 점선 테두리(드롭존 느낌) -->
<Rectangle RadiusX="10" RadiusY="10"
<Rectangle RadiusX="12" RadiusY="12"
Stroke="{StaticResource FsBorderHairline}"
StrokeThickness="1.25"
StrokeThickness="1.5"
StrokeDashArray="5 4"
Fill="#04FFFFFF"/>
Fill="{StaticResource FsBgSurface}"/>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
Margin="48,52">
<Image Width="128" Height="128"
Margin="48,44">
<Image Width="120" Height="120"
HorizontalAlignment="Center"
RenderOptions.BitmapScalingMode="HighQuality"
Source="pack://application:,,,/Everything2Everything;component/Assets/illus-empty.png"/>
<TextBlock Margin="0,16,0,0" FontSize="16" FontWeight="SemiBold"
Foreground="{StaticResource FsTextPrimary}"
HorizontalAlignment="Center"
Text="Drop files anywhere to queue"/>
Text="변환할 파일을 드래그하여 추가하세요"/>
<TextBlock Margin="0,6,0,0" Style="{StaticResource FsCaptionStyle}"
HorizontalAlignment="Center"
TextAlignment="Center"
Text="이미지 · 문서 · 데이터 · 영상 · 오디오 등 무엇이든"/>
<Button Margin="0,22,0,0"
<Button Margin="0,20,0,0"
HorizontalAlignment="Center"
Style="{StaticResource FsSecondaryButtonStyle}"
Padding="18,8"
Content="Browse…"
Command="{Binding AddFilesCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<TextBlock Margin="0,12,0,0" Style="{StaticResource FsCaptionStyle}"
HorizontalAlignment="Center"
Foreground="{StaticResource FsTextTertiary}"
Text="또는 Ctrl+O"/>
Style="{StaticResource FsPrimaryButtonStyle}"
Padding="20,10"
Command="{Binding AddFilesCommand, RelativeSource={RelativeSource AncestorType=Window}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="Add24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="파일 찾기…" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<!-- 단축키 안내 칩 -->
<Border Margin="0,16,0,0" HorizontalAlignment="Center"
Background="{StaticResource FsBgPanel}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="1" CornerRadius="12" Padding="12,4">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="단축키" Style="{StaticResource FsCaptionStyle}" Margin="0,0,6,0" VerticalAlignment="Center"/>
<Border Background="{StaticResource FsBgBase}" CornerRadius="4" Padding="6,2" Margin="0,0,4,0">
<TextBlock Text="Ctrl + O" FontFamily="{StaticResource FsFontMono}" FontSize="11" Foreground="{StaticResource FsAccentBlue}" VerticalAlignment="Center"/>
</Border>
<TextBlock Text="파일 열기 ·" Style="{StaticResource FsCaptionStyle}" Margin="0,0,4,0" VerticalAlignment="Center"/>
<Border Background="{StaticResource FsBgBase}" CornerRadius="4" Padding="6,2" Margin="0,0,4,0">
<TextBlock Text="Ctrl + Enter" FontFamily="{StaticResource FsFontMono}" FontSize="11" Foreground="{StaticResource FsAccentGreen}" VerticalAlignment="Center"/>
</Border>
<TextBlock Text="변환 시작" Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
</StackPanel>
</Border>
</StackPanel>
</Grid>
</Border>
@ -788,11 +790,8 @@
ItemsSource="{Binding ActiveQueue, RelativeSource={RelativeSource AncestorType=Window}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<!-- row hover: FsListRowStyle 느낌(투명→surface hover, hairline 보더) + 좌측 형식색 accent bar -->
<Border x:Name="QueueRow" Margin="0,0,0,8" Padding="0" CornerRadius="6"
Background="{StaticResource FsBgPanel}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="1"
<!-- row hover: FsListRowStyle Neo Glass 카드 + 좌측 형식색 accent bar -->
<Border x:Name="QueueRow" Style="{StaticResource FsListRowStyle}" Padding="0"
Cursor="Hand">
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewMouseLeftButtonUp">
@ -801,15 +800,15 @@
</i:Interaction.Triggers>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2"/>
<ColumnDefinition Width="3"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 좌측 2px 세로 accent bar: 해당 파일 형식색 -->
<!-- 좌측 3px 세로 accent bar: 해당 파일 형식색 -->
<Border Grid.Column="0"
Width="2"
Width="3"
Background="{Binding FormatBrush}"
CornerRadius="6,0,0,6"/>
<Grid Grid.Column="1" Margin="12,10">
CornerRadius="10,0,0,10"/>
<Grid Grid.Column="1" Margin="14,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="28"/>
<ColumnDefinition Width="40"/>
@ -823,16 +822,28 @@
<Image Grid.Column="1" Width="34" Height="34" VerticalAlignment="Center"
RenderOptions.BitmapScalingMode="HighQuality"
Source="{Binding GlyphSource}"/>
<StackPanel Grid.Column="2" Margin="16,0,0,0"
<StackPanel Grid.Column="2" Margin="14,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding FileName}"
Style="{StaticResource FsBodyStyle}"
FontWeight="Medium"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"/>
<StackPanel Orientation="Horizontal" Margin="0,4,0,0" VerticalAlignment="Center">
<!-- Format Category Badge Pill (designpaca R1/R3) -->
<Border x:Name="ItemCategoryBadge" Tag="CategoryPill"
Background="{Binding FormatBrush}"
CornerRadius="3" Padding="5,1" Margin="0,0,8,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding FormatLabel}"
FontSize="10" FontWeight="Bold"
Foreground="White"
VerticalAlignment="Center"/>
</Border>
<TextBlock Text="{Binding MetaLine}"
Style="{StaticResource FsCaptionStyle}"
Margin="0,2,0,0"/>
<ProgressBar Height="3" Margin="0,4,0,0"
VerticalAlignment="Center"/>
</StackPanel>
<ProgressBar Height="2" Margin="0,5,0,0"
Minimum="0" Maximum="100"
Value="{Binding ProgressValue, Mode=OneWay}"
Visibility="{Binding ProgressVisibility, Mode=OneWay}"/>
@ -862,15 +873,6 @@
</Grid>
</Grid>
</Border>
<DataTemplate.Triggers>
<!-- row hover: 배경 살짝 밝게 + hairline 보더 -->
<Trigger SourceName="QueueRow" Property="IsMouseOver" Value="True">
<Setter TargetName="QueueRow" Property="Background"
Value="{StaticResource FsBgSurfaceHover}"/>
<Setter TargetName="QueueRow" Property="BorderBrush"
Value="{StaticResource FsBorderHairline}"/>
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
@ -883,13 +885,13 @@
<!-- 빈 상태 안내 (Empty State) -->
<Grid x:Name="PastResultsEmpty" Visibility="Collapsed">
<Border Background="{StaticResource FsBgBase}" Padding="32">
<Grid MaxWidth="440" HorizontalAlignment="Center" VerticalAlignment="Center">
<Rectangle RadiusX="10" RadiusY="10"
<Grid MaxWidth="460" HorizontalAlignment="Center" VerticalAlignment="Center">
<Rectangle RadiusX="12" RadiusY="12"
Stroke="{StaticResource FsBorderHairline}"
StrokeThickness="1.25"
StrokeThickness="1.5"
StrokeDashArray="5 4"
Fill="#04FFFFFF"/>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="48,52">
Fill="{StaticResource FsBgSurface}"/>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="48,44">
<ui:SymbolIcon Symbol="History24" FontSize="48"
Foreground="{StaticResource FsTextTertiary}"
HorizontalAlignment="Center"/>
@ -901,6 +903,17 @@
HorizontalAlignment="Center"
TextAlignment="Center"
Text="파일을 변환하면 완료된 내역과 절감된 용량이 여기에 기록됩니다"/>
<Button Margin="0,20,0,0"
HorizontalAlignment="Center"
Style="{StaticResource FsSecondaryButtonStyle}"
Padding="16,8"
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="Active">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="ArrowLeft24" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="큐로 이동하여 파일 추가하기" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Border>
@ -933,14 +946,12 @@
<Border Height="1" Background="{StaticResource FsBorderSubtle}"
Margin="0,0,0,12"/>
<!-- Rows -->
<!-- Rows: Elevated Neo Glass Cards -->
<ItemsControl ItemsSource="{Binding Entries}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#0DFFFFFF"
BorderThickness="0,0,0,1" Padding="0,12"
Cursor="Hand"
Background="Transparent">
<Border Style="{StaticResource FsListRowStyle}"
Cursor="Hand">
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewMouseLeftButtonUp">
<i:InvokeCommandAction Command="{Binding PastRowCommand, RelativeSource={RelativeSource AncestorType=Window}}" PassEventArgsToCommand="True"/>
@ -948,11 +959,11 @@
</i:Interaction.Triggers>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="42"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="40"/>
</Grid.ColumnDefinitions>
@ -960,15 +971,25 @@
RenderOptions.BitmapScalingMode="HighQuality"
Source="{Binding GlyphSource}"/>
<StackPanel Grid.Column="1" Margin="16,0,0,0"
<StackPanel Grid.Column="1" Margin="14,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding FileName}"
Style="{StaticResource FsBodyStyle}"
FontWeight="Medium"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"/>
<StackPanel Orientation="Horizontal" Margin="0,4,0,0" VerticalAlignment="Center">
<Border Background="{Binding FormatBrush}"
CornerRadius="4" Padding="6,1" Margin="0,0,8,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding FormatLabel}"
FontSize="10" FontWeight="Bold"
Foreground="White"
VerticalAlignment="Center"/>
</Border>
<TextBlock Text="{Binding MetaLine}"
Style="{StaticResource FsCaptionStyle}"
Margin="0,2,0,0"/>
VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>
<TextBlock Grid.Column="2"
@ -982,30 +1003,35 @@
FontFamily="{StaticResource FsFontMono}"
FontSize="12"
Foreground="{StaticResource FsAccentGreen}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<StackPanel Grid.Column="4" Orientation="Horizontal"
VerticalAlignment="Center">
<Path Width="12" Height="12" Stretch="Uniform"
<Border Grid.Column="4" Background="#10B98122" CornerRadius="12"
Padding="8,3" HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Path Width="10" Height="10" Stretch="Uniform"
Stroke="{StaticResource FsAccentGreen}"
StrokeThickness="3"
StrokeThickness="2.5"
VerticalAlignment="Center"
Data="M20,6 L9,17 L4,12"/>
<TextBlock Text="Success" Margin="6,0,0,0"
FontSize="11" FontWeight="Medium"
Foreground="{StaticResource FsAccentGreen}"/>
<TextBlock Text="완료" Margin="5,0,0,0"
FontSize="11" FontWeight="SemiBold"
Foreground="{StaticResource FsAccentGreen}"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
<Button Grid.Column="5"
Style="{StaticResource FsSecondaryButtonStyle}"
Background="Transparent"
BorderThickness="0"
Padding="6"
Style="{StaticResource FsIconButtonStyle}"
Width="32" Height="32"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Command="{Binding OpenFolderCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding RevealPath}"
ToolTip="출력 파일이 있는 폴더 열기">
<Path Width="14" Height="14" Stretch="Uniform"
Stroke="{StaticResource FsTextTertiary}"
StrokeThickness="2"
Stroke="{StaticResource FsTextSecondary}"
StrokeThickness="1.8"
Data="M22,19 C22,20.1 21.1,21 20,21 L4,21 C2.9,21 2,20.1 2,19 L2,5 C2,3.9 2.9,3 4,3 L9,3 L11,6 L20,6 C21.1,6 22,6.9 22,8 Z"/>
</Button>
</Grid>
@ -1036,17 +1062,27 @@
</Grid.RowDefinitions>
<!-- 헤더 -->
<Border Grid.Row="0" Padding="20,16,20,12"
<Border Grid.Row="0" Padding="20,12,16,12"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Grid VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Left">
<Path Width="14" Height="14" Stretch="Uniform"
Stroke="{StaticResource FsTextSecondary}" StrokeThickness="2"
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"/>
<TextBlock Text="PREVIEW" Margin="8,0,0,0"
Style="{StaticResource FsLabelStyle}"
VerticalAlignment="Center"/>
</StackPanel>
<Button HorizontalAlignment="Right" VerticalAlignment="Center"
Style="{StaticResource FsIconButtonStyle}"
Width="28" Height="28"
Command="{Binding ToggleInspectorCommand, RelativeSource={RelativeSource AncestorType=Window}}"
ToolTip="미리보기 패널 닫기">
<ui:SymbolIcon Symbol="Dismiss24" FontSize="14" Foreground="{StaticResource FsTextSecondary}"/>
</Button>
</Grid>
</Border>
<!-- 이미지 영역 -->
@ -1208,5 +1244,4 @@
</Grid>
</Grid>
</Grid>
</Grid>
</ui:FluentWindow>

View file

@ -70,6 +70,24 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
public ICommand BatchSelectAllCommand { get; }
public ICommand BatchRemoveSelectedCommand { get; }
public ICommand BatchClearCompletedCommand { get; }
public ICommand ToggleInspectorCommand { get; }
private bool _isInspectorVisible = true;
public bool IsInspectorVisible
{
get => _isInspectorVisible;
set
{
if (_isInspectorVisible != value)
{
_isInspectorVisible = value;
if (InspectorColumn != null)
{
InspectorColumn.Width = _isInspectorVisible ? new GridLength(380) : new GridLength(0);
}
}
}
}
private string _searchText = "";
public string SearchText
@ -124,6 +142,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
BatchSelectAllCommand = new RelayCommand(p => BatchSelectAll(p));
BatchRemoveSelectedCommand = new RelayCommand(_ => BatchRemoveSelected());
BatchClearCompletedCommand = new RelayCommand(_ => BatchClearCompleted());
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"));
@ -138,6 +157,11 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
InitializeComponent();
if (SmartPresetCombo is not null)
{
SmartPresetCombo.SelectionChanged += OnSmartPresetChanged;
}
// ActiveQueueList/PastResultsList의 ItemsSource는 XAML이 ActiveQueue/PastResults에 바인딩(선언적).
InitializeOutputFormats();
@ -220,6 +244,11 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
UpdatePastResultsVisibility();
}
public void ToggleInspector()
{
IsInspectorVisible = !IsInspectorVisible;
}
// ============== Drag & Drop ==============
private void HandleDragOver(DragEventArgs? e)
@ -317,12 +346,12 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
var count = _activeQueue.Count;
if (_cts is not null)
{
ProcessQueueButton.Content = $"Processing… ({count} files)";
ProcessQueueButton.Content = $"변환 처리 중… ({count}개 파일)";
ProcessQueueButton.IsEnabled = false;
}
else if (count == 0)
{
ProcessQueueButton.Content = "Idle — drop files to begin";
ProcessQueueButton.Content = "대기 중 — 파일을 드래그하여 추가하세요";
ProcessQueueButton.IsEnabled = false;
}
else if (string.IsNullOrEmpty(SelectedOutputExtension))
@ -332,7 +361,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
}
else
{
ProcessQueueButton.Content = $"Process Queue ({count})";
ProcessQueueButton.Content = $"대기열 일괄 변환 시작 ({count}개) [Ctrl + Enter]";
ProcessQueueButton.IsEnabled = true;
}
}
@ -577,6 +606,107 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
}
}
private IReadOnlyList<FormatPreset> _currentFormatPresets = Array.Empty<FormatPreset>();
private bool _suppressPresetChanged;
private void UpdateSmartPresetsForFormat(string? ext)
{
if (SmartPresetCombo is null) return;
_currentFormatPresets = FormatPresetEngine.GetPresetsForExtension(ext);
_suppressPresetChanged = true;
try
{
SmartPresetCombo.Items.Clear();
foreach (var preset in _currentFormatPresets)
{
SmartPresetCombo.Items.Add(new ComboBoxItem
{
Content = preset.Title,
Tag = preset.Id,
ToolTip = preset.Description,
});
}
if (SmartPresetCombo.Items.Count > 0)
{
SmartPresetCombo.SelectedIndex = 0;
}
}
finally
{
_suppressPresetChanged = false;
}
ApplySelectedSmartPreset();
}
private void OnSmartPresetChanged(object sender, SelectionChangedEventArgs e)
{
if (_suppressPresetChanged) return;
ApplySelectedSmartPreset();
}
private void ApplySelectedSmartPreset()
{
if (SmartPresetCombo is null || SmartPresetCombo.SelectedIndex < 0) return;
if (SmartPresetCombo.SelectedIndex >= _currentFormatPresets.Count) return;
var preset = _currentFormatPresets[SmartPresetCombo.SelectedIndex];
preset.Apply(_options);
if (SmartPresetDescriptionText is not null)
{
SmartPresetDescriptionText.Text = preset.Description;
}
if (SmartPresetChipsPanel is not null)
{
SmartPresetChipsPanel.Children.Clear();
var chipStyle = TryFindResource("FsSpecChipStyle") as Style;
var monoFont = TryFindResource("FsFontMono") as FontFamily;
var cyanBrush = TryFindResource("FsAccentCyan") as Brush ?? Brushes.Cyan;
foreach (var chip in preset.SpecChips)
{
var border = new Border();
if (chipStyle is not null)
{
border.Style = chipStyle;
}
else
{
border.CornerRadius = new CornerRadius(6);
border.Padding = new Thickness(8, 3, 8, 3);
border.Margin = new Thickness(0, 0, 6, 6);
}
var tb = new TextBlock
{
Text = chip,
FontSize = 11,
Foreground = cyanBrush,
VerticalAlignment = VerticalAlignment.Center,
};
if (monoFont is not null)
{
tb.FontFamily = monoFont;
}
border.Child = tb;
SmartPresetChipsPanel.Children.Add(border);
}
}
if (QualitySlider is not null)
{
QualitySlider.Value = _options.Quality;
}
if (QualityValueText is not null)
{
QualityValueText.Text = _options.Quality.ToString(CultureInfo.InvariantCulture) + "%";
}
}
private void ApplyFilterCategory(string? categoryName)
{
if (Enum.TryParse<FilterCategory>(categoryName, true, out var cat))
@ -1189,6 +1319,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
UpdateOutputDestHint(keepExt);
UpdateCombineState(keepExt);
UpdateProcessQueueButton();
UpdateSmartPresetsForFormat(keepExt);
if (OutputFormatHint is not null)
{
@ -1233,6 +1364,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
UpdateOutputFormatBadge(ext);
UpdateQualityPanelForFormat(ext);
UpdateOutputDestHint(ext);
UpdateSmartPresetsForFormat(ext);
}
}

View file

@ -0,0 +1,374 @@
using System;
using System.Collections.Generic;
namespace Everything2Everything.Core.Presets;
/// <summary>
/// 출력 형식에 맞춰 동적으로 제공되는 세부 프리셋 정보 및 기술 스펙
/// </summary>
public sealed record FormatPreset(
string Id,
string Title,
string Description,
IReadOnlyList<string> SpecChips,
Action<dynamic> ApplyAction)
{
public void Apply(dynamic options) => ApplyAction(options);
}
/// <summary>
/// 확장자별 최적화 프리셋을 공급하는 스마트 프리셋 엔진 (TDD SSOT)
/// </summary>
public static class FormatPresetEngine
{
public static IReadOnlyList<FormatPreset> GetPresetsForExtension(string? outputExtension)
{
var ext = (outputExtension ?? "").Trim().ToLowerInvariant();
if (!ext.StartsWith('.') && !string.IsNullOrEmpty(ext)) ext = "." + ext;
return ext switch
{
".mp3" => Mp3Presets(),
".m4a" or ".aac" => AacPresets(ext),
".flac" or ".wav" => LosslessAudioPresets(ext),
".ogg" or ".opus" => OggOpusPresets(ext),
".webp" => WebpPresets(),
".avif" => AvifPresets(),
".jpg" or ".jpeg" => JpgPresets(),
".png" => PngPresets(),
".pdf" => PdfPresets(),
".mp4" => Mp4Presets(),
".mkv" or ".webm" or ".mov" or ".avi" => GeneralVideoPresets(ext),
".csv" or ".xlsx" or ".json" => DataPresets(ext),
".docx" or ".html" or ".md" or ".txt" => DocumentPresets(ext),
_ => FallbackPresets(ext)
};
}
private static IReadOnlyList<FormatPreset> Mp3Presets() => new List<FormatPreset>
{
new(
"mp3-high-320",
"최고음질 320k (스튜디오 마스터링)",
"CBR 320kbps · 48kHz · 고해상도 음악 및 마스터링 보존",
new[] { "320 kbps", "48 kHz", "CBR", "스테레오" },
opt => { opt.AudioBitrateKbps = 320; opt.AudioVbr = false; opt.SampleRateIndex = 2; opt.ChannelsIndex = 2; }
),
new(
"mp3-standard-192",
"표준 스트리밍 192k (일반 감상)",
"VBR 192kbps · 44.1kHz · 음질과 용량의 최적 균형",
new[] { "192 kbps", "44.1 kHz", "VBR", "스테레오" },
opt => { opt.AudioBitrateKbps = 192; opt.AudioVbr = true; opt.SampleRateIndex = 1; opt.ChannelsIndex = 2; }
),
new(
"mp3-voice-128",
"팟캐스트·음성 128k (보이스 최적화)",
"CBR 128kbps · 모노 다운믹스 · 라우드니스 정규화",
new[] { "128 kbps", "모노", "음성 선명화", "Loudnorm" },
opt => { opt.AudioBitrateKbps = 128; opt.AudioVbr = false; opt.ChannelsIndex = 1; opt.Loudnorm = true; }
),
new(
"mp3-compact-64",
"초절약 압축 64k (용량 극소화)",
"CBR 64kbps · 32kHz · 녹음본 보관 및 가벼운 전송",
new[] { "64 kbps", "모노", "초경량 보관" },
opt => { opt.AudioBitrateKbps = 96; opt.AudioVbr = false; opt.ChannelsIndex = 1; }
)
};
private static IReadOnlyList<FormatPreset> AacPresets(string ext) => new List<FormatPreset>
{
new(
"aac-high-320",
"최고음질 320k (Apple/고음질)",
$"{ext.ToUpperInvariant()} 고비트레이트 · 48kHz · 투명한 음질",
new[] { "320 kbps", "48 kHz", "AAC LC", "스테레오" },
opt => { opt.AudioBitrateKbps = 320; opt.AudioVbr = false; opt.SampleRateIndex = 2; opt.ChannelsIndex = 2; }
),
new(
"aac-standard-192",
"표준 192k (모바일·웹)",
$"{ext.ToUpperInvariant()} 192kbps · 44.1kHz 표준 인코딩",
new[] { "192 kbps", "44.1 kHz", "표준" },
opt => { opt.AudioBitrateKbps = 192; opt.AudioVbr = false; opt.SampleRateIndex = 1; opt.ChannelsIndex = 2; }
),
new(
"aac-compact-128",
"가벼운 공유 128k",
"128kbps 효율적 압축 전송용",
new[] { "128 kbps", "경량화" },
opt => { opt.AudioBitrateKbps = 128; opt.ChannelsIndex = 2; }
)
};
private static IReadOnlyList<FormatPreset> LosslessAudioPresets(string ext) => new List<FormatPreset>
{
new(
"lossless-master",
"무손실 스튜디오 마스터 (24-bit 96kHz)",
$"{ext.ToUpperInvariant()} 원음 무손실 아카이빙",
new[] { "무손실 100%", "24-bit", "96 kHz" },
opt => { opt.AudioBitrateKbps = 320; opt.SampleRateIndex = 0; opt.ChannelsIndex = 0; }
),
new(
"lossless-cd",
"CD 음질 무손실 (16-bit 44.1kHz)",
$"{ext.ToUpperInvariant()} 표준 CD 무손실 추출",
new[] { "무손실", "16-bit", "44.1 kHz" },
opt => { opt.AudioBitrateKbps = 320; opt.SampleRateIndex = 1; opt.ChannelsIndex = 2; }
)
};
private static IReadOnlyList<FormatPreset> OggOpusPresets(string ext) => new List<FormatPreset>
{
new(
"opus-high",
"고음질 보이스/음악 (192k)",
$"{ext.ToUpperInvariant()} 최신 고효율 코덱",
new[] { "192 kbps", "차세대 코덱" },
opt => { opt.AudioBitrateKbps = 192; }
),
new(
"opus-voice",
"음성 스트리밍 (96k)",
"디스코드/보이스 최적화",
new[] { "96 kbps", "초저지연" },
opt => { opt.AudioBitrateKbps = 96; }
)
};
private static IReadOnlyList<FormatPreset> WebpPresets() => new List<FormatPreset>
{
new(
"webp-web-q85",
"웹 고화질 (Q85 · 추천)",
"Quality 85 · EXIF 메타데이터 제거 · 웹 게시 표준",
new[] { "Quality 85", "Strip EXIF", "웹 최적화" },
opt => { opt.ImageQuality = 85; opt.StripMetadata = true; }
),
new(
"webp-compact-q65",
"웹 초경량 (Q65 · 빠른 로딩)",
"Quality 65 · 고압축 이미지로 첫 페이지 로딩 가속",
new[] { "Quality 65", "Strip EXIF", "초경량" },
opt => { opt.ImageQuality = 65; opt.StripMetadata = true; }
),
new(
"webp-lossless",
"무손실 그래픽 (Lossless 100%)",
"100% 무손실 픽셀 보존 · 알파 투명도 완벽 보존",
new[] { "무손실 100%", "투명도 보존", "그래픽·아이콘" },
opt => { opt.ImageQuality = 100; opt.StripMetadata = false; }
),
new(
"webp-sns-thumb",
"SNS 썸네일 (Q75)",
"피드 및 카드 썸네일 최적화",
new[] { "Quality 75", "썸네일" },
opt => { opt.ImageQuality = 75; opt.StripMetadata = true; }
)
};
private static IReadOnlyList<FormatPreset> AvifPresets() => new List<FormatPreset>
{
new(
"avif-balanced",
"차세대 초고압축 (Q55)",
"AV1 코덱 기반 압축률 극대화",
new[] { "Quality 55", "AV1 코덱", "초고압축" },
opt => { opt.ImageQuality = 85; opt.StripMetadata = true; }
),
new(
"avif-high",
"고화질 아카이빙 (Q75)",
"색상 심도 10-bit HDR 보존",
new[] { "Quality 75", "10-bit HDR" },
opt => { opt.ImageQuality = 95; opt.StripMetadata = false; }
)
};
private static IReadOnlyList<FormatPreset> JpgPresets() => new List<FormatPreset>
{
new(
"jpg-photo-q95",
"디지털 인화·고화질 (Q95)",
"Quality 95 · 색상 프로파일 보존 · 선명한 사진",
new[] { "Quality 95", "ICC 보존", "고해상도" },
opt => { opt.ImageQuality = 95; opt.StripMetadata = false; }
),
new(
"jpg-web-q80",
"웹 표준 (Q80 · 권장)",
"Quality 80 · 프로그레시브 JPEG · 메타데이터 제거",
new[] { "Quality 80", "Strip EXIF", "프로그레시브" },
opt => { opt.ImageQuality = 80; opt.StripMetadata = true; }
),
new(
"jpg-compact-q70",
"모바일 메신저 (Q70)",
"Quality 70 · 카카오톡/문자 전송 가벼운 용량",
new[] { "Quality 70", "용량 절약" },
opt => { opt.ImageQuality = 70; opt.StripMetadata = true; }
)
};
private static IReadOnlyList<FormatPreset> PngPresets() => new List<FormatPreset>
{
new(
"png-max-compress",
"최대 압축 무손실 (Level 9)",
"PNG Deflate 최대 압축 · 무손실 픽셀 완전 보존",
new[] { "PNG Level 9", "무손실", "디테일 100%" },
opt => { opt.ImageQuality = 100; opt.StripMetadata = false; }
),
new(
"png-web-graphic",
"웹 그래픽 최적화",
"불필요한 청크 제거 · 알파 투명도 보존",
new[] { "웹 최적화", "투명도 보존", "EXIF 제거" },
opt => { opt.ImageQuality = 100; opt.StripMetadata = true; }
)
};
private static IReadOnlyList<FormatPreset> PdfPresets() => new List<FormatPreset>
{
new(
"pdf-print-300",
"인쇄용 고해상도 (300 DPI)",
"300 DPI 래스터/벡터 폰트 임베딩 · 인쇄소 출력용",
new[] { "300 DPI", "벡터 폰트", "최고품질" },
opt => { opt.Quality = 100; }
),
new(
"pdf-web-150",
"웹 배포·문서 공유 (150 DPI)",
"150 DPI 텍스트 선명도 및 적정 용량 밸런스",
new[] { "150 DPI", "문서 공유", "경량화" },
opt => { opt.Quality = 85; }
),
new(
"pdf-ebook-96",
"모바일·전자책 (96 DPI)",
"96 DPI 초경량화 · 빠른 스크롤 및 이메일 첨부",
new[] { "96 DPI", "초경량", "이메일 첨부" },
opt => { opt.Quality = 70; }
),
new(
"pdf-bw-scan",
"흑백 스캔 압축 (문서 전용)",
"그레이스케일 고압축 · 텍스트 가독성 강화",
new[] { "그레이스케일", "고압축" },
opt => { opt.Quality = 60; }
)
};
private static IReadOnlyList<FormatPreset> Mp4Presets() => new List<FormatPreset>
{
new(
"mp4-web-1080p",
"웹 스트리밍 1080p (추천)",
"H.264 Fast · CRF 23 · AAC 192k · 웹/브라우저 재생",
new[] { "1080p", "H.264 Fast", "CRF 23", "AAC 192k" },
opt => { opt.VideoCrf = 23; opt.VideoPreset = "fast"; opt.ResolutionIndex = 3; opt.AudioBitrateKbps = 192; }
),
new(
"mp4-sns-720p",
"SNS 메신저 720p (10MB 타겟)",
"H.264 VeryFast · CRF 26 · AAC 128k · 모바일 가벼운 공유",
new[] { "720p", "CRF 26", "AAC 128k", "용량 절약" },
opt => { opt.VideoCrf = 26; opt.VideoPreset = "veryfast"; opt.ResolutionIndex = 4; opt.AudioBitrateKbps = 128; }
),
new(
"mp4-master-crf18",
"초고화질 마스터 (CRF 18)",
"원본 해상도 유지 · H.264 Medium · 오디오 무손실 복사",
new[] { "원본 해상도", "CRF 18", "Medium", "고화질 보존" },
opt => { opt.VideoCrf = 18; opt.VideoPreset = "medium"; opt.ResolutionIndex = 0; opt.AudioBitrateKbps = 320; }
),
new(
"mp4-audio-extract",
"오디오 트랙 추출 (AAC)",
"영상에서 오디오만 고음질로 분리",
new[] { "오디오 전용", "AAC 256k" },
opt => { opt.AudioBitrateKbps = 256; }
)
};
private static IReadOnlyList<FormatPreset> GeneralVideoPresets(string ext) => new List<FormatPreset>
{
new(
"video-standard",
$"{ext.ToUpperInvariant()} 표준 인코딩",
"CRF 23 · Fast 프리셋",
new[] { "CRF 23", "Fast", ext },
opt => { opt.VideoCrf = 23; opt.VideoPreset = "fast"; }
),
new(
"video-quality",
$"{ext.ToUpperInvariant()} 고화질 보존",
"CRF 18 · Medium 프리셋",
new[] { "CRF 18", "Medium" },
opt => { opt.VideoCrf = 18; opt.VideoPreset = "medium"; }
)
};
private static IReadOnlyList<FormatPreset> DataPresets(string ext) => new List<FormatPreset>
{
new(
"data-standard",
"표준 UTF-8 인코딩",
$"{ext.ToUpperInvariant()} 표준 서식 정리",
new[] { "UTF-8", "표준 포맷" },
_ => { }
),
new(
"data-excel-compat",
"Excel 호환 서식 (BOM 포함)",
"Excel에서 한글 깨짐 없는 UTF-8 BOM",
new[] { "UTF-8 BOM", "엑셀 호환" },
_ => { }
)
};
private static IReadOnlyList<FormatPreset> DocumentPresets(string ext) => new List<FormatPreset>
{
new(
"doc-standard",
"표준 문서 서식",
$"{ext.ToUpperInvariant()} 기본 텍스트/스타일 변환",
new[] { "표준 서식", ext },
_ => { }
),
new(
"doc-clean",
"서식 단순화 (경량)",
"군더더기 태그 및 서식 정돈",
new[] { "클린 서식", "경량화" },
_ => { }
)
};
private static IReadOnlyList<FormatPreset> FallbackPresets(string ext) => new List<FormatPreset>
{
new(
"default-standard",
"기본 표준 변환",
$"{ext} 기본 파라미터 적용",
new[] { "기본값", ext },
opt => { opt.Quality = 85; }
),
new(
"default-quality",
"품질 우선 변환",
"최고 품질 설정 적용",
new[] { "고품질", "무손실 우선" },
opt => { opt.Quality = 100; }
)
};
}

View file

@ -161,7 +161,6 @@ public class DesignAuditAstTests
var actionsPanel = doc.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "StackPanel" &&
e.Attribute("Grid.Column")?.Value == "1" &&
e.Attribute("Orientation")?.Value == "Horizontal" &&
e.Elements().Any(c => c.Name.LocalName == "Button" && c.Attribute("Command")?.Value?.Contains("SettingsCommand") == true));
@ -234,12 +233,15 @@ public class DesignAuditAstTests
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
// 1. Quick Presets (PresetCommand 바인딩 버튼 존재)
var presetButtons = doc.Descendants()
.Where(e => (e.Name.LocalName == "Button" || e.Name.LocalName == "ToggleButton") &&
e.Attribute("Command")?.Value.Contains("PresetCommand") == true)
.ToList();
Assert.True(presetButtons.Count >= 4, "빠른 최적화 프리셋 버튼 4개(웹/고화질/문서/모바일)가 MainWindow.xaml에 선언되어야 합니다.");
// 1. Dynamic Smart Presets (SmartPresetCombo 및 SmartPresetChipsPanel 존재)
var smartPresetCombo = doc.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "ComboBox" &&
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "SmartPresetCombo");
Assert.NotNull(smartPresetCombo);
var specChipsPanel = doc.Descendants()
.FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "SmartPresetChipsPanel");
Assert.NotNull(specChipsPanel);
// 2. SearchBox (검색창 존재)
var searchBox = doc.Descendants()
@ -259,4 +261,164 @@ public class DesignAuditAstTests
.ToList();
Assert.NotEmpty(openButtons);
}
[Fact]
public void MainWindow_MustHave_CollapsibleInspectorColumn_And_ToggleCommand()
{
// 우측 미리보기 인스펙터를 원클릭으로 접고 펼쳐 큐 가로폭을 극대화할 수 있도록
// InspectorColumn 명명된 ColumnDefinition과 ToggleInspectorCommand 버튼이 존재해야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var inspectorCol = doc.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "ColumnDefinition" &&
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "InspectorColumn");
Assert.NotNull(inspectorCol);
var toggleBtn = doc.Descendants()
.FirstOrDefault(e => (e.Name.LocalName == "Button" || e.Name.LocalName == "ToggleButton") &&
e.Attribute("Command")?.Value.Contains("ToggleInspectorCommand") == true);
Assert.NotNull(toggleBtn);
}
[Fact]
public void FormatShiftTheme_MustDefine_DoubleBezelAndCardStyles()
{
// Fluent 2 / Dark Instrument 디자인 고도화를 위해
// 8px 그리드 기반의 카드(Double-Bezel) 및 툴바 표준 스타일이 FormatShiftTheme.xaml에 선언되어야 한다.
var themeFile = Path.Combine(ViewsDir, "FormatShiftTheme.xaml");
var doc = XDocument.Parse(File.ReadAllText(themeFile));
var cardStyle = doc.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "Style" &&
e.Attribute(XName.Get("Key", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "FsCardStyle");
Assert.NotNull(cardStyle);
var toolbarBtnStyle = doc.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "Style" &&
e.Attribute(XName.Get("Key", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "FsToolbarButtonStyle");
Assert.NotNull(toolbarBtnStyle);
}
[Fact]
public void InspectorHeader_MustHave_DismissOrCloseButton_WithToggleCommand()
{
// 우측 미리보기 인스펙터 헤더에 직관적으로 닫을 수 있는 닫기 버튼이 제공되어야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var inspectorCol = doc.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "Border" &&
e.Attribute("Grid.Column")?.Value == "1" &&
e.Descendants().Any(d => d.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PreviewImageArea"));
Assert.NotNull(inspectorCol);
var header = inspectorCol.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "Border" && e.Attribute("Grid.Row")?.Value == "0");
Assert.NotNull(header);
var closeBtn = header.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "Button" &&
e.Attribute("Command")?.Value.Contains("ToggleInspectorCommand") == true);
Assert.NotNull(closeBtn);
}
[Fact]
public void EmptyStates_MustHave_ActionableCallToActions_And_Shortcuts()
{
// Zero-Void 원칙: DropZone과 PastResults 빈 상태 화면은 단순 텍스트뿐 아니라
// 사용자의 즉각적인 행동을 유도하는 Action 버튼을 포함해야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var dropEmpty = doc.Descendants()
.FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "DropZoneEmpty");
Assert.NotNull(dropEmpty);
var dropCta = dropEmpty.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "Button" &&
e.Attribute("Command")?.Value.Contains("AddFilesCommand") == true);
Assert.NotNull(dropCta);
var pastEmpty = doc.Descendants()
.FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PastResultsEmpty");
Assert.NotNull(pastEmpty);
var pastCta = pastEmpty.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "Button" &&
e.Attribute("Command")?.Value.Contains("TabCommand") == true);
Assert.NotNull(pastCta);
}
[Fact]
public void TopHeader_MustHave_EngineStatusTelemetryBadge()
{
// designpaca R1/R2: 정밀 계측기 상단 툴바에 실시간 엔진 가동 상태 및 프로바이더 텔레메트리 뱃지가 존재해야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var engineTelemetry = doc.Descendants()
.FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "EngineTelemetryBadge");
Assert.NotNull(engineTelemetry);
}
[Fact]
public void FormatShiftTheme_MustDefine_InstrumentTokens_And_CategoryBrushes()
{
// designpaca R2/R3: FormatShiftTheme.xaml에 포맷 카테고리별 시그니처 브러시와 정밀 카드 스타일이 선언되어 있어야 한다.
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("FsBadgeImageBrush", keys);
Assert.Contains("FsBadgeDocBrush", keys);
Assert.Contains("FsBadgeVideoBrush", keys);
Assert.Contains("FsBadgeAudioBrush", keys);
Assert.Contains("FsInstrumentCardStyle", keys);
}
[Fact]
public void ActiveQueueList_ItemTemplate_MustHave_InstrumentCard_With_CategoryAndTelemetry()
{
// designpaca R1/R3: 대기열 리스트 DataTemplate에 단순 텍스트가 아닌 정밀 계측 카드와 카테고리 뱃지가 구조화되어야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var activeList = doc.Descendants()
.FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ActiveQueueList");
Assert.NotNull(activeList);
var dataTemplate = activeList.Descendants().FirstOrDefault(e => e.Name.LocalName == "DataTemplate");
Assert.NotNull(dataTemplate);
// 카테고리 뱃지 및 모노스페이스 텔레메트리 식별
var hasCategoryPill = dataTemplate.Descendants().Any(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ItemCategoryBadge" ||
e.Attribute("Tag")?.Value == "CategoryPill");
Assert.True(hasCategoryPill, "대기열 카드에 포맷 카테고리 뱃지(ItemCategoryBadge)가 있어야 합니다.");
}
[Fact]
public void SwissMinimal_Tokens_MustInclude_NeonCyanSignatureAccent()
{
// designpaca 사용자 인터뷰 결정: 스위스 미니멀 + 네온 시안(#06B6D4) 시그니처 악센트 토큰 검증
var themeFile = Path.Combine(ViewsDir, "FormatShiftTheme.xaml");
var doc = XDocument.Parse(File.ReadAllText(themeFile));
var cyanBrush = doc.Descendants()
.FirstOrDefault(e => e.Attribute(XName.Get("Key", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "FsAccentCyan");
Assert.NotNull(cyanBrush);
Assert.Equal("#06B6D4", cyanBrush.Attribute("Color")?.Value);
var cyanBgBrush = doc.Descendants()
.FirstOrDefault(e => e.Attribute(XName.Get("Key", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "FsAccentCyanBg");
Assert.NotNull(cyanBgBrush);
Assert.Equal("#083344", cyanBgBrush.Attribute("Color")?.Value);
}
}

View file

@ -25,16 +25,21 @@ public class DesignAuditVisualTreeTests
public bool Contains(string key) => _d.ContainsKey(key);
}
private static readonly object AppInitLock = new();
private static void RunOnSta(Action action)
{
Exception? ex = null;
var thread = new Thread(() =>
{
try
{
lock (AppInitLock)
{
if (Application.Current == null)
{
_ = new Application();
try { _ = new Application(); } catch (InvalidOperationException) { }
}
}
action();
}
@ -75,6 +80,32 @@ public class DesignAuditVisualTreeTests
});
}
[Fact]
public void MainWindow_ToggleInspectorCommand_TogglesColumnWidthAndLayout()
{
RunOnSta(() =>
{
var engine = Everything2EverythingBootstrap.CreateDefault();
var store = new FakeSettingsStore();
var window = new MainWindow(engine, store);
Assert.True(window.IsInspectorVisible);
// Toggle to collapsed
window.ToggleInspectorCommand.Execute(null);
Assert.False(window.IsInspectorVisible);
var content = (UIElement)window.Content;
content.Measure(new Size(1200, 800));
content.Arrange(new Rect(0, 0, 1200, 800));
Assert.True(content.DesiredSize.Width > 0);
// Toggle back to expanded
window.ToggleInspectorCommand.Execute(null);
Assert.True(window.IsInspectorVisible);
});
}
[Fact]
public void DiagnoseWindow_MeasureAndArrange_HasValidLayoutBounds()
{
@ -155,6 +186,69 @@ public class DesignAuditVisualTreeTests
});
}
[Fact]
public void DiagnoseWindow_ProviderCards_MustHaveStructuredGridAlignment_And_TextWrapping()
{
RunOnSta(() =>
{
var engine = Everything2EverythingBootstrap.CreateDefault();
var window = new DiagnoseWindow(engine);
window.PopulateAsync().GetAwaiter().GetResult();
var content = (UIElement)window.Content;
content.Measure(new Size(640, 540));
content.Arrange(new Rect(0, 0, 640, 540));
// 1. ItemsPanel 내부에 카드들이 채워졌는지 검증
var itemsPanel = (StackPanel)window.FindName("ItemsPanel");
Assert.NotNull(itemsPanel);
Assert.True(itemsPanel.Children.Count >= 2, "환경 섹션 및 프로바이더 카드들이 채워져야 합니다.");
// 2. 입력/출력 텍스트가 단일 비정렬 문자열("입력: ... → 출력: ...")로 오버플로되지 않고
// 정형화된 그리드 또는 TextWrapping이 적용된 구조인지 검증
var allTextBlocks = FindLogicalChildren<TextBlock>(window).ToList();
var rawUnwrappedOverflows = allTextBlocks.Where(tb => tb.Text.Contains("입력:") && tb.Text.Contains("→") && tb.TextWrapping != TextWrapping.Wrap).ToList();
Assert.Empty(rawUnwrappedOverflows);
// 3. 프로바이더 카드 내에 '입력 형식' 및 '출력 형식' 라벨이 명확히 분리 정렬되어 있는지 검증
var inputLabels = allTextBlocks.Where(tb => tb.Text == "입력 형식").ToList();
var outputLabels = allTextBlocks.Where(tb => tb.Text == "출력 형식").ToList();
Assert.NotEmpty(inputLabels);
Assert.NotEmpty(outputLabels);
// 4. 모든 가로 정렬 헤더(제목 + 뱃지)에서 VerticalAlignment가 Center로 통일되어 있는지 검증
var badges = allTextBlocks.Where(tb => tb.Text is "준비됨" or "개발 중" or "점검 필요" or "비활성").ToList();
Assert.NotEmpty(badges);
foreach (var badge in badges)
{
var border = badge.Parent as Border;
Assert.NotNull(border);
Assert.Equal(VerticalAlignment.Center, border.VerticalAlignment);
}
});
}
[Fact]
public void MainWindow_TopHeader_EngineTelemetryBadge_HasValidBounds_And_CenterAlignment()
{
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 badge = (Border)window.FindName("EngineTelemetryBadge");
Assert.NotNull(badge);
Assert.Equal(VerticalAlignment.Center, badge.VerticalAlignment);
Assert.True(badge.DesiredSize.Width > 0);
Assert.True(badge.DesiredSize.Height > 0);
});
}
private static IEnumerable<T> FindLogicalChildren<T>(object parent) where T : DependencyObject
{
if (parent is ContentControl cc && cc.Content != null)

View file

@ -0,0 +1,81 @@
using Everything2Everything.App.ViewModels;
using Everything2Everything.Core.Presets;
using Xunit;
namespace Everything2Everything.Tests;
/// <summary>
/// 출력 형식 연동 스마트 프리셋 엔진 TDD 단위 테스트 (RED -> GREEN)
/// </summary>
public class FormatPresetEngineTests
{
[Theory]
[InlineData(".mp3", "최고음질 320k", 320)]
[InlineData(".m4a", "최고음질 320k", 320)]
[InlineData(".aac", "표준 192k", 192)]
public void GetPresetsForExtension_Audio_ReturnsAudioSpecificPresets(string ext, string expectedTitle, int expectedBitrate)
{
var presets = FormatPresetEngine.GetPresetsForExtension(ext);
Assert.NotEmpty(presets);
Assert.Contains(presets, p => p.Title.Contains(expectedTitle));
var targetPreset = presets.First(p => p.Title.Contains(expectedTitle));
Assert.NotEmpty(targetPreset.SpecChips);
var options = new OptionsViewModel();
targetPreset.Apply(options);
Assert.Equal(expectedBitrate, options.AudioBitrateKbps);
}
[Fact]
public void GetPresetsForExtension_WebP_ReturnsImageQualityPresets()
{
var presets = FormatPresetEngine.GetPresetsForExtension(".webp");
Assert.True(presets.Count >= 3);
var q85 = presets.First(p => p.Title.Contains("웹 고화질"));
Assert.Contains("Quality 85", q85.SpecChips);
var options = new OptionsViewModel();
q85.Apply(options);
Assert.Equal(85, options.ImageQuality);
Assert.True(options.StripMetadata);
}
[Fact]
public void GetPresetsForExtension_Pdf_ReturnsResolutionPresets()
{
var presets = FormatPresetEngine.GetPresetsForExtension(".pdf");
Assert.True(presets.Count >= 3);
var print300 = presets.First(p => p.Title.Contains("인쇄용") || p.Title.Contains("300"));
Assert.Contains(print300.SpecChips, c => c.Contains("300 DPI"));
var options = new OptionsViewModel();
print300.Apply(options);
Assert.Equal(100, options.Quality);
}
[Fact]
public void GetPresetsForExtension_Video_ReturnsCrfPresets()
{
var presets = FormatPresetEngine.GetPresetsForExtension(".mp4");
Assert.True(presets.Count >= 3);
var web1080 = presets.First(p => p.Title.Contains("1080p"));
Assert.Contains(web1080.SpecChips, c => c.Contains("1080p") || c.Contains("H.264"));
var options = new OptionsViewModel();
web1080.Apply(options);
Assert.Equal(23, options.VideoCrf);
Assert.Equal("fast", options.VideoPreset);
}
[Fact]
public void GetPresetsForExtension_UnknownExtension_ReturnsFallbackPresets()
{
var presets = FormatPresetEngine.GetPresetsForExtension(".xyz");
Assert.NotEmpty(presets);
Assert.Contains(presets, p => p.Title.Contains("기본"));
}
}

View file

@ -0,0 +1,330 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Xml.Linq;
using Everything2Everything.App.Views;
using Everything2Everything.Core;
using Xunit;
namespace Everything2Everything.Tests;
public class UnifiedTitleBarTests
{
private static readonly string SolutionRoot = FindSolutionRoot();
private static readonly string MainWindowXamlPath = Path.Combine(SolutionRoot, "src", "Everything2Everything.App", "Views", "MainWindow.xaml");
private static string FindSolutionRoot()
{
var dir = Directory.GetCurrentDirectory();
while (dir != null && !File.Exists(Path.Combine(dir, "Everything2Everything.slnx")))
{
dir = Directory.GetParent(dir)?.FullName;
}
return dir ?? throw new DirectoryNotFoundException("솔루션 루트를 찾을 수 없습니다.");
}
private sealed class FakeSettingsStore : ISettingsStore
{
private readonly Dictionary<string, string> _d = new();
public string? Get(string key) => _d.TryGetValue(key, out var v) ? v : null;
public void Set(string key, string value) => _d[key] = value;
public void Remove(string key) => _d.Remove(key);
public bool Contains(string key) => _d.ContainsKey(key);
}
[Fact]
public void MainWindow_MustNotHave_Isolated32PxTitleBarRow()
{
// 32px짜리 고립된 빈 타이틀바 행이 존재하지 않아야 하며,
// 타이틀바와 윈도우 조작 버튼(최소/최대/닫기)이 메인 헤더와 하나의 통합 바로 통합되어야 한다.
var doc = XDocument.Parse(File.ReadAllText(MainWindowXamlPath));
var rootGrid = doc.Root?.Elements().FirstOrDefault(e => e.Name.LocalName == "Grid");
Assert.NotNull(rootGrid);
var rowDefs = rootGrid.Element(rootGrid.Name.Namespace + "Grid.RowDefinitions")?.Elements().ToList();
Assert.NotNull(rowDefs);
var has32PxRow = rowDefs.Any(r => r.Attribute("Height")?.Value == "32");
Assert.False(has32PxRow, "최상위 Grid에 32px 분리된 타이틀바 행이 여전히 존재합니다. 헤더와 통합된 단일 바로 구성되어야 합니다.");
}
[Fact]
public void MainWindow_TitleBar_MustBeIntegratedWithTopHeader()
{
// ui:TitleBar 가 분리된 빈 줄이 아니라 통합 헤더 영역(Height >= 48) 내에 배치되어야 한다.
var doc = XDocument.Parse(File.ReadAllText(MainWindowXamlPath));
var titleBar = doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "TitleBar");
Assert.NotNull(titleBar);
var gridRow = titleBar.Attribute("Grid.Row")?.Value;
var rootGrid = doc.Root?.Elements().FirstOrDefault(e => e.Name.LocalName == "Grid");
var rowDefs = rootGrid?.Element(rootGrid.Name.Namespace + "Grid.RowDefinitions")?.Elements().ToList();
if (gridRow != null && int.TryParse(gridRow, out int rowIndex) && rowDefs != null && rowIndex < rowDefs.Count)
{
var rowHeight = rowDefs[rowIndex].Attribute("Height")?.Value;
Assert.NotEqual("32", rowHeight);
}
}
private static readonly object AppInitLock = new();
[Fact]
public void MainWindow_UnifiedTitleBar_MeasuresAndArrangesCorrectly()
{
Exception? ex = null;
var thread = new Thread(() =>
{
try
{
lock (AppInitLock)
{
if (Application.Current == null)
{
try { _ = new Application(); } catch (InvalidOperationException) { }
}
}
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));
Assert.True(content.DesiredSize.Width > 0);
Assert.True(content.DesiredSize.Height > 0);
var titleBar = FindLogicalChild<Wpf.Ui.Controls.TitleBar>(window);
Assert.NotNull(titleBar);
Assert.True(titleBar.ActualHeight >= 40 || titleBar.DesiredSize.Height >= 40,
$"통합 TitleBar의 높이가 최소 40px 이상이어야 합니다. (실제: {titleBar.DesiredSize.Height})");
}
catch (Exception e)
{
ex = e;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (ex != null)
{
throw new AggregateException("STA 스레드 실행 중 예외 발생", ex);
}
}
[Fact]
public void MainWindow_MustNotHave_ClunkyBrandContainer_Or_RedundantTitleText()
{
// 사용자가 불필요하고 이상하다고 지적한 상단 좌측 320px 컨테이너 및 'FormatShift Utility' 문구가 완전히 제거되었는지 정적 AST 검증
var doc = XDocument.Parse(File.ReadAllText(MainWindowXamlPath));
var titleBar = doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "TitleBar");
Assert.NotNull(titleBar);
var header = titleBar.Elements().FirstOrDefault(e => e.Name.LocalName == "TitleBar.Header");
Assert.NotNull(header);
var allTextBlocks = header.Descendants().Where(e => e.Name.LocalName == "TextBlock" || e.Name.LocalName == "Run").ToList();
var redundantTexts = allTextBlocks.Where(tb =>
(tb.Attribute("Text")?.Value?.Contains("FormatShift") ?? false) ||
(tb.Attribute("Text")?.Value?.Contains("Utility") ?? false) ||
tb.Value.Contains("FormatShift") || tb.Value.Contains("Utility")).ToList();
Assert.Empty(redundantTexts);
// 320px 너비의 패널 컨테이너 Border가 존재하지 않아야 함
var columnDefs = header.Descendants().Where(e => e.Name.LocalName == "ColumnDefinition").ToList();
var has320Col = columnDefs.Any(c => c.Attribute("Width")?.Value == "320");
Assert.False(has320Col, "상단 바에 320px짜리 투박한 사이드바 헤더 컨테이너가 남아있습니다. 제거되어야 합니다.");
}
[Fact]
public void MainWindow_TopHeader_AllElements_MustSharePreciseVerticalCenterline()
{
// 상단 바 내 모든 요소(최소/최대/닫기 창 버튼, 탭, 엔진 뱃지, 액션 버튼)가
// 시각적으로 들뜨거나 가라앉지 않고 동일한 수직 중심선(Centerline, 오차 2.0px 이내)에 완벽 정렬되어야 한다.
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));
var titleBar = (Wpf.Ui.Controls.TitleBar)window.FindName("AppTitleBar");
Assert.NotNull(titleBar);
titleBar.ApplyTemplate();
var minBtn = (FrameworkElement?)titleBar.Template.FindName("PART_MinimizeButton", titleBar);
Assert.NotNull(minBtn);
var tabBtn = (FrameworkElement)window.FindName("TabActiveBtn");
var badge = (FrameworkElement)window.FindName("EngineTelemetryBadge");
var clearBtn = (FrameworkElement)window.FindName("ClearAllButton");
// TitleBar 좌표계 기준 각 컨트롤의 수직 중심 Y좌표 계산
double CenterY(FrameworkElement element)
{
var pt = element.TransformToAncestor(titleBar).Transform(new Point(0, element.ActualHeight / 2.0));
return pt.Y;
}
var minCenterY = CenterY(minBtn);
var tabCenterY = CenterY(tabBtn);
var badgeCenterY = CenterY(badge);
var clearCenterY = CenterY(clearBtn);
// 오차 2.0px 이내로 모든 요소가 동일한 수직 기준선상에 정렬되어야 함
Assert.True(Math.Abs(tabCenterY - minCenterY) <= 2.0,
$"최소화 버튼 중심선({minCenterY:F1}px)과 탭 중심선({tabCenterY:F1}px)의 수직 정렬 오차가 너무 큽니다.");
Assert.True(Math.Abs(badgeCenterY - minCenterY) <= 2.0,
$"최소화 버튼 중심선({minCenterY:F1}px)과 엔진 뱃지 중심선({badgeCenterY:F1}px)의 수직 정렬 오차가 너무 큽니다.");
Assert.True(Math.Abs(clearCenterY - minCenterY) <= 2.0,
$"최소화 버튼 중심선({minCenterY:F1}px)과 액션 버튼 중심선({clearCenterY:F1}px)의 수직 정렬 오차가 너무 큽니다.");
try
{
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 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_window.png";
using var fs = File.Create(outPath);
enc.Save(fs);
}
catch { }
});
}
[Fact]
public void MainWindow_HeaderElements_MustBeIntegratedIntoTitleBarHeaderAndTrailingContent_ToPreventNcHitTestBlocking()
{
// Wpf.Ui.Controls.TitleBar의 HwndSourceHook(WM_NCHITTEST)은
// 오직 Header, CenterContent, TrailingContent 내부에 포함된 요소만 HTCLIENT(클릭 허용)로 반환하며,
// 그 외의 형제 요소나 TitleBar 외부 겹침 요소는 모두 HTCAPTION(창 이동 드래그)으로 가로채 클릭을 차단한다.
// 따라서 상단 탭, 뱃지, 액션 버튼들은 반드시 TitleBar.Header와 TitleBar.TrailingContent 내에 직접 배치되어야 한다.
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));
var tb = (Wpf.Ui.Controls.TitleBar)window.FindName("AppTitleBar");
Assert.NotNull(tb);
Assert.NotNull(tb.Header);
Assert.NotNull(tb.TrailingContent);
var tabActive = window.FindName("TabActiveBtn") as UIElement;
Assert.NotNull(tabActive);
var telemetryBadge = window.FindName("EngineTelemetryBadge") as UIElement;
Assert.NotNull(telemetryBadge);
var clearBtn = window.FindName("ClearAllButton") as UIElement;
Assert.NotNull(clearBtn);
// tabActive와 telemetryBadge가 tb.Header의 자식인지 검증
var headerElement = tb.Header as DependencyObject;
Assert.NotNull(headerElement);
Assert.True(IsDescendantOf(tabActive, headerElement), "TabActiveBtn은 반드시 AppTitleBar.Header 내부에 배치되어야 클릭이 차단되지 않습니다.");
Assert.True(IsDescendantOf(telemetryBadge, headerElement), "EngineTelemetryBadge는 AppTitleBar.Header 내부에 배치되어야 합니다.");
// clearBtn이 tb.TrailingContent의 자식인지 검증
var trailingElement = tb.TrailingContent as DependencyObject;
Assert.NotNull(trailingElement);
Assert.True(IsDescendantOf(clearBtn, trailingElement), "ClearAllButton은 반드시 AppTitleBar.TrailingContent 내부에 배치되어야 클릭이 차단되지 않습니다.");
});
}
[Fact]
public void MainWindow_HeaderLayout_MustBeSpaciousAndNonOverlapping()
{
// 최소 지원 해상도(1080px)에서도 좌측 탭과 우측 액션 버튼이 충돌하지 않도록,
// 우측 액션 툴바(설정, 메뉴, 진단, 로그, 비우기)의 총 가로폭이 260px 이하여야 한다.
RunOnSta(() =>
{
var engine = Everything2EverythingBootstrap.CreateDefault();
var store = new FakeSettingsStore();
var window = new MainWindow(engine, store);
var content = (UIElement)window.Content;
content.Measure(new Size(1080, 720));
content.Arrange(new Rect(0, 0, 1080, 720));
var tb = (Wpf.Ui.Controls.TitleBar)window.FindName("AppTitleBar");
Assert.NotNull(tb);
var trailing = tb.TrailingContent as FrameworkElement;
Assert.NotNull(trailing);
Assert.True(trailing.DesiredSize.Width <= 260,
$"우측 액션 버튼 그룹의 가로 폭이 너무 넓습니다({trailing.DesiredSize.Width}px). 260px 이하로 컴팩트하게 구성되어야 왼쪽 탭과 겹치지 않습니다.");
});
}
private static bool IsDescendantOf(DependencyObject? node, DependencyObject targetAncestor)
{
while (node != null)
{
if (node == targetAncestor) return true;
node = LogicalTreeHelper.GetParent(node) ?? (node as FrameworkElement)?.Parent;
}
return false;
}
private static void RunOnSta(Action action)
{
Exception? ex = null;
var thread = new Thread(() =>
{
try
{
lock (AppInitLock)
{
if (Application.Current == null)
{
try { _ = new Application(); } catch (InvalidOperationException) { }
}
}
action();
}
catch (Exception e)
{
ex = e;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (ex != null) throw new AggregateException("STA 스레드 실행 중 예외 발생", ex);
}
private static T? FindLogicalChild<T>(DependencyObject parent) where T : DependencyObject
{
foreach (var child in LogicalTreeHelper.GetChildren(parent))
{
if (child is T typed) return typed;
if (child is DependencyObject dep)
{
var found = FindLogicalChild<T>(dep);
if (found != null) return found;
}
}
return null;
}
}