JJ_MKVMaker/lib/ui/explorer/rsync_flow.dart

102 lines
4.3 KiB
Dart

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<void> 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<void> _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.copySkipNewer);
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<Object>().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);
}
}