import 'package:flutter/material.dart'; import '../../app/rsync_runner.dart'; import '../../core/sync_preview.dart' show PreviewAction, PreviewItem; import '../../core/vfs.dart' show vDisplay; import '../../l10n/tr.dart'; import '../monitor_page.dart' show PruneChoice; import '../path_label.dart' show shortPath; import '../sync_preview_view.dart'; import '../theme.dart'; /// Rsync 화면의 실행 확인 창 (E5c) - 실행할 것 · 옵션 · 원본 지우기 · 미리 보기. 규칙은 [RsyncRun] 이 가진다. /// [실행] 이면 true, [취소] 면 false, 닫으면 null. Future showRsyncRunDialog(BuildContext context, RsyncRun run) { // 72: 실행 전 비교 결과 (지워질 수를 실행 버튼에) List? preview; Object? previewError; // 15 검토 F2: 미리 볼 수 없는 거르기 규칙을 알고 실행하는지 (지우기가 들어 있을 때) var filterAck = false; return showDialog( context: context, builder: (ctx) => StatefulBuilder(builder: (ctx, set) => AlertDialog( scrollable: true, // 110: 버튼이 좁아 세로로 쌓여도 [취소] 가 먼저 (아래 · 위가 바뀌어 [취소] 자리에 [실행] 이 오지 않게) actionsOverflowDirection: VerticalDirection.down, title: Text(run.both ? tr('rsync 양쪽 (⇄)') : 'rsync'), content: SizedBox( width: 560, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ for (final t in run.tasks) ...[ // 109: 끝 폴더가 보이게 짧게, 전체 경로는 작게 Text(shortPath(t.sources.first), style: const TextStyle(fontWeight: FontWeight.w600)), Text('→ ${shortPath(t.dest)}', style: const TextStyle(fontWeight: FontWeight.w600)), Text('${vDisplay(t.sources.first)}/ → ${vDisplay(t.dest)}/', style: TextStyle(fontSize: 11, color: JjColors.textDim)), // 111: 이 쌍은 처음 실행 때 기억한 옵션을 쓴다 (환경 설정의 rsync 옵션을 바꿔도 그대로) if (run.center.tasks.any((x) => x.id == t.id)) Text(tr('이 폴더 쌍은 기억한 옵션을 씁니다 (환경 설정이 아니라 모니터링 > 복사 · rsync 에서 고칩니다)'), style: TextStyle(fontSize: 11, color: JjColors.textDim)), Text('rsync ${run.runOpts(run.tasks.indexOf(t))}${run.removeSource ? ' --remove-source-files' : ''}' '${t.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [t.bandwidthKBps])}' : ''}', style: TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)), const SizedBox(height: 10), ], Text(tr('폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).'), style: TextStyle(fontSize: 12, color: JjColors.textDim)), // 222: 이동이면 동작대로 (예전에는 이동일 때도 "복사 · 원본 폴더는 그대로" 라고 썼다) if (run.nested) Text( run.removeSource && !run.both ? tr('원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래로 옮겨집니다 (원본 파일은 지워지고, 원본 폴더는 아래에서 고른 대로).') : tr('원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (원본 폴더는 그대로 남음).'), style: TextStyle(fontSize: 12, color: JjColors.warning)), // 1280×720 등 낮은 화면에서도 보이게: 원본 지우기 체크 · 경고는 비교 목록보다 위에 (178) if (run.both) ...[ Text(keepOptionsTogether(tr('양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).')), style: TextStyle(fontSize: 12, color: JjColors.textDim)), if (run.optionMove) Text(tr('양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).'), style: TextStyle(fontSize: 12, color: JjColors.warning)), ] else ...[ CheckboxListTile( contentPadding: EdgeInsets.zero, value: run.removeSource, onChanged: (v) => set(() { run.setRemoveSource(v ?? false); preview = null; previewError = null; }), title: Text(keepOptionsTogether(tr('원본 파일 지우기 (--remove-source-files)'))), subtitle: Text(tr('대상으로 옮긴 파일을 원본에서 지웁니다 (이동)')), ), if (run.removeSource) ...[ // 178: 원본이 지워진다는 것을 분명히 (환경 설정 옵션 때문에 켜졌으면 그렇다고) Text( run.optionMove ? tr('원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에 --remove-source-files 가 있어 켜 두었습니다 - 끄면 이번에는 원본을 남깁니다.') : tr('원본 파일이 지워집니다 (이동).'), style: TextStyle(fontSize: 12, color: JjColors.warning, fontWeight: FontWeight.w600)), const SizedBox(height: 4), Text(tr('끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:'), style: TextStyle(fontSize: 12, color: JjColors.textDim)), const SizedBox(height: 6), PruneChoice(value: run.prune, onChanged: (v) => set(() => run.prune = v)), ], ], // 72: 무엇이 바뀌는지 파일별로 (→ · ← · 받는 쪽에만 있음 · 지워짐) const SizedBox(height: 8), if (run.badOptions().isNotEmpty) OptionProblemNote(run.badOptions()), // 관리자 지시 (10/20): [실행] 이 막힐 때는 막히는 이유 한 줄만 (미리 볼 수 없음 · [알고 실행합니다] 는 숨김) if (run.badOptions().isEmpty && run.filterLimits().isNotEmpty) FilterLimitNote(unsupported: run.filterLimits(), deletes: run.deletes(), ack: filterAck, onAck: (v) => set(() => filterAck = v)), SyncPreviewView( // 옵션이 바뀌면 (원본 파일 지우기) 다시 비교 key: ValueKey([for (var i = 0; i < run.tasks.length; i++) run.runOpts(i)].join('|')), // 177: rsync 로 맞추면 옵션의 거르기 (--exclude 등) 도 비교에 (WebDAV 는 앱이 직접 - 거르기를 따르지 않음) // 177 · 229: rsync 로 맞추면 옵션의 거르기도 비교에 (WebDAV 등은 앱이 직접) compute: run.preview, // 97: 다시 비교하는 동안은 예전 결과로 실행하지 못하게 onStart: () => set(() { preview = null; previewError = null; }), onResult: (items) => set(() { preview = items; previewError = null; }), onError: (e) => set(() { preview = null; previewError = e; }), ), if (run.nestedDelete()) Text(tr('원본이 대상 안에 있으면 지우기 (--delete) 를 함께 쓸 수 없습니다 (대상의 다른 파일이 모두 지워집니다).'), style: TextStyle(fontSize: 12, color: JjColors.alert, fontWeight: FontWeight.w600)), // 110: 이 줄은 늘 같은 자리를 차지한다 (글이 사라져 창 높이가 바뀌면 버튼이 움직인다) SizedBox( height: 34, child: run.deletes() && preview == null ? Text( previewError != null ? tr('지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.') : tr('지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.'), style: TextStyle(fontSize: 12, color: JjColors.warning), ) : null, ), const SizedBox(height: 4), if (run.viaDav) Text(tr('WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files · 거르기 (--exclude · --include · --filter · --delete-excluded) 를 따름).'), style: TextStyle(fontSize: 12, color: JjColors.accent)), ]), ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))), Builder(builder: (_) { final dels = preview?.where((x) => x.action == PreviewAction.delete).length ?? 0; return FilledButton( style: dels > 0 ? FilledButton.styleFrom(backgroundColor: JjColors.danger) : null, onPressed: run.nestedDelete() || run.badOptions().isNotEmpty || (run.deletes() && preview == null) || (run.deletes() && run.filterLimits().isNotEmpty && !filterAck) ? null : () => Navigator.pop(ctx, true), // 110: 글자는 늘 같게 (비교가 끝나 글이 길어지면 버튼 배치가 바뀌어 [취소] 를 누르려다 [실행] 을 누르게 됨). // 지울 수는 위의 빨간 글 "받는 쪽에서 N개가 지워집니다" 로 보인다 child: Text(run.removeSource ? tr('이동') : tr('실행')), ); }), ], )), ); }