WIP 86 보충 (관리자 · 15): 덮어쓰기에도 받는 쪽이 더 새것이면 그대로 · Windows 에서 쓸 수 없는 이름은 미리 건너뜀 · 릴리스에 7-Zip 꼭 (검토 대기)
- [덮어쓰기] 를 골라도 설정 "받는 쪽이 더 새것이면 건너뛰기" (229, 처음 켜짐) 를 따른다: 이미 있는 파일이 압축 안의 것보다 2초 넘게 새것이면 덮지 않는다 (ZIP 은 DOS 시각, 7z 는 목록의 Modified 로 견줘 목록 파일에서 뺌). 끝 알림 "n개는 더 새 파일이라 그대로 둠". 확인 창에 [덮어쓰기] 를 고르면 "덮어쓴 파일은 되돌릴 수 없습니다" (휴지통에 남지 않음). - 압축 안 이름이 이 기기에서 쓸 수 없으면 (Windows: CON · NUL · ? * " < > | · 끝 점 · 빈칸 - 78 의 nameProblem) 풀기 전에 걸러 확인 창에 "풀 수 없는 이름 n개 (건너뜀)" - 나머지는 그대로 푼다 (풀다 실패하거나 7-Zip 이 다른 이름으로 바꾸지 않게). Android 는 지금처럼. - 릴리스: make_zip.ps1 이 Lib\7zip\7z.exe · 7z.dll · License.txt 가 없으면 실패. CMake 는 없으면 경고 (개발 빌드는 그대로). - (번역 파일에는 다음 커밋 87 의 글도 함께 들어 있다 - 쓰이지 않는 열쇠) - 시험: archive_ops_test (쓸 수 없는 이름 · ZIP 과 실제 7z 의 받는 쪽이 더 새 파일). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음6
parent
219510fec0
commit
c440b688ea
|
|
@ -321,6 +321,7 @@
|
|||
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} failed: {1} {2}{3}",
|
||||
"WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV, OpenSubtitles, etc. Server list and user names are kept",
|
||||
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "Keeps WebDAV and OpenSubtitles passwords in this device's secure storage. When off, nothing is saved (existing ones are removed) and you enter them each time the app starts",
|
||||
"WebDAV 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV reads everything inside from the server, which can take a long time. Analyze?",
|
||||
"WebDAV 서버 \"{0}\" 을(를) 지울까요?": "Delete WebDAV server \"{0}\"?",
|
||||
"WebDAV 서버 고치기": "Edit WebDAV server",
|
||||
"WebDAV 서버 목록과 그 저장된 비밀번호도 지워집니다. 동영상 목록 · 즐겨찾기 · 파일은 그대로입니다": "The WebDAV server list and its saved passwords are deleted too. The video list, bookmarks and files are kept",
|
||||
|
|
@ -435,6 +436,7 @@
|
|||
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2} (change at the top of the main screen)",
|
||||
"{0} · {1}배 · {2}": "{0} · {1}× · {2}",
|
||||
"{0} · 링크 {1}개 제외": "{0} · {1} links excluded",
|
||||
"{0} · 파일 {1}개": "{0} · {1} files",
|
||||
"{0} → {1} 번역 {2}%": "{0} → {1} translation {2}%",
|
||||
"{0} 는 이미 끝났습니다": "{0} has already finished",
|
||||
"{0} 라이선스": "{0} license",
|
||||
|
|
@ -510,6 +512,7 @@
|
|||
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "Delete {0} items? This cannot be undone.",
|
||||
"{0}개 항목을 휴지통으로 보낼까요?": "Move {0} items to the Recycle Bin?",
|
||||
"{0}개 항목을 휴지통으로 보냈습니다.": "Moved {0} items to the Recycle Bin.",
|
||||
"{0}개는 더 새 파일이라 그대로 둠": "{0} kept (existing file is newer)",
|
||||
"{0}개는 원본에 남음 (건너뜀)": "{0} left in the source (skipped)",
|
||||
"{0}개는 지우지 않았습니다 (취소).": "{0} were not deleted (cancelled).",
|
||||
"{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} were too large for the Recycle Bin and were deleted permanently (as Windows asked).",
|
||||
|
|
@ -924,6 +927,8 @@
|
|||
"덮어쓰기 (원래 파일은 없어집니다)": "Overwrite (the existing file is replaced)",
|
||||
"덮어쓰기여도 다 만든 뒤에만 바꿉니다 (도중에 취소 · 실패하면 예전 MKV 그대로)": "Even when overwriting, the old file is replaced only after the new one is finished (if cancelled or failed, the old MKV stays)",
|
||||
"덮어쓰기여도 다 만든 뒤에만 바꿉니다. 도중에 취소 · 실패하면 예전 MKV 는 그대로 남습니다. 늘 같게 하려면 환경 설정 > MKV 만들기 > 만들 MKV 가 이미 있으면": "Even when overwriting, the old file is replaced only after the new one is finished. If cancelled or failed, the old MKV stays. To always do the same: Settings > Make MKV > If the MKV already exists",
|
||||
"덮어쓴 파일은 되돌릴 수 없습니다 (받는 쪽이 더 새 파일은 그대로 둡니다).": "Overwritten files can't be restored (files that are newer at the destination are kept).",
|
||||
"덮어쓴 파일은 되돌릴 수 없습니다.": "Overwritten files can't be restored.",
|
||||
"덮어씁니다": "overwrites it",
|
||||
"덴마크어": "Danish",
|
||||
"독일어": "German",
|
||||
|
|
@ -1004,6 +1009,7 @@
|
|||
"링크 {0} (건너뜀)": "Links {0} (skipped)",
|
||||
"링크 {0}개는 넣지 않음": "{0} links not included",
|
||||
"링크 {0}개는 만들지 않고 건너뜁니다: {1}": "{0} links will be skipped (not created): {1}",
|
||||
"링크 {0}개는 세지 않음": "{0} links not counted",
|
||||
"링크 · junction 은 넣지 않습니다": "Links and junctions are not included",
|
||||
"마스터 · 최상 비밀번호": "Master and super passwords",
|
||||
"마스터 비밀번호": "Master password",
|
||||
|
|
@ -1270,8 +1276,10 @@
|
|||
"복사했습니다.": "copied.",
|
||||
"볼 그림이 없습니다.": "No images to show.",
|
||||
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "This video is unavailable (deleted · region restricted).",
|
||||
"분석": "Analyze",
|
||||
"분석 실패: {0} - {1}": "Analysis failed: {0} - {1}",
|
||||
"분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "Re-analyzed {0} videos that had failed",
|
||||
"분석하지 못했습니다: {0}": "Couldn't analyze: {0}",
|
||||
"붙여넣기": "Paste",
|
||||
"붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "Nothing to paste. First hold items with [Copy] · [Move].",
|
||||
"브라우저": "Browser",
|
||||
|
|
@ -1505,6 +1513,7 @@
|
|||
"아직 지원하지 않는 플랫폼입니다: {0}": "This platform is not supported yet: {0}",
|
||||
"아직 허용되지 않았습니다": "Not allowed yet",
|
||||
"안내 닫기": "Close tip",
|
||||
"안에 폴더가 없습니다": "No folders inside",
|
||||
"안쪽 파일을 확인하는 중 …": "Checking files inside …",
|
||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "{0} files inside are newer on the receiving side - they are skipped when merging (they stay in the source)",
|
||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "{0} files inside are newer on the receiving side - merging overwrites them with older ones",
|
||||
|
|
@ -1893,12 +1902,14 @@
|
|||
"일본어": "Japanese",
|
||||
"일시정지": "Pause",
|
||||
"읽기 전용 매체입니다": "Read-only media",
|
||||
"읽는 중 · 폴더 {0}개 · {1}": "Reading · {0} folders · {1}",
|
||||
"읽을 수 없습니다": "Cannot read",
|
||||
"읽을 수 없습니다: {0}": "Cannot read: {0}",
|
||||
"읽을 수 없어 건너뛴 폴더: {0}": "Skipped folders that could not be read: {0}",
|
||||
"읽을 수 없음": "Cannot read",
|
||||
"읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "The original file that could not be read was not deleted; it is kept here:",
|
||||
"읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "{0} unreadable folders (permission or connection problem) - not searched inside",
|
||||
"읽지 못한 폴더 {0}개 (크기가 실제보다 작을 수 있음)": "{0} folders couldn't be read (sizes may be smaller than actual)",
|
||||
"임시 파일 · 남은 조각 정리": "Clean temporary files · leftovers",
|
||||
"자동": "Auto",
|
||||
"자동 (가로로 긴 그림만)": "Auto (wide images only)",
|
||||
|
|
@ -1986,6 +1997,7 @@
|
|||
"저장": "Save",
|
||||
"저장 (MKV 반영)": "Save (apply to MKV)",
|
||||
"저장 (MKV 반영) *": "Save (apply to MKV) *",
|
||||
"저장 공간 분석": "Storage analysis",
|
||||
"저장 공간 정리": "Storage cleanup",
|
||||
"저장 공간 정리: 파일 {0}개 · {1}": "Storage cleanup: {0} files · {1}",
|
||||
"저장 공간이 모자랍니다": "Not enough storage space",
|
||||
|
|
@ -2269,6 +2281,9 @@
|
|||
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 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",
|
||||
"큰 파일": "Large files",
|
||||
"큰 파일이 없습니다": "No large files",
|
||||
"큰 폴더": "Large folders",
|
||||
"키 있음": "Key saved",
|
||||
"타갈로그어": "Tagalog",
|
||||
"타밀어": "Tamil",
|
||||
|
|
@ -2290,6 +2305,7 @@
|
|||
"트레이의 JJ_MKVMaker 아이콘을 누르면 열립니다.": "Click the JJ_MKVMaker icon in the tray to open it.",
|
||||
"파일": "File",
|
||||
"파일 {0}/{1}": "Files {0}/{1}",
|
||||
"파일 {0}개": "{0} files",
|
||||
"파일 {0}개 · {1}": "{0} files · {1}",
|
||||
"파일 관리자에서 보기": "Show in file manager",
|
||||
"파일 복사 · 이동 방법": "File copy · move method",
|
||||
|
|
@ -2382,6 +2398,8 @@
|
|||
"표준": "Standard",
|
||||
"푸는 중": "Extracting",
|
||||
"푸는 중: {0}": "Extracting: {0}",
|
||||
"풀 수 없는 이름 {0}개": "{0} invalid names",
|
||||
"풀 수 없는 이름 {0}개 (건너뜀): {1}": "{0} names can't be used on this device (skipped): {1}",
|
||||
"풀기": "Extract",
|
||||
"풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?": "The extracted size ({0}) is larger than the free space ({1}). Extract anyway?",
|
||||
"풀지 못했습니다: {0}": "Could not extract: {0}",
|
||||
|
|
|
|||
|
|
@ -321,6 +321,7 @@
|
|||
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失敗: {1} {2}{3}",
|
||||
"WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV・OpenSubtitles など。サーバー一覧と ID は残します",
|
||||
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "WebDAV・OpenSubtitles のパスワードをこの端末の安全な保存領域に保存します。オフにすると保存せず (既存のものも削除)、アプリを起動するたびに入力します",
|
||||
"WebDAV 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV はサーバーから中身をすべて読むため、時間がかかることがあります。分析しますか?",
|
||||
"WebDAV 서버 \"{0}\" 을(를) 지울까요?": "WebDAV サーバー「{0}」を削除しますか?",
|
||||
"WebDAV 서버 고치기": "WebDAV サーバーを編集",
|
||||
"WebDAV 서버 목록과 그 저장된 비밀번호도 지워집니다. 동영상 목록 · 즐겨찾기 · 파일은 그대로입니다": "WebDAV サーバー一覧とその保存したパスワードも消えます。動画リスト・お気に入り・ファイルはそのままです",
|
||||
|
|
@ -435,6 +436,7 @@
|
|||
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)",
|
||||
"{0} · {1}배 · {2}": "{0}・{1} 倍・{2}",
|
||||
"{0} · 링크 {1}개 제외": "{0} · リンク {1} 件を除く",
|
||||
"{0} · 파일 {1}개": "{0} · ファイル {1} 件",
|
||||
"{0} → {1} 번역 {2}%": "{0} → {1} 翻訳 {2}%",
|
||||
"{0} 는 이미 끝났습니다": "{0} はすでに終わりました",
|
||||
"{0} 라이선스": "{0} ライセンス",
|
||||
|
|
@ -510,6 +512,7 @@
|
|||
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "{0} 件の項目を削除しますか? 元に戻せません。",
|
||||
"{0}개 항목을 휴지통으로 보낼까요?": "{0} 個の項目をごみ箱に移動しますか?",
|
||||
"{0}개 항목을 휴지통으로 보냈습니다.": "{0} 個の項目をごみ箱に移動しました。",
|
||||
"{0}개는 더 새 파일이라 그대로 둠": "{0} 件は新しいファイルなのでそのまま",
|
||||
"{0}개는 원본에 남음 (건너뜀)": "{0} 件は元の場所に残っています (スキップ)",
|
||||
"{0}개는 지우지 않았습니다 (취소).": "{0} 個は削除しませんでした (キャンセル)。",
|
||||
"{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} 個はごみ箱より大きいため完全に削除しました (Windows の確認どおり)。",
|
||||
|
|
@ -924,6 +927,8 @@
|
|||
"덮어쓰기 (원래 파일은 없어집니다)": "上書き (元のファイルは消えます)",
|
||||
"덮어쓰기여도 다 만든 뒤에만 바꿉니다 (도중에 취소 · 실패하면 예전 MKV 그대로)": "上書きでも作り終えてから置き換えます (途中でキャンセル · 失敗すると以前の MKV はそのまま)",
|
||||
"덮어쓰기여도 다 만든 뒤에만 바꿉니다. 도중에 취소 · 실패하면 예전 MKV 는 그대로 남습니다. 늘 같게 하려면 환경 설정 > MKV 만들기 > 만들 MKV 가 이미 있으면": "上書きでも作り終えてから置き換えます。途中でキャンセル · 失敗すると以前の MKV はそのまま残ります。いつも同じにするには: 環境設定 > MKV 作成 > 作る MKV がすでにあるとき",
|
||||
"덮어쓴 파일은 되돌릴 수 없습니다 (받는 쪽이 더 새 파일은 그대로 둡니다).": "上書きしたファイルは元に戻せません (受け取る側が新しいファイルはそのまま残します)。",
|
||||
"덮어쓴 파일은 되돌릴 수 없습니다.": "上書きしたファイルは元に戻せません。",
|
||||
"덮어씁니다": "上書きします",
|
||||
"덴마크어": "デンマーク語",
|
||||
"독일어": "ドイツ語",
|
||||
|
|
@ -1004,6 +1009,7 @@
|
|||
"링크 {0} (건너뜀)": "リンク {0} (スキップ)",
|
||||
"링크 {0}개는 넣지 않음": "リンク {0} 件は含めず",
|
||||
"링크 {0}개는 만들지 않고 건너뜁니다: {1}": "リンク {0} 件は作成せずスキップします: {1}",
|
||||
"링크 {0}개는 세지 않음": "リンク {0} 件は数えず",
|
||||
"링크 · junction 은 넣지 않습니다": "リンク・ジャンクションは含めません",
|
||||
"마스터 · 최상 비밀번호": "マスター / 最上位パスワード",
|
||||
"마스터 비밀번호": "マスターパスワード",
|
||||
|
|
@ -1270,8 +1276,10 @@
|
|||
"복사했습니다.": "コピーしました。",
|
||||
"볼 그림이 없습니다.": "表示する画像がありません。",
|
||||
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "視聴できない動画です(削除・地域制限)。",
|
||||
"분석": "分析",
|
||||
"분석 실패: {0} - {1}": "分析失敗: {0} - {1}",
|
||||
"분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "分析に失敗していた動画 {0} 件を再分析しました",
|
||||
"분석하지 못했습니다: {0}": "分析できませんでした: {0}",
|
||||
"붙여넣기": "貼り付け",
|
||||
"붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "貼り付ける項目がありません。先に [コピー] · [移動] で保持してください。",
|
||||
"브라우저": "ブラウザー",
|
||||
|
|
@ -1505,6 +1513,7 @@
|
|||
"아직 지원하지 않는 플랫폼입니다: {0}": "まだ対応していないプラットフォームです: {0}",
|
||||
"아직 허용되지 않았습니다": "まだ許可されていません",
|
||||
"안내 닫기": "案内を閉じる",
|
||||
"안에 폴더가 없습니다": "中にフォルダーがありません",
|
||||
"안쪽 파일을 확인하는 중 …": "中のファイルを確認中 …",
|
||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "中に受け取る側の方が新しいファイルが {0} 件 - 合わせてもスキップします (元に残ります)",
|
||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "中に受け取る側の方が新しいファイルが {0} 件 - 合わせると古いもので上書きします",
|
||||
|
|
@ -1893,12 +1902,14 @@
|
|||
"일본어": "日本語",
|
||||
"일시정지": "一時停止",
|
||||
"읽기 전용 매체입니다": "読み取り専用メディアです",
|
||||
"읽는 중 · 폴더 {0}개 · {1}": "読み込み中 · フォルダー {0} 件 · {1}",
|
||||
"읽을 수 없습니다": "読み込めません",
|
||||
"읽을 수 없습니다: {0}": "読み込めません: {0}",
|
||||
"읽을 수 없어 건너뛴 폴더: {0}": "読み込めずスキップしたフォルダー: {0}",
|
||||
"읽을 수 없음": "読み取れません",
|
||||
"읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "読めなかった元のファイルは削除せず、ここに保管しました:",
|
||||
"읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "読めなかったフォルダー {0} 個 (権限・接続の問題) - その中は探せませんでした",
|
||||
"읽지 못한 폴더 {0}개 (크기가 실제보다 작을 수 있음)": "読めなかったフォルダー {0} 件 (サイズが実際より小さいことがあります)",
|
||||
"임시 파일 · 남은 조각 정리": "一時ファイル・残った断片を整理",
|
||||
"자동": "自動",
|
||||
"자동 (가로로 긴 그림만)": "自動 (横長の画像のみ)",
|
||||
|
|
@ -1986,6 +1997,7 @@
|
|||
"저장": "保存",
|
||||
"저장 (MKV 반영)": "保存(MKV に反映)",
|
||||
"저장 (MKV 반영) *": "保存(MKV に反映)*",
|
||||
"저장 공간 분석": "ストレージ分析",
|
||||
"저장 공간 정리": "ストレージの整理",
|
||||
"저장 공간 정리: 파일 {0}개 · {1}": "ストレージの整理: ファイル {0}個・{1}",
|
||||
"저장 공간이 모자랍니다": "保存容量が足りません",
|
||||
|
|
@ -2269,6 +2281,9 @@
|
|||
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "サイズを変えると再エンコードが必要なため、コーデックを「元のまま」から H.264 に変更しました。",
|
||||
"크기순 정렬일 때만 (기본)": "サイズ順のときだけ (既定)",
|
||||
"크메르어": "クメール語",
|
||||
"큰 파일": "大きいファイル",
|
||||
"큰 파일이 없습니다": "大きいファイルはありません",
|
||||
"큰 폴더": "大きいフォルダー",
|
||||
"키 있음": "キーあり",
|
||||
"타갈로그어": "タガログ語",
|
||||
"타밀어": "タミル語",
|
||||
|
|
@ -2290,6 +2305,7 @@
|
|||
"트레이의 JJ_MKVMaker 아이콘을 누르면 열립니다.": "トレイの JJ_MKVMaker アイコンをクリックすると開きます。",
|
||||
"파일": "ファイル",
|
||||
"파일 {0}/{1}": "ファイル {0}/{1}",
|
||||
"파일 {0}개": "ファイル {0} 件",
|
||||
"파일 {0}개 · {1}": "ファイル {0} 件 · {1}",
|
||||
"파일 관리자에서 보기": "ファイルマネージャーで表示",
|
||||
"파일 복사 · 이동 방법": "ファイルのコピー · 移動方法",
|
||||
|
|
@ -2382,6 +2398,8 @@
|
|||
"표준": "標準",
|
||||
"푸는 중": "展開中",
|
||||
"푸는 중: {0}": "展開中: {0}",
|
||||
"풀 수 없는 이름 {0}개": "展開できない名前 {0} 件",
|
||||
"풀 수 없는 이름 {0}개 (건너뜀): {1}": "展開できない名前 {0} 件 (スキップ): {1}",
|
||||
"풀기": "展開",
|
||||
"풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?": "展開後のサイズ ({0}) が空き容量 ({1}) より大きいです。それでも展開しますか?",
|
||||
"풀지 못했습니다: {0}": "展開できませんでした: {0}",
|
||||
|
|
|
|||
|
|
@ -321,6 +321,7 @@
|
|||
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失败: {1} {2}{3}",
|
||||
"WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV · OpenSubtitles 等。保留服务器列表和用户名",
|
||||
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "将 WebDAV · OpenSubtitles 密码保存在本设备的安全存储中。关闭后不保存 (已有的也会删除),每次启动应用时重新输入",
|
||||
"WebDAV 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV 需要从服务器读取全部内容,可能需要较长时间。要分析吗?",
|
||||
"WebDAV 서버 \"{0}\" 을(를) 지울까요?": "删除 WebDAV 服务器“{0}”吗?",
|
||||
"WebDAV 서버 고치기": "编辑 WebDAV 服务器",
|
||||
"WebDAV 서버 목록과 그 저장된 비밀번호도 지워집니다. 동영상 목록 · 즐겨찾기 · 파일은 그대로입니다": "WebDAV 服务器列表及其保存的密码也会被删除。视频列表 · 收藏 · 文件保持不变",
|
||||
|
|
@ -435,6 +436,7 @@
|
|||
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)",
|
||||
"{0} · {1}배 · {2}": "{0} · {1} 倍 · {2}",
|
||||
"{0} · 링크 {1}개 제외": "{0} · 已排除 {1} 个链接",
|
||||
"{0} · 파일 {1}개": "{0} · {1} 个文件",
|
||||
"{0} → {1} 번역 {2}%": "{0} → {1} 翻译 {2}%",
|
||||
"{0} 는 이미 끝났습니다": "{0} 已经完成",
|
||||
"{0} 라이선스": "{0} 许可证",
|
||||
|
|
@ -510,6 +512,7 @@
|
|||
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "删除 {0} 个项目吗? 此操作无法撤销。",
|
||||
"{0}개 항목을 휴지통으로 보낼까요?": "将 {0} 个项目移到回收站吗?",
|
||||
"{0}개 항목을 휴지통으로 보냈습니다.": "已将 {0} 个项目移到回收站。",
|
||||
"{0}개는 더 새 파일이라 그대로 둠": "{0} 个因现有文件更新而保留",
|
||||
"{0}개는 원본에 남음 (건너뜀)": "{0} 个留在源中(已跳过)",
|
||||
"{0}개는 지우지 않았습니다 (취소).": "{0} 个未删除 (已取消)。",
|
||||
"{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} 个超出回收站容量,已永久删除 (按 Windows 的提示)。",
|
||||
|
|
@ -924,6 +927,8 @@
|
|||
"덮어쓰기 (원래 파일은 없어집니다)": "覆盖 (原文件将被替换)",
|
||||
"덮어쓰기여도 다 만든 뒤에만 바꿉니다 (도중에 취소 · 실패하면 예전 MKV 그대로)": "即使覆盖,也只在制作完成后才替换(中途取消 · 失败时保留原来的 MKV)",
|
||||
"덮어쓰기여도 다 만든 뒤에만 바꿉니다. 도중에 취소 · 실패하면 예전 MKV 는 그대로 남습니다. 늘 같게 하려면 환경 설정 > MKV 만들기 > 만들 MKV 가 이미 있으면": "即使覆盖,也只在制作完成后才替换。中途取消 · 失败时原来的 MKV 会保留。若要始终相同:设置 > 制作 MKV > 要制作的 MKV 已存在时",
|
||||
"덮어쓴 파일은 되돌릴 수 없습니다 (받는 쪽이 더 새 파일은 그대로 둡니다).": "被覆盖的文件无法恢复 (目标处较新的文件会保留)。",
|
||||
"덮어쓴 파일은 되돌릴 수 없습니다.": "被覆盖的文件无法恢复。",
|
||||
"덮어씁니다": "覆盖",
|
||||
"덴마크어": "丹麦语",
|
||||
"독일어": "德语",
|
||||
|
|
@ -1004,6 +1009,7 @@
|
|||
"링크 {0} (건너뜀)": "链接 {0}(跳过)",
|
||||
"링크 {0}개는 넣지 않음": "{0} 个链接未包含",
|
||||
"링크 {0}개는 만들지 않고 건너뜁니다: {1}": "{0} 个链接将跳过 (不创建): {1}",
|
||||
"링크 {0}개는 세지 않음": "{0} 个链接未计入",
|
||||
"링크 · junction 은 넣지 않습니다": "不包含链接和 junction",
|
||||
"마스터 · 최상 비밀번호": "主密码和最高密码",
|
||||
"마스터 비밀번호": "主密码",
|
||||
|
|
@ -1270,8 +1276,10 @@
|
|||
"복사했습니다.": "已复制。",
|
||||
"볼 그림이 없습니다.": "没有可显示的图片。",
|
||||
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "该视频无法观看(已删除 · 地区限制)。",
|
||||
"분석": "分析",
|
||||
"분석 실패: {0} - {1}": "分析失败: {0} - {1}",
|
||||
"분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "已重新分析之前分析失败的 {0} 个视频",
|
||||
"분석하지 못했습니다: {0}": "无法分析: {0}",
|
||||
"붙여넣기": "粘贴",
|
||||
"붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "没有可粘贴的项目。请先用 [复制] · [移动] 暂存。",
|
||||
"브라우저": "浏览器",
|
||||
|
|
@ -1505,6 +1513,7 @@
|
|||
"아직 지원하지 않는 플랫폼입니다: {0}": "尚不支持此平台: {0}",
|
||||
"아직 허용되지 않았습니다": "尚未允许",
|
||||
"안내 닫기": "关闭提示",
|
||||
"안에 폴더가 없습니다": "里面没有文件夹",
|
||||
"안쪽 파일을 확인하는 중 …": "正在检查里面的文件 …",
|
||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "里面有 {0} 个文件在接收端较新 - 合并时会跳过(保留在源中)",
|
||||
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "里面有 {0} 个文件在接收端较新 - 合并会用旧文件覆盖",
|
||||
|
|
@ -1893,12 +1902,14 @@
|
|||
"일본어": "日语",
|
||||
"일시정지": "暂停",
|
||||
"읽기 전용 매체입니다": "只读介质",
|
||||
"읽는 중 · 폴더 {0}개 · {1}": "正在读取 · {0} 个文件夹 · {1}",
|
||||
"읽을 수 없습니다": "无法读取",
|
||||
"읽을 수 없습니다: {0}": "无法读取: {0}",
|
||||
"읽을 수 없어 건너뛴 폴더: {0}": "无法读取而跳过的文件夹: {0}",
|
||||
"읽을 수 없음": "无法读取",
|
||||
"읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "无法读取的原文件没有删除,保存在这里:",
|
||||
"읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "{0} 个无法读取的文件夹 (权限 · 连接问题) - 未在其中查找",
|
||||
"읽지 못한 폴더 {0}개 (크기가 실제보다 작을 수 있음)": "{0} 个文件夹无法读取 (大小可能小于实际)",
|
||||
"임시 파일 · 남은 조각 정리": "清理临时文件 · 残留碎片",
|
||||
"자동": "自动",
|
||||
"자동 (가로로 긴 그림만)": "自动 (仅横向图片)",
|
||||
|
|
@ -1986,6 +1997,7 @@
|
|||
"저장": "保存",
|
||||
"저장 (MKV 반영)": "保存(应用到 MKV)",
|
||||
"저장 (MKV 반영) *": "保存(应用到 MKV)*",
|
||||
"저장 공간 분석": "存储空间分析",
|
||||
"저장 공간 정리": "存储空间清理",
|
||||
"저장 공간 정리: 파일 {0}개 · {1}": "存储空间清理: 文件 {0} 个 · {1}",
|
||||
"저장 공간이 모자랍니다": "存储空间不足",
|
||||
|
|
@ -2269,6 +2281,9 @@
|
|||
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "更改大小需要重新编码,因此编解码器已从“保持原样”改为 H.264。",
|
||||
"크기순 정렬일 때만 (기본)": "仅在按大小排序时 (默认)",
|
||||
"크메르어": "高棉语",
|
||||
"큰 파일": "大文件",
|
||||
"큰 파일이 없습니다": "没有大文件",
|
||||
"큰 폴더": "大文件夹",
|
||||
"키 있음": "已有密钥",
|
||||
"타갈로그어": "他加禄语",
|
||||
"타밀어": "泰米尔语",
|
||||
|
|
@ -2290,6 +2305,7 @@
|
|||
"트레이의 JJ_MKVMaker 아이콘을 누르면 열립니다.": "点击托盘中的 JJ_MKVMaker 图标即可打开。",
|
||||
"파일": "文件",
|
||||
"파일 {0}/{1}": "文件 {0}/{1}",
|
||||
"파일 {0}개": "{0} 个文件",
|
||||
"파일 {0}개 · {1}": "{0} 个文件 · {1}",
|
||||
"파일 관리자에서 보기": "在文件管理器中显示",
|
||||
"파일 복사 · 이동 방법": "文件复制 · 移动方式",
|
||||
|
|
@ -2382,6 +2398,8 @@
|
|||
"표준": "标准",
|
||||
"푸는 중": "正在解压",
|
||||
"푸는 중: {0}": "正在解压: {0}",
|
||||
"풀 수 없는 이름 {0}개": "{0} 个无法解压的名称",
|
||||
"풀 수 없는 이름 {0}개 (건너뜀): {1}": "{0} 个名称无法在本设备使用 (跳过): {1}",
|
||||
"풀기": "解压",
|
||||
"풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?": "解压后大小 ({0}) 大于剩余空间 ({1})。仍要解压吗?",
|
||||
"풀지 못했습니다: {0}": "无法解压: {0}",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:archive/archive_io.dart';
|
|||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../l10n/tr.dart';
|
||||
import 'file_ops.dart' show NameConflict;
|
||||
import 'file_ops.dart' show NameConflict, nameProblem;
|
||||
|
||||
/// 86: 압축 종류 (확장자로)
|
||||
enum ArchiveKind { zip, sevenZip, rar }
|
||||
|
|
@ -28,7 +28,11 @@ class ArchiveEntry {
|
|||
|
||||
/// 7-Zip 에 넘길 원래 이름 (Windows 의 7z 는 '\' 로 보여 준다)
|
||||
final String raw;
|
||||
const ArchiveEntry(this.name, {this.size = 0, this.isDir = false, this.isLink = false, this.encrypted = false, String? raw}) : raw = raw ?? name;
|
||||
|
||||
/// 압축 안 파일의 바뀐 날 (모르면 null) - 덮어쓰기에서 받는 쪽이 더 새것인지 볼 때
|
||||
final DateTime? modified;
|
||||
const ArchiveEntry(this.name, {this.size = 0, this.isDir = false, this.isLink = false, this.encrypted = false, String? raw, this.modified})
|
||||
: raw = raw ?? name;
|
||||
}
|
||||
|
||||
/// 비밀번호가 필요하거나 틀림
|
||||
|
|
@ -64,20 +68,26 @@ class ExtractPlan {
|
|||
/// 만들지 않고 건너뛰는 링크
|
||||
final List<String> links;
|
||||
|
||||
/// 이 기기에서 쓸 수 없는 이름 (Windows: CON · NUL · ? * " < > | · 끝 점 · 빈칸 …) - (이름, 까닭), 건너뜀
|
||||
final List<(String, String)> badNames;
|
||||
|
||||
/// 풀린 크기 합 (바이트)
|
||||
final int totalSize;
|
||||
|
||||
/// 비밀번호가 걸린 항목이 있음
|
||||
final bool encrypted;
|
||||
const ExtractPlan(this.files, this.unsafe, this.links, this.totalSize, this.encrypted);
|
||||
const ExtractPlan(this.files, this.unsafe, this.links, this.totalSize, this.encrypted, [this.badNames = const []]);
|
||||
}
|
||||
|
||||
/// 목록 [entries] 에서 풀 것을 고른다. [names] 가 있으면 그 이름 (폴더면 그 안 모두) 만
|
||||
ExtractPlan planExtract(List<ArchiveEntry> entries, {List<String> names = const []}) {
|
||||
/// 15 지적 · 관리자 판단: 리눅스 · 맥에서 만든 압축의 이름이 Windows 에서 쓸 수 없으면 (78 의 nameProblem) 미리 걸러 건너뛴다
|
||||
ExtractPlan planExtract(List<ArchiveEntry> entries, {List<String> names = const [], bool? windowsRules}) {
|
||||
final win = windowsRules ?? Platform.isWindows;
|
||||
bool wanted(String n) => names.isEmpty || names.any((x) => n == x || n.startsWith(x.endsWith('/') ? x : '$x/'));
|
||||
final files = <ArchiveEntry>[];
|
||||
final unsafe = <(String, String)>[];
|
||||
final links = <String>[];
|
||||
final badNames = <(String, String)>[];
|
||||
var size = 0;
|
||||
var enc = false;
|
||||
for (final e in entries) {
|
||||
|
|
@ -92,11 +102,17 @@ ExtractPlan planExtract(List<ArchiveEntry> entries, {List<String> names = const
|
|||
continue;
|
||||
}
|
||||
if (e.isDir) continue;
|
||||
// 이름의 각 부분이 이 기기에서 쓸 수 있는지 (풀다가 실패하거나 7-Zip 이 다른 이름으로 바꾸지 않게)
|
||||
final why = [for (final s in e.name.split('/')) if (s.isNotEmpty && s != '.') nameProblem(s, windowsRules: win)].nonNulls.firstOrNull;
|
||||
if (why != null) {
|
||||
badNames.add((e.name, why));
|
||||
continue;
|
||||
}
|
||||
files.add(e);
|
||||
size += e.size;
|
||||
enc |= e.encrypted;
|
||||
}
|
||||
return ExtractPlan(files, unsafe, links, size, enc);
|
||||
return ExtractPlan(files, unsafe, links, size, enc, badNames);
|
||||
}
|
||||
|
||||
/// 풀 때 [dest] 안의 자리 ([name] 은 [unsafeArchivePath] 를 지난 것)
|
||||
|
|
@ -118,6 +134,25 @@ class ExtractResult {
|
|||
int done = 0;
|
||||
final skippedExisting = <String>[];
|
||||
final renamed = <String>[];
|
||||
|
||||
/// 덮어쓰기인데 받는 쪽이 더 새것이라 그대로 둔 것 (229 와 같은 규칙)
|
||||
final keptNewer = <String>[];
|
||||
}
|
||||
|
||||
/// 덮어쓰기에서 건너뛸지: 받는 쪽 파일이 압축 안의 것보다 2초 넘게 새것
|
||||
bool _targetNewer(String target, DateTime? modified) {
|
||||
if (modified == null) return false;
|
||||
try {
|
||||
return File(target).lastModifiedSync().isAfter(modified.add(const Duration(seconds: 2)));
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// ZIP 의 MS-DOS 날짜 · 시각 (지역 시각) → DateTime (0 이면 null)
|
||||
DateTime? dosDateTime(int date, int time) {
|
||||
if (date == 0) return null;
|
||||
return DateTime(1980 + (date >> 9), (date >> 5) & 0xF, date & 0x1F, time >> 11, (time >> 5) & 0x3F, (time & 0x1F) * 2);
|
||||
}
|
||||
|
||||
/// 취소 (풀기 · 묶기 도중)
|
||||
|
|
@ -142,7 +177,8 @@ Future<List<ArchiveEntry>> listZip(String path) async {
|
|||
isDir: h.filename.endsWith('/') || h.filename.endsWith('\\'),
|
||||
// 유닉스 파일 종류 (상위 16비트) 가 심볼릭 링크 (0xA000)
|
||||
isLink: (h.externalFileAttributes >> 16) & 0xF000 == 0xA000,
|
||||
encrypted: h.generalPurposeBitFlag & 0x1 != 0),
|
||||
encrypted: h.generalPurposeBitFlag & 0x1 != 0,
|
||||
modified: dosDateTime(h.lastModifiedFileDate, h.lastModifiedFileTime)),
|
||||
];
|
||||
} finally {
|
||||
await input.close();
|
||||
|
|
@ -151,7 +187,11 @@ Future<List<ArchiveEntry>> listZip(String path) async {
|
|||
|
||||
/// ZIP 풀기 - [plan] 의 파일만, 같은 이름은 [conflict] 대로
|
||||
Future<ExtractResult> extractZipPlan(String path, String dest, ExtractPlan plan,
|
||||
{NameConflict conflict = NameConflict.rename, String? password, void Function(int done, int total)? onProgress, bool Function()? cancelled}) async {
|
||||
{NameConflict conflict = NameConflict.rename,
|
||||
bool skipNewer = false,
|
||||
String? password,
|
||||
void Function(int done, int total)? onProgress,
|
||||
bool Function()? cancelled}) async {
|
||||
// 비밀번호가 걸린 항목인데 비밀번호가 없으면 (암호 글자를 그대로 쓰지 않게) 먼저 묻는다
|
||||
if (plan.encrypted && (password == null || password.isEmpty)) throw const ArchivePasswordException();
|
||||
final input = InputFileStream(path);
|
||||
|
|
@ -160,6 +200,7 @@ Future<ExtractResult> extractZipPlan(String path, String dest, ExtractPlan plan,
|
|||
// verify: CRC 를 맞춰 보아 비밀번호가 틀리면 (풀린 내용이 다르면) 실패로
|
||||
final a = ZipDecoder().decodeStream(input, password: password, verify: plan.encrypted);
|
||||
final want = {for (final e in plan.files) e.name};
|
||||
final modified = {for (final e in plan.files) e.name: e.modified};
|
||||
final todo = [for (final f in a.files) if (f.isFile && want.contains(f.name.replaceAll('\\', '/'))) f];
|
||||
for (final f in todo) {
|
||||
if (cancelled?.call() ?? false) throw const ArchiveCancelled();
|
||||
|
|
@ -173,6 +214,11 @@ Future<ExtractResult> extractZipPlan(String path, String dest, ExtractPlan plan,
|
|||
target = _freeName(target);
|
||||
r.renamed.add(f.name);
|
||||
}
|
||||
// 관리자 판단: 덮어쓰기에도 "받는 쪽이 더 새것이면 건너뛰기" (229)
|
||||
if (conflict == NameConflict.overwrite && skipNewer && _targetNewer(target, modified[f.name.replaceAll('\\', '/')])) {
|
||||
r.keptNewer.add(f.name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
await Directory(p.dirname(target)).create(recursive: true);
|
||||
final bytes = f.readBytes();
|
||||
|
|
@ -270,6 +316,7 @@ List<ArchiveEntry> parse7zList(String out) {
|
|||
entries.add(ArchiveEntry(raw.replaceAll('\\', '/'),
|
||||
raw: raw,
|
||||
size: int.tryParse(m['Size'] ?? '') ?? 0,
|
||||
modified: DateTime.tryParse((m['Modified'] ?? '').replaceFirst(' ', 'T')),
|
||||
isDir: (m['Folder'] ?? '') == '+' || RegExp(r'^[A-Z_]*D').hasMatch(attr),
|
||||
isLink: link,
|
||||
encrypted: (m['Encrypted'] ?? '') == '+'));
|
||||
|
|
@ -326,8 +373,18 @@ Future<List<ArchiveEntry>> list7z(String exe, String path, {String? password}) a
|
|||
|
||||
/// 7z · RAR 풀기 - [plan] 의 파일만 (목록 파일로 넘김, 링크 · 막은 것은 빠짐). 같은 이름은 [conflict] 대로
|
||||
Future<ExtractResult> extract7zPlan(String exe, String path, String dest, ExtractPlan plan,
|
||||
{NameConflict conflict = NameConflict.rename, String? password, void Function(double v)? onProgress, bool Function()? cancelled}) async {
|
||||
{NameConflict conflict = NameConflict.rename,
|
||||
bool skipNewer = false,
|
||||
String? password,
|
||||
void Function(double v)? onProgress,
|
||||
bool Function()? cancelled}) async {
|
||||
final r = ExtractResult();
|
||||
// 관리자 판단: 덮어쓰기 (-aoa) 에도 받는 쪽이 더 새 파일은 목록에서 빼서 그대로 둔다 (229)
|
||||
if (conflict == NameConflict.overwrite && skipNewer) {
|
||||
final keep = [for (final e in plan.files) if (_targetNewer(extractTarget(dest, e.name), e.modified)) e];
|
||||
r.keptNewer.addAll(keep.map((e) => e.name));
|
||||
plan = ExtractPlan([for (final e in plan.files) if (!keep.contains(e)) e], plan.unsafe, plan.links, plan.totalSize, plan.encrypted, plan.badNames);
|
||||
}
|
||||
if (plan.files.isEmpty) return r;
|
||||
final tmp = await Directory.systemTemp.createTemp('jj_7z_');
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -173,6 +173,12 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
|
|||
[plan.unsafe.length, plan.unsafe.take(5).map((x) => '${x.$1} (${x.$2})').join(', ')]),
|
||||
style: const TextStyle(color: JjColors.danger, fontSize: 12)),
|
||||
],
|
||||
if (plan.badNames.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(trf('풀 수 없는 이름 {0}개 (건너뜀): {1}',
|
||||
[plan.badNames.length, plan.badNames.take(5).map((x) => '${x.$1} (${x.$2})').join(', ')]),
|
||||
style: const TextStyle(color: Colors.orangeAccent, fontSize: 12)),
|
||||
],
|
||||
if (plan.links.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(trf('링크 {0}개는 만들지 않고 건너뜁니다: {1}', [plan.links.length, plan.links.take(5).join(', ')]),
|
||||
|
|
@ -201,6 +207,13 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
|
|||
onChanged: (v) => set(() => conflict = v!),
|
||||
),
|
||||
]),
|
||||
// 관리자 판단: 덮어쓴 파일은 휴지통에 남지 않는다 - 되돌릴 수 없다고 알린다 (받는 쪽이 더 새것이면 설정대로 그대로 둠)
|
||||
if (conflict == NameConflict.overwrite)
|
||||
Text(
|
||||
c.settings.copySkipNewer
|
||||
? tr('덮어쓴 파일은 되돌릴 수 없습니다 (받는 쪽이 더 새 파일은 그대로 둡니다).')
|
||||
: tr('덮어쓴 파일은 되돌릴 수 없습니다.'),
|
||||
style: const TextStyle(color: Colors.orangeAccent, fontSize: 12)),
|
||||
// 관리자 판단: 풀린 크기가 빈 공간보다 크면 풀기 전에 알리고 묻는다 (압축 폭탄 · 공간 부족)
|
||||
if (short)
|
||||
Text(trf('풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?', [formatSize(plan.totalSize), formatSize(f)]),
|
||||
|
|
@ -229,16 +242,23 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
|
|||
final r = await _withProgress(context, trf('압축 푸는 중 · {0}', [vBasename(path)]), (set, cancelled) async {
|
||||
if (kind == ArchiveKind.zip) {
|
||||
return extractZipPlan(local, dest, plan,
|
||||
conflict: conflict, password: pw, cancelled: cancelled, onProgress: (d, t) => set(t == 0 ? null : d / t));
|
||||
conflict: conflict,
|
||||
skipNewer: c.settings.copySkipNewer,
|
||||
password: pw,
|
||||
cancelled: cancelled,
|
||||
onProgress: (d, t) => set(t == 0 ? null : d / t));
|
||||
}
|
||||
return extract7zPlan(exe!, local, dest, plan, conflict: conflict, password: pw, cancelled: cancelled, onProgress: set);
|
||||
return extract7zPlan(exe!, local, dest, plan,
|
||||
conflict: conflict, skipNewer: c.settings.copySkipNewer, password: pw, cancelled: cancelled, onProgress: set);
|
||||
});
|
||||
say([
|
||||
trf('{0}개를 풀었습니다: {1}', [r.done, vDisplay(dest)]),
|
||||
if (r.skippedExisting.isNotEmpty) trf('같은 이름이라 건너뜀 {0}개', [r.skippedExisting.length]),
|
||||
if (r.renamed.isNotEmpty) trf('이름을 바꿔 풂 {0}개', [r.renamed.length]),
|
||||
if (r.keptNewer.isNotEmpty) trf('{0}개는 더 새 파일이라 그대로 둠', [r.keptNewer.length]),
|
||||
if (plan.unsafe.isNotEmpty) trf('막은 경로 {0}개', [plan.unsafe.length]),
|
||||
if (plan.links.isNotEmpty) trf('건너뛴 링크 {0}개', [plan.links.length]),
|
||||
if (plan.badNames.isNotEmpty) trf('풀 수 없는 이름 {0}개', [plan.badNames.length]),
|
||||
].join(' · '));
|
||||
return dest;
|
||||
} on ArchivePasswordException catch (e) {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,18 @@ void main() {
|
|||
expect(plan.encrypted, isTrue);
|
||||
});
|
||||
|
||||
test('86 보충: Windows 에서 쓸 수 없는 이름 (CON · ? · 끝 점) 은 미리 걸러 건너뛴다 (다른 것은 그대로)', () {
|
||||
final plan = planExtract([
|
||||
const ArchiveEntry('ok/a.txt'),
|
||||
const ArchiveEntry('con.txt'),
|
||||
const ArchiveEntry('dir/what?.txt'),
|
||||
const ArchiveEntry('end./x.txt'),
|
||||
], windowsRules: true);
|
||||
expect(plan.files.map((e) => e.name), ['ok/a.txt']);
|
||||
expect(plan.badNames.map((x) => x.$1), ['con.txt', 'dir/what?.txt', 'end./x.txt']);
|
||||
expect(planExtract([const ArchiveEntry('con.txt')], windowsRules: false).files, hasLength(1), reason: 'Android 는 지금처럼');
|
||||
});
|
||||
|
||||
group('ZIP', () {
|
||||
test('만들기 (폴더 · 파일, 링크는 넣지 않음) → 목록 → 풀기 (같은 이름은 고른 대로)', () async {
|
||||
final src = Directory(p.join(tmp.path, 'src'))..createSync();
|
||||
|
|
@ -73,6 +85,23 @@ void main() {
|
|||
expect(File(p.join(dest.path, 'one.txt')).readAsStringSync(), '1');
|
||||
});
|
||||
|
||||
test('86 보충: 덮어쓰기에도 받는 쪽이 더 새 파일은 그대로 (229 와 같게)', () async {
|
||||
final a = Archive()
|
||||
..addFile(ArchiveFile.string('old.txt', 'from zip')..lastModTime = DateTime(2020, 1, 1).millisecondsSinceEpoch ~/ 1000)
|
||||
..addFile(ArchiveFile.string('new.txt', 'from zip')..lastModTime = DateTime(2020, 1, 1).millisecondsSinceEpoch ~/ 1000);
|
||||
final zip = p.join(tmp.path, 'n.zip');
|
||||
File(zip).writeAsBytesSync(ZipEncoder().encode(a));
|
||||
final dest = Directory(p.join(tmp.path, 'd'))..createSync();
|
||||
final older = File(p.join(dest.path, 'old.txt'))..writeAsStringSync('mine');
|
||||
older.setLastModifiedSync(DateTime(2019, 1, 1));
|
||||
File(p.join(dest.path, 'new.txt')).writeAsStringSync('mine'); // 지금 = 더 새것
|
||||
final plan = planExtract(await listZip(zip));
|
||||
final r = await extractZipPlan(zip, dest.path, plan, conflict: NameConflict.overwrite, skipNewer: true);
|
||||
expect(r.keptNewer, ['new.txt']);
|
||||
expect(File(p.join(dest.path, 'new.txt')).readAsStringSync(), 'mine');
|
||||
expect(File(p.join(dest.path, 'old.txt')).readAsStringSync(), 'from zip');
|
||||
});
|
||||
|
||||
test('zip slip: ../ · 드라이브 경로 · 링크 항목은 대상 밖에 쓰지 않고 알린다', () async {
|
||||
final a = Archive()
|
||||
..addFile(ArchiveFile.string('ok.txt', 'ok'))
|
||||
|
|
@ -149,6 +178,13 @@ void main() {
|
|||
expect(File(p.join(dest, 'src', 'a_1.txt')).existsSync() || File(p.join(dest, 'src', 'a (2).txt')).existsSync() ||
|
||||
Directory(p.join(dest, 'src')).listSync().length > 3, isTrue);
|
||||
|
||||
// 86 보충: 덮어쓰기에도 받는 쪽이 더 새 파일은 그대로 (7z)
|
||||
final mine = File(p.join(dest, 'src', 'a.txt'))..writeAsStringSync('mine');
|
||||
mine.setLastModifiedSync(DateTime.now().add(const Duration(days: 1)));
|
||||
final keep = await extract7zPlan(exe, plain, dest, planExtract(list), conflict: NameConflict.overwrite, skipNewer: true);
|
||||
expect(keep.keptNewer, ['src/a.txt']);
|
||||
expect(mine.readAsStringSync(), 'mine');
|
||||
|
||||
await expectLater(list7z(exe, enc), throwsA(isA<ArchivePasswordException>()));
|
||||
await expectLater(list7z(exe, enc, password: 'nope'), throwsA(isA<ArchivePasswordException>().having((e) => e.wrong, 'wrong', isTrue)));
|
||||
final encList = await list7z(exe, enc, password: 'pw2');
|
||||
|
|
|
|||
|
|
@ -34,6 +34,13 @@ if (-not ($list -contains "$name/jj_mkvmaker.exe")) {
|
|||
Remove-Item $Zip -Force
|
||||
throw "zip 맨 위에 $name/jj_mkvmaker.exe 가 없습니다"
|
||||
}
|
||||
# 86 (관리자 판단): 7-Zip 이 빠진 채 정식 판이 나가지 않게 (CMake 는 없으면 경고만 하고 빼고 빌드한다)
|
||||
$need = @("$name/Lib/7zip/7z.exe", "$name/Lib/7zip/7z.dll", "$name/Lib/7zip/License.txt")
|
||||
$missing = $need | Where-Object { -not ($list -contains $_) }
|
||||
if ($missing) {
|
||||
Remove-Item $Zip -Force
|
||||
throw ("7-Zip 이 빠졌습니다 (tool\fetch_7zip.ps1 로 받은 뒤 다시 빌드):`n" + ($missing -join "`n"))
|
||||
}
|
||||
$item = Get-Item $Zip
|
||||
$hash = (Get-FileHash $Zip -Algorithm SHA256).Hash.ToLower()
|
||||
"{0} {1} bytes {2} files sha256 {3}" -f $item.Name, $item.Length, $list.Count, $hash
|
||||
|
|
|
|||
|
|
@ -150,6 +150,9 @@ file(GLOB JJ_7ZIP_FILES "${JJ_7ZIP_DIR}/7z.exe" "${JJ_7ZIP_DIR}/7z.dll" "${JJ_7Z
|
|||
if(JJ_7ZIP_FILES)
|
||||
install(FILES ${JJ_7ZIP_FILES}
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/7zip" COMPONENT Runtime)
|
||||
else()
|
||||
# 개발 빌드는 그대로 (7z · RAR 풀기만 빠짐). 정식 판은 make_zip.ps1 검사가 막는다
|
||||
message(WARNING "7-Zip 이 없습니다 (third_party/7zip/windows) - 7z · RAR 풀기가 빠집니다. tool\\fetch_7zip.ps1 로 받으세요.")
|
||||
endif()
|
||||
install(FILES "${JJ_LICENSES}/LGPL-2.1.txt" DESTINATION "${CMAKE_INSTALL_PREFIX}/7zip" COMPONENT Runtime OPTIONAL)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue