모니터링 카드 실패 줄 [자세히]: 원문 (명령 줄 포함) 보기 · 복사 (237 보충)
- 사용자 편의 (관리자 지시): 원인을 물어볼 때 쓰게 원문을 CopyTask.lastDetail 에 두고 보기 · 복사 (비밀은 가림) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>fix_mkv
parent
f1206ae1f2
commit
b9739fd71c
|
|
@ -1005,6 +1005,7 @@
|
|||
"복사해 둔 AI 모델 ({0}) 을 되살렸습니다": "Restored the copied AI models ({0})",
|
||||
"복사해 둔 AI 모델 ({0}) 을 되살리는 중": "Restoring the copied AI models ({0})",
|
||||
"복사해 둔 AI 모델을 되살리지 못했습니다 (사본은 Download/JJ_MKVMaker/AI 모델 보관 에 그대로): {0}": "Could not restore the copied AI models (the copies remain in Download/JJ_MKVMaker/AI 모델 보관): {0}",
|
||||
"복사했습니다": "Copied",
|
||||
"복사했습니다.": "copied.",
|
||||
"볼 그림이 없습니다.": "No images to show.",
|
||||
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "This video is unavailable (deleted · region restricted).",
|
||||
|
|
|
|||
|
|
@ -1005,6 +1005,7 @@
|
|||
"복사해 둔 AI 모델 ({0}) 을 되살렸습니다": "コピーした AI モデル ({0}) を復元しました",
|
||||
"복사해 둔 AI 모델 ({0}) 을 되살리는 중": "コピーした AI モデル ({0}) を復元中",
|
||||
"복사해 둔 AI 모델을 되살리지 못했습니다 (사본은 Download/JJ_MKVMaker/AI 모델 보관 에 그대로): {0}": "コピーした AI モデルを復元できませんでした (コピーは Download/JJ_MKVMaker/AI 모델 보관 にそのまま): {0}",
|
||||
"복사했습니다": "コピーしました",
|
||||
"복사했습니다.": "コピーしました。",
|
||||
"볼 그림이 없습니다.": "表示する画像がありません。",
|
||||
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "視聴できない動画です(削除・地域制限)。",
|
||||
|
|
|
|||
|
|
@ -1005,6 +1005,7 @@
|
|||
"복사해 둔 AI 모델 ({0}) 을 되살렸습니다": "已恢复复制的 AI 模型 ({0})",
|
||||
"복사해 둔 AI 모델 ({0}) 을 되살리는 중": "正在恢复复制的 AI 模型 ({0})",
|
||||
"복사해 둔 AI 모델을 되살리지 못했습니다 (사본은 Download/JJ_MKVMaker/AI 모델 보관 에 그대로): {0}": "无法恢复复制的 AI 模型 (副本仍在 Download/JJ_MKVMaker/AI 모델 보관): {0}",
|
||||
"복사했습니다": "已复制",
|
||||
"복사했습니다.": "已复制。",
|
||||
"볼 그림이 없습니다.": "没有可显示的图片。",
|
||||
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "该视频无法观看(已删除 · 地区限制)。",
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ class CopyCenter extends ChangeNotifier {
|
|||
: userErrorText(e), // 237: 예외 이름 없이 (원래 글은 작업 기록에)
|
||||
lastFiles: job.doneFiles,
|
||||
lastLeft: e == null ? job.leftInSource : 0,
|
||||
lastDetail: e == null || e is FileOpCancelled ? '' : '$e',
|
||||
));
|
||||
}
|
||||
c.note(e == null
|
||||
|
|
|
|||
|
|
@ -138,6 +138,9 @@ class CopyTask {
|
|||
/// 235: 이동 뒤 원본에 남은 파일 수 (건너뜀 - 216). 카드에 "n개는 원본에 남음" 으로
|
||||
final int lastLeft;
|
||||
|
||||
/// 237: 실패 원문 (예외 이름 · 명령 줄 포함) - 카드의 [자세히] 에서 보고 복사한다. 화면 글은 [lastMessage]
|
||||
final String lastDetail;
|
||||
|
||||
/// 이동 ([move], rsync --remove-source-files) 뒤 원본 정리: '' 그대로 · 'keep' 빈 폴더를 지우고 원본 폴더는 남김 ·
|
||||
/// 'all' 원본 폴더까지 (비었으면) 지움 (find 원본/ -type d -empty -delete)
|
||||
final String prune;
|
||||
|
|
@ -157,6 +160,7 @@ class CopyTask {
|
|||
this.lastMessage = '',
|
||||
this.lastFiles = 0,
|
||||
this.lastLeft = 0,
|
||||
this.lastDetail = '',
|
||||
this.prune = '',
|
||||
});
|
||||
|
||||
|
|
@ -174,6 +178,7 @@ class CopyTask {
|
|||
String? lastMessage,
|
||||
int? lastFiles,
|
||||
int? lastLeft,
|
||||
String? lastDetail,
|
||||
String? prune,
|
||||
}) =>
|
||||
CopyTask(
|
||||
|
|
@ -191,6 +196,7 @@ class CopyTask {
|
|||
lastMessage: lastMessage ?? this.lastMessage,
|
||||
lastFiles: lastFiles ?? this.lastFiles,
|
||||
lastLeft: lastLeft ?? this.lastLeft,
|
||||
lastDetail: lastDetail ?? this.lastDetail,
|
||||
prune: prune ?? this.prune,
|
||||
);
|
||||
|
||||
|
|
@ -209,6 +215,7 @@ class CopyTask {
|
|||
'lastMessage': lastMessage,
|
||||
'lastFiles': lastFiles,
|
||||
'lastLeft': lastLeft,
|
||||
'lastDetail': lastDetail,
|
||||
'prune': prune,
|
||||
};
|
||||
|
||||
|
|
@ -227,6 +234,7 @@ class CopyTask {
|
|||
lastMessage: j['lastMessage'] as String? ?? '',
|
||||
lastFiles: (j['lastFiles'] as num?)?.toInt() ?? 0,
|
||||
lastLeft: (j['lastLeft'] as num?)?.toInt() ?? 0,
|
||||
lastDetail: j['lastDetail'] as String? ?? '',
|
||||
prune: const ['keep', 'all'].contains(j['prune']) ? j['prune'] as String : '',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import '../app/copy_center.dart';
|
|||
import '../app/live_sync.dart';
|
||||
import '../app/settings.dart';
|
||||
import '../app/transfer_job.dart';
|
||||
import '../core/download_detect.dart' show redactUrlSecrets;
|
||||
import '../core/secret_gate.dart';
|
||||
import '../core/file_ops.dart';
|
||||
import '../core/sync_tools.dart';
|
||||
|
|
@ -332,6 +333,33 @@ class _CopyCardState extends State<_CopyCard> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
/// 237: 실패 원문 보기 · 복사
|
||||
Future<void> _showFailure(CopyTask t) {
|
||||
final detail = redactUrlSecrets(t.lastDetail);
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(t.lastMessage),
|
||||
content: SizedBox(
|
||||
width: 560,
|
||||
child: SelectableText(detail, style: const TextStyle(fontFamily: 'monospace', fontSize: 12)),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(ClipboardData(text: detail));
|
||||
if (ctx.mounted) Navigator.pop(ctx);
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(tr('복사했습니다'))));
|
||||
},
|
||||
child: Text(tr('복사')),
|
||||
),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('닫기'))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _run(CopyTask t) async {
|
||||
// 236: 옵션 칸의 포커스를 푼다 - 남아 있으면 폰 · 태블릿에서 키보드가 올라와 끝 알림을 가린다
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
|
@ -517,7 +545,17 @@ class _CopyCardState extends State<_CopyCard> {
|
|||
if (t.lastResult == 'failed' && !running && t.lastMessage.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 28, top: 4),
|
||||
child: Text(t.lastMessage, maxLines: 3, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11, color: Colors.redAccent)),
|
||||
child: Wrap(crossAxisAlignment: WrapCrossAlignment.center, spacing: 4, children: [
|
||||
Text(t.lastMessage, maxLines: 3, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11, color: Colors.redAccent)),
|
||||
// 237: 원문 (예외 이름 · 명령 줄) 은 [자세히] 에서 - 보고 복사해 물어볼 수 있게 (다운로드 실패 줄 136 과 같게)
|
||||
if (t.lastDetail.trim().isNotEmpty)
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact, padding: const EdgeInsets.symmetric(horizontal: 6), minimumSize: const Size(0, 28)),
|
||||
onPressed: () => _showFailure(t),
|
||||
child: Text(tr('자세히')),
|
||||
),
|
||||
]),
|
||||
),
|
||||
if (job != null && (running || job.finished && job.error == null))
|
||||
Padding(padding: const EdgeInsets.only(left: 28, top: 6), child: TransferProgressLines(job: job)),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show SystemChannels;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/copy_center.dart';
|
||||
|
|
@ -241,6 +242,34 @@ void main() {
|
|||
await settle(tester, () => CopyCenter.of(c).jobs['1']?.finished ?? true, rounds: 200);
|
||||
});
|
||||
|
||||
testWidgets('237: 모니터링 카드 실패 글은 예외 이름 없이, 원문 (명령 줄 포함) 은 [자세히] 에서 보고 복사', (tester) async {
|
||||
const raw = 'ProcessException: 원본 파일을 지우지 못했습니다\n Command: rsync -avPog --remove-source-files a/ b/';
|
||||
c.settings.copyTasks = [
|
||||
const CopyTask(id: '1', sources: ['a'], dest: 'b', method: 'rsync', options: '-avPog', move: true)
|
||||
.copyWith(lastResult: 'failed', lastRun: DateTime(2026, 10, 12, 9).toIso8601String(), lastMessage: '원본 파일을 지우지 못했습니다', lastDetail: raw),
|
||||
];
|
||||
expect(CopyTask.fromJson(c.settings.copyTasks.single.toJson()).lastDetail, raw);
|
||||
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();
|
||||
expect(find.text('원본 파일을 지우지 못했습니다'), findsOneWidget);
|
||||
expect(find.textContaining('ProcessException'), findsNothing);
|
||||
await tester.tap(find.text('자세히'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('Command: rsync -avPog --remove-source-files'), findsOneWidget);
|
||||
String? copied;
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
||||
if (call.method == 'Clipboard.setData') copied = (call.arguments as Map)['text'] as String?;
|
||||
return null;
|
||||
});
|
||||
addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null));
|
||||
await tester.tap(find.text('복사'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(copied, raw);
|
||||
});
|
||||
|
||||
test('237: 사용자에게 보이는 실패 글에는 예외 이름 · 명령 줄이 없다', () {
|
||||
expect(userErrorText(const ProcessException('rsync', ['-a'], '원본 파일을 지우지 못했습니다', 23)), '원본 파일을 지우지 못했습니다');
|
||||
expect(userErrorText(const FileSystemException('지울 수 없음', r'C:\x', OSError('액세스가 거부되었습니다', 5))), r'지울 수 없음 · C:\x · 액세스가 거부되었습니다');
|
||||
|
|
|
|||
Loading…
Reference in New Issue