WIP 82 파일 탐색기 창 사이 끌어 놓기: 마우스 8px 끌기 · 터치 ≡ 손잡이, 놓을 곳 강조, 묻기 (복사 · 이동), 이동 [되돌리기] (검토 대기)
- 관리자 지시 (F11) · 판단: 창 사이 끌어 놓기로 복사 · 이동 (태블릿 · PC). 미리 보기 · 확인 · 되돌리기. - 끌기: PC 마우스는 줄을 8px 넘게 움직였을 때만 (한 번 누르기 = 고르기 · 두 번 = 열기는 그대로). 터치는 선택 모드에서 표시한 줄의 ≡ 손잡이 (길게 누르기 메뉴는 그대로). 누른 줄이 표시한 것이면 표시한 것 모두 (바깥 것만). - 놓기: 다른 창의 폴더 줄 = 그 폴더 안, 창의 빈 곳 = 그 창의 지금 폴더. 놓을 수 있으면 강조, 못 놓는 곳 (같은 창 · 자기 안 · 원본 폴더 자신) 은 빨간 테두리이고 놓아도 아무 일 없음. WebDAV 창도 지금의 복사 · 이동으로. - 설정 "끌어 놓기 기본": 묻기 (처음 값) / 복사 / 이동. 묻기 창은 [복사] 가 처음 고름 (Enter) · [이동] · [취소]. 어느 쪽이든 지금의 복사 · 이동 확인 창 (같은 이름 등) · 진행 패널을 그대로 탄다. - 이동만 끝난 알림에 [되돌리기] (10초): 실제로 옮긴 것만 원래 자리로 (원래 폴더마다 지금의 이동 - 같은 이름이면 지금의 같은 이름 창). 대상에 새로 생긴 항목은 통째로, 이미 있던 폴더에 합친 것은 앱이 옮긴 파일만 (TransferJob.movedFiles) - 그 폴더 전체를 옮기지 않는다. robocopy 로 합친 폴더는 무엇을 옮겼는지 몰라 되돌리지 않고 알린다. 복사 되돌리기는 없음. - 내 판단: 끝난 알림 (n개 옮김) 은 [되돌리기] 알림으로 바로 바꾼다 (진행 막대가 내려가기를 기다리지 않게). - 시험: explorer_test 82 (놓는 곳 규칙, 4px 는 누르기, 같은 창 · 묻기 [취소] 는 아무 일 없음, 이동 → 되돌리기 - 합친 폴더의 원래 파일은 그대로 · 옮긴 파일만 돌아옴). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음6
parent
b1f20914a0
commit
39dab23138
|
|
@ -474,6 +474,7 @@
|
||||||
"{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: moves the {1} checked videos to {2}",
|
"{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: moves the {1} checked videos to {2}",
|
||||||
"{0}개": "{0}",
|
"{0}개": "{0}",
|
||||||
"{0}개 · {1}": "{0} · {1}",
|
"{0}개 · {1}": "{0} · {1}",
|
||||||
|
"{0}개 · {1} …": "{0} items · {1} …",
|
||||||
"{0}개 · 주소 {1}개": "{0} · {1} links",
|
"{0}개 · 주소 {1}개": "{0} · {1} links",
|
||||||
"{0}개 더 보기": "Show {0} more",
|
"{0}개 더 보기": "Show {0} more",
|
||||||
"{0}개 맞춤": "{0} synced",
|
"{0}개 맞춤": "{0} synced",
|
||||||
|
|
@ -488,6 +489,7 @@
|
||||||
"{0}개 풀기": "Extract {0}",
|
"{0}개 풀기": "Extract {0}",
|
||||||
"{0}개 항목": "{0} items",
|
"{0}개 항목": "{0} items",
|
||||||
"{0}개 항목을 {1}": "{0} items {1}",
|
"{0}개 항목을 {1}": "{0} items {1}",
|
||||||
|
"{0}개 항목을 놓았습니다": "Dropped {0} items",
|
||||||
"{0}개 항목을 다음 폴더로 {1}\n{2}": "{0} items to this folder — {1}\n{2}",
|
"{0}개 항목을 다음 폴더로 {1}\n{2}": "{0} items to this folder — {1}\n{2}",
|
||||||
"{0}개 항목을 다음 폴더로 복사할까요?\n{1}": "Copy {0} items to this folder?\n{1}",
|
"{0}개 항목을 다음 폴더로 복사할까요?\n{1}": "Copy {0} items to this folder?\n{1}",
|
||||||
"{0}개 항목을 다음 폴더로 옮길까요?\n{1}": "Move {0} items to this folder?\n{1}",
|
"{0}개 항목을 다음 폴더로 옮길까요?\n{1}": "Move {0} items to this folder?\n{1}",
|
||||||
|
|
@ -504,6 +506,7 @@
|
||||||
"{0}개를 영구 삭제했습니다.": "Permanently deleted {0} item(s).",
|
"{0}개를 영구 삭제했습니다.": "Permanently deleted {0} item(s).",
|
||||||
"{0}개를 옮겼습니다 → {1}": "Moved {0} → {1}",
|
"{0}개를 옮겼습니다 → {1}": "Moved {0} → {1}",
|
||||||
"{0}개를 옮겼습니다 → {1} ({2})": "Moved {0} → {1} ({2})",
|
"{0}개를 옮겼습니다 → {1} ({2})": "Moved {0} → {1} ({2})",
|
||||||
|
"{0}개를 옮겼습니다.": "Moved {0} items.",
|
||||||
"{0}개를 찾아 여기서 멈췄습니다. 이름을 더 자세히 넣거나 아래 폴더로 들어가 찾으세요.": "Stopped after finding {0}. Enter a more specific name or search inside a subfolder.",
|
"{0}개를 찾아 여기서 멈췄습니다. 이름을 더 자세히 넣거나 아래 폴더로 들어가 찾으세요.": "Stopped after finding {0}. Enter a more specific name or search inside a subfolder.",
|
||||||
"{0}개를 풀었습니다: {1}": "Extracted {0}: {1}",
|
"{0}개를 풀었습니다: {1}": "Extracted {0}: {1}",
|
||||||
"{0}개에서 멈췄습니다": "Stopped at {0}",
|
"{0}개에서 멈췄습니다": "Stopped at {0}",
|
||||||
|
|
@ -712,7 +715,9 @@
|
||||||
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). Chrome 엔진은 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "When off, page scripts do not run (fewer ads · trackers, but script-built sites such as YouTube will not show). With the Chrome engine, page translation · video detection also stop.",
|
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). Chrome 엔진은 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "When off, page scripts do not run (fewer ads · trackers, but script-built sites such as YouTube will not show). With the Chrome engine, page translation · video detection also stop.",
|
||||||
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "When off, page scripts do not run (fewer ads · trackers, but script-built sites such as YouTube will not show). Page translation · video detection also stop.",
|
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "When off, page scripts do not run (fewer ads · trackers, but script-built sites such as YouTube will not show). Page translation · video detection also stop.",
|
||||||
"끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다": "When off, the web page keeps playing sound while you look at settings, the download list or other screens",
|
"끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다": "When off, the web page keeps playing sound while you look at settings, the download list or other screens",
|
||||||
|
"끌어 놓기 기본": "Drag and drop default",
|
||||||
"끌어다 놓은 항목에 동영상이 없습니다.": "There are no videos in the dropped items.",
|
"끌어다 놓은 항목에 동영상이 없습니다.": "There are no videos in the dropped items.",
|
||||||
|
"끌어서 다른 창으로": "Drag to the other pane",
|
||||||
"끝": "End",
|
"끝": "End",
|
||||||
"끝 (오른쪽 · 아래)": "Edge (right · bottom)",
|
"끝 (오른쪽 · 아래)": "Edge (right · bottom)",
|
||||||
"끝 = 현재": "End = now",
|
"끝 = 현재": "End = now",
|
||||||
|
|
@ -810,6 +815,7 @@
|
||||||
"다른 저장소로는 이름만 바꿀 수 없습니다": "Cannot just rename onto another storage",
|
"다른 저장소로는 이름만 바꿀 수 없습니다": "Cannot just rename onto another storage",
|
||||||
"다른 창 폴더로": "To the other pane's folder",
|
"다른 창 폴더로": "To the other pane's folder",
|
||||||
"다른 창에서 열기": "Open in other pane",
|
"다른 창에서 열기": "Open in other pane",
|
||||||
|
"다른 창으로 끌어 놓았을 때. 어느 쪽이든 복사 · 이동 확인 창 (같은 이름 등) 은 그대로 뜹니다": "When you drop onto the other pane. Either way, the copy/move confirmation (same names etc.) still appears",
|
||||||
"다른 창으로 복사": "Copy to other pane",
|
"다른 창으로 복사": "Copy to other pane",
|
||||||
"다른 창으로 이동": "Move to other pane",
|
"다른 창으로 이동": "Move to other pane",
|
||||||
"다른 폴더로 이동": "Move to another folder",
|
"다른 폴더로 이동": "Move to another folder",
|
||||||
|
|
@ -1074,6 +1080,7 @@
|
||||||
"문자셋 경고": "Charset warning",
|
"문자셋 경고": "Charset warning",
|
||||||
"문제 {0}개 - 고치기 전에는 바꾸지 않습니다": "{0} problems - nothing is renamed until they're fixed",
|
"문제 {0}개 - 고치기 전에는 바꾸지 않습니다": "{0} problems - nothing is renamed until they're fixed",
|
||||||
"묻기": "Ask",
|
"묻기": "Ask",
|
||||||
|
"묻기 (기본)": "Ask (default)",
|
||||||
"물어보지 않기": "Never ask",
|
"물어보지 않기": "Never ask",
|
||||||
"미리 보기": "Quick look",
|
"미리 보기": "Quick look",
|
||||||
"미리 볼 수 없습니다: {0}": "Cannot preview: {0}",
|
"미리 볼 수 없습니다: {0}": "Cannot preview: {0}",
|
||||||
|
|
@ -2353,6 +2360,7 @@
|
||||||
"한국어": "Korean",
|
"한국어": "Korean",
|
||||||
"한글": "Hangul",
|
"한글": "Hangul",
|
||||||
"할 일": "Task",
|
"할 일": "Task",
|
||||||
|
"합친 폴더 {0}개는 무엇을 옮겼는지 몰라 되돌리지 않습니다.": "{0} merged folders can't be undone (what was moved isn't known).",
|
||||||
"항목 {0}/{1}": "Items {0}/{1}",
|
"항목 {0}/{1}": "Items {0}/{1}",
|
||||||
"항목마다 따로 (기본)": "Each item separately (default)",
|
"항목마다 따로 (기본)": "Each item separately (default)",
|
||||||
"해상도 올리기 메모리": "Upscaling memory",
|
"해상도 올리기 메모리": "Upscaling memory",
|
||||||
|
|
|
||||||
|
|
@ -474,6 +474,7 @@
|
||||||
"{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: チェックした動画 {1}個を {2} に移動します",
|
"{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: チェックした動画 {1}個を {2} に移動します",
|
||||||
"{0}개": "{0}個",
|
"{0}개": "{0}個",
|
||||||
"{0}개 · {1}": "{0}個・{1}",
|
"{0}개 · {1}": "{0}個・{1}",
|
||||||
|
"{0}개 · {1} …": "{0} 件 · {1} …",
|
||||||
"{0}개 · 주소 {1}개": "{0}個・URL {1}個",
|
"{0}개 · 주소 {1}개": "{0}個・URL {1}個",
|
||||||
"{0}개 더 보기": "さらに {0} 件表示",
|
"{0}개 더 보기": "さらに {0} 件表示",
|
||||||
"{0}개 맞춤": "{0} 件を同期",
|
"{0}개 맞춤": "{0} 件を同期",
|
||||||
|
|
@ -488,6 +489,7 @@
|
||||||
"{0}개 풀기": "{0} 個を展開",
|
"{0}개 풀기": "{0} 個を展開",
|
||||||
"{0}개 항목": "{0} 項目",
|
"{0}개 항목": "{0} 項目",
|
||||||
"{0}개 항목을 {1}": "{0} 件の項目を{1}",
|
"{0}개 항목을 {1}": "{0} 件の項目を{1}",
|
||||||
|
"{0}개 항목을 놓았습니다": "{0} 件の項目をドロップしました",
|
||||||
"{0}개 항목을 다음 폴더로 {1}\n{2}": "{0} 件の項目を次のフォルダーへ{1}\n{2}",
|
"{0}개 항목을 다음 폴더로 {1}\n{2}": "{0} 件の項目を次のフォルダーへ{1}\n{2}",
|
||||||
"{0}개 항목을 다음 폴더로 복사할까요?\n{1}": "{0} 件の項目を次のフォルダーへコピーしますか?\n{1}",
|
"{0}개 항목을 다음 폴더로 복사할까요?\n{1}": "{0} 件の項目を次のフォルダーへコピーしますか?\n{1}",
|
||||||
"{0}개 항목을 다음 폴더로 옮길까요?\n{1}": "{0} 件の項目を次のフォルダーへ移動しますか?\n{1}",
|
"{0}개 항목을 다음 폴더로 옮길까요?\n{1}": "{0} 件の項目を次のフォルダーへ移動しますか?\n{1}",
|
||||||
|
|
@ -504,6 +506,7 @@
|
||||||
"{0}개를 영구 삭제했습니다.": "{0} 件を完全に削除しました。",
|
"{0}개를 영구 삭제했습니다.": "{0} 件を完全に削除しました。",
|
||||||
"{0}개를 옮겼습니다 → {1}": "{0}個を移動しました → {1}",
|
"{0}개를 옮겼습니다 → {1}": "{0}個を移動しました → {1}",
|
||||||
"{0}개를 옮겼습니다 → {1} ({2})": "{0}個を移動しました → {1}({2})",
|
"{0}개를 옮겼습니다 → {1} ({2})": "{0}個を移動しました → {1}({2})",
|
||||||
|
"{0}개를 옮겼습니다.": "{0} 件を移動しました。",
|
||||||
"{0}개를 찾아 여기서 멈췄습니다. 이름을 더 자세히 넣거나 아래 폴더로 들어가 찾으세요.": "{0} 件見つけたところで止めました。名前をより詳しく入力するか、下のフォルダーに入って探してください。",
|
"{0}개를 찾아 여기서 멈췄습니다. 이름을 더 자세히 넣거나 아래 폴더로 들어가 찾으세요.": "{0} 件見つけたところで止めました。名前をより詳しく入力するか、下のフォルダーに入って探してください。",
|
||||||
"{0}개를 풀었습니다: {1}": "{0} 個を展開しました: {1}",
|
"{0}개를 풀었습니다: {1}": "{0} 個を展開しました: {1}",
|
||||||
"{0}개에서 멈췄습니다": "{0} 件で停止しました",
|
"{0}개에서 멈췄습니다": "{0} 件で停止しました",
|
||||||
|
|
@ -712,7 +715,9 @@
|
||||||
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). Chrome 엔진은 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "オフにするとページのスクリプトを実行しません (広告 · 追跡は減りますが、YouTube のようにスクリプトで作られたサイトは表示されません)。Chrome エンジンではページ翻訳 · 動画の検出も止まります。",
|
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). Chrome 엔진은 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "オフにするとページのスクリプトを実行しません (広告 · 追跡は減りますが、YouTube のようにスクリプトで作られたサイトは表示されません)。Chrome エンジンではページ翻訳 · 動画の検出も止まります。",
|
||||||
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "オフにするとページのスクリプトを実行しません (広告 · 追跡は減りますが、YouTube のようにスクリプトで作られたサイトは表示されません)。ページ翻訳 · 動画の検出も止まります。",
|
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "オフにするとページのスクリプトを実行しません (広告 · 追跡は減りますが、YouTube のようにスクリプトで作られたサイトは表示されません)。ページ翻訳 · 動画の検出も止まります。",
|
||||||
"끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다": "オフにすると、環境設定・ダウンロード一覧・他の画面を見ている間も Web ページの音が流れ続けます",
|
"끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다": "オフにすると、環境設定・ダウンロード一覧・他の画面を見ている間も Web ページの音が流れ続けます",
|
||||||
|
"끌어 놓기 기본": "ドラッグ&ドロップの既定",
|
||||||
"끌어다 놓은 항목에 동영상이 없습니다.": "ドロップした項目に動画がありません。",
|
"끌어다 놓은 항목에 동영상이 없습니다.": "ドロップした項目に動画がありません。",
|
||||||
|
"끌어서 다른 창으로": "ドラッグしてもう一方のウィンドウへ",
|
||||||
"끝": "終了",
|
"끝": "終了",
|
||||||
"끝 (오른쪽 · 아래)": "端 (右 · 下)",
|
"끝 (오른쪽 · 아래)": "端 (右 · 下)",
|
||||||
"끝 = 현재": "終了 = 現在",
|
"끝 = 현재": "終了 = 現在",
|
||||||
|
|
@ -810,6 +815,7 @@
|
||||||
"다른 저장소로는 이름만 바꿀 수 없습니다": "別のストレージへは名前の変更だけではできません",
|
"다른 저장소로는 이름만 바꿀 수 없습니다": "別のストレージへは名前の変更だけではできません",
|
||||||
"다른 창 폴더로": "もう一方の画面のフォルダーへ",
|
"다른 창 폴더로": "もう一方の画面のフォルダーへ",
|
||||||
"다른 창에서 열기": "もう一方のペインで開く",
|
"다른 창에서 열기": "もう一方のペインで開く",
|
||||||
|
"다른 창으로 끌어 놓았을 때. 어느 쪽이든 복사 · 이동 확인 창 (같은 이름 등) 은 그대로 뜹니다": "もう一方のウィンドウにドロップしたとき。どちらでもコピー・移動の確認 (同じ名前など) は表示されます",
|
||||||
"다른 창으로 복사": "もう一方のペインへコピー",
|
"다른 창으로 복사": "もう一方のペインへコピー",
|
||||||
"다른 창으로 이동": "もう一方のペインへ移動",
|
"다른 창으로 이동": "もう一方のペインへ移動",
|
||||||
"다른 폴더로 이동": "他のフォルダーに移動",
|
"다른 폴더로 이동": "他のフォルダーに移動",
|
||||||
|
|
@ -1074,6 +1080,7 @@
|
||||||
"문자셋 경고": "文字コードの警告",
|
"문자셋 경고": "文字コードの警告",
|
||||||
"문제 {0}개 - 고치기 전에는 바꾸지 않습니다": "問題 {0} 件 - 直すまで変更しません",
|
"문제 {0}개 - 고치기 전에는 바꾸지 않습니다": "問題 {0} 件 - 直すまで変更しません",
|
||||||
"묻기": "確認する",
|
"묻기": "確認する",
|
||||||
|
"묻기 (기본)": "確認する (既定)",
|
||||||
"물어보지 않기": "尋ねない",
|
"물어보지 않기": "尋ねない",
|
||||||
"미리 보기": "クイックプレビュー",
|
"미리 보기": "クイックプレビュー",
|
||||||
"미리 볼 수 없습니다: {0}": "プレビューできません: {0}",
|
"미리 볼 수 없습니다: {0}": "プレビューできません: {0}",
|
||||||
|
|
@ -2353,6 +2360,7 @@
|
||||||
"한국어": "韓国語",
|
"한국어": "韓国語",
|
||||||
"한글": "ハングル",
|
"한글": "ハングル",
|
||||||
"할 일": "内容",
|
"할 일": "内容",
|
||||||
|
"합친 폴더 {0}개는 무엇을 옮겼는지 몰라 되돌리지 않습니다.": "結合したフォルダー {0} 件は、何を移動したか分からないため元に戻しません。",
|
||||||
"항목 {0}/{1}": "項目 {0}/{1}",
|
"항목 {0}/{1}": "項目 {0}/{1}",
|
||||||
"항목마다 따로 (기본)": "項目ごとに (既定)",
|
"항목마다 따로 (기본)": "項目ごとに (既定)",
|
||||||
"해상도 올리기 메모리": "高解像度化のメモリ",
|
"해상도 올리기 메모리": "高解像度化のメモリ",
|
||||||
|
|
|
||||||
|
|
@ -474,6 +474,7 @@
|
||||||
"{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: 将勾选的 {1} 个视频移到 {2}",
|
"{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: 将勾选的 {1} 个视频移到 {2}",
|
||||||
"{0}개": "{0} 个",
|
"{0}개": "{0} 个",
|
||||||
"{0}개 · {1}": "{0} 个 · {1}",
|
"{0}개 · {1}": "{0} 个 · {1}",
|
||||||
|
"{0}개 · {1} …": "{0} 项 · {1} …",
|
||||||
"{0}개 · 주소 {1}개": "{0} 个 · 地址 {1} 个",
|
"{0}개 · 주소 {1}개": "{0} 个 · 地址 {1} 个",
|
||||||
"{0}개 더 보기": "再显示 {0} 个",
|
"{0}개 더 보기": "再显示 {0} 个",
|
||||||
"{0}개 맞춤": "已同步 {0} 个",
|
"{0}개 맞춤": "已同步 {0} 个",
|
||||||
|
|
@ -488,6 +489,7 @@
|
||||||
"{0}개 풀기": "解压 {0} 个",
|
"{0}개 풀기": "解压 {0} 个",
|
||||||
"{0}개 항목": "{0} 个项目",
|
"{0}개 항목": "{0} 个项目",
|
||||||
"{0}개 항목을 {1}": "{0} 个项目{1}",
|
"{0}개 항목을 {1}": "{0} 个项目{1}",
|
||||||
|
"{0}개 항목을 놓았습니다": "已放下 {0} 项",
|
||||||
"{0}개 항목을 다음 폴더로 {1}\n{2}": "将 {0} 个项目到以下文件夹 — {1}\n{2}",
|
"{0}개 항목을 다음 폴더로 {1}\n{2}": "将 {0} 个项目到以下文件夹 — {1}\n{2}",
|
||||||
"{0}개 항목을 다음 폴더로 복사할까요?\n{1}": "要将 {0} 个项目复制到以下文件夹吗?\n{1}",
|
"{0}개 항목을 다음 폴더로 복사할까요?\n{1}": "要将 {0} 个项目复制到以下文件夹吗?\n{1}",
|
||||||
"{0}개 항목을 다음 폴더로 옮길까요?\n{1}": "要将 {0} 个项目移动到以下文件夹吗?\n{1}",
|
"{0}개 항목을 다음 폴더로 옮길까요?\n{1}": "要将 {0} 个项目移动到以下文件夹吗?\n{1}",
|
||||||
|
|
@ -504,6 +506,7 @@
|
||||||
"{0}개를 영구 삭제했습니다.": "已永久删除 {0} 个项目。",
|
"{0}개를 영구 삭제했습니다.": "已永久删除 {0} 个项目。",
|
||||||
"{0}개를 옮겼습니다 → {1}": "已移动 {0} 个 → {1}",
|
"{0}개를 옮겼습니다 → {1}": "已移动 {0} 个 → {1}",
|
||||||
"{0}개를 옮겼습니다 → {1} ({2})": "已移动 {0} 个 → {1}({2})",
|
"{0}개를 옮겼습니다 → {1} ({2})": "已移动 {0} 个 → {1}({2})",
|
||||||
|
"{0}개를 옮겼습니다.": "已移动 {0} 项。",
|
||||||
"{0}개를 찾아 여기서 멈췄습니다. 이름을 더 자세히 넣거나 아래 폴더로 들어가 찾으세요.": "找到 {0} 个后已停止。请输入更具体的名称或进入下级文件夹查找。",
|
"{0}개를 찾아 여기서 멈췄습니다. 이름을 더 자세히 넣거나 아래 폴더로 들어가 찾으세요.": "找到 {0} 个后已停止。请输入更具体的名称或进入下级文件夹查找。",
|
||||||
"{0}개를 풀었습니다: {1}": "已解压 {0} 个: {1}",
|
"{0}개를 풀었습니다: {1}": "已解压 {0} 个: {1}",
|
||||||
"{0}개에서 멈췄습니다": "已在 {0} 个处暂停",
|
"{0}개에서 멈췄습니다": "已在 {0} 个处暂停",
|
||||||
|
|
@ -712,7 +715,9 @@
|
||||||
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). Chrome 엔진은 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "关闭后不运行页面脚本 (广告 · 跟踪会减少,但 YouTube 等由脚本构建的网站将无法显示)。使用 Chrome 引擎时,页面翻译 · 视频检测也会停止。",
|
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). Chrome 엔진은 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "关闭后不运行页面脚本 (广告 · 跟踪会减少,但 YouTube 等由脚本构建的网站将无法显示)。使用 Chrome 引擎时,页面翻译 · 视频检测也会停止。",
|
||||||
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "关闭后不运行页面脚本 (广告 · 跟踪会减少,但 YouTube 等由脚本构建的网站将无法显示)。页面翻译 · 视频检测也会停止。",
|
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "关闭后不运行页面脚本 (广告 · 跟踪会减少,但 YouTube 等由脚本构建的网站将无法显示)。页面翻译 · 视频检测也会停止。",
|
||||||
"끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다": "关闭后,在查看设置、下载列表或其他界面时网页声音会继续播放",
|
"끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다": "关闭后,在查看设置、下载列表或其他界面时网页声音会继续播放",
|
||||||
|
"끌어 놓기 기본": "拖放默认操作",
|
||||||
"끌어다 놓은 항목에 동영상이 없습니다.": "拖入的项目中没有视频。",
|
"끌어다 놓은 항목에 동영상이 없습니다.": "拖入的项目中没有视频。",
|
||||||
|
"끌어서 다른 창으로": "拖到另一个窗格",
|
||||||
"끝": "结束",
|
"끝": "结束",
|
||||||
"끝 (오른쪽 · 아래)": "边缘 (右 · 下)",
|
"끝 (오른쪽 · 아래)": "边缘 (右 · 下)",
|
||||||
"끝 = 현재": "结束 = 当前",
|
"끝 = 현재": "结束 = 当前",
|
||||||
|
|
@ -810,6 +815,7 @@
|
||||||
"다른 저장소로는 이름만 바꿀 수 없습니다": "不能仅通过重命名移到其他存储",
|
"다른 저장소로는 이름만 바꿀 수 없습니다": "不能仅通过重命名移到其他存储",
|
||||||
"다른 창 폴더로": "到另一窗格的文件夹",
|
"다른 창 폴더로": "到另一窗格的文件夹",
|
||||||
"다른 창에서 열기": "在另一窗格中打开",
|
"다른 창에서 열기": "在另一窗格中打开",
|
||||||
|
"다른 창으로 끌어 놓았을 때. 어느 쪽이든 복사 · 이동 확인 창 (같은 이름 등) 은 그대로 뜹니다": "拖放到另一个窗格时。无论哪种,都会显示复制/移动确认 (同名等)",
|
||||||
"다른 창으로 복사": "复制到另一窗格",
|
"다른 창으로 복사": "复制到另一窗格",
|
||||||
"다른 창으로 이동": "移动到另一窗格",
|
"다른 창으로 이동": "移动到另一窗格",
|
||||||
"다른 폴더로 이동": "移动到其他文件夹",
|
"다른 폴더로 이동": "移动到其他文件夹",
|
||||||
|
|
@ -1074,6 +1080,7 @@
|
||||||
"문자셋 경고": "字符集警告",
|
"문자셋 경고": "字符集警告",
|
||||||
"문제 {0}개 - 고치기 전에는 바꾸지 않습니다": "{0} 个问题 - 修正前不会重命名",
|
"문제 {0}개 - 고치기 전에는 바꾸지 않습니다": "{0} 个问题 - 修正前不会重命名",
|
||||||
"묻기": "询问",
|
"묻기": "询问",
|
||||||
|
"묻기 (기본)": "询问 (默认)",
|
||||||
"물어보지 않기": "不询问",
|
"물어보지 않기": "不询问",
|
||||||
"미리 보기": "快速预览",
|
"미리 보기": "快速预览",
|
||||||
"미리 볼 수 없습니다: {0}": "无法预览: {0}",
|
"미리 볼 수 없습니다: {0}": "无法预览: {0}",
|
||||||
|
|
@ -2353,6 +2360,7 @@
|
||||||
"한국어": "韩语",
|
"한국어": "韩语",
|
||||||
"한글": "韩文",
|
"한글": "韩文",
|
||||||
"할 일": "任务",
|
"할 일": "任务",
|
||||||
|
"합친 폴더 {0}개는 무엇을 옮겼는지 몰라 되돌리지 않습니다.": "{0} 个合并的文件夹无法撤销 (不清楚移动了哪些内容)。",
|
||||||
"항목 {0}/{1}": "项目 {0}/{1}",
|
"항목 {0}/{1}": "项目 {0}/{1}",
|
||||||
"항목마다 따로 (기본)": "逐个项目 (默认)",
|
"항목마다 따로 (기본)": "逐个项目 (默认)",
|
||||||
"해상도 올리기 메모리": "提升分辨率内存",
|
"해상도 올리기 메모리": "提升分辨率内存",
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,9 @@ class AppSettings {
|
||||||
|
|
||||||
/// 80: 폴더 크기 계산 - 'sort' 크기순 정렬일 때만 (처음 값) · 'always' 늘 · 'off' 안 함 (길게 누르기 [폴더 크기 계산] 은 늘 됨)
|
/// 80: 폴더 크기 계산 - 'sort' 크기순 정렬일 때만 (처음 값) · 'always' 늘 · 'off' 안 함 (길게 누르기 [폴더 크기 계산] 은 늘 됨)
|
||||||
String explorerFolderSize = 'sort';
|
String explorerFolderSize = 'sort';
|
||||||
|
|
||||||
|
/// 82: 창 사이 끌어 놓기 - 'ask' 묻기 (처음 값, 관리자 판단) · 'copy' 복사 · 'move' 이동
|
||||||
|
String explorerDropAction = 'ask';
|
||||||
bool explorerShowHidden = false;
|
bool explorerShowHidden = false;
|
||||||
|
|
||||||
// ── 복사 · 이동 (파일 탐색기) · rsync · lsync (Rsync 화면) ──
|
// ── 복사 · 이동 (파일 탐색기) · rsync · lsync (Rsync 화면) ──
|
||||||
|
|
@ -741,6 +744,7 @@ class AppSettings {
|
||||||
'explorerSort': explorerSort,
|
'explorerSort': explorerSort,
|
||||||
'explorerSortDesc': explorerSortDesc,
|
'explorerSortDesc': explorerSortDesc,
|
||||||
'explorerFolderSize': explorerFolderSize,
|
'explorerFolderSize': explorerFolderSize,
|
||||||
|
'explorerDropAction': explorerDropAction,
|
||||||
'explorerSortPerFolder': explorerSortPerFolder,
|
'explorerSortPerFolder': explorerSortPerFolder,
|
||||||
'explorerFolderSorts': explorerFolderSorts,
|
'explorerFolderSorts': explorerFolderSorts,
|
||||||
'explorerShowHidden': explorerShowHidden,
|
'explorerShowHidden': explorerShowHidden,
|
||||||
|
|
@ -958,6 +962,7 @@ class AppSettings {
|
||||||
..explorerSortDesc = j['explorerSortDesc'] as bool? ?? false
|
..explorerSortDesc = j['explorerSortDesc'] as bool? ?? false
|
||||||
..explorerFolderSize = const ['sort', 'always', 'off'].contains(j['explorerFolderSize']) ? j['explorerFolderSize'] as String : 'sort'
|
..explorerFolderSize = const ['sort', 'always', 'off'].contains(j['explorerFolderSize']) ? j['explorerFolderSize'] as String : 'sort'
|
||||||
..explorerSortPerFolder = j['explorerSortPerFolder'] == true
|
..explorerSortPerFolder = j['explorerSortPerFolder'] == true
|
||||||
|
..explorerDropAction = const ['ask', 'copy', 'move'].contains(j['explorerDropAction']) ? j['explorerDropAction'] as String : 'ask'
|
||||||
..explorerFolderSorts = {
|
..explorerFolderSorts = {
|
||||||
if (j['explorerFolderSorts'] is Map)
|
if (j['explorerFolderSorts'] is Map)
|
||||||
for (final e in (j['explorerFolderSorts'] as Map).entries)
|
for (final e in (j['explorerFolderSorts'] as Map).entries)
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,9 @@ class TransferJob extends ChangeNotifier {
|
||||||
bool cancelled = false;
|
bool cancelled = false;
|
||||||
final made = <String>[];
|
final made = <String>[];
|
||||||
|
|
||||||
|
/// 82: 앱이 하나씩 복사 · 이동한 원본 파일 (폴더를 합칠 때 실제로 옮긴 것만 되돌리려고 - 이름 바꾸기로 통째 옮긴 것은 [made] 만)
|
||||||
|
final movedFiles = <String>[];
|
||||||
|
|
||||||
/// 마지막 출력 몇 줄 (실패했을 때 보여 줌)
|
/// 마지막 출력 몇 줄 (실패했을 때 보여 줌)
|
||||||
final log = <String>[];
|
final log = <String>[];
|
||||||
|
|
||||||
|
|
@ -321,6 +324,7 @@ class TransferJob extends ChangeNotifier {
|
||||||
bandwidthKBps: bandwidthKBps,
|
bandwidthKBps: bandwidthKBps,
|
||||||
onFileDone: (src) {
|
onFileDone: (src) {
|
||||||
filesDone[i]++;
|
filesDone[i]++;
|
||||||
|
movedFiles.add(src);
|
||||||
currentFile = vBasename(src);
|
currentFile = vBasename(src);
|
||||||
_tick();
|
_tick();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../core/file_ops.dart';
|
||||||
|
import '../core/vfs.dart';
|
||||||
|
import '../l10n/tr.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// 82: 창 사이 끌어 놓기로 옮기는 것 - 어느 창 ([from], 창 번호) 의 어떤 항목들
|
||||||
|
class DragLoad {
|
||||||
|
final int from;
|
||||||
|
final List<String> paths;
|
||||||
|
const DragLoad(this.from, this.paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 82: [load] 를 [to] 창의 [dest] 폴더에 놓을 수 있는지 - 같은 창 · 자기 안 · 원본 폴더 자신은 안 됨
|
||||||
|
bool canDropInto(DragLoad load, int to, String dest) {
|
||||||
|
if (load.from == to || load.paths.isEmpty) return false;
|
||||||
|
if (load.paths.any((x) => isSameOrInside(dest, x))) return false;
|
||||||
|
if (load.paths.every((x) => samePath(vDirname(x), dest))) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 82: 마우스로 [slop] (8px) 넘게 움직였을 때만 끌기 - 한 번 누르기 (고르기) · 두 번 누르기 (열기) 가 흔들리지 않게
|
||||||
|
class MouseDraggable<T extends Object> extends Draggable<T> {
|
||||||
|
final double slop;
|
||||||
|
const MouseDraggable({
|
||||||
|
super.key,
|
||||||
|
required super.child,
|
||||||
|
required super.feedback,
|
||||||
|
super.data,
|
||||||
|
super.childWhenDragging,
|
||||||
|
super.onDragStarted,
|
||||||
|
super.onDragEnd,
|
||||||
|
this.slop = 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
MultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) =>
|
||||||
|
_SlopMultiDrag(slop, supportedDevices: const {PointerDeviceKind.mouse})..onStart = onStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SlopMultiDrag extends MultiDragGestureRecognizer {
|
||||||
|
final double slop;
|
||||||
|
_SlopMultiDrag(this.slop, {super.supportedDevices}) : super(debugOwner: null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
MultiDragPointerState createNewPointerState(PointerDownEvent event) => _SlopState(slop, event.position, event.kind, gestureSettings);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get debugDescription => 'explorer drag';
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SlopState extends MultiDragPointerState {
|
||||||
|
final double slop;
|
||||||
|
_SlopState(this.slop, super.initialPosition, super.kind, super.gestureSettings);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void checkForResolutionAfterMove() {
|
||||||
|
if (pendingDelta!.distance > slop) resolve(GestureDisposition.accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void accepted(GestureMultiDragStartCallback starter) => starter(initialPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 82: 끄는 동안 손가락 · 마우스 옆에 보이는 것: "n개 · 첫 이름"
|
||||||
|
Widget dragFeedback(List<String> paths) => Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: JjColors.panelHigh,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(color: JjColors.accent),
|
||||||
|
),
|
||||||
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
const Icon(Icons.drive_file_move_outline, size: 16, color: JjColors.accent),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(paths.length == 1 ? vBasename(paths.first) : trf('{0}개 · {1} …', [paths.length, vBasename(paths.first)]),
|
||||||
|
style: const TextStyle(fontSize: 12, color: JjColors.text)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 82: 놓았을 때 묻기 - 복사 (Enter, 처음 고름) · 이동 · 취소. 취소하면 null
|
||||||
|
Future<bool?> askDropAction(BuildContext context, List<String> paths, String dest) => showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: Text(trf('{0}개 항목을 놓았습니다', [paths.length])),
|
||||||
|
content: Text('${vDisplay(dest)}\n${paths.take(5).map(vBasename).join(', ')}${paths.length > 5 ? ' …' : ''}'),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('취소'))),
|
||||||
|
OutlinedButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('이동'))),
|
||||||
|
FilledButton(autofocus: true, onPressed: () => Navigator.pop(ctx, false), child: Text(tr('복사'))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
@ -45,6 +45,7 @@ import '../l10n/tr.dart';
|
||||||
import 'explorer_bookmarks.dart';
|
import 'explorer_bookmarks.dart';
|
||||||
import 'explorer_search.dart';
|
import 'explorer_search.dart';
|
||||||
import 'quick_look.dart';
|
import 'quick_look.dart';
|
||||||
|
import 'explorer_drag.dart';
|
||||||
import 'batch_rename_dialog.dart';
|
import 'batch_rename_dialog.dart';
|
||||||
import '../core/folder_size.dart';
|
import '../core/folder_size.dart';
|
||||||
import '../core/batch_rename.dart';
|
import '../core/batch_rename.dart';
|
||||||
|
|
@ -2127,7 +2128,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [dest] 로 복사 · 이동 (확인 · 아래 진행 막대 · 취소). 끝까지 했으면 true.
|
/// [dest] 로 복사 · 이동 (확인 · 아래 진행 막대 · 취소). 끝까지 했으면 true.
|
||||||
Future<bool> _runTransfer(_Pane pane, List<String> sources, String dest, {required bool move}) async {
|
Future<bool> _runTransfer(_Pane pane, List<String> sources, String dest,
|
||||||
|
{required bool move, void Function(TransferJob job)? onFinished}) async {
|
||||||
if (sources.isEmpty) {
|
if (sources.isEmpty) {
|
||||||
_snack(_pickFirst(move));
|
_snack(_pickFirst(move));
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -2330,6 +2332,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
if (job.skipped.isNotEmpty) trf('같은 이름이라 건너뜀: {0}', [_namesOf(job.skipped)]),
|
if (job.skipped.isNotEmpty) trf('같은 이름이라 건너뜀: {0}', [_namesOf(job.skipped)]),
|
||||||
if (skippedHere.isNotEmpty) trf('이미 이 폴더에 있어 건너뜀: {0}', [_namesOf(skippedHere)]),
|
if (skippedHere.isNotEmpty) trf('이미 이 폴더에 있어 건너뜀: {0}', [_namesOf(skippedHere)]),
|
||||||
].join(' · '));
|
].join(' · '));
|
||||||
|
// 82: 끌어 놓기의 이동 되돌리기 (끝난 알림 바로 뒤에 - 진행 막대가 내려가기를 기다리지 않게)
|
||||||
|
if (error == null && mounted) onFinished?.call(job);
|
||||||
// 다 되면 진행 막대가 잠깐 100% 를 보인 뒤 내려간다
|
// 다 되면 진행 막대가 잠깐 100% 를 보인 뒤 내려간다
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 1200));
|
await Future<void>.delayed(const Duration(milliseconds: 1200));
|
||||||
if (mounted && _jobs.contains(job)) setState(() => _jobs = [for (final j in _jobs) if (j != job) j]);
|
if (mounted && _jobs.contains(job)) setState(() => _jobs = [for (final j in _jobs) if (j != job) j]);
|
||||||
|
|
@ -3401,19 +3405,113 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
onTapDown: (_) {
|
onTapDown: (_) {
|
||||||
if (_active != i) setState(() => _active = i);
|
if (_active != i) setState(() => _active = i);
|
||||||
},
|
},
|
||||||
child: Container(
|
// 82: 창의 빈 곳에 놓으면 그 창의 지금 폴더로. 놓을 수 있으면 강조, 못 놓으면 (같은 창 등) 빨간 테두리
|
||||||
|
child: DragTarget<DragLoad>(
|
||||||
|
onWillAcceptWithDetails: (d) => canDropInto(d.data, i, _panes[i].current),
|
||||||
|
onAcceptWithDetails: (d) => unawaited(_onDrop(d.data, i, _panes[i].current)),
|
||||||
|
builder: (context, ok, no) => Container(
|
||||||
margin: const EdgeInsets.all(4),
|
margin: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: JjColors.bg,
|
color: ok.isNotEmpty ? JjColors.accent.withValues(alpha: 0.06) : JjColors.bg,
|
||||||
border: Border.all(color: active ? JjColors.accent : JjColors.border, width: active ? 1.5 : 1),
|
border: Border.all(
|
||||||
|
color: ok.isNotEmpty
|
||||||
|
? JjColors.accent
|
||||||
|
: no.isNotEmpty
|
||||||
|
? JjColors.danger
|
||||||
|
: active
|
||||||
|
? JjColors.accent
|
||||||
|
: JjColors.border,
|
||||||
|
width: ok.isNotEmpty || no.isNotEmpty ? 2 : (active ? 1.5 : 1)),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Column(children: children),
|
child: Column(children: children),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 82: 끌 항목 - 누른 것이 표시한 것 중 하나면 표시한 것 모두 (바깥 것만), 아니면 그것 하나
|
||||||
|
List<String> _dragPaths(_Pane pane, FileEntry e) =>
|
||||||
|
_isMarked(pane, e.path) && pane.marked.isNotEmpty ? outermostPaths(pane.marked) : [e.path];
|
||||||
|
|
||||||
|
/// 82: 놓음 - 설정 "끌어 놓기 기본" (묻기 · 복사 · 이동) 뒤 지금의 복사 · 이동 (확인 창 · 같은 이름 · 진행 패널).
|
||||||
|
/// 이동이 끝나면 아래 알림에 [되돌리기] (10초)
|
||||||
|
Future<void> _onDrop(DragLoad load, int to, String dest) async {
|
||||||
|
if (!canDropInto(load, to, dest) || !mounted) return;
|
||||||
|
setState(() => _active = to);
|
||||||
|
final mode = c.settings.explorerDropAction;
|
||||||
|
final move = mode == 'ask' ? await askDropAction(context, load.paths, dest) : mode == 'move';
|
||||||
|
if (move == null || !mounted) return;
|
||||||
|
// 되돌리기에 쓸 것: 대상에 이미 있던 이름 (합친 폴더는 통째로 되돌리지 않게)
|
||||||
|
final existed = <String>{};
|
||||||
|
if (move) {
|
||||||
|
for (final s in load.paths) {
|
||||||
|
final t = vJoin(dest, vBasename(s));
|
||||||
|
if (await vExists(t)) existed.add(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mounted) return;
|
||||||
|
await _runTransfer(_panes[load.from], load.paths, dest,
|
||||||
|
move: move, onFinished: move ? (job) => _offerUndoMove(load, job, existed) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 82: 이동 되돌리기 알림. 실제로 옮긴 것만 원래 자리로: 새로 생긴 항목은 통째로, 이미 있던 폴더에 합친 것은 앱이 옮긴 파일만
|
||||||
|
/// (robocopy 로 합친 폴더는 무엇을 옮겼는지 몰라 되돌리지 않고 알린다)
|
||||||
|
void _offerUndoMove(DragLoad load, TransferJob job, Set<String> existed) {
|
||||||
|
// (원래 폴더 → 지금 경로들)
|
||||||
|
final back = <String, List<String>>{};
|
||||||
|
var notUndoable = 0;
|
||||||
|
final pairs = job.made.length == load.paths.length
|
||||||
|
? [for (var i = 0; i < load.paths.length; i++) (load.paths[i], job.made[i])]
|
||||||
|
: [
|
||||||
|
for (final m in job.made)
|
||||||
|
for (final s in load.paths)
|
||||||
|
if (vBasename(s) == vBasename(m)) (s, m),
|
||||||
|
];
|
||||||
|
for (final (src, now) in pairs) {
|
||||||
|
if (!existed.contains(now)) {
|
||||||
|
back.putIfAbsent(vDirname(src), () => []).add(now);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 합친 폴더: 그 안에서 실제로 옮긴 파일만
|
||||||
|
final files = [for (final f in job.movedFiles) if (isSameOrInside(f, src) && !samePath(f, src)) f];
|
||||||
|
if (files.isEmpty) {
|
||||||
|
notUndoable++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (final f in files) {
|
||||||
|
final rest = f.substring(src.length + 1);
|
||||||
|
back.putIfAbsent(vDirname(f), () => []).add(vJoin(now, rest));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (back.isEmpty || !mounted) return;
|
||||||
|
final moved = back.values.fold(0, (a, l) => a + l.length);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..hideCurrentSnackBar()
|
||||||
|
..showSnackBar(SnackBar(
|
||||||
|
duration: const Duration(seconds: 10),
|
||||||
|
content: Text([
|
||||||
|
trf('{0}개를 옮겼습니다.', [moved]),
|
||||||
|
if (notUndoable > 0) trf('합친 폴더 {0}개는 무엇을 옮겼는지 몰라 되돌리지 않습니다.', [notUndoable]),
|
||||||
|
].join(' ')),
|
||||||
|
action: SnackBarAction(label: tr('되돌리기'), onPressed: () => unawaited(_undoMove(_panes[load.from], back))),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 82: 되돌리기 - 원래 폴더마다 지금의 이동으로 (같은 이름이 생겼으면 지금의 같은 이름 창)
|
||||||
|
Future<void> _undoMove(_Pane pane, Map<String, List<String>> back) async {
|
||||||
|
for (final MapEntry(key: dir, value: items) in back.entries) {
|
||||||
|
final there = [for (final x in items) if (await vExists(x)) x];
|
||||||
|
if (there.isEmpty || !mounted) continue;
|
||||||
|
try {
|
||||||
|
if (!await vExists(dir)) await vMkdirs(dir);
|
||||||
|
} catch (_) {}
|
||||||
|
if (!mounted) return;
|
||||||
|
await _runTransfer(pane, there, dir, move: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 선택 모드 막대: 고른 수 · 모두 고르기 · 선택 끝
|
/// 선택 모드 막대: 고른 수 · 모두 고르기 · 선택 끝
|
||||||
Widget _marksBar(_Pane pane, {String? listDir}) {
|
Widget _marksBar(_Pane pane, {String? listDir}) {
|
||||||
final dir = listDir ?? pane.current;
|
final dir = listDir ?? pane.current;
|
||||||
|
|
@ -4098,7 +4196,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
cell(_date(e.modified), 3),
|
cell(_date(e.modified), 3),
|
||||||
];
|
];
|
||||||
|
|
||||||
return GestureDetector(
|
final paneIndex = _panes.indexOf(pane);
|
||||||
|
final drag = real && !widget.rsync;
|
||||||
|
Widget line = GestureDetector(
|
||||||
// Rsync 화면은 고르기만 (파일 기능 메뉴 없음)
|
// Rsync 화면은 고르기만 (파일 기능 메뉴 없음)
|
||||||
// 25: Rsync 화면에서도 (폴더 메뉴)
|
// 25: Rsync 화면에서도 (폴더 메뉴)
|
||||||
onSecondaryTapUp: real ? (d) => _menu(pane, e, d.globalPosition) : null,
|
onSecondaryTapUp: real ? (d) => _menu(pane, e, d.globalPosition) : null,
|
||||||
|
|
@ -4124,6 +4224,16 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 82: 터치로 끌기 - 선택 모드에서 표시한 줄의 ≡ 손잡이 (길게 누르기는 메뉴 그대로)
|
||||||
|
if (drag && _selecting && marked)
|
||||||
|
Draggable<DragLoad>(
|
||||||
|
data: DragLoad(paneIndex, _dragPaths(pane, e)),
|
||||||
|
feedback: dragFeedback(_dragPaths(pane, e)),
|
||||||
|
child: Tooltip(
|
||||||
|
message: tr('끌어서 다른 창으로'),
|
||||||
|
child: const SizedBox(width: 28, height: 36, child: Icon(Icons.drag_indicator, size: 18, color: JjColors.textDim)),
|
||||||
|
),
|
||||||
|
),
|
||||||
// 표시 동그라미는 두 번 누르기 영역 밖 (기다리지 않고 바로)
|
// 표시 동그라미는 두 번 누르기 영역 밖 (기다리지 않고 바로)
|
||||||
if ((real || (widget.rsync && row.isRoot)) && (_selecting || widget.rsync))
|
if ((real || (widget.rsync && row.isRoot)) && (_selecting || widget.rsync))
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|
@ -4151,6 +4261,27 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (!drag) return line;
|
||||||
|
// 82: 폴더 줄 위에 놓으면 그 폴더 안으로 (놓을 수 있으면 강조)
|
||||||
|
if (e.isDir) {
|
||||||
|
final inner = line;
|
||||||
|
line = DragTarget<DragLoad>(
|
||||||
|
onWillAcceptWithDetails: (d) => canDropInto(d.data, paneIndex, e.path),
|
||||||
|
onAcceptWithDetails: (d) => unawaited(_onDrop(d.data, paneIndex, e.path)),
|
||||||
|
builder: (context, ok, _) => ok.isEmpty
|
||||||
|
? inner
|
||||||
|
: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: JjColors.accent.withValues(alpha: 0.15),
|
||||||
|
border: Border.all(color: JjColors.accent, width: 1.5),
|
||||||
|
),
|
||||||
|
child: inner,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 82: 마우스로 8px 넘게 끌면 시작 (고르기 · 두 번 누르기는 그대로)
|
||||||
|
final paths = _dragPaths(pane, e);
|
||||||
|
return MouseDraggable<DragLoad>(data: DragLoad(paneIndex, paths), feedback: dragFeedback(paths), child: line);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _icon(FileEntry e, bool isRoot, bool open, {bool up = false}) {
|
Widget _icon(FileEntry e, bool isRoot, bool open, {bool up = false}) {
|
||||||
|
|
|
||||||
|
|
@ -767,6 +767,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
onChanged: (v) => c.updateSettings((x) => x.explorerClick = v!),
|
onChanged: (v) => c.updateSettings((x) => x.explorerClick = v!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 82: 창 사이 끌어 놓기 (처음: 묻기)
|
||||||
|
SettingTile(
|
||||||
|
title: Text(tr('끌어 놓기 기본')),
|
||||||
|
subtitle: Text(tr('다른 창으로 끌어 놓았을 때. 어느 쪽이든 복사 · 이동 확인 창 (같은 이름 등) 은 그대로 뜹니다')),
|
||||||
|
trailing: DropdownButton<String>(
|
||||||
|
value: s.explorerDropAction,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(value: 'ask', child: Text(tr('묻기 (기본)'))),
|
||||||
|
DropdownMenuItem(value: 'copy', child: Text(tr('복사'))),
|
||||||
|
DropdownMenuItem(value: 'move', child: Text(tr('이동'))),
|
||||||
|
],
|
||||||
|
onChanged: (v) => c.updateSettings((x) => x.explorerDropAction = v ?? 'ask'),
|
||||||
|
),
|
||||||
|
),
|
||||||
// 84: 정렬을 폴더마다 기억 (처음: 끔)
|
// 84: 정렬을 폴더마다 기억 (처음: 끔)
|
||||||
SettingTile(
|
SettingTile(
|
||||||
title: Text(tr('정렬을 폴더마다 기억')),
|
title: Text(tr('정렬을 폴더마다 기억')),
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import 'package:jj_mkvmaker/core/file_ops.dart' show formatSize;
|
||||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||||
import 'package:jj_mkvmaker/ui/explorer_search.dart';
|
import 'package:jj_mkvmaker/ui/explorer_search.dart';
|
||||||
import 'package:jj_mkvmaker/ui/quick_look.dart';
|
import 'package:jj_mkvmaker/ui/quick_look.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/explorer_drag.dart';
|
||||||
import 'package:jj_mkvmaker/ui/path_label.dart';
|
import 'package:jj_mkvmaker/ui/path_label.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'support/fake_recycle_bin.dart';
|
import 'support/fake_recycle_bin.dart';
|
||||||
|
|
@ -1014,6 +1015,93 @@ void main() {
|
||||||
expect(find.textContaining('담았습니다'), findsOneWidget);
|
expect(find.textContaining('담았습니다'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('82 창 사이 끌어 놓기', () {
|
||||||
|
test('놓을 수 있는 곳: 같은 창 · 자기 안 · 원본 폴더 자신은 안 됨', () {
|
||||||
|
final a = p.join('C:', 'x', 'a'), f = p.join('C:', 'x', 'f.txt');
|
||||||
|
expect(canDropInto(DragLoad(0, [f]), 1, p.join('C:', 'y')), isTrue);
|
||||||
|
expect(canDropInto(DragLoad(0, [f]), 0, p.join('C:', 'y')), isFalse, reason: '같은 창');
|
||||||
|
expect(canDropInto(DragLoad(0, [a]), 1, p.join(a, 'in')), isFalse, reason: '자기 안');
|
||||||
|
expect(canDropInto(DragLoad(0, [f]), 1, p.join('C:', 'x')), isFalse, reason: '원본 폴더 자신');
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 오른쪽 창에 보이는 [name] 줄 (두 창 모두 같은 이름이 보이면 오른쪽 것)
|
||||||
|
Offset rightOf(WidgetTester tester, String name) {
|
||||||
|
final all = find.text(name).evaluate().map((e) => tester.getCenter(find.byWidget(e.widget).first)).toList()
|
||||||
|
..sort((x, y) => x.dx.compareTo(y.dx));
|
||||||
|
return all.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> mouseDrag(WidgetTester tester, Offset from, Offset to) async {
|
||||||
|
final g = await tester.startGesture(from, kind: PointerDeviceKind.mouse);
|
||||||
|
await g.moveBy(const Offset(20, 0));
|
||||||
|
await tester.pump();
|
||||||
|
await g.moveTo(to);
|
||||||
|
await tester.pump();
|
||||||
|
await g.up();
|
||||||
|
await calm(tester);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('짧게 움직이면 고르기 · 같은 창에는 놓아도 아무 일 없음 · 묻기 [취소] 도 아무 일 없음', (tester) async {
|
||||||
|
await open(tester);
|
||||||
|
final doc = tester.getCenter(find.text('doc.txt').first);
|
||||||
|
// 4px 만 움직임 = 누르기 (끌기 아님)
|
||||||
|
final g = await tester.startGesture(doc, kind: PointerDeviceKind.mouse);
|
||||||
|
await g.moveBy(const Offset(4, 0));
|
||||||
|
await g.up();
|
||||||
|
await calm(tester);
|
||||||
|
expect(find.byType(AlertDialog), findsNothing);
|
||||||
|
// 같은 창의 sub 위에 놓기 - 못 놓음
|
||||||
|
await mouseDrag(tester, doc, tester.getCenter(find.text('sub').first));
|
||||||
|
expect(find.byType(AlertDialog), findsNothing, reason: '같은 창에는 놓지 않는다');
|
||||||
|
// 오른쪽 창 (right 폴더) 에 놓기 → 묻기 → 취소
|
||||||
|
await mouseDrag(tester, doc, rightOf(tester, 'right'));
|
||||||
|
expect(find.text('1개 항목을 놓았습니다'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('취소').last);
|
||||||
|
await calm(tester);
|
||||||
|
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
|
||||||
|
expect(File(p.join(right, 'doc.txt')).existsSync(), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('이동 → [되돌리기]: 새로 생긴 것은 통째로, 이미 있던 폴더에 합친 것은 옮긴 파일만', (tester) async {
|
||||||
|
// 오른쪽에 이미 sub 폴더 (원래 파일 하나) - 합치기
|
||||||
|
File(p.join(right, 'sub', 'mine.txt'))
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('mine');
|
||||||
|
c.settings
|
||||||
|
..explorerDropAction = 'move'
|
||||||
|
..copyConflict = 'overwrite';
|
||||||
|
await open(tester);
|
||||||
|
await act(tester, () => tester.tap(find.text('선택').first));
|
||||||
|
await act(tester, () => tester.tap(markOf('doc.txt')));
|
||||||
|
await act(tester, () => tester.tap(markOf('sub')));
|
||||||
|
await calm(tester);
|
||||||
|
await mouseDrag(tester, tester.getCenter(find.text('doc.txt').first), rightOf(tester, 'right'));
|
||||||
|
// (놓은 뒤 대상에 이미 있는 이름을 실제 파일로 확인하므로 실제 시간을 흘려 기다린다)
|
||||||
|
await settle(tester, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty);
|
||||||
|
// 지금의 이동 확인 창 그대로
|
||||||
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '이동')));
|
||||||
|
await settle(tester, () => find.text('되돌리기').evaluate().isNotEmpty);
|
||||||
|
expect(File(p.join(right, 'doc.txt')).existsSync(), isTrue);
|
||||||
|
expect(File(p.join(right, 'sub', 'inner.txt')).existsSync(), isTrue);
|
||||||
|
expect(Directory(p.join(left, 'sub')).existsSync(), isFalse);
|
||||||
|
// 되돌리기: 원래 폴더마다 지금의 이동 (확인 창)
|
||||||
|
await act(tester, () => tester.tap(find.text('되돌리기')));
|
||||||
|
// 원래 폴더가 둘 (left · left/sub) 이라 이동 확인 창도 둘
|
||||||
|
for (var i = 0; i < 2; i++) {
|
||||||
|
await settle(tester, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty);
|
||||||
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '이동')));
|
||||||
|
await settle(tester, () => find.byType(AlertDialog).evaluate().isEmpty, rounds: 50);
|
||||||
|
}
|
||||||
|
await settle(tester, () => File(p.join(left, 'doc.txt')).existsSync() && File(p.join(left, 'sub', 'inner.txt')).existsSync());
|
||||||
|
expect(File(p.join(left, 'doc.txt')).readAsStringSync(), 'hello');
|
||||||
|
expect(File(p.join(left, 'sub', 'inner.txt')).readAsStringSync(), 'inner');
|
||||||
|
expect(File(p.join(right, 'sub', 'mine.txt')).readAsStringSync(), 'mine', reason: '원래 있던 폴더 · 파일은 그대로 (폴더째 옮기지 않음)');
|
||||||
|
expect(File(p.join(right, 'sub', 'inner.txt')).existsSync(), isFalse);
|
||||||
|
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||||
|
await calm(tester);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('81: Space 로 빠른 보기 (글 파일 앞부분) · 필터에 치는 중에는 그냥 빈칸 · TC 는 F3', (tester) async {
|
testWidgets('81: Space 로 빠른 보기 (글 파일 앞부분) · 필터에 치는 중에는 그냥 빈칸 · TC 는 F3', (tester) async {
|
||||||
c.settings.explorerStyle = 'windows';
|
c.settings.explorerStyle = 'windows';
|
||||||
await open(tester);
|
await open(tester);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue