WIP 80 파일 탐색기 폴더 크기: 필요할 때 계산 · 기억, 크기순 정렬 + 79-a [그 자리로] 뒤에도 돌아가기 줄 (검토 대기)
80 (관리자 지시 F9): 폴더 크기를 목록에 표시하고 크기순으로 정렬 (필요할 때 계산하고 기억).
- FolderSizes: 안의 파일 크기 합, 한 번에 하나씩 (디스크 · NAS 를 몰아 읽지 않게), 이번 실행 동안 기억.
읽지 못한 폴더가 있으면 "1.2GB+". 다시 읽는 폴더 (새로 고침 · 복사 · 이동 · 지우기) 는 그 폴더 · 안쪽 · 위 폴더 크기를 잊는다.
- 설정 "폴더 크기": 크기순 정렬일 때만 (처음 값) / 늘 계산 / 계산 안 함. 길게 누르기 [폴더 크기 계산] 은 늘 된다.
- 내 판단: 자동 계산은 지금 보고 있는 폴더 바로 안의 폴더만 - 트리에 보이는 위 폴더들 (C:\ · 사용자 폴더 …) 까지 하면
드라이브 전체를 읽느라 정작 보고 있는 폴더가 뒤로 밀렸다 (시험에서 발견).
- 보이는 곳: Windows 탐색기 모양은 크기 열, TC 모양은 <DIR> 자리, X-plore · 좁은 화면은 폴더 이름 아래. 계산 중 "…".
- 크기순 정렬은 아는 폴더 크기로 (모르는 폴더는 가장 작게, 폴더는 늘 위 - 예전 그대로). 크기가 나오면 다시 줄 세운다.
- 측정은 바깥 영역 (Zone.root) 에서 - 화면을 그리다 맡긴 계산이 그 영역에 묶이지 않게 (위젯 시험에서 읽기가 끝나지 않았다).
79-a (관리자 판단 10/17): [그 자리로] 로 나가도 [찾기 결과로 돌아가기] 줄을 남긴다.
- 시험: folder_size_test (합 · 기억 · 잊기 · "+" · 정렬), explorer_test 80 (크기순 자동 · 새로 고침 뒤 다시 · "안 함" + 길게 누르기), 79-a 그 자리로.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음6
parent
60d4db7583
commit
ef1cb069c1
|
|
@ -599,6 +599,7 @@
|
||||||
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "The path is too long for the Recycle Bin (the Windows Recycle Bin does not accept paths over 260 characters).",
|
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "The path is too long for the Recycle Bin (the Windows Recycle Bin does not accept paths over 260 characters).",
|
||||||
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "The path is too long (the Windows Recycle Bin doesn't accept paths over 260 characters) - you can delete it permanently",
|
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "The path is too long (the Windows Recycle Bin doesn't accept paths over 260 characters) - you can delete it permanently",
|
||||||
"경로를 복사했습니다: {0}": "Copied path: {0}",
|
"경로를 복사했습니다: {0}": "Copied path: {0}",
|
||||||
|
"계산 안 함": "Don't calculate",
|
||||||
"계속": "Always",
|
"계속": "Always",
|
||||||
"계속 (앱이 켜져 있는 동안)": "Always (while the app is running)",
|
"계속 (앱이 켜져 있는 동안)": "Always (while the app is running)",
|
||||||
"계속 받기": "Keep downloading",
|
"계속 받기": "Keep downloading",
|
||||||
|
|
@ -771,6 +772,7 @@
|
||||||
"누르기": "Tap",
|
"누르기": "Tap",
|
||||||
"누르면 일정 바꾸기": "Tap to change the schedule",
|
"누르면 일정 바꾸기": "Tap to change the schedule",
|
||||||
"늘 건너뛰기": "Always skip",
|
"늘 건너뛰기": "Always skip",
|
||||||
|
"늘 계산": "Always calculate",
|
||||||
"늘 덮어쓰기": "Always overwrite",
|
"늘 덮어쓰기": "Always overwrite",
|
||||||
"늘 받아서 열기": "Always download and open",
|
"늘 받아서 열기": "Always download and open",
|
||||||
"늘 이름 바꾸기": "Always keep both (rename)",
|
"늘 이름 바꾸기": "Always keep both (rename)",
|
||||||
|
|
@ -2173,6 +2175,7 @@
|
||||||
"크기 · 보정을 원래대로 돌려 코덱도 \"원본 유지\" 로 되돌렸습니다.": "Size and adjustments are back to original, so the codec was set back to \"Keep original\".",
|
"크기 · 보정을 원래대로 돌려 코덱도 \"원본 유지\" 로 되돌렸습니다.": "Size and adjustments are back to original, so the codec was set back to \"Keep original\".",
|
||||||
"크기 이상": "Min size",
|
"크기 이상": "Min size",
|
||||||
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "Changing the size requires re-encoding, so the codec was changed from \"Keep original\" to H.264.",
|
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "Changing the size requires re-encoding, so the codec was changed from \"Keep original\" to H.264.",
|
||||||
|
"크기순 정렬일 때만 (기본)": "Only when sorting by size (default)",
|
||||||
"크메르어": "Khmer",
|
"크메르어": "Khmer",
|
||||||
"키 있음": "Key saved",
|
"키 있음": "Key saved",
|
||||||
"타갈로그어": "Tagalog",
|
"타갈로그어": "Tagalog",
|
||||||
|
|
@ -2254,11 +2257,14 @@
|
||||||
"폴더 삭제: {0} (안의 {1}개 포함)": "Folder deleted: {0} (including {1} inside)",
|
"폴더 삭제: {0} (안의 {1}개 포함)": "Folder deleted: {0} (including {1} inside)",
|
||||||
"폴더 선택": "Choose folder",
|
"폴더 선택": "Choose folder",
|
||||||
"폴더 수정": "Edit folder",
|
"폴더 수정": "Edit folder",
|
||||||
|
"폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. 길게 누르기 [폴더 크기 계산] 은 늘 됩니다": "Shows the total size of the files inside each folder. Large folders or a NAS can take a while to read. Long-press [Calculate folder size] always works",
|
||||||
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "Syncs the folder contents (no same-named folder is created in the target).",
|
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "Syncs the folder contents (no same-named folder is created in the target).",
|
||||||
"폴더 열기": "Open folder",
|
"폴더 열기": "Open folder",
|
||||||
"폴더 이름": "Folder name",
|
"폴더 이름": "Folder name",
|
||||||
"폴더 이름 바꾸기 · 이동": "Rename · move folder",
|
"폴더 이름 바꾸기 · 이동": "Rename · move folder",
|
||||||
"폴더 이름에 쓸 수 없는 글자가 있습니다 (\\ / : * ? \" < > |)": "The folder name has characters that cannot be used (\\ / : * ? \" < > |)",
|
"폴더 이름에 쓸 수 없는 글자가 있습니다 (\\ / : * ? \" < > |)": "The folder name has characters that cannot be used (\\ / : * ? \" < > |)",
|
||||||
|
"폴더 크기": "Folder size",
|
||||||
|
"폴더 크기 계산": "Calculate folder size",
|
||||||
"폴더: {0}": "Folder: {0}",
|
"폴더: {0}": "Folder: {0}",
|
||||||
"폴더가 들어 있을 때": "When folders are included",
|
"폴더가 들어 있을 때": "When folders are included",
|
||||||
"폴더가 아닙니다: {0}": "Not a folder: {0}",
|
"폴더가 아닙니다: {0}": "Not a folder: {0}",
|
||||||
|
|
|
||||||
|
|
@ -599,6 +599,7 @@
|
||||||
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "パスが長すぎてごみ箱に入れられません (Windows のごみ箱は 260 文字を超えるパスを受け付けません)。",
|
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "パスが長すぎてごみ箱に入れられません (Windows のごみ箱は 260 文字を超えるパスを受け付けません)。",
|
||||||
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "パスが長すぎます (Windows のごみ箱は 260 文字を超えるパスを受け付けません) - 完全に削除はできます",
|
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "パスが長すぎます (Windows のごみ箱は 260 文字を超えるパスを受け付けません) - 完全に削除はできます",
|
||||||
"경로를 복사했습니다: {0}": "パスをコピーしました: {0}",
|
"경로를 복사했습니다: {0}": "パスをコピーしました: {0}",
|
||||||
|
"계산 안 함": "計算しない",
|
||||||
"계속": "常時",
|
"계속": "常時",
|
||||||
"계속 (앱이 켜져 있는 동안)": "常時 (アプリの起動中)",
|
"계속 (앱이 켜져 있는 동안)": "常時 (アプリの起動中)",
|
||||||
"계속 받기": "ダウンロードを続ける",
|
"계속 받기": "ダウンロードを続ける",
|
||||||
|
|
@ -771,6 +772,7 @@
|
||||||
"누르기": "タップ",
|
"누르기": "タップ",
|
||||||
"누르면 일정 바꾸기": "タップで予定を変更",
|
"누르면 일정 바꾸기": "タップで予定を変更",
|
||||||
"늘 건너뛰기": "常にスキップ",
|
"늘 건너뛰기": "常にスキップ",
|
||||||
|
"늘 계산": "常に計算",
|
||||||
"늘 덮어쓰기": "常に上書き",
|
"늘 덮어쓰기": "常に上書き",
|
||||||
"늘 받아서 열기": "常にダウンロードして開く",
|
"늘 받아서 열기": "常にダウンロードして開く",
|
||||||
"늘 이름 바꾸기": "常に名前を変える",
|
"늘 이름 바꾸기": "常に名前を変える",
|
||||||
|
|
@ -2173,6 +2175,7 @@
|
||||||
"크기 · 보정을 원래대로 돌려 코덱도 \"원본 유지\" 로 되돌렸습니다.": "サイズ・補正を元に戻したので、コーデックも「元のまま」に戻しました。",
|
"크기 · 보정을 원래대로 돌려 코덱도 \"원본 유지\" 로 되돌렸습니다.": "サイズ・補正を元に戻したので、コーデックも「元のまま」に戻しました。",
|
||||||
"크기 이상": "最小サイズ",
|
"크기 이상": "最小サイズ",
|
||||||
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "サイズを変えると再エンコードが必要なため、コーデックを「元のまま」から H.264 に変更しました。",
|
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "サイズを変えると再エンコードが必要なため、コーデックを「元のまま」から H.264 に変更しました。",
|
||||||
|
"크기순 정렬일 때만 (기본)": "サイズ順のときだけ (既定)",
|
||||||
"크메르어": "クメール語",
|
"크메르어": "クメール語",
|
||||||
"키 있음": "キーあり",
|
"키 있음": "キーあり",
|
||||||
"타갈로그어": "タガログ語",
|
"타갈로그어": "タガログ語",
|
||||||
|
|
@ -2254,11 +2257,14 @@
|
||||||
"폴더 삭제: {0} (안의 {1}개 포함)": "フォルダーを削除: {0}(中の {1}個を含む)",
|
"폴더 삭제: {0} (안의 {1}개 포함)": "フォルダーを削除: {0}(中の {1}個を含む)",
|
||||||
"폴더 선택": "フォルダーを選択",
|
"폴더 선택": "フォルダーを選択",
|
||||||
"폴더 수정": "フォルダーを編集",
|
"폴더 수정": "フォルダーを編集",
|
||||||
|
"폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. 길게 누르기 [폴더 크기 계산] 은 늘 됩니다": "フォルダー内のファイルサイズの合計を一覧に表示します。大きなフォルダー・NAS は読み込みに時間がかかることがあります。長押しの [フォルダーサイズを計算] はいつでも使えます",
|
||||||
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "フォルダーの中身を合わせます (対象に同じ名前のフォルダーは作りません)。",
|
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "フォルダーの中身を合わせます (対象に同じ名前のフォルダーは作りません)。",
|
||||||
"폴더 열기": "フォルダーを開く",
|
"폴더 열기": "フォルダーを開く",
|
||||||
"폴더 이름": "フォルダー名",
|
"폴더 이름": "フォルダー名",
|
||||||
"폴더 이름 바꾸기 · 이동": "フォルダー名の変更・移動",
|
"폴더 이름 바꾸기 · 이동": "フォルダー名の変更・移動",
|
||||||
"폴더 이름에 쓸 수 없는 글자가 있습니다 (\\ / : * ? \" < > |)": "フォルダー名に使えない文字があります(\\ / : * ? \" < > |)",
|
"폴더 이름에 쓸 수 없는 글자가 있습니다 (\\ / : * ? \" < > |)": "フォルダー名に使えない文字があります(\\ / : * ? \" < > |)",
|
||||||
|
"폴더 크기": "フォルダーサイズ",
|
||||||
|
"폴더 크기 계산": "フォルダーサイズを計算",
|
||||||
"폴더: {0}": "フォルダー: {0}",
|
"폴더: {0}": "フォルダー: {0}",
|
||||||
"폴더가 들어 있을 때": "フォルダーを含むとき",
|
"폴더가 들어 있을 때": "フォルダーを含むとき",
|
||||||
"폴더가 아닙니다: {0}": "フォルダーではありません: {0}",
|
"폴더가 아닙니다: {0}": "フォルダーではありません: {0}",
|
||||||
|
|
|
||||||
|
|
@ -599,6 +599,7 @@
|
||||||
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "路径过长,无法放入回收站 (Windows 回收站不接受超过 260 个字符的路径)。",
|
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "路径过长,无法放入回收站 (Windows 回收站不接受超过 260 个字符的路径)。",
|
||||||
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "路径过长 (Windows 回收站不接受超过 260 个字符的路径) - 可以永久删除",
|
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "路径过长 (Windows 回收站不接受超过 260 个字符的路径) - 可以永久删除",
|
||||||
"경로를 복사했습니다: {0}": "已复制路径: {0}",
|
"경로를 복사했습니다: {0}": "已复制路径: {0}",
|
||||||
|
"계산 안 함": "不计算",
|
||||||
"계속": "持续",
|
"계속": "持续",
|
||||||
"계속 (앱이 켜져 있는 동안)": "持续 (应用运行期间)",
|
"계속 (앱이 켜져 있는 동안)": "持续 (应用运行期间)",
|
||||||
"계속 받기": "继续下载",
|
"계속 받기": "继续下载",
|
||||||
|
|
@ -771,6 +772,7 @@
|
||||||
"누르기": "点按",
|
"누르기": "点按",
|
||||||
"누르면 일정 바꾸기": "点按以更改计划",
|
"누르면 일정 바꾸기": "点按以更改计划",
|
||||||
"늘 건너뛰기": "总是跳过",
|
"늘 건너뛰기": "总是跳过",
|
||||||
|
"늘 계산": "始终计算",
|
||||||
"늘 덮어쓰기": "总是覆盖",
|
"늘 덮어쓰기": "总是覆盖",
|
||||||
"늘 받아서 열기": "始终下载后打开",
|
"늘 받아서 열기": "始终下载后打开",
|
||||||
"늘 이름 바꾸기": "总是重命名",
|
"늘 이름 바꾸기": "总是重命名",
|
||||||
|
|
@ -2173,6 +2175,7 @@
|
||||||
"크기 · 보정을 원래대로 돌려 코덱도 \"원본 유지\" 로 되돌렸습니다.": "大小 · 调整已恢复原样,编解码器也恢复为“保持原样”。",
|
"크기 · 보정을 원래대로 돌려 코덱도 \"원본 유지\" 로 되돌렸습니다.": "大小 · 调整已恢复原样,编解码器也恢复为“保持原样”。",
|
||||||
"크기 이상": "最小大小",
|
"크기 이상": "最小大小",
|
||||||
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "更改大小需要重新编码,因此编解码器已从“保持原样”改为 H.264。",
|
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "更改大小需要重新编码,因此编解码器已从“保持原样”改为 H.264。",
|
||||||
|
"크기순 정렬일 때만 (기본)": "仅在按大小排序时 (默认)",
|
||||||
"크메르어": "高棉语",
|
"크메르어": "高棉语",
|
||||||
"키 있음": "已有密钥",
|
"키 있음": "已有密钥",
|
||||||
"타갈로그어": "他加禄语",
|
"타갈로그어": "他加禄语",
|
||||||
|
|
@ -2254,11 +2257,14 @@
|
||||||
"폴더 삭제: {0} (안의 {1}개 포함)": "已删除文件夹: {0}(含其中 {1} 个)",
|
"폴더 삭제: {0} (안의 {1}개 포함)": "已删除文件夹: {0}(含其中 {1} 个)",
|
||||||
"폴더 선택": "选择文件夹",
|
"폴더 선택": "选择文件夹",
|
||||||
"폴더 수정": "编辑文件夹",
|
"폴더 수정": "编辑文件夹",
|
||||||
|
"폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. 길게 누르기 [폴더 크기 계산] 은 늘 됩니다": "在列表中显示文件夹内文件大小的总和。大型文件夹或 NAS 可能需要较长时间读取。长按 [计算文件夹大小] 始终可用",
|
||||||
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "同步文件夹内的内容 (不会在目标中新建同名文件夹)。",
|
"폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).": "同步文件夹内的内容 (不会在目标中新建同名文件夹)。",
|
||||||
"폴더 열기": "打开文件夹",
|
"폴더 열기": "打开文件夹",
|
||||||
"폴더 이름": "文件夹名称",
|
"폴더 이름": "文件夹名称",
|
||||||
"폴더 이름 바꾸기 · 이동": "重命名 · 移动文件夹",
|
"폴더 이름 바꾸기 · 이동": "重命名 · 移动文件夹",
|
||||||
"폴더 이름에 쓸 수 없는 글자가 있습니다 (\\ / : * ? \" < > |)": "文件夹名称中有不能使用的字符(\\ / : * ? \" < > |)",
|
"폴더 이름에 쓸 수 없는 글자가 있습니다 (\\ / : * ? \" < > |)": "文件夹名称中有不能使用的字符(\\ / : * ? \" < > |)",
|
||||||
|
"폴더 크기": "文件夹大小",
|
||||||
|
"폴더 크기 계산": "计算文件夹大小",
|
||||||
"폴더: {0}": "文件夹: {0}",
|
"폴더: {0}": "文件夹: {0}",
|
||||||
"폴더가 들어 있을 때": "包含文件夹时",
|
"폴더가 들어 있을 때": "包含文件夹时",
|
||||||
"폴더가 아닙니다: {0}": "不是文件夹: {0}",
|
"폴더가 아닙니다: {0}": "不是文件夹: {0}",
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,9 @@ class AppSettings {
|
||||||
/// 정렬 · 숨긴 항목
|
/// 정렬 · 숨긴 항목
|
||||||
String explorerSort = 'name';
|
String explorerSort = 'name';
|
||||||
bool explorerSortDesc = false;
|
bool explorerSortDesc = false;
|
||||||
|
|
||||||
|
/// 80: 폴더 크기 계산 - 'sort' 크기순 정렬일 때만 (처음 값) · 'always' 늘 · 'off' 안 함 (길게 누르기 [폴더 크기 계산] 은 늘 됨)
|
||||||
|
String explorerFolderSize = 'sort';
|
||||||
bool explorerShowHidden = false;
|
bool explorerShowHidden = false;
|
||||||
|
|
||||||
// ── 복사 · 이동 (파일 탐색기) · rsync · lsync (Rsync 화면) ──
|
// ── 복사 · 이동 (파일 탐색기) · rsync · lsync (Rsync 화면) ──
|
||||||
|
|
@ -731,6 +734,7 @@ class AppSettings {
|
||||||
'explorerButtons': explorerButtons,
|
'explorerButtons': explorerButtons,
|
||||||
'explorerSort': explorerSort,
|
'explorerSort': explorerSort,
|
||||||
'explorerSortDesc': explorerSortDesc,
|
'explorerSortDesc': explorerSortDesc,
|
||||||
|
'explorerFolderSize': explorerFolderSize,
|
||||||
'explorerShowHidden': explorerShowHidden,
|
'explorerShowHidden': explorerShowHidden,
|
||||||
'explorerPaths': explorerPaths,
|
'explorerPaths': explorerPaths,
|
||||||
'copyMethodFile': copyMethodFile,
|
'copyMethodFile': copyMethodFile,
|
||||||
|
|
@ -944,6 +948,7 @@ class AppSettings {
|
||||||
..explorerButtons = [for (final x in (j['explorerButtons'] as List?) ?? const []) '$x']
|
..explorerButtons = [for (final x in (j['explorerButtons'] as List?) ?? const []) '$x']
|
||||||
..explorerSort = const ['date', 'size', 'type'].contains(j['explorerSort']) ? j['explorerSort'] as String : 'name'
|
..explorerSort = const ['date', 'size', 'type'].contains(j['explorerSort']) ? j['explorerSort'] as String : 'name'
|
||||||
..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'
|
||||||
..explorerShowHidden = j['explorerShowHidden'] as bool? ?? false
|
..explorerShowHidden = j['explorerShowHidden'] as bool? ?? false
|
||||||
..explorerPaths = [for (final x in (j['explorerPaths'] as List?) ?? const []) '$x']
|
..explorerPaths = [for (final x in (j['explorerPaths'] as List?) ?? const []) '$x']
|
||||||
..copyMethodFile = _method(j['copyMethodFile'])
|
..copyMethodFile = _method(j['copyMethodFile'])
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,15 @@ class FileEntry {
|
||||||
enum SortBy { name, date, size, type }
|
enum SortBy { name, date, size, type }
|
||||||
|
|
||||||
/// 정렬: 폴더 먼저, 이름은 숫자를 숫자로 비교 (file2 < file10)
|
/// 정렬: 폴더 먼저, 이름은 숫자를 숫자로 비교 (file2 < file10)
|
||||||
List<FileEntry> sortEntries(List<FileEntry> list, SortBy by, {bool descending = false}) {
|
/// 80: [dirSize] 가 있으면 크기순에서 폴더도 그 크기로 (모르는 폴더는 가장 작게)
|
||||||
|
List<FileEntry> sortEntries(List<FileEntry> list, SortBy by, {bool descending = false, int? Function(FileEntry dir)? dirSize}) {
|
||||||
|
int sizeOf(FileEntry e) => e.isDir ? (dirSize?.call(e) ?? -1) : e.size;
|
||||||
int cmp(FileEntry a, FileEntry b) {
|
int cmp(FileEntry a, FileEntry b) {
|
||||||
if (a.isDir != b.isDir) return a.isDir ? -1 : 1; // 폴더는 거꾸로여도 늘 위
|
if (a.isDir != b.isDir) return a.isDir ? -1 : 1; // 폴더는 거꾸로여도 늘 위
|
||||||
final r = switch (by) {
|
final r = switch (by) {
|
||||||
SortBy.name => 0,
|
SortBy.name => 0,
|
||||||
SortBy.date => a.modified.compareTo(b.modified),
|
SortBy.date => a.modified.compareTo(b.modified),
|
||||||
SortBy.size => a.size.compareTo(b.size),
|
SortBy.size => sizeOf(a).compareTo(sizeOf(b)),
|
||||||
SortBy.type => a.ext.compareTo(b.ext),
|
SortBy.type => a.ext.compareTo(b.ext),
|
||||||
};
|
};
|
||||||
final v = r != 0 ? r : naturalCompare(a.name, b.name);
|
final v = r != 0 ? r : naturalCompare(a.name, b.name);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'file_ops.dart';
|
||||||
|
import 'vfs.dart';
|
||||||
|
|
||||||
|
/// 80: 폴더 크기 (안의 파일 크기 합) - 필요할 때 계산하고 이번 실행 동안 기억한다.
|
||||||
|
/// 한 번에 하나씩 (디스크 · NAS 를 몰아 읽지 않게). 바뀐 폴더는 [forget] 으로 그 폴더와 위 폴더들을 지운다.
|
||||||
|
class FolderSizes extends ChangeNotifier {
|
||||||
|
final _known = <String, FolderSize>{};
|
||||||
|
final _waiting = <String>[];
|
||||||
|
final _queued = <String>{};
|
||||||
|
bool _running = false;
|
||||||
|
bool _disposed = false;
|
||||||
|
|
||||||
|
/// 읽기 (시험에서 바꿔 끼움)
|
||||||
|
final Future<List<FileEntry>> Function(String dir) list;
|
||||||
|
|
||||||
|
FolderSizes({Future<List<FileEntry>> Function(String dir)? list}) : list = list ?? _vList;
|
||||||
|
|
||||||
|
static Future<List<FileEntry>> _vList(String dir) async => [
|
||||||
|
for (final e in await vList(dir, strict: true)) FileEntry(e.path, isDir: e.isDir, size: e.size, modified: e.modified),
|
||||||
|
];
|
||||||
|
|
||||||
|
static String _key(String path) => isDav(path) ? vNorm(path) : path.toLowerCase();
|
||||||
|
|
||||||
|
/// 아는 크기 (없으면 null)
|
||||||
|
FolderSize? of(String path) => _known[_key(path)];
|
||||||
|
|
||||||
|
/// 계산 중이거나 기다리는 중인지
|
||||||
|
bool busy(String path) => _queued.contains(_key(path));
|
||||||
|
|
||||||
|
/// 계산해 두기 (이미 알거나 기다리는 중이면 그대로)
|
||||||
|
void request(String path) {
|
||||||
|
final k = _key(path);
|
||||||
|
if (_known.containsKey(k) || _queued.contains(k)) return;
|
||||||
|
_queued.add(k);
|
||||||
|
_waiting.add(path);
|
||||||
|
notifyListeners();
|
||||||
|
// 화면을 그리는 중 (프레임 뒤 콜백) 에 맡겨도 그 영역에 묶이지 않게 바깥 영역에서 읽는다
|
||||||
|
// (위젯 시험의 가짜 시간 영역에서는 실제 파일 읽기가 끝나지 않았다)
|
||||||
|
Zone.root.run(() => unawaited(_pump()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 이 폴더 · 그 안의 폴더들 · 위 폴더들의 크기를 잊는다 (안의 것이 바뀜 - 새로 고침 · 복사 · 이동 · 지우기)
|
||||||
|
void forget(String path) {
|
||||||
|
final k = _key(path);
|
||||||
|
final before = _known.length;
|
||||||
|
_known.removeWhere((x, _) => x.startsWith('$k\\') || x.startsWith('$k/'));
|
||||||
|
var changed = _known.length != before;
|
||||||
|
var d = path;
|
||||||
|
while (true) {
|
||||||
|
changed |= _known.remove(_key(d)) != null;
|
||||||
|
final up = vDirname(d);
|
||||||
|
if (up == d || up.isEmpty) break;
|
||||||
|
d = up;
|
||||||
|
}
|
||||||
|
if (changed && !_disposed) notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pump() async {
|
||||||
|
if (_running) return;
|
||||||
|
_running = true;
|
||||||
|
try {
|
||||||
|
while (_waiting.isNotEmpty && !_disposed) {
|
||||||
|
final path = _waiting.removeAt(0);
|
||||||
|
final size = await _measure(path);
|
||||||
|
if (_disposed) return;
|
||||||
|
_queued.remove(_key(path));
|
||||||
|
_known[_key(path)] = size;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 안의 파일 크기 합. 읽지 못한 폴더가 있으면 [FolderSize.partial] (화면에 "+")
|
||||||
|
Future<FolderSize> _measure(String root) async {
|
||||||
|
var bytes = 0;
|
||||||
|
var partial = false;
|
||||||
|
final pending = [root];
|
||||||
|
while (pending.isNotEmpty && !_disposed) {
|
||||||
|
final dir = pending.removeLast();
|
||||||
|
// 안쪽 폴더를 이미 알면 다시 읽지 않는다
|
||||||
|
final known = dir == root ? null : _known[_key(dir)];
|
||||||
|
if (known != null) {
|
||||||
|
bytes += known.bytes;
|
||||||
|
partial |= known.partial;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (final e in await list(dir)) {
|
||||||
|
if (e.isDir) {
|
||||||
|
pending.add(e.path);
|
||||||
|
} else if (e.size > 0) {
|
||||||
|
bytes += e.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
partial = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FolderSize(bytes, partial: partial);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_disposed = true;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FolderSize {
|
||||||
|
final int bytes;
|
||||||
|
|
||||||
|
/// 읽지 못한 폴더가 있어 실제보다 작을 수 있음
|
||||||
|
final bool partial;
|
||||||
|
const FolderSize(this.bytes, {this.partial = false});
|
||||||
|
|
||||||
|
/// "1.2GB" · 읽지 못한 곳이 있으면 "1.2GB+"
|
||||||
|
String get label => '${formatSize(bytes)}${partial ? '+' : ''}';
|
||||||
|
}
|
||||||
|
|
@ -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 'batch_rename_dialog.dart';
|
import 'batch_rename_dialog.dart';
|
||||||
|
import '../core/folder_size.dart';
|
||||||
import '../core/batch_rename.dart';
|
import '../core/batch_rename.dart';
|
||||||
|
|
||||||
/// 파일 탐색기 (X-plore 참고): 두 창 (트리 목록) + 가운데 기능 버튼 줄.
|
/// 파일 탐색기 (X-plore 참고): 두 창 (트리 목록) + 가운데 기능 버튼 줄.
|
||||||
|
|
@ -317,6 +318,38 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
/// 화면 단축키 (F5 · Delete …) 를 받는 곳. 76: 빠른 필터 칸을 닫으면 여기로 돌려준다
|
/// 화면 단축키 (F5 · Delete …) 를 받는 곳. 76: 빠른 필터 칸을 닫으면 여기로 돌려준다
|
||||||
final _keys = FocusNode(debugLabel: 'explorer');
|
final _keys = FocusNode(debugLabel: 'explorer');
|
||||||
|
|
||||||
|
/// 80: 폴더 크기 (필요할 때 계산하고 이번 실행 동안 기억)
|
||||||
|
late final _sizes = FolderSizes()..addListener(_onSizes);
|
||||||
|
|
||||||
|
/// 80: 폴더 크기가 나오면 - 크기순이면 다시 줄 세우고 화면 갱신
|
||||||
|
void _onSizes() {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (c.settings.explorerSort == 'size') {
|
||||||
|
for (final pane in _panes) {
|
||||||
|
for (final k in pane.cache.keys.toList()) {
|
||||||
|
pane.cache[k] = _sorted(pane.cache[k]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 80: 폴더 크기 글 (모르면 null). 설정이 자동이면 계산을 맡긴다 -
|
||||||
|
/// 지금 보고 있는 폴더 바로 안의 폴더만 (트리에 보이는 위 폴더들 (C:\ · 사용자 폴더 …) 은 드라이브 전체를 읽게 되어 자동으로 하지 않는다)
|
||||||
|
String? _dirSize(_Pane pane, FileEntry e) {
|
||||||
|
final s = _sizes.of(e.path);
|
||||||
|
if (s != null) return s.label;
|
||||||
|
if (_sizes.busy(e.path)) return '…';
|
||||||
|
final mode = c.settings.explorerFolderSize;
|
||||||
|
if ((mode == 'always' || (mode == 'sort' && c.settings.explorerSort == 'size')) && samePath(vDirname(e.path), pane.current)) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) _sizes.request(e.path);
|
||||||
|
});
|
||||||
|
return '…';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -361,6 +394,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_keys.dispose();
|
_keys.dispose();
|
||||||
|
_sizes.dispose();
|
||||||
if (widget.rsync) {
|
if (widget.rsync) {
|
||||||
ExplorerPage._openRsync--;
|
ExplorerPage._openRsync--;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -725,6 +759,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
|
|
||||||
Future<void> _load(_Pane pane, String dir, {bool force = false}) async {
|
Future<void> _load(_Pane pane, String dir, {bool force = false}) async {
|
||||||
if (!force && pane.cache.containsKey(dir)) return;
|
if (!force && pane.cache.containsKey(dir)) return;
|
||||||
|
// 80: 다시 읽는 폴더는 안이 바뀌었을 수 있다 - 그 폴더와 위 폴더들의 크기를 잊는다
|
||||||
|
if (force) _sizes.forget(dir);
|
||||||
pane.loading.add(dir);
|
pane.loading.add(dir);
|
||||||
pane.changed();
|
pane.changed();
|
||||||
try {
|
try {
|
||||||
|
|
@ -762,7 +798,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
|
|
||||||
List<FileEntry> _sorted(List<FileEntry> list) {
|
List<FileEntry> _sorted(List<FileEntry> list) {
|
||||||
final by = SortBy.values.firstWhere((s) => s.name == c.settings.explorerSort, orElse: () => SortBy.name);
|
final by = SortBy.values.firstWhere((s) => s.name == c.settings.explorerSort, orElse: () => SortBy.name);
|
||||||
return sortEntries(list, by, descending: c.settings.explorerSortDesc);
|
return sortEntries(list, by, descending: c.settings.explorerSortDesc, dirSize: (d) => _sizes.of(d.path)?.bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 그 폴더로: 저장 장치부터 그 폴더까지 펼치고 지금 폴더로 정한다
|
/// 그 폴더로: 저장 장치부터 그 폴더까지 펼치고 지금 폴더로 정한다
|
||||||
|
|
@ -1310,6 +1346,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
// 74: 폰에서 범위 고르기 - 첫 항목을 표시한 뒤 마지막 항목을 길게 눌러
|
// 74: 폰에서 범위 고르기 - 첫 항목을 표시한 뒤 마지막 항목을 길게 눌러
|
||||||
if (_selecting && _canRange(pane, e)) ('range', Icons.unfold_more, tr('여기까지 범위 고르기')),
|
if (_selecting && _canRange(pane, e)) ('range', Icons.unfold_more, tr('여기까지 범위 고르기')),
|
||||||
if (e.isDir) ('open', Icons.folder_open, tr('열기')),
|
if (e.isDir) ('open', Icons.folder_open, tr('열기')),
|
||||||
|
// 80: 이 폴더 크기를 지금 계산 (설정이 "안 함" 이어도)
|
||||||
|
if (e.isDir) ('folderSize', Icons.data_usage, tr('폴더 크기 계산')),
|
||||||
if (e.isDir && dual) ('openOther', Icons.vertical_split_outlined, tr('다른 창에서 열기')),
|
if (e.isDir && dual) ('openOther', Icons.vertical_split_outlined, tr('다른 창에서 열기')),
|
||||||
if (e.isDir) ('playFolder', Icons.play_circle_outline, tr('이 폴더의 동영상 재생')),
|
if (e.isDir) ('playFolder', Icons.play_circle_outline, tr('이 폴더의 동영상 재생')),
|
||||||
if (video || audio) ('playInternal', Icons.play_circle_outline, tr('내장 플레이어로 재생')),
|
if (video || audio) ('playInternal', Icons.play_circle_outline, tr('내장 플레이어로 재생')),
|
||||||
|
|
@ -1395,6 +1433,10 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
case 'select':
|
case 'select':
|
||||||
setState(() => _selecting = true);
|
setState(() => _selecting = true);
|
||||||
if (!_isMarked(pane, e.path)) await _toggleMark(pane, e);
|
if (!_isMarked(pane, e.path)) await _toggleMark(pane, e);
|
||||||
|
case 'folderSize':
|
||||||
|
_sizes
|
||||||
|
..forget(e.path)
|
||||||
|
..request(e.path);
|
||||||
case 'rename':
|
case 'rename':
|
||||||
if (_isMarked(pane, e.path) && pane.marked.length > 1) {
|
if (_isMarked(pane, e.path) && pane.marked.length > 1) {
|
||||||
await _batchRename(pane, pane.marked.toList());
|
await _batchRename(pane, pane.marked.toList());
|
||||||
|
|
@ -2685,7 +2727,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
/// 79: 마지막 찾기 조건 (이번 실행 동안 다음 찾기 창의 처음 값)
|
/// 79: 마지막 찾기 조건 (이번 실행 동안 다음 찾기 창의 처음 값)
|
||||||
SearchConditions _lastSearch = const SearchConditions();
|
SearchConditions _lastSearch = const SearchConditions();
|
||||||
|
|
||||||
/// 79-a: 결과 화면에서 복사 · 이동 · 지우기를 한 뒤 [찾기 결과로 돌아가기] 로 다시 열 결과 (다음 찾기 · ✕ · 결과 화면을 그냥 닫으면 없어짐)
|
/// 79-a: 결과 화면에서 복사 · 이동 · 지우기 · [그 자리로] 를 한 뒤 [찾기 결과로 돌아가기] 로 다시 열 결과 (다음 찾기 · ✕ · 결과 화면을 그냥 닫으면 없어짐)
|
||||||
({_Pane pane, String root, SearchConditions cond, List<FileEntry> found, bool done, Map<String, String> moved})? _searchBack;
|
({_Pane pane, String root, SearchConditions cond, List<FileEntry> found, bool done, Map<String, String> moved})? _searchBack;
|
||||||
|
|
||||||
/// 79: 찾기 - 이름 · 크기 · 바뀐 날 · 종류 조건 → 결과 목록 화면 (고르고 그대로 복사 · 이동 · 지우기, 누르면 열기 · 재생)
|
/// 79: 찾기 - 이름 · 크기 · 바뀐 날 · 종류 조건 → 결과 목록 화면 (고르고 그대로 복사 · 이동 · 지우기, 누르면 열기 · 재생)
|
||||||
|
|
@ -2748,7 +2790,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
setState(() => _searchBack = null);
|
setState(() => _searchBack = null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (act.kind != 'reveal') {
|
// 관리자 판단 (10/17): [그 자리로] 로 나가도 남긴다 - 한 곳을 확인하고 다음 결과로 가는 것이 가장 흔한 쓰임
|
||||||
|
{
|
||||||
setState(() => _searchBack = (
|
setState(() => _searchBack = (
|
||||||
pane: pane,
|
pane: pane,
|
||||||
root: root,
|
root: root,
|
||||||
|
|
@ -2760,7 +2803,6 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
}
|
}
|
||||||
switch (act.kind) {
|
switch (act.kind) {
|
||||||
case 'reveal':
|
case 'reveal':
|
||||||
setState(() => _searchBack = null);
|
|
||||||
final path = act.paths.first;
|
final path = act.paths.first;
|
||||||
final dir = await vIsDir(path);
|
final dir = await vIsDir(path);
|
||||||
await _goTo(pane, dir ? path : vDirname(path), fresh: true);
|
await _goTo(pane, dir ? path : vDirname(path), fresh: true);
|
||||||
|
|
@ -3856,7 +3898,10 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
children: [
|
children: [
|
||||||
Text(name, maxLines: 1, overflow: TextOverflow.ellipsis, style: nameStyle),
|
Text(name, maxLines: 1, overflow: TextOverflow.ellipsis, style: nameStyle),
|
||||||
if (look.rich && e.isDir && pane.errors[e.path] != null)
|
if (look.rich && e.isDir && pane.errors[e.path] != null)
|
||||||
Text(tr('읽을 수 없음'), style: dim.copyWith(color: Colors.redAccent)),
|
Text(tr('읽을 수 없음'), style: dim.copyWith(color: Colors.redAccent))
|
||||||
|
// 80: 열 없는 모양 (X-plore · 좁은 화면) 은 폴더 이름 아래에 크기
|
||||||
|
else if (e.isDir && !row.isRoot && !row.isUp && (!look.columns || compact) && _dirSize(pane, e) != null)
|
||||||
|
Text(_dirSize(pane, e)!, style: dim, maxLines: 1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -3875,12 +3920,12 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
Expanded(flex: 6, child: nameCell()),
|
Expanded(flex: 6, child: nameCell()),
|
||||||
cell(_date(e.modified), 3),
|
cell(_date(e.modified), 3),
|
||||||
cell(typeLabel(e), 2),
|
cell(typeLabel(e), 2),
|
||||||
cell(e.isDir ? '' : formatSize(e.size), 2, align: TextAlign.end),
|
cell(e.isDir ? (_dirSize(pane, e) ?? '') : formatSize(e.size), 2, align: TextAlign.end),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
Expanded(flex: 6, child: nameCell()),
|
Expanded(flex: 6, child: nameCell()),
|
||||||
cell(e.isDir ? '' : e.ext, 1),
|
cell(e.isDir ? '' : e.ext, 1),
|
||||||
cell(e.isDir ? '<DIR>' : formatSize(e.size), 2, align: TextAlign.end),
|
cell(e.isDir ? (_dirSize(pane, e) ?? '<DIR>') : formatSize(e.size), 2, align: TextAlign.end),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
cell(_date(e.modified), 3),
|
cell(_date(e.modified), 3),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -767,6 +767,21 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
onChanged: (v) => c.updateSettings((x) => x.explorerClick = v!),
|
onChanged: (v) => c.updateSettings((x) => x.explorerClick = v!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 80: 폴더 크기 (필요할 때 계산하고 이번 실행 동안 기억)
|
||||||
|
SettingTile(
|
||||||
|
title: Text(tr('폴더 크기')),
|
||||||
|
subtitle: Text(tr('폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. '
|
||||||
|
'길게 누르기 [폴더 크기 계산] 은 늘 됩니다')),
|
||||||
|
trailing: DropdownButton<String>(
|
||||||
|
value: s.explorerFolderSize,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(value: 'sort', child: Text(tr('크기순 정렬일 때만 (기본)'))),
|
||||||
|
DropdownMenuItem(value: 'always', child: Text(tr('늘 계산'))),
|
||||||
|
DropdownMenuItem(value: 'off', child: Text(tr('계산 안 함'))),
|
||||||
|
],
|
||||||
|
onChanged: (v) => c.updateSettings((x) => x.explorerFolderSize = v ?? 'sort'),
|
||||||
|
),
|
||||||
|
),
|
||||||
SettingTile(
|
SettingTile(
|
||||||
title: Text(tr('스타일')),
|
title: Text(tr('스타일')),
|
||||||
subtitle: Text(tr('아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · '
|
subtitle: Text(tr('아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · '
|
||||||
|
|
|
||||||
|
|
@ -964,6 +964,58 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('80: 크기순 정렬이면 폴더 크기를 계산해 보이고, "안 함" 이어도 길게 누르기 [폴더 크기 계산]', (tester) async {
|
||||||
|
File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(3000, 1));
|
||||||
|
c.settings
|
||||||
|
..explorerSort = 'size'
|
||||||
|
..explorerSortDesc = true;
|
||||||
|
await open(tester);
|
||||||
|
// sub = inner.txt (5) + big.bin (3000)
|
||||||
|
await settle(tester, () => find.text('3KB').evaluate().isNotEmpty);
|
||||||
|
expect(find.text('3KB'), findsWidgets);
|
||||||
|
|
||||||
|
// 바뀌면 (새로 고침) 다시 계산
|
||||||
|
File(p.join(left, 'sub', 'more.bin')).writeAsBytesSync(List.filled(3000, 1));
|
||||||
|
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.f5));
|
||||||
|
await settle(tester, () => find.text('6KB').evaluate().isNotEmpty);
|
||||||
|
expect(find.text('6KB'), findsWidgets);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('80: "계산 안 함" - 크기를 보이지 않다가 [폴더 크기 계산] 을 누르면', (tester) async {
|
||||||
|
File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(3000, 1));
|
||||||
|
c.settings
|
||||||
|
..explorerSort = 'size'
|
||||||
|
..explorerFolderSize = 'off';
|
||||||
|
await open(tester);
|
||||||
|
await settle(tester, () => false, rounds: 15);
|
||||||
|
expect(find.text('3KB'), findsNothing);
|
||||||
|
await tester.longPress(find.text('sub').first);
|
||||||
|
await calm(tester);
|
||||||
|
await act(tester, () => tester.tap(find.text('폴더 크기 계산')));
|
||||||
|
await settle(tester, () => find.text('3KB').evaluate().isNotEmpty);
|
||||||
|
expect(find.text('3KB'), findsWidgets);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('79-a: [그 자리로] 로 나가도 [찾기 결과로 돌아가기] 가 남는다 (관리자 판단) · ✕ 로 닫음', (tester) async {
|
||||||
|
await open(tester);
|
||||||
|
await act(tester, () => tester.tap(find.text('찾기').first));
|
||||||
|
await calm(tester);
|
||||||
|
await tester.enterText(find.widgetWithText(TextField, '이름 (비워도 됨)'), 'inner');
|
||||||
|
await tester.pump();
|
||||||
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '찾기')));
|
||||||
|
await settle(tester, () => find.text('찾기 결과 · 1개').evaluate().isNotEmpty);
|
||||||
|
await act(tester, () => tester.tap(find.byTooltip('그 자리로')));
|
||||||
|
await settle(tester, () => find.text('찾기 결과로 돌아가기').evaluate().isNotEmpty);
|
||||||
|
await calm(tester); // 결과 화면이 닫히는 움직임까지
|
||||||
|
expect(find.byType(SearchResultsPage), findsNothing);
|
||||||
|
await act(tester, () => tester.tap(find.text('찾기 결과로 돌아가기')));
|
||||||
|
await settle(tester, () => find.text('찾기 결과 · 1개').evaluate().isNotEmpty);
|
||||||
|
// 결과 화면을 그냥 닫으면 없어진다
|
||||||
|
await act(tester, () => tester.pageBack());
|
||||||
|
await calm(tester);
|
||||||
|
expect(find.text('찾기 결과로 돌아가기'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('79: 찾기 결과를 목록으로 - 이름 없이 종류 조건으로 찾고, 골라서 그대로 다른 창으로 복사', (tester) async {
|
testWidgets('79: 찾기 결과를 목록으로 - 이름 없이 종류 조건으로 찾고, 골라서 그대로 다른 창으로 복사', (tester) async {
|
||||||
await open(tester);
|
await open(tester);
|
||||||
File(p.join(left, 'sub', 'big.mkv'))
|
File(p.join(left, 'sub', 'big.mkv'))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/file_ops.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/folder_size.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// 80: 폴더 크기 - 필요할 때 계산 · 기억, 읽지 못한 곳은 "+", 바뀌면 그 폴더와 위 폴더를 잊음, 크기순 정렬
|
||||||
|
void main() {
|
||||||
|
late Directory tmp;
|
||||||
|
setUp(() {
|
||||||
|
tmp = Directory.systemTemp.createTempSync('jj_fsize80_');
|
||||||
|
File(p.join(tmp.path, 'a', 'x.bin'))
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytesSync(List.filled(1000, 0));
|
||||||
|
File(p.join(tmp.path, 'a', 'deep', 'y.bin'))
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytesSync(List.filled(24, 0));
|
||||||
|
File(p.join(tmp.path, 'b', 'z.bin'))
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytesSync(List.filled(5, 0));
|
||||||
|
});
|
||||||
|
tearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
|
||||||
|
Future<void> idle(FolderSizes s, String path) async {
|
||||||
|
for (var i = 0; i < 200 && s.of(path) == null; i++) {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('안쪽 폴더까지 합 · 기억 · 바뀌면 그 폴더와 위 폴더를 잊음', () async {
|
||||||
|
var reads = 0;
|
||||||
|
final s = FolderSizes(list: (d) {
|
||||||
|
reads++;
|
||||||
|
return listEntries(d, showHidden: true);
|
||||||
|
});
|
||||||
|
final a = p.join(tmp.path, 'a');
|
||||||
|
s.request(a);
|
||||||
|
expect(s.busy(a), isTrue);
|
||||||
|
await idle(s, a);
|
||||||
|
expect(s.of(a)!.bytes, 1024);
|
||||||
|
expect(s.of(a)!.partial, isFalse);
|
||||||
|
final first = reads;
|
||||||
|
s.request(a);
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||||
|
expect(reads, first, reason: '아는 것은 다시 읽지 않는다');
|
||||||
|
|
||||||
|
// 안쪽 폴더 크기만 잊어도 위 폴더 (a) 도 잊는다
|
||||||
|
s.request(p.join(a, 'deep'));
|
||||||
|
await idle(s, p.join(a, 'deep'));
|
||||||
|
s.forget(p.join(a, 'deep'));
|
||||||
|
expect(s.of(a), isNull);
|
||||||
|
expect(s.of(p.join(a, 'deep')), isNull);
|
||||||
|
|
||||||
|
// 위 폴더를 잊으면 (새로 고침) 안쪽 폴더도
|
||||||
|
s.request(p.join(a, 'deep'));
|
||||||
|
await idle(s, p.join(a, 'deep'));
|
||||||
|
s.forget(tmp.path);
|
||||||
|
expect(s.of(p.join(a, 'deep')), isNull);
|
||||||
|
s.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('읽지 못한 폴더가 있으면 "+" (실제보다 작을 수 있음)', () async {
|
||||||
|
final a = p.join(tmp.path, 'a');
|
||||||
|
final s = FolderSizes(list: (d) {
|
||||||
|
if (p.basename(d) == 'deep') throw const FileSystemException('권한 없음');
|
||||||
|
return listEntries(d, showHidden: true);
|
||||||
|
});
|
||||||
|
s.request(a);
|
||||||
|
await idle(s, a);
|
||||||
|
expect(s.of(a)!.bytes, 1000);
|
||||||
|
expect(s.of(a)!.label, '1000B+');
|
||||||
|
s.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('크기순 정렬: 아는 폴더는 그 크기로, 모르는 폴더는 가장 작게 (폴더는 늘 위)', () {
|
||||||
|
final now = DateTime(2026);
|
||||||
|
final list = [
|
||||||
|
FileEntry('/r/f.bin', isDir: false, size: 10, modified: now),
|
||||||
|
FileEntry('/r/small', isDir: true, modified: now),
|
||||||
|
FileEntry('/r/big', isDir: true, modified: now),
|
||||||
|
FileEntry('/r/unknown', isDir: true, modified: now),
|
||||||
|
];
|
||||||
|
final sizes = {'/r/small': 5, '/r/big': 5000};
|
||||||
|
final up = sortEntries(list, SortBy.size, dirSize: (d) => sizes[d.path]);
|
||||||
|
expect(up.map((e) => e.name), ['unknown', 'small', 'big', 'f.bin']);
|
||||||
|
final down = sortEntries(list, SortBy.size, descending: true, dirSize: (d) => sizes[d.path]);
|
||||||
|
expect(down.map((e) => e.name), ['big', 'small', 'unknown', 'f.bin']);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue