WIP 암호 보관함: 평문이 남을 수 있는 두 곳 (관리자 지시 10/21) - ① 넣기의 [원본 지우기 (이동)]: "원본은 휴지통으로 갑니다 - 휴지통에 평문이 남습니다" 를 보이고 [휴지통으로 (처음 값)] / [영구 삭제] 를 고름, 영구 삭제는 탐색기의 영구 삭제 길 (Shift+Delete 와 같은 확인 창을 한 번 더) ② [보기]: 그림은 메모리에서 바로 (파일로 풀지 않음, ReaderSource 를 MemoryImage 로), 동영상 · 음악만 앱 전용 폴더 (Android getCacheDir · Windows 앱 데이터의 temp) 아래 jj_vault_view_… 에 잠깐 - 닫으면 지움, 자동 잠금 · [잠그기] 때 열린 보기를 닫고 지움, 앱이 강제로 꺼져 남은 것은 다음에 켤 때 지움 (jj_hwp_ 정리처럼), Windows 에서 막 쓴 파일을 다른 프로그램이 잠깐 잡아 지우기가 실패하는 것은 몇 번 더 시도 · 시험: 영구 삭제 고르기 · 그림은 파일 0 · 동영상 보기 뒤 0 · 잠그면 보기 닫힘 · 남은 것 0 · 켤 때 정리 (시험의 보기 폴더는 파일마다 임시로 - flutter_test_config) (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore_p3b
parent
286e6b4698
commit
dc4bae5329
|
|
@ -2073,6 +2073,7 @@
|
||||||
"원본에 없는 것을 대상에서 지우기": "Delete from target what is not in the source",
|
"원본에 없는 것을 대상에서 지우기": "Delete from target what is not in the source",
|
||||||
"원본에는 아무것도 없는데 대상에는 {0}개가 있습니다. SD 카드가 빠졌거나 네트워크 · 권한 문제일 수 있습니다. 일부러 원본을 비웠다면 지울 목록을 보고 [그래도 맞추기] 를 누르세요.": "The source is empty but the target has {0} items. The SD card may have been removed, or there may be a network or permission problem. If you emptied the source on purpose, review the list and press [Sync anyway].",
|
"원본에는 아무것도 없는데 대상에는 {0}개가 있습니다. SD 카드가 빠졌거나 네트워크 · 권한 문제일 수 있습니다. 일부러 원본을 비웠다면 지울 목록을 보고 [그래도 맞추기] 를 누르세요.": "The source is empty but the target has {0} items. The SD card may have been removed, or there may be a network or permission problem. If you emptied the source on purpose, review the list and press [Sync anyway].",
|
||||||
"원본으로": "Back to the original",
|
"원본으로": "Back to the original",
|
||||||
|
"원본은 휴지통으로 갑니다 - 휴지통에 평문 (암호화하지 않은 원본) 이 남습니다": "Originals go to the trash - the unencrypted originals remain in the trash",
|
||||||
"원본을 읽을 수 없어 멈춤": "Stopped: can't read the source",
|
"원본을 읽을 수 없어 멈춤": "Stopped: can't read the source",
|
||||||
"원본을 읽을 수 없어 멈췄습니다": "Stopped because the source can't be read",
|
"원본을 읽을 수 없어 멈췄습니다": "Stopped because the source can't be read",
|
||||||
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "This run removes source files, so it can run once the list of files to move is shown.",
|
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "This run removes source files, so it can run once the list of files to move is shown.",
|
||||||
|
|
@ -2599,6 +2600,7 @@
|
||||||
"지난 업데이트가 남긴 임시 파일을 지움: {0}": "Removed temporary files left by the last update: {0}",
|
"지난 업데이트가 남긴 임시 파일을 지움: {0}": "Removed temporary files left by the last update: {0}",
|
||||||
"지난 작업 기록": "Old job logs",
|
"지난 작업 기록": "Old job logs",
|
||||||
"지난번에 남은 문서 보기 임시 폴더를 지움: {0}개": "Removed {0} leftover document-view temp folders",
|
"지난번에 남은 문서 보기 임시 폴더를 지움: {0}개": "Removed {0} leftover document-view temp folders",
|
||||||
|
"지난번에 남은 보관함 보기 임시 폴더를 지움: {0}개": "Removed {0} leftover vault view temp folders",
|
||||||
"지문 확인하고 연결": "Check fingerprint and connect",
|
"지문 확인하고 연결": "Check fingerprint and connect",
|
||||||
"지우고 맞추기": "Delete and sync",
|
"지우고 맞추기": "Delete and sync",
|
||||||
"지우고 초기화": "Delete and reset",
|
"지우고 초기화": "Delete and reset",
|
||||||
|
|
@ -3033,10 +3035,12 @@
|
||||||
"휴지통에서 지운 것은 되살릴 수 없습니다.": "Items deleted from the trash can't be restored.",
|
"휴지통에서 지운 것은 되살릴 수 없습니다.": "Items deleted from the trash can't be restored.",
|
||||||
"휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)": "Not found in the Recycle Bin (already emptied or restored)",
|
"휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)": "Not found in the Recycle Bin (already emptied or restored)",
|
||||||
"휴지통으로": "To Recycle Bin",
|
"휴지통으로": "To Recycle Bin",
|
||||||
|
"휴지통으로 (처음 값)": "To the trash (default)",
|
||||||
"휴지통으로 보내기": "Move to Recycle Bin",
|
"휴지통으로 보내기": "Move to Recycle Bin",
|
||||||
"휴지통으로 옮기지 못했습니다: {0} ({1})": "Couldn't move to the trash: {0} ({1})",
|
"휴지통으로 옮기지 못했습니다: {0} ({1})": "Couldn't move to the trash: {0} ({1})",
|
||||||
"휴지통을 거치지 않고 영구 삭제": "Delete permanently without the trash",
|
"휴지통을 거치지 않고 영구 삭제": "Delete permanently without the trash",
|
||||||
"휴지통을 거치지 않고 영구 삭제 (Shift+Delete)": "Delete permanently without the Recycle Bin (Shift+Delete)",
|
"휴지통을 거치지 않고 영구 삭제 (Shift+Delete)": "Delete permanently without the Recycle Bin (Shift+Delete)",
|
||||||
|
"휴지통을 거치지 않습니다 - 지우기 전에 확인 창을 한 번 더 띄웁니다": "Skips the trash - asks for confirmation once more before deleting",
|
||||||
"휴지통이 꺼져 있습니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).": "The trash is off (Settings > File explorer > Keep in trash).",
|
"휴지통이 꺼져 있습니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).": "The trash is off (Settings > File explorer > Keep in trash).",
|
||||||
"휴지통이 꺼져 있습니다 - 지우면 바로 영구 삭제됩니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).": "The trash is off - deleting removes items permanently right away (Settings > File explorer > Keep in trash).",
|
"휴지통이 꺼져 있습니다 - 지우면 바로 영구 삭제됩니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).": "The trash is off - deleting removes items permanently right away (Settings > File explorer > Keep in trash).",
|
||||||
"휴지통이 비어 있습니다": "The trash is empty",
|
"휴지통이 비어 있습니다": "The trash is empty",
|
||||||
|
|
|
||||||
|
|
@ -2073,6 +2073,7 @@
|
||||||
"원본에 없는 것을 대상에서 지우기": "元にないものを対象から削除",
|
"원본에 없는 것을 대상에서 지우기": "元にないものを対象から削除",
|
||||||
"원본에는 아무것도 없는데 대상에는 {0}개가 있습니다. SD 카드가 빠졌거나 네트워크 · 권한 문제일 수 있습니다. 일부러 원본을 비웠다면 지울 목록을 보고 [그래도 맞추기] 를 누르세요.": "元には何もありませんが、対象には {0} 個あります。SD カードが抜けたか、ネットワーク・権限の問題かもしれません。意図的に元を空にした場合は、削除する一覧を見て [それでも同期] を押してください。",
|
"원본에는 아무것도 없는데 대상에는 {0}개가 있습니다. SD 카드가 빠졌거나 네트워크 · 권한 문제일 수 있습니다. 일부러 원본을 비웠다면 지울 목록을 보고 [그래도 맞추기] 를 누르세요.": "元には何もありませんが、対象には {0} 個あります。SD カードが抜けたか、ネットワーク・権限の問題かもしれません。意図的に元を空にした場合は、削除する一覧を見て [それでも同期] を押してください。",
|
||||||
"원본으로": "元の画像で",
|
"원본으로": "元の画像で",
|
||||||
|
"원본은 휴지통으로 갑니다 - 휴지통에 평문 (암호화하지 않은 원본) 이 남습니다": "元のファイルはごみ箱に移ります - ごみ箱に暗号化されていない元のファイルが残ります",
|
||||||
"원본을 읽을 수 없어 멈춤": "元を読めないため停止",
|
"원본을 읽을 수 없어 멈춤": "元を読めないため停止",
|
||||||
"원본을 읽을 수 없어 멈췄습니다": "元を読めないため停止しました",
|
"원본을 읽을 수 없어 멈췄습니다": "元を読めないため停止しました",
|
||||||
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "元のファイルを削除する実行のため、移動する一覧が表示されてから実行できます。",
|
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "元のファイルを削除する実行のため、移動する一覧が表示されてから実行できます。",
|
||||||
|
|
@ -2599,6 +2600,7 @@
|
||||||
"지난 업데이트가 남긴 임시 파일을 지움: {0}": "前回のアップデートが残した一時ファイルを削除: {0}",
|
"지난 업데이트가 남긴 임시 파일을 지움: {0}": "前回のアップデートが残した一時ファイルを削除: {0}",
|
||||||
"지난 작업 기록": "過去の作業記録",
|
"지난 작업 기록": "過去の作業記録",
|
||||||
"지난번에 남은 문서 보기 임시 폴더를 지움: {0}개": "前回残った文書表示の一時フォルダーを削除: {0} 個",
|
"지난번에 남은 문서 보기 임시 폴더를 지움: {0}개": "前回残った文書表示の一時フォルダーを削除: {0} 個",
|
||||||
|
"지난번에 남은 보관함 보기 임시 폴더를 지움: {0}개": "前回残った保管庫表示の一時フォルダーを削除: {0} 個",
|
||||||
"지문 확인하고 연결": "フィンガープリントを確認して接続",
|
"지문 확인하고 연결": "フィンガープリントを確認して接続",
|
||||||
"지우고 맞추기": "削除して同期",
|
"지우고 맞추기": "削除して同期",
|
||||||
"지우고 초기화": "削除して初期化",
|
"지우고 초기화": "削除して初期化",
|
||||||
|
|
@ -3033,10 +3035,12 @@
|
||||||
"휴지통에서 지운 것은 되살릴 수 없습니다.": "ゴミ箱から削除したものは元に戻せません。",
|
"휴지통에서 지운 것은 되살릴 수 없습니다.": "ゴミ箱から削除したものは元に戻せません。",
|
||||||
"휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)": "ごみ箱に見つかりません (空にしたか元に戻した)",
|
"휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)": "ごみ箱に見つかりません (空にしたか元に戻した)",
|
||||||
"휴지통으로": "ごみ箱へ",
|
"휴지통으로": "ごみ箱へ",
|
||||||
|
"휴지통으로 (처음 값)": "ごみ箱へ (既定)",
|
||||||
"휴지통으로 보내기": "ごみ箱に移動",
|
"휴지통으로 보내기": "ごみ箱に移動",
|
||||||
"휴지통으로 옮기지 못했습니다: {0} ({1})": "ゴミ箱に移せませんでした: {0} ({1})",
|
"휴지통으로 옮기지 못했습니다: {0} ({1})": "ゴミ箱に移せませんでした: {0} ({1})",
|
||||||
"휴지통을 거치지 않고 영구 삭제": "ゴミ箱を使わずに完全に削除",
|
"휴지통을 거치지 않고 영구 삭제": "ゴミ箱を使わずに完全に削除",
|
||||||
"휴지통을 거치지 않고 영구 삭제 (Shift+Delete)": "ごみ箱を使わずに完全に削除 (Shift+Delete)",
|
"휴지통을 거치지 않고 영구 삭제 (Shift+Delete)": "ごみ箱を使わずに完全に削除 (Shift+Delete)",
|
||||||
|
"휴지통을 거치지 않습니다 - 지우기 전에 확인 창을 한 번 더 띄웁니다": "ごみ箱を経由しません - 削除前にもう一度確認します",
|
||||||
"휴지통이 꺼져 있습니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).": "ゴミ箱はオフです (設定 > ファイルエクスプローラー > ゴミ箱の保管)。",
|
"휴지통이 꺼져 있습니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).": "ゴミ箱はオフです (設定 > ファイルエクスプローラー > ゴミ箱の保管)。",
|
||||||
"휴지통이 꺼져 있습니다 - 지우면 바로 영구 삭제됩니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).": "ゴミ箱はオフです - 削除するとすぐに完全削除されます (設定 > ファイルエクスプローラー > ゴミ箱の保管)。",
|
"휴지통이 꺼져 있습니다 - 지우면 바로 영구 삭제됩니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).": "ゴミ箱はオフです - 削除するとすぐに完全削除されます (設定 > ファイルエクスプローラー > ゴミ箱の保管)。",
|
||||||
"휴지통이 비어 있습니다": "ゴミ箱は空です",
|
"휴지통이 비어 있습니다": "ゴミ箱は空です",
|
||||||
|
|
|
||||||
|
|
@ -2073,6 +2073,7 @@
|
||||||
"원본에 없는 것을 대상에서 지우기": "从目标中删除源中没有的内容",
|
"원본에 없는 것을 대상에서 지우기": "从目标中删除源中没有的内容",
|
||||||
"원본에는 아무것도 없는데 대상에는 {0}개가 있습니다. SD 카드가 빠졌거나 네트워크 · 권한 문제일 수 있습니다. 일부러 원본을 비웠다면 지울 목록을 보고 [그래도 맞추기] 를 누르세요.": "源中没有任何内容,但目标中有 {0} 个项目。可能是 SD 卡被拔出,或网络 · 权限有问题。如果你有意清空了源,请查看要删除的列表并按 [仍然同步]。",
|
"원본에는 아무것도 없는데 대상에는 {0}개가 있습니다. SD 카드가 빠졌거나 네트워크 · 권한 문제일 수 있습니다. 일부러 원본을 비웠다면 지울 목록을 보고 [그래도 맞추기] 를 누르세요.": "源中没有任何内容,但目标中有 {0} 个项目。可能是 SD 卡被拔出,或网络 · 权限有问题。如果你有意清空了源,请查看要删除的列表并按 [仍然同步]。",
|
||||||
"원본으로": "回到原图",
|
"원본으로": "回到原图",
|
||||||
|
"원본은 휴지통으로 갑니다 - 휴지통에 평문 (암호화하지 않은 원본) 이 남습니다": "原文件将进入回收站 - 回收站中会留下未加密的原文件",
|
||||||
"원본을 읽을 수 없어 멈춤": "无法读取源,已停止",
|
"원본을 읽을 수 없어 멈춤": "无法读取源,已停止",
|
||||||
"원본을 읽을 수 없어 멈췄습니다": "无法读取源,已停止",
|
"원본을 읽을 수 없어 멈췄습니다": "无法读取源,已停止",
|
||||||
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "此操作会删除源文件,需在显示要移动的列表后才能执行。",
|
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "此操作会删除源文件,需在显示要移动的列表后才能执行。",
|
||||||
|
|
@ -2599,6 +2600,7 @@
|
||||||
"지난 업데이트가 남긴 임시 파일을 지움: {0}": "已删除上次更新留下的临时文件: {0}",
|
"지난 업데이트가 남긴 임시 파일을 지움: {0}": "已删除上次更新留下的临时文件: {0}",
|
||||||
"지난 작업 기록": "旧任务记录",
|
"지난 작업 기록": "旧任务记录",
|
||||||
"지난번에 남은 문서 보기 임시 폴더를 지움: {0}개": "已删除上次留下的文档查看临时文件夹: {0} 个",
|
"지난번에 남은 문서 보기 임시 폴더를 지움: {0}개": "已删除上次留下的文档查看临时文件夹: {0} 个",
|
||||||
|
"지난번에 남은 보관함 보기 임시 폴더를 지움: {0}개": "已删除上次残留的保管库查看临时文件夹: {0} 个",
|
||||||
"지문 확인하고 연결": "确认指纹并连接",
|
"지문 확인하고 연결": "确认指纹并连接",
|
||||||
"지우고 맞추기": "删除并同步",
|
"지우고 맞추기": "删除并同步",
|
||||||
"지우고 초기화": "删除并重置",
|
"지우고 초기화": "删除并重置",
|
||||||
|
|
@ -3033,10 +3035,12 @@
|
||||||
"휴지통에서 지운 것은 되살릴 수 없습니다.": "从回收站删除的项目无法还原。",
|
"휴지통에서 지운 것은 되살릴 수 없습니다.": "从回收站删除的项目无法还原。",
|
||||||
"휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)": "在回收站中找不到 (已清空或已恢复)",
|
"휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)": "在回收站中找不到 (已清空或已恢复)",
|
||||||
"휴지통으로": "移到回收站",
|
"휴지통으로": "移到回收站",
|
||||||
|
"휴지통으로 (처음 값)": "移到回收站 (默认)",
|
||||||
"휴지통으로 보내기": "移到回收站",
|
"휴지통으로 보내기": "移到回收站",
|
||||||
"휴지통으로 옮기지 못했습니다: {0} ({1})": "无法移到回收站: {0} ({1})",
|
"휴지통으로 옮기지 못했습니다: {0} ({1})": "无法移到回收站: {0} ({1})",
|
||||||
"휴지통을 거치지 않고 영구 삭제": "不经回收站永久删除",
|
"휴지통을 거치지 않고 영구 삭제": "不经回收站永久删除",
|
||||||
"휴지통을 거치지 않고 영구 삭제 (Shift+Delete)": "不经过回收站永久删除 (Shift+Delete)",
|
"휴지통을 거치지 않고 영구 삭제 (Shift+Delete)": "不经过回收站永久删除 (Shift+Delete)",
|
||||||
|
"휴지통을 거치지 않습니다 - 지우기 전에 확인 창을 한 번 더 띄웁니다": "不经过回收站 - 删除前会再次确认",
|
||||||
"휴지통이 꺼져 있습니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).": "回收站已关闭 (设置 > 文件资源管理器 > 回收站保留)。",
|
"휴지통이 꺼져 있습니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).": "回收站已关闭 (设置 > 文件资源管理器 > 回收站保留)。",
|
||||||
"휴지통이 꺼져 있습니다 - 지우면 바로 영구 삭제됩니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).": "回收站已关闭 - 删除会立即永久删除 (设置 > 文件资源管理器 > 回收站保留)。",
|
"휴지통이 꺼져 있습니다 - 지우면 바로 영구 삭제됩니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).": "回收站已关闭 - 删除会立即永久删除 (设置 > 文件资源管理器 > 回收站保留)。",
|
||||||
"휴지통이 비어 있습니다": "回收站为空",
|
"휴지통이 비어 있습니다": "回收站为空",
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import '../services/system_usage.dart';
|
||||||
import 'settings.dart';
|
import 'settings.dart';
|
||||||
import 'cleanup.dart' show removeUpdateLeftovers;
|
import 'cleanup.dart' show removeUpdateLeftovers;
|
||||||
import '../core/hwp_source.dart' show removeStaleHwpWork;
|
import '../core/hwp_source.dart' show removeStaleHwpWork;
|
||||||
|
import '../core/vault_view.dart' show removeStaleVaultViews, vaultViewRootOf;
|
||||||
import '../l10n/tr.dart';
|
import '../l10n/tr.dart';
|
||||||
import '../core/remote_idle.dart';
|
import '../core/remote_idle.dart';
|
||||||
|
|
||||||
|
|
@ -221,6 +222,11 @@ class AppController extends ChangeNotifier {
|
||||||
final hwp = removeStaleHwpWork(await services.storage.tempDirectory());
|
final hwp = removeStaleHwpWork(await services.storage.tempDirectory());
|
||||||
if (hwp.isNotEmpty) _log(trf('지난번에 남은 문서 보기 임시 폴더를 지움: {0}개', [hwp.length]));
|
if (hwp.isNotEmpty) _log(trf('지난번에 남은 문서 보기 임시 폴더를 지움: {0}개', [hwp.length]));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
// 관리자 지시 (10/21): 강제로 꺼져 남은 암호 보관함 보기 폴더 (잠깐 푼 평문)
|
||||||
|
try {
|
||||||
|
final vault = removeStaleVaultViews((await vaultViewRootOf()).path);
|
||||||
|
if (vault.isNotEmpty) _log(trf('지난번에 남은 보관함 보기 임시 폴더를 지움: {0}개', [vault.length]));
|
||||||
|
} catch (_) {}
|
||||||
}());
|
}());
|
||||||
ffmpegVersion = await _tool.version();
|
ffmpegVersion = await _tool.version();
|
||||||
if (ffmpegVersion == null) {
|
if (ffmpegVersion == null) {
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,13 @@ class Vault {
|
||||||
await tmp.rename(outPath);
|
await tmp.rename(outPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 메모리로 푼다 (관리자 지시 10/21: 그림 보기는 파일로 풀지 않음). [maxBytes] 보다 크면 [StateError]
|
||||||
|
Future<Uint8List> readBytes(VaultEntry e, {int maxBytes = 256 * 1024 * 1024}) async {
|
||||||
|
if (e.size > maxBytes) throw StateError('too big');
|
||||||
|
final data = await File(p.join(dir.path, '${e.blob}$_ext')).readAsBytes();
|
||||||
|
return VaultCrypto.decryptAll(_key, data);
|
||||||
|
}
|
||||||
|
|
||||||
/// 보관함에서 지운다 (목록 먼저 - 그 뒤 내용 파일)
|
/// 보관함에서 지운다 (목록 먼저 - 그 뒤 내용 파일)
|
||||||
Future<void> remove(VaultEntry e) async {
|
Future<void> remove(VaultEntry e) async {
|
||||||
_entries.removeWhere((x) => x.blob == e.blob);
|
_entries.removeWhere((x) => x.blob == e.blob);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
/// 암호 보관함 [보기] 에서 잠깐 푼 파일을 두는 곳 (관리자 지시 10/21): 공유 폴더 (Download · 시스템 TEMP) 가 아니라
|
||||||
|
/// 앱 전용 - Android getCacheDir (다른 앱이 못 봄), Windows 앱 데이터 폴더의 temp. 시험에서 바꾼다
|
||||||
|
Future<Directory> Function() vaultViewRootOf = () async {
|
||||||
|
if (Platform.isAndroid) return getTemporaryDirectory();
|
||||||
|
return Directory(p.join((await getApplicationSupportDirectory()).path, 'temp'));
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 푼 파일을 두는 폴더 이름 앞 (실행마다 새 폴더)
|
||||||
|
const vaultViewPrefix = 'jj_vault_view_';
|
||||||
|
|
||||||
|
/// 앱이 강제로 꺼져 남은 보기 폴더를 지운다 (앱을 켤 때 - 그때는 보관함이 열려 있지 않음). 지운 이름
|
||||||
|
List<String> removeStaleVaultViews(String root) {
|
||||||
|
final removed = <String>[];
|
||||||
|
try {
|
||||||
|
for (final e in Directory(root).listSync(followLinks: false)) {
|
||||||
|
final name = p.basename(e.path);
|
||||||
|
if (e is! Directory || !name.startsWith(vaultViewPrefix)) continue;
|
||||||
|
try {
|
||||||
|
e.deleteSync(recursive: true);
|
||||||
|
removed.add(name);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
@ -2196,7 +2196,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
case 'sqlite':
|
case 'sqlite':
|
||||||
await openSqliteViewer(context, c, e.path);
|
await openSqliteViewer(context, c, e.path);
|
||||||
case 'vaultAdd':
|
case 'vaultAdd':
|
||||||
await openVault(context, c, add: targets, deleteOriginals: (paths) => _delete(pane, paths));
|
await openVault(context, c, add: targets, deleteOriginals: (paths, {required permanent}) {
|
||||||
|
// 관리자 지시 (10/21): [영구 삭제] 를 고르면 휴지통을 거치지 않음 (Shift+Delete 와 같은 길 - 탐색기의 영구 삭제 확인 창을 한 번 더)
|
||||||
|
_shiftDelete = permanent;
|
||||||
|
return _delete(pane, paths);
|
||||||
|
});
|
||||||
await _refreshAll([vDirname(e.path)]);
|
await _refreshAll([vDirname(e.path)]);
|
||||||
case 'openWith':
|
case 'openWith':
|
||||||
final local = await _fetch(e.path);
|
final local = await _fetch(e.path);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import '../core/reader_sources.dart';
|
||||||
import '../core/secret_gate.dart';
|
import '../core/secret_gate.dart';
|
||||||
import '../core/vault_crypto.dart';
|
import '../core/vault_crypto.dart';
|
||||||
import '../core/vault_store.dart';
|
import '../core/vault_store.dart';
|
||||||
|
import '../core/vault_view.dart';
|
||||||
import '../core/vfs.dart' show vDisplay;
|
import '../core/vfs.dart' show vDisplay;
|
||||||
import '../l10n/tr.dart';
|
import '../l10n/tr.dart';
|
||||||
import 'player_page.dart';
|
import 'player_page.dart';
|
||||||
|
|
@ -24,7 +25,7 @@ import 'theme.dart';
|
||||||
|
|
||||||
/// X-plore 비교 5-2 암호 보관함 화면 (사용자 결정 10/21, 관리자 결정 - 설계안 위키 프로젝트/암호 보관함 · 루트 설계안 (10-21))
|
/// X-plore 비교 5-2 암호 보관함 화면 (사용자 결정 10/21, 관리자 결정 - 설계안 위키 프로젝트/암호 보관함 · 루트 설계안 (10-21))
|
||||||
Future<void> openVault(BuildContext context, AppController c,
|
Future<void> openVault(BuildContext context, AppController c,
|
||||||
{List<String> add = const [], Future<void> Function(List<String> paths)? deleteOriginals}) =>
|
{List<String> add = const [], Future<void> Function(List<String> paths, {required bool permanent})? deleteOriginals}) =>
|
||||||
Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => VaultPage(c: c, add: add, deleteOriginals: deleteOriginals)));
|
Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => VaultPage(c: c, add: add, deleteOriginals: deleteOriginals)));
|
||||||
|
|
||||||
/// 보관함 폴더 (Android: Download/JJ_MKVMaker/vault · Windows: 문서\JJ_MKVMaker\vault). 시험에서 바꿈
|
/// 보관함 폴더 (Android: Download/JJ_MKVMaker/vault · Windows: 문서\JJ_MKVMaker\vault). 시험에서 바꿈
|
||||||
|
|
@ -48,8 +49,8 @@ class VaultPage extends StatefulWidget {
|
||||||
/// 열면서 넣을 파일 (탐색기 [보관함에 넣기])
|
/// 열면서 넣을 파일 (탐색기 [보관함에 넣기])
|
||||||
final List<String> add;
|
final List<String> add;
|
||||||
|
|
||||||
/// [원본 지우기 (이동)] 일 때 탐색기의 지우기 (확인 · 휴지통)
|
/// [원본 지우기 (이동)] 일 때 탐색기의 지우기 (확인 창 · [permanent] 가 아니면 휴지통)
|
||||||
final Future<void> Function(List<String> paths)? deleteOriginals;
|
final Future<void> Function(List<String> paths, {required bool permanent})? deleteOriginals;
|
||||||
const VaultPage({super.key, required this.c, this.add = const [], this.deleteOriginals});
|
const VaultPage({super.key, required this.c, this.add = const [], this.deleteOriginals});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -83,7 +84,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_lock();
|
_lock(closeViews: false);
|
||||||
_pw.dispose();
|
_pw.dispose();
|
||||||
_pw2.dispose();
|
_pw2.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
@ -103,19 +104,43 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _lock() {
|
/// 그림은 메모리에서 바로 (파일로 풀지 않음)
|
||||||
|
static const _imageMax = 256 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// 이 화면의 길 (잠글 때 위에 열린 보기 화면을 닫으려고)
|
||||||
|
ModalRoute<dynamic>? _route;
|
||||||
|
|
||||||
|
/// 잠금: 열쇠를 버리고, 위에 열린 보기 (그림 · 플레이어) 를 닫고, 잠깐 푼 파일을 지운다 (관리자 지시 10/21)
|
||||||
|
void _lock({bool closeViews = true}) {
|
||||||
_vault = null;
|
_vault = null;
|
||||||
if (_stage == _Stage.open) _stage = _Stage.locked;
|
if (_stage == _Stage.open) _stage = _Stage.locked;
|
||||||
|
final r = _route;
|
||||||
|
if (closeViews && r != null && mounted && !r.isCurrent) Navigator.of(context).popUntil((x) => x == r);
|
||||||
unawaited(_clearViewDir());
|
unawaited(_clearViewDir());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Directory> _viewDir() async => Directory(p.join(await c.services.storage.tempDirectory(), 'jj_vault_view'));
|
/// 이번에 연 동안 잠깐 푼 파일을 두는 앱 전용 폴더 (jj_vault_view_…)
|
||||||
|
Directory? _session;
|
||||||
|
|
||||||
|
Future<Directory> _viewDir() async {
|
||||||
|
final d = _session ??= Directory(p.join((await vaultViewRootOf()).path, '$vaultViewPrefix${DateTime.now().microsecondsSinceEpoch}'));
|
||||||
|
await d.create(recursive: true);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _clearViewDir() async {
|
Future<void> _clearViewDir() async {
|
||||||
|
final d = _session;
|
||||||
|
_session = null;
|
||||||
|
if (d == null) return;
|
||||||
|
// Windows 는 막 쓴 파일을 백신 · 색인이 잠깐 잡고 있어 지우기가 실패할 수 있다 - 몇 번 더 (남으면 다음에 켤 때 지움)
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
try {
|
try {
|
||||||
final d = await _viewDir();
|
|
||||||
if (await d.exists()) await d.delete(recursive: true);
|
if (await d.exists()) await d.delete(recursive: true);
|
||||||
} catch (_) {}
|
return;
|
||||||
|
} catch (_) {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _check() async {
|
Future<void> _check() async {
|
||||||
|
|
@ -244,6 +269,8 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
||||||
if (paths.isEmpty || _vault == null) return;
|
if (paths.isEmpty || _vault == null) return;
|
||||||
_pendingAdd = [];
|
_pendingAdd = [];
|
||||||
var move = false;
|
var move = false;
|
||||||
|
// 관리자 지시 (10/21): 휴지통으로 보내면 평문 원본이 휴지통에 남는다 - 알리고 고르게 (처음 값 휴지통: 실수로 잃지 않게)
|
||||||
|
var permanent = false;
|
||||||
final go = await showDialog<bool>(
|
final go = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => StatefulBuilder(
|
builder: (ctx) => StatefulBuilder(
|
||||||
|
|
@ -262,6 +289,23 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
||||||
title: Text(tr('원본 지우기 (이동)')),
|
title: Text(tr('원본 지우기 (이동)')),
|
||||||
subtitle: Text(tr('다 넣고 확인한 뒤에 탐색기의 지우기 (확인 · 휴지통) 로 지웁니다. 끄면 원본은 그대로 (복사)')),
|
subtitle: Text(tr('다 넣고 확인한 뒤에 탐색기의 지우기 (확인 · 휴지통) 로 지웁니다. 끄면 원본은 그대로 (복사)')),
|
||||||
),
|
),
|
||||||
|
if (move) ...[
|
||||||
|
Text(tr('원본은 휴지통으로 갑니다 - 휴지통에 평문 (암호화하지 않은 원본) 이 남습니다'),
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
|
||||||
|
RadioGroup<bool>(
|
||||||
|
groupValue: permanent,
|
||||||
|
onChanged: (v) => set(() => permanent = v ?? false),
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
RadioListTile<bool>(dense: true, contentPadding: EdgeInsets.zero, value: false, title: Text(tr('휴지통으로 (처음 값)'))),
|
||||||
|
RadioListTile<bool>(
|
||||||
|
dense: true,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
value: true,
|
||||||
|
title: Text(tr('영구 삭제')),
|
||||||
|
subtitle: Text(tr('휴지통을 거치지 않습니다 - 지우기 전에 확인 창을 한 번 더 띄웁니다'))),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||||
|
|
@ -284,28 +328,42 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
||||||
if (mounted) setState(() => _busy = false);
|
if (mounted) setState(() => _busy = false);
|
||||||
}
|
}
|
||||||
if (done.isNotEmpty) _snack(trf('{0}개를 보관함에 넣었습니다', [done.length]));
|
if (done.isNotEmpty) _snack(trf('{0}개를 보관함에 넣었습니다', [done.length]));
|
||||||
if (move && done.isNotEmpty && widget.deleteOriginals != null) await widget.deleteOriginals!(done);
|
if (move && done.isNotEmpty && widget.deleteOriginals != null) await widget.deleteOriginals!(done, permanent: permanent);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _view(VaultEntry e) async {
|
Future<void> _view(VaultEntry e) async {
|
||||||
|
final ext = p.extension(e.name).replaceFirst('.', '').toLowerCase();
|
||||||
|
// 그림: 메모리에서 바로 (파일로 풀지 않음 - 관리자 지시 10/21)
|
||||||
|
if (c.settings.imageExts.contains(ext) && e.size <= _imageMax) {
|
||||||
try {
|
try {
|
||||||
|
setState(() => _busy = true);
|
||||||
|
final bytes = await _vault!.readBytes(e, maxBytes: _imageMax);
|
||||||
|
if (mounted) setState(() => _busy = false);
|
||||||
|
if (!mounted) return;
|
||||||
|
await openReader(context, c, _VaultImageSource(e.name, bytes));
|
||||||
|
} catch (err) {
|
||||||
|
if (mounted) setState(() => _busy = false);
|
||||||
|
_snack(err is VaultAuthException ? tr('이 파일은 바뀌었거나 깨져 풀 수 없습니다') : trf('열 수 없습니다: {0}', [err]));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isVideoFile(e.name) && !isAudioFile(e.name)) {
|
||||||
|
_snack(tr('이 종류는 앱 안에서 볼 수 없습니다 - [꺼내기] 로 꺼내서 보세요'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 동영상 · 음악: 앱 전용 폴더 (jj_vault_view_…) 에 잠깐 풀고, 닫으면 · 잠그면 · 다음에 켤 때 지운다
|
||||||
final d = await _viewDir();
|
final d = await _viewDir();
|
||||||
await d.create(recursive: true);
|
|
||||||
final out = p.join(d.path, 'v_${DateTime.now().microsecondsSinceEpoch}${p.extension(e.name)}');
|
final out = p.join(d.path, 'v_${DateTime.now().microsecondsSinceEpoch}${p.extension(e.name)}');
|
||||||
setState(() => _busy = true);
|
setState(() => _busy = true);
|
||||||
await _vault!.export(e, out);
|
await _vault!.export(e, out);
|
||||||
if (mounted) setState(() => _busy = false);
|
if (mounted) setState(() => _busy = false);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final ext = p.extension(e.name).replaceFirst('.', '').toLowerCase();
|
{
|
||||||
if (c.settings.imageExts.contains(ext)) {
|
|
||||||
await openReader(context, c, ImageFilesSource([out], tempDir: d.path, title: e.name));
|
|
||||||
} else if (isVideoFile(out) || isAudioFile(out)) {
|
|
||||||
final create = c.services.createMediaPlayer;
|
final create = c.services.createMediaPlayer;
|
||||||
if (create != null) {
|
if (create != null) {
|
||||||
await Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => PlayerPage(c: c, player: create(), files: [out])));
|
await Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => PlayerPage(c: c, player: create(), files: [out])));
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
_snack(tr('이 종류는 앱 안에서 볼 수 없습니다 - [꺼내기] 로 꺼내서 보세요'));
|
|
||||||
}
|
}
|
||||||
// 본 뒤 풀어 둔 것은 지운다
|
// 본 뒤 풀어 둔 것은 지운다
|
||||||
await File(out).delete().catchError((_) => File(out));
|
await File(out).delete().catchError((_) => File(out));
|
||||||
|
|
@ -432,7 +490,12 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(
|
Widget build(BuildContext context) {
|
||||||
|
_route ??= ModalRoute.of(context);
|
||||||
|
return _scaffold();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _scaffold() => Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(tr('암호 보관함')),
|
title: Text(tr('암호 보관함')),
|
||||||
actions: [
|
actions: [
|
||||||
|
|
@ -585,3 +648,19 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 보관함 그림을 메모리에서 바로 보는 보기 화면 원본 (파일로 풀지 않음)
|
||||||
|
class _VaultImageSource extends ReaderSource {
|
||||||
|
final String name;
|
||||||
|
final Uint8List bytes;
|
||||||
|
_VaultImageSource(this.name, this.bytes);
|
||||||
|
@override
|
||||||
|
String get title => name;
|
||||||
|
@override
|
||||||
|
int get length => 1;
|
||||||
|
@override
|
||||||
|
String pageName(int i) => name;
|
||||||
|
@override
|
||||||
|
Future<ReaderImage> load(int i, {required int maxWidth}) async =>
|
||||||
|
ReaderImage(provider: ResizeImage.resizeIfNeeded(maxWidth, null, MemoryImage(bytes)));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/vault_view.dart';
|
||||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||||
|
|
||||||
/// 모든 단위 시험 파일 앞에 돈다 (flutter_test 의 flutter_test_config).
|
/// 모든 단위 시험 파일 앞에 돈다 (flutter_test 의 flutter_test_config).
|
||||||
|
|
@ -10,6 +11,8 @@ import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||||
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
|
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
|
||||||
final root = Directory.systemTemp.createTempSync('jj_store_');
|
final root = Directory.systemTemp.createTempSync('jj_store_');
|
||||||
DesktopStorageService.tempRoot = root.path;
|
DesktopStorageService.tempRoot = root.path;
|
||||||
|
// 암호 보관함 보기 임시 폴더도 (기본은 실제 앱 데이터 폴더 - 시험이 건드리지 않게)
|
||||||
|
vaultViewRootOf = () async => Directory('${root.path}${Platform.pathSeparator}vault_view');
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
try {
|
try {
|
||||||
root.deleteSync(recursive: true);
|
root.deleteSync(recursive: true);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@ import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||||
import 'package:jj_mkvmaker/services/secret_store.dart';
|
import 'package:jj_mkvmaker/services/secret_store.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/vault_view.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/reader_page.dart' show ReaderPage;
|
||||||
import 'package:jj_mkvmaker/ui/vault_page.dart';
|
import 'package:jj_mkvmaker/ui/vault_page.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
|
@ -41,7 +44,7 @@ void main() {
|
||||||
Future<void> until(WidgetTester t, bool Function() ok, {int rounds = 200}) async {
|
Future<void> until(WidgetTester t, bool Function() ok, {int rounds = 200}) async {
|
||||||
for (var i = 0; i < rounds && !ok(); i++) {
|
for (var i = 0; i < rounds && !ok(); i++) {
|
||||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
|
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
|
||||||
await t.pump();
|
await t.pump(const Duration(milliseconds: 20));
|
||||||
}
|
}
|
||||||
// 닫히는 창 · 메뉴의 막 (barrier) 이 사라질 때까지
|
// 닫히는 창 · 메뉴의 막 (barrier) 이 사라질 때까지
|
||||||
for (var i = 0; i < 8; i++) {
|
for (var i = 0; i < 8; i++) {
|
||||||
|
|
@ -71,9 +74,13 @@ void main() {
|
||||||
addTearDown(t.view.reset);
|
addTearDown(t.view.reset);
|
||||||
final src = File(p.join(tmp.path, '비밀 문서.pdf'))..writeAsBytesSync(List.generate(3000, (i) => i % 256));
|
final src = File(p.join(tmp.path, '비밀 문서.pdf'))..writeAsBytesSync(List.generate(3000, (i) => i % 256));
|
||||||
final deleted = <List<String>>[];
|
final deleted = <List<String>>[];
|
||||||
|
final permanents = <bool>[];
|
||||||
final c = ctl();
|
final c = ctl();
|
||||||
await t.pumpWidget(MaterialApp(
|
await t.pumpWidget(MaterialApp(
|
||||||
home: VaultPage(c: c, add: [src.path], deleteOriginals: (paths) async => deleted.add(paths))));
|
home: VaultPage(c: c, add: [src.path], deleteOriginals: (paths, {required permanent}) async {
|
||||||
|
deleted.add(paths);
|
||||||
|
permanents.add(permanent);
|
||||||
|
})));
|
||||||
await until(t, () => find.text('새 보관함 만들기').evaluate().isNotEmpty);
|
await until(t, () => find.text('새 보관함 만들기').evaluate().isNotEmpty);
|
||||||
expect(find.textContaining('다른 앱도 이 폴더의 암호화된 파일을 볼 수 있습니다'), findsOneWidget);
|
expect(find.textContaining('다른 앱도 이 폴더의 암호화된 파일을 볼 수 있습니다'), findsOneWidget);
|
||||||
expect(find.textContaining('이 폴더를 지우면 보관함도 사라집니다'), findsOneWidget);
|
expect(find.textContaining('이 폴더를 지우면 보관함도 사라집니다'), findsOneWidget);
|
||||||
|
|
@ -91,9 +98,15 @@ void main() {
|
||||||
expect(t.widget<CheckboxListTile>(find.byType(CheckboxListTile)).value, isFalse, reason: '처음은 복사');
|
expect(t.widget<CheckboxListTile>(find.byType(CheckboxListTile)).value, isFalse, reason: '처음은 복사');
|
||||||
await t.tap(find.text('원본 지우기 (이동)'));
|
await t.tap(find.text('원본 지우기 (이동)'));
|
||||||
await t.pump();
|
await t.pump();
|
||||||
|
// 관리자 지시 (10/21): 휴지통에 평문이 남는다는 알림 · 처음 값 휴지통 · 영구 삭제를 고를 수 있음
|
||||||
|
expect(find.textContaining('휴지통에 평문'), findsOneWidget);
|
||||||
|
expect(find.text('휴지통으로 (처음 값)'), findsOneWidget);
|
||||||
|
await t.tap(find.text('영구 삭제'));
|
||||||
|
await t.pump();
|
||||||
await t.tap(find.widgetWithText(FilledButton, '넣기'));
|
await t.tap(find.widgetWithText(FilledButton, '넣기'));
|
||||||
await until(t, () => deleted.isNotEmpty);
|
await until(t, () => deleted.isNotEmpty);
|
||||||
expect(deleted.single, [src.path], reason: '다 넣은 뒤 탐색기의 지우기로');
|
expect(deleted.single, [src.path], reason: '다 넣은 뒤 탐색기의 지우기로');
|
||||||
|
expect(permanents.single, isTrue, reason: '영구 삭제를 골랐음 (탐색기가 영구 삭제 확인 창을 한 번 더)');
|
||||||
expect(find.text('비밀 문서.pdf'), findsOneWidget);
|
expect(find.text('비밀 문서.pdf'), findsOneWidget);
|
||||||
expect(Directory(p.join(tmp.path, 'vault')).listSync().map((f) => p.basename(f.path)).join(), isNot(contains('비밀')));
|
expect(Directory(p.join(tmp.path, 'vault')).listSync().map((f) => p.basename(f.path)).join(), isNot(contains('비밀')));
|
||||||
// 잠금 → 틀린 비밀번호 → 맞는 비밀번호
|
// 잠금 → 틀린 비밀번호 → 맞는 비밀번호
|
||||||
|
|
@ -116,6 +129,8 @@ void main() {
|
||||||
await t.tap(find.widgetWithText(FilledButton, '지우기'));
|
await t.tap(find.widgetWithText(FilledButton, '지우기'));
|
||||||
await until(t, () => find.text('보관함이 비어 있습니다').evaluate().isNotEmpty);
|
await until(t, () => find.text('보관함이 비어 있습니다').evaluate().isNotEmpty);
|
||||||
expect(find.text('보관함이 비어 있습니다'), findsOneWidget);
|
expect(find.text('보관함이 비어 있습니다'), findsOneWidget);
|
||||||
|
// 목록 쓰기 (index.jjv.tmp → index.jjv) 가 끝날 때까지 (화면은 먼저 바뀔 수 있다)
|
||||||
|
await until(t, () => !Directory(p.join(tmp.path, 'vault')).listSync().any((f) => f.path.endsWith('.tmp')));
|
||||||
await t.pumpWidget(const SizedBox());
|
await t.pumpWidget(const SizedBox());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -175,6 +190,8 @@ void main() {
|
||||||
await t.tap(find.widgetWithText(FilledButton, '열기'));
|
await t.tap(find.widgetWithText(FilledButton, '열기'));
|
||||||
await until(t, () => find.text('보관함이 비어 있습니다').evaluate().isNotEmpty);
|
await until(t, () => find.text('보관함이 비어 있습니다').evaluate().isNotEmpty);
|
||||||
expect(find.text('보관함이 비어 있습니다'), findsOneWidget);
|
expect(find.text('보관함이 비어 있습니다'), findsOneWidget);
|
||||||
|
// 목록 쓰기 (index.jjv.tmp → index.jjv) 가 끝날 때까지 (화면은 먼저 바뀔 수 있다)
|
||||||
|
await until(t, () => !Directory(p.join(tmp.path, 'vault')).listSync().any((f) => f.path.endsWith('.tmp')));
|
||||||
await t.pumpWidget(const SizedBox());
|
await t.pumpWidget(const SizedBox());
|
||||||
});
|
});
|
||||||
testWidgets('파일 탐색기: [보관함에 넣기] 는 보관함을 켰을 때 · 이 기기의 파일에만 (폴더는 2단계)', (t) async {
|
testWidgets('파일 탐색기: [보관함에 넣기] 는 보관함을 켰을 때 · 이 기기의 파일에만 (폴더는 2단계)', (t) async {
|
||||||
|
|
@ -207,4 +224,60 @@ void main() {
|
||||||
await t.pump(const Duration(seconds: 1));
|
await t.pump(const Duration(seconds: 1));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
test('앱을 켤 때 남은 보기 폴더 (jj_vault_view_…) 만 지움', () {
|
||||||
|
final root = Directory(p.join(tmp.path, 'root'))..createSync();
|
||||||
|
Directory(p.join(root.path, 'jj_vault_view_123')).createSync();
|
||||||
|
File(p.join(root.path, 'jj_vault_view_123', 'v.mp4')).writeAsStringSync('plain');
|
||||||
|
Directory(p.join(root.path, 'other')).createSync();
|
||||||
|
expect(removeStaleVaultViews(root.path), ['jj_vault_view_123']);
|
||||||
|
expect(Directory(root.path).listSync().map((e) => p.basename(e.path)), ['other']);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('보기: 그림은 메모리에서 (파일로 풀지 않음) · 동영상은 앱 전용 폴더에 잠깐 · 잠그면 열린 보기를 닫고 지움 → 남은 것 0', (t) async {
|
||||||
|
t.view.physicalSize = const Size(900, 1400);
|
||||||
|
t.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(t.view.reset);
|
||||||
|
final viewRoot = Directory(p.join(tmp.path, 'viewroot'));
|
||||||
|
final wasRoot = vaultViewRootOf;
|
||||||
|
vaultViewRootOf = () async => viewRoot;
|
||||||
|
addTearDown(() => vaultViewRootOf = wasRoot);
|
||||||
|
List<String> leftovers() =>
|
||||||
|
viewRoot.existsSync() ? viewRoot.listSync(recursive: true).map((e) => e.path).toList() : const <String>[];
|
||||||
|
final photo = File(p.join(tmp.path, 'photo.png'))..writeAsBytesSync(img.encodePng(img.Image(width: 20, height: 10)));
|
||||||
|
final clip = File(p.join(tmp.path, 'clip.mp4'))..writeAsBytesSync(List.filled(500, 1));
|
||||||
|
final c = ctl()..settings.vaultAutoLockSeconds = 0;
|
||||||
|
await t.pumpWidget(MaterialApp(home: VaultPage(c: c, add: [photo.path, clip.path])));
|
||||||
|
await until(t, () => find.text('새 보관함 만들기').evaluate().isNotEmpty);
|
||||||
|
await t.enterText(find.byType(TextField).at(0), 'pw');
|
||||||
|
await t.enterText(find.byType(TextField).at(1), 'pw');
|
||||||
|
await t.tap(find.text('비밀번호를 잊으면 되살릴 수 없음을 압니다'));
|
||||||
|
await t.pump();
|
||||||
|
await t.tap(find.widgetWithText(FilledButton, '만들기'));
|
||||||
|
await until(t, () => find.widgetWithText(FilledButton, '넣기').evaluate().isNotEmpty);
|
||||||
|
await t.tap(find.widgetWithText(FilledButton, '넣기'));
|
||||||
|
await until(t, () => find.text('photo.png').evaluate().isNotEmpty && find.text('clip.mp4').evaluate().isNotEmpty);
|
||||||
|
// 동영상 보기 (시험에는 플레이어가 없어 바로 돌아옴) → 푼 파일은 지워짐
|
||||||
|
await t.tap(find.text('clip.mp4'));
|
||||||
|
await until(t, () => find.byType(LinearProgressIndicator).evaluate().isEmpty);
|
||||||
|
expect(leftovers().where((x) => x.endsWith('.mp4')), isEmpty, reason: '보기를 닫으면 지움');
|
||||||
|
// 그림 보기: 파일로 풀지 않음
|
||||||
|
await t.tap(find.text('photo.png'));
|
||||||
|
await until(t, () => find.byType(ReaderPage).evaluate().isNotEmpty);
|
||||||
|
expect(find.byType(ReaderPage), findsOneWidget);
|
||||||
|
expect(leftovers().where((x) => x.endsWith('.png')), isEmpty, reason: '그림은 메모리에서');
|
||||||
|
// 앱이 뒤로 갔다 돌아옴 (자동 잠금 0초) → 보기 닫고 잠금 · 남은 것 0
|
||||||
|
t.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
|
||||||
|
t.binding.handleAppLifecycleStateChanged(AppLifecycleState.hidden);
|
||||||
|
t.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused);
|
||||||
|
await t.pump();
|
||||||
|
t.binding.handleAppLifecycleStateChanged(AppLifecycleState.hidden);
|
||||||
|
t.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
|
||||||
|
t.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
|
||||||
|
await until(t, () => find.byType(ReaderPage).evaluate().isEmpty);
|
||||||
|
expect(find.byType(ReaderPage), findsNothing, reason: '잠그면 열린 보기를 닫음');
|
||||||
|
expect(find.widgetWithText(FilledButton, '열기'), findsOneWidget, reason: '잠금 화면');
|
||||||
|
await until(t, () => leftovers().isEmpty);
|
||||||
|
expect(leftovers(), isEmpty, reason: '잠그면 잠깐 푼 폴더도 지움');
|
||||||
|
await t.pumpWidget(const SizedBox());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue