모니터링 [실행] 의 이동도 미리 보기 · 확인 (210)

관리자 지시 (데이터): 모니터링 카드의 [실행] 은 따로 묻는 창이 없어 이동 (원본 지우기) 을 곧바로 했다.
이동이면 "원본을 지우는 작업입니다 (이동)" 창에 옮길 목록을 보이고, 목록이 나온 뒤에만 [실행]. 복사만은 지금처럼 바로.
탐색기 이동 · Rsync 화면 · [최종 정리] 는 이미 자기 확인 창이 있어 두 번 묻지 않는다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix_mkv
RyuJongJin 2026-10-11 21:25:29 +09:00
parent cd00d3ce3a
commit 954b86b84c
6 changed files with 56 additions and 8 deletions

View File

@ -1358,6 +1358,8 @@
"원본으로": "Back to the original",
"원본을 읽을 수 없어 멈춤": "Stopped: can't read the source",
"원본을 읽을 수 없어 멈췄습니다": "Stopped because the source can't be read",
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "This run removes source files, so it can run once the list of files to move is shown.",
"원본을 지우는 작업입니다 (이동)": "This task removes source files (move)",
"원본의 {0}배": "{0}× original",
"원본의 빈 폴더 정리 중…": "Cleaning up empty folders in the source…",
"원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.": "Moves all files from the source to the target (deleting them from the source), then deletes the empty folders left in the source.",
@ -1478,6 +1480,7 @@
"이동 폴더 (환경 설정에서 바꿀 수 있음)": "Move folder (can be changed in Settings)",
"이동 폴더로 (확인 뒤, 기본)": "To the move folder (after confirming, default)",
"이동: {0} → {1}{2}": "Moved: {0} → {1}{2}",
"이동: 대상에 옮긴 파일은 원본에서 지워집니다": "Move: files moved to the destination are removed from the source",
"이동: 옮길 폴더를 골라 이동 버튼을 만듭니다 (환경 설정 > MKV 만들기 > 이동 버튼에서 여러 개 만들 수 있음)": "Move: choose a folder to create a move button (you can make several in Settings > Make MKV > Move buttons)",
"이동: 이 동영상을 이동 폴더로 옮깁니다{0}": "Move: moves this video to the move folder{0}",
"이동: 체크한 동영상 {0}개를 이동 폴더로 옮깁니다": "Move: moves the {0} checked videos to the move folder",

View File

@ -1358,6 +1358,8 @@
"원본으로": "元の画像で",
"원본을 읽을 수 없어 멈춤": "元を読めないため停止",
"원본을 읽을 수 없어 멈췄습니다": "元を読めないため停止しました",
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "元のファイルを削除する実行のため、移動する一覧が表示されてから実行できます。",
"원본을 지우는 작업입니다 (이동)": "元のファイルを削除する作業です (移動)",
"원본의 {0}배": "元の {0}倍",
"원본의 빈 폴더 정리 중…": "元の空フォルダーを整理中…",
"원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.": "元のファイルをすべて対象へ移し (元から削除)、元に残った空フォルダーを削除します。",
@ -1478,6 +1480,7 @@
"이동 폴더 (환경 설정에서 바꿀 수 있음)": "移動フォルダー(環境設定で変更できます)",
"이동 폴더로 (확인 뒤, 기본)": "移動フォルダーへ (確認後、既定)",
"이동: {0} → {1}{2}": "移動: {0} → {1}{2}",
"이동: 대상에 옮긴 파일은 원본에서 지워집니다": "移動: 移動先に移したファイルは元の場所から削除されます",
"이동: 옮길 폴더를 골라 이동 버튼을 만듭니다 (환경 설정 > MKV 만들기 > 이동 버튼에서 여러 개 만들 수 있음)": "移動: 移動先フォルダーを選んで移動ボタンを作ります(環境設定 > MKV 作成 > 移動ボタンで複数作れます)",
"이동: 이 동영상을 이동 폴더로 옮깁니다{0}": "移動: この動画を移動フォルダーに移します{0}",
"이동: 체크한 동영상 {0}개를 이동 폴더로 옮깁니다": "移動: チェックした動画 {0}個を移動フォルダーに移します",

View File

@ -1358,6 +1358,8 @@
"원본으로": "回到原图",
"원본을 읽을 수 없어 멈춤": "无法读取源,已停止",
"원본을 읽을 수 없어 멈췄습니다": "无法读取源,已停止",
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "此操作会删除源文件,需在显示要移动的列表后才能执行。",
"원본을 지우는 작업입니다 (이동)": "此任务会删除源文件(移动)",
"원본의 {0}배": "原始的 {0} 倍",
"원본의 빈 폴더 정리 중…": "正在清理源中的空文件夹…",
"원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.": "将源中的所有文件移到目标 (从源中删除),然后删除源中剩下的空文件夹。",
@ -1478,6 +1480,7 @@
"이동 폴더 (환경 설정에서 바꿀 수 있음)": "移动目标文件夹(可在设置中更改)",
"이동 폴더로 (확인 뒤, 기본)": "移到移动文件夹 (确认后,默认)",
"이동: {0} → {1}{2}": "已移动: {0} → {1}{2}",
"이동: 대상에 옮긴 파일은 원본에서 지워집니다": "移动:已移到目标的文件会从源中删除",
"이동: 옮길 폴더를 골라 이동 버튼을 만듭니다 (환경 설정 > MKV 만들기 > 이동 버튼에서 여러 개 만들 수 있음)": "移动: 选择目标文件夹以创建移动按钮(可在 设置 > 制作 MKV > 移动按钮 中创建多个)",
"이동: 이 동영상을 이동 폴더로 옮깁니다{0}": "移动: 将此视频移到移动目标文件夹{0}",
"이동: 체크한 동영상 {0}개를 이동 폴더로 옮깁니다": "移动: 将勾选的 {0} 个视频移到移动目标文件夹",

View File

@ -332,8 +332,9 @@ class _CopyCardState extends State<_CopyCard> {
}
Future<void> _run(CopyTask t) async {
// 104: 지우기가 들어 있으면 Rsync 화면과 같은 미리 보기를 거친다 (그사이 대상에 생긴 파일을 묻지 않고 지우지 않게)
if (!await confirmDeletingRun(context, t) || !mounted) return;
// 104: 지우기가 들어 있으면 Rsync 화면과 같은 미리 보기를 거친다 (그사이 대상에 생긴 파일을 묻지 않고 지우지 않게).
// 210: 이동 (원본 지우기) 도 - 카드의 [실행] 은 따로 묻는 창이 없어 곧바로 원본을 지웠다. 복사만은 지금처럼 바로
if (!await confirmDeletingRun(context, t, includeMove: true) || !mounted) return;
widget.c.note(trf('rsync 시작 ({0}): {1} → {2}', [tr('모니터링의 [실행]'), t.sources.join(', '), t.dest]));
String? exe;
if (t.method == 'rsync') {

View File

@ -167,8 +167,11 @@ bool taskDeletes(CopyTask t) {
/// 104: 지우기가 들어 있는 작업은 어디서 실행하든 (모니터링 [실행] · 옵션 저장 뒤 실행 · 최종 정리) Rsync 화면과 같은 미리 보기를 거친다.
/// 지울 목록이 나온 뒤에만 [실행] 이 켜진다. 지우기가 없으면 묻지 않고 true
Future<bool> confirmDeletingRun(BuildContext context, CopyTask t) async {
if (!taskDeletes(t)) return true;
/// [includeMove] (210): 이동 (원본 파일 지우기) 도 미리 보기 · 확인을 거친다 - 모니터링의 [실행] 처럼 따로 묻는 창이 없을 때
Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeMove = false}) async {
final deletes = taskDeletes(t);
final moves = includeMove && t.move;
if (!deletes && !moves) return true;
List<PreviewItem>? preview;
Object? error;
final update = optionsUpdate(t.options);
@ -181,7 +184,7 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t) async {
return AlertDialog(
scrollable: true,
actionsOverflowDirection: VerticalDirection.down, // 110: [취소] 가 먼저
title: Text(tr('지우기가 들어 있는 작업입니다')),
title: Text(deletes ? tr('지우기가 들어 있는 작업입니다') : tr('원본을 지우는 작업입니다 (이동)')),
content: SizedBox(
width: 560,
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
@ -191,10 +194,17 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t) async {
Text('${vDisplay(s)} → ${vDisplay(dstOf(s))}', style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
],
Text('${t.method} ${t.options}', style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
// 210: 이동이면 실행 뒤 원본에서 지워진다는 것을 분명히
if (t.move)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(tr('이동: 대상에 옮긴 파일은 원본에서 지워집니다'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
),
const SizedBox(height: 8),
SyncPreviewView(
compute: () async => [
for (final s in t.sources) ...await previewSync(s, dstOf(s), toRight: true, delete: true, update: update),
for (final s in t.sources) ...await previewSync(s, dstOf(s), toRight: true, delete: deletes, update: update),
],
onStart: () => set(() {
preview = null;
@ -213,7 +223,9 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t) async {
? Text(
error != null
? tr('지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.')
: tr('지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.'),
: deletes
? tr('지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.')
: tr('원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent),
)
: null,
@ -223,7 +235,7 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t) async {
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
FilledButton(
style: dels > 0 ? FilledButton.styleFrom(backgroundColor: JjColors.danger) : null,
style: dels > 0 || moves ? FilledButton.styleFrom(backgroundColor: JjColors.danger) : null,
onPressed: preview == null ? null : () => Navigator.pop(ctx, true),
child: Text(tr('실행')), // 110: 글자를 바꾸지 않는다 (버튼 자리가 움직이지 않게)
),

View File

@ -167,6 +167,32 @@ void main() {
expect(find.text('아직 실행 안 함'), findsOneWidget);
});
testWidgets('210: 모니터링 [실행] 의 이동 (원본 지우기) 도 미리 보기 · 확인을 거친다 (취소하면 원본 그대로) · 복사는 바로', (tester) async {
final src = p.join(tmp.path, 'mv'), dst = p.join(tmp.path, 'mvd');
final a = File(p.join(src, 'a.txt'))
..createSync(recursive: true)
..writeAsStringSync('a');
Directory(dst).createSync();
c.settings.copyTasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, move: true, method: 'builtin')];
tester.view.physicalSize = const Size(1500, 1000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c)));
await tester.pump();
await tester.tap(find.widgetWithText(FilledButton, '실행').first);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('원본을 지우는 작업입니다 (이동)'), findsOneWidget);
expect(find.text('이동: 대상에 옮긴 파일은 원본에서 지워집니다'), findsOneWidget);
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200);
expect(find.textContaining('a.txt'), findsWidgets, reason: '옮길 목록');
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
await settle(tester, () => false, rounds: 10);
expect(a.existsSync(), isTrue, reason: '취소하면 원본 그대로');
expect(File(p.join(dst, 'a.txt')).existsSync(), isFalse);
expect(find.text('아직 실행 안 함'), findsOneWidget);
});
testWidgets('105: 원본이 대상 안 + 창에서 [원본 파일 지우기] 로 지우기 옵션이 생기면 막는다', (tester) async {
final outer = p.join(tmp.path, 'O'), inner = p.join(outer, 'in');
File(p.join(inner, 'n.txt'))