diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 51b84f9..2fee53b 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -351,6 +351,7 @@ "{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: moves this video to {1}", "{0}: 이미 이동 폴더에 있습니다": "{0}: already in the move folder", "{0}: 작업 중이라 옮기지 않았습니다": "{0}: not moved because it is being processed", + "{0}: 저장하지 않았습니다": "{0}: not saved", "{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: moves the {1} checked videos to {2}", "{0}개": "{0}", "{0}개 · {1}": "{0} · {1}", @@ -594,6 +595,7 @@ "넘기는 방향: 왼쪽 → 오른쪽": "Page direction: left → right", "넣지 못했습니다: {0}": "Could not add: {0}", "네덜란드어": "Dutch", + "네트워크 공유 (SMB) 는 아직 지원하지 않습니다. WebDAV 탭을 쓰세요": "Network shares (SMB) aren't supported yet. Use the WebDAV tab", "네트워크 공유는 공유 폴더 이름까지 넣으세요 (예: \\\\NAS\\영상).": "For a network share, include the shared folder name (e.g. \\\\NAS\\Videos).", "네팔어": "Nepali", "노랑": "Yellow", @@ -2034,6 +2036,7 @@ "환경 설정 열기": "Open settings", "환경 설정대로 {0} (환경 설정 > 파일 탐색기 > 같은 이름이 있을 때)": "As set in Settings, it {0} (Settings > File explorer > When a file with the same name exists)", "환경 설정을 열려면 마스터 비밀번호를 넣으세요. 앱을 끌 때까지 다시 묻지 않습니다.": "Enter the master password to open settings. It won't be asked again until the app is closed.", + "환경 설정의 {0} 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)": "{0} from Settings is not used when copying (to move, use [Move])", "회전": "Rotation", "회전 없음": "No rotation", "회차": "Episode", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 94c8957..8e69ca7 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -351,6 +351,7 @@ "{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: この動画を {1} に移動します", "{0}: 이미 이동 폴더에 있습니다": "{0}: 既に移動フォルダーにあります", "{0}: 작업 중이라 옮기지 않았습니다": "{0}: 作業中のため移動しませんでした", + "{0}: 저장하지 않았습니다": "{0}: 保存しませんでした", "{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: チェックした動画 {1}個を {2} に移動します", "{0}개": "{0}個", "{0}개 · {1}": "{0}個・{1}", @@ -594,6 +595,7 @@ "넘기는 방향: 왼쪽 → 오른쪽": "めくる方向: 左 → 右", "넣지 못했습니다: {0}": "追加できませんでした: {0}", "네덜란드어": "オランダ語", + "네트워크 공유 (SMB) 는 아직 지원하지 않습니다. WebDAV 탭을 쓰세요": "ネットワーク共有 (SMB) にはまだ対応していません。WebDAV タブを使ってください", "네트워크 공유는 공유 폴더 이름까지 넣으세요 (예: \\\\NAS\\영상).": "ネットワーク共有は共有フォルダー名まで入力してください (例: \\\\NAS\\動画)。", "네팔어": "ネパール語", "노랑": "黄色", @@ -2034,6 +2036,7 @@ "환경 설정 열기": "環境設定を開く", "환경 설정대로 {0} (환경 설정 > 파일 탐색기 > 같은 이름이 있을 때)": "設定どおり{0} (環境設定 > ファイルエクスプローラー > 同じ名前があるとき)", "환경 설정을 열려면 마스터 비밀번호를 넣으세요. 앱을 끌 때까지 다시 묻지 않습니다.": "環境設定を開くにはマスターパスワードを入力してください。アプリを終了するまで再度尋ねません。", + "환경 설정의 {0} 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)": "環境設定の {0} はコピーでは使いません (移動は [移動] で)", "회전": "回転", "회전 없음": "回転なし", "회차": "話数", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index be046ca..2d1f13b 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -351,6 +351,7 @@ "{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: 将此视频移到 {1}", "{0}: 이미 이동 폴더에 있습니다": "{0}: 已在移动文件夹中", "{0}: 작업 중이라 옮기지 않았습니다": "{0}: 正在处理,未移动", + "{0}: 저장하지 않았습니다": "{0}:未保存", "{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: 将勾选的 {1} 个视频移到 {2}", "{0}개": "{0} 个", "{0}개 · {1}": "{0} 个 · {1}", @@ -594,6 +595,7 @@ "넘기는 방향: 왼쪽 → 오른쪽": "翻页方向: 左 → 右", "넣지 못했습니다: {0}": "无法加入: {0}", "네덜란드어": "荷兰语", + "네트워크 공유 (SMB) 는 아직 지원하지 않습니다. WebDAV 탭을 쓰세요": "尚不支持网络共享(SMB)。请使用 WebDAV 标签", "네트워크 공유는 공유 폴더 이름까지 넣으세요 (예: \\\\NAS\\영상).": "网络共享请输入到共享文件夹名称 (例: \\\\NAS\\视频)。", "네팔어": "尼泊尔语", "노랑": "黄色", @@ -2034,6 +2036,7 @@ "환경 설정 열기": "打开设置", "환경 설정대로 {0} (환경 설정 > 파일 탐색기 > 같은 이름이 있을 때)": "按设置{0} (设置 > 文件资源管理器 > 存在同名项目时)", "환경 설정을 열려면 마스터 비밀번호를 넣으세요. 앱을 끌 때까지 다시 묻지 않습니다.": "要打开设置,请输入主密码。在关闭应用前不会再次询问。", + "환경 설정의 {0} 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)": "设置中的 {0} 不用于复制(要移动请用 [移动])", "회전": "旋转", "회전 없음": "不旋转", "회차": "集数", diff --git a/lib/ui/ai_upscale_ui.dart b/lib/ui/ai_upscale_ui.dart index 1f9fbb4..e56e150 100644 --- a/lib/ui/ai_upscale_ui.dart +++ b/lib/ui/ai_upscale_ui.dart @@ -300,7 +300,13 @@ Future upscaleOne(BuildContext context, AppController c, {required Stri AiJobs.instance.done(text, view: () async { AiJobs.instance.clearDoneView(); - await _compareAndSave(nav, c, result, input: input, nameFrom: nameFrom, outDir: outDir); + final saved = await _compareAndSave(nav, c, result, input: input, nameFrom: nameFrom, outDir: outDir); + // 작업 현황 줄도 결과로 (어디에 저장했는지 + [열기]) + if (saved != null) { + AiJobs.instance.done(trf('저장했습니다: {0}', [saved]), dir: p.dirname(saved)); + } else { + AiJobs.instance.done(trf('{0}: 저장하지 않았습니다', [nameShort])); + } }, dispose: () { try { diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 12ecd02..5e762bc 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -65,6 +65,10 @@ class ExplorerPage extends StatefulWidget { @visibleForTesting static List<(String, String)> Function()? debugVolumes; + /// 시험: 경로 입력을 Android 처럼 (SMB 안내, 179). null 이면 실제 기기 + @visibleForTesting + static bool? debugAndroidPaths; + static const routeName = 'explorer'; static const rsyncRouteName = 'rsync'; static int _open = 0, _openRsync = 0; @@ -1848,6 +1852,15 @@ class _ExplorerPageState extends State { '\n\n${trf('방법: {0}', [tr(method.label)])}' '${method == CopyMethod.builtin ? '' : ' · ${withoutSourceRemovingOptions(task.options)}${move ? (method == CopyMethod.rsync ? ' --remove-source-files' : ' /MOVE') : ''}'}' '${task.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [task.bandwidthKBps])}' : ''}'), + // 178 보충: 환경 설정의 원본 지우기 옵션은 복사에서 뺀다 - 왜 빠졌는지 알림 + if (!move && method != CopyMethod.builtin && optionsRemoveSource(task.options)) + Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + trf('환경 설정의 {0} 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)', + [splitOptions(task.options).where(isSourceRemovingOption).join(' ')]), + style: const TextStyle(fontSize: 12, color: JjColors.textDim)), + ), FutureBuilder>( future: clashesFuture, builder: (ctx, snap) { @@ -3083,6 +3096,8 @@ class _ExplorerPageState extends State { setState(() => _active = _panes.indexOf(pane)); final path = await showDialog(context: context, builder: (_) => _PathDialog(initial: pane.current)); if (path == null || !mounted) return; + // 179: SMB 를 넣었다가 [WebDAV 서버 추가] 를 누름 + if (path == _PathDialog.addDav) return _editDav(pane, null); await _goTo(pane, path, fresh: true); } @@ -3090,6 +3105,8 @@ class _ExplorerPageState extends State { static Future<(String?, String?)> checkEnteredPath(String input) async { var path = input.trim().replaceAll('"', ''); if (path.isEmpty) return (null, null); + // 179: Android 는 네트워크 공유 (SMB) 를 아직 못 연다 - 폴더가 없다고 하지 않고 WebDAV 로 안내 + if ((ExplorerPage.debugAndroidPaths ?? Platform.isAndroid) && looksLikeSmb(path)) return (null, smbNotSupported); if (Platform.isWindows && RegExp(r'^\\\\[^\\/]+[\\/]?$').hasMatch(path)) { return (null, tr('네트워크 공유는 공유 폴더 이름까지 넣으세요 (예: \\\\NAS\\영상).')); } @@ -3732,10 +3749,18 @@ class _PatternDialogState extends State<_PatternDialog> { ); } +/// 179: SMB 경로 (smb:// · \\서버\공유) +bool looksLikeSmb(String path) => RegExp(r'^(smb|cifs)://', caseSensitive: false).hasMatch(path) || path.startsWith(r'\\'); + +String get smbNotSupported => tr('네트워크 공유 (SMB) 는 아직 지원하지 않습니다. WebDAV 탭을 쓰세요'); + class _PathDialog extends StatefulWidget { const _PathDialog({required this.initial}); final String initial; + /// 창이 이것을 돌려주면 WebDAV 서버 추가 창을 연다 + static const addDav = '\u0000webdav'; + @override State<_PathDialog> createState() => _PathDialogState(); } @@ -3778,6 +3803,9 @@ class _PathDialogState extends State<_PathDialog> { onSubmitted: (_) => _submit(), ), actions: [ + // 179: SMB 를 넣었으면 바로 WebDAV 서버 추가로 + if (_error == smbNotSupported) + TextButton(onPressed: () => Navigator.pop(context, _PathDialog.addDav), child: Text(tr('WebDAV 서버 추가'))), TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('취소'))), FilledButton(onPressed: _checking ? null : _submit, child: Text(tr('확인'))), ], diff --git a/test/explorer_conflict_test.dart b/test/explorer_conflict_test.dart index 03f0b70..b7bea20 100644 --- a/test/explorer_conflict_test.dart +++ b/test/explorer_conflict_test.dart @@ -406,6 +406,33 @@ void main() { expect(find.textContaining(right), findsWidgets); }); + testWidgets('179: Android 에서 SMB 경로를 넣으면 "아직 지원하지 않음 · WebDAV" 안내와 [WebDAV 서버 추가]', (tester) async { + ExplorerPage.debugAndroidPaths = true; + addTearDown(() => ExplorerPage.debugAndroidPaths = null); + expect([for (final x in ['smb://nas/v', r'\\nas\v', 'SMB://x', '/storage/x', 'dav://a/']) looksLikeSmb(x)], [true, true, true, false, false]); + tester.view.physicalSize = const Size(1600, 900); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c)))); + await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); + await tester.tap(find.byTooltip('경로 입력').first); + await tester.pumpAndSettle(); + expect(find.widgetWithText(TextButton, 'WebDAV 서버 추가'), findsNothing); + await tester.enterText(find.byType(TextField).last, 'smb://nas/영상'); + await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '확인'))); + await settle(tester, () => find.textContaining('SMB').evaluate().isNotEmpty); + expect(find.text('네트워크 공유 (SMB) 는 아직 지원하지 않습니다. WebDAV 탭을 쓰세요'), findsOneWidget); + expect(find.text('폴더가 없거나 열 수 없습니다'), findsNothing); + // [WebDAV 서버 추가] → 서버 추가 창 + await tester.tap(find.widgetWithText(TextButton, 'WebDAV 서버 추가')); + await settle(tester, () => find.text('서버 이름').evaluate().isNotEmpty || find.textContaining('주소').evaluate().isNotEmpty, rounds: 50); + expect(find.widgetWithText(AlertDialog, 'WebDAV 서버 추가'), findsOneWidget); + expect(find.textContaining('SMB'), findsNothing, reason: '경로 창은 닫힘'); + await tester.tap(find.text('취소').last); + await tester.pumpAndSettle(); + expect(find.byType(AlertDialog), findsNothing); + }); + testWidgets('37-2: 보던 폴더가 밖에서 지워지면 새로 읽을 때 남아 있는 상위 폴더로 옮기고 알린다', (tester) async { final deep = Directory(p.join(left, 'a', 'b'))..createSync(recursive: true); c.settings.explorerPaths = [deep.path, right]; diff --git a/test/explorer_test.dart b/test/explorer_test.dart index 0d0eab3..3088b2f 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -121,6 +121,29 @@ void main() { expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue); }); + testWidgets('178 보충: 설정의 원본 지우기 옵션은 복사 창에서 빠진 까닭을 알린다 (이동 창에는 없음)', (tester) async { + c.settings + ..copyMethodFile = 'robocopy' + ..robocopyOptions = '/E /MOV'; + await open(tester); + await act(tester, () => tester.tap(find.text('doc.txt'))); + await settle(tester, () => false, rounds: 15); + await act(tester, () => tester.tap(find.text('복사').first)); + await tester.pumpAndSettle(); + expect(find.textContaining('방법: robocopy'), findsOneWidget); + expect(find.text('환경 설정의 /MOV 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)'), findsOneWidget); + await act(tester, () => tester.tap(find.text('취소').last)); + await tester.pumpAndSettle(); + await act(tester, () => tester.tap(find.text('이동').first)); + await tester.pumpAndSettle(); + expect(find.textContaining('옮길까요?'), findsOneWidget); + expect(find.textContaining('복사에서는 쓰지 않습니다'), findsNothing); + await act(tester, () => tester.tap(find.text('취소').last)); + await tester.pumpAndSettle(); + expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue); + expect(File(p.join(right, 'doc.txt')).existsSync(), isFalse); + }, skip: !Platform.isWindows); + testWidgets('바로 실행 모드: 폴더를 누르면 펼치고, 파일을 누르면 바로 실행', (tester) async { c.settings.explorerClick = 'open'; await open(tester); diff --git a/test/upscale_job_test.dart b/test/upscale_job_test.dart index 23f7ac1..08c0948 100644 --- a/test/upscale_job_test.dart +++ b/test/upscale_job_test.dart @@ -115,6 +115,9 @@ void main() { final saved = [for (final f in tmp.listSync(recursive: true).whereType()) if (!p.equals(f.path, input)) f.path]; expect(saved, isNotEmpty, reason: '저장됨 (원본 옆, 새 파일)'); expect(AiJobs.instance.lastDoneView, isNull, reason: '[보기] 는 한 번'); + // 작업 현황 줄도 저장한 곳으로 ([열기] 가 그 폴더) + expect(AiJobs.instance.lastDone, '저장했습니다: ${saved.single}'); + expect(AiJobs.instance.lastDoneDir, p.dirname(saved.single)); ScaffoldMessenger.of(tester.element(find.text('올리기'))).clearSnackBars(); await tester.pumpAndSettle(); });