feat(ui): elevate cockpit ergonomics, add search watermark, queue selection styling, and shortcut bindings
This commit is contained in:
parent
4ca7352dc3
commit
388d6707d0
6 changed files with 462 additions and 23 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -47,6 +47,9 @@ src/Everything2Everything.Shell/Everythi*/
|
|||
# .NET artifacts dir from BuildMsix.ps1
|
||||
artifacts/
|
||||
|
||||
# test assets
|
||||
test_assets/
|
||||
|
||||
# NuGet packages restore + nuget.exe cache
|
||||
packages/
|
||||
tools/*
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?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.18.0" ProcessorArchitecture="x64" />
|
||||
<Identity Name="Everything2Everything.YunChan" Publisher="CN=Everything2EverythingDev" Version="1.0.20.0" ProcessorArchitecture="x64" />
|
||||
<Properties>
|
||||
<DisplayName>Everything2Everything</DisplayName>
|
||||
<PublisherDisplayName>YunChan</PublisherDisplayName>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@
|
|||
<Window.InputBindings>
|
||||
<KeyBinding Key="O" Modifiers="Ctrl" Command="{Binding AddFilesCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
||||
<KeyBinding Key="Enter" Modifiers="Ctrl" Command="{Binding ProcessQueueCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
||||
<KeyBinding Key="A" Modifiers="Ctrl" Command="{Binding SelectAllQueueCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
||||
<KeyBinding Key="Delete" Command="{Binding DeleteSelectedQueueCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
||||
<KeyBinding Key="Escape" Command="{Binding CloseCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
||||
<KeyBinding Key="F5" Command="{Binding RefreshCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
||||
</Window.InputBindings>
|
||||
|
|
@ -174,7 +176,7 @@
|
|||
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto" Padding="16,16,16,16">
|
||||
<StackPanel>
|
||||
<!-- Smart Conversion Deck: Output Format + Dynamic Presets + Live Spec Chips + Quality -->
|
||||
<Border Style="{StaticResource FsDoubleBezelShellStyle}" Margin="0,0,0,16">
|
||||
<Border Style="{StaticResource FsDoubleBezelShellStyle}" Margin="0,0,0,12">
|
||||
<Border Style="{StaticResource FsDoubleBezelCoreStyle}">
|
||||
<StackPanel>
|
||||
<Grid Margin="0,0,0,12">
|
||||
|
|
@ -217,6 +219,7 @@
|
|||
</Grid>
|
||||
<TextBlock x:Name="OutputFormatHint" Margin="0,0,0,12"
|
||||
Style="{StaticResource FsCaptionStyle}"
|
||||
TextWrapping="Wrap"
|
||||
Text="모든 입력에서 변환 가능한 형식이 표시됩니다"/>
|
||||
|
||||
<Border Height="1" Background="{StaticResource FsBorderSubtle}" Margin="0,0,0,12"/>
|
||||
|
|
@ -464,16 +467,16 @@
|
|||
</Border>
|
||||
|
||||
<!-- Card 3: Output Destination & Conflict Rule -->
|
||||
<Border Style="{StaticResource FsDoubleBezelShellStyle}" Margin="0,0,0,16">
|
||||
<Border Style="{StaticResource FsDoubleBezelShellStyle}" Margin="0,0,0,12">
|
||||
<Border Style="{StaticResource FsDoubleBezelCoreStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="저장 위치 및 충돌 규칙" Style="{StaticResource FsLabelStyle}"/>
|
||||
<Grid Margin="0,10,0,0">
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox x:Name="OutputPathTextBox"
|
||||
<TextBox x:Name="OutputPathTextBox"
|
||||
Style="{StaticResource FsPathInputStyle}"
|
||||
Text="{Binding Options.CustomOutputDirectory, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="비워두면 원본 폴더 옆 서브폴더에 저장"/>
|
||||
|
|
@ -481,11 +484,11 @@
|
|||
Content="…" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Command="{Binding PickOutputFolderCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
||||
</Grid>
|
||||
<TextBlock x:Name="OutputDestHint" Margin="0,6,0,0"
|
||||
<TextBlock x:Name="OutputDestHint" Margin="0,5,0,0"
|
||||
Style="{StaticResource FsCaptionStyle}"
|
||||
Text="비워두면 원본 옆 _jpg 폴더에 저장됩니다"/>
|
||||
|
||||
<Border Height="1" Background="{StaticResource FsBorderSubtle}" Margin="0,14,0,14"/>
|
||||
<Border Height="1" Background="{StaticResource FsBorderSubtle}" Margin="0,8,0,8"/>
|
||||
|
||||
<TextBlock Text="파일 충돌 해결" Style="{StaticResource FsLabelStyle}"/>
|
||||
<Border Margin="0,8,0,0"
|
||||
|
|
@ -592,7 +595,7 @@
|
|||
|
||||
<!-- Sidebar footer -->
|
||||
<Border Grid.Row="1" BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="0,1,0,0" Padding="24">
|
||||
BorderThickness="0,1,0,0" Padding="16,12">
|
||||
<StackPanel>
|
||||
<TextBlock x:Name="CapabilityStatusText" Margin="0,0,0,12"
|
||||
Style="{StaticResource FsCaptionStyle}"
|
||||
|
|
@ -666,16 +669,56 @@
|
|||
BorderThickness="0,0,1,1" Padding="16,8">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="160"/>
|
||||
<ColumnDefinition Width="175"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- Search Box -->
|
||||
<Grid Grid.Column="0">
|
||||
<!-- Search Box with Live Watermark & Clear Button -->
|
||||
<Grid Grid.Column="0" Width="175" VerticalAlignment="Center">
|
||||
<TextBox x:Name="SearchBox" Style="{StaticResource FsPathInputStyle}"
|
||||
Padding="30,5,8,5" Text="{Binding SearchText, RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="파일명 또는 확장자로 검색"/>
|
||||
Padding="28,5,24,5" Text="{Binding SearchText, RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="파일명 또는 확장자로 검색" VerticalAlignment="Center"/>
|
||||
<ui:SymbolIcon Symbol="Search24" FontSize="13" Foreground="{StaticResource FsTextTertiary}"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Center" Margin="9,0,0,0" IsHitTestVisible="False"/>
|
||||
HorizontalAlignment="Left" VerticalAlignment="Center" Margin="8,0,0,0" IsHitTestVisible="False"/>
|
||||
<TextBlock x:Name="SearchPlaceholderText"
|
||||
Text="파일명 검색…"
|
||||
Foreground="{StaticResource FsTextTertiary}"
|
||||
VerticalAlignment="Center" Margin="28,0,0,0"
|
||||
IsHitTestVisible="False">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock" BasedOn="{StaticResource FsCaptionStyle}">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Text, ElementName=SearchBox}" Value="">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Text, ElementName=SearchBox}" Value="{x:Null}">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<Button x:Name="SearchClearButton"
|
||||
Width="20" Height="20"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
Margin="0,0,6,0"
|
||||
ToolTip="검색어 지우기"
|
||||
Command="{Binding ClearSearchCommand, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button" BasedOn="{StaticResource FsIconButtonStyle}">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Text, ElementName=SearchBox}" Value="">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Text, ElementName=SearchBox}" Value="{x:Null}">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
<ui:SymbolIcon Symbol="Dismiss24" FontSize="10" Foreground="{StaticResource FsTextTertiary}" VerticalAlignment="Center"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
<!-- Category Filter Chips & Inspector Toggle -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
|
|
@ -751,6 +794,28 @@
|
|||
<CheckBox x:Name="BatchSelectAllCheck" Content="전체 선택" VerticalAlignment="Center"
|
||||
Command="{Binding BatchSelectAllCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding IsChecked, RelativeSource={RelativeSource Self}}"/>
|
||||
<!-- Live Queue Telemetry Capsule (Cockpit Summary) -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Border Background="#10FFFFFF" BorderBrush="{StaticResource FsBorderHairline}"
|
||||
BorderThickness="1" CornerRadius="12" Padding="10,3" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="QueueSummaryCountText" Text="0개 항목"
|
||||
FontFamily="{StaticResource FsFontSans}" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource FsTextPrimary}" VerticalAlignment="Center"/>
|
||||
<TextBlock Text=" · " FontSize="11" Foreground="{StaticResource FsTextTertiary}" VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="QueueSummarySizeText" Text="0 B"
|
||||
FontFamily="{StaticResource FsFontMono}" FontSize="11"
|
||||
Foreground="{StaticResource FsAccentCyan}" VerticalAlignment="Center"/>
|
||||
<Border x:Name="QueueSelectedBadge" Visibility="Collapsed"
|
||||
Margin="8,0,0,0" Background="{StaticResource FsAccentCyanBg}"
|
||||
CornerRadius="6" Padding="6,1" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="QueueSelectedText" Text="0개 선택됨"
|
||||
FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource FsAccentCyan}" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button Margin="0,0,8,0" Padding="10,4" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Command="{Binding BatchRemoveSelectedCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
|
|
@ -843,9 +908,18 @@
|
|||
ItemsSource="{Binding ActiveQueue, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<!-- row hover: FsListRowStyle Neo Glass 카드 + 좌측 형식색 accent bar -->
|
||||
<Border x:Name="QueueRow" Style="{StaticResource FsListRowStyle}" Padding="0"
|
||||
Cursor="Hand">
|
||||
<!-- row hover: FsListRowStyle Neo Glass 카드 + 좌측 형식색 accent bar + IsSelected 강조 -->
|
||||
<Border x:Name="QueueRow" Padding="0" Cursor="Hand">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border" BasedOn="{StaticResource FsListRowStyle}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected}" Value="True">
|
||||
<Setter Property="BorderBrush" Value="#6038BDF8"/>
|
||||
<Setter Property="Background" Value="#162033"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<i:Interaction.Triggers>
|
||||
<i:EventTrigger EventName="PreviewMouseLeftButtonUp">
|
||||
<i:InvokeCommandAction Command="{Binding QueueRowCommand, RelativeSource={RelativeSource AncestorType=Window}}" PassEventArgsToCommand="True"/>
|
||||
|
|
@ -939,6 +1013,52 @@
|
|||
|
||||
<!-- Past Results view -->
|
||||
<Grid x:Name="PastResultsContainer" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Telemetry Header Bar -->
|
||||
<Border x:Name="PastResultsHeaderBar" Grid.Row="0"
|
||||
Background="{StaticResource FsBgPanel}"
|
||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="0,0,0,1" Padding="24,8">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Background="#10FFFFFF" BorderBrush="{StaticResource FsBorderHairline}"
|
||||
BorderThickness="1" CornerRadius="12" Padding="10,4" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ui:SymbolIcon Symbol="CheckmarkCircle24" FontSize="13" Foreground="{StaticResource FsAccentGreen}"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="누적 변환: " Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="PastTotalCountText" Text="0개 파일"
|
||||
FontFamily="{StaticResource FsFontMono}" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource FsTextPrimary}" VerticalAlignment="Center"/>
|
||||
<TextBlock Text=" · " FontSize="11" Foreground="{StaticResource FsTextTertiary}" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="총 절감: " Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="PastTotalSavingsText" Text="0 B"
|
||||
FontFamily="{StaticResource FsFontMono}" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource FsAccentGreen}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button Padding="10,4" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Command="{Binding ExportLogCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
ToolTip="변환 기록 CSV/JSON 내보내기">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ui:SymbolIcon Symbol="ArrowDownload24" FontSize="12" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<TextBlock Text="내보내기" FontSize="11" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<!-- 빈 상태 안내 (Empty State) -->
|
||||
<Grid x:Name="PastResultsEmpty" Visibility="Collapsed">
|
||||
<Border Background="{StaticResource FsBgBase}" Padding="24">
|
||||
|
|
@ -1106,6 +1226,7 @@
|
|||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
|
@ -1211,6 +1332,60 @@
|
|||
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<StackPanel>
|
||||
<!-- Conversion Pipeline Card (원본 -> 변환 대상 예상) -->
|
||||
<Border x:Name="ConversionPipelineCard"
|
||||
Margin="0,0,0,12"
|
||||
Background="{StaticResource FsBgSurface}"
|
||||
BorderBrush="{StaticResource FsBorderHairline}"
|
||||
BorderThickness="1" CornerRadius="8" Padding="12,10">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" VerticalAlignment="Center">
|
||||
<TextBlock Text="현재 파일" Style="{StaticResource FsCaptionStyle}" FontSize="10" VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="PipelineSourceText" Text="—"
|
||||
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
||||
FontWeight="SemiBold" Foreground="{StaticResource FsTextPrimary}"
|
||||
Margin="0,2,0,0" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ui:SymbolIcon Grid.Column="1" Symbol="ArrowRight24" FontSize="14"
|
||||
Foreground="{StaticResource FsAccentCyan}"
|
||||
VerticalAlignment="Center" Margin="8,0"/>
|
||||
<StackPanel Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Right">
|
||||
<TextBlock Text="변환 대상" Style="{StaticResource FsCaptionStyle}" FontSize="10" HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="PipelineTargetText" Text="—"
|
||||
FontFamily="{StaticResource FsFontMono}" FontSize="12"
|
||||
FontWeight="SemiBold" Foreground="{StaticResource FsAccentCyan}"
|
||||
Margin="0,2,0,0" HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Border Grid.Row="1" Margin="0,8,0,0" Background="#0CFFFFFF"
|
||||
CornerRadius="4" Padding="8,4" VerticalAlignment="Center">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="PipelineLossText" Text="최적화 변환 준비"
|
||||
FontSize="11" Foreground="{StaticResource FsAccentGreen}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="PipelineEstimatedText" Grid.Column="1"
|
||||
Text="실시간 엔진 매칭"
|
||||
FontFamily="{StaticResource FsFontMono}" FontSize="11"
|
||||
Foreground="{StaticResource FsAccentGreen}" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="PreviewFileName"
|
||||
Style="{StaticResource FsBodyStyle}"
|
||||
FontWeight="SemiBold" TextWrapping="NoWrap"
|
||||
|
|
@ -1268,9 +1443,11 @@
|
|||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="0"
|
||||
<Button x:Name="PreviewOpenButton"
|
||||
Grid.Column="0"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Padding="12,8"
|
||||
IsEnabled="{Binding HasSelectedItem, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
Command="{Binding PreviewOpenFileCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
ToolTip="기본 프로그램으로 파일 열기">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
|
|
@ -1278,9 +1455,11 @@
|
|||
<TextBlock Text="열기" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="2"
|
||||
<Button x:Name="PreviewRevealButton"
|
||||
Grid.Column="2"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Padding="12,8"
|
||||
IsEnabled="{Binding HasSelectedItem, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
Command="{Binding PreviewOpenFolderCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
ToolTip="탐색기에서 파일 위치 열기">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
|
|
|
|||
|
|
@ -18,8 +18,30 @@ using LossClass = Everything2Everything.Core.Providers.LossClass;
|
|||
|
||||
namespace Everything2Everything.App.Views;
|
||||
|
||||
public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
||||
public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
|
||||
private bool _hasSelectedItem;
|
||||
public bool HasSelectedItem
|
||||
{
|
||||
get => _hasSelectedItem;
|
||||
set
|
||||
{
|
||||
if (_hasSelectedItem != value)
|
||||
{
|
||||
_hasSelectedItem = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand ClearSearchCommand { get; }
|
||||
public ICommand SelectAllQueueCommand => BatchSelectAllCommand;
|
||||
public ICommand DeleteSelectedQueueCommand => BatchRemoveSelectedCommand;
|
||||
|
||||
private readonly ConversionEngine _engine;
|
||||
private readonly ISettingsStore _settings;
|
||||
private readonly OptionsViewModel _options = new();
|
||||
|
|
@ -153,6 +175,11 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
|||
DragLeaveCommand = new RelayCommand(_ => DropHintOverlay.Visibility = Visibility.Collapsed);
|
||||
DropCommand = new RelayCommand(p => HandleFilesDropped(p as DragEventArgs));
|
||||
QueueRowCommand = new RelayCommand(p => HandleQueueRowClick(p as MouseButtonEventArgs));
|
||||
ClearSearchCommand = new RelayCommand(_ =>
|
||||
{
|
||||
SearchText = "";
|
||||
if (SearchBox is not null) SearchBox.Text = "";
|
||||
});
|
||||
PastRowCommand = new RelayCommand(p => HandlePastRowClick(p as MouseButtonEventArgs));
|
||||
|
||||
InitializeComponent();
|
||||
|
|
@ -302,7 +329,9 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
|||
foreach (var path in paths)
|
||||
{
|
||||
if (!File.Exists(path) || existing.Contains(path)) continue;
|
||||
_activeQueue.Add(QueueItem.FromPath(path));
|
||||
var item = QueueItem.FromPath(path);
|
||||
item.PropertyChanged += OnQueueItemPropertyChanged;
|
||||
_activeQueue.Add(item);
|
||||
}
|
||||
UpdateBadges();
|
||||
UpdateProcessQueueButton();
|
||||
|
|
@ -316,9 +345,18 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
|||
}
|
||||
}
|
||||
|
||||
private void OnQueueItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(QueueItem.IsSelected))
|
||||
{
|
||||
UpdateQueueSummary();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveQueueItem(QueueItem? item)
|
||||
{
|
||||
if (item is null) return;
|
||||
item.PropertyChanged -= OnQueueItemPropertyChanged;
|
||||
_activeQueue.Remove(item);
|
||||
UpdateBadges();
|
||||
UpdateProcessQueueButton();
|
||||
|
|
@ -350,6 +388,40 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
|||
var count = _pastResults.Sum(g => g.Entries.Count);
|
||||
TabPastBadge.Text = count.ToString(CultureInfo.InvariantCulture);
|
||||
UpdatePastResultsVisibility();
|
||||
UpdateQueueSummary();
|
||||
UpdatePastResultsTelemetry();
|
||||
}
|
||||
|
||||
private void UpdateQueueSummary()
|
||||
{
|
||||
if (QueueSummaryCountText is null || QueueSummarySizeText is null) return;
|
||||
var count = _activeQueue.Count;
|
||||
long totalBytes = _activeQueue.Sum(q => q.SourceSizeBytes);
|
||||
QueueSummaryCountText.Text = $"총 {count}개 항목";
|
||||
QueueSummarySizeText.Text = HumanizeBytes(totalBytes);
|
||||
|
||||
var selectedCount = _activeQueue.Count(q => q.IsSelected);
|
||||
if (QueueSelectedBadge is not null && QueueSelectedText is not null)
|
||||
{
|
||||
if (selectedCount > 0)
|
||||
{
|
||||
QueueSelectedBadge.Visibility = Visibility.Visible;
|
||||
QueueSelectedText.Text = $"{selectedCount}개 선택됨";
|
||||
}
|
||||
else
|
||||
{
|
||||
QueueSelectedBadge.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePastResultsTelemetry()
|
||||
{
|
||||
if (PastTotalCountText is null || PastTotalSavingsText is null) return;
|
||||
var totalCount = _pastResults.Sum(g => g.Entries.Count);
|
||||
var totalSavings = _pastResults.Sum(g => g.SessionSavingsBytes);
|
||||
PastTotalCountText.Text = $"{totalCount}개 파일";
|
||||
PastTotalSavingsText.Text = HumanizeBytes(totalSavings);
|
||||
}
|
||||
|
||||
private void UpdateProcessQueueButton()
|
||||
|
|
@ -523,10 +595,44 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
|||
var info = FileInspectorBuilder.Build(filePath);
|
||||
PreviewFileName.Text = string.IsNullOrEmpty(fileName) ? info.FileName : fileName;
|
||||
PreviewFilePath.Text = string.IsNullOrEmpty(filePath) ? info.FullPath : filePath;
|
||||
PreviewFormatText.Text = string.IsNullOrEmpty(formatLabel) || formatLabel == "—" ? info.Extension.TrimStart('.').ToUpperInvariant() : formatLabel;
|
||||
PreviewSizeText.Text = string.IsNullOrEmpty(sizeText) || sizeText == "—" ? info.FormattedSize : sizeText;
|
||||
var fmt = string.IsNullOrEmpty(formatLabel) || formatLabel == "—" ? info.Extension.TrimStart('.').ToUpperInvariant() : formatLabel;
|
||||
var sz = string.IsNullOrEmpty(sizeText) || sizeText == "—" ? info.FormattedSize : sizeText;
|
||||
PreviewFormatText.Text = fmt;
|
||||
PreviewSizeText.Text = sz;
|
||||
PreviewDimText.Text = info.DimensionsOrMeta;
|
||||
PreviewPageText.Text = info.Category.ToKoreanLabel();
|
||||
UpdateConversionPipelineCard(fmt, sz);
|
||||
HasSelectedItem = !string.IsNullOrEmpty(filePath);
|
||||
}
|
||||
|
||||
private void UpdateConversionPipelineCard(string? formatLabel, string? sizeText)
|
||||
{
|
||||
if (ConversionPipelineCard is null || PipelineSourceText is null || PipelineTargetText is null) return;
|
||||
|
||||
if (string.IsNullOrEmpty(formatLabel) || formatLabel == "—")
|
||||
{
|
||||
PipelineSourceText.Text = "선택 대기";
|
||||
PipelineTargetText.Text = (SelectedOutputExtension ?? ".jpg").TrimStart('.').ToUpperInvariant();
|
||||
if (PipelineLossText is not null) PipelineLossText.Text = "파일을 선택하면 변환 정보 표시";
|
||||
if (PipelineEstimatedText is not null) PipelineEstimatedText.Text = "대기 중";
|
||||
return;
|
||||
}
|
||||
|
||||
PipelineSourceText.Text = $"{formatLabel} · {sizeText ?? "—"}";
|
||||
var targetExt = (SelectedOutputExtension ?? ".jpg").TrimStart('.').ToUpperInvariant();
|
||||
PipelineTargetText.Text = targetExt;
|
||||
|
||||
var isSame = string.Equals(formatLabel, targetExt, StringComparison.OrdinalIgnoreCase);
|
||||
if (isSame)
|
||||
{
|
||||
if (PipelineLossText is not null) PipelineLossText.Text = "무손실 재압축 및 메타 정돈";
|
||||
if (PipelineEstimatedText is not null) PipelineEstimatedText.Text = "최적화";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (PipelineLossText is not null) PipelineLossText.Text = $"{formatLabel} → {targetExt} 변환";
|
||||
if (PipelineEstimatedText is not null) PipelineEstimatedText.Text = "실시간 매칭";
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowPreviewLoading()
|
||||
|
|
@ -751,6 +857,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
|||
{
|
||||
bool select = parameter is true;
|
||||
BatchQueueService.SetSelectionAll(_activeQueue, select);
|
||||
UpdateQueueSummary();
|
||||
}
|
||||
|
||||
private void BatchRemoveSelected()
|
||||
|
|
@ -1334,6 +1441,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
|||
UpdateCombineState(keepExt);
|
||||
UpdateProcessQueueButton();
|
||||
UpdateSmartPresetsForFormat(keepExt);
|
||||
UpdateConversionPipelineCard(PreviewFormatText?.Text, PreviewSizeText?.Text);
|
||||
|
||||
if (OutputFormatHint is not null)
|
||||
{
|
||||
|
|
@ -1379,6 +1487,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
|||
UpdateQualityPanelForFormat(ext);
|
||||
UpdateOutputDestHint(ext);
|
||||
UpdateSmartPresetsForFormat(ext);
|
||||
UpdateConversionPipelineCard(PreviewFormatText?.Text, PreviewSizeText?.Text);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -947,6 +947,137 @@ public class DesignAuditAstTests
|
|||
Assert.True(int.TryParse(widthVal, out var w) && w <= 180,
|
||||
$"SearchBox 컬럼 폭은 180px 이하여야 필터 버튼들이 잘리지 않습니다. 현재: {widthVal}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchActionBar_MustInclude_LiveCockpitSummary()
|
||||
{
|
||||
// BatchActionBar는 단순 버튼 나열이 아니라 큐의 총 파일 개수 및 총 데이터 용량을 한눈에 보여주는
|
||||
// QueueSummaryCountText 및 QueueSummarySizeText 콕핏 텔레메트리 요소를 포함해야 한다.
|
||||
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||
|
||||
var batchBar = doc.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "BatchActionBar");
|
||||
Assert.NotNull(batchBar);
|
||||
|
||||
var summaryCount = batchBar.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "QueueSummaryCountText");
|
||||
var summarySize = batchBar.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "QueueSummarySizeText");
|
||||
|
||||
Assert.NotNull(summaryCount);
|
||||
Assert.NotNull(summarySize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inspector_MustInclude_ConversionPipelineCard()
|
||||
{
|
||||
// 미리보기 패널(Inspector)은 원본과 대상 형식 간 변환 경로 및 예상 절감 효과를 즉시 시각화하는
|
||||
// ConversionPipelineCard 컨테이너를 포함해야 한다.
|
||||
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||
|
||||
var pipelineCard = doc.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ConversionPipelineCard");
|
||||
Assert.NotNull(pipelineCard);
|
||||
|
||||
var sourceText = pipelineCard.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PipelineSourceText");
|
||||
var targetText = pipelineCard.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PipelineTargetText");
|
||||
|
||||
Assert.NotNull(sourceText);
|
||||
Assert.NotNull(targetText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PastResults_MustInclude_TelemetryHeaderBar()
|
||||
{
|
||||
// 변환 기록 뷰(PastResultsContainer)는 단순 리스트 노출 전에 누적 변환 수량 및 총 절감량을 요약하고
|
||||
// 빠른 내보내기 액션을 제공하는 PastResultsHeaderBar 툴바를 포함해야 한다.
|
||||
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||
|
||||
var container = doc.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PastResultsContainer");
|
||||
Assert.NotNull(container);
|
||||
|
||||
var headerBar = container.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PastResultsHeaderBar");
|
||||
Assert.NotNull(headerBar);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sidebar_OutputFormatHint_MustWrapText()
|
||||
{
|
||||
// 320px 좁은 사이드바 폭에서도 설명 문구가 '자동 필...'처럼 잘리지 않도록
|
||||
// OutputFormatHint는 TextWrapping="Wrap"을 선언해야 한다.
|
||||
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||
|
||||
var hint = doc.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "OutputFormatHint");
|
||||
Assert.NotNull(hint);
|
||||
|
||||
var textWrapping = hint.Attribute("TextWrapping")?.Value;
|
||||
Assert.Equal("Wrap", textWrapping);
|
||||
}
|
||||
[Fact]
|
||||
public void SearchBox_MustInclude_WatermarkPlaceholder_And_ClearButton()
|
||||
{
|
||||
// 검색창은 플레이스홀더 워터마크(SearchPlaceholderText)와 입력 내용 원클릭 지우기 버튼(SearchClearButton)을 포함해야 한다.
|
||||
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||
|
||||
var placeholder = doc.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "SearchPlaceholderText");
|
||||
var clearBtn = doc.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "SearchClearButton");
|
||||
|
||||
Assert.NotNull(placeholder);
|
||||
Assert.NotNull(clearBtn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inspector_ActionButtons_MustBind_IsEnabled_To_Selection()
|
||||
{
|
||||
// 파일이 선택되지 않았을 때 무의미한 클릭을 방지하기 위해
|
||||
// 미리보기 하단의 열기(PreviewOpenButton) 및 폴더보기(PreviewRevealButton) 버튼은 IsEnabled 바인딩 또는 제어를 가져야 한다.
|
||||
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||
|
||||
var openBtn = doc.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PreviewOpenButton");
|
||||
var revealBtn = doc.Descendants().FirstOrDefault(e =>
|
||||
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PreviewRevealButton");
|
||||
|
||||
Assert.NotNull(openBtn);
|
||||
Assert.NotNull(revealBtn);
|
||||
|
||||
var openEnabled = openBtn.Attribute("IsEnabled")?.Value;
|
||||
var revealEnabled = revealBtn.Attribute("IsEnabled")?.Value;
|
||||
|
||||
Assert.NotNull(openEnabled);
|
||||
Assert.NotNull(revealEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Window_InputBindings_MustInclude_SelectAll_And_Delete()
|
||||
{
|
||||
// Raycast / Linear 급 생산성을 위해 전체 선택(Ctrl+A) 및 선택 항목 삭제(Delete) 단축키가 Window.InputBindings에 등록되어야 한다.
|
||||
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
|
||||
var doc = XDocument.Parse(File.ReadAllText(file));
|
||||
|
||||
var inputBindings = doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "Window.InputBindings");
|
||||
Assert.NotNull(inputBindings);
|
||||
|
||||
var keyBindings = inputBindings.Elements().Where(e => e.Name.LocalName == "KeyBinding").ToList();
|
||||
bool hasCtrlA = keyBindings.Any(kb => kb.Attribute("Key")?.Value == "A" && kb.Attribute("Modifiers")?.Value == "Ctrl");
|
||||
bool hasDelete = keyBindings.Any(kb => kb.Attribute("Key")?.Value == "Delete");
|
||||
|
||||
Assert.True(hasCtrlA, "Window.InputBindings에 Ctrl+A 단축키가 등록되어야 합니다.");
|
||||
Assert.True(hasDelete, "Window.InputBindings에 Delete 단축키가 등록되어야 합니다.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -416,6 +416,19 @@ public class UnifiedTitleBarTests
|
|||
Assert.Equal("50%", q2.DisplayStateText);
|
||||
Assert.Equal("변환 완료", q3.DisplayStateText);
|
||||
|
||||
var summaryCount = (TextBlock)window.FindName("QueueSummaryCountText");
|
||||
var summarySize = (TextBlock)window.FindName("QueueSummarySizeText");
|
||||
Assert.NotNull(summaryCount);
|
||||
Assert.NotNull(summarySize);
|
||||
Assert.Equal("총 3개 항목", summaryCount.Text);
|
||||
|
||||
var pipeSource = (TextBlock)window.FindName("PipelineSourceText");
|
||||
var pipeTarget = (TextBlock)window.FindName("PipelineTargetText");
|
||||
Assert.NotNull(pipeSource);
|
||||
Assert.NotNull(pipeTarget);
|
||||
Assert.Equal("JPG · 4.2 MB", pipeSource.Text);
|
||||
Assert.Equal("JPG", pipeTarget.Text);
|
||||
|
||||
try
|
||||
{
|
||||
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
|
||||
|
|
@ -583,6 +596,10 @@ public class UnifiedTitleBarTests
|
|||
Assert.Equal(Visibility.Collapsed, pastResultsEmpty.Visibility);
|
||||
Assert.Equal(Visibility.Visible, pastResultsView.Visibility);
|
||||
|
||||
var pastTotalCount = (TextBlock)window.FindName("PastTotalCountText");
|
||||
Assert.NotNull(pastTotalCount);
|
||||
Assert.Equal("2개 파일", pastTotalCount.Text);
|
||||
|
||||
// Render populated state
|
||||
try
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue