1
0
Fork 0

feat: Everything2Everything 양방향 변환 매트릭스 피보팅

EverythingToJpeg(N→JPEG 단방향)에서 Everything2Everything(N×M 매트릭스)으로
앱을 피보팅. 7개 Provider, 200+ 변환 쌍 지원.

리네이밍
- 디렉토리/솔루션/csproj/namespace/AssemblyName/MSIX Identity 일괄 치환
- 임시파일 prefix e2j_ → e2e_, 로고 라벨 E2J → E2E

아키텍처
- ConversionPair(input, output) 신규 + ProviderCapability.SupportedConversions
- IConverterProvider.ConvertAsync에 outputExtension 파라미터 추가
- ProviderRegistry를 Dictionary<(input, output), Provider> 매트릭스로 재작성
  + OutputsForInput / OutputsForFile 쿼리 API
- ConversionEngine: 출력 ext 라우팅, 동일 입출력 자동 skip,
  서브폴더 suffix를 출력 ext에서 자동 도출
- ConvertOptions를 형식별 sub-record로 분리
  (Jpeg/Png/Webp/Avif/Tiff/Pdf*/Html*/Ocr)

Provider 매트릭스
- MagickProvider: PNG/JPEG/WebP/AVIF/BMP/TIFF/GIF/PDF 양방향 + RAW/PSD 디코딩
  (단일이미지→1페이지 PDF, GIF/TIFF→다페이지 PDF)
- HeicProvider: HEIC/HEIF → 이미지 7종
- PdfProvider: PDF → 이미지 6종 (PNG임베드 후 ImageMagick 인코딩)
- HtmlProvider: HTML/HTM → 이미지 + PDF (CDP printToPDF)
- DocxProvider/HwpxProvider: PDF + 이미지 7종 (LibreOffice 활용)
- OcrProvider 신규: 이미지/PDF → TXT/DOCX
  (Windows.Media.Ocr + DocumentFormat.OpenXml, PDF는 페이지별 OCR 후 결합)

UI
- 사이드바 TARGET FORMAT을 동적 ComboBox로 (큐 파일 매트릭스의 교집합만 표시)
- 출력 형식별 색상 배지 + Quality 패널 라벨 동적
- OUTPUT DESTINATION hint도 출력 ext 기반 동적

CLI
- 신규 'to <ext> <files...>' verb (예: to png photo.heic doc.docx)
- help 텍스트 양방향 컨셉으로 갱신

셸 통합
- ContextMenuRegistrar 카스케이드로 재설계
  (ExtendedSubCommandsKey + Everything2Everything.SubMenu.<ext>)
- 입력별 매트릭스에 따라 인기 출력 10종(JPEG/PNG/WebP/PDF/TXT/DOCX/AVIF/GIF/TIFF/BMP)
  동적 노출, 마지막에 '변환…' 옵션
- 메뉴 라벨 'JPEG로 변환' → 'Everything2Everything으로 변환'

기타
- Core.csproj TFM net9.0-windows → net9.0-windows10.0.19041.0 (WinRT API용)
- DocumentFormat.OpenXml 3.1.0 NuGet 추가
- README 양방향 매트릭스 기준 전면 개편
- MSIX appxmanifest Description / IExplorerCommand DLL 라벨 갱신

빌드: 0 errors, 0 warnings
This commit is contained in:
Yun Chan 2026-05-07 13:40:08 +09:00
parent 96861e627d
commit 9b7e5f0d0c
63 changed files with 1788 additions and 733 deletions

View file

@ -0,0 +1,60 @@
<Application x:Class="Everything2Everything.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Everything2Everything.App"
ShutdownMode="OnLastWindowClose">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemesDictionary Theme="Dark"/>
<ui:ControlsDictionary/>
<ResourceDictionary Source="Views/FormatShiftTheme.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- Spacing tokens (Fluent 8pt grid) -->
<Thickness x:Key="SpacingXS">4</Thickness>
<Thickness x:Key="SpacingS">8</Thickness>
<Thickness x:Key="SpacingM">16</Thickness>
<Thickness x:Key="SpacingL">24</Thickness>
<Thickness x:Key="SpacingXL">32</Thickness>
<!-- Typography (Fluent 2 type ramp, Segoe UI Variable) -->
<Style x:Key="TextTitleLarge" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI Variable Display, Segoe UI"/>
<Setter Property="FontSize" Value="28"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style x:Key="TextTitle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI Variable Display, Segoe UI"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style x:Key="TextSubtitle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI Variable Text, Segoe UI"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style x:Key="TextBodyStrong" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI Variable Text, Segoe UI"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style x:Key="TextBody" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI Variable Text, Segoe UI"/>
<Setter Property="FontSize" Value="14"/>
</Style>
<Style x:Key="TextCaption" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI Variable Small, Segoe UI"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}"/>
</Style>
<!-- Status badge brushes -->
<SolidColorBrush x:Key="BadgeReadyBrush" Color="#10B981"/>
<SolidColorBrush x:Key="BadgeWarnBrush" Color="#F59E0B"/>
<SolidColorBrush x:Key="BadgeInfoBrush" Color="#3B82F6"/>
<SolidColorBrush x:Key="BadgeMutedBrush" Color="#6B7280"/>
</ResourceDictionary>
</Application.Resources>
</Application>

View file

@ -0,0 +1,155 @@
using System.Windows;
using Everything2Everything.App.Cli;
using Everything2Everything.App.Views;
using Everything2Everything.Core;
namespace Everything2Everything.App;
public partial class App : Application
{
public ConversionEngine Engine { get; } = Everything2EverythingBootstrap.CreateDefault();
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
WireGlobalExceptionLogging();
var parsed = CliRouter.Parse(e.Args);
switch (parsed.Mode)
{
case CliRouter.Mode.Help:
ConsoleHelper.WriteLine(CliRouter.HelpText());
Environment.Exit(0);
return;
case CliRouter.Mode.Register:
Environment.Exit(CliRouter.RunRegister(register: true));
return;
case CliRouter.Mode.Unregister:
Environment.Exit(CliRouter.RunRegister(register: false));
return;
case CliRouter.Mode.Diagnose:
ShowDiagnoseWindow();
return;
case CliRouter.Mode.Quick:
if (parsed.Files.Count == 0)
{
MessageBox.Show("변환할 파일이 없습니다.", "Everything2Everything",
MessageBoxButton.OK, MessageBoxImage.Information);
Shutdown(1);
return;
}
await RunQuickAsync(parsed.Files, parsed.OutputExtension ?? ".jpg");
return;
case CliRouter.Mode.Dialog:
ShowConvertDialog(parsed.Files);
return;
case CliRouter.Mode.ShowMain:
default:
ShowMainWindow();
return;
}
}
private void ShowMainWindow()
{
var window = new MainWindow();
MainWindow = window;
window.Show();
}
private void ShowConvertDialog(IReadOnlyList<string> files)
{
var window = new Views.MainWindow(files);
MainWindow = window;
window.Show();
}
private void ShowDiagnoseWindow()
{
var window = new DiagnoseWindow(Engine);
MainWindow = window;
window.Show();
}
private async Task RunQuickAsync(IReadOnlyList<string> files, string outputExtension)
{
var logPath = Path.Combine(Path.GetTempPath(), "Everything2Everything_quick.log");
var log = new System.Text.StringBuilder();
log.AppendLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Quick start → {outputExtension}, {files.Count} file(s)");
foreach (var f in files) log.AppendLine($" src: {f}");
var progress = new QuickProgressWindow(files.Count);
progress.Show();
try
{
var options = ConvertOptions.Quick();
var reporter = new Progress<ConvertProgress>(p => progress.Report(p));
var results = await Engine.ConvertManyAsync(files, ".jpg", options, reporter);
foreach (var r in results)
{
log.AppendLine($" [{r.Status}] {Path.GetFileName(r.SourcePath)} → {r.OutputPaths.Count} output(s)");
if (r.Message is { Length: > 0 }) log.AppendLine($" msg: {r.Message}");
if (r.Error is not null) log.AppendLine($" err: {r.Error}");
foreach (var o in r.OutputPaths) log.AppendLine($" out: {o}");
}
progress.Finish(results);
}
catch (Exception ex)
{
log.AppendLine($" EXCEPTION {ex.GetType().Name}: {ex.Message}");
log.AppendLine(ex.ToString());
try { progress.Close(); } catch { }
MessageBox.Show($"변환 중 오류: {ex.Message}\n\n로그: {logPath}", "Everything2Everything",
MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
try { File.WriteAllText(logPath, log.ToString()); } catch { }
}
}
private static void WireGlobalExceptionLogging()
{
var path = Path.Combine(Path.GetTempPath(), "Everything2Everything_unhandled.log");
void Append(string source, Exception? ex)
{
try
{
File.AppendAllText(path,
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {source}\n{ex}\n\n");
}
catch { }
}
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
Append("AppDomain.UnhandledException", e.ExceptionObject as Exception);
Current.DispatcherUnhandledException += (_, e) =>
{
Append("Application.DispatcherUnhandledException", e.Exception);
MessageBox.Show(
"예기치 못한 오류:\n\n" + e.Exception.Message + "\n\n로그: " + path,
"Everything2Everything",
MessageBoxButton.OK, MessageBoxImage.Error);
e.Handled = true;
};
System.Threading.Tasks.TaskScheduler.UnobservedTaskException += (_, e) =>
{
Append("TaskScheduler.UnobservedTaskException", e.Exception);
e.SetObserved();
};
}
}

View file

@ -0,0 +1,10 @@
using System.Windows;
[assembly:ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View file

@ -0,0 +1,139 @@
using Everything2Everything.App.Shell;
using Everything2Everything.Core;
namespace Everything2Everything.App.Cli;
internal static class CliRouter
{
public enum Mode
{
ShowMain,
Quick,
Dialog,
Register,
Unregister,
Diagnose,
Help,
}
public sealed record ParsedArgs(Mode Mode, IReadOnlyList<string> Files, string? OutputExtension = null);
public static ParsedArgs Parse(string[] args)
{
if (args is null || args.Length == 0)
return new ParsedArgs(Mode.ShowMain, Array.Empty<string>());
var verb = args[0].Trim().ToLowerInvariant();
var rest = args.Skip(1).Where(a => !string.IsNullOrWhiteSpace(a)).ToList();
if (verb == "to")
{
if (rest.Count < 2)
return new ParsedArgs(Mode.Help, Array.Empty<string>());
var outputExt = NormalizeExt(rest[0]);
var files = ExpandFiles(rest.Skip(1));
return new ParsedArgs(Mode.Quick, files, outputExt);
}
return verb switch
{
"quick" => new ParsedArgs(Mode.Quick, ExpandFiles(rest), ".jpg"),
"dialog" => new ParsedArgs(Mode.Dialog, ExpandFiles(rest)),
"register" => new ParsedArgs(Mode.Register, Array.Empty<string>()),
"unregister" => new ParsedArgs(Mode.Unregister, Array.Empty<string>()),
"diagnose" or "doctor" => new ParsedArgs(Mode.Diagnose, Array.Empty<string>()),
"help" or "--help" or "-h" or "/?" => new ParsedArgs(Mode.Help, Array.Empty<string>()),
_ when File.Exists(args[0]) => new ParsedArgs(Mode.Dialog, ExpandFiles(args)),
_ => new ParsedArgs(Mode.ShowMain, Array.Empty<string>()),
};
}
private static string NormalizeExt(string ext)
{
var trimmed = ext.Trim().ToLowerInvariant();
return trimmed.StartsWith('.') ? trimmed : "." + trimmed;
}
private static IReadOnlyList<string> ExpandFiles(IEnumerable<string> raw)
{
var list = new List<string>();
foreach (var arg in raw)
{
if (string.IsNullOrWhiteSpace(arg)) continue;
try
{
if (File.Exists(arg)) { list.Add(Path.GetFullPath(arg)); continue; }
if (Directory.Exists(arg))
{
foreach (var f in Directory.EnumerateFiles(arg, "*", SearchOption.TopDirectoryOnly))
list.Add(Path.GetFullPath(f));
}
}
catch { }
}
return list;
}
public static string HelpText()
{
var engine = Everything2EverythingBootstrap.CreateDefault();
var inputs = string.Join(", ",
engine.Providers.Implemented.SelectMany(p => p.Capability.InputExtensions).Distinct().OrderBy(e => e));
var outputs = string.Join(", ",
engine.Providers.Implemented.SelectMany(p => p.Capability.OutputExtensions).Distinct().OrderBy(e => e));
var coming = string.Join(", ",
engine.Providers.ComingSoon.SelectMany(p => p.Capability.InputExtensions).Distinct().OrderBy(e => e));
return $"""
Everything2Everything ( )
:
Everything2Everything.exe to <ext> <...> <ext> (: to png a.jpg b.heic)
Everything2Everything.exe quick <...> ( .jpg)
Everything2Everything.exe dialog <...>
Everything2Everything.exe register ( )
Everything2Everything.exe unregister
Everything2Everything.exe diagnose ·
Everything2Everything.exe
: {inputs}
: {outputs}
: {coming}
""";
}
public static int RunRegister(bool register)
{
var logPath = Path.Combine(Path.GetTempPath(), "Everything2Everything_register.log");
var log = new System.Text.StringBuilder();
log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] RunRegister start, register={register}");
try
{
log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] Creating engine...");
var engine = Everything2EverythingBootstrap.CreateDefault();
log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] Engine OK. Implemented providers: {string.Join(",", engine.Providers.Implemented.Select(p => p.Capability.Id))}");
if (register)
{
log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] Calling Register...");
ContextMenuRegistrar.Register(engine);
}
else
{
ContextMenuRegistrar.Unregister(engine);
}
log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] OK");
File.WriteAllText(logPath, log.ToString());
Console.Out.WriteLine($"등록 완료. 로그: {logPath}");
return 0;
}
catch (Exception ex)
{
log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] EXCEPTION {ex.GetType().Name}: {ex.Message}");
log.AppendLine(ex.ToString());
try { File.WriteAllText(logPath, log.ToString()); } catch { }
Console.Error.WriteLine(ex.Message);
return 1;
}
}
}

View file

@ -0,0 +1,24 @@
using System.Runtime.InteropServices;
namespace Everything2Everything.App.Cli;
internal static class ConsoleHelper
{
private static bool _attached;
public static void WriteLine(string text)
{
EnsureAttached();
Console.Out.WriteLine(text);
Console.Out.Flush();
}
private static void EnsureAttached()
{
if (_attached) return;
_attached = AttachConsole(-1);
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AttachConsole(int dwProcessId);
}

View file

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<AssemblyName>Everything2Everything</AssemblyName>
<RootNamespace>Everything2Everything.App</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<NoWarn>$(NoWarn);NU1901;NU1902;NU1903;NU1904</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Everything2Everything.Core\Everything2Everything.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="WPF-UI" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<Using Include="System.IO" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,5 @@
global using MessageBox = System.Windows.MessageBox;
global using MessageBoxButton = System.Windows.MessageBoxButton;
global using MessageBoxImage = System.Windows.MessageBoxImage;
global using MessageBoxResult = System.Windows.MessageBoxResult;
global using TextBlock = System.Windows.Controls.TextBlock;

View file

@ -0,0 +1,177 @@
using Everything2Everything.Core;
using Everything2Everything.Core.Providers;
using Microsoft.Win32;
namespace Everything2Everything.App.Shell;
internal static class ContextMenuRegistrar
{
private const string MainVerb = "Everything2Everything";
private const string MainLabel = "Everything2Everything으로 변환";
private const string SubmenuKeyPrefix = "Everything2Everything.SubMenu.";
private static readonly (string Ext, string Label, string SortPrefix)[] PopularOutputs =
{
(".jpg", "JPEG (.jpg)", "01"),
(".png", "PNG (.png)", "02"),
(".webp", "WebP (.webp)", "03"),
(".pdf", "PDF (.pdf)", "04"),
(".txt", "텍스트 (.txt) — OCR", "05"),
(".docx", "Word (.docx) — OCR", "06"),
(".avif", "AVIF (.avif)", "07"),
(".gif", "GIF (.gif)", "08"),
(".tif", "TIFF (.tif)", "09"),
(".bmp", "BMP (.bmp)", "10"),
};
public static void Register(ConversionEngine engine)
{
var exe = GetAppExecutablePath();
var icon = exe + ",0";
foreach (var ext in CollectInputExtensions(engine))
{
var outputs = engine.Providers.OutputsForInput(ext)
.Select(o => o.ToLowerInvariant())
.ToHashSet();
var availableOutputs = PopularOutputs
.Where(p => outputs.Contains(p.Ext) && !string.Equals(p.Ext, ext, StringComparison.OrdinalIgnoreCase))
.ToList();
if (availableOutputs.Count == 0) continue;
WriteCascade(ext, exe, icon, availableOutputs);
}
NotifyShell();
}
public static void Unregister(ConversionEngine engine)
{
foreach (var ext in CollectInputExtensions(engine))
{
DeleteVerb(ext, MainVerb);
DeleteVerb(ext, "Everything2Everything.Quick");
DeleteVerb(ext, "Everything2Everything.Dialog");
DeleteSubmenuTree(ext);
}
NotifyShell();
}
private static IEnumerable<string> CollectInputExtensions(ConversionEngine engine)
{
return engine.Providers.Implemented
.Where(p => p.Capability.CanRegisterContextMenu)
.SelectMany(p => p.Capability.InputExtensions)
.Select(e => e.StartsWith('.') ? e : "." + e)
.Select(e => e.ToLowerInvariant())
.Distinct();
}
private static void WriteCascade(
string ext,
string exe,
string icon,
IReadOnlyList<(string Ext, string Label, string SortPrefix)> availableOutputs)
{
var submenuKeyName = SubmenuKeyPrefix + ext.TrimStart('.');
var verbPath = $@"Software\Classes\SystemFileAssociations\{ext}\shell\{MainVerb}";
using (var verbKey = Registry.CurrentUser.CreateSubKey(verbPath, writable: true)
?? throw new InvalidOperationException($"레지스트리 키 생성 실패: {verbPath}"))
{
verbKey.SetValue(null, MainLabel, RegistryValueKind.String);
verbKey.SetValue("MUIVerb", MainLabel, RegistryValueKind.String);
verbKey.SetValue("Icon", icon, RegistryValueKind.String);
verbKey.SetValue("SubCommands", "", RegistryValueKind.String);
verbKey.SetValue("ExtendedSubCommandsKey", submenuKeyName, RegistryValueKind.String);
try { verbKey.DeleteSubKeyTree("command", throwOnMissingSubKey: false); } catch { }
}
var submenuShellPath = $@"Software\Classes\{submenuKeyName}\shell";
using (var existing = Registry.CurrentUser.OpenSubKey(submenuShellPath, writable: true))
{
if (existing is not null)
{
foreach (var name in existing.GetSubKeyNames())
{
try { existing.DeleteSubKeyTree(name, throwOnMissingSubKey: false); } catch { }
}
}
}
foreach (var (outExt, outLabel, sortPrefix) in availableOutputs)
{
var subVerbName = $"{sortPrefix}_{outExt.TrimStart('.')}";
var cliExt = outExt.TrimStart('.');
WriteSubmenuItem(submenuKeyName, subVerbName, outLabel, icon,
$"\"{exe}\" to {cliExt} \"%1\"");
}
WriteSubmenuItem(submenuKeyName, "98_dialog", "변환… (옵션 선택)", icon,
$"\"{exe}\" dialog \"%1\"");
}
private static void WriteSubmenuItem(string submenuKeyName, string verbName, string label, string icon, string command)
{
var path = $@"Software\Classes\{submenuKeyName}\shell\{verbName}";
using var key = Registry.CurrentUser.CreateSubKey(path, writable: true)
?? throw new InvalidOperationException($"서브메뉴 키 생성 실패: {path}");
key.SetValue(null, label, RegistryValueKind.String);
key.SetValue("MUIVerb", label, RegistryValueKind.String);
key.SetValue("Icon", icon, RegistryValueKind.String);
using var commandKey = key.CreateSubKey("command", writable: true)
?? throw new InvalidOperationException("command 하위 키 생성 실패");
commandKey.SetValue(null, command, RegistryValueKind.String);
}
private static void DeleteVerb(string ext, string verb)
{
var parentPath = $@"Software\Classes\SystemFileAssociations\{ext}\shell";
try
{
using var parent = Registry.CurrentUser.OpenSubKey(parentPath, writable: true);
parent?.DeleteSubKeyTree(verb, throwOnMissingSubKey: false);
}
catch
{
}
}
private static void DeleteSubmenuTree(string ext)
{
var submenuKeyName = SubmenuKeyPrefix + ext.TrimStart('.');
var path = $@"Software\Classes\{submenuKeyName}";
try
{
Registry.CurrentUser.DeleteSubKeyTree(path, throwOnMissingSubKey: false);
}
catch
{
}
}
private static string GetAppExecutablePath()
{
var exe = Environment.ProcessPath;
if (!string.IsNullOrEmpty(exe) && File.Exists(exe)) return exe;
return AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar + "Everything2Everything.exe";
}
private static void NotifyShell()
{
try { NativeMethods.SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero); }
catch { }
}
private static class NativeMethods
{
[System.Runtime.InteropServices.DllImport("shell32.dll")]
public static extern void SHChangeNotify(int wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
}
}

View file

@ -0,0 +1,32 @@
<ui:FluentWindow x:Class="Everything2Everything.App.Views.DiagnoseWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="진단"
Width="640" Height="540"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowCornerPreference="Round"
WindowStartupLocation="CenterOwner">
<Grid>
<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">
<StackPanel x:Name="ItemsPanel"/>
</ScrollViewer>
<Border Grid.Row="2" Padding="32,16"
Background="{DynamicResource LayerOnAcrylicFillColorDefaultBrush}"
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"/>
</StackPanel>
</Border>
</Grid>
</ui:FluentWindow>

View file

@ -0,0 +1,118 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Everything2Everything.Core;
using Everything2Everything.Core.Providers;
using Wpf.Ui.Controls;
namespace Everything2Everything.App.Views;
public partial class DiagnoseWindow : FluentWindow
{
private readonly ConversionEngine _engine;
public DiagnoseWindow(ConversionEngine engine)
{
_engine = engine;
InitializeComponent();
Loaded += async (_, _) => await PopulateAsync();
}
private async Task PopulateAsync()
{
ItemsPanel.Children.Clear();
ItemsPanel.Children.Add(BuildSection("환경", new[]
{
("OS", Environment.OSVersion.VersionString),
(".NET", Environment.Version.ToString()),
("실행 경로", Environment.ProcessPath ?? AppContext.BaseDirectory),
}));
foreach (var provider in _engine.Providers.All)
{
var availability = await provider.CheckAvailabilityAsync();
ItemsPanel.Children.Add(BuildProviderCard(provider, availability));
}
}
private static UIElement BuildSection(string title, IEnumerable<(string Key, string Value)> items)
{
var card = new CardControl { Padding = new Thickness(16, 12, 16, 12), Margin = new Thickness(0, 0, 0, 12) };
var stack = new StackPanel();
stack.Children.Add(new TextBlock
{
Text = title,
FontSize = 14,
FontWeight = FontWeights.SemiBold,
Margin = new Thickness(0, 0, 0, 8),
});
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) });
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 };
Grid.SetColumn(valueText, 1);
row.Children.Add(keyText);
row.Children.Add(valueText);
stack.Children.Add(row);
}
card.Content = 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 stack = new StackPanel();
var header = new StackPanel { Orientation = Orientation.Horizontal };
header.Children.Add(new TextBlock
{
Text = provider.Capability.DisplayName,
FontSize = 14,
FontWeight = FontWeights.SemiBold,
});
var (badgeText, brushKey) = (provider.Capability.Status, availability.IsReady) switch
{
(ProviderStatus.ComingSoon, _) => ("개발 중", "BadgeMutedBrush"),
(ProviderStatus.Disabled, _) => ("비활성", "BadgeMutedBrush"),
(_, true) => ("준비됨", "BadgeReadyBrush"),
_ => ("점검 필요", "BadgeWarnBrush"),
};
header.Children.Add(new Border
{
Background = (Brush)Application.Current.FindResource(brushKey),
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
{
Text = $"입력: {string.Join(", ", provider.Capability.InputExtensions)} → 출력: {string.Join(", ", provider.Capability.OutputExtensions)}",
FontSize = 11,
Foreground = (Brush)Application.Current.FindResource("TextFillColorTertiaryBrush"),
Margin = new Thickness(0, 4, 0, 0),
});
if (!availability.IsReady && !string.IsNullOrEmpty(availability.Reason))
{
stack.Children.Add(new TextBlock
{
Text = availability.Reason,
FontSize = 11,
Foreground = (Brush)Application.Current.FindResource("BadgeWarnBrush"),
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 4, 0, 0),
});
}
card.Content = stack;
return card;
}
private async void OnRefreshClick(object sender, RoutedEventArgs e) => await PopulateAsync();
private void OnCloseClick(object sender, RoutedEventArgs e) => Close();
}

View file

@ -0,0 +1,294 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Backgrounds -->
<SolidColorBrush x:Key="FsBgBase" Color="#090A0C"/>
<SolidColorBrush x:Key="FsBgPanel" Color="#131417"/>
<SolidColorBrush x:Key="FsBgSurface" Color="#1C1E22"/>
<SolidColorBrush x:Key="FsBgSurfaceHover" Color="#23252A"/>
<SolidColorBrush x:Key="FsBgInput" Color="#0F1012"/>
<!-- Borders -->
<SolidColorBrush x:Key="FsBorderSubtle" Color="#26282D"/>
<SolidColorBrush x:Key="FsBorderStrong" Color="#3A3D45"/>
<!-- Text -->
<SolidColorBrush x:Key="FsTextPrimary" Color="#ECEEFA"/>
<SolidColorBrush x:Key="FsTextSecondary" Color="#8B909A"/>
<SolidColorBrush x:Key="FsTextTertiary" Color="#5D626C"/>
<!-- Accents -->
<SolidColorBrush x:Key="FsAccentBlue" Color="#3B72FF"/>
<SolidColorBrush x:Key="FsAccentGreen" Color="#10B981"/>
<SolidColorBrush x:Key="FsAccentAmber" Color="#F59E0B"/>
<SolidColorBrush x:Key="FsAccentRed" Color="#EF4444"/>
<!-- Format pills -->
<SolidColorBrush x:Key="FsFmtPdf" Color="#E53E3E"/>
<SolidColorBrush x:Key="FsFmtPng" Color="#805AD5"/>
<SolidColorBrush x:Key="FsFmtHeic" Color="#D69E2E"/>
<SolidColorBrush x:Key="FsFmtJpg" Color="#3182CE"/>
<SolidColorBrush x:Key="FsFmtDocx" Color="#2B6CB0"/>
<SolidColorBrush x:Key="FsFmtHtml" Color="#DD6B20"/>
<SolidColorBrush x:Key="FsFmtRaw" Color="#319795"/>
<SolidColorBrush x:Key="FsFmtGif" Color="#9F7AEA"/>
<SolidColorBrush x:Key="FsFmtTiff" Color="#38B2AC"/>
<SolidColorBrush x:Key="FsFmtWebp" Color="#48BB78"/>
<SolidColorBrush x:Key="FsFmtBmp" Color="#718096"/>
<SolidColorBrush x:Key="FsFmtAvif" Color="#ED64A6"/>
<SolidColorBrush x:Key="FsFmtHwp" Color="#9B2C2C"/>
<SolidColorBrush x:Key="FsFmtOther" Color="#4A5568"/>
<!-- Font families -->
<FontFamily x:Key="FsFontSans">Inter, Segoe UI Variable Text, Segoe UI</FontFamily>
<FontFamily x:Key="FsFontMono">JetBrains Mono, Cascadia Mono, Consolas</FontFamily>
<!-- Typography styles -->
<Style x:Key="FsLabelStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource FsFontSans}"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource FsTextSecondary}"/>
<Setter Property="TextOptions.TextFormattingMode" Value="Display"/>
</Style>
<Style x:Key="FsCaptionStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource FsFontSans}"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="Foreground" Value="{StaticResource FsTextTertiary}"/>
</Style>
<Style x:Key="FsBodyStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource FsFontSans}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="{StaticResource FsTextPrimary}"/>
</Style>
<Style x:Key="FsMonoStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource FsFontMono}"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="{StaticResource FsTextSecondary}"/>
</Style>
<!-- Slider style: 2px track, 14px round thumb, blue fill up to value -->
<Style x:Key="FsSliderStyle" TargetType="Slider">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource FsAccentBlue}"/>
<Setter Property="MinHeight" Value="20"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="IsMoveToPointEnabled" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Slider">
<Grid>
<Border Height="2" CornerRadius="1"
Background="{StaticResource FsBorderStrong}"
VerticalAlignment="Center"/>
<Track x:Name="PART_Track">
<Track.DecreaseRepeatButton>
<RepeatButton Command="Slider.DecreaseLarge">
<RepeatButton.Template>
<ControlTemplate TargetType="RepeatButton">
<Border Height="2" CornerRadius="1"
Background="{StaticResource FsAccentBlue}"
VerticalAlignment="Center"/>
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb>
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Ellipse Width="14" Height="14"
Fill="{StaticResource FsTextPrimary}"/>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="Slider.IncreaseLarge">
<RepeatButton.Template>
<ControlTemplate TargetType="RepeatButton">
<Border Background="Transparent"/>
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.IncreaseRepeatButton>
</Track>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Tab pill style -->
<Style x:Key="FsTabPillStyle" TargetType="ToggleButton">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource FsTextSecondary}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="14,6"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="PART_Bd" CornerRadius="20"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
TextElement.Foreground="{TemplateBinding Foreground}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsAccentBlue}"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsBgSurfaceHover}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsChecked" Value="True"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsAccentBlue}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Segment button style (Skip / Rename / Replace) -->
<Style x:Key="FsSegmentStyle" TargetType="ToggleButton">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource FsTextSecondary}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="PART_Bd" CornerRadius="4" Padding="0,6">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
TextElement.Foreground="{TemplateBinding Foreground}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsBgSurface}"/>
<Setter Property="Foreground" Value="{StaticResource FsTextPrimary}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Primary button (Processing Queue / Run) -->
<Style x:Key="FsPrimaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{StaticResource FsAccentBlue}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="12"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="PART_Bd"
Background="{TemplateBinding Background}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
TextElement.Foreground="{TemplateBinding Foreground}"
TextElement.FontSize="{TemplateBinding FontSize}"
TextElement.FontWeight="{TemplateBinding FontWeight}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="PART_Bd" Property="Opacity" Value="0.5"/>
<Setter Property="Cursor" Value="Arrow"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="PART_Bd" Property="Background" Value="#2D5DDB"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Secondary button -->
<Style x:Key="FsSecondaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{StaticResource FsBgSurface}"/>
<Setter Property="Foreground" Value="{StaticResource FsTextPrimary}"/>
<Setter Property="BorderBrush" Value="{StaticResource FsBorderSubtle}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="12,6"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="PART_Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
TextElement.Foreground="{TemplateBinding Foreground}"
TextElement.FontSize="{TemplateBinding FontSize}"
TextElement.FontWeight="{TemplateBinding FontWeight}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="PART_Bd" Property="Background" Value="{StaticResource FsBgSurfaceHover}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Tab badge -->
<Style x:Key="FsTabBadgeStyle" TargetType="Border">
<Setter Property="CornerRadius" Value="10"/>
<Setter Property="Padding" Value="6,1"/>
<Setter Property="Background" Value="{StaticResource FsBgSurface}"/>
</Style>
<!-- Path input box -->
<Style x:Key="FsPathInputStyle" TargetType="TextBox">
<Setter Property="Background" Value="{StaticResource FsBgInput}"/>
<Setter Property="Foreground" Value="{StaticResource FsTextSecondary}"/>
<Setter Property="BorderBrush" Value="{StaticResource FsBorderSubtle}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="FontFamily" Value="{StaticResource FsFontMono}"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="CaretBrush" Value="{StaticResource FsTextPrimary}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ScrollViewer x:Name="PART_ContentHost"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -0,0 +1,681 @@
<ui:FluentWindow x:Class="Everything2Everything.App.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="FormatShift Utility"
Width="1280" Height="960"
MinWidth="1080" MinHeight="640"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="None"
WindowCornerPreference="Round"
WindowStartupLocation="CenterScreen"
AllowDrop="True"
Drop="OnFilesDropped"
DragOver="OnDragOver"
DragLeave="OnDragLeave"
TextOptions.TextFormattingMode="Display"
UseLayoutRounding="True">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<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="Escape" Command="{Binding CloseCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<KeyBinding Key="F5" Command="{Binding RefreshCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
</Window.InputBindings>
<Grid Background="{StaticResource FsBgBase}">
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Title bar (window controls only) -->
<ui:TitleBar Grid.Row="0" Title="" ShowMaximize="True" ShowMinimize="True"/>
<!-- App body -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="320"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ============== SIDEBAR ============== -->
<Border Grid.Column="0"
Background="{StaticResource FsBgPanel}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="0,0,1,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="56"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Sidebar header -->
<Border Grid.Row="0" BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="0,0,0,1" Padding="24,0">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Path Width="18" Height="18" Stretch="Uniform"
Stroke="{StaticResource FsAccentBlue}" StrokeThickness="2"
Data="M3,3 L21,3 L21,21 L3,21 Z M12,8 L12,16 M8,12 L16,12"/>
<TextBlock Margin="8,0,0,0" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource FsTextPrimary}"
FontFamily="{StaticResource FsFontSans}">
FormatShift
<Run Foreground="{StaticResource FsTextTertiary}" FontWeight="Medium"
Text=" Utility"/>
</TextBlock>
</StackPanel>
</Border>
<!-- Sidebar content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="24,24,24,24">
<StackPanel>
<!-- Target Format -->
<StackPanel Margin="0,0,0,32">
<TextBlock Text="TARGET FORMAT" Style="{StaticResource FsLabelStyle}"/>
<Grid Margin="0,8,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border x:Name="OutputFormatBadge" Grid.Column="0"
Width="36" Height="28" CornerRadius="4"
Background="{StaticResource FsFmtJpg}"
VerticalAlignment="Center">
<TextBlock x:Name="OutputFormatBadgeText"
Text="JPG" Foreground="White" FontSize="9"
FontWeight="SemiBold"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ComboBox x:Name="OutputFormatCombo" Grid.Column="1"
Margin="8,0,0,0" VerticalAlignment="Center"
SelectionChanged="OnOutputFormatChanged"/>
</Grid>
<TextBlock x:Name="OutputFormatHint" Margin="0,6,0,0"
Style="{StaticResource FsCaptionStyle}"
Text="모든 입력에서 변환 가능한 형식이 표시됩니다"/>
</StackPanel>
<!-- Encoding Quality -->
<StackPanel x:Name="QualityPanel" Margin="0,0,0,32">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="QualityLabelText" Text="ENCODING QUALITY" Style="{StaticResource FsLabelStyle}"/>
<Border Grid.Column="1" CornerRadius="4"
Background="{StaticResource FsBgInput}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="1" Padding="6,2">
<TextBlock x:Name="QualityValueText"
FontFamily="{StaticResource FsFontMono}" FontSize="12"
Foreground="{StaticResource FsAccentBlue}"
Text="85%"/>
</Border>
</Grid>
<Slider x:Name="QualitySlider" Margin="0,12,0,0"
Style="{StaticResource FsSliderStyle}"
Minimum="1" Maximum="100" Value="85"
ValueChanged="OnQualityChanged"/>
<Grid Margin="0,4,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Smaller File" Style="{StaticResource FsCaptionStyle}"/>
<TextBlock Grid.Column="1" Text="Higher Quality" Style="{StaticResource FsCaptionStyle}"/>
</Grid>
</StackPanel>
<!-- Output Destination -->
<StackPanel Margin="0,0,0,32">
<TextBlock Text="OUTPUT DESTINATION" Style="{StaticResource FsLabelStyle}"/>
<Grid Margin="0,8,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="OutputPathTextBox"
Style="{StaticResource FsPathInputStyle}"
ToolTip="비워두면 원본 폴더 옆 서브폴더에 저장"/>
<Button Grid.Column="1" Margin="8,0,0,0" Width="36"
Content="…" Style="{StaticResource FsSecondaryButtonStyle}"
Click="OnPickOutputFolderClick"/>
</Grid>
<TextBlock x:Name="OutputDestHint" Margin="0,6,0,0"
Style="{StaticResource FsCaptionStyle}"
Text="비워두면 원본 옆 _jpg 폴더에 저장됩니다"/>
</StackPanel>
<!-- File Conflict Rule -->
<StackPanel Margin="0,0,0,32">
<TextBlock Text="FILE CONFLICT RULE" Style="{StaticResource FsLabelStyle}"/>
<Border Margin="0,8,0,0"
Background="{StaticResource FsBgInput}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="1" CornerRadius="6" Padding="2">
<UniformGrid Rows="1" Columns="3">
<ToggleButton x:Name="ConflictSkipBtn" Content="Skip"
Style="{StaticResource FsSegmentStyle}"
Click="OnConflictSegmentClick"
Tag="Skip"/>
<ToggleButton x:Name="ConflictRenameBtn" Content="Rename"
Style="{StaticResource FsSegmentStyle}"
IsChecked="True"
Click="OnConflictSegmentClick"
Tag="Rename"/>
<ToggleButton x:Name="ConflictReplaceBtn" Content="Replace"
Style="{StaticResource FsSegmentStyle}"
Click="OnConflictSegmentClick"
Tag="Replace"/>
</UniformGrid>
</Border>
</StackPanel>
<!-- Stats Box -->
<Border Background="{StaticResource FsBgInput}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="1" CornerRadius="6" Padding="12">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="PROCESSED TODAY"
FontFamily="{StaticResource FsFontSans}"
FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource FsTextSecondary}"/>
<TextBlock x:Name="ProcessedTodayText"
FontFamily="{StaticResource FsFontMono}"
FontSize="14"
Foreground="{StaticResource FsTextPrimary}"
Text="0" Margin="0,4,0,0"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="EST. SPACE SAVED"
FontFamily="{StaticResource FsFontSans}"
FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource FsTextSecondary}"/>
<TextBlock x:Name="SpaceSavedText"
FontFamily="{StaticResource FsFontMono}"
FontSize="14"
Foreground="{StaticResource FsAccentGreen}"
Text="0 B" Margin="0,4,0,0"/>
</StackPanel>
</Grid>
</Border>
</StackPanel>
</ScrollViewer>
<!-- Sidebar footer -->
<Border Grid.Row="2" BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="0,1,0,0" Padding="24">
<StackPanel>
<TextBlock x:Name="CapabilityStatusText" Margin="0,0,0,12"
Style="{StaticResource FsCaptionStyle}"
TextWrapping="Wrap" Visibility="Collapsed"/>
<Button x:Name="ProcessQueueButton"
Content="Idle — drop files to begin"
Style="{StaticResource FsPrimaryButtonStyle}"
IsEnabled="False"
Click="OnProcessQueueClick"/>
</StackPanel>
</Border>
</Grid>
</Border>
<!-- ============== MAIN ============== -->
<Grid Grid.Column="1" Background="{StaticResource FsBgBase}">
<Grid.RowDefinitions>
<RowDefinition Height="56"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Nav bar -->
<Border Grid.Row="0" BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="0,0,0,1" Padding="24,0">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Tabs -->
<Border HorizontalAlignment="Left"
Background="{StaticResource FsBgPanel}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="1" CornerRadius="20" Padding="4">
<StackPanel Orientation="Horizontal">
<ToggleButton x:Name="TabActiveBtn"
Style="{StaticResource FsTabPillStyle}"
Click="OnTabClick" Tag="Active">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Active Queue" VerticalAlignment="Center"/>
<Border Margin="8,0,0,0" Style="{StaticResource FsTabBadgeStyle}">
<TextBlock x:Name="TabActiveBadge" Text="0"
FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource FsTextTertiary}"/>
</Border>
</StackPanel>
</ToggleButton>
<ToggleButton x:Name="TabPastBtn" Margin="8,0,0,0"
IsChecked="True"
Style="{StaticResource FsTabPillStyle}"
Click="OnTabClick" Tag="Past">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Past Results" VerticalAlignment="Center"/>
<Border Margin="8,0,0,0" Style="{StaticResource FsTabBadgeStyle}">
<TextBlock x:Name="TabPastBadge" Text="0"
FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource FsTextTertiary}"/>
</Border>
</StackPanel>
</ToggleButton>
</StackPanel>
</Border>
<!-- Actions -->
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button Content="Register Menu" Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Click="OnRegisterClick"/>
<Button Content="Diagnose" Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Click="OnDiagnoseClick"/>
<Button Content="Export Log" Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Click="OnExportLogClick"/>
<Button x:Name="ClearAllButton" Content="Clear All"
Style="{StaticResource FsSecondaryButtonStyle}"
Click="OnClearAllClick"/>
</StackPanel>
</Grid>
</Border>
<!-- View content (좌측: 탭 컨텐츠 / 우측: Preview 컬럼) -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="360"/>
<ColumnDefinition Width="380"/>
</Grid.ColumnDefinitions>
<!-- 좌측: Active Queue OR Past Results -->
<Grid Grid.Column="0">
<!-- Active Queue view -->
<Grid x:Name="ActiveQueueView" Visibility="Collapsed">
<Grid x:Name="DropZoneEmpty">
<Border Background="{StaticResource FsBgBase}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
Margin="48">
<Path Width="48" Height="48" Stretch="Uniform"
Stroke="{StaticResource FsAccentBlue}" StrokeThickness="1.5"
Data="M21,15 L21,19 C21,20.1 20.1,21 19,21 L5,21 C3.9,21 3,20.1 3,19 L3,15 M7,10 L12,15 L17,10 M12,15 L12,3"/>
<TextBlock Margin="0,16,0,0" FontSize="16" FontWeight="SemiBold"
Foreground="{StaticResource FsTextPrimary}"
HorizontalAlignment="Center"
Text="Drop files anywhere to queue"/>
<TextBlock Margin="0,4,0,0" Style="{StaticResource FsCaptionStyle}"
HorizontalAlignment="Center"
Text="PNG · JPG · GIF · HEIC · RAW · PDF · DOCX · HTML · HWP/HWPX"/>
</StackPanel>
</Border>
</Grid>
<ScrollViewer x:Name="ActiveQueueScroll" VerticalScrollBarVisibility="Auto"
Padding="24" Visibility="Collapsed">
<ItemsControl x:Name="ActiveQueueList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="0,0,0,8" Padding="12,10" CornerRadius="6"
Background="{StaticResource FsBgPanel}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="1"
Cursor="Hand"
PreviewMouseLeftButtonUp="OnQueueRowClick"
Tag="{Binding}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="40"/>
</Grid.ColumnDefinitions>
<Border Width="32" Height="32" CornerRadius="4"
Background="{Binding FormatBrush}"
VerticalAlignment="Center">
<TextBlock Text="{Binding FormatLabel}"
Foreground="White" FontSize="10"
FontWeight="SemiBold"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<StackPanel Grid.Column="1" Margin="16,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding FileName}"
Style="{StaticResource FsBodyStyle}"
FontWeight="Medium"
TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding MetaLine}"
Style="{StaticResource FsCaptionStyle}"
Margin="0,2,0,0"/>
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding SizeText}"
Style="{StaticResource FsMonoStyle}"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="3" Text="{Binding StateText}"
FontFamily="{StaticResource FsFontMono}"
FontSize="12"
Foreground="{Binding StateBrush}"
VerticalAlignment="Center"/>
<Button Grid.Column="4"
Style="{StaticResource FsSecondaryButtonStyle}"
Background="Transparent"
BorderThickness="0"
Padding="6"
Click="OnRemoveQueueItem"
Tag="{Binding}">
<Path Width="14" Height="14" Stretch="Uniform"
Stroke="{StaticResource FsTextTertiary}"
StrokeThickness="2"
Data="M18,6 L6,18 M6,6 L18,18"/>
</Button>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
<!-- Past Results view (default) -->
<ScrollViewer x:Name="PastResultsView" VerticalScrollBarVisibility="Auto"
Padding="24">
<ItemsControl x:Name="PastResultsList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,40">
<!-- Date header -->
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding DateTitle}"
FontFamily="{StaticResource FsFontSans}"
FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource FsTextPrimary}"/>
<Border Grid.Column="1" CornerRadius="20" Padding="10,4"
Background="#10B98122">
<TextBlock Text="{Binding SessionSavingsText}"
FontFamily="{StaticResource FsFontMono}"
FontSize="12"
Foreground="{StaticResource FsAccentGreen}"/>
</Border>
</Grid>
<Border Height="1" Background="{StaticResource FsBorderSubtle}"
Margin="0,0,0,12"/>
<!-- Rows -->
<ItemsControl ItemsSource="{Binding Entries}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#0DFFFFFF"
BorderThickness="0,0,0,1" Padding="0,12"
Cursor="Hand"
Background="Transparent"
PreviewMouseLeftButtonUp="OnPastRowClick"
Tag="{Binding}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="40"/>
</Grid.ColumnDefinitions>
<Border Width="32" Height="32" CornerRadius="4"
Background="{Binding FormatBrush}"
VerticalAlignment="Center">
<TextBlock Text="{Binding FormatLabel}"
Foreground="White" FontSize="10"
FontWeight="SemiBold"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<StackPanel Grid.Column="1" Margin="16,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding FileName}"
Style="{StaticResource FsBodyStyle}"
FontWeight="Medium"
TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding MetaLine}"
Style="{StaticResource FsCaptionStyle}"
Margin="0,2,0,0"/>
</StackPanel>
<TextBlock Grid.Column="2"
Text="{Binding SizeText}"
Style="{StaticResource FsMonoStyle}"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="3"
Text="{Binding SavingsText}"
FontFamily="{StaticResource FsFontMono}"
FontSize="12"
Foreground="{StaticResource FsAccentGreen}"
VerticalAlignment="Center"/>
<StackPanel Grid.Column="4" Orientation="Horizontal"
VerticalAlignment="Center">
<Path Width="12" Height="12" Stretch="Uniform"
Stroke="{StaticResource FsAccentGreen}"
StrokeThickness="3"
Data="M20,6 L9,17 L4,12"/>
<TextBlock Text="Success" Margin="6,0,0,0"
FontSize="11" FontWeight="Medium"
Foreground="{StaticResource FsAccentGreen}"/>
</StackPanel>
<Button Grid.Column="5"
Style="{StaticResource FsSecondaryButtonStyle}"
Background="Transparent"
BorderThickness="0"
Padding="6"
Click="OnOpenFolderClick"
Tag="{Binding SourcePath}"
ToolTip="원본 폴더 열기">
<Path Width="14" Height="14" Stretch="Uniform"
Stroke="{StaticResource FsTextTertiary}"
StrokeThickness="2"
Data="M22,19 C22,20.1 21.1,21 20,21 L4,21 C2.9,21 2,20.1 2,19 L2,5 C2,3.9 2.9,3 4,3 L9,3 L11,6 L20,6 C21.1,6 22,6.9 22,8 Z"/>
</Button>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
<!-- /좌측 컬럼 끝 -->
<!-- 우측: Preview 컬럼 (항상 보임) -->
<Border Grid.Column="1"
Background="{StaticResource FsBgPanel}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="1,0,0,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 헤더 -->
<Border Grid.Row="0" Padding="20,16,20,12"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Path Width="14" Height="14" Stretch="Uniform"
Stroke="{StaticResource FsTextSecondary}" StrokeThickness="2"
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>
</Border>
<!-- 이미지 영역 -->
<Grid Grid.Row="1" Margin="20,16,20,16" x:Name="PreviewImageArea">
<Border Background="{StaticResource FsBgInput}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="1" CornerRadius="6">
<Grid>
<!-- Empty state -->
<StackPanel x:Name="PreviewEmpty" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
<Path Width="36" Height="36" Stretch="Uniform"
Stroke="{StaticResource FsTextTertiary}" StrokeThickness="1.5"
Data="M1,12 C1,12 5,4 12,4 C19,4 23,12 23,12 C23,12 19,20 12,20 C5,20 1,12 1,12 Z M12,9 A3,3 0 1,1 12,15 A3,3 0 1,1 12,9 Z"/>
<TextBlock Margin="0,12,0,0" FontSize="13" FontWeight="SemiBold"
Foreground="{StaticResource FsTextSecondary}"
HorizontalAlignment="Center"
Text="No selection"/>
<TextBlock Margin="0,4,0,0" Style="{StaticResource FsCaptionStyle}"
HorizontalAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap" MaxWidth="240"
Text="Active Queue 항목을 클릭하면 여기에 미리보기가 표시됩니다."/>
</StackPanel>
<!-- Loading -->
<StackPanel x:Name="PreviewLoading" Visibility="Collapsed"
HorizontalAlignment="Center" VerticalAlignment="Center">
<ProgressBar IsIndeterminate="True" Width="180" Height="3"
Foreground="{StaticResource FsAccentBlue}"
Background="{StaticResource FsBgSurface}"/>
<TextBlock Margin="0,12,0,0" Style="{StaticResource FsCaptionStyle}"
HorizontalAlignment="Center"
Text="미리보기 생성 중…"/>
</StackPanel>
<!-- Image -->
<Image x:Name="PreviewImage" Stretch="Uniform" Margin="16"
Visibility="Collapsed"/>
<!-- Reason -->
<StackPanel x:Name="PreviewReason" Visibility="Collapsed"
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
<Path Width="32" Height="32" Stretch="Uniform"
Stroke="{StaticResource FsAccentAmber}" StrokeThickness="1.5"
Data="M12,2 L22,20 L2,20 Z M12,9 L12,14 M12,17 L12,17.01"/>
<TextBlock x:Name="PreviewReasonText" Margin="0,12,0,0"
Style="{StaticResource FsBodyStyle}"
FontSize="12"
HorizontalAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap" MaxWidth="280"/>
</StackPanel>
</Grid>
</Border>
</Grid>
<!-- 메타 -->
<Border Grid.Row="2" Padding="20,12,20,20"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="0,1,0,0">
<StackPanel>
<TextBlock x:Name="PreviewFileName"
Style="{StaticResource FsBodyStyle}"
FontWeight="SemiBold" TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"
Text="—"/>
<TextBlock x:Name="PreviewFilePath" Margin="0,4,0,0"
FontFamily="{StaticResource FsFontMono}" FontSize="11"
Foreground="{StaticResource FsTextTertiary}"
TextTrimming="CharacterEllipsis" MaxHeight="32"/>
<Border Margin="0,12,0,12" Height="1"
Background="{StaticResource FsBorderSubtle}"/>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Format"
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
<TextBlock x:Name="PreviewFormatText" Grid.Row="0" Grid.Column="1"
FontFamily="{StaticResource FsFontMono}" FontSize="12"
Foreground="{StaticResource FsTextPrimary}" Margin="0,0,0,4"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Size"
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
<TextBlock x:Name="PreviewSizeText" Grid.Row="1" Grid.Column="1"
FontFamily="{StaticResource FsFontMono}" FontSize="12"
Foreground="{StaticResource FsTextPrimary}" Margin="0,0,0,4"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Dimensions"
Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
<TextBlock x:Name="PreviewDimText" Grid.Row="2" Grid.Column="1"
FontFamily="{StaticResource FsFontMono}" FontSize="12"
Foreground="{StaticResource FsTextPrimary}" Text="—"
Margin="0,0,0,4"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Pages"
Style="{StaticResource FsCaptionStyle}"/>
<TextBlock x:Name="PreviewPageText" Grid.Row="3" Grid.Column="1"
FontFamily="{StaticResource FsFontMono}" FontSize="12"
Foreground="{StaticResource FsTextPrimary}" Text="—"/>
</Grid>
<Button Content="Open in Explorer" Margin="0,16,0,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Click="OnPreviewOpenFolder"
HorizontalAlignment="Stretch"/>
</StackPanel>
</Border>
</Grid>
</Border>
<!-- Drop hint overlay (전체 덮음) -->
<Border x:Name="DropHintOverlay" Visibility="Collapsed" Grid.ColumnSpan="2"
Background="#CC090A0C" IsHitTestVisible="False">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<Path Width="64" Height="64" Stretch="Uniform"
Stroke="{StaticResource FsAccentBlue}" StrokeThickness="2"
Data="M21,15 L21,19 C21,20.1 20.1,21 19,21 L5,21 C3.9,21 3,20.1 3,19 L3,15 M7,10 L12,15 L17,10 M12,15 L12,3"/>
<TextBlock Text="Drop to add to queue" Margin="0,16,0,0"
FontSize="18" FontWeight="SemiBold"
Foreground="{StaticResource FsTextPrimary}"
HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</Grid>
</Grid>
</Grid>
</ui:FluentWindow>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
<ui:FluentWindow x:Class="Everything2Everything.App.Views.QuickProgressWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="JPEG로 빠른 변환"
Width="560" Height="220"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowCornerPreference="Round"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Title="JPEG로 빠른 변환"/>
<Grid Grid.Row="1" Margin="32,12,32,24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="변환 중…" Style="{StaticResource TextSubtitle}"/>
<TextBlock x:Name="StatusText" Grid.Row="1" Margin="0,4,0,12"
Style="{StaticResource TextCaption}" TextTrimming="CharacterEllipsis"/>
<ProgressBar x:Name="OverallProgress" Grid.Row="2" Height="6"
Minimum="0" Maximum="1"/>
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
<ui:Button x:Name="OpenFolderButton" Content="결과 폴더 열기"
Icon="{ui:SymbolIcon Folder24}"
Click="OnOpenFolderClick" IsEnabled="False" Margin="0,0,8,0"/>
<ui:Button x:Name="CloseButton" Content="닫기" Click="OnCloseClick"
Appearance="Primary" IsEnabled="False"/>
</StackPanel>
</Grid>
</Grid>
</ui:FluentWindow>

View file

@ -0,0 +1,81 @@
using System.Windows;
using Everything2Everything.Core;
using Wpf.Ui.Controls;
namespace Everything2Everything.App.Views;
public partial class QuickProgressWindow : FluentWindow
{
private readonly int _total;
private string? _firstSuccessOutput;
public QuickProgressWindow(int total)
{
_total = total;
InitializeComponent();
StatusText.Text = $"0 / {_total}";
}
public void Report(ConvertProgress p)
{
if (!CheckAccess()) { Dispatcher.Invoke(() => Report(p)); return; }
var overall = _total == 0 ? 0 : (p.Index + p.FileProgress) / _total;
OverallProgress.Value = Math.Clamp(overall, 0, 1);
StatusText.Text = $"{Math.Min(p.Index + 1, _total)} / {_total} — {Path.GetFileName(p.CurrentPath)}";
}
public void Finish(IReadOnlyList<ConvertResult> results)
{
if (!CheckAccess()) { Dispatcher.Invoke(() => Finish(results)); return; }
var success = results.Count(r => r.Status == ConvertStatus.Success);
var skipped = results.Count(r => r.Status == ConvertStatus.Skipped);
var failed = results.Count(r => r.Status == ConvertStatus.Failed);
var outputs = results.Sum(r => r.OutputPaths.Count);
OverallProgress.Value = 1;
StatusText.Text = $"성공 {success}개 (출력 {outputs}), 건너뜀 {skipped}, 실패 {failed}";
CloseButton.IsEnabled = true;
_firstSuccessOutput = results
.FirstOrDefault(r => r.Status == ConvertStatus.Success)?
.OutputPaths.FirstOrDefault();
OpenFolderButton.IsEnabled = _firstSuccessOutput is not null;
if (failed > 0)
{
var detail = string.Join("\n",
results.Where(r => r.Status == ConvertStatus.Failed)
.Take(5)
.Select(r => $"• {Path.GetFileName(r.SourcePath)}: {r.Message}"));
MessageBox.Show(this, "일부 파일 변환에 실패했습니다.\n\n" + detail,
"Everything2Everything", MessageBoxButton.OK, MessageBoxImage.Warning);
}
else if (failed == 0 && skipped == 0 && _firstSuccessOutput is not null)
{
OpenInExplorer(_firstSuccessOutput);
Close();
}
}
private void OnOpenFolderClick(object sender, RoutedEventArgs e)
{
if (_firstSuccessOutput is not null) OpenInExplorer(_firstSuccessOutput);
}
private void OnCloseClick(object sender, RoutedEventArgs e) => Close();
private static void OpenInExplorer(string path)
{
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"/select,\"{path}\"",
UseShellExecute = true,
});
}
catch { }
}
}

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Everything2Everything.App"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> <!-- Win10/11 -->
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings>
</application>
</assembly>