diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 6953c84..595d89f 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -163,6 +163,7 @@ "{0} 브라우저의 YouTube 로그인 쿠키 사용 (Chrome · Edge 는 브라우저를 닫아야 읽힐 수 있음)": "Use the YouTube sign-in cookies of the {0} browser (Chrome · Edge may need to be closed to be read)", "{0} 순으로 정렬{1}": "Sort by {0}{1}", "{0} 아래에 만듭니다": "Created under {0}", + "{0} 에 붙여넣기 ({1}개)": "Paste into {0} ({1})", "{0} 에서 받을 파일을 찾지 못했습니다": "Could not find a file to download from {0}", "{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg does not have the {0} encoder.", "{0} 추가": "Add {0}", @@ -179,6 +180,7 @@ "{0}개 표시함": "{0} marked", "{0}개 항목을 {1}": "{0} items {1}", "{0}개 항목을 다음 폴더로 {1}\n{2}": "{0} items to this folder — {1}\n{2}", + "{0}개 항목을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).": "{0} items held. Press [Paste] in the destination folder ({1}).", "{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "Delete {0} items? This cannot be undone.", "{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "Imported {0} into \"Other bookmarks\".", "{0}개를 옮겼습니다 → {1}": "Moved {0} → {1}", @@ -440,6 +442,7 @@ "보기 {0}초 · Ctrl+휠 확대/축소": "View {0} s · Ctrl+wheel to zoom", "보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "Choose the buttons to show and drag to reorder.", "복사": "Copy", + "복사 (붙여넣기로)": "Copy (to paste)", "복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).": "Copy · move works with two panes (Pane layout > Two panes).", "복사 · 이동할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "Choose items to copy · move (tap, or mark with the circle on the right).", "복사(Ctrl+C)한 주소 자동 다운로드": "Auto-download copied (Ctrl+C) URLs", @@ -448,6 +451,8 @@ "복사했습니다.": "copied.", "볼 수 없는 영상입니다 (삭제 · 지역 제한).": "This video is unavailable (deleted · region restricted).", "분석 실패: {0} - {1}": "Analysis failed: {0} - {1}", + "붙여넣기": "Paste", + "붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "Nothing to paste. First hold items with [Copy] · [Move].", "브라우저 엔진 (앱 안)": "Browser engine (in-app)", "브라우저 쿠키를 읽지 못했습니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 \"앱 안 브라우저\" 를 고르거나, 고른 브라우저가 설치되어 있는지 확인하세요 (Chrome · Edge 는 브라우저를 닫아야 읽힙니다).": "Could not read the browser cookies. In Settings > Downloads > YouTube cookies, choose \"In-app browser\", or check that the chosen browser is installed (Chrome · Edge must be closed to be read).", "비공개 · 회원 전용 영상입니다.": "This is a private · members-only video.", @@ -651,6 +656,7 @@ "이 폴더의 동영상 재생": "Play videos in this folder", "이동": "Move", "이동 ({0})": "Move ({0})", + "이동 (붙여넣기로)": "Move (to paste)", "이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)": "Move buttons (bottom right of the details, stacked upward)", "이동 버튼 추가": "Add move button", "이동 버튼의 폴더": "Folder of the move button", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 160f5c4..fe0b3f4 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -163,6 +163,7 @@ "{0} 브라우저의 YouTube 로그인 쿠키 사용 (Chrome · Edge 는 브라우저를 닫아야 읽힐 수 있음)": "{0} ブラウザーの YouTube ログインクッキーを使う(Chrome・Edge はブラウザーを閉じないと読めないことがあります)", "{0} 순으로 정렬{1}": "{0} 順に並べ替え{1}", "{0} 아래에 만듭니다": "{0} の下に作成します", + "{0} 에 붙여넣기 ({1}개)": "{0} に貼り付け ({1} 件)", "{0} 에서 받을 파일을 찾지 못했습니다": "{0} からダウンロードするファイルが見つかりませんでした", "{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg に {0} エンコーダーがありません。", "{0} 추가": "{0} を追加", @@ -179,6 +180,7 @@ "{0}개 표시함": "{0} 件をマーク", "{0}개 항목을 {1}": "{0} 件の項目を{1}", "{0}개 항목을 다음 폴더로 {1}\n{2}": "{0} 件の項目を次のフォルダーへ{1}\n{2}", + "{0}개 항목을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).": "{0} 件を保持しました。入れるフォルダーで [貼り付け] を押してください ({1})。", "{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "{0} 件の項目を削除しますか? 元に戻せません。", "{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "{0}個を「その他のブックマーク」に取り込みました。", "{0}개를 옮겼습니다 → {1}": "{0}個を移動しました → {1}", @@ -440,6 +442,7 @@ "보기 {0}초 · Ctrl+휠 확대/축소": "表示 {0}秒・Ctrl+ホイールで拡大/縮小", "보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "表示するボタンを選び、ドラッグで順序を変えます。", "복사": "コピー", + "복사 (붙여넣기로)": "コピー (貼り付け用)", "복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).": "コピー · 移動は 2 ペインで使います (ペインの配置 > 2 ペイン)。", "복사 · 이동할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "コピー · 移動する項目を選んでください (タップ、または右の丸でマーク)。", "복사(Ctrl+C)한 주소 자동 다운로드": "コピー(Ctrl+C)した URL を自動ダウンロード", @@ -448,6 +451,8 @@ "복사했습니다.": "コピーしました。", "볼 수 없는 영상입니다 (삭제 · 지역 제한).": "視聴できない動画です(削除・地域制限)。", "분석 실패: {0} - {1}": "分析失敗: {0} - {1}", + "붙여넣기": "貼り付け", + "붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "貼り付ける項目がありません。先に [コピー] · [移動] で保持してください。", "브라우저 엔진 (앱 안)": "ブラウザーエンジン(アプリ内)", "브라우저 쿠키를 읽지 못했습니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 \"앱 안 브라우저\" 를 고르거나, 고른 브라우저가 설치되어 있는지 확인하세요 (Chrome · Edge 는 브라우저를 닫아야 읽힙니다).": "ブラウザーのクッキーを読めませんでした。環境設定 > ダウンロード > YouTube クッキー で「アプリ内ブラウザー」を選ぶか、選んだブラウザーがインストールされているか確認してください(Chrome・Edge はブラウザーを閉じないと読めません)。", "비공개 · 회원 전용 영상입니다.": "非公開・メンバー限定の動画です。", @@ -651,6 +656,7 @@ "이 폴더의 동영상 재생": "このフォルダーの動画を再生", "이동": "移動", "이동 ({0})": "移動({0})", + "이동 (붙여넣기로)": "移動 (貼り付け用)", "이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)": "移動ボタン(詳細の右下、上に 1つずつ)", "이동 버튼 추가": "移動ボタンを追加", "이동 버튼의 폴더": "移動ボタンのフォルダー", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 01d502b..7b0850a 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -163,6 +163,7 @@ "{0} 브라우저의 YouTube 로그인 쿠키 사용 (Chrome · Edge 는 브라우저를 닫아야 읽힐 수 있음)": "使用 {0} 浏览器的 YouTube 登录 Cookie(Chrome · Edge 可能需要关闭浏览器才能读取)", "{0} 순으로 정렬{1}": "按{0}排序{1}", "{0} 아래에 만듭니다": "在 {0} 下创建", + "{0} 에 붙여넣기 ({1}개)": "粘贴到 {0} ({1} 个)", "{0} 에서 받을 파일을 찾지 못했습니다": "未在 {0} 找到要下载的文件", "{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg 中没有 {0} 编码器。", "{0} 추가": "添加 {0}", @@ -179,6 +180,7 @@ "{0}개 표시함": "已标记 {0} 个", "{0}개 항목을 {1}": "{0} 个项目{1}", "{0}개 항목을 다음 폴더로 {1}\n{2}": "将 {0} 个项目到以下文件夹 — {1}\n{2}", + "{0}개 항목을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).": "已暂存 {0} 个项目。请在目标文件夹中按 [粘贴] ({1})。", "{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "删除 {0} 个项目吗? 此操作无法撤销。", "{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "已将 {0} 个导入“其他书签”。", "{0}개를 옮겼습니다 → {1}": "已移动 {0} 个 → {1}", @@ -440,6 +442,7 @@ "보기 {0}초 · Ctrl+휠 확대/축소": "显示 {0} 秒 · Ctrl+滚轮缩放", "보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "选择要显示的按钮,拖动以调整顺序。", "복사": "复制", + "복사 (붙여넣기로)": "复制 (用于粘贴)", "복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).": "复制 · 移动需在双窗格中使用 (窗格布局 > 双窗格)。", "복사 · 이동할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "请选择要复制 · 移动的项目 (点按,或用右侧圆圈标记)。", "복사(Ctrl+C)한 주소 자동 다운로드": "自动下载复制(Ctrl+C)的地址", @@ -448,6 +451,8 @@ "복사했습니다.": "已复制。", "볼 수 없는 영상입니다 (삭제 · 지역 제한).": "该视频无法观看(已删除 · 地区限制)。", "분석 실패: {0} - {1}": "分析失败: {0} - {1}", + "붙여넣기": "粘贴", + "붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "没有可粘贴的项目。请先用 [复制] · [移动] 暂存。", "브라우저 엔진 (앱 안)": "浏览器引擎(应用内)", "브라우저 쿠키를 읽지 못했습니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 \"앱 안 브라우저\" 를 고르거나, 고른 브라우저가 설치되어 있는지 확인하세요 (Chrome · Edge 는 브라우저를 닫아야 읽힙니다).": "未能读取浏览器 Cookie。请在 设置 > 下载 > YouTube Cookie 中选择“应用内浏览器”,或确认所选浏览器已安装(Chrome · Edge 需关闭浏览器才能读取)。", "비공개 · 회원 전용 영상입니다.": "这是私享 · 会员专享视频。", @@ -651,6 +656,7 @@ "이 폴더의 동영상 재생": "播放此文件夹中的视频", "이동": "移动", "이동 ({0})": "移动({0})", + "이동 (붙여넣기로)": "移动 (用于粘贴)", "이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)": "移动按钮(详细信息右下角,向上依次排列)", "이동 버튼 추가": "添加移动按钮", "이동 버튼의 폴더": "移动按钮的文件夹", diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index b76952b..9bd56ab 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -62,6 +62,7 @@ enum ExplorerButton { rename(Icons.drive_file_rename_outline, '이름 변경'), copy(Icons.copy_outlined, '복사'), move(Icons.drive_file_move_outline, '이동'), + paste(Icons.content_paste, '붙여넣기'), delete(Icons.delete_outline, '삭제'), search(Icons.search, '찾기'), sort(Icons.sort, '정렬 기준'), @@ -387,6 +388,11 @@ class _ExplorerPageState extends State { ('rename', Icons.drive_file_rename_outline, tr('이름 변경')), if (dual) ('copy', Icons.copy_outlined, tr('다른 창으로 복사')), if (dual) ('move', Icons.drive_file_move_outline, tr('다른 창으로 이동')), + // 어느 배치에서나: 담아 두었다가 원하는 폴더에서 붙여넣기 + ('clipCopy', Icons.content_copy, dual ? tr('복사 (붙여넣기로)') : tr('복사')), + ('clipMove', Icons.content_cut, dual ? tr('이동 (붙여넣기로)') : tr('이동')), + if (_clip.isNotEmpty) + ('paste', Icons.content_paste, trf('{0} 에 붙여넣기 ({1}개)', [p.basename(e.isDir ? e.path : p.dirname(e.path)), _clip.length])), ('delete', Icons.delete_outline, tr('삭제')), if (!Platform.isAndroid || e.isDir) ('reveal', Icons.folder_outlined, tr('파일 관리자에서 보기')), ('info', Icons.info_outline, tr('정보')), @@ -427,6 +433,12 @@ class _ExplorerPageState extends State { await _transfer(pane, [e.path], move: false); case 'move': await _transfer(pane, [e.path], move: true); + case 'clipCopy': + _toClipboard(pane.marked.contains(e.path) ? pane.marked.toList() : [e.path], move: false); + case 'clipMove': + _toClipboard(pane.marked.contains(e.path) ? pane.marked.toList() : [e.path], move: true); + case 'paste': + await _paste(pane, e.isDir ? e.path : p.dirname(e.path)); case 'delete': await _delete(pane, [e.path]); case 'reveal': @@ -513,6 +525,8 @@ class _ExplorerPageState extends State { await _transfer(pane, _targets(pane), move: false); case ExplorerButton.move: await _transfer(pane, _targets(pane), move: true); + case ExplorerButton.paste: + await _paste(pane, pane.current); case ExplorerButton.delete: await _delete(pane, _targets(pane)); case ExplorerButton.search: @@ -623,17 +637,50 @@ class _ExplorerPageState extends State { } /// 다른 창의 지금 폴더로 복사 · 이동 (진행 창 · 취소) + /// 복사 · 이동 버튼: 두 창이면 다른 창의 지금 폴더로, 그 밖의 배치는 담아 두기 (붙여넣기로) Future _transfer(_Pane pane, List sources, {required bool move}) async { final other = _other; if (other == null) { - _snack(tr('복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).')); + _toClipboard(sources, move: move); return; } + await _runTransfer(pane, sources, other.current, move: move); + } + + /// 복사 · 이동할 항목 (붙여넣기 전까지 담아 둠) + List _clip = []; + bool _clipMove = false; + + void _toClipboard(List sources, {required bool move}) { if (sources.isEmpty) { _snack(tr('복사 · 이동할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).')); return; } - final dest = other.current; + setState(() { + _clip = [...sources]; + _clipMove = move; + }); + _snack(trf('{0}개 항목을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).', + [sources.length, move ? tr('이동') : tr('복사')])); + } + + Future _paste(_Pane pane, String dest) async { + if (_clip.isEmpty) { + _snack(tr('붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.')); + return; + } + final items = _clip, move = _clipMove; + final done = await _runTransfer(pane, items, dest, move: move); + // 옮긴 것은 다시 붙여넣을 수 없으므로 비운다 (복사는 여러 곳에 붙여넣을 수 있게 남김) + if (done && move && mounted) setState(() => _clip = []); + } + + /// [dest] 로 복사 · 이동 (확인 · 진행 창 · 취소). 끝까지 했으면 true. + Future _runTransfer(_Pane pane, List sources, String dest, {required bool move}) async { + if (sources.isEmpty) { + _snack(tr('복사 · 이동할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).')); + return false; + } final go = await showDialog( context: context, builder: (ctx) => AlertDialog( @@ -645,7 +692,7 @@ class _ExplorerPageState extends State { ], ), ); - if (go != true || !mounted) return; + if (go != true || !mounted) return false; final ops = FileOps(); final progress = ValueNotifier<(String, double)>(('', 0)); showDialog( @@ -679,11 +726,18 @@ class _ExplorerPageState extends State { if (mounted) Navigator.of(context).pop(); pane.marked.clear(); await _refreshAll([dest, ...sources.map(p.dirname)]); - other.expanded.add(dest); - other.changed(); + // 넣은 폴더가 창의 지금 폴더면 펼쳐서 새 항목이 보이게 + for (final x in _panes) { + if (samePath(x.current, dest)) { + x.expanded.add(dest); + await _load(x, dest, force: true); + } + x.changed(); + } _snack(error != null ? trf('끝나지 못했습니다: {0}', [error]) : trf('{0}개 항목을 {1}', [made.length, move ? tr('옮겼습니다.') : tr('복사했습니다.')])); + return error == null; } Future _askName(String title, String initial) { @@ -1020,7 +1074,7 @@ class _ExplorerPageState extends State { vertical: side, onPressed: _button, enabled: (b) => switch (b) { - ExplorerButton.copy || ExplorerButton.move => dual, + ExplorerButton.paste => _clip.isNotEmpty, _ => true, }, selected: (b) => b == ExplorerButton.hidden && s.explorerShowHidden, diff --git a/test/explorer_test.dart b/test/explorer_test.dart index e718213..7fb53ef 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -134,15 +134,30 @@ void main() { await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); expect(find.text('inner.txt'), findsOneWidget); - // Total Commander: [폴더] · · 확장자 칸 + // 이 배치에도 복사 · 이동: 길게 눌러 [복사] 로 담고 → 다른 폴더를 골라 [붙여넣기] + await tester.longPress(find.text('inner.txt')); + await tester.pumpAndSettle(); + expect(find.text('다른 창으로 복사'), findsNothing); + await tester.tap(find.text('복사').last); + await tester.pumpAndSettle(); await act(tester, () => tester.tap(find.text('..'))); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); + await act(tester, () => tester.tap(find.text('붙여넣기'))); + await tester.pumpAndSettle(); + await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사'))); + await settle(tester, () => File(p.join(left, 'inner.txt')).existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty); + expect(File(p.join(left, 'inner.txt')).readAsStringSync(), 'inner'); + expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue); // 복사이므로 원본은 그대로 + await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); + expect(find.text('inner.txt'), findsOneWidget); // 목록에 바로 보임 + + // Total Commander: [폴더] · · 확장자 칸 await c.updateSettings((s) => s.explorerStyle = 'totalcmd'); await settle(tester, () => find.text('[sub]').evaluate().length == 2); expect(find.text('[sub]'), findsNWidgets(2)); expect(find.text(''), findsOneWidget); expect(find.text('doc'), findsOneWidget); // 확장자를 뺀 이름 - expect(find.text('txt'), findsOneWidget); // 확장자 칸 + expect(find.text('txt'), findsNWidgets(2)); // 확장자 칸 (doc.txt · 붙여넣은 inner.txt) expect(find.text('확장자'), findsOneWidget); }); @@ -180,7 +195,9 @@ void main() { testWidgets('창 배치: 한 창 · 버튼 줄 숨김, 버튼 구성: 버튼 빼기 → 설정에 저장', (tester) async { await open(tester); - await tester.tap(find.text('창 배치').first); + await tester.tap(find.byTooltip('창 배치 · 버튼 구성')); + await tester.pumpAndSettle(); + await tester.tap(find.text('창 배치').last); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(ChoiceChip, '한 창')); await tester.pump();