모니터링 [실행] 의 이동도 미리 보기 · 확인 (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", "원본으로": "Back to the original",
"원본을 읽을 수 없어 멈춤": "Stopped: can't read the source", "원본을 읽을 수 없어 멈춤": "Stopped: can't read the source",
"원본을 읽을 수 없어 멈췄습니다": "Stopped because the source can't be read", "원본을 읽을 수 없어 멈췄습니다": "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", "원본의 {0}배": "{0}× original",
"원본의 빈 폴더 정리 중…": "Cleaning up empty folders in the source…", "원본의 빈 폴더 정리 중…": "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.", "원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.": "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)", "이동 폴더 (환경 설정에서 바꿀 수 있음)": "Move folder (can be changed in Settings)",
"이동 폴더로 (확인 뒤, 기본)": "To the move folder (after confirming, default)", "이동 폴더로 (확인 뒤, 기본)": "To the move folder (after confirming, default)",
"이동: {0} → {1}{2}": "Moved: {0} → {1}{2}", "이동: {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)", "이동: 옮길 폴더를 골라 이동 버튼을 만듭니다 (환경 설정 > 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 this video to the move folder{0}",
"이동: 체크한 동영상 {0}개를 이동 폴더로 옮깁니다": "Move: moves the {0} checked videos to the move folder", "이동: 체크한 동영상 {0}개를 이동 폴더로 옮깁니다": "Move: moves the {0} checked videos to the move folder",

View File

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

View File

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

View File

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

View File

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

View File

@ -167,6 +167,32 @@ void main() {
expect(find.text('아직 실행 안 함'), findsOneWidget); 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 { testWidgets('105: 원본이 대상 안 + 창에서 [원본 파일 지우기] 로 지우기 옵션이 생기면 막는다', (tester) async {
final outer = p.join(tmp.path, 'O'), inner = p.join(outer, 'in'); final outer = p.join(tmp.path, 'O'), inner = p.join(outer, 'in');
File(p.join(inner, 'n.txt')) File(p.join(inner, 'n.txt'))