using System.Windows;
using Everything2Everything.App.Cli;
using Everything2Everything.App.Views;
using Everything2Everything.Core;
using Microsoft.Extensions.DependencyInjection;
namespace Everything2Everything.App;
public partial class App : Application
{
private readonly IServiceProvider _services;
/// App·LlmProvider가 공유하는 설정 저장소 (키 저장 즉시 변환에 반영). DI 컨테이너 단일 싱글턴.
public ISettingsStore Settings { get; }
public ConversionEngine Engine { get; }
public App()
{
// 컴포지션 루트 — Core의 DI 확장(Scrutor 자동등록)으로 Provider/Registry/Engine/Settings를 구성.
var services = new ServiceCollection();
services.AddEverything2Everything();
_services = services.BuildServiceProvider();
Settings = _services.GetRequiredService();
Engine = _services.GetRequiredService();
}
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(Engine, Settings);
MainWindow = window;
window.Show();
}
private void ShowConvertDialog(IReadOnlyList files)
{
var window = new Views.MainWindow(Engine, Settings, files);
MainWindow = window;
window.Show();
}
private void ShowDiagnoseWindow()
{
var window = new DiagnoseWindow(Engine);
MainWindow = window;
window.Show();
}
private async Task RunQuickAsync(IReadOnlyList files, string outputExtension)
{
// 빠른 변환 전, 출력 형식에 맞춘 간단 옵션 팝업.
// '자세히 옵션…'이면 풀 UI(MainWindow)로 전환, '취소'면 종료, '변환'이면 선택 옵션으로 진행.
var optWin = new QuickOptionsWindow(outputExtension, files.Count, Settings);
var confirmed = optWin.ShowDialog();
if (optWin.OpenFullUi) { ShowConvertDialog(files); return; }
if (confirmed != true) { Shutdown(0); return; }
// 변환 경로 동안은 명시적 종료 모드 — 진행 창을 닫아도 변환을 취소(ffmpeg 종료)한 '뒤' 앱을 종료한다.
// (기본 OnLastWindowClose면 창 닫는 즉시 종료가 시작돼 ffmpeg가 고아로 백그라운드에 남는다.)
ShutdownMode = System.Windows.ShutdownMode.OnExplicitShutdown;
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}");
using var cts = new CancellationTokenSource();
var progress = new QuickProgressWindow(files.Count, cts, outputExtension);
var closed = new TaskCompletionSource();
progress.Closed += (_, _) => closed.TrySetResult();
progress.Show();
try
{
var options = optWin.Options.ToConvertOptions();
var reporter = new Progress(p => progress.Report(p));
var results = await Engine.ConvertManyAsync(
files, outputExtension, options, reporter, BatchMode.Independent, cts.Token);
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);
await closed.Task; // 결과 창을 사용자가 닫을 때까지 대기(성공 경로)
}
catch (OperationCanceledException)
{
// 사용자가 취소(취소 버튼/창 닫기) — ffmpeg는 이미 중단된 뒤 여기에 도달. 진행 창 닫고 종료.
log.AppendLine(" CANCELLED by user");
try { progress.Close(); } catch { }
}
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 { }
Shutdown(0); // 모든 경로에서 명시적 종료(백그라운드 잔류·고아 프로세스 방지)
}
}
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();
};
}
}