diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 6240441..f0c565d 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -299,6 +299,7 @@ "{0} 에서 그리는 중 ({1}초)": "Drawing on {0} ({1} s)", "{0} 에서 받을 파일을 찾지 못했습니다": "Could not find a file to download from {0}", "{0} 외 {1}개": "{0} and {1} more", + "{0} 은(는) 이미 이 폴더에 있어 옮길 것이 없습니다.\n{1}": "{0} is already in this folder, so there is nothing to move.\n{1}", "{0} 을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).": "Picked up {0}. Open the target folder and tap [Paste] ({1}).", "{0} 을(를) 설치했습니다.": "Installed {0}.", "{0} 을(를) 지울까요?": "Delete {0}?", @@ -388,9 +389,11 @@ "같은 이름이 있을 때": "When a file with the same name exists", "같은 이름이라 건너뜀: {0}": "Skipped (same name): {0}", "같은 조건으로 다시 (시드 그대로)": "Again with the same settings (same seed)", + "같은 폴더에 사본을 만들까요? (이름 (2))\n{0}": "Make a copy in the same folder? (name (2))\n{0}", "같은 폴더의 모든 동영상": "All videos in the same folder", "같은 폴더의 자막은 자동으로 추가됩니다": "Subtitles in the same folder are added automatically", "같은 폴더이거나 한쪽이 다른 쪽 안에 있습니다. 다른 폴더를 고르세요.": "Same folder, or one is inside the other. Choose different folders.", + "같은 폴더입니다": "Same folder", "같은 항목을 같은 곳으로 복사 · 이동하는 중입니다.": "The same items are already being copied/moved to the same place.", "같은 화질이면 H.264 먼저 받기": "Prefer H.264 at the same quality", "개수": "Count", @@ -877,6 +880,7 @@ "보기": "View", "보기 {0}초 · Ctrl+휠 확대/축소": "View {0} s · Ctrl+wheel to zoom", "보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "How often [Slideshow] in the viewer turns to the next page", + "보던 폴더가 없어져 상위 폴더로 옮겼습니다": "The folder you were viewing is gone, so moved to its parent folder", "보안": "Security", "보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "Choose the buttons to show and drag to reorder.", "보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "Adjustments require re-encoding, so the codec was changed from \"Keep original\" to H.264.", @@ -936,6 +940,7 @@ "빠른 디코더 (TAESD)": "Fast decoder (TAESD)", "빼고 싶은 것 (선택)": "What to avoid (optional)", "사라지는 것": "What is lost", + "사본 만들기": "Make a copy", "사용 안 함": "Off", "사용 안 함. \"Sign in to confirm you're not a bot\" 오류가 나오면 설정하세요": "Off. Set this if the \"Sign in to confirm you're not a bot\" error appears", "사용: {0}": "Using: {0}", @@ -1373,7 +1378,9 @@ "이미 동영상 목록에 있습니다 ({0}개).": "Already in the video list ({0}).", "이미 목록에 있는 동영상입니다 ({0}개).": "These videos are already in the list ({0}).", "이미 받는 중이거나 받을 수 없는 주소입니다.": "Already downloading or a URL that cannot be downloaded.", + "이미 이 폴더에 있습니다": "Already in this folder", "이미 이 폴더에 있습니다: {0}": "Already in this folder: {0}", + "이미 이 폴더에 있어 건너뜀: {0}": "Skipped, already in this folder: {0}", "이미 있는 폴더입니다: {0}": "The folder already exists: {0}", "이미 작업 중입니다": "Already working", "이미지 · PDF · ZIP 보기": "Image · PDF · ZIP viewer", @@ -1785,6 +1792,7 @@ "폴더: {0}": "Folder: {0}", "폴더가 들어 있을 때": "When folders are included", "폴더가 아닙니다: {0}": "Not a folder: {0}", + "폴더가 없거나 열 수 없습니다": "The folder does not exist or cannot be opened", "폴더가 없거나 열 수 없습니다: {0}": "The folder doesn't exist or can't be opened: {0}", "폴더가 없습니다": "The folder does not exist", "폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "Folders are shown, but the videos and files inside need this permission. Turn on JJ_MKVMaker on the permission screen and come back.", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 503dc7b..9842b47 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -299,6 +299,7 @@ "{0} 에서 그리는 중 ({1}초)": "{0} で描画中 ({1} 秒)", "{0} 에서 받을 파일을 찾지 못했습니다": "{0} からダウンロードするファイルが見つかりませんでした", "{0} 외 {1}개": "{0} ほか {1} 件", + "{0} 은(는) 이미 이 폴더에 있어 옮길 것이 없습니다.\n{1}": "{0} は既にこのフォルダーにあるため移動するものがありません。\n{1}", "{0} 을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).": "{0} を保持しました。入れるフォルダーで [貼り付け] を押してください ({1})。", "{0} 을(를) 설치했습니다.": "{0} をインストールしました。", "{0} 을(를) 지울까요?": "{0} を削除しますか?", @@ -388,9 +389,11 @@ "같은 이름이 있을 때": "同じ名前があるとき", "같은 이름이라 건너뜀: {0}": "同じ名前のためスキップ: {0}", "같은 조건으로 다시 (시드 그대로)": "同じ条件でもう一度 (シードはそのまま)", + "같은 폴더에 사본을 만들까요? (이름 (2))\n{0}": "同じフォルダーにコピーを作りますか? (名前 (2))\n{0}", "같은 폴더의 모든 동영상": "同じフォルダーのすべての動画", "같은 폴더의 자막은 자동으로 추가됩니다": "同じフォルダーの字幕は自動で追加されます", "같은 폴더이거나 한쪽이 다른 쪽 안에 있습니다. 다른 폴더를 고르세요.": "同じフォルダーか、一方がもう一方の中にあります。別のフォルダーを選んでください。", + "같은 폴더입니다": "同じフォルダーです", "같은 항목을 같은 곳으로 복사 · 이동하는 중입니다.": "同じ項目を同じ場所へコピー・移動中です。", "같은 화질이면 H.264 먼저 받기": "同じ画質なら H.264 を優先", "개수": "枚数", @@ -877,6 +880,7 @@ "보기": "表示", "보기 {0}초 · Ctrl+휠 확대/축소": "表示 {0}秒・Ctrl+ホイールで拡大/縮小", "보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "表示画面の [連続表示] が次のページへ進む間隔", + "보던 폴더가 없어져 상위 폴더로 옮겼습니다": "見ていたフォルダーがなくなったため上のフォルダーに移動しました", "보안": "セキュリティ", "보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "表示するボタンを選び、ドラッグで順序を変えます。", "보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "補正には再エンコードが必要なため、コーデックを「元のまま」から H.264 に変更しました。", @@ -936,6 +940,7 @@ "빠른 디코더 (TAESD)": "高速デコーダー (TAESD)", "빼고 싶은 것 (선택)": "避けたいもの (任意)", "사라지는 것": "消えるもの", + "사본 만들기": "コピーを作る", "사용 안 함": "使用しない", "사용 안 함. \"Sign in to confirm you're not a bot\" 오류가 나오면 설정하세요": "使用しない。「Sign in to confirm you're not a bot」エラーが出たら設定してください", "사용: {0}": "使用: {0}", @@ -1373,7 +1378,9 @@ "이미 동영상 목록에 있습니다 ({0}개).": "既に動画リストにあります({0}個)。", "이미 목록에 있는 동영상입니다 ({0}개).": "既にリストにある動画です({0}個)。", "이미 받는 중이거나 받을 수 없는 주소입니다.": "既にダウンロード中か、ダウンロードできない URL です。", + "이미 이 폴더에 있습니다": "既にこのフォルダーにあります", "이미 이 폴더에 있습니다: {0}": "すでにこのフォルダーにあります: {0}", + "이미 이 폴더에 있어 건너뜀: {0}": "既にこのフォルダーにあるためスキップ: {0}", "이미 있는 폴더입니다: {0}": "既にあるフォルダーです: {0}", "이미 작업 중입니다": "すでに作業中です", "이미지 · PDF · ZIP 보기": "画像・PDF・ZIP ビューアー", @@ -1785,6 +1792,7 @@ "폴더: {0}": "フォルダー: {0}", "폴더가 들어 있을 때": "フォルダーを含むとき", "폴더가 아닙니다: {0}": "フォルダーではありません: {0}", + "폴더가 없거나 열 수 없습니다": "フォルダーがないか開けません", "폴더가 없거나 열 수 없습니다: {0}": "フォルダーがないか、開けません: {0}", "폴더가 없습니다": "フォルダーがありません", "폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "フォルダーは見えても、中の動画・ファイルはこの権限が必要です。許可画面で JJ_MKVMaker をオンにして戻ってください。", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 8fa689b..8d45170 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -299,6 +299,7 @@ "{0} 에서 그리는 중 ({1}초)": "正在 {0} 上绘制 ({1} 秒)", "{0} 에서 받을 파일을 찾지 못했습니다": "未在 {0} 找到要下载的文件", "{0} 외 {1}개": "{0} 等 {1} 个", + "{0} 은(는) 이미 이 폴더에 있어 옮길 것이 없습니다.\n{1}": "{0} 已在此文件夹中,无需移动。\n{1}", "{0} 을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).": "已暂存 {0}。请在目标文件夹中点按 [粘贴] ({1})。", "{0} 을(를) 설치했습니다.": "已安装 {0}。", "{0} 을(를) 지울까요?": "要删除 {0} 吗?", @@ -388,9 +389,11 @@ "같은 이름이 있을 때": "存在同名项目时", "같은 이름이라 건너뜀: {0}": "因同名已跳过: {0}", "같은 조건으로 다시 (시드 그대로)": "以相同条件再来一次 (种子不变)", + "같은 폴더에 사본을 만들까요? (이름 (2))\n{0}": "要在同一文件夹中创建副本吗?(名称 (2))\n{0}", "같은 폴더의 모든 동영상": "同一文件夹中的所有视频", "같은 폴더의 자막은 자동으로 추가됩니다": "同一文件夹中的字幕会自动添加", "같은 폴더이거나 한쪽이 다른 쪽 안에 있습니다. 다른 폴더를 고르세요.": "是同一文件夹,或一个在另一个里面。请选择其他文件夹。", + "같은 폴더입니다": "同一文件夹", "같은 항목을 같은 곳으로 복사 · 이동하는 중입니다.": "正在将相同项目复制 · 移动到同一位置。", "같은 화질이면 H.264 먼저 받기": "相同画质时优先 H.264", "개수": "数量", @@ -877,6 +880,7 @@ "보기": "查看", "보기 {0}초 · Ctrl+휠 확대/축소": "显示 {0} 秒 · Ctrl+滚轮缩放", "보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "浏览画面中 [连续浏览] 翻到下一页的间隔", + "보던 폴더가 없어져 상위 폴더로 옮겼습니다": "正在查看的文件夹已不存在,已转到上级文件夹", "보안": "安全", "보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "选择要显示的按钮,拖动以调整顺序。", "보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "调整需要重新编码,因此编解码器已从“保持原样”改为 H.264。", @@ -936,6 +940,7 @@ "빠른 디코더 (TAESD)": "快速解码器 (TAESD)", "빼고 싶은 것 (선택)": "要排除的内容 (可选)", "사라지는 것": "丢失的内容", + "사본 만들기": "创建副本", "사용 안 함": "不使用", "사용 안 함. \"Sign in to confirm you're not a bot\" 오류가 나오면 설정하세요": "不使用。如出现“Sign in to confirm you're not a bot”错误请设置", "사용: {0}": "使用: {0}", @@ -1373,7 +1378,9 @@ "이미 동영상 목록에 있습니다 ({0}개).": "已在视频列表中({0} 个)。", "이미 목록에 있는 동영상입니다 ({0}개).": "这些视频已在列表中({0} 个)。", "이미 받는 중이거나 받을 수 없는 주소입니다.": "已在下载中,或该地址无法下载。", + "이미 이 폴더에 있습니다": "已在此文件夹中", "이미 이 폴더에 있습니다: {0}": "已在此文件夹中: {0}", + "이미 이 폴더에 있어 건너뜀: {0}": "已在此文件夹中,已跳过: {0}", "이미 있는 폴더입니다: {0}": "文件夹已存在: {0}", "이미 작업 중입니다": "已在处理中", "이미지 · PDF · ZIP 보기": "图片 · PDF · ZIP 查看", @@ -1785,6 +1792,7 @@ "폴더: {0}": "文件夹: {0}", "폴더가 들어 있을 때": "包含文件夹时", "폴더가 아닙니다: {0}": "不是文件夹: {0}", + "폴더가 없거나 열 수 없습니다": "文件夹不存在或无法打开", "폴더가 없거나 열 수 없습니다: {0}": "文件夹不存在或无法打开: {0}", "폴더가 없습니다": "文件夹不存在", "폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "虽然能看到文件夹,但其中的视频和文件需要此权限。请在权限界面开启 JJ_MKVMaker 后返回。", diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index a381b9a..bef2de4 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -610,6 +610,21 @@ class _ExplorerPageState extends State { } catch (e) { pane.cache[dir] = const []; pane.errors[dir] = isAndroidRestricted(dir) ? androidRestrictedError : '$e'; + // 37-2: 보던 폴더가 밖에서 지워졌으면 남아 있는 가장 가까운 상위 폴더로 (제목 · 경로 칸도 그 폴더로) + if (!isDav(dir) && samePath(dir, pane.current) && !samePath(dir, pane.root) && !Directory(dir).existsSync()) { + var up = vDirname(dir); + while (!samePath(up, pane.root) && !Directory(up).existsSync() && vDirname(up) != up) { + up = vDirname(up); + } + pane.errors.remove(dir); + pane.cache.remove(dir); + pane.expanded.remove(dir); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _snack(tr('보던 폴더가 없어져 상위 폴더로 옮겼습니다')); + unawaited(_goTo(pane, up, fresh: true)); + }); + } } finally { pane.loading.remove(dir); pane.changed(); @@ -1614,6 +1629,36 @@ class _ExplorerPageState extends State { _snack(tr('같은 항목을 같은 곳으로 복사 · 이동하는 중입니다.')); return false; } + // 135: 같은 폴더로 - 짧은 알림은 놓치기 쉬워 창으로. 이동은 할 것이 없고, 복사는 사본을 만들지 한 번 묻는다 + final here = [for (final x in sources) if (samePath(vDirname(x), dest)) x]; + var sameFolderCopy = false; + // 이동인데 일부만 이미 그 폴더에 있으면 그것만 건너뛰고 나머지를 옮긴다 (관리자: 섞어 고른 것을 다시 고르지 않게) + var skippedHere = const []; + if (move && here.isNotEmpty && here.length < sources.length) { + skippedHere = here; + sources = [for (final x in sources) if (!here.contains(x)) x]; + } else if (here.isNotEmpty && (move || here.length == sources.length)) { + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + scrollable: true, + title: Text(move ? tr('이미 이 폴더에 있습니다') : tr('같은 폴더입니다')), + content: Text(move + ? trf('{0} 은(는) 이미 이 폴더에 있어 옮길 것이 없습니다.\n{1}', [_namesOf(here), vDisplay(dest)]) + : trf('같은 폴더에 사본을 만들까요? (이름 (2))\n{0}', [_namesOf(here)])), + actions: [ + if (move) + FilledButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('확인'))) + else ...[ + TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))), + FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('사본 만들기'))), + ], + ], + ), + ); + if (ok != true || !mounted) return false; + sameFolderCopy = true; // 이미 물었으니 아래 확인 창은 건너뛴다 + } // 원본 · 대상이 없거나 (다른 곳에서 지움 - 목록을 새로 고침) 폴더를 자기 안으로 넣으려 하면 시작하지 않는다 final problem = transferProblem(sources, dest, move: move); if (problem != null) { @@ -1643,7 +1688,7 @@ class _ExplorerPageState extends State { _ => null, }; var conflict = always ?? NameConflict.rename; - final go = await showDialog( + final go = sameFolderCopy ? true : await showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setInner) => AlertDialog( @@ -1652,6 +1697,7 @@ class _ExplorerPageState extends State { content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${trf('{0}개 항목을 다음 폴더로 {1}\n{2}', [sources.length, move ? tr('옮길까요?') : tr('복사할까요?'), vDisplay(dest)])}' '\n${_namesOf(sources)}' + '${skippedHere.isEmpty ? '' : '\n${trf('이미 이 폴더에 있어 건너뜀: {0}', [_namesOf(skippedHere)])}'}' '\n\n${trf('방법: {0}', [tr(method.label)])}' '${method == CopyMethod.builtin ? '' : ' · ${task.options}'}' '${task.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [task.bandwidthKBps])}' : ''}'), @@ -1736,6 +1782,7 @@ class _ExplorerPageState extends State { : [ trf('{0}개 항목을 {1}', [job.made.length, move ? tr('옮겼습니다.') : tr('복사했습니다.')]), if (job.skipped.isNotEmpty) trf('같은 이름이라 건너뜀: {0}', [_namesOf(job.skipped)]), + if (skippedHere.isNotEmpty) trf('이미 이 폴더에 있어 건너뜀: {0}', [_namesOf(skippedHere)]), ].join(' · ')); // 다 되면 진행 막대가 잠깐 100% 를 보인 뒤 내려간다 await Future.delayed(const Duration(milliseconds: 1200)); @@ -2736,27 +2783,29 @@ class _ExplorerPageState extends State { ), ); - /// 경로를 직접 넣어 가기 (37). 없는 폴더면 알린다 + /// 경로를 직접 넣어 가기 (37). 146 · 37-2: 없는 폴더면 창을 닫지 않고 칸 아래에 알린다 (고쳐서 다시 넣을 수 있게) Future _enterPath(_Pane pane) async { setState(() => _active = _panes.indexOf(pane)); - final input = await _askName( - Platform.isWindows ? tr('경로 입력 (예: D:\\영상 · \\\\NAS\\공유)') : tr('경로 입력'), pane.current, - hint: tr('경로'), selectAll: true); - if (input == null || !mounted) return; + final path = await showDialog(context: context, builder: (_) => _PathDialog(initial: pane.current)); + if (path == null || !mounted) return; + await _goTo(pane, path, fresh: true); + } + + /// 146: 넣은 경로를 고쳐 쓰기 · 확인. 갈 수 있으면 그 경로, 아니면 칸 아래에 보일 이유 + static Future<(String?, String?)> checkEnteredPath(String input) async { var path = input.trim().replaceAll('"', ''); - if (path.isEmpty) return; + if (path.isEmpty) return (null, null); if (Platform.isWindows && RegExp(r'^\\\\[^\\/]+[\\/]?$').hasMatch(path)) { - _snack(tr('네트워크 공유는 공유 폴더 이름까지 넣으세요 (예: \\\\NAS\\영상).')); - return; + return (null, tr('네트워크 공유는 공유 폴더 이름까지 넣으세요 (예: \\\\NAS\\영상).')); } if (!isDav(path) && Platform.isWindows && RegExp(r'^[a-zA-Z]:$').hasMatch(path)) path = '$path\\'; - final ok = isDav(path) || await Directory(path).exists(); - if (!mounted) return; - if (!ok) { - _snack(trf('폴더가 없거나 열 수 없습니다: {0}', [path])); - return; + bool ok; + try { + ok = isDav(path) || await Directory(path).exists(); + } catch (_) { + ok = false; } - await _goTo(pane, path, fresh: true); + return ok ? (path, null) : (null, tr('폴더가 없거나 열 수 없습니다')); } /// WebDAV 서버 추가 (없으면 [root] 는 null) · 고치기 (탭 길게 누르기). 저장하면 그 서버로 간다. @@ -3265,3 +3314,56 @@ class _Thumb extends StatelessWidget { }, ); } + +/// 146 · 37-2: 경로 입력 창 - 없는 폴더면 닫지 않고 칸 아래에 "폴더가 없거나 열 수 없습니다" +class _PathDialog extends StatefulWidget { + const _PathDialog({required this.initial}); + final String initial; + + @override + State<_PathDialog> createState() => _PathDialogState(); +} + +class _PathDialogState extends State<_PathDialog> { + late final _ctl = TextEditingController(text: widget.initial) + ..selection = TextSelection(baseOffset: 0, extentOffset: widget.initial.length); + String? _error; + bool _checking = false; + + @override + void dispose() { + _ctl.dispose(); + super.dispose(); + } + + Future _submit() async { + setState(() => _checking = true); + final (path, error) = await _ExplorerPageState.checkEnteredPath(_ctl.text); + if (!mounted) return; + setState(() { + _checking = false; + _error = error; + }); + if (path != null) Navigator.pop(context, path); + } + + @override + Widget build(BuildContext context) => AlertDialog( + scrollable: true, + title: Text(Platform.isWindows ? tr('경로 입력 (예: D:\\영상 · \\\\NAS\\공유)') : tr('경로 입력')), + content: TextField( + controller: _ctl, + autofocus: true, + decoration: InputDecoration(hintText: tr('경로'), errorText: _error, errorMaxLines: 3), + // 고치기 시작하면 앞의 오류는 지운다 + onChanged: (_) { + if (_error != null) setState(() => _error = null); + }, + onSubmitted: (_) => _submit(), + ), + actions: [ + 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 fa26d8a..7616cf5 100644 --- a/test/explorer_conflict_test.dart +++ b/test/explorer_conflict_test.dart @@ -257,6 +257,115 @@ void main() { expect(find.text('다시 시도'), findsOneWidget); }); + testWidgets('135: 두 창이 같은 폴더 - [이동] 은 "이미 이 폴더에 있습니다" 창 · [복사] 는 사본을 만들지 묻는다', (tester) async { + c.settings.explorerPaths = [left, left]; + 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().length >= 2); + await tester.runAsync(() => tester.tap(find.text('선택').first)); + await settle(tester, () => false, rounds: 5); + await tester.runAsync(() => tester.tap(find.text('doc.txt').first)); + await settle(tester, () => false, rounds: 5); + expect(find.textContaining('1개 표시함'), findsOneWidget); + await tester.runAsync(() => tester.tap(find.text('이동').first)); + await settle(tester, () => find.byType(AlertDialog).evaluate().isNotEmpty, rounds: 30); + // 짧은 알림은 놓치기 쉬워 창으로 (135) + expect(find.descendant(of: find.byType(AlertDialog), matching: find.text('이미 이 폴더에 있습니다')), findsOneWidget); + expect(find.textContaining('doc.txt 은(는) 이미 이 폴더에 있어 옮길 것이 없습니다'), findsOneWidget); + await tester.tap(find.widgetWithText(FilledButton, '확인')); + await tester.pumpAndSettle(); + expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue); + // 같은 폴더로 [복사] 는 사본을 만들지 묻는다: [취소] → 그대로, [사본 만들기] → "doc (2).txt" + await tester.runAsync(() => tester.tap(find.text('복사').first)); + await settle(tester, () => find.byType(AlertDialog).evaluate().isNotEmpty, rounds: 30); + expect(find.textContaining('같은 폴더에 사본을 만들까요? (이름 (2))'), findsOneWidget); + await tester.tap(find.text('취소')); + await tester.pumpAndSettle(); + expect(File(p.join(left, 'doc (2).txt')).existsSync(), isFalse); + await tester.runAsync(() => tester.tap(find.text('복사').first)); + await settle(tester, () => find.byType(AlertDialog).evaluate().isNotEmpty, rounds: 30); + await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '사본 만들기'))); + await settle(tester, () => File(p.join(left, 'doc (2).txt')).existsSync() && find.byType(SnackBar).evaluate().isNotEmpty); + expect(File(p.join(left, 'doc (2).txt')).readAsStringSync(), 'new'); + expect(find.widgetWithText(FilledButton, '복사'), findsNothing, reason: '확인 창을 두 번 띄우지 않음'); + }); + + testWidgets('135 보충: 섞어 고른 [이동] - 이미 그 폴더에 있는 것만 건너뛰고 나머지는 옮긴다 (확인 창 · 끝난 알림에 적음)', (tester) async { + File(p.join(left, 'a.txt')).writeAsStringSync('a'); + File(p.join(right, 'b.txt')).writeAsStringSync('b'); + c.settings.explorerPaths = [tmp.path, right]; // 왼쪽 창은 left · right 를 함께 보는 위 폴더 + 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('left').evaluate().isNotEmpty); + // 왼쪽 창에서 left · right 를 펼침 + await tester.runAsync(() => tester.tap(find.text('left').first)); + await settle(tester, () => find.text('a.txt').evaluate().isNotEmpty); + await tester.runAsync(() => tester.tap(find.text('right').first)); + await settle(tester, () => find.text('b.txt').evaluate().length >= 2); + await tester.runAsync(() => tester.tap(find.text('선택').first)); + await settle(tester, () => false, rounds: 5); + await tester.runAsync(() => tester.tap(find.text('a.txt').first)); + await tester.runAsync(() => tester.tap(find.text('b.txt').first)); + await settle(tester, () => false, rounds: 5); + expect(find.textContaining('2개 표시함'), findsOneWidget); + await tester.runAsync(() => tester.tap(find.text('이동').first)); + await settle(tester, () => find.byType(AlertDialog).evaluate().isNotEmpty, rounds: 30); + expect(find.textContaining('이미 이 폴더에 있어 건너뜀: b.txt'), findsOneWidget); + await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '이동'))); + await settle(tester, () => File(p.join(right, 'a.txt')).existsSync() && find.byType(SnackBar).evaluate().isNotEmpty); + expect(File(p.join(right, 'a.txt')).readAsStringSync(), 'a'); + expect(File(p.join(left, 'a.txt')).existsSync(), isFalse); + expect(File(p.join(right, 'b.txt')).readAsStringSync(), 'b'); + expect(find.textContaining('이미 이 폴더에 있어 건너뜀: b.txt'), findsOneWidget, reason: '끝난 알림에도'); + }); + + testWidgets('146 · 37-2: 없는 경로를 넣으면 창을 닫지 않고 칸 아래에 "폴더가 없거나 열 수 없습니다" · 고친 경로로 간다', (tester) async { + 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(); + await tester.enterText(find.byType(TextField).last, p.join(tmp.path, 'nope')); + await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '확인'))); + await settle(tester, () => find.text('폴더가 없거나 열 수 없습니다').evaluate().isNotEmpty); + expect(find.text('폴더가 없거나 열 수 없습니다'), findsOneWidget, reason: '칸 아래 (창은 그대로)'); + expect(find.byType(AlertDialog), findsOneWidget); + // 고치기 시작하면 오류는 지워지고, 있는 폴더면 창을 닫고 간다 + await tester.enterText(find.byType(TextField).last, right); + await tester.pump(); + expect(find.text('폴더가 없거나 열 수 없습니다'), findsNothing); + await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '확인'))); + await settle(tester, () => find.byType(AlertDialog).evaluate().isEmpty); + await settle(tester, () => false, rounds: 10); + expect(find.byType(AlertDialog), findsNothing); + expect(find.textContaining(right), findsWidgets); + }); + + testWidgets('37-2: 보던 폴더가 밖에서 지워지면 새로 읽을 때 남아 있는 상위 폴더로 옮기고 알린다', (tester) async { + final deep = Directory(p.join(left, 'a', 'b'))..createSync(recursive: true); + c.settings.explorerPaths = [deep.path, right]; + 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.textContaining(deep.path).evaluate().isNotEmpty); + // 밖에서 a 폴더째 지움 → [새로 고침] + Directory(p.join(left, 'a')).deleteSync(recursive: true); + await tester.runAsync(() => tester.tap(find.text('새로 고침').first)); + await settle(tester, () => find.textContaining('보던 폴더가 없어져 상위 폴더로 옮겼습니다').evaluate().isNotEmpty); + expect(find.textContaining('보던 폴더가 없어져 상위 폴더로 옮겼습니다'), findsOneWidget); + await settle(tester, () => false, rounds: 10); + // 제목 (지금 경로) 도 남아 있는 가장 가까운 상위 (left) 로 + expect(find.textContaining(p.join(left, 'a')), findsNothing); + expect(find.text('doc.txt'), findsWidgets); + }); + testWidgets('48: 건너뛰기를 고르면 그대로 두고 건너뛴 것을 알린다', (tester) async { await startCopy(tester); await tester.tap(find.text('건너뛰기')); diff --git a/test/explorer_parallel_test.dart b/test/explorer_parallel_test.dart index 99563ed..62de632 100644 --- a/test/explorer_parallel_test.dart +++ b/test/explorer_parallel_test.dart @@ -111,8 +111,8 @@ void main() { await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인'))); await settle(tester, () => find.textContaining('폴더가 없거나 열 수 없습니다').evaluate().isNotEmpty, rounds: 20); expect(find.textContaining('폴더가 없거나 열 수 없습니다'), findsOneWidget); - await tester.tap(find.byTooltip('경로 입력').first); - await tester.pumpAndSettle(); + // 146: 창은 열린 채 - 그 칸에서 고쳐 넣는다 + expect(find.byType(AlertDialog), findsOneWidget); await tester.enterText(find.byType(TextField).last, deep); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인'))); await settle(tester, () => find.text('deep.txt').evaluate().isNotEmpty);