/// 약속: Windows 의 종료 요청. [WindowsQuit.call] 이 true 를 돌려주면 프로그램을 끝낸다 (정리는 이미 마침). /// - 창 ✕ · 종료 버튼: 설정 "종료를 누르면" 에 따라 백그라운드로 (창만 숨김) / 묻지 않고 종료 / 고르기 /// - 트레이 "종료" · 업데이트 설치 ([force]): 항상 종료 (받는 중인 다운로드 · 복사가 있으면 확인) /// 끝낼 때는 작업 중지 → 다운로드 정리 → Tor 끄기 → 중복 실행 정리 → 작업이 멈출 때까지 → 목록 · 창 위치 저장 차례로. library; import 'package:flutter/material.dart'; import '../app/app_controller.dart'; import '../app/copy_center.dart'; import '../app/download_manager.dart'; import '../app/tor_service.dart'; import '../l10n/tr.dart'; import '../platform/windows/desktop_shell.dart'; import '../platform/windows/exit_trace.dart'; import '../platform/windows/session_marker.dart'; import '../platform/windows/single_instance.dart'; import '../platform/windows/window_memory.dart'; import '../ui/exit_dialog.dart'; import '../ui/exit_guard.dart' show confirmStopCopies; import '../ui/downloads_page.dart' show confirmExit; import 'startup_helpers.dart'; class WindowsQuit { final AppController controller; final DownloadManager downloads; final GlobalKey navigatorKey; final SingleInstance instance; final WindowMemory memory; final SessionMarker session; WindowsQuit({ required this.controller, required this.downloads, required this.navigatorKey, required this.instance, required this.memory, required this.session, }); Future call({bool force = false}) async { final shell = controller.services.shell; final desktop = shell is DesktopShell ? shell : null; var mode = force || (desktop?.closeFromTray ?? false) ? 'force' : controller.settings.run.closeAction; // 트레이 아이콘이 없으면 숨긴 창을 끝낼 방법이 없다 → 고르게 한다 if (mode == 'background' && !(desktop?.hasTray ?? false)) mode = 'ask'; if (mode == 'ask') { final ctx = navigatorKey.currentContext; if (ctx == null || !ctx.mounted) return false; final n = downloads.activeCount; final copies = CopyCenter.peekOf(controller)?.activeCount ?? 0; final running = [ if (controller.currentJob != null) controller.currentJob!, if (n > 0) trf('다운로드 {0}개', [n]), if (copies > 0) trf('복사 · 이동 {0}개', [copies]), ].join(' · '); final pick = await showCloseChoice(ctx, running: running.isEmpty ? tr('진행 중인 작업 없음') : running, hotkey: controller.settings.run.showHotkey); if (pick == null) return false; if (pick.$2) controller.updateSettings((x) => x.run.closeAction = pick.$1); mode = pick.$1; } if (mode == 'background') { await shell.hide(); return false; } if (mode == 'force') { final ctx = navigatorKey.currentContext; if (ctx != null && ctx.mounted && !await confirmExit(ctx, downloads)) return false; } // 43: 복사 · 이동 중이면 (설정이 "묻지 않고 종료" 여도) 한 번 묻는다 { final ctx = navigatorKey.currentContext; if (ctx != null && ctx.mounted && !await confirmStopCopies(ctx, controller)) return false; } ExitTrace.start(); final job = controller.currentJob; final wait = controller.pendingJobs.length; final steps = [ ExitStep(job == null ? tr('작업 확인 (진행 중인 작업 없음)') : trf('작업 중지: {0}{1}', [job, wait > 0 ? trf(' · 대기 {0}개', [wait]) : '']), () async => controller.cancel()), ExitStep(tr('다운로드 정리 (yt-dlp · aria2 종료)'), downloads.shutdown), ExitStep(tr('Tor 끄기'), () => TorService.instance.stop()), // 228: 다음 실행이 이 창에 넘기려 하지 않게 (instance.json 지우기 · 잠금 파일 놓기) ExitStep(tr('중복 실행 막기 정리'), instance.close), ExitStep(tr('쓰레드 확인 (AI · 변환 작업이 멈출 때까지)'), () async { while (controller.busy) { await Future.delayed(const Duration(milliseconds: 100)); } }), ExitStep(tr('동영상 목록 · 창 위치 저장'), () async { controller.stopVideoListShare(); await memory.save(); controller.note(tr('── 정상 종료 ──')); session.markClean(); }), ExitStep(tr('창 종료'), () async {}), ]; final ctx2 = navigatorKey.currentContext; if (ctx2 != null && ctx2.mounted) { await showExitProgress(ctx2, lastWork: lastWorkText(controller), steps: [ for (final s in steps) ExitStep(s.label, () async { try { await s.run(); } finally { ExitTrace.mark(trf('끝: {0}', [s.label])); } }), ]); ExitTrace.mark(tr('종료 창 닫힘')); } else { for (final s in steps) { try { await s.run().timeout(const Duration(seconds: 8)); } catch (_) {} } } return true; } }