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
RyuJongJin 2026-10-17 14:47:39 +09:00
parent 219510fec0
commit c440b688ea
8 changed files with 187 additions and 10 deletions

View File

@ -321,6 +321,7 @@
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} failed: {1} {2}{3}", "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 등. 서버 목록과 아이디는 남깁니다": "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 · 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 서버 \"{0}\" 을(를) 지울까요?": "Delete WebDAV server \"{0}\"?",
"WebDAV 서버 고치기": "Edit WebDAV server", "WebDAV 서버 고치기": "Edit WebDAV server",
"WebDAV 서버 목록과 그 저장된 비밀번호도 지워집니다. 동영상 목록 · 즐겨찾기 · 파일은 그대로입니다": "The WebDAV server list and its saved passwords are deleted too. The video list, bookmarks and files are kept", "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} (change at the top of the main screen)",
"{0} · {1}배 · {2}": "{0} · {1}× · {2}", "{0} · {1}배 · {2}": "{0} · {1}× · {2}",
"{0} · 링크 {1}개 제외": "{0} · {1} links excluded", "{0} · 링크 {1}개 제외": "{0} · {1} links excluded",
"{0} · 파일 {1}개": "{0} · {1} files",
"{0} → {1} 번역 {2}%": "{0} → {1} translation {2}%", "{0} → {1} 번역 {2}%": "{0} → {1} translation {2}%",
"{0} 는 이미 끝났습니다": "{0} has already finished", "{0} 는 이미 끝났습니다": "{0} has already finished",
"{0} 라이선스": "{0} license", "{0} 라이선스": "{0} license",
@ -510,6 +512,7 @@
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "Delete {0} items? This cannot be undone.", "{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "Delete {0} items? This cannot be undone.",
"{0}개 항목을 휴지통으로 보낼까요?": "Move {0} items to the Recycle Bin?", "{0}개 항목을 휴지통으로 보낼까요?": "Move {0} items to the Recycle Bin?",
"{0}개 항목을 휴지통으로 보냈습니다.": "Moved {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} left in the source (skipped)",
"{0}개는 지우지 않았습니다 (취소).": "{0} were not deleted (cancelled).", "{0}개는 지우지 않았습니다 (취소).": "{0} were not deleted (cancelled).",
"{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} were too large for the Recycle Bin and were deleted permanently (as Windows asked).", "{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)", "덮어쓰기 (원래 파일은 없어집니다)": "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 그대로)": "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", "덮어쓰기여도 다 만든 뒤에만 바꿉니다. 도중에 취소 · 실패하면 예전 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", "덮어씁니다": "overwrites it",
"덴마크어": "Danish", "덴마크어": "Danish",
"독일어": "German", "독일어": "German",
@ -1004,6 +1009,7 @@
"링크 {0} (건너뜀)": "Links {0} (skipped)", "링크 {0} (건너뜀)": "Links {0} (skipped)",
"링크 {0}개는 넣지 않음": "{0} links not included", "링크 {0}개는 넣지 않음": "{0} links not included",
"링크 {0}개는 만들지 않고 건너뜁니다: {1}": "{0} links will be skipped (not created): {1}", "링크 {0}개는 만들지 않고 건너뜁니다: {1}": "{0} links will be skipped (not created): {1}",
"링크 {0}개는 세지 않음": "{0} links not counted",
"링크 · junction 은 넣지 않습니다": "Links and junctions are not included", "링크 · junction 은 넣지 않습니다": "Links and junctions are not included",
"마스터 · 최상 비밀번호": "Master and super passwords", "마스터 · 최상 비밀번호": "Master and super passwords",
"마스터 비밀번호": "Master password", "마스터 비밀번호": "Master password",
@ -1270,8 +1276,10 @@
"복사했습니다.": "copied.", "복사했습니다.": "copied.",
"볼 그림이 없습니다.": "No images to show.", "볼 그림이 없습니다.": "No images to show.",
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "This video is unavailable (deleted · region restricted).", "볼 수 없는 영상입니다 (삭제 · 지역 제한).": "This video is unavailable (deleted · region restricted).",
"분석": "Analyze",
"분석 실패: {0} - {1}": "Analysis failed: {0} - {1}", "분석 실패: {0} - {1}": "Analysis failed: {0} - {1}",
"분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "Re-analyzed {0} videos that had failed", "분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "Re-analyzed {0} videos that had failed",
"분석하지 못했습니다: {0}": "Couldn't analyze: {0}",
"붙여넣기": "Paste", "붙여넣기": "Paste",
"붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "Nothing to paste. First hold items with [Copy] · [Move].", "붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "Nothing to paste. First hold items with [Copy] · [Move].",
"브라우저": "Browser", "브라우저": "Browser",
@ -1505,6 +1513,7 @@
"아직 지원하지 않는 플랫폼입니다: {0}": "This platform is not supported yet: {0}", "아직 지원하지 않는 플랫폼입니다: {0}": "This platform is not supported yet: {0}",
"아직 허용되지 않았습니다": "Not allowed yet", "아직 허용되지 않았습니다": "Not allowed yet",
"안내 닫기": "Close tip", "안내 닫기": "Close tip",
"안에 폴더가 없습니다": "No folders inside",
"안쪽 파일을 확인하는 중 …": "Checking files 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 - 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", "안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "{0} files inside are newer on the receiving side - merging overwrites them with older ones",
@ -1893,12 +1902,14 @@
"일본어": "Japanese", "일본어": "Japanese",
"일시정지": "Pause", "일시정지": "Pause",
"읽기 전용 매체입니다": "Read-only media", "읽기 전용 매체입니다": "Read-only media",
"읽는 중 · 폴더 {0}개 · {1}": "Reading · {0} folders · {1}",
"읽을 수 없습니다": "Cannot read", "읽을 수 없습니다": "Cannot read",
"읽을 수 없습니다: {0}": "Cannot read: {0}", "읽을 수 없습니다: {0}": "Cannot read: {0}",
"읽을 수 없어 건너뛴 폴더: {0}": "Skipped folders that could not be read: {0}", "읽을 수 없어 건너뛴 폴더: {0}": "Skipped folders that could not be read: {0}",
"읽을 수 없음": "Cannot read", "읽을 수 없음": "Cannot read",
"읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "The original file that could not be read was not deleted; it is kept here:", "읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "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} 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", "임시 파일 · 남은 조각 정리": "Clean temporary files · leftovers",
"자동": "Auto", "자동": "Auto",
"자동 (가로로 긴 그림만)": "Auto (wide images only)", "자동 (가로로 긴 그림만)": "Auto (wide images only)",
@ -1986,6 +1997,7 @@
"저장": "Save", "저장": "Save",
"저장 (MKV 반영)": "Save (apply to MKV)", "저장 (MKV 반영)": "Save (apply to MKV)",
"저장 (MKV 반영) *": "Save (apply to MKV) *", "저장 (MKV 반영) *": "Save (apply to MKV) *",
"저장 공간 분석": "Storage analysis",
"저장 공간 정리": "Storage cleanup", "저장 공간 정리": "Storage cleanup",
"저장 공간 정리: 파일 {0}개 · {1}": "Storage cleanup: {0} files · {1}", "저장 공간 정리: 파일 {0}개 · {1}": "Storage cleanup: {0} files · {1}",
"저장 공간이 모자랍니다": "Not enough storage space", "저장 공간이 모자랍니다": "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.", "크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 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)", "크기순 정렬일 때만 (기본)": "Only when sorting by size (default)",
"크메르어": "Khmer", "크메르어": "Khmer",
"큰 파일": "Large files",
"큰 파일이 없습니다": "No large files",
"큰 폴더": "Large folders",
"키 있음": "Key saved", "키 있음": "Key saved",
"타갈로그어": "Tagalog", "타갈로그어": "Tagalog",
"타밀어": "Tamil", "타밀어": "Tamil",
@ -2290,6 +2305,7 @@
"트레이의 JJ_MKVMaker 아이콘을 누르면 열립니다.": "Click the JJ_MKVMaker icon in the tray to open it.", "트레이의 JJ_MKVMaker 아이콘을 누르면 열립니다.": "Click the JJ_MKVMaker icon in the tray to open it.",
"파일": "File", "파일": "File",
"파일 {0}/{1}": "Files {0}/{1}", "파일 {0}/{1}": "Files {0}/{1}",
"파일 {0}개": "{0} files",
"파일 {0}개 · {1}": "{0} files · {1}", "파일 {0}개 · {1}": "{0} files · {1}",
"파일 관리자에서 보기": "Show in file manager", "파일 관리자에서 보기": "Show in file manager",
"파일 복사 · 이동 방법": "File copy · move method", "파일 복사 · 이동 방법": "File copy · move method",
@ -2382,6 +2398,8 @@
"표준": "Standard", "표준": "Standard",
"푸는 중": "Extracting", "푸는 중": "Extracting",
"푸는 중: {0}": "Extracting: {0}", "푸는 중: {0}": "Extracting: {0}",
"풀 수 없는 이름 {0}개": "{0} invalid names",
"풀 수 없는 이름 {0}개 (건너뜀): {1}": "{0} names can't be used on this device (skipped): {1}",
"풀기": "Extract", "풀기": "Extract",
"풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?": "The extracted size ({0}) is larger than the free space ({1}). Extract anyway?", "풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?": "The extracted size ({0}) is larger than the free space ({1}). Extract anyway?",
"풀지 못했습니다: {0}": "Could not extract: {0}", "풀지 못했습니다: {0}": "Could not extract: {0}",

View File

@ -321,6 +321,7 @@
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失敗: {1} {2}{3}", "WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失敗: {1} {2}{3}",
"WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV・OpenSubtitles など。サーバー一覧と ID は残します", "WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV・OpenSubtitles など。サーバー一覧と ID は残します",
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "WebDAV・OpenSubtitles のパスワードをこの端末の安全な保存領域に保存します。オフにすると保存せず (既存のものも削除)、アプリを起動するたびに入力します", "WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "WebDAV・OpenSubtitles のパスワードをこの端末の安全な保存領域に保存します。オフにすると保存せず (既存のものも削除)、アプリを起動するたびに入力します",
"WebDAV 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV はサーバーから中身をすべて読むため、時間がかかることがあります。分析しますか?",
"WebDAV 서버 \"{0}\" 을(를) 지울까요?": "WebDAV サーバー「{0}」を削除しますか?", "WebDAV 서버 \"{0}\" 을(를) 지울까요?": "WebDAV サーバー「{0}」を削除しますか?",
"WebDAV 서버 고치기": "WebDAV サーバーを編集", "WebDAV 서버 고치기": "WebDAV サーバーを編集",
"WebDAV 서버 목록과 그 저장된 비밀번호도 지워집니다. 동영상 목록 · 즐겨찾기 · 파일은 그대로입니다": "WebDAV サーバー一覧とその保存したパスワードも消えます。動画リスト・お気に入り・ファイルはそのままです", "WebDAV 서버 목록과 그 저장된 비밀번호도 지워집니다. 동영상 목록 · 즐겨찾기 · 파일은 그대로입니다": "WebDAV サーバー一覧とその保存したパスワードも消えます。動画リスト・お気に入り・ファイルはそのままです",
@ -435,6 +436,7 @@
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)",
"{0} · {1}배 · {2}": "{0}・{1} 倍・{2}", "{0} · {1}배 · {2}": "{0}・{1} 倍・{2}",
"{0} · 링크 {1}개 제외": "{0} · リンク {1} 件を除く", "{0} · 링크 {1}개 제외": "{0} · リンク {1} 件を除く",
"{0} · 파일 {1}개": "{0} · ファイル {1} 件",
"{0} → {1} 번역 {2}%": "{0} → {1} 翻訳 {2}%", "{0} → {1} 번역 {2}%": "{0} → {1} 翻訳 {2}%",
"{0} 는 이미 끝났습니다": "{0} はすでに終わりました", "{0} 는 이미 끝났습니다": "{0} はすでに終わりました",
"{0} 라이선스": "{0} ライセンス", "{0} 라이선스": "{0} ライセンス",
@ -510,6 +512,7 @@
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "{0} 件の項目を削除しますか? 元に戻せません。", "{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "{0} 件の項目を削除しますか? 元に戻せません。",
"{0}개 항목을 휴지통으로 보낼까요?": "{0} 個の項目をごみ箱に移動しますか?", "{0}개 항목을 휴지통으로 보낼까요?": "{0} 個の項目をごみ箱に移動しますか?",
"{0}개 항목을 휴지통으로 보냈습니다.": "{0} 個の項目をごみ箱に移動しました。", "{0}개 항목을 휴지통으로 보냈습니다.": "{0} 個の項目をごみ箱に移動しました。",
"{0}개는 더 새 파일이라 그대로 둠": "{0} 件は新しいファイルなのでそのまま",
"{0}개는 원본에 남음 (건너뜀)": "{0} 件は元の場所に残っています (スキップ)", "{0}개는 원본에 남음 (건너뜀)": "{0} 件は元の場所に残っています (スキップ)",
"{0}개는 지우지 않았습니다 (취소).": "{0} 個は削除しませんでした (キャンセル)。", "{0}개는 지우지 않았습니다 (취소).": "{0} 個は削除しませんでした (キャンセル)。",
"{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} 個はごみ箱より大きいため完全に削除しました (Windows の確認どおり)。", "{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} 個はごみ箱より大きいため完全に削除しました (Windows の確認どおり)。",
@ -924,6 +927,8 @@
"덮어쓰기 (원래 파일은 없어집니다)": "上書き (元のファイルは消えます)", "덮어쓰기 (원래 파일은 없어집니다)": "上書き (元のファイルは消えます)",
"덮어쓰기여도 다 만든 뒤에만 바꿉니다 (도중에 취소 · 실패하면 예전 MKV 그대로)": "上書きでも作り終えてから置き換えます (途中でキャンセル · 失敗すると以前の MKV はそのまま)", "덮어쓰기여도 다 만든 뒤에만 바꿉니다 (도중에 취소 · 실패하면 예전 MKV 그대로)": "上書きでも作り終えてから置き換えます (途中でキャンセル · 失敗すると以前の MKV はそのまま)",
"덮어쓰기여도 다 만든 뒤에만 바꿉니다. 도중에 취소 · 실패하면 예전 MKV 는 그대로 남습니다. 늘 같게 하려면 환경 설정 > MKV 만들기 > 만들 MKV 가 이미 있으면": "上書きでも作り終えてから置き換えます。途中でキャンセル · 失敗すると以前の MKV はそのまま残ります。いつも同じにするには: 環境設定 > MKV 作成 > 作る MKV がすでにあるとき", "덮어쓰기여도 다 만든 뒤에만 바꿉니다. 도중에 취소 · 실패하면 예전 MKV 는 그대로 남습니다. 늘 같게 하려면 환경 설정 > MKV 만들기 > 만들 MKV 가 이미 있으면": "上書きでも作り終えてから置き換えます。途中でキャンセル · 失敗すると以前の MKV はそのまま残ります。いつも同じにするには: 環境設定 > MKV 作成 > 作る MKV がすでにあるとき",
"덮어쓴 파일은 되돌릴 수 없습니다 (받는 쪽이 더 새 파일은 그대로 둡니다).": "上書きしたファイルは元に戻せません (受け取る側が新しいファイルはそのまま残します)。",
"덮어쓴 파일은 되돌릴 수 없습니다.": "上書きしたファイルは元に戻せません。",
"덮어씁니다": "上書きします", "덮어씁니다": "上書きします",
"덴마크어": "デンマーク語", "덴마크어": "デンマーク語",
"독일어": "ドイツ語", "독일어": "ドイツ語",
@ -1004,6 +1009,7 @@
"링크 {0} (건너뜀)": "リンク {0} (スキップ)", "링크 {0} (건너뜀)": "リンク {0} (スキップ)",
"링크 {0}개는 넣지 않음": "リンク {0} 件は含めず", "링크 {0}개는 넣지 않음": "リンク {0} 件は含めず",
"링크 {0}개는 만들지 않고 건너뜁니다: {1}": "リンク {0} 件は作成せずスキップします: {1}", "링크 {0}개는 만들지 않고 건너뜁니다: {1}": "リンク {0} 件は作成せずスキップします: {1}",
"링크 {0}개는 세지 않음": "リンク {0} 件は数えず",
"링크 · junction 은 넣지 않습니다": "リンク・ジャンクションは含めません", "링크 · junction 은 넣지 않습니다": "リンク・ジャンクションは含めません",
"마스터 · 최상 비밀번호": "マスター / 最上位パスワード", "마스터 · 최상 비밀번호": "マスター / 最上位パスワード",
"마스터 비밀번호": "マスターパスワード", "마스터 비밀번호": "マスターパスワード",
@ -1270,8 +1276,10 @@
"복사했습니다.": "コピーしました。", "복사했습니다.": "コピーしました。",
"볼 그림이 없습니다.": "表示する画像がありません。", "볼 그림이 없습니다.": "表示する画像がありません。",
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "視聴できない動画です(削除・地域制限)。", "볼 수 없는 영상입니다 (삭제 · 지역 제한).": "視聴できない動画です(削除・地域制限)。",
"분석": "分析",
"분석 실패: {0} - {1}": "分析失敗: {0} - {1}", "분석 실패: {0} - {1}": "分析失敗: {0} - {1}",
"분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "分析に失敗していた動画 {0} 件を再分析しました", "분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "分析に失敗していた動画 {0} 件を再分析しました",
"분석하지 못했습니다: {0}": "分析できませんでした: {0}",
"붙여넣기": "貼り付け", "붙여넣기": "貼り付け",
"붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "貼り付ける項目がありません。先に [コピー] · [移動] で保持してください。", "붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "貼り付ける項目がありません。先に [コピー] · [移動] で保持してください。",
"브라우저": "ブラウザー", "브라우저": "ブラウザー",
@ -1505,6 +1513,7 @@
"아직 지원하지 않는 플랫폼입니다: {0}": "まだ対応していないプラットフォームです: {0}", "아직 지원하지 않는 플랫폼입니다: {0}": "まだ対応していないプラットフォームです: {0}",
"아직 허용되지 않았습니다": "まだ許可されていません", "아직 허용되지 않았습니다": "まだ許可されていません",
"안내 닫기": "案内を閉じる", "안내 닫기": "案内を閉じる",
"안에 폴더가 없습니다": "中にフォルダーがありません",
"안쪽 파일을 확인하는 중 …": "中のファイルを確認中 …", "안쪽 파일을 확인하는 중 …": "中のファイルを確認中 …",
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "中に受け取る側の方が新しいファイルが {0} 件 - 合わせてもスキップします (元に残ります)", "안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "中に受け取る側の方が新しいファイルが {0} 件 - 合わせてもスキップします (元に残ります)",
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "中に受け取る側の方が新しいファイルが {0} 件 - 合わせると古いもので上書きします", "안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "中に受け取る側の方が新しいファイルが {0} 件 - 合わせると古いもので上書きします",
@ -1893,12 +1902,14 @@
"일본어": "日本語", "일본어": "日本語",
"일시정지": "一時停止", "일시정지": "一時停止",
"읽기 전용 매체입니다": "読み取り専用メディアです", "읽기 전용 매체입니다": "読み取り専用メディアです",
"읽는 중 · 폴더 {0}개 · {1}": "読み込み中 · フォルダー {0} 件 · {1}",
"읽을 수 없습니다": "読み込めません", "읽을 수 없습니다": "読み込めません",
"읽을 수 없습니다: {0}": "読み込めません: {0}", "읽을 수 없습니다: {0}": "読み込めません: {0}",
"읽을 수 없어 건너뛴 폴더: {0}": "読み込めずスキップしたフォルダー: {0}", "읽을 수 없어 건너뛴 폴더: {0}": "読み込めずスキップしたフォルダー: {0}",
"읽을 수 없음": "読み取れません", "읽을 수 없음": "読み取れません",
"읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "読めなかった元のファイルは削除せず、ここに保管しました:", "읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "読めなかった元のファイルは削除せず、ここに保管しました:",
"읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "読めなかったフォルダー {0} 個 (権限・接続の問題) - その中は探せませんでした", "읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "読めなかったフォルダー {0} 個 (権限・接続の問題) - その中は探せませんでした",
"읽지 못한 폴더 {0}개 (크기가 실제보다 작을 수 있음)": "読めなかったフォルダー {0} 件 (サイズが実際より小さいことがあります)",
"임시 파일 · 남은 조각 정리": "一時ファイル・残った断片を整理", "임시 파일 · 남은 조각 정리": "一時ファイル・残った断片を整理",
"자동": "自動", "자동": "自動",
"자동 (가로로 긴 그림만)": "自動 (横長の画像のみ)", "자동 (가로로 긴 그림만)": "自動 (横長の画像のみ)",
@ -1986,6 +1997,7 @@
"저장": "保存", "저장": "保存",
"저장 (MKV 반영)": "保存(MKV に反映)", "저장 (MKV 반영)": "保存(MKV に反映)",
"저장 (MKV 반영) *": "保存(MKV に反映)*", "저장 (MKV 반영) *": "保存(MKV に反映)*",
"저장 공간 분석": "ストレージ分析",
"저장 공간 정리": "ストレージの整理", "저장 공간 정리": "ストレージの整理",
"저장 공간 정리: 파일 {0}개 · {1}": "ストレージの整理: ファイル {0}個・{1}", "저장 공간 정리: 파일 {0}개 · {1}": "ストレージの整理: ファイル {0}個・{1}",
"저장 공간이 모자랍니다": "保存容量が足りません", "저장 공간이 모자랍니다": "保存容量が足りません",
@ -2269,6 +2281,9 @@
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "サイズを変えると再エンコードが必要なため、コーデックを「元のまま」から H.264 に変更しました。", "크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "サイズを変えると再エンコードが必要なため、コーデックを「元のまま」から H.264 に変更しました。",
"크기순 정렬일 때만 (기본)": "サイズ順のときだけ (既定)", "크기순 정렬일 때만 (기본)": "サイズ順のときだけ (既定)",
"크메르어": "クメール語", "크메르어": "クメール語",
"큰 파일": "大きいファイル",
"큰 파일이 없습니다": "大きいファイルはありません",
"큰 폴더": "大きいフォルダー",
"키 있음": "キーあり", "키 있음": "キーあり",
"타갈로그어": "タガログ語", "타갈로그어": "タガログ語",
"타밀어": "タミル語", "타밀어": "タミル語",
@ -2290,6 +2305,7 @@
"트레이의 JJ_MKVMaker 아이콘을 누르면 열립니다.": "トレイの JJ_MKVMaker アイコンをクリックすると開きます。", "트레이의 JJ_MKVMaker 아이콘을 누르면 열립니다.": "トレイの JJ_MKVMaker アイコンをクリックすると開きます。",
"파일": "ファイル", "파일": "ファイル",
"파일 {0}/{1}": "ファイル {0}/{1}", "파일 {0}/{1}": "ファイル {0}/{1}",
"파일 {0}개": "ファイル {0} 件",
"파일 {0}개 · {1}": "ファイル {0} 件 · {1}", "파일 {0}개 · {1}": "ファイル {0} 件 · {1}",
"파일 관리자에서 보기": "ファイルマネージャーで表示", "파일 관리자에서 보기": "ファイルマネージャーで表示",
"파일 복사 · 이동 방법": "ファイルのコピー · 移動方法", "파일 복사 · 이동 방법": "ファイルのコピー · 移動方法",
@ -2382,6 +2398,8 @@
"표준": "標準", "표준": "標準",
"푸는 중": "展開中", "푸는 중": "展開中",
"푸는 중: {0}": "展開中: {0}", "푸는 중: {0}": "展開中: {0}",
"풀 수 없는 이름 {0}개": "展開できない名前 {0} 件",
"풀 수 없는 이름 {0}개 (건너뜀): {1}": "展開できない名前 {0} 件 (スキップ): {1}",
"풀기": "展開", "풀기": "展開",
"풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?": "展開後のサイズ ({0}) が空き容量 ({1}) より大きいです。それでも展開しますか?", "풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?": "展開後のサイズ ({0}) が空き容量 ({1}) より大きいです。それでも展開しますか?",
"풀지 못했습니다: {0}": "展開できませんでした: {0}", "풀지 못했습니다: {0}": "展開できませんでした: {0}",

View File

@ -321,6 +321,7 @@
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失败: {1} {2}{3}", "WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失败: {1} {2}{3}",
"WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV · OpenSubtitles 等。保留服务器列表和用户名", "WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV · OpenSubtitles 等。保留服务器列表和用户名",
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "将 WebDAV · OpenSubtitles 密码保存在本设备的安全存储中。关闭后不保存 (已有的也会删除),每次启动应用时重新输入", "WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "将 WebDAV · OpenSubtitles 密码保存在本设备的安全存储中。关闭后不保存 (已有的也会删除),每次启动应用时重新输入",
"WebDAV 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV 需要从服务器读取全部内容,可能需要较长时间。要分析吗?",
"WebDAV 서버 \"{0}\" 을(를) 지울까요?": "删除 WebDAV 服务器“{0}”吗?", "WebDAV 서버 \"{0}\" 을(를) 지울까요?": "删除 WebDAV 服务器“{0}”吗?",
"WebDAV 서버 고치기": "编辑 WebDAV 服务器", "WebDAV 서버 고치기": "编辑 WebDAV 服务器",
"WebDAV 서버 목록과 그 저장된 비밀번호도 지워집니다. 동영상 목록 · 즐겨찾기 · 파일은 그대로입니다": "WebDAV 服务器列表及其保存的密码也会被删除。视频列表 · 收藏 · 文件保持不变", "WebDAV 서버 목록과 그 저장된 비밀번호도 지워집니다. 동영상 목록 · 즐겨찾기 · 파일은 그대로입니다": "WebDAV 服务器列表及其保存的密码也会被删除。视频列表 · 收藏 · 文件保持不变",
@ -435,6 +436,7 @@
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)",
"{0} · {1}배 · {2}": "{0} · {1} 倍 · {2}", "{0} · {1}배 · {2}": "{0} · {1} 倍 · {2}",
"{0} · 링크 {1}개 제외": "{0} · 已排除 {1} 个链接", "{0} · 링크 {1}개 제외": "{0} · 已排除 {1} 个链接",
"{0} · 파일 {1}개": "{0} · {1} 个文件",
"{0} → {1} 번역 {2}%": "{0} → {1} 翻译 {2}%", "{0} → {1} 번역 {2}%": "{0} → {1} 翻译 {2}%",
"{0} 는 이미 끝났습니다": "{0} 已经完成", "{0} 는 이미 끝났습니다": "{0} 已经完成",
"{0} 라이선스": "{0} 许可证", "{0} 라이선스": "{0} 许可证",
@ -510,6 +512,7 @@
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "删除 {0} 个项目吗? 此操作无法撤销。", "{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "删除 {0} 个项目吗? 此操作无法撤销。",
"{0}개 항목을 휴지통으로 보낼까요?": "将 {0} 个项目移到回收站吗?", "{0}개 항목을 휴지통으로 보낼까요?": "将 {0} 个项目移到回收站吗?",
"{0}개 항목을 휴지통으로 보냈습니다.": "已将 {0} 个项目移到回收站。", "{0}개 항목을 휴지통으로 보냈습니다.": "已将 {0} 个项目移到回收站。",
"{0}개는 더 새 파일이라 그대로 둠": "{0} 个因现有文件更新而保留",
"{0}개는 원본에 남음 (건너뜀)": "{0} 个留在源中(已跳过)", "{0}개는 원본에 남음 (건너뜀)": "{0} 个留在源中(已跳过)",
"{0}개는 지우지 않았습니다 (취소).": "{0} 个未删除 (已取消)。", "{0}개는 지우지 않았습니다 (취소).": "{0} 个未删除 (已取消)。",
"{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} 个超出回收站容量,已永久删除 (按 Windows 的提示)。", "{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} 个超出回收站容量,已永久删除 (按 Windows 的提示)。",
@ -924,6 +927,8 @@
"덮어쓰기 (원래 파일은 없어집니다)": "覆盖 (原文件将被替换)", "덮어쓰기 (원래 파일은 없어집니다)": "覆盖 (原文件将被替换)",
"덮어쓰기여도 다 만든 뒤에만 바꿉니다 (도중에 취소 · 실패하면 예전 MKV 그대로)": "即使覆盖,也只在制作完成后才替换(中途取消 · 失败时保留原来的 MKV)", "덮어쓰기여도 다 만든 뒤에만 바꿉니다 (도중에 취소 · 실패하면 예전 MKV 그대로)": "即使覆盖,也只在制作完成后才替换(中途取消 · 失败时保留原来的 MKV)",
"덮어쓰기여도 다 만든 뒤에만 바꿉니다. 도중에 취소 · 실패하면 예전 MKV 는 그대로 남습니다. 늘 같게 하려면 환경 설정 > MKV 만들기 > 만들 MKV 가 이미 있으면": "即使覆盖,也只在制作完成后才替换。中途取消 · 失败时原来的 MKV 会保留。若要始终相同:设置 > 制作 MKV > 要制作的 MKV 已存在时", "덮어쓰기여도 다 만든 뒤에만 바꿉니다. 도중에 취소 · 실패하면 예전 MKV 는 그대로 남습니다. 늘 같게 하려면 환경 설정 > MKV 만들기 > 만들 MKV 가 이미 있으면": "即使覆盖,也只在制作完成后才替换。中途取消 · 失败时原来的 MKV 会保留。若要始终相同:设置 > 制作 MKV > 要制作的 MKV 已存在时",
"덮어쓴 파일은 되돌릴 수 없습니다 (받는 쪽이 더 새 파일은 그대로 둡니다).": "被覆盖的文件无法恢复 (目标处较新的文件会保留)。",
"덮어쓴 파일은 되돌릴 수 없습니다.": "被覆盖的文件无法恢复。",
"덮어씁니다": "覆盖", "덮어씁니다": "覆盖",
"덴마크어": "丹麦语", "덴마크어": "丹麦语",
"독일어": "德语", "독일어": "德语",
@ -1004,6 +1009,7 @@
"링크 {0} (건너뜀)": "链接 {0}(跳过)", "링크 {0} (건너뜀)": "链接 {0}(跳过)",
"링크 {0}개는 넣지 않음": "{0} 个链接未包含", "링크 {0}개는 넣지 않음": "{0} 个链接未包含",
"링크 {0}개는 만들지 않고 건너뜁니다: {1}": "{0} 个链接将跳过 (不创建): {1}", "링크 {0}개는 만들지 않고 건너뜁니다: {1}": "{0} 个链接将跳过 (不创建): {1}",
"링크 {0}개는 세지 않음": "{0} 个链接未计入",
"링크 · junction 은 넣지 않습니다": "不包含链接和 junction", "링크 · junction 은 넣지 않습니다": "不包含链接和 junction",
"마스터 · 최상 비밀번호": "主密码和最高密码", "마스터 · 최상 비밀번호": "主密码和最高密码",
"마스터 비밀번호": "主密码", "마스터 비밀번호": "主密码",
@ -1270,8 +1276,10 @@
"복사했습니다.": "已复制。", "복사했습니다.": "已复制。",
"볼 그림이 없습니다.": "没有可显示的图片。", "볼 그림이 없습니다.": "没有可显示的图片。",
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "该视频无法观看(已删除 · 地区限制)。", "볼 수 없는 영상입니다 (삭제 · 지역 제한).": "该视频无法观看(已删除 · 地区限制)。",
"분석": "分析",
"분석 실패: {0} - {1}": "分析失败: {0} - {1}", "분석 실패: {0} - {1}": "分析失败: {0} - {1}",
"분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "已重新分析之前分析失败的 {0} 个视频", "분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "已重新分析之前分析失败的 {0} 个视频",
"분석하지 못했습니다: {0}": "无法分析: {0}",
"붙여넣기": "粘贴", "붙여넣기": "粘贴",
"붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "没有可粘贴的项目。请先用 [复制] · [移动] 暂存。", "붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "没有可粘贴的项目。请先用 [复制] · [移动] 暂存。",
"브라우저": "浏览器", "브라우저": "浏览器",
@ -1505,6 +1513,7 @@
"아직 지원하지 않는 플랫폼입니다: {0}": "尚不支持此平台: {0}", "아직 지원하지 않는 플랫폼입니다: {0}": "尚不支持此平台: {0}",
"아직 허용되지 않았습니다": "尚未允许", "아직 허용되지 않았습니다": "尚未允许",
"안내 닫기": "关闭提示", "안내 닫기": "关闭提示",
"안에 폴더가 없습니다": "里面没有文件夹",
"안쪽 파일을 확인하는 중 …": "正在检查里面的文件 …", "안쪽 파일을 확인하는 중 …": "正在检查里面的文件 …",
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "里面有 {0} 个文件在接收端较新 - 合并时会跳过(保留在源中)", "안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "里面有 {0} 个文件在接收端较新 - 合并时会跳过(保留在源中)",
"안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "里面有 {0} 个文件在接收端较新 - 合并会用旧文件覆盖", "안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "里面有 {0} 个文件在接收端较新 - 合并会用旧文件覆盖",
@ -1893,12 +1902,14 @@
"일본어": "日语", "일본어": "日语",
"일시정지": "暂停", "일시정지": "暂停",
"읽기 전용 매체입니다": "只读介质", "읽기 전용 매체입니다": "只读介质",
"읽는 중 · 폴더 {0}개 · {1}": "正在读取 · {0} 个文件夹 · {1}",
"읽을 수 없습니다": "无法读取", "읽을 수 없습니다": "无法读取",
"읽을 수 없습니다: {0}": "无法读取: {0}", "읽을 수 없습니다: {0}": "无法读取: {0}",
"읽을 수 없어 건너뛴 폴더: {0}": "无法读取而跳过的文件夹: {0}", "읽을 수 없어 건너뛴 폴더: {0}": "无法读取而跳过的文件夹: {0}",
"읽을 수 없음": "无法读取", "읽을 수 없음": "无法读取",
"읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "无法读取的原文件没有删除,保存在这里:", "읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "无法读取的原文件没有删除,保存在这里:",
"읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "{0} 个无法读取的文件夹 (权限 · 连接问题) - 未在其中查找", "읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "{0} 个无法读取的文件夹 (权限 · 连接问题) - 未在其中查找",
"읽지 못한 폴더 {0}개 (크기가 실제보다 작을 수 있음)": "{0} 个文件夹无法读取 (大小可能小于实际)",
"임시 파일 · 남은 조각 정리": "清理临时文件 · 残留碎片", "임시 파일 · 남은 조각 정리": "清理临时文件 · 残留碎片",
"자동": "自动", "자동": "自动",
"자동 (가로로 긴 그림만)": "自动 (仅横向图片)", "자동 (가로로 긴 그림만)": "自动 (仅横向图片)",
@ -1986,6 +1997,7 @@
"저장": "保存", "저장": "保存",
"저장 (MKV 반영)": "保存(应用到 MKV)", "저장 (MKV 반영)": "保存(应用到 MKV)",
"저장 (MKV 반영) *": "保存(应用到 MKV)*", "저장 (MKV 반영) *": "保存(应用到 MKV)*",
"저장 공간 분석": "存储空间分析",
"저장 공간 정리": "存储空间清理", "저장 공간 정리": "存储空间清理",
"저장 공간 정리: 파일 {0}개 · {1}": "存储空间清理: 文件 {0} 个 · {1}", "저장 공간 정리: 파일 {0}개 · {1}": "存储空间清理: 文件 {0} 个 · {1}",
"저장 공간이 모자랍니다": "存储空间不足", "저장 공간이 모자랍니다": "存储空间不足",
@ -2269,6 +2281,9 @@
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "更改大小需要重新编码,因此编解码器已从“保持原样”改为 H.264。", "크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "更改大小需要重新编码,因此编解码器已从“保持原样”改为 H.264。",
"크기순 정렬일 때만 (기본)": "仅在按大小排序时 (默认)", "크기순 정렬일 때만 (기본)": "仅在按大小排序时 (默认)",
"크메르어": "高棉语", "크메르어": "高棉语",
"큰 파일": "大文件",
"큰 파일이 없습니다": "没有大文件",
"큰 폴더": "大文件夹",
"키 있음": "已有密钥", "키 있음": "已有密钥",
"타갈로그어": "他加禄语", "타갈로그어": "他加禄语",
"타밀어": "泰米尔语", "타밀어": "泰米尔语",
@ -2290,6 +2305,7 @@
"트레이의 JJ_MKVMaker 아이콘을 누르면 열립니다.": "点击托盘中的 JJ_MKVMaker 图标即可打开。", "트레이의 JJ_MKVMaker 아이콘을 누르면 열립니다.": "点击托盘中的 JJ_MKVMaker 图标即可打开。",
"파일": "文件", "파일": "文件",
"파일 {0}/{1}": "文件 {0}/{1}", "파일 {0}/{1}": "文件 {0}/{1}",
"파일 {0}개": "{0} 个文件",
"파일 {0}개 · {1}": "{0} 个文件 · {1}", "파일 {0}개 · {1}": "{0} 个文件 · {1}",
"파일 관리자에서 보기": "在文件管理器中显示", "파일 관리자에서 보기": "在文件管理器中显示",
"파일 복사 · 이동 방법": "文件复制 · 移动方式", "파일 복사 · 이동 방법": "文件复制 · 移动方式",
@ -2382,6 +2398,8 @@
"표준": "标准", "표준": "标准",
"푸는 중": "正在解压", "푸는 중": "正在解压",
"푸는 중: {0}": "正在解压: {0}", "푸는 중: {0}": "正在解压: {0}",
"풀 수 없는 이름 {0}개": "{0} 个无法解压的名称",
"풀 수 없는 이름 {0}개 (건너뜀): {1}": "{0} 个名称无法在本设备使用 (跳过): {1}",
"풀기": "解压", "풀기": "解压",
"풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?": "解压后大小 ({0}) 大于剩余空间 ({1})。仍要解压吗?", "풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?": "解压后大小 ({0}) 大于剩余空间 ({1})。仍要解压吗?",
"풀지 못했습니다: {0}": "无法解压: {0}", "풀지 못했습니다: {0}": "无法解压: {0}",

View File

@ -6,7 +6,7 @@ import 'package:archive/archive_io.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import '../l10n/tr.dart'; import '../l10n/tr.dart';
import 'file_ops.dart' show NameConflict; import 'file_ops.dart' show NameConflict, nameProblem;
/// 86: 압축 종류 (확장자로) /// 86: 압축 종류 (확장자로)
enum ArchiveKind { zip, sevenZip, rar } enum ArchiveKind { zip, sevenZip, rar }
@ -28,7 +28,11 @@ class ArchiveEntry {
/// 7-Zip 에 넘길 원래 이름 (Windows 의 7z 는 '\' 로 보여 준다) /// 7-Zip 에 넘길 원래 이름 (Windows 의 7z 는 '\' 로 보여 준다)
final String raw; 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; final List<String> links;
/// 이 기기에서 쓸 수 없는 이름 (Windows: CON · NUL · ? * " < > | · 끝 점 · 빈칸 …) - (이름, 까닭), 건너뜀
final List<(String, String)> badNames;
/// 풀린 크기 합 (바이트) /// 풀린 크기 합 (바이트)
final int totalSize; final int totalSize;
/// 비밀번호가 걸린 항목이 있음 /// 비밀번호가 걸린 항목이 있음
final bool encrypted; 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] 가 있으면 그 이름 (폴더면 그 안 모두) 만 /// 목록 [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/')); bool wanted(String n) => names.isEmpty || names.any((x) => n == x || n.startsWith(x.endsWith('/') ? x : '$x/'));
final files = <ArchiveEntry>[]; final files = <ArchiveEntry>[];
final unsafe = <(String, String)>[]; final unsafe = <(String, String)>[];
final links = <String>[]; final links = <String>[];
final badNames = <(String, String)>[];
var size = 0; var size = 0;
var enc = false; var enc = false;
for (final e in entries) { for (final e in entries) {
@ -92,11 +102,17 @@ ExtractPlan planExtract(List<ArchiveEntry> entries, {List<String> names = const
continue; continue;
} }
if (e.isDir) 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); files.add(e);
size += e.size; size += e.size;
enc |= e.encrypted; enc |= e.encrypted;
} }
return ExtractPlan(files, unsafe, links, size, enc); return ExtractPlan(files, unsafe, links, size, enc, badNames);
} }
/// 풀 때 [dest] 안의 자리 ([name] 은 [unsafeArchivePath] 를 지난 것) /// 풀 때 [dest] 안의 자리 ([name] 은 [unsafeArchivePath] 를 지난 것)
@ -118,6 +134,25 @@ class ExtractResult {
int done = 0; int done = 0;
final skippedExisting = <String>[]; final skippedExisting = <String>[];
final renamed = <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('\\'), isDir: h.filename.endsWith('/') || h.filename.endsWith('\\'),
// 유닉스 파일 종류 (상위 16비트) 가 심볼릭 링크 (0xA000) // 유닉스 파일 종류 (상위 16비트) 가 심볼릭 링크 (0xA000)
isLink: (h.externalFileAttributes >> 16) & 0xF000 == 0xA000, isLink: (h.externalFileAttributes >> 16) & 0xF000 == 0xA000,
encrypted: h.generalPurposeBitFlag & 0x1 != 0), encrypted: h.generalPurposeBitFlag & 0x1 != 0,
modified: dosDateTime(h.lastModifiedFileDate, h.lastModifiedFileTime)),
]; ];
} finally { } finally {
await input.close(); await input.close();
@ -151,7 +187,11 @@ Future<List<ArchiveEntry>> listZip(String path) async {
/// ZIP 풀기 - [plan] 의 파일만, 같은 이름은 [conflict] 대로 /// ZIP 풀기 - [plan] 의 파일만, 같은 이름은 [conflict] 대로
Future<ExtractResult> extractZipPlan(String path, String dest, ExtractPlan plan, 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(); if (plan.encrypted && (password == null || password.isEmpty)) throw const ArchivePasswordException();
final input = InputFileStream(path); final input = InputFileStream(path);
@ -160,6 +200,7 @@ Future<ExtractResult> extractZipPlan(String path, String dest, ExtractPlan plan,
// verify: CRC 를 맞춰 보아 비밀번호가 틀리면 (풀린 내용이 다르면) 실패로 // verify: CRC 를 맞춰 보아 비밀번호가 틀리면 (풀린 내용이 다르면) 실패로
final a = ZipDecoder().decodeStream(input, password: password, verify: plan.encrypted); final a = ZipDecoder().decodeStream(input, password: password, verify: plan.encrypted);
final want = {for (final e in plan.files) e.name}; 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]; final todo = [for (final f in a.files) if (f.isFile && want.contains(f.name.replaceAll('\\', '/'))) f];
for (final f in todo) { for (final f in todo) {
if (cancelled?.call() ?? false) throw const ArchiveCancelled(); if (cancelled?.call() ?? false) throw const ArchiveCancelled();
@ -173,6 +214,11 @@ Future<ExtractResult> extractZipPlan(String path, String dest, ExtractPlan plan,
target = _freeName(target); target = _freeName(target);
r.renamed.add(f.name); 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); await Directory(p.dirname(target)).create(recursive: true);
final bytes = f.readBytes(); final bytes = f.readBytes();
@ -270,6 +316,7 @@ List<ArchiveEntry> parse7zList(String out) {
entries.add(ArchiveEntry(raw.replaceAll('\\', '/'), entries.add(ArchiveEntry(raw.replaceAll('\\', '/'),
raw: raw, raw: raw,
size: int.tryParse(m['Size'] ?? '') ?? 0, size: int.tryParse(m['Size'] ?? '') ?? 0,
modified: DateTime.tryParse((m['Modified'] ?? '').replaceFirst(' ', 'T')),
isDir: (m['Folder'] ?? '') == '+' || RegExp(r'^[A-Z_]*D').hasMatch(attr), isDir: (m['Folder'] ?? '') == '+' || RegExp(r'^[A-Z_]*D').hasMatch(attr),
isLink: link, isLink: link,
encrypted: (m['Encrypted'] ?? '') == '+')); encrypted: (m['Encrypted'] ?? '') == '+'));
@ -326,8 +373,18 @@ Future<List<ArchiveEntry>> list7z(String exe, String path, {String? password}) a
/// 7z · RAR 풀기 - [plan] 의 파일만 (목록 파일로 넘김, 링크 · 막은 것은 빠짐). 같은 이름은 [conflict] 대로 /// 7z · RAR 풀기 - [plan] 의 파일만 (목록 파일로 넘김, 링크 · 막은 것은 빠짐). 같은 이름은 [conflict] 대로
Future<ExtractResult> extract7zPlan(String exe, String path, String dest, ExtractPlan plan, 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(); 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; if (plan.files.isEmpty) return r;
final tmp = await Directory.systemTemp.createTemp('jj_7z_'); final tmp = await Directory.systemTemp.createTemp('jj_7z_');
try { try {

View File

@ -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(', ')]), [plan.unsafe.length, plan.unsafe.take(5).map((x) => '${x.$1} (${x.$2})').join(', ')]),
style: const TextStyle(color: JjColors.danger, fontSize: 12)), 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) ...[ if (plan.links.isNotEmpty) ...[
const SizedBox(height: 6), const SizedBox(height: 6),
Text(trf('링크 {0}개는 만들지 않고 건너뜁니다: {1}', [plan.links.length, plan.links.take(5).join(', ')]), 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!), 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) if (short)
Text(trf('풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?', [formatSize(plan.totalSize), formatSize(f)]), 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 { final r = await _withProgress(context, trf('압축 푸는 중 · {0}', [vBasename(path)]), (set, cancelled) async {
if (kind == ArchiveKind.zip) { if (kind == ArchiveKind.zip) {
return extractZipPlan(local, dest, plan, 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([ say([
trf('{0}개를 풀었습니다: {1}', [r.done, vDisplay(dest)]), trf('{0}개를 풀었습니다: {1}', [r.done, vDisplay(dest)]),
if (r.skippedExisting.isNotEmpty) trf('같은 이름이라 건너뜀 {0}개', [r.skippedExisting.length]), if (r.skippedExisting.isNotEmpty) trf('같은 이름이라 건너뜀 {0}개', [r.skippedExisting.length]),
if (r.renamed.isNotEmpty) trf('이름을 바꿔 풂 {0}개', [r.renamed.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.unsafe.isNotEmpty) trf('막은 경로 {0}개', [plan.unsafe.length]),
if (plan.links.isNotEmpty) trf('건너뛴 링크 {0}개', [plan.links.length]), if (plan.links.isNotEmpty) trf('건너뛴 링크 {0}개', [plan.links.length]),
if (plan.badNames.isNotEmpty) trf('풀 수 없는 이름 {0}개', [plan.badNames.length]),
].join(' · ')); ].join(' · '));
return dest; return dest;
} on ArchivePasswordException catch (e) { } on ArchivePasswordException catch (e) {

View File

@ -37,6 +37,18 @@ void main() {
expect(plan.encrypted, isTrue); 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', () { group('ZIP', () {
test('만들기 (폴더 · 파일, 링크는 넣지 않음) → 목록 → 풀기 (같은 이름은 고른 대로)', () async { test('만들기 (폴더 · 파일, 링크는 넣지 않음) → 목록 → 풀기 (같은 이름은 고른 대로)', () async {
final src = Directory(p.join(tmp.path, 'src'))..createSync(); 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'); 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 { test('zip slip: ../ · 드라이브 경로 · 링크 항목은 대상 밖에 쓰지 않고 알린다', () async {
final a = Archive() final a = Archive()
..addFile(ArchiveFile.string('ok.txt', 'ok')) ..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() || 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); 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), throwsA(isA<ArchivePasswordException>()));
await expectLater(list7z(exe, enc, password: 'nope'), throwsA(isA<ArchivePasswordException>().having((e) => e.wrong, 'wrong', isTrue))); await expectLater(list7z(exe, enc, password: 'nope'), throwsA(isA<ArchivePasswordException>().having((e) => e.wrong, 'wrong', isTrue)));
final encList = await list7z(exe, enc, password: 'pw2'); final encList = await list7z(exe, enc, password: 'pw2');

View File

@ -34,6 +34,13 @@ if (-not ($list -contains "$name/jj_mkvmaker.exe")) {
Remove-Item $Zip -Force Remove-Item $Zip -Force
throw "zip 맨 위에 $name/jj_mkvmaker.exe 가 없습니다" 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 $item = Get-Item $Zip
$hash = (Get-FileHash $Zip -Algorithm SHA256).Hash.ToLower() $hash = (Get-FileHash $Zip -Algorithm SHA256).Hash.ToLower()
"{0} {1} bytes {2} files sha256 {3}" -f $item.Name, $item.Length, $list.Count, $hash "{0} {1} bytes {2} files sha256 {3}" -f $item.Name, $item.Length, $list.Count, $hash

View File

@ -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) if(JJ_7ZIP_FILES)
install(FILES ${JJ_7ZIP_FILES} install(FILES ${JJ_7ZIP_FILES}
DESTINATION "${CMAKE_INSTALL_PREFIX}/7zip" COMPONENT Runtime) 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() endif()
install(FILES "${JJ_LICENSES}/LGPL-2.1.txt" DESTINATION "${CMAKE_INSTALL_PREFIX}/7zip" COMPONENT Runtime OPTIONAL) install(FILES "${JJ_LICENSES}/LGPL-2.1.txt" DESTINATION "${CMAKE_INSTALL_PREFIX}/7zip" COMPONENT Runtime OPTIONAL)