WIP X-plore f: 압축 (ZIP · 7z · RAR) 을 트리 안에서 폴더처럼 펼치기 · 읽기 전용 (검토 대기)
- 압축 파일의 › 로 펼치면 안의 폴더 · 파일이 트리에 (목록에 폴더 이름이 없어도 경로에서 폴더를 만든다). 안의 파일을 열면 그 파일만 임시 폴더 (jj_arcview) 로 꺼내 연다 (보기 · 재생). - 읽기 전용 (관리자 지시): 압축 안의 이름 바꾸기 · 일괄 이름 · 지우기 · 이동은 막고 알림. 압축 안으로 붙여넣기 · 놓기 · 새 폴더도 막음. 압축 안의 것을 복사 · 붙여넣기 · 끌어 놓으면 86 의 풀기 창 (위험한 경로 · 링크 검사 · 같은 이름 · 갈 곳) 으로 꺼낸다. - 비밀번호가 걸린 압축 · 파일은 미리 보지 않고 [압축 풀기] 로 안내 (비밀번호를 명령 줄 · 기록에 남기지 않는 86 흐름 그대로). - 7z · RAR 은 동봉 7-Zip 이 있을 때만 (없으면 알림). WebDAV · SFTP 위의 압축과 Rsync 화면은 펼치지 않음. - 내 판단: 압축 안 폴더를 펼쳐 지금 폴더가 압축 안이 되면 [새 폴더] · 붙여넣기를 막는다 (캡처에서 발견). - 알려진 한계: 펼친 뒤 압축 파일이 바뀌면 다시 펼칠 때까지 예전 목록. - 시험: explorer_test "X-plore f", 캡처 release\test\shots_xplore_f (archive_ko/en_412/960). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore
parent
b6e47178e3
commit
a80d8bcafe
|
|
@ -1322,7 +1322,9 @@
|
||||||
"비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "Passwords · API keys (WebDAV · OpenSubtitles · AI services)",
|
"비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "Passwords · API keys (WebDAV · OpenSubtitles · AI services)",
|
||||||
"비밀번호 · 자막 API 키는 백업하지 않으므로 다시 넣어 주세요. 자동 백업은 하루에 한 번쯤 만들어져 그 뒤에 바꾼 것은 빠졌을 수 있습니다.": "Passwords and the subtitle API key aren't backed up, so please enter them again. Auto Backup runs about once a day, so changes made after it may be missing.",
|
"비밀번호 · 자막 API 키는 백업하지 않으므로 다시 넣어 주세요. 자동 백업은 하루에 한 번쯤 만들어져 그 뒤에 바꾼 것은 빠졌을 수 있습니다.": "Passwords and the subtitle API key aren't backed up, so please enter them again. Auto Backup runs about once a day, so changes made after it may be missing.",
|
||||||
"비밀번호 기억 (안전 저장소)": "Remember passwords (secure storage)",
|
"비밀번호 기억 (안전 저장소)": "Remember passwords (secure storage)",
|
||||||
|
"비밀번호가 걸린 압축은 안을 미리 볼 수 없습니다 - [압축 풀기] 로 푸세요": "Can't preview a password-protected archive - use [Extract]",
|
||||||
"비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)": "Some files are password-protected (you'll be asked when extracting)",
|
"비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)": "Some files are password-protected (you'll be asked when extracting)",
|
||||||
|
"비밀번호가 걸린 파일입니다 - [압축 풀기] 로 푸세요": "This file is password-protected - use [Extract]",
|
||||||
"비밀번호가 저장된 기능을 쓸 때": "When using features with saved passwords",
|
"비밀번호가 저장된 기능을 쓸 때": "When using features with saved passwords",
|
||||||
"비밀번호가 틀렸습니다": "Wrong password",
|
"비밀번호가 틀렸습니다": "Wrong password",
|
||||||
"비밀번호가 틀렸습니다 ({0}번)": "Wrong password ({0} times)",
|
"비밀번호가 틀렸습니다 ({0}번)": "Wrong password ({0} times)",
|
||||||
|
|
@ -1553,12 +1555,14 @@
|
||||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "{0} files inside are newer on the receiving side - merging overwrites them with older ones",
|
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "{0} files inside are newer on the receiving side - merging overwrites them with older ones",
|
||||||
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "Notifications show as JJ_MKVMaker and open the app when clicked (current user only, no admin rights)",
|
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "Notifications show as JJ_MKVMaker and open the app when clicked (current user only, no admin rights)",
|
||||||
"암호화 안 됨 (http)": "Not encrypted (http)",
|
"암호화 안 됨 (http)": "Not encrypted (http)",
|
||||||
|
"압축 안은 읽기 전용입니다 (꺼내기만 할 수 있습니다)": "Inside an archive is read-only (you can only extract)",
|
||||||
"압축 옆 새 폴더 \"{0}\"": "New folder \"{0}\" next to the archive",
|
"압축 옆 새 폴더 \"{0}\"": "New folder \"{0}\" next to the archive",
|
||||||
"압축 파일 안에 없습니다": "Not in the archive",
|
"압축 파일 안에 없습니다": "Not in the archive",
|
||||||
"압축 파일 안에 프로그램이 없습니다.": "The program is not in the archive.",
|
"압축 파일 안에 프로그램이 없습니다.": "The program is not in the archive.",
|
||||||
"압축 푸는 중 · {0}": "Extracting · {0}",
|
"압축 푸는 중 · {0}": "Extracting · {0}",
|
||||||
"압축 풀기": "Extract",
|
"압축 풀기": "Extract",
|
||||||
"압축 풀기 · {0}": "Extract · {0}",
|
"압축 풀기 · {0}": "Extract · {0}",
|
||||||
|
"압축에서 꺼내지 못했습니다: {0}": "Couldn't extract from the archive: {0}",
|
||||||
"압축을 읽지 못했습니다: {0}": "Couldn't read the archive: {0}",
|
"압축을 읽지 못했습니다: {0}": "Couldn't read the archive: {0}",
|
||||||
"압축을 풀 수 없습니다: {0}": "Cannot extract the archive: {0}",
|
"압축을 풀 수 없습니다: {0}": "Cannot extract the archive: {0}",
|
||||||
"압축이 있는 폴더": "Archive's folder",
|
"압축이 있는 폴더": "Archive's folder",
|
||||||
|
|
@ -1807,6 +1811,7 @@
|
||||||
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "Only an irreversible hash is stored on this device, not the password itself.",
|
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "Only an irreversible hash is stored on this device, not the password itself.",
|
||||||
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "About {0} per image on this device (based on last time)",
|
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "About {0} per image on this device (based on last time)",
|
||||||
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "About {0} per image, {1} in total on this device (based on last time)",
|
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "About {0} per image, {1} in total on this device (based on last time)",
|
||||||
|
"이 기기에서는 7z · RAR 안을 볼 수 없습니다 (다른 앱으로 열어 주세요)": "7z / RAR contents can't be browsed on this device (open it with another app)",
|
||||||
"이 기기에서는 AI 번역을 쓸 수 없습니다.": "AI translation is not available on this device.",
|
"이 기기에서는 AI 번역을 쓸 수 없습니다.": "AI translation is not available on this device.",
|
||||||
"이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요": "The AI engine was not found on this device. Try reinstalling the app",
|
"이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요": "The AI engine was not found on this device. Try reinstalling the app",
|
||||||
"이 기기에서는 쓸 수 없는 컴포넌트입니다": "This component is not available on this device",
|
"이 기기에서는 쓸 수 없는 컴포넌트입니다": "This component is not available on this device",
|
||||||
|
|
|
||||||
|
|
@ -1322,7 +1322,9 @@
|
||||||
"비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "パスワード・API キー (WebDAV・OpenSubtitles・AI サービス)",
|
"비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "パスワード・API キー (WebDAV・OpenSubtitles・AI サービス)",
|
||||||
"비밀번호 · 자막 API 키는 백업하지 않으므로 다시 넣어 주세요. 자동 백업은 하루에 한 번쯤 만들어져 그 뒤에 바꾼 것은 빠졌을 수 있습니다.": "パスワード・字幕 API キーはバックアップされないので、もう一度入力してください。自動バックアップは 1 日に 1 回程度作成されるため、その後に変更したものは含まれていない場合があります。",
|
"비밀번호 · 자막 API 키는 백업하지 않으므로 다시 넣어 주세요. 자동 백업은 하루에 한 번쯤 만들어져 그 뒤에 바꾼 것은 빠졌을 수 있습니다.": "パスワード・字幕 API キーはバックアップされないので、もう一度入力してください。自動バックアップは 1 日に 1 回程度作成されるため、その後に変更したものは含まれていない場合があります。",
|
||||||
"비밀번호 기억 (안전 저장소)": "パスワードを記憶 (安全な保存領域)",
|
"비밀번호 기억 (안전 저장소)": "パスワードを記憶 (安全な保存領域)",
|
||||||
|
"비밀번호가 걸린 압축은 안을 미리 볼 수 없습니다 - [압축 풀기] 로 푸세요": "パスワード付きアーカイブの中はプレビューできません - [展開] で展開してください",
|
||||||
"비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)": "パスワード付きのファイルがあります (展開時に確認します)",
|
"비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)": "パスワード付きのファイルがあります (展開時に確認します)",
|
||||||
|
"비밀번호가 걸린 파일입니다 - [압축 풀기] 로 푸세요": "パスワード付きのファイルです - [展開] で展開してください",
|
||||||
"비밀번호가 저장된 기능을 쓸 때": "パスワードを保存した機能を使うとき",
|
"비밀번호가 저장된 기능을 쓸 때": "パスワードを保存した機能を使うとき",
|
||||||
"비밀번호가 틀렸습니다": "パスワードが違います",
|
"비밀번호가 틀렸습니다": "パスワードが違います",
|
||||||
"비밀번호가 틀렸습니다 ({0}번)": "パスワードが違います ({0} 回)",
|
"비밀번호가 틀렸습니다 ({0}번)": "パスワードが違います ({0} 回)",
|
||||||
|
|
@ -1553,12 +1555,14 @@
|
||||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "中に受け取る側の方が新しいファイルが {0} 件 - 合わせると古いもので上書きします",
|
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "中に受け取る側の方が新しいファイルが {0} 件 - 合わせると古いもので上書きします",
|
||||||
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知が JJ_MKVMaker の名前で表示され、クリックするとアプリが開きます (管理者権限なしで現在のユーザーのみ)",
|
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知が JJ_MKVMaker の名前で表示され、クリックするとアプリが開きます (管理者権限なしで現在のユーザーのみ)",
|
||||||
"암호화 안 됨 (http)": "暗号化なし (http)",
|
"암호화 안 됨 (http)": "暗号化なし (http)",
|
||||||
|
"압축 안은 읽기 전용입니다 (꺼내기만 할 수 있습니다)": "アーカイブ内は読み取り専用です (取り出しのみできます)",
|
||||||
"압축 옆 새 폴더 \"{0}\"": "アーカイブの隣の新しいフォルダー「{0}」",
|
"압축 옆 새 폴더 \"{0}\"": "アーカイブの隣の新しいフォルダー「{0}」",
|
||||||
"압축 파일 안에 없습니다": "圧縮ファイル内にありません",
|
"압축 파일 안에 없습니다": "圧縮ファイル内にありません",
|
||||||
"압축 파일 안에 프로그램이 없습니다.": "圧縮ファイル内にプログラムがありません。",
|
"압축 파일 안에 프로그램이 없습니다.": "圧縮ファイル内にプログラムがありません。",
|
||||||
"압축 푸는 중 · {0}": "展開中 · {0}",
|
"압축 푸는 중 · {0}": "展開中 · {0}",
|
||||||
"압축 풀기": "展開",
|
"압축 풀기": "展開",
|
||||||
"압축 풀기 · {0}": "展開 · {0}",
|
"압축 풀기 · {0}": "展開 · {0}",
|
||||||
|
"압축에서 꺼내지 못했습니다: {0}": "アーカイブから取り出せませんでした: {0}",
|
||||||
"압축을 읽지 못했습니다: {0}": "アーカイブを読み込めませんでした: {0}",
|
"압축을 읽지 못했습니다: {0}": "アーカイブを読み込めませんでした: {0}",
|
||||||
"압축을 풀 수 없습니다: {0}": "圧縮ファイルを展開できません: {0}",
|
"압축을 풀 수 없습니다: {0}": "圧縮ファイルを展開できません: {0}",
|
||||||
"압축이 있는 폴더": "アーカイブのあるフォルダー",
|
"압축이 있는 폴더": "アーカイブのあるフォルダー",
|
||||||
|
|
@ -1807,6 +1811,7 @@
|
||||||
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "この端末にはパスワード自体ではなく、元に戻せないハッシュだけが残ります。",
|
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "この端末にはパスワード自体ではなく、元に戻せないハッシュだけが残ります。",
|
||||||
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0} (前回の所要時間から)",
|
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0} (前回の所要時間から)",
|
||||||
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0}・全部で約 {1} (前回の所要時間から)",
|
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0}・全部で約 {1} (前回の所要時間から)",
|
||||||
|
"이 기기에서는 7z · RAR 안을 볼 수 없습니다 (다른 앱으로 열어 주세요)": "この端末では 7z・RAR の中を表示できません (他のアプリで開いてください)",
|
||||||
"이 기기에서는 AI 번역을 쓸 수 없습니다.": "この端末では AI 翻訳を使えません。",
|
"이 기기에서는 AI 번역을 쓸 수 없습니다.": "この端末では AI 翻訳を使えません。",
|
||||||
"이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요": "この端末で AI エンジンが見つかりませんでした。アプリを再インストールしてください",
|
"이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요": "この端末で AI エンジンが見つかりませんでした。アプリを再インストールしてください",
|
||||||
"이 기기에서는 쓸 수 없는 컴포넌트입니다": "この端末では使えないコンポーネントです",
|
"이 기기에서는 쓸 수 없는 컴포넌트입니다": "この端末では使えないコンポーネントです",
|
||||||
|
|
|
||||||
|
|
@ -1322,7 +1322,9 @@
|
||||||
"비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "密码 · API 密钥 (WebDAV · OpenSubtitles · AI 服务)",
|
"비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "密码 · API 密钥 (WebDAV · OpenSubtitles · AI 服务)",
|
||||||
"비밀번호 · 자막 API 키는 백업하지 않으므로 다시 넣어 주세요. 자동 백업은 하루에 한 번쯤 만들어져 그 뒤에 바꾼 것은 빠졌을 수 있습니다.": "密码 · 字幕 API 密钥不会备份,请重新输入。自动备份大约每天创建一次,之后更改的内容可能缺失。",
|
"비밀번호 · 자막 API 키는 백업하지 않으므로 다시 넣어 주세요. 자동 백업은 하루에 한 번쯤 만들어져 그 뒤에 바꾼 것은 빠졌을 수 있습니다.": "密码 · 字幕 API 密钥不会备份,请重新输入。自动备份大约每天创建一次,之后更改的内容可能缺失。",
|
||||||
"비밀번호 기억 (안전 저장소)": "记住密码 (安全存储)",
|
"비밀번호 기억 (안전 저장소)": "记住密码 (安全存储)",
|
||||||
|
"비밀번호가 걸린 압축은 안을 미리 볼 수 없습니다 - [압축 풀기] 로 푸세요": "无法预览有密码的压缩包 - 请用 [解压]",
|
||||||
"비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)": "部分文件设有密码 (解压时会询问)",
|
"비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)": "部分文件设有密码 (解压时会询问)",
|
||||||
|
"비밀번호가 걸린 파일입니다 - [압축 풀기] 로 푸세요": "此文件有密码 - 请用 [解压]",
|
||||||
"비밀번호가 저장된 기능을 쓸 때": "使用保存了密码的功能时",
|
"비밀번호가 저장된 기능을 쓸 때": "使用保存了密码的功能时",
|
||||||
"비밀번호가 틀렸습니다": "密码错误",
|
"비밀번호가 틀렸습니다": "密码错误",
|
||||||
"비밀번호가 틀렸습니다 ({0}번)": "密码错误 ({0} 次)",
|
"비밀번호가 틀렸습니다 ({0}번)": "密码错误 ({0} 次)",
|
||||||
|
|
@ -1553,12 +1555,14 @@
|
||||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "里面有 {0} 个文件在接收端较新 - 合并会用旧文件覆盖",
|
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "里面有 {0} 个文件在接收端较新 - 合并会用旧文件覆盖",
|
||||||
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知以 JJ_MKVMaker 名义显示,点击即可打开应用 (仅当前用户,无需管理员权限)",
|
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知以 JJ_MKVMaker 名义显示,点击即可打开应用 (仅当前用户,无需管理员权限)",
|
||||||
"암호화 안 됨 (http)": "未加密 (http)",
|
"암호화 안 됨 (http)": "未加密 (http)",
|
||||||
|
"압축 안은 읽기 전용입니다 (꺼내기만 할 수 있습니다)": "压缩包内为只读 (只能解压取出)",
|
||||||
"압축 옆 새 폴더 \"{0}\"": "压缩包旁的新文件夹“{0}”",
|
"압축 옆 새 폴더 \"{0}\"": "压缩包旁的新文件夹“{0}”",
|
||||||
"압축 파일 안에 없습니다": "压缩文件中没有",
|
"압축 파일 안에 없습니다": "压缩文件中没有",
|
||||||
"압축 파일 안에 프로그램이 없습니다.": "压缩文件中没有程序。",
|
"압축 파일 안에 프로그램이 없습니다.": "压缩文件中没有程序。",
|
||||||
"압축 푸는 중 · {0}": "正在解压 · {0}",
|
"압축 푸는 중 · {0}": "正在解压 · {0}",
|
||||||
"압축 풀기": "解压",
|
"압축 풀기": "解压",
|
||||||
"압축 풀기 · {0}": "解压 · {0}",
|
"압축 풀기 · {0}": "解压 · {0}",
|
||||||
|
"압축에서 꺼내지 못했습니다: {0}": "无法从压缩包取出: {0}",
|
||||||
"압축을 읽지 못했습니다: {0}": "无法读取压缩包: {0}",
|
"압축을 읽지 못했습니다: {0}": "无法读取压缩包: {0}",
|
||||||
"압축을 풀 수 없습니다: {0}": "无法解压: {0}",
|
"압축을 풀 수 없습니다: {0}": "无法解压: {0}",
|
||||||
"압축이 있는 폴더": "压缩包所在文件夹",
|
"압축이 있는 폴더": "压缩包所在文件夹",
|
||||||
|
|
@ -1807,6 +1811,7 @@
|
||||||
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "本设备只保存无法还原的哈希值,而不是密码本身。",
|
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "本设备只保存无法还原的哈希值,而不是密码本身。",
|
||||||
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "本设备每张约 {0} (按上次耗时)",
|
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "本设备每张约 {0} (按上次耗时)",
|
||||||
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "本设备每张约 {0} · 共约 {1} (按上次耗时)",
|
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "本设备每张约 {0} · 共约 {1} (按上次耗时)",
|
||||||
|
"이 기기에서는 7z · RAR 안을 볼 수 없습니다 (다른 앱으로 열어 주세요)": "此设备无法浏览 7z / RAR 内容 (请用其他应用打开)",
|
||||||
"이 기기에서는 AI 번역을 쓸 수 없습니다.": "此设备无法使用 AI 翻译。",
|
"이 기기에서는 AI 번역을 쓸 수 없습니다.": "此设备无法使用 AI 翻译。",
|
||||||
"이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요": "在此设备上找不到 AI 引擎。请尝试重新安装应用",
|
"이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요": "在此设备上找不到 AI 引擎。请尝试重新安装应用",
|
||||||
"이 기기에서는 쓸 수 없는 컴포넌트입니다": "此组件在本设备上不可用",
|
"이 기기에서는 쓸 수 없는 컴포넌트입니다": "此组件在本设备上不可用",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:archive/archive.dart' show Archive, ArchiveFile, ZipEncoder;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
@ -16,7 +17,7 @@ import 'package:jj_mkvmaker/ui/theme.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
/// X-plore 비교 P1 (관리자 지시 10/18): X-plore 스타일 탐색기를 PNG 로 (JJ_SHOT_DIR, 없으면 임시 폴더).
|
/// X-plore 비교 P1 (관리자 지시 10/18): X-plore 스타일 탐색기를 PNG 로 (JJ_SHOT_DIR, 없으면 임시 폴더).
|
||||||
/// 트리 맨 위 저장 장치 · 즐겨찾기 (c) · 표시 동그라미 늘 보이기 (⚖) · 한 창의 "갈 곳" 줄 (a).
|
/// 트리 맨 위 저장 장치 · 즐겨찾기 (c) · 표시 동그라미 늘 보이기 (⚖) · 한 창의 "갈 곳" 줄 (a) · 압축을 폴더처럼 (f).
|
||||||
/// 크기 412 (폰 세로, 한 창) · 960 (두 창), 언어 ko · en. 시험 파일은 임시 폴더에만, 설정은 메모리에만.
|
/// 크기 412 (폰 세로, 한 창) · 960 (두 창), 언어 ko · en. 시험 파일은 임시 폴더에만, 설정은 메모리에만.
|
||||||
void main() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
@ -39,6 +40,9 @@ void main() {
|
||||||
}
|
}
|
||||||
File(p.join(a.path, 'Show S01', 'ep01.mkv')).writeAsBytesSync(List.filled(5000, 0));
|
File(p.join(a.path, 'Show S01', 'ep01.mkv')).writeAsBytesSync(List.filled(5000, 0));
|
||||||
File(p.join(a.path, 'notes.txt')).writeAsStringSync('memo');
|
File(p.join(a.path, 'notes.txt')).writeAsStringSync('memo');
|
||||||
|
File(p.join(a.path, 'Photos.zip')).writeAsBytesSync(ZipEncoder().encode(Archive()
|
||||||
|
..addFile(ArchiveFile.string('2026/beach.jpg', 'x'))
|
||||||
|
..addFile(ArchiveFile.string('readme.txt', 'readme'))));
|
||||||
ExplorerPage.debugVolumes = () => [(a.path, 'C:'), (b.path, 'D:')];
|
ExplorerPage.debugVolumes = () => [(a.path, 'C:'), (b.path, 'D:')];
|
||||||
addTearDown(() => ExplorerPage.debugVolumes = null);
|
addTearDown(() => ExplorerPage.debugVolumes = null);
|
||||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
|
@ -76,6 +80,14 @@ void main() {
|
||||||
await t.tap(find.byKey(const ValueKey<String>('k2:\u0000favorites')).first);
|
await t.tap(find.byKey(const ValueKey<String>('k2:\u0000favorites')).first);
|
||||||
await settle();
|
await settle();
|
||||||
await shot('tree');
|
await shot('tree');
|
||||||
|
// f: 압축을 › 로 펼치기 (안의 폴더도)
|
||||||
|
Finder chevron(String name) => find.descendant(
|
||||||
|
of: find.ancestor(of: find.text(name), matching: find.byType(InkWell)).first, matching: find.byIcon(Icons.chevron_right));
|
||||||
|
await t.tap(chevron('Photos.zip').first);
|
||||||
|
await settle();
|
||||||
|
await t.tap(chevron('2026').first);
|
||||||
|
await settle();
|
||||||
|
await shot('archive');
|
||||||
await t.pumpWidget(const SizedBox());
|
await t.pumpWidget(const SizedBox());
|
||||||
await settle(4);
|
await settle(4);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ import 'quick_look.dart';
|
||||||
import 'explorer_drag.dart';
|
import 'explorer_drag.dart';
|
||||||
import 'archive_ui.dart';
|
import 'archive_ui.dart';
|
||||||
import 'storage_page.dart';
|
import 'storage_page.dart';
|
||||||
import '../core/archive_ops.dart' show ArchiveKind, archiveKindOf;
|
import '../core/archive_ops.dart'
|
||||||
|
show ArchiveEntry, ArchiveKind, ArchivePasswordException, archiveKindOf, extract7zPlan, extractZipPlan, list7z, listZip, planExtract, unsafeArchivePath;
|
||||||
import 'batch_rename_dialog.dart';
|
import 'batch_rename_dialog.dart';
|
||||||
import '../core/app_trash.dart';
|
import '../core/app_trash.dart';
|
||||||
import '../core/folder_size.dart';
|
import '../core/folder_size.dart';
|
||||||
|
|
@ -786,6 +787,12 @@ 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;
|
||||||
|
// X-plore 비교 f: 압축 안 (읽어 둔 목록에서)
|
||||||
|
if (_archiveOf(dir) != null) {
|
||||||
|
pane.cache[dir] = _sorted(_archiveList(dir), dir);
|
||||||
|
pane.changed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 80: 다시 읽는 폴더는 안이 바뀌었을 수 있다 - 그 폴더와 위 폴더들의 크기를 잊는다
|
// 80: 다시 읽는 폴더는 안이 바뀌었을 수 있다 - 그 폴더와 위 폴더들의 크기를 잊는다
|
||||||
if (force) _sizes.forget(dir);
|
if (force) _sizes.forget(dir);
|
||||||
pane.loading.add(dir);
|
pane.loading.add(dir);
|
||||||
|
|
@ -1035,7 +1042,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
if (foldersOnly && !e.isDir) continue;
|
if (foldersOnly && !e.isDir) continue;
|
||||||
final at = out.length;
|
final at = out.length;
|
||||||
out.add(_Row(e, depth));
|
out.add(_Row(e, depth));
|
||||||
final inner = e.isDir && walk(e.path, depth + 1);
|
// X-plore 비교 f: 펼친 압축 (ZIP · 7z · RAR) 도 안을 폴더처럼
|
||||||
|
final inner = (e.isDir || _archives.containsKey(e.path)) && walk(e.path, depth + 1);
|
||||||
if (!inner && !_passes(pane, e)) {
|
if (!inner && !_passes(pane, e)) {
|
||||||
out.length = at;
|
out.length = at;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1363,6 +1371,14 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
/// 트리 폴더 펼치기 · 접기 (› 화살표 · 열기)
|
/// 트리 폴더 펼치기 · 접기 (› 화살표 · 열기)
|
||||||
Future<void> _toggle(_Pane pane, FileEntry e) async {
|
Future<void> _toggle(_Pane pane, FileEntry e) async {
|
||||||
setState(() => _active = _panes.indexOf(pane));
|
setState(() => _active = _panes.indexOf(pane));
|
||||||
|
// X-plore 비교 f: 압축 파일을 폴더처럼 펼치기 (읽기 전용)
|
||||||
|
if (_canBrowseArchive(e) && !pane.expanded.contains(e.path)) {
|
||||||
|
if (!await _readArchive(e.path)) return;
|
||||||
|
pane.cache.remove(e.path);
|
||||||
|
pane.expanded.add(e.path);
|
||||||
|
await _load(pane, e.path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (pane.expanded.contains(e.path)) {
|
if (pane.expanded.contains(e.path)) {
|
||||||
pane.expanded.remove(e.path);
|
pane.expanded.remove(e.path);
|
||||||
// 접은 폴더 안이 지금 폴더였으면 접은 폴더로
|
// 접은 폴더 안이 지금 폴더였으면 접은 폴더로
|
||||||
|
|
@ -1386,6 +1402,12 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
|
|
||||||
/// 파일 열기 (기본 동작): 동영상은 내장 플레이어 (또는 확장자별 프로그램), 그 밖은 기본 연결 프로그램
|
/// 파일 열기 (기본 동작): 동영상은 내장 플레이어 (또는 확장자별 프로그램), 그 밖은 기본 연결 프로그램
|
||||||
Future<void> _open(String path, {_Pane? pane}) async {
|
Future<void> _open(String path, {_Pane? pane}) async {
|
||||||
|
// X-plore 비교 f: 압축 안의 파일 - 그 파일만 임시 폴더로 꺼내 연다
|
||||||
|
if (_insideArchive(path)) {
|
||||||
|
final local = await _extractOne(path);
|
||||||
|
if (local == null || !mounted) return;
|
||||||
|
path = local;
|
||||||
|
}
|
||||||
if (isVideoFile(path)) {
|
if (isVideoFile(path)) {
|
||||||
await _play([path]);
|
await _play([path]);
|
||||||
return;
|
return;
|
||||||
|
|
@ -1788,6 +1810,121 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
_Pane get _pane => _panes[_active];
|
_Pane get _pane => _panes[_active];
|
||||||
_Pane? get _other => _dual || _hiddenTarget ? _panes[1 - _active] : null;
|
_Pane? get _other => _dual || _hiddenTarget ? _panes[1 - _active] : null;
|
||||||
|
|
||||||
|
// ───────── X-plore 비교 f: 압축을 트리 안에서 폴더처럼 (ZIP · 7z · RAR, 읽기 전용) ─────────
|
||||||
|
|
||||||
|
/// 펼쳐 본 압축의 목록 (압축 경로 → 항목)
|
||||||
|
final _archives = <String, List<ArchiveEntry>>{};
|
||||||
|
|
||||||
|
/// 트리에서 펼칠 수 있는 압축 (이 기기의 파일 · 탐색기 화면)
|
||||||
|
bool _canBrowseArchive(FileEntry e) => !widget.rsync && !e.isDir && !isDav(e.path) && archiveKindOf(e.path) != null;
|
||||||
|
|
||||||
|
/// [path] 가 들어 있는 (펼쳐 둔) 압축 - 압축 자신도
|
||||||
|
String? _archiveOf(String path) {
|
||||||
|
String? best;
|
||||||
|
for (final a in _archives.keys) {
|
||||||
|
if ((samePath(path, a) || isSameOrInside(path, a)) && (best == null || a.length > best.length)) best = a;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 압축 안의 항목인지 (압축 파일 자신은 아님)
|
||||||
|
bool _insideArchive(String path) {
|
||||||
|
final a = _archiveOf(path);
|
||||||
|
return a != null && !samePath(a, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 압축 안 경로 → 압축 안의 이름 ("폴더/파일")
|
||||||
|
String _innerName(String archive, String path) => p.relative(path, from: archive).replaceAll('\\', '/');
|
||||||
|
|
||||||
|
/// 압축의 목록을 읽어 둔다 (비밀번호가 걸린 7z 등은 [압축 풀기] 로 안내). 읽었으면 true
|
||||||
|
Future<bool> _readArchive(String path) async {
|
||||||
|
if (_archives.containsKey(path)) return true;
|
||||||
|
final kind = archiveKindOf(path)!;
|
||||||
|
try {
|
||||||
|
if (kind == ArchiveKind.zip) {
|
||||||
|
_archives[path] = await listZip(path);
|
||||||
|
} else {
|
||||||
|
final exe = await bundledSevenZip();
|
||||||
|
if (exe == null) {
|
||||||
|
_snack(tr('이 기기에서는 7z · RAR 안을 볼 수 없습니다 (다른 앱으로 열어 주세요)'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_archives[path] = await list7z(exe, path);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} on ArchivePasswordException {
|
||||||
|
_snack(tr('비밀번호가 걸린 압축은 안을 미리 볼 수 없습니다 - [압축 풀기] 로 푸세요'));
|
||||||
|
} catch (e) {
|
||||||
|
_snack(trf('압축을 읽지 못했습니다: {0}', [e]));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 압축 안 폴더 [dir] 의 바로 아래 항목 (목록의 폴더 이름이 없어도 경로에서 폴더를 만든다)
|
||||||
|
List<FileEntry> _archiveList(String dir) {
|
||||||
|
final arc = _archiveOf(dir)!;
|
||||||
|
final rel = samePath(dir, arc) ? '' : _innerName(arc, dir);
|
||||||
|
final out = <String, FileEntry>{};
|
||||||
|
for (final a in _archives[arc]!) {
|
||||||
|
if (a.isLink) continue;
|
||||||
|
final n = a.name.replaceAll('\\', '/').replaceAll(RegExp(r'^(\./)+|/+$'), '');
|
||||||
|
if (n.isEmpty || unsafeArchivePath(n) != null) continue;
|
||||||
|
if (rel.isNotEmpty && !n.startsWith('$rel/')) continue;
|
||||||
|
final rest = rel.isEmpty ? n : n.substring(rel.length + 1);
|
||||||
|
if (rest.isEmpty) continue;
|
||||||
|
final first = rest.split('/').first;
|
||||||
|
final folder = rest.contains('/') || a.isDir;
|
||||||
|
final path = p.joinAll([arc, ...(rel.isEmpty ? first : '$rel/$first').split('/')]);
|
||||||
|
final old = out[first];
|
||||||
|
if (old == null || (folder && !old.isDir)) {
|
||||||
|
out[first] = FileEntry(path, isDir: folder, size: folder ? 0 : a.size, modified: a.modified ?? DateTime(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.values.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 압축 안의 파일 하나를 임시 폴더로 꺼낸다 (보기 · 재생). 꺼낸 경로 (못 하면 null)
|
||||||
|
Future<String?> _extractOne(String path) async {
|
||||||
|
final arc = _archiveOf(path)!;
|
||||||
|
final inner = _innerName(arc, path);
|
||||||
|
final dest = p.join(await c.services.storage.tempDirectory(), 'jj_arcview', '${arc.hashCode.toUnsigned(32)}');
|
||||||
|
final plan = planExtract(_archives[arc]!, names: [inner]);
|
||||||
|
if (plan.encrypted) {
|
||||||
|
_snack(tr('비밀번호가 걸린 파일입니다 - [압축 풀기] 로 푸세요'));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (archiveKindOf(arc) == ArchiveKind.zip) {
|
||||||
|
await extractZipPlan(arc, dest, plan, conflict: NameConflict.overwrite);
|
||||||
|
} else {
|
||||||
|
final exe = await bundledSevenZip();
|
||||||
|
if (exe == null) return null;
|
||||||
|
await extract7zPlan(exe, arc, dest, plan, conflict: NameConflict.overwrite);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_snack(trf('압축에서 꺼내지 못했습니다: {0}', [e]));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final out = p.joinAll([dest, ...inner.split('/')]);
|
||||||
|
return File(out).existsSync() ? out : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 압축 안의 것을 꺼내기 - 86 의 풀기 창 (검사 · 갈 곳 · 같은 이름) 그대로
|
||||||
|
Future<void> _extractFromArchive(List<String> paths, String? otherDir) async {
|
||||||
|
final arc = _archiveOf(paths.firstWhere(_insideArchive))!;
|
||||||
|
final names = [for (final x in paths) if (_insideArchive(x) && samePath(_archiveOf(x)!, arc)) _innerName(arc, x)];
|
||||||
|
final done = await extractArchiveFlow(context, c, arc, otherDir: otherDir, names: names);
|
||||||
|
if (done != null && mounted) await _refreshAll([done]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 압축 안을 바꾸려 하면 막고 알린다 (읽기 전용). 막았으면 true
|
||||||
|
/// [self] 면 압축 파일 자신도 (그 안으로 넣는 곳일 때)
|
||||||
|
bool _blockArchive(List<String> paths, {bool self = false}) {
|
||||||
|
if (!paths.any((x) => _insideArchive(x) || (self && _archives.containsKey(x)))) return false;
|
||||||
|
_snack(tr('압축 안은 읽기 전용입니다 (꺼내기만 할 수 있습니다)'));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// X-plore 비교 a: 한 창인데 "숨은 다른 창으로" 복사 · 이동 (위에 갈 곳 줄)
|
/// X-plore 비교 a: 한 창인데 "숨은 다른 창으로" 복사 · 이동 (위에 갈 곳 줄)
|
||||||
bool get _hiddenTarget => !widget.rsync && !_dual && !_split && c.settings.explorerOnePaneCopy == 'other';
|
bool get _hiddenTarget => !widget.rsync && !_dual && !_split && c.settings.explorerOnePaneCopy == 'other';
|
||||||
|
|
||||||
|
|
@ -1929,6 +2066,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
final t = _targets(pane);
|
final t = _targets(pane);
|
||||||
await _addMkv(t.isEmpty ? [pane.current] : t);
|
await _addMkv(t.isEmpty ? [pane.current] : t);
|
||||||
case ExplorerButton.newFolder:
|
case ExplorerButton.newFolder:
|
||||||
|
// X-plore 비교 f: 펼친 압축 안 (지금 폴더가 압축 안의 폴더) 에는 만들지 않는다
|
||||||
|
if (_blockArchive([pane.current], self: true)) return;
|
||||||
final name = await _askName(tr('새 폴더'), '');
|
final name = await _askName(tr('새 폴더'), '');
|
||||||
if (name == null) return;
|
if (name == null) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -2041,6 +2180,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _rename(_Pane pane, String path) async {
|
Future<void> _rename(_Pane pane, String path) async {
|
||||||
|
if (_blockArchive([path])) return;
|
||||||
final name = await _askName(tr('이름 변경'), vBasename(path));
|
final name = await _askName(tr('이름 변경'), vBasename(path));
|
||||||
if (name == null || name == vBasename(path)) return;
|
if (name == null || name == vBasename(path)) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -2058,6 +2198,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
|
|
||||||
/// 78: 일괄 이름 바꾸기 - 번호는 목록에 보이는 차례대로. 끝나면 [되돌리기]
|
/// 78: 일괄 이름 바꾸기 - 번호는 목록에 보이는 차례대로. 끝나면 [되돌리기]
|
||||||
Future<void> _batchRename(_Pane pane, List<String> paths) async {
|
Future<void> _batchRename(_Pane pane, List<String> paths) async {
|
||||||
|
if (_blockArchive(paths)) return;
|
||||||
int at(String x) {
|
int at(String x) {
|
||||||
final i = (pane.cache[vDirname(x)] ?? const <FileEntry>[]).indexWhere((e) => samePath(e.path, x));
|
final i = (pane.cache[vDirname(x)] ?? const <FileEntry>[]).indexWhere((e) => samePath(e.path, x));
|
||||||
return i < 0 ? 1 << 30 : i;
|
return i < 0 ? 1 << 30 : i;
|
||||||
|
|
@ -2106,6 +2247,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
/// 지우기. Windows 의 로컬 파일은 기본으로 휴지통으로 (창에서 "영구 삭제" 를 고르거나 Shift+Delete).
|
/// 지우기. Windows 의 로컬 파일은 기본으로 휴지통으로 (창에서 "영구 삭제" 를 고르거나 Shift+Delete).
|
||||||
/// WebDAV · Android 는 휴지통이 없어 바로 지운다. 여러 개면 실패해도 나머지를 계속하고, 못 지운 것을 알린다 (65).
|
/// WebDAV · Android 는 휴지통이 없어 바로 지운다. 여러 개면 실패해도 나머지를 계속하고, 못 지운 것을 알린다 (65).
|
||||||
Future<void> _delete(_Pane pane, List<String> paths) async {
|
Future<void> _delete(_Pane pane, List<String> paths) async {
|
||||||
|
if (_blockArchive(paths)) return;
|
||||||
paths = outermostPaths(paths); // 79-c: 고른 폴더 안의 것은 한 번만 (확인 창 개수도)
|
paths = outermostPaths(paths); // 79-c: 고른 폴더 안의 것은 한 번만 (확인 창 개수도)
|
||||||
final shift = _shiftDelete;
|
final shift = _shiftDelete;
|
||||||
_shiftDelete = false;
|
_shiftDelete = false;
|
||||||
|
|
@ -2398,6 +2540,15 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
/// 다른 창의 지금 폴더로 복사 · 이동 (진행 창 · 취소)
|
/// 다른 창의 지금 폴더로 복사 · 이동 (진행 창 · 취소)
|
||||||
/// 복사 · 이동 버튼: 두 창이면 다른 창의 지금 폴더로, 그 밖의 배치는 담아 두기 (붙여넣기로)
|
/// 복사 · 이동 버튼: 두 창이면 다른 창의 지금 폴더로, 그 밖의 배치는 담아 두기 (붙여넣기로)
|
||||||
Future<void> _transfer(_Pane pane, List<String> sources, {required bool move}) async {
|
Future<void> _transfer(_Pane pane, List<String> sources, {required bool move}) async {
|
||||||
|
// X-plore 비교 f: 압축 안의 것 - 복사는 꺼내기 (86 의 풀기 검사 그대로), 이동은 막음
|
||||||
|
if (sources.any(_insideArchive)) {
|
||||||
|
if (move) {
|
||||||
|
_blockArchive(sources);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _extractFromArchive(sources, _other?.current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
sources = outermostPaths(sources); // 79-c: 고른 폴더 안의 것은 한 번만
|
sources = outermostPaths(sources); // 79-c: 고른 폴더 안의 것은 한 번만
|
||||||
final other = _other;
|
final other = _other;
|
||||||
if (other == null) {
|
if (other == null) {
|
||||||
|
|
@ -2511,6 +2662,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
_snack(_pickFirst(move));
|
_snack(_pickFirst(move));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// X-plore 비교 f: 압축 안으로는 넣지 않고 (읽기 전용), 압축 안의 것을 붙여넣으면 꺼내기로
|
||||||
|
if (_blockArchive([dest], self: true)) return false;
|
||||||
|
if (sources.any(_insideArchive)) {
|
||||||
|
if (move) return !_blockArchive(sources);
|
||||||
|
await _extractFromArchive(sources, dest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// 23: 앞의 복사 · 이동이 끝나지 않아도 함께 시작한다 (아래 진행 막대에 하나씩). 같은 것을 같은 곳으로만 막는다
|
// 23: 앞의 복사 · 이동이 끝나지 않아도 함께 시작한다 (아래 진행 막대에 하나씩). 같은 것을 같은 곳으로만 막는다
|
||||||
if (_jobs.any((j) => !j.finished && samePath(j.dest, dest) && j.sources.toSet().containsAll(sources))) {
|
if (_jobs.any((j) => !j.finished && samePath(j.dest, dest) && j.sources.toSet().containsAll(sources))) {
|
||||||
_snack(tr('같은 항목을 같은 곳으로 복사 · 이동하는 중입니다.'));
|
_snack(tr('같은 항목을 같은 곳으로 복사 · 이동하는 중입니다.'));
|
||||||
|
|
@ -3888,6 +4046,15 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
/// 이동이 끝나면 아래 알림에 [되돌리기] (10초)
|
/// 이동이 끝나면 아래 알림에 [되돌리기] (10초)
|
||||||
Future<void> _onDrop(DragLoad load, int to, String dest) async {
|
Future<void> _onDrop(DragLoad load, int to, String dest) async {
|
||||||
if (!canDropInto(load, to, dest) || !mounted) return;
|
if (!canDropInto(load, to, dest) || !mounted) return;
|
||||||
|
// X-plore 비교 f: 압축 안으로는 놓지 않고, 압축 안의 것은 꺼내기로
|
||||||
|
if (_insideArchive(dest) || _archives.containsKey(dest)) {
|
||||||
|
_snack(tr('압축 안은 읽기 전용입니다 (꺼내기만 할 수 있습니다)'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (load.paths.any(_insideArchive)) {
|
||||||
|
await _extractFromArchive(load.paths, dest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() => _active = to);
|
setState(() => _active = to);
|
||||||
final mode = c.settings.explorerDropAction;
|
final mode = c.settings.explorerDropAction;
|
||||||
final move = mode == 'ask' ? await askDropAction(context, load.paths, dest) : mode == 'move';
|
final move = mode == 'ask' ? await askDropAction(context, load.paths, dest) : mode == 'move';
|
||||||
|
|
@ -4541,7 +4708,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
final isCurrent = e.isDir && !list && !row.isUp && samePath(pane.current, e.path);
|
final isCurrent = e.isDir && !list && !row.isUp && samePath(pane.current, e.path);
|
||||||
final isFocused = !row.isUp && pane.focused != null && samePath(pane.focused!, e.path);
|
final isFocused = !row.isUp && pane.focused != null && samePath(pane.focused!, e.path);
|
||||||
final marked = _isMarked(pane, e.path);
|
final marked = _isMarked(pane, e.path);
|
||||||
final open = e.isDir && !list && pane.expanded.contains(e.path);
|
final open = (e.isDir || _archives.containsKey(e.path)) && !list && pane.expanded.contains(e.path);
|
||||||
final loading = pane.loading.contains(e.path);
|
final loading = pane.loading.contains(e.path);
|
||||||
final name = row.isUp
|
final name = row.isUp
|
||||||
? '..'
|
? '..'
|
||||||
|
|
@ -4563,7 +4730,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
Widget chevron() => SizedBox(
|
Widget chevron() => SizedBox(
|
||||||
width: chevronWidth,
|
width: chevronWidth,
|
||||||
height: look.rowHeightOf(context),
|
height: look.rowHeightOf(context),
|
||||||
child: e.isDir && !list && !row.isUp
|
child: (e.isDir || _canBrowseArchive(e)) && !list && !row.isUp
|
||||||
? (loading
|
? (loading
|
||||||
? const Center(
|
? const Center(
|
||||||
child: SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 1.5)))
|
child: SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 1.5)))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:archive/archive.dart' show Archive, ArchiveFile, ZipEncoder;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -255,6 +256,41 @@ void main() {
|
||||||
await tester.pump(const Duration(seconds: 1));
|
await tester.pump(const Duration(seconds: 1));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('X-plore f: 압축을 트리 안에서 폴더처럼 펼치기 · 안은 읽기 전용 · 복사는 꺼내기', (tester) async {
|
||||||
|
final zip = p.join(left, 'pack.zip');
|
||||||
|
File(zip).writeAsBytesSync(ZipEncoder().encode(Archive()
|
||||||
|
..addFile(ArchiveFile.string('top.txt', 'top'))
|
||||||
|
..addFile(ArchiveFile.string('a/b.txt', 'bee'))));
|
||||||
|
await open(tester);
|
||||||
|
await settle(tester, () => find.text('pack.zip').evaluate().isNotEmpty);
|
||||||
|
// › 로 펼치면 압축 안의 것이 폴더처럼 (목록에 폴더 이름이 없어도 경로에서 폴더를 만든다)
|
||||||
|
await act(tester, () => tester.tap(chevronOf('pack.zip').first));
|
||||||
|
await settle(tester, () => find.text('top.txt').evaluate().isNotEmpty);
|
||||||
|
expect(find.text('a'), findsOneWidget);
|
||||||
|
await act(tester, () => tester.tap(chevronOf('a').first));
|
||||||
|
await settle(tester, () => find.text('b.txt').evaluate().isNotEmpty);
|
||||||
|
expect(find.text('b.txt'), findsOneWidget);
|
||||||
|
// 안의 것을 지우려 하면 막는다 (압축은 그대로)
|
||||||
|
await act(tester, () => tester.tap(find.text('top.txt')));
|
||||||
|
await settle(tester, () => false, rounds: 10);
|
||||||
|
await act(tester, () => tester.tap(find.text('삭제').first));
|
||||||
|
await settle(tester, () => find.textContaining('읽기 전용').evaluate().isNotEmpty);
|
||||||
|
expect(find.textContaining('압축 안은 읽기 전용'), findsOneWidget);
|
||||||
|
expect(File(zip).existsSync(), isTrue);
|
||||||
|
expect(Directory(p.join(left, 'pack')).existsSync(), isFalse, reason: '풀지 않음');
|
||||||
|
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
// 압축 안의 폴더를 펼쳐 지금 폴더가 압축 안이면 [새 폴더] 도 막는다 (이름을 묻지 않음)
|
||||||
|
await act(tester, () => tester.tap(find.text('a')));
|
||||||
|
await settle(tester, () => false, rounds: 10);
|
||||||
|
await act(tester, () => tester.tap(find.text('새 폴더').first));
|
||||||
|
await settle(tester, () => find.textContaining('읽기 전용').evaluate().isNotEmpty);
|
||||||
|
expect(find.textContaining('압축 안은 읽기 전용'), findsOneWidget);
|
||||||
|
expect(find.byType(TextField), findsNothing, reason: '이름 묻는 창 없음');
|
||||||
|
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('X-plore e: Android 앱 휴지통 - 지우기 = 휴지통으로 · [되돌리기] · [휴지통] 화면에서 되살리기', (tester) async {
|
testWidgets('X-plore e: Android 앱 휴지통 - 지우기 = 휴지통으로 · [되돌리기] · [휴지통] 화면에서 되살리기', (tester) async {
|
||||||
// 저장 장치는 시험 폴더 (실제 C:\ 에 .jj_trash 를 만들지 않게)
|
// 저장 장치는 시험 폴더 (실제 C:\ 에 .jj_trash 를 만들지 않게)
|
||||||
ExplorerPage.debugAndroidTrash = true;
|
ExplorerPage.debugAndroidTrash = true;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue