152 lines
9.6 KiB
Dart
152 lines
9.6 KiB
Dart
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<bool?> showRsyncRunDialog(BuildContext context, RsyncRun run) {
|
||
// 72: 실행 전 비교 결과 (지워질 수를 실행 버튼에)
|
||
List<PreviewItem>? preview;
|
||
Object? previewError;
|
||
// 15 검토 F2: 미리 볼 수 없는 거르기 규칙을 알고 실행하는지 (지우기가 들어 있을 때)
|
||
var filterAck = false;
|
||
return showDialog<bool>(
|
||
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('실행')),
|
||
);
|
||
}),
|
||
],
|
||
)),
|
||
);
|
||
}
|