import 'package:flutter/widgets.dart'; import '../../app/copy_center.dart'; import '../../app/rsync_runner.dart'; import '../../core/file_ops.dart' show FileOpCancelled; import '../../core/vfs.dart' show vDirname, vMissingSync; import '../../l10n/tr.dart'; import 'explorer_controller.dart'; import 'explorer_jobs.dart'; import 'rsync_run_dialog.dart'; import 'transfer_flow.dart'; /// 약속 (E5d-5): Rsync 화면의 [→ ← ⇄] - 두 창에서 고른 폴더로 rsync. 막는 조건 · 옵션 · 미리 보기 · 실행은 [RsyncRun], /// 확인 창은 rsync_run_dialog, 진행은 [ExplorerJobs], rsync 실행 파일은 [ExplorerTransfers.ensureRsync]. /// 끝나면 두 폴더를 다시 읽고 결과를 알린다 (Host). 확인 창이 떠 있는 동안 또 누르면 무시 class ExplorerRsyncFlow { ExplorerRsyncFlow(this.ctl, this.jobs, this.transfers); final ExplorerController ctl; final ExplorerJobs jobs; final ExplorerTransfers transfers; /// Rsync 화면: 고른 왼쪽 · 오른쪽 폴더로 rsync. [right] 왼쪽 → 오른쪽, [left] 오른쪽 → 왼쪽 (둘 다면 함께). /// 폴더 "안의 것" 을 맞춘다 (원본/ → 대상/). 함께 할 때는 받는 쪽이 더 새 파일은 건너뛴다 (-u). /// 실행한 것은 모니터링에 남는다 (같은 것을 다시 하면 기억한 옵션으로). /// 확인 창이 떠 있는 동안 또 누르면 무시 (창이 겹쳐 아래 창을 잘못 누르지 않게 - 110) bool _asking = false; Future run(BuildContext context, {required bool right, required bool left}) async { if (_asking) return; _asking = true; try { await _run(context, right: right, left: left); } finally { _asking = false; } } Future _run(BuildContext context, {required bool right, required bool left}) async { final l = ctl.panes[0].sel.marked.firstOrNull, r = ctl.panes[1].sel.marked.firstOrNull; // 막는 조건 · 작업 · 옵션 · 미리 보기 · 실행은 RsyncRun (E5c), 확인 창은 rsync_run_dialog final (:run, :problem, :missing) = RsyncRun.prepare(CopyCenter.of(ctl.c), l: l, r: r, right: right, left: left, allowInnerToOuter: ctl.c.settings.allowInnerToOuter, jobs: this.jobs.list, viaDav: l != null && r != null && ExplorerTransfers.viaDav([l, r]), copySkipNewer: ctl.c.settings.copy.skipNewer); if (run == null) { ctl.host.snack(problem!); if (missing) { for (final x in ctl.panes) { x.sel.removeWhere(vMissingSync); } await ctl.refreshAll([vDirname(l!), vDirname(r!)]); } return; } if (!context.mounted) return; final go = await showRsyncRunDialog(context, run); // 110: 무엇을 골랐는지 작업 기록에 (실행 · 취소 · 닫힘) ctl.c.note(trf('Rsync 확인 창 ({0}): {1}', [ run.both ? '⇄' : right ? '→' : '←', go == true ? tr('실행') : go == false ? tr('취소') : tr('닫힘 (실행 안 함)'), ])); if (go != true || !context.mounted) return; if (run.nestedDelete()) { ctl.host.snack(RsyncRun.nestedDeleteMessage); return; } final exe = run.viaDav ? '' : await transfers.ensureRsync(context); if (exe == null || !context.mounted) return; final res = await run.execute( rsyncExe: exe, alive: () => context.mounted, note: ctl.c.note, onStarted: this.jobs.start); // 205: 시작하지 못하면 이유를 알린다 if (res.notStarted != null && context.mounted) ctl.host.snack(trf('시작하지 못했습니다: {0}', [res.notStarted])); final jobs = res.jobs; if (jobs.isEmpty || !context.mounted) return; // 원본 폴더까지 지웠으면 고른 표시도 뺀다 for (final x in ctl.panes) { x.sel.removeWhere(vMissingSync); } await ctl.refreshAll([run.l, run.r, vDirname(run.l), vDirname(run.r)]); for (final x in ctl.panes) { x.changed(); } final error = jobs.map((j) => j.error).whereType().firstOrNull; ctl.host.snack(error is FileOpCancelled ? tr('취소했습니다.') : error != null ? trf('끝나지 못했습니다: {0}', [error]) // 212 · 216: 이동인지 · 몇 개인지 · 원본에 남은 것 : jobs.map((j) => j.doneSummary()).join('\n')); await this.jobs.dropLater(jobs); } }