초기 커밋: Phase 1 — 레지스트리 기반 컨텍스트 메뉴 + 핵심 변환 파이프라인
- Provider 추상화(IConverterProvider + ProviderCapability) — Available/RequiresExternal/ComingSoon 1급 시민 - 변환 구현: Magick(이미지·RAW·GIF·TIFF), HEIC/HEIF(libheif decode→Magick encode), PDF(PDFium), DOCX(Word COM 또는 LibreOffice 자동 감지) - ComingSoon 스텁: HTML, HWP/HWPX (UI 노출, 컨텍스트 메뉴 등록 자동 제외) - WPF + WPF-UI 4.3 Fluent 2 UI: 메인 창(드래그&드롭, Provider 카드), 변환 창(썸네일·옵션·진행률), 빠른 변환 진행 창, 진단 창 - 단일 EXE에 verb 라우팅: quick / dialog / register / unregister / diagnose / help - HKCU SystemFileAssociations 기반 컨텍스트 메뉴 등록 (Win11 "추가 옵션 표시"에 노출) - README, packaging/ Phase 2 placeholder 포함 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
8fa4a613d7
37 changed files with 2727 additions and 0 deletions
59
src/EverythingToJpeg.App/App.xaml
Normal file
59
src/EverythingToJpeg.App/App.xaml
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<Application x:Class="EverythingToJpeg.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:EverythingToJpeg.App"
|
||||
ShutdownMode="OnExplicitShutdown">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemesDictionary Theme="Dark"/>
|
||||
<ui:ControlsDictionary/>
|
||||
</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>
|
||||
100
src/EverythingToJpeg.App/App.xaml.cs
Normal file
100
src/EverythingToJpeg.App/App.xaml.cs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
using System.Windows;
|
||||
using EverythingToJpeg.App.Cli;
|
||||
using EverythingToJpeg.App.Views;
|
||||
using EverythingToJpeg.Core;
|
||||
|
||||
namespace EverythingToJpeg.App;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public ConversionEngine Engine { get; } = EverythingToJpegBootstrap.CreateDefault();
|
||||
|
||||
protected override async void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
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("변환할 파일이 없습니다.", "EverythingToJpeg",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
Shutdown(1);
|
||||
return;
|
||||
}
|
||||
await RunQuickAsync(parsed.Files);
|
||||
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 ConvertWindow(Engine, files);
|
||||
MainWindow = window;
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void ShowDiagnoseWindow()
|
||||
{
|
||||
var window = new DiagnoseWindow(Engine);
|
||||
MainWindow = window;
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private async Task RunQuickAsync(IReadOnlyList<string> files)
|
||||
{
|
||||
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, options, reporter);
|
||||
progress.Finish(results);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"변환 중 오류: {ex.Message}", "EverythingToJpeg",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
progress.Close();
|
||||
Shutdown(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/EverythingToJpeg.App/AssemblyInfo.cs
Normal file
10
src/EverythingToJpeg.App/AssemblyInfo.cs
Normal 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)
|
||||
)]
|
||||
120
src/EverythingToJpeg.App/Cli/CliRouter.cs
Normal file
120
src/EverythingToJpeg.App/Cli/CliRouter.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
using EverythingToJpeg.App.Shell;
|
||||
using EverythingToJpeg.Core;
|
||||
|
||||
namespace EverythingToJpeg.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);
|
||||
|
||||
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();
|
||||
|
||||
return verb switch
|
||||
{
|
||||
"quick" => new ParsedArgs(Mode.Quick, ExpandFiles(rest)),
|
||||
"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 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 = EverythingToJpegBootstrap.CreateDefault();
|
||||
var supported = string.Join(", ",
|
||||
engine.Providers.Implemented.SelectMany(p => p.Capability.Extensions).Distinct().OrderBy(e => e));
|
||||
var coming = string.Join(", ",
|
||||
engine.Providers.ComingSoon.SelectMany(p => p.Capability.Extensions).Distinct().OrderBy(e => e));
|
||||
|
||||
return $"""
|
||||
EverythingToJpeg — 모든 것을 JPEG로
|
||||
|
||||
사용:
|
||||
EverythingToJpeg.exe quick <파일들...> 빠른 변환 (다이얼로그 없이 즉시)
|
||||
EverythingToJpeg.exe dialog <파일들...> 상세 옵션 다이얼로그 표시
|
||||
EverythingToJpeg.exe register 컨텍스트 메뉴 등록 (현재 사용자)
|
||||
EverythingToJpeg.exe unregister 컨텍스트 메뉴 해제
|
||||
EverythingToJpeg.exe diagnose 지원 형식·외부 도구 진단
|
||||
EverythingToJpeg.exe 메인 창 표시
|
||||
|
||||
지원 (지금): {supported}
|
||||
지원 예정 : {coming}
|
||||
""";
|
||||
}
|
||||
|
||||
public static int RunRegister(bool register)
|
||||
{
|
||||
var logPath = Path.Combine(Path.GetTempPath(), "EverythingToJpeg_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 = EverythingToJpegBootstrap.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/EverythingToJpeg.App/Cli/ConsoleHelper.cs
Normal file
24
src/EverythingToJpeg.App/Cli/ConsoleHelper.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace EverythingToJpeg.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);
|
||||
}
|
||||
29
src/EverythingToJpeg.App/EverythingToJpeg.App.csproj
Normal file
29
src/EverythingToJpeg.App/EverythingToJpeg.App.csproj
Normal 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>EverythingToJpeg</AssemblyName>
|
||||
<RootNamespace>EverythingToJpeg.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="..\EverythingToJpeg.Core\EverythingToJpeg.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="WPF-UI" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="System.IO" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
5
src/EverythingToJpeg.App/GlobalUsings.cs
Normal file
5
src/EverythingToJpeg.App/GlobalUsings.cs
Normal 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;
|
||||
96
src/EverythingToJpeg.App/Shell/ContextMenuRegistrar.cs
Normal file
96
src/EverythingToJpeg.App/Shell/ContextMenuRegistrar.cs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
using EverythingToJpeg.Core;
|
||||
using EverythingToJpeg.Core.Providers;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace EverythingToJpeg.App.Shell;
|
||||
|
||||
internal static class ContextMenuRegistrar
|
||||
{
|
||||
private const string QuickVerb = "EverythingToJpeg.Quick";
|
||||
private const string DialogVerb = "EverythingToJpeg.Dialog";
|
||||
|
||||
private const string QuickLabel = "JPEG로 빠른 변환";
|
||||
private const string DialogLabel = "JPEG로 변환…";
|
||||
|
||||
public static void Register(ConversionEngine engine)
|
||||
{
|
||||
var exe = GetAppExecutablePath();
|
||||
var icon = exe + ",0";
|
||||
|
||||
foreach (var ext in CollectExtensions(engine))
|
||||
{
|
||||
WriteVerb(ext, QuickVerb, QuickLabel, icon, $"\"{exe}\" quick \"%1\"");
|
||||
WriteVerb(ext, DialogVerb, DialogLabel, icon, $"\"{exe}\" dialog \"%1\"");
|
||||
}
|
||||
|
||||
NotifyShell();
|
||||
}
|
||||
|
||||
public static void Unregister(ConversionEngine engine)
|
||||
{
|
||||
foreach (var ext in CollectExtensions(engine))
|
||||
{
|
||||
DeleteVerb(ext, QuickVerb);
|
||||
DeleteVerb(ext, DialogVerb);
|
||||
}
|
||||
NotifyShell();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> CollectExtensions(ConversionEngine engine)
|
||||
{
|
||||
return engine.Providers.Implemented
|
||||
.Where(p => p.Capability.CanRegisterContextMenu)
|
||||
.SelectMany(p => p.Capability.Extensions)
|
||||
.Select(e => e.StartsWith('.') ? e : "." + e)
|
||||
.Select(e => e.ToLowerInvariant())
|
||||
.Distinct();
|
||||
}
|
||||
|
||||
private static void WriteVerb(string ext, string verb, string label, string icon, string command)
|
||||
{
|
||||
var keyPath = $@"Software\Classes\SystemFileAssociations\{ext}\shell\{verb}";
|
||||
using var verbKey = Registry.CurrentUser.CreateSubKey(keyPath, writable: true)
|
||||
?? throw new InvalidOperationException($"레지스트리 키 생성 실패: {keyPath}");
|
||||
|
||||
verbKey.SetValue(null, label, RegistryValueKind.String);
|
||||
verbKey.SetValue("Icon", icon, RegistryValueKind.String);
|
||||
verbKey.SetValue("MUIVerb", label, RegistryValueKind.String);
|
||||
|
||||
using var commandKey = verbKey.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 string GetAppExecutablePath()
|
||||
{
|
||||
var exe = Environment.ProcessPath;
|
||||
if (!string.IsNullOrEmpty(exe) && File.Exists(exe)) return exe;
|
||||
return AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar)
|
||||
+ Path.DirectorySeparatorChar + "EverythingToJpeg.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);
|
||||
}
|
||||
}
|
||||
141
src/EverythingToJpeg.App/Views/ConvertWindow.xaml
Normal file
141
src/EverythingToJpeg.App/Views/ConvertWindow.xaml
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<ui:FluentWindow x:Class="EverythingToJpeg.App.Views.ConvertWindow"
|
||||
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="940" Height="700"
|
||||
MinWidth="720" MinHeight="540"
|
||||
ExtendsContentIntoTitleBar="True"
|
||||
WindowBackdropType="Mica"
|
||||
WindowCornerPreference="Round"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
AllowDrop="True"
|
||||
Drop="OnFilesDropped"
|
||||
DragOver="OnDragOver">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:TitleBar Grid.Row="0" Title="JPEG로 변환"/>
|
||||
|
||||
<Grid Grid.Row="1" Margin="32,8,32,16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MinWidth="320"/>
|
||||
<ColumnDefinition Width="320"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- File list panel -->
|
||||
<ui:Card Grid.Column="0" Padding="0" Margin="0,0,16,0">
|
||||
<DockPanel>
|
||||
<Grid DockPanel.Dock="Top" Margin="20,16,20,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel>
|
||||
<TextBlock Text="대상 파일" Style="{StaticResource TextSubtitle}"/>
|
||||
<TextBlock x:Name="FilesSummaryText" Style="{StaticResource TextCaption}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<ui:Button Content="추가" Icon="{ui:SymbolIcon Add24}"
|
||||
Click="OnAddFilesClick" Margin="0,0,8,0"/>
|
||||
<ui:Button Content="비우기" Icon="{ui:SymbolIcon Delete24}"
|
||||
Click="OnClearFilesClick"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="20,0,20,16">
|
||||
<ItemsControl x:Name="FilesList"/>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</ui:Card>
|
||||
|
||||
<!-- Options panel -->
|
||||
<ui:Card Grid.Column="1" Padding="0">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="20,16,20,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="옵션" Style="{StaticResource TextSubtitle}" Margin="0,0,0,12"/>
|
||||
|
||||
<TextBlock Text="JPEG 품질" Style="{StaticResource TextBodyStrong}" Margin="0,0,0,4"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="48"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Slider x:Name="QualitySlider" Minimum="50" Maximum="100" Value="92"
|
||||
SmallChange="1" LargeChange="5" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding ElementName=QualitySlider, Path=Value, StringFormat={}{0:0}}"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Right"
|
||||
FontWeight="SemiBold"/>
|
||||
</Grid>
|
||||
<TextBlock Style="{StaticResource TextCaption}"
|
||||
Text="92 권장. 90+에서 시각적으로 거의 무손실."/>
|
||||
|
||||
<TextBlock Text="출력 위치" Style="{StaticResource TextBodyStrong}" Margin="0,16,0,4"/>
|
||||
<ComboBox x:Name="OutputModeCombo" SelectedIndex="0">
|
||||
<ComboBoxItem Content="원본 폴더 안에 _jpeg 하위 폴더"/>
|
||||
<ComboBoxItem Content="원본과 같은 폴더"/>
|
||||
<ComboBoxItem Content="사용자 지정 폴더…"/>
|
||||
</ComboBox>
|
||||
<Grid Margin="0,8,0,0" x:Name="CustomFolderRow" Visibility="Collapsed">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox x:Name="CustomFolderTextBox" />
|
||||
<ui:Button Grid.Column="1" Content="찾아보기" Margin="6,0,0,0"
|
||||
Click="OnBrowseClick"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="이름 충돌" Style="{StaticResource TextBodyStrong}" Margin="0,16,0,4"/>
|
||||
<ComboBox x:Name="CollisionCombo" SelectedIndex="0">
|
||||
<ComboBoxItem Content="번호 붙여 새 파일"/>
|
||||
<ComboBoxItem Content="덮어쓰기"/>
|
||||
<ComboBoxItem Content="건너뛰기"/>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Text="긴 변 최대 픽셀 (선택)" Style="{StaticResource TextBodyStrong}" Margin="0,16,0,4"/>
|
||||
<TextBox x:Name="MaxLongEdgeTextBox" ToolTip="비워 두면 원본 크기 유지"/>
|
||||
|
||||
<TextBlock Text="PDF 렌더 DPI" Style="{StaticResource TextBodyStrong}" Margin="0,16,0,4"/>
|
||||
<Slider x:Name="DpiSlider" Minimum="72" Maximum="450" Value="200"/>
|
||||
<TextBlock Style="{StaticResource TextCaption}"
|
||||
Text="{Binding ElementName=DpiSlider, Path=Value, StringFormat=현재 {0:0} DPI}"/>
|
||||
|
||||
<CheckBox x:Name="FlattenCheckBox" Content="투명 영역을 흰색으로 채우기"
|
||||
IsChecked="True" Margin="0,16,0,0"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ui:Card>
|
||||
</Grid>
|
||||
|
||||
<!-- Bottom action bar -->
|
||||
<Border Grid.Row="2" Background="{DynamicResource LayerOnAcrylicFillColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0"
|
||||
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
||||
Padding="32,16">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<ProgressBar x:Name="OverallProgress" Height="6" Minimum="0" Maximum="1"
|
||||
IsIndeterminate="False"/>
|
||||
<TextBlock x:Name="ProgressStatusText" Style="{StaticResource TextCaption}"
|
||||
Margin="0,6,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Margin="20,0,0,0">
|
||||
<ui:Button x:Name="CancelButton" Content="닫기" Click="OnCancelClick"
|
||||
Margin="0,0,8,0" Padding="20,6"/>
|
||||
<ui:Button x:Name="ConvertButton" Content="변환 시작"
|
||||
Icon="{ui:SymbolIcon ArrowRight24}"
|
||||
Appearance="Primary" Click="OnConvertClick"
|
||||
Padding="24,6"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</ui:FluentWindow>
|
||||
360
src/EverythingToJpeg.App/Views/ConvertWindow.xaml.cs
Normal file
360
src/EverythingToJpeg.App/Views/ConvertWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using EverythingToJpeg.Core;
|
||||
using EverythingToJpeg.Core.Providers;
|
||||
using Wpf.Ui.Controls;
|
||||
|
||||
namespace EverythingToJpeg.App.Views;
|
||||
|
||||
public partial class ConvertWindow : FluentWindow
|
||||
{
|
||||
private readonly ConversionEngine _engine;
|
||||
private readonly ObservableCollection<FileEntry> _entries = new();
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
public ConvertWindow(ConversionEngine engine, IReadOnlyList<string> initialFiles)
|
||||
{
|
||||
_engine = engine;
|
||||
InitializeComponent();
|
||||
|
||||
FilesList.ItemsSource = _entries;
|
||||
FilesList.ItemTemplate = (DataTemplate)CreateFileEntryTemplate();
|
||||
|
||||
AddFiles(initialFiles);
|
||||
|
||||
OutputModeCombo.SelectionChanged += (_, _) =>
|
||||
CustomFolderRow.Visibility = OutputModeCombo.SelectedIndex == 2
|
||||
? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void AddFiles(IEnumerable<string> paths)
|
||||
{
|
||||
var existing = new HashSet<string>(_entries.Select(e => e.Path), StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var p in paths)
|
||||
{
|
||||
if (!File.Exists(p)) continue;
|
||||
if (existing.Contains(p)) continue;
|
||||
|
||||
var entry = new FileEntry(p, _engine);
|
||||
_entries.Add(entry);
|
||||
_ = entry.LoadThumbnailAsync();
|
||||
}
|
||||
UpdateSummary();
|
||||
}
|
||||
|
||||
private void UpdateSummary()
|
||||
{
|
||||
FilesSummaryText.Text = _entries.Count == 0
|
||||
? "비어 있음 — 파일을 끌어다 놓거나 추가하세요"
|
||||
: $"{_entries.Count}개 파일";
|
||||
}
|
||||
|
||||
private void OnDragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop)
|
||||
? DragDropEffects.Copy : DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnFilesDropped(object sender, DragEventArgs e)
|
||||
{
|
||||
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
|
||||
if (e.Data.GetData(DataFormats.FileDrop) is not string[] paths) return;
|
||||
AddFiles(ExpandPaths(paths));
|
||||
}
|
||||
|
||||
private void OnAddFilesClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Multiselect = true,
|
||||
Title = "추가할 파일 선택",
|
||||
Filter = "지원 파일|*.png;*.jpg;*.jpeg;*.gif;*.bmp;*.tif;*.tiff;*.webp;*.avif;*.heic;*.heif;*.psd;*.dng;*.nef;*.cr2;*.cr3;*.arw;*.raf;*.orf;*.rw2;*.srw;*.pef;*.pdf;*.docx;*.doc|모든 파일|*.*",
|
||||
};
|
||||
if (dlg.ShowDialog(this) == true) AddFiles(dlg.FileNames);
|
||||
}
|
||||
|
||||
private void OnClearFilesClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_entries.Clear();
|
||||
UpdateSummary();
|
||||
}
|
||||
|
||||
private void OnRemoveEntry(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement fe && fe.DataContext is FileEntry entry)
|
||||
{
|
||||
_entries.Remove(entry);
|
||||
UpdateSummary();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBrowseClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFolderDialog { Title = "출력 폴더 선택" };
|
||||
if (dlg.ShowDialog(this) == true)
|
||||
CustomFolderTextBox.Text = dlg.FolderName;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ExpandPaths(IEnumerable<string> paths)
|
||||
{
|
||||
foreach (var p in paths)
|
||||
{
|
||||
if (File.Exists(p)) yield return p;
|
||||
else if (Directory.Exists(p))
|
||||
{
|
||||
foreach (var f in Directory.EnumerateFiles(p, "*", SearchOption.TopDirectoryOnly))
|
||||
yield return f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnConvertClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_entries.Count == 0)
|
||||
{
|
||||
ShowInfo("변환할 파일이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
ConvertButton.IsEnabled = false;
|
||||
CancelButton.Content = "취소";
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
var options = BuildOptions();
|
||||
var reporter = new Progress<ConvertProgress>(p =>
|
||||
{
|
||||
var overall = p.Total == 0 ? 0 : (p.Index + p.FileProgress) / p.Total;
|
||||
OverallProgress.Value = Math.Clamp(overall, 0, 1);
|
||||
ProgressStatusText.Text = $"{Math.Min(p.Index + 1, p.Total)} / {p.Total} — {Path.GetFileName(p.CurrentPath)}";
|
||||
UpdateEntryProgress(p);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var sources = _entries.Select(en => en.Path).ToList();
|
||||
var results = await _engine.ConvertManyAsync(sources, options, reporter, _cts.Token);
|
||||
ApplyResults(results);
|
||||
ProgressStatusText.Text = SummarizeResults(results);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
ProgressStatusText.Text = "변환이 취소되었습니다.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ProgressStatusText.Text = "오류: " + ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ConvertButton.IsEnabled = true;
|
||||
CancelButton.Content = "닫기";
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateEntryProgress(ConvertProgress p)
|
||||
{
|
||||
if (p.Index >= _entries.Count) return;
|
||||
for (var i = 0; i < _entries.Count; i++)
|
||||
{
|
||||
if (i < p.Index) _entries[i].State = "완료";
|
||||
else if (i == p.Index) _entries[i].State = "변환 중…";
|
||||
else _entries[i].State = "대기";
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyResults(IReadOnlyList<ConvertResult> results)
|
||||
{
|
||||
foreach (var result in results)
|
||||
{
|
||||
var entry = _entries.FirstOrDefault(e => string.Equals(e.Path, result.SourcePath, StringComparison.OrdinalIgnoreCase));
|
||||
if (entry is null) continue;
|
||||
entry.State = result.Status switch
|
||||
{
|
||||
ConvertStatus.Success => $"성공 ({result.OutputPaths.Count}개)",
|
||||
ConvertStatus.Skipped => "건너뜀",
|
||||
ConvertStatus.Failed => "실패: " + result.Message,
|
||||
_ => entry.State,
|
||||
};
|
||||
entry.IsFailed = result.Status == ConvertStatus.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
private static string SummarizeResults(IReadOnlyList<ConvertResult> results)
|
||||
{
|
||||
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);
|
||||
return $"성공 {success}개 (출력 {outputs}), 건너뜀 {skipped}, 실패 {failed}";
|
||||
}
|
||||
|
||||
private void OnCancelClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_cts is { } cts) { cts.Cancel(); return; }
|
||||
Close();
|
||||
}
|
||||
|
||||
private ConvertOptions BuildOptions()
|
||||
{
|
||||
var opts = new ConvertOptions
|
||||
{
|
||||
Quality = (int)QualitySlider.Value,
|
||||
PdfDpi = (int)DpiSlider.Value,
|
||||
FlattenTransparency = FlattenCheckBox.IsChecked == true,
|
||||
};
|
||||
opts.OutputLocation = OutputModeCombo.SelectedIndex switch
|
||||
{
|
||||
1 => OutputLocation.SameFolderAsSource,
|
||||
2 => OutputLocation.Custom,
|
||||
_ => OutputLocation.SubfolderBesideSource,
|
||||
};
|
||||
if (opts.OutputLocation == OutputLocation.Custom)
|
||||
opts.CustomOutputDirectory = CustomFolderTextBox.Text;
|
||||
|
||||
opts.OnCollision = CollisionCombo.SelectedIndex switch
|
||||
{
|
||||
1 => NameCollision.Overwrite,
|
||||
2 => NameCollision.Skip,
|
||||
_ => NameCollision.AppendNumber,
|
||||
};
|
||||
|
||||
if (int.TryParse(MaxLongEdgeTextBox.Text, out var maxEdge) && maxEdge > 0)
|
||||
opts.MaxLongEdgePixels = maxEdge;
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
private void ShowInfo(string message)
|
||||
=> MessageBox.Show(this, message, "EverythingToJpeg",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
|
||||
private object CreateFileEntryTemplate()
|
||||
{
|
||||
const string xaml = """
|
||||
<DataTemplate 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">
|
||||
<Border Margin="0,0,0,6" Padding="12,8" CornerRadius="6"
|
||||
Background="{DynamicResource SubtleFillColorTransparentBrush}"
|
||||
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
||||
BorderThickness="1">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="44"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Width="40" Height="40" CornerRadius="4"
|
||||
Background="{DynamicResource ControlFillColorDefaultBrush}">
|
||||
<Image Source="{Binding Thumbnail}" Stretch="UniformToFill"/>
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" Margin="12,0,8,0" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding FileName}" FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding SubText}" FontSize="11"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding State}" FontSize="11" Margin="0,2,0,0">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsFailed}" Value="True">
|
||||
<Setter Property="Foreground" Value="#F87171"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<ui:Button Grid.Column="2" Icon="{ui:SymbolIcon Dismiss20}"
|
||||
Appearance="Transparent" Click="OnRemoveEntry"
|
||||
ToolTip="목록에서 제거"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
""";
|
||||
return System.Windows.Markup.XamlReader.Parse(xaml);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FileEntry : System.ComponentModel.INotifyPropertyChanged
|
||||
{
|
||||
private readonly ConversionEngine _engine;
|
||||
private string _state = "대기";
|
||||
private bool _isFailed;
|
||||
private ImageSource? _thumbnail;
|
||||
|
||||
public FileEntry(string path, ConversionEngine engine)
|
||||
{
|
||||
Path = path;
|
||||
_engine = engine;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
public string FileName => System.IO.Path.GetFileName(Path);
|
||||
|
||||
public string SubText
|
||||
{
|
||||
get
|
||||
{
|
||||
var ext = System.IO.Path.GetExtension(Path).ToLowerInvariant();
|
||||
string handler;
|
||||
if (_engine.Providers.TryGetForFile(Path, out var provider) && provider is not null)
|
||||
handler = provider.Capability.DisplayName;
|
||||
else
|
||||
handler = "지원되지 않음";
|
||||
try
|
||||
{
|
||||
var size = new FileInfo(Path).Length;
|
||||
return $"{ext} · {handler} · {FormatBytes(size)}";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return $"{ext} · {handler}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string State { get => _state; set { _state = value; Raise(nameof(State)); } }
|
||||
public bool IsFailed { get => _isFailed; set { _isFailed = value; Raise(nameof(IsFailed)); } }
|
||||
public ImageSource? Thumbnail { get => _thumbnail; set { _thumbnail = value; Raise(nameof(Thumbnail)); } }
|
||||
|
||||
public Task LoadThumbnailAsync() => Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var ext = System.IO.Path.GetExtension(Path).ToLowerInvariant();
|
||||
if (ext is ".png" or ".jpg" or ".jpeg" or ".bmp" or ".gif")
|
||||
{
|
||||
var bmp = new BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.UriSource = new Uri(Path);
|
||||
bmp.DecodePixelWidth = 80;
|
||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bmp.EndInit();
|
||||
bmp.Freeze();
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() => Thumbnail = bmp);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
|
||||
private static string FormatBytes(long bytes)
|
||||
{
|
||||
string[] units = { "B", "KB", "MB", "GB" };
|
||||
double size = bytes;
|
||||
var unit = 0;
|
||||
while (size >= 1024 && unit < units.Length - 1) { size /= 1024; unit++; }
|
||||
return $"{size:0.#} {units[unit]}";
|
||||
}
|
||||
|
||||
public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;
|
||||
private void Raise(string n) => PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(n));
|
||||
}
|
||||
32
src/EverythingToJpeg.App/Views/DiagnoseWindow.xaml
Normal file
32
src/EverythingToJpeg.App/Views/DiagnoseWindow.xaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<ui:FluentWindow x:Class="EverythingToJpeg.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>
|
||||
118
src/EverythingToJpeg.App/Views/DiagnoseWindow.xaml.cs
Normal file
118
src/EverythingToJpeg.App/Views/DiagnoseWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using EverythingToJpeg.Core;
|
||||
using EverythingToJpeg.Core.Providers;
|
||||
using Wpf.Ui.Controls;
|
||||
|
||||
namespace EverythingToJpeg.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.Extensions)}",
|
||||
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();
|
||||
}
|
||||
111
src/EverythingToJpeg.App/Views/MainWindow.xaml
Normal file
111
src/EverythingToJpeg.App/Views/MainWindow.xaml
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<ui:FluentWindow x:Class="EverythingToJpeg.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="EverythingToJpeg"
|
||||
Width="880" Height="640"
|
||||
MinWidth="640" MinHeight="480"
|
||||
ExtendsContentIntoTitleBar="True"
|
||||
WindowBackdropType="Mica"
|
||||
WindowCornerPreference="Round"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
AllowDrop="True"
|
||||
Drop="OnFilesDropped"
|
||||
DragOver="OnDragOver"
|
||||
DragLeave="OnDragLeave">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:TitleBar Grid.Row="0" Title="EverythingToJpeg">
|
||||
<ui:TitleBar.Icon>
|
||||
<ui:SymbolIcon Symbol="ImageMultiple24"/>
|
||||
</ui:TitleBar.Icon>
|
||||
</ui:TitleBar>
|
||||
|
||||
<Grid Grid.Row="1" Margin="32,16,32,32">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Hero -->
|
||||
<StackPanel Grid.Row="0" Margin="0,0,0,24">
|
||||
<TextBlock Text="모든 것을 JPEG로." Style="{StaticResource TextTitleLarge}"/>
|
||||
<TextBlock Margin="0,4,0,0" Style="{StaticResource TextBody}"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}">
|
||||
파일을 우클릭하면 끝. PNG · GIF · HEIC · RAW · PDF · DOCX 가 모두 한 번에 변환됩니다.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Drop zone -->
|
||||
<Border x:Name="DropZone" Grid.Row="1"
|
||||
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
||||
BorderThickness="2"
|
||||
Background="{DynamicResource ControlFillColorDefaultBrush}"
|
||||
CornerRadius="12"
|
||||
Padding="32,24"
|
||||
Margin="0,0,0,20">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsDraggingOver, RelativeSource={RelativeSource AncestorType=Window}}" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemFillColorAttentionBrush}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<ui:SymbolIcon Symbol="ImageArrowCounterclockwise24" FontSize="36"
|
||||
Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="여기에 파일이나 폴더를 끌어다 놓으세요"
|
||||
Style="{StaticResource TextSubtitle}"
|
||||
HorizontalAlignment="Center" Margin="0,12,0,4"/>
|
||||
<TextBlock Style="{StaticResource TextCaption}" HorizontalAlignment="Center">
|
||||
또는
|
||||
<Hyperlink Click="OnPickFilesClick">파일 선택</Hyperlink>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Provider list -->
|
||||
<ui:Card Grid.Row="2" Padding="0">
|
||||
<DockPanel>
|
||||
<Grid DockPanel.Dock="Top" Margin="20,16,20,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="지원 형식" Style="{StaticResource TextSubtitle}"/>
|
||||
<TextBlock Grid.Column="1" Style="{StaticResource TextCaption}"
|
||||
VerticalAlignment="Center">
|
||||
준비된 형식만 우클릭 메뉴에 등록됩니다
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="20,0,20,16">
|
||||
<ItemsControl x:Name="ProvidersList"/>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</ui:Card>
|
||||
|
||||
<!-- Action bar -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal"
|
||||
HorizontalAlignment="Right" Margin="0,20,0,0">
|
||||
<ui:Button Content="진단" Icon="{ui:SymbolIcon Stethoscope24}"
|
||||
Click="OnDiagnoseClick" Margin="0,0,8,0"/>
|
||||
<ui:Button x:Name="UnregisterButton" Content="컨텍스트 메뉴 해제"
|
||||
Icon="{ui:SymbolIcon DismissCircle24}"
|
||||
Click="OnUnregisterClick" Margin="0,0,8,0"/>
|
||||
<ui:Button x:Name="RegisterButton" Content="컨텍스트 메뉴 등록"
|
||||
Icon="{ui:SymbolIcon CheckmarkCircle24}"
|
||||
Appearance="Primary" Click="OnRegisterClick"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ui:FluentWindow>
|
||||
273
src/EverythingToJpeg.App/Views/MainWindow.xaml.cs
Normal file
273
src/EverythingToJpeg.App/Views/MainWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
using EverythingToJpeg.App.Shell;
|
||||
using EverythingToJpeg.Core.Providers;
|
||||
using Wpf.Ui.Controls;
|
||||
|
||||
namespace EverythingToJpeg.App.Views;
|
||||
|
||||
public partial class MainWindow : FluentWindow, INotifyPropertyChanged
|
||||
{
|
||||
private bool _isDraggingOver;
|
||||
public bool IsDraggingOver
|
||||
{
|
||||
get => _isDraggingOver;
|
||||
set { _isDraggingOver = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = this;
|
||||
Loaded += async (_, _) => await PopulateAsync();
|
||||
}
|
||||
|
||||
private async Task PopulateAsync()
|
||||
{
|
||||
var engine = ((App)Application.Current).Engine;
|
||||
ProvidersList.Items.Clear();
|
||||
foreach (var provider in engine.Providers.All)
|
||||
{
|
||||
var availability = await provider.CheckAvailabilityAsync();
|
||||
ProvidersList.Items.Add(BuildProviderRow(provider.Capability, availability));
|
||||
}
|
||||
}
|
||||
|
||||
private static UIElement BuildProviderRow(ProviderCapability cap, ProviderAvailability availability)
|
||||
{
|
||||
var (badge, badgeKey) = cap.Status switch
|
||||
{
|
||||
ProviderStatus.Available => availability.IsReady
|
||||
? ("준비됨", "BadgeReadyBrush")
|
||||
: ("점검 필요", "BadgeWarnBrush"),
|
||||
ProviderStatus.Preview => ("프리뷰", "BadgeInfoBrush"),
|
||||
ProviderStatus.RequiresExternal => availability.IsReady
|
||||
? ("외부 도구 감지됨", "BadgeReadyBrush")
|
||||
: ("외부 도구 필요", "BadgeWarnBrush"),
|
||||
ProviderStatus.ComingSoon => ("개발 중", "BadgeMutedBrush"),
|
||||
_ => ("비활성", "BadgeMutedBrush"),
|
||||
};
|
||||
|
||||
var card = new CardControl
|
||||
{
|
||||
Padding = new Thickness(16, 12, 16, 12),
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
};
|
||||
|
||||
var stack = new StackPanel();
|
||||
|
||||
var headerStack = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
headerStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = cap.DisplayName,
|
||||
FontFamily = new FontFamily("Segoe UI Variable Text, Segoe UI"),
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
headerStack.Children.Add(new Border
|
||||
{
|
||||
Background = (Brush)Application.Current.FindResource(badgeKey),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(8, 2, 8, 2),
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
Child = new TextBlock { Text = badge, FontSize = 11, Foreground = Brushes.White },
|
||||
});
|
||||
stack.Children.Add(headerStack);
|
||||
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = cap.Summary,
|
||||
FontFamily = new FontFamily("Segoe UI Variable Text, Segoe UI"),
|
||||
FontSize = 12,
|
||||
Foreground = (Brush)Application.Current.FindResource("TextFillColorSecondaryBrush"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 4, 0, 0),
|
||||
});
|
||||
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "확장자: " + string.Join(", ", cap.Extensions),
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
if (cap.ExternalDependencies.Count > 0)
|
||||
{
|
||||
foreach (var dep in cap.ExternalDependencies)
|
||||
{
|
||||
var line = new TextBlock
|
||||
{
|
||||
FontSize = 11,
|
||||
Foreground = (Brush)Application.Current.FindResource("TextFillColorSecondaryBrush"),
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
line.Inlines.Add(new Run($"• {dep.Name} — {dep.Description} "));
|
||||
if (!string.IsNullOrEmpty(dep.DownloadUrl))
|
||||
{
|
||||
var hl = new Hyperlink(new Run(dep.DownloadUrl)) { NavigateUri = new Uri(dep.DownloadUrl) };
|
||||
hl.RequestNavigate += (_, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = e.Uri.AbsoluteUri,
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
e.Handled = true;
|
||||
};
|
||||
line.Inlines.Add(hl);
|
||||
}
|
||||
stack.Children.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(cap.RoadmapNote))
|
||||
{
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "로드맵: " + cap.RoadmapNote,
|
||||
FontSize = 11,
|
||||
FontStyle = FontStyles.Italic,
|
||||
Foreground = (Brush)Application.Current.FindResource("TextFillColorTertiaryBrush"),
|
||||
Margin = new Thickness(0, 4, 0, 0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
}
|
||||
|
||||
card.Content = stack;
|
||||
return card;
|
||||
}
|
||||
|
||||
private void OnDragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
{
|
||||
e.Effects = DragDropEffects.Copy;
|
||||
IsDraggingOver = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Effects = DragDropEffects.None;
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnDragLeave(object sender, DragEventArgs e)
|
||||
{
|
||||
IsDraggingOver = false;
|
||||
}
|
||||
|
||||
private void OnFilesDropped(object sender, DragEventArgs e)
|
||||
{
|
||||
IsDraggingOver = false;
|
||||
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
|
||||
if (e.Data.GetData(DataFormats.FileDrop) is not string[] paths) return;
|
||||
OpenConvertWindow(paths);
|
||||
}
|
||||
|
||||
private void OnPickFilesClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = "변환할 파일 선택",
|
||||
Multiselect = true,
|
||||
Filter = "지원 파일|*.png;*.jpg;*.jpeg;*.gif;*.bmp;*.tif;*.tiff;*.webp;*.avif;*.heic;*.heif;*.psd;*.dng;*.nef;*.cr2;*.cr3;*.arw;*.raf;*.orf;*.rw2;*.srw;*.pef;*.pdf;*.docx;*.doc|모든 파일|*.*",
|
||||
};
|
||||
if (dlg.ShowDialog(this) == true)
|
||||
{
|
||||
OpenConvertWindow(dlg.FileNames);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenConvertWindow(string[] paths)
|
||||
{
|
||||
var files = ExpandPaths(paths);
|
||||
if (files.Count == 0) return;
|
||||
var window = new ConvertWindow(((App)Application.Current).Engine, files) { Owner = this };
|
||||
window.ShowDialog();
|
||||
}
|
||||
|
||||
private static List<string> ExpandPaths(IEnumerable<string> paths)
|
||||
{
|
||||
var list = new List<string>();
|
||||
foreach (var p in paths)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(p)) list.Add(p);
|
||||
else if (Directory.Exists(p))
|
||||
list.AddRange(Directory.EnumerateFiles(p, "*", SearchOption.TopDirectoryOnly));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private async void OnRegisterClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
ContextMenuRegistrar.Register(((App)Application.Current).Engine);
|
||||
ShowToast("컨텍스트 메뉴를 등록했습니다.\n파일 위에서 우클릭 → \"추가 옵션 표시\"에서 보입니다.");
|
||||
await PopulateAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("등록 중 오류: " + ex.Message, "EverythingToJpeg",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnUnregisterClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
ContextMenuRegistrar.Unregister(((App)Application.Current).Engine);
|
||||
ShowToast("컨텍스트 메뉴를 해제했습니다.");
|
||||
await PopulateAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("해제 중 오류: " + ex.Message, "EverythingToJpeg",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDiagnoseClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var window = new DiagnoseWindow(((App)Application.Current).Engine) { Owner = this };
|
||||
window.ShowDialog();
|
||||
}
|
||||
|
||||
private void ShowToast(string message)
|
||||
{
|
||||
MessageBox.Show(this, message, "EverythingToJpeg",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
40
src/EverythingToJpeg.App/Views/QuickProgressWindow.xaml
Normal file
40
src/EverythingToJpeg.App/Views/QuickProgressWindow.xaml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<ui:FluentWindow x:Class="EverythingToJpeg.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>
|
||||
81
src/EverythingToJpeg.App/Views/QuickProgressWindow.xaml.cs
Normal file
81
src/EverythingToJpeg.App/Views/QuickProgressWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
using System.Windows;
|
||||
using EverythingToJpeg.Core;
|
||||
using Wpf.Ui.Controls;
|
||||
|
||||
namespace EverythingToJpeg.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,
|
||||
"EverythingToJpeg", 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 { }
|
||||
}
|
||||
}
|
||||
27
src/EverythingToJpeg.App/app.manifest
Normal file
27
src/EverythingToJpeg.App/app.manifest
Normal 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="EverythingToJpeg.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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue