WIP 암호 보관함 1단계-2 화면 (관리자 결정 10/21): 환경 설정 > 보안 [암호 보관함] (처음 끔) · [보관함 열기] · [보관함 자동 잠금] (처음 1분). 만들기 창에 자리 · "다른 앱도 이 폴더의 암호화된 파일을 볼 수 있음 (내용 · 이름은 안 보임)" · "이 폴더를 지우면 보관함도 사라짐 · 폴더 복사 = 백업" · "잊으면 되살릴 수 없음" + 체크해야 [만들기]. 잠금 풀기는 "보관함을 여는 중… (비밀번호 확인)" (Isolate), 5번 틀리면 30초. 목록 (넣은 날 · 이름 정렬) · [보기] (임시로 풀어 앱 안 보기 화면 · 플레이어, 닫으면 지움) · [꺼내기] (고른 폴더, 같은 이름이면 (2)) · [이름 변경] · [보관함에서 지우기] (확인). 파일 탐색기 길게 누르기 [보관함에 넣기] (보관함을 켰을 때 · 이 기기의 파일만) - 넣기 창의 [원본 지우기 (이동)] 처음 끔, 켜면 다 넣은 뒤 탐색기의 지우기 (확인 · 휴지통). 가운데 버튼 [암호 보관함] (끝에). [마스터로도 열기] (처음 끔, 마스터가 있어야) - 보관함 열쇠 한 벌을 안전 저장소에, 열 때 마스터 확인 (SecretGate), 마스터를 초기화 (clearMaster) 하면 그 한 벌만 지움. 최상 초기화 창에 "보관함은 초기화되지 않음". 자리: Android Download/JJ_MKVMaker/vault · Windows 문서\JJ_MKVMaker\vault. 앱이 뒤로 간 뒤 정한 시간이 지나면 다시 잠금. 보관함 열쇠는 설정 파일 · 버전 보관본에 들어가지 않음 (안전 저장소에만, [마스터로도 열기] 일 때) · 시험 3개 (만들기 → 넣기 / 이동 → 잠금 → 틀린 비밀번호 → 열기 → 꺼내기 → 지우기, 5번 틀림 · 마스터로 열기 · 마스터 초기화, 탐색기 메뉴) (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore_p3b
parent
9c72320737
commit
286e6b4698
|
|
@ -36,6 +36,7 @@
|
|||
"\"{0}\" 에 처음 연결합니다": "Connecting to \"{0}\" for the first time",
|
||||
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "These items are not in \"{0}\" but only in the target \"{1}\". Syncing with delete removes them from the target (cannot be undone).",
|
||||
"\"{0}\" 은(는) 비밀번호가 걸려 있습니다.": "\"{0}\" is password-protected.",
|
||||
"\"{0}\" 을 보관함에서 지웁니다. 되돌릴 수 없습니다 (먼저 [꺼내기] 를 하지 않았으면 내용이 사라집니다)": "Delete \"{0}\" from the vault. This cannot be undone (if you have not used [Export] first, the content is lost)",
|
||||
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "Move \"{0}\" to the \"{1}\" folder?",
|
||||
"\"{0}\" 을(를) 지울까요?": "Delete \"{0}\"?",
|
||||
"\"{0}\" 을(를) 지울까요? 되돌릴 수 없습니다.": "Delete \"{0}\"? This cannot be undone.",
|
||||
|
|
@ -85,6 +86,7 @@
|
|||
"16진": "Hex",
|
||||
"16진 보기 · {0}": "Hex view · {0}",
|
||||
"1배 (선명하게)": "1× (sharpen)",
|
||||
"1분 (기본)": "1 minute (default)",
|
||||
"1분 안": "under 1 min",
|
||||
"2/2 · 해상도 올리는 중": "2/2 · Upscaling",
|
||||
"5초 뒤로": "Back 5 seconds",
|
||||
|
|
@ -576,6 +578,7 @@
|
|||
"{0}개 맞춤": "{0} synced",
|
||||
"{0}개 맞춤 · 같은 이름의 폴더가 있어 건너뜀 {1}개": "{0} synced · skipped {1} (a folder with the same name exists)",
|
||||
"{0}개 목록에서 숨기기": "Hide {0} items from list",
|
||||
"{0}개 보관함에 넣기": "Add {0} to vault",
|
||||
"{0}개 보임": "{0} shown",
|
||||
"{0}개 삭제": "Delete {0} items",
|
||||
"{0}개 선택": "{0} selected",
|
||||
|
|
@ -604,6 +607,7 @@
|
|||
"{0}개는 휴지통에 들어가지 않아 영구 삭제했습니다 (Windows 가 묻고 고른 대로).": "{0} didn't fit in the Recycle Bin and were deleted permanently (as chosen when Windows asked).",
|
||||
"{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "Imported {0} into \"Other bookmarks\".",
|
||||
"{0}개를 묶었습니다: {1}": "Compressed {0} files: {1}",
|
||||
"{0}개를 보관함에 넣었습니다": "Added {0} to the vault",
|
||||
"{0}개를 영구 삭제할까요?": "Delete {0} items permanently?",
|
||||
"{0}개를 영구 삭제했습니다.": "Permanently deleted {0} item(s).",
|
||||
"{0}개를 옮겼습니다 → {1}": "Moved {0} → {1}",
|
||||
|
|
@ -874,6 +878,9 @@
|
|||
"길게 누르면 {0}": "long-press: {0}",
|
||||
"길이": "Length",
|
||||
"길이 {0}": "Length {0}",
|
||||
"꺼내기": "Export",
|
||||
"꺼내지 못했습니다: {0}": "Could not export: {0}",
|
||||
"꺼냈습니다: {0}": "Exported: {0}",
|
||||
"끄기": "Off",
|
||||
"끄면 (기본) Tor 화면을 떠날 때 (다른 화면 · 앱이 뒤로) 지웁니다. 일반 브라우저와는 늘 따로입니다.": "When off (default), they are cleared when you leave the Tor screen (another screen, or the app goes to the background). They are always kept separate from the normal browser.",
|
||||
"끄면 더 안전하지만 많은 사이트가 보이지 않습니다 (다음에 열 때부터).": "Off is safer, but many sites will not display (from the next time it opens).",
|
||||
|
|
@ -949,7 +956,10 @@
|
|||
"넘기는 방향: 오른쪽 → 왼쪽 (만화)": "Page direction: right → left (manga)",
|
||||
"넘기는 방향: 왼쪽 → 오른쪽": "Page direction: left → right",
|
||||
"넘으면 오래 보지 않은 것부터 지웁니다": "When full, the least recently viewed are deleted first",
|
||||
"넣기": "Add",
|
||||
"넣기는 파일 탐색기에서 파일을 길게 눌러 [보관함에 넣기]": "To add, long-press a file in the file explorer and choose [Add to vault]",
|
||||
"넣으면 AES-256 으로 암호화합니다. 비우면 보통 ZIP": "If set, encrypts with AES-256. Leave empty for a normal ZIP",
|
||||
"넣은 날": "Date added",
|
||||
"넣지 못했습니다: {0}": "Could not add: {0}",
|
||||
"네덜란드어": "Dutch",
|
||||
"네트워크 · DNS": "Network · DNS",
|
||||
|
|
@ -973,6 +983,7 @@
|
|||
"늘 받아서 열기": "Always download and open",
|
||||
"늘 이름 바꾸기": "Always keep both (rename)",
|
||||
"늘 주소로 열기": "Always open by address",
|
||||
"다 넣고 확인한 뒤에 탐색기의 지우기 (확인 · 휴지통) 로 지웁니다. 끄면 원본은 그대로 (복사)": "After everything is added and checked, the originals are deleted through the file explorer's delete (confirm · trash). Off keeps the originals (copy)",
|
||||
"다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)": "As soon as they finish, put them in the Make MKV video list and remove them from the download list (same as the checkbox on the download screen)",
|
||||
"다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)": "When finished, automatically add to the Make MKV video list and remove from this download list (the downloaded file stays)",
|
||||
"다 받은 {0} 은 이미 MKV 목록에 있어 다운로드 목록에서 뺐습니다 (받은 파일은 그대로)": "{0} is already in the MKV list, so it was removed from the download list (the file is kept)",
|
||||
|
|
@ -983,6 +994,7 @@
|
|||
"다른 버전 ({0}) 을 쓰다가 v{1} 로 돌아왔습니다. 그 버전은 이 버전의 설정 일부를 모를 수 있습니다.\nv{1} 를 마지막으로 쓸 때 보관해 둔 설정 (동영상 목록 · 즐겨찾기 포함) 으로 되돌릴까요?": "You came back to v{1} after using another version ({0}). That version may not know some of this version's settings.\nRestore the settings kept when you last used v{1} (including the video list and bookmarks)?",
|
||||
"다른 버전 고르기": "Choose another version",
|
||||
"다른 브라우저의 즐겨찾기 가져오기": "Import bookmarks from another browser",
|
||||
"다른 앱도 이 폴더의 암호화된 파일을 볼 수 있습니다 (내용 · 이름은 보이지 않습니다)": "Other apps can see the encrypted files in this folder (not their content or names)",
|
||||
"다른 앱에는 아이디 · 비밀번호를 넘기지 않습니다.\n\n• 받아서 열기: 임시 폴더로 받은 뒤 엽니다. 어느 앱에서나 열리지만 다 받을 때까지 기다려야 합니다.\n• 주소로 열기: 받지 않고 바로 재생합니다. 그 앱이 아이디 · 비밀번호를 물으면 넣어 주세요 (VLC 등). 묻지 않는 앱에서는 열리지 않습니다.": "Your user name and password are not passed to other apps.\n\n• Download and open: downloads to a temporary folder, then opens. Works with any app, but you must wait until the download finishes.\n• Open by address: plays right away without downloading. If the app asks for a user name and password, enter them (VLC etc.). Apps that don't ask cannot open it.",
|
||||
"다른 앱에는 아이디 · 비밀번호를 넘기지 않습니다. 받아서 열기는 어느 앱에서나 열리고, 주소로 열기는 아이디를 묻는 앱 (VLC 등) 만 열립니다": "User names and passwords aren't passed to other apps. Download-and-open works with any app; open-by-address works only with apps that ask for a login (VLC etc.)",
|
||||
"다른 앱에서 동영상을 열 때": "When opening a video from another app",
|
||||
|
|
@ -1198,6 +1210,7 @@
|
|||
"마스터 비밀번호 정하기": "Set master password",
|
||||
"마스터 비밀번호가 필요합니다": "The master password is needed",
|
||||
"마스터 비밀번호가 필요해 기다리는 중": "Waiting for the master password",
|
||||
"마스터 비밀번호로 열기": "Open with master password",
|
||||
"마스터 비밀번호로 잠겨 있습니다": "Locked with the master password",
|
||||
"마스터 비밀번호를 끌까요?": "Turn off the master password?",
|
||||
"마스터 비밀번호를 넣어야 저장된 비밀번호를 쓸 수 있습니다": "Enter the master password to use saved passwords",
|
||||
|
|
@ -1205,6 +1218,8 @@
|
|||
"마스터 비밀번호를 묻는 때": "When to ask for the master password",
|
||||
"마스터 비밀번호를 초기화했습니다": "Reset the master password",
|
||||
"마스터 비밀번호와 같게 정할 수 없습니다": "Can't be the same as the master password",
|
||||
"마스터로 여는 열쇠가 없습니다 (마스터를 초기화했으면 없어집니다) - 보관함 비밀번호로 여세요": "There is no master key copy (it is removed when the master password is reset) - open with the vault password",
|
||||
"마스터로도 열기": "Also open with master password",
|
||||
"마지막": "Last",
|
||||
"마지막 인코딩 설정을 기억합니다": "Remembers the last encoding settings",
|
||||
"마지막 장이라 계속 보기를 멈췄습니다.": "Slideshow stopped at the last page.",
|
||||
|
|
@ -1245,6 +1260,7 @@
|
|||
"매번 묻기 (기본)": "Ask every time (default)",
|
||||
"매일 종일": "Every day, all day",
|
||||
"먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "First asks via mDNS (NAS devices that announce their names), then tries an SMB (445) connection to devices on the network this device is on (Wi-Fi · wired) and closes it right away. No files or account details are sent.",
|
||||
"먼저 환경 설정 > 보안 에서 마스터 비밀번호를 정하세요": "First set a master password in Settings > Security",
|
||||
"멈추고 옮기기": "Stop and move",
|
||||
"멈춤": "Stopped",
|
||||
"멈춤 (시작 안 됨)": "Stalled",
|
||||
|
|
@ -1357,6 +1373,7 @@
|
|||
"바뀐 때: {0}": "Modified: {0}",
|
||||
"바뀐 지 … 일 안": "Modified within … days",
|
||||
"바뀐 지 … 일 지남": "Modified more than … days ago",
|
||||
"바로": "Immediately",
|
||||
"바로 끔": "Stop right away",
|
||||
"바로 시작 (기본)": "Start right away (default)",
|
||||
"바로 시작: 켜진 동기화를 모두 시작 · 골라서 시작: 켤 때 고르기 · 시작 안 함: 모니터링 > lsync 의 시작 버튼으로": "Start right away: start all enabled syncs · Choose: pick at startup · Don't start: use the start button in Monitor > lsync",
|
||||
|
|
@ -1450,6 +1467,20 @@
|
|||
"변환": "Convert",
|
||||
"변환기 안에서 오류가 났습니다": "An error occurred inside the converter",
|
||||
"보관한 설정으로 되살리고 다시 시작": "Restore kept settings and restart",
|
||||
"보관함 비밀번호": "Vault password",
|
||||
"보관함 비밀번호 바꾸기": "Change vault password",
|
||||
"보관함 비밀번호를 바꿨습니다": "Vault password changed",
|
||||
"보관함 열기": "Open vault",
|
||||
"보관함 열쇠 한 벌을 이 기기의 안전 저장소에 두고, 마스터 비밀번호로도 열 수 있게 합니다. 보관함 비밀번호를 잊어도 열 수 있지만, 마스터 비밀번호를 아는 사람도 열 수 있게 됩니다. 마스터를 초기화하면 이 길만 없어지고 보관함 비밀번호는 그대로입니다.": "Keeps a copy of the vault key in this device's secure storage so the master password can also open the vault. You can open it even if you forget the vault password, but anyone who knows the master password can open it too. Resetting the master password removes only this path; the vault password stays the same.",
|
||||
"보관함 자동 잠금": "Vault auto-lock",
|
||||
"보관함 폴더를 열 수 없습니다: {0}": "Cannot open the vault folder: {0}",
|
||||
"보관함에 넣기": "Add to vault",
|
||||
"보관함에 넣기 · {0}개": "Add to vault · {0}",
|
||||
"보관함에서 지우기": "Delete from vault",
|
||||
"보관함을 만들지 못했습니다: {0}": "Could not create the vault: {0}",
|
||||
"보관함을 여는 중… (비밀번호 확인)": "Opening the vault… (checking password)",
|
||||
"보관함을 열지 못했습니다: {0}": "Could not open the vault: {0}",
|
||||
"보관함이 비어 있습니다": "The vault is empty",
|
||||
"보관해 둔 설정을 되살렸습니다: {0}": "Restored the kept settings: {0}",
|
||||
"보기": "View",
|
||||
"보기 {0}초 · Ctrl+휠 확대/축소": "View {0} s · Ctrl+wheel to zoom",
|
||||
|
|
@ -1536,6 +1567,7 @@
|
|||
"비밀번호가 걸린 압축은 안을 미리 볼 수 없습니다 - [압축 풀기] 로 푸세요": "Can't preview a password-protected archive - use [Extract]",
|
||||
"비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)": "Some files are password-protected (you'll be asked when extracting)",
|
||||
"비밀번호가 걸린 파일입니다 - [압축 풀기] 로 푸세요": "This file is password-protected - use [Extract]",
|
||||
"비밀번호가 여러 번 틀려 30초 뒤에 다시 넣을 수 있습니다": "Too many wrong passwords - try again in 30 seconds",
|
||||
"비밀번호가 저장된 기능을 쓸 때": "When using features with saved passwords",
|
||||
"비밀번호가 틀렸습니다": "Wrong password",
|
||||
"비밀번호가 틀렸습니다 ({0}번)": "Wrong password ({0} times)",
|
||||
|
|
@ -1543,6 +1575,8 @@
|
|||
"비밀번호를 넣으세요": "Enter a password",
|
||||
"비밀번호를 안전 저장소에 저장했습니다": "Password saved to secure storage",
|
||||
"비밀번호를 잊었습니다": "I forgot the password",
|
||||
"비밀번호를 잊으면 되살릴 수 없습니다 (뒷문 없음)": "If you forget the password, it cannot be recovered (no back door)",
|
||||
"비밀번호를 잊으면 되살릴 수 없음을 압니다": "I understand a forgotten password cannot be recovered",
|
||||
"비밀번호를 잊으면 이 ZIP 을 풀 수 없습니다 (앱이 기억하지 않습니다). 파일 이름 목록은 암호화되지 않습니다.": "If you forget the password, this ZIP can't be extracted (the app doesn't remember it). The list of file names is not encrypted.",
|
||||
"비밀번호에 한글 등 영문 밖 글자가 있으면 512MB 넘는 파일 ({0}) 은 묶을 수 없습니다. 영문 · 숫자 · 기호 비밀번호를 쓰거나 파일을 나눠 주세요.": "With a password containing non-English letters (e.g. Korean), files over 512MB ({0}) can't be added. Use a password of English letters, digits and symbols, or split the file.",
|
||||
"비어 있는 압축 파일입니다.": "The archive is empty.",
|
||||
|
|
@ -1595,6 +1629,7 @@
|
|||
"새 버전을 확인하지 못했습니다": "the update check failed.",
|
||||
"새 버전을 확인할 수 없습니다: {0}": "Cannot check for a new version: {0}",
|
||||
"새 버전이 예전 기본값을 바꿨으면 켤 때 한 번 무엇이 바뀌었는지 알려 줍니다": "If a new version changed old defaults, shows once at startup what changed",
|
||||
"새 보관함 만들기": "Create a new vault",
|
||||
"새 비밀번호": "New password",
|
||||
"새 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 예전 비밀번호로 돌아갑니다.": "The new password has not been saved yet. If you close the app, it goes back to the old password.",
|
||||
"새 즐겨찾기 추가": "Add new bookmark",
|
||||
|
|
@ -1825,6 +1860,9 @@
|
|||
"알리지 않음": "Don't notify",
|
||||
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "Notifications show as JJ_MKVMaker and open the app when clicked (current user only, no admin rights)",
|
||||
"암호 ZIP 으로 묶는 중 · {0}": "Making password ZIP · {0}",
|
||||
"암호 보관함": "Encrypted vault",
|
||||
"암호 보관함은 초기화되지 않습니다 - 보관함 파일은 그대로 남고 보관함 비밀번호로만 열 수 있습니다. [마스터로도 열기] 를 켰다면 마스터를 초기화할 때 그 길만 없어집니다": "The encrypted vault is not reset - its files stay and can be opened only with the vault password. If [Also open with master password] is on, resetting the master password removes only that path",
|
||||
"암호 보관함은 환경 설정 > 보안 에서 켭니다 (처음 꺼짐)": "Turn on the encrypted vault in Settings > Security (off by default)",
|
||||
"암호가 걸린 문서입니다": "This document is password-protected",
|
||||
"암호가 맞지 않습니다": "The password is incorrect",
|
||||
"암호로 묶기": "Make password ZIP",
|
||||
|
|
@ -1874,6 +1912,7 @@
|
|||
"앱을 닫아 진행 중이던 작업이 멈췄습니다. 닫아도 계속하려면 환경 설정에서 '백그라운드로 실행' 을 켜세요.": "Closing the app stopped the running tasks. To keep them going after closing, turn on 'Run in background' in Settings.",
|
||||
"앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)": "The app was newly installed and has no settings. Restore the saved settings of v{0} (including the video list and bookmarks)?\n(Subtitle API keys and passwords were not saved - please enter them again)",
|
||||
"앱을 지우면 다음은 되살릴 수 없습니다:": "Removing the app loses the following for good:",
|
||||
"앱이 뒤로 간 뒤 이만큼 지나 돌아오면 다시 비밀번호를 묻습니다": "Asks for the password again when you return after the app has been in the background this long",
|
||||
"앱이 켜져 있는 동안 늘 동기화합니다.": "Always syncs while the app is running.",
|
||||
"약 {0} 남음": "about {0} left",
|
||||
"약 {0}~{1}분": "about {0}–{1} min",
|
||||
|
|
@ -2016,6 +2055,7 @@
|
|||
"원본 옆에 \"이름_x2\" (ZIP 은 ZIP 옆 \"ZIP이름_AI\" 폴더)": "Next to the original as \"name_x2\" (for ZIPs, in a \"ZIPname_AI\" folder next to the ZIP)",
|
||||
"원본 옆으로": "Next to the original",
|
||||
"원본 유지 (재인코딩 없음)": "Keep original (no re-encoding)",
|
||||
"원본 지우기 (이동)": "Delete originals (move)",
|
||||
"원본 크기": "Original size",
|
||||
"원본 파일 지우기 (--remove-source-files)": "Delete source files (--remove-source-files)",
|
||||
"원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다.": "Could not remove the source files (no permission or in use). They were copied to the destination and the source is still there.",
|
||||
|
|
@ -2159,10 +2199,13 @@
|
|||
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "This location (network drive, network share, USB memory etc.) has no Recycle Bin, so items are deleted right away.",
|
||||
"이 이동 버튼 지우기": "Delete this move button",
|
||||
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "These options will be used the next time this move runs. Run it now with the new options?",
|
||||
"이 종류는 앱 안에서 볼 수 없습니다 - [꺼내기] 로 꺼내서 보세요": "This type cannot be viewed in the app - use [Export] to view it",
|
||||
"이 쪽을 그리지 못했습니다": "Couldn't render this page",
|
||||
"이 파일에 맞는 자막 우선 (영상 해시)": "Prefer subtitles that match this file (video hash)",
|
||||
"이 파일용": "For this file",
|
||||
"이 파일은 바뀌었거나 깨져 풀 수 없습니다": "This file has been changed or is damaged and cannot be decrypted",
|
||||
"이 파일을 열 수 있는 앱이 없습니다.": "No app can open this file.",
|
||||
"이 판이 모르는 보관함 형식입니다 - 앱을 새 판으로 올려 주세요 (보관함은 그대로 둡니다)": "This version does not know this vault format - please update the app (the vault is left untouched)",
|
||||
"이 페이지 번역": "Translate this page",
|
||||
"이 페이지 지우기": "Delete this page",
|
||||
"이 페이지를 {0}(으)로 번역": "Translate this page to {0}",
|
||||
|
|
@ -2175,6 +2218,7 @@
|
|||
"이 폴더를 rsync 원본 · 대상으로 고르기": "Choose this folder as rsync source · target",
|
||||
"이 폴더를 볼 권한이 없습니다": "No permission to view this folder",
|
||||
"이 폴더를 읽을 권한이 없습니다": "No permission to read this folder",
|
||||
"이 폴더를 지우면 보관함도 사라집니다. 폴더를 통째로 복사하면 그대로 백업이 됩니다 (비밀번호로 열림)": "Deleting this folder deletes the vault. Copying the whole folder is a complete backup (opens with the password)",
|
||||
"이 폴더에 동영상이 없습니다.": "There are no videos in this folder.",
|
||||
"이 폴더에 빈 .nomedia 파일을 만듭니다. 갤러리 · 음악 앱이 이 폴더 (안의 폴더 포함) 의 그림 · 동영상 · 음악을 보이지 않게 됩니다.": "Creates an empty .nomedia file in this folder. Gallery and music apps will stop showing the pictures, videos and music in this folder (including subfolders).",
|
||||
"이 폴더에 새 폴더": "New folder in this folder",
|
||||
|
|
@ -2300,6 +2344,7 @@
|
|||
"자동: 그래픽카드 (GPU) 가 있으면 사진용, CPU 뿐이면 가볍고 빠른 만화용. 원본은 덮어쓰지 않고 새 파일로 저장합니다": "Auto: the photo model with a graphics card (GPU), the light and fast comic model with CPU only. Originals are never overwritten; results are saved as new files",
|
||||
"자동: 잰 결과 {0} 가 가장 빠릅니다": "Auto: measured {0} as the fastest",
|
||||
"자동: 처음 그릴 때 짧게 재서 가장 빠른 장치를 고릅니다": "Auto: a short test on the first drawing picks the fastest processor",
|
||||
"자리: {0}": "Location: {0}",
|
||||
"자릿수": "Digits",
|
||||
"자막": "Subtitles",
|
||||
"자막 (AI · 인터넷)": "Subtitles (AI · online)",
|
||||
|
|
@ -2362,6 +2407,7 @@
|
|||
"잘못된 간격: {0}": "Invalid step: {0}",
|
||||
"잘못된 범위: {0}": "Invalid range: {0}",
|
||||
"잘못된 칸: {0}": "Invalid field: {0}",
|
||||
"잠그기": "Lock",
|
||||
"재개": "Resume",
|
||||
"재생": "Play",
|
||||
"재생 목록": "Playlist",
|
||||
|
|
@ -2772,6 +2818,7 @@
|
|||
"파일 폴더": "File folder",
|
||||
"파일 확인 중…": "Checking files…",
|
||||
"파일만 골랐을 때": "When only files are selected",
|
||||
"파일을 AES-256 으로 암호화해 보관합니다 (이름까지 숨김). 보관함 비밀번호를 잊으면 되살릴 수 없습니다": "Keeps files encrypted with AES-256 (names hidden too). A forgotten vault password cannot be recovered",
|
||||
"파일을 복사하지 못했습니다": "Could not copy the file",
|
||||
"파일을 분석할 수 없습니다: {0}": "Cannot analyze the file: {0}",
|
||||
"파일을 읽을 수 없습니다": "Can't read the file",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
"\"{0}\" 에 처음 연결합니다": "「{0}」に初めて接続します",
|
||||
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "「{0}」にはなく、対象「{1}」にだけある項目です。削除を含めて同期すると対象から削除されます (元に戻せません)。",
|
||||
"\"{0}\" 은(는) 비밀번호가 걸려 있습니다.": "「{0}」にはパスワードが設定されています。",
|
||||
"\"{0}\" 을 보관함에서 지웁니다. 되돌릴 수 없습니다 (먼저 [꺼내기] 를 하지 않았으면 내용이 사라집니다)": "「{0}」を保管庫から削除します。元に戻せません (先に [取り出す] をしていなければ内容は失われます)",
|
||||
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "「{0}」を「{1}」フォルダーに移動しますか?",
|
||||
"\"{0}\" 을(를) 지울까요?": "「{0}」を削除しますか?",
|
||||
"\"{0}\" 을(를) 지울까요? 되돌릴 수 없습니다.": "「{0}」を削除しますか? 元に戻せません。",
|
||||
|
|
@ -85,6 +86,7 @@
|
|||
"16진": "16進",
|
||||
"16진 보기 · {0}": "16進表示 · {0}",
|
||||
"1배 (선명하게)": "1倍 (くっきり)",
|
||||
"1분 (기본)": "1 分 (既定)",
|
||||
"1분 안": "1 分以内",
|
||||
"2/2 · 해상도 올리는 중": "2/2 · 解像度を上げています",
|
||||
"5초 뒤로": "5秒戻る",
|
||||
|
|
@ -576,6 +578,7 @@
|
|||
"{0}개 맞춤": "{0} 件を同期",
|
||||
"{0}개 맞춤 · 같은 이름의 폴더가 있어 건너뜀 {1}개": "{0} 件同期 · 同名のフォルダーがあるため {1} 件スキップ",
|
||||
"{0}개 목록에서 숨기기": "{0} 件をリストから隠す",
|
||||
"{0}개 보관함에 넣기": "{0} 件を保管庫に入れる",
|
||||
"{0}개 보임": "{0} 件表示",
|
||||
"{0}개 삭제": "{0} 件を削除",
|
||||
"{0}개 선택": "{0}個選択",
|
||||
|
|
@ -604,6 +607,7 @@
|
|||
"{0}개는 휴지통에 들어가지 않아 영구 삭제했습니다 (Windows 가 묻고 고른 대로).": "{0} 件はごみ箱に入らなかったため完全に削除しました (Windows の確認で選んだとおり)。",
|
||||
"{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "{0}個を「その他のブックマーク」に取り込みました。",
|
||||
"{0}개를 묶었습니다: {1}": "{0} 件をまとめました: {1}",
|
||||
"{0}개를 보관함에 넣었습니다": "{0} 件を保管庫に入れました",
|
||||
"{0}개를 영구 삭제할까요?": "{0} 件を完全に削除しますか?",
|
||||
"{0}개를 영구 삭제했습니다.": "{0} 件を完全に削除しました。",
|
||||
"{0}개를 옮겼습니다 → {1}": "{0}個を移動しました → {1}",
|
||||
|
|
@ -874,6 +878,9 @@
|
|||
"길게 누르면 {0}": "長押しで{0}",
|
||||
"길이": "長さ",
|
||||
"길이 {0}": "長さ {0}",
|
||||
"꺼내기": "取り出す",
|
||||
"꺼내지 못했습니다: {0}": "取り出せませんでした: {0}",
|
||||
"꺼냈습니다: {0}": "取り出しました: {0}",
|
||||
"끄기": "オフ",
|
||||
"끄면 (기본) Tor 화면을 떠날 때 (다른 화면 · 앱이 뒤로) 지웁니다. 일반 브라우저와는 늘 따로입니다.": "オフ (既定) の場合は Tor 画面を離れるとき (別の画面 · アプリがバックグラウンドへ) に消去します。通常のブラウザーとは常に別です。",
|
||||
"끄면 더 안전하지만 많은 사이트가 보이지 않습니다 (다음에 열 때부터).": "オフの方が安全ですが、多くのサイトが表示されません (次に開くときから)。",
|
||||
|
|
@ -949,7 +956,10 @@
|
|||
"넘기는 방향: 오른쪽 → 왼쪽 (만화)": "めくる方向: 右 → 左 (マンガ)",
|
||||
"넘기는 방향: 왼쪽 → 오른쪽": "めくる方向: 左 → 右",
|
||||
"넘으면 오래 보지 않은 것부터 지웁니다": "上限を超えると、長く見ていないものから削除します",
|
||||
"넣기": "入れる",
|
||||
"넣기는 파일 탐색기에서 파일을 길게 눌러 [보관함에 넣기]": "入れるには、ファイルエクスプローラーでファイルを長押しして [保管庫に入れる]",
|
||||
"넣으면 AES-256 으로 암호화합니다. 비우면 보통 ZIP": "入力すると AES-256 で暗号化します。空欄なら通常の ZIP",
|
||||
"넣은 날": "入れた日",
|
||||
"넣지 못했습니다: {0}": "追加できませんでした: {0}",
|
||||
"네덜란드어": "オランダ語",
|
||||
"네트워크 · DNS": "ネットワーク・DNS",
|
||||
|
|
@ -973,6 +983,7 @@
|
|||
"늘 받아서 열기": "常にダウンロードして開く",
|
||||
"늘 이름 바꾸기": "常に名前を変える",
|
||||
"늘 주소로 열기": "常にアドレスで開く",
|
||||
"다 넣고 확인한 뒤에 탐색기의 지우기 (확인 · 휴지통) 로 지웁니다. 끄면 원본은 그대로 (복사)": "すべて入れて確認した後、エクスプローラーの削除 (確認・ごみ箱) で削除します。オフなら元のファイルはそのまま (コピー)",
|
||||
"다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)": "ダウンロードが終わり次第 MKV 作成の動画リストに入れ、ダウンロードリストからは外します(ダウンロード画面のチェックボックスと同じ)",
|
||||
"다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)": "ダウンロードが終わると MKV 作成の動画リストに自動で入れ、このダウンロードリストからは外します(ダウンロードしたファイルはそのまま)",
|
||||
"다 받은 {0} 은 이미 MKV 목록에 있어 다운로드 목록에서 뺐습니다 (받은 파일은 그대로)": "ダウンロードした {0} はすでに MKV 一覧にあるため、ダウンロード一覧から外しました (ファイルはそのまま)",
|
||||
|
|
@ -983,6 +994,7 @@
|
|||
"다른 버전 ({0}) 을 쓰다가 v{1} 로 돌아왔습니다. 그 버전은 이 버전의 설정 일부를 모를 수 있습니다.\nv{1} 를 마지막으로 쓸 때 보관해 둔 설정 (동영상 목록 · 즐겨찾기 포함) 으로 되돌릴까요?": "別のバージョン ({0}) を使ったあと v{1} に戻りました。そのバージョンはこのバージョンの設定の一部を知らない可能性があります。\nv{1} を最後に使ったときに保管した設定 (動画リスト · ブックマークを含む) に戻しますか?",
|
||||
"다른 버전 고르기": "別のバージョンを選ぶ",
|
||||
"다른 브라우저의 즐겨찾기 가져오기": "他のブラウザーのブックマークを取り込む",
|
||||
"다른 앱도 이 폴더의 암호화된 파일을 볼 수 있습니다 (내용 · 이름은 보이지 않습니다)": "他のアプリもこのフォルダーの暗号化されたファイルを見ることができます (内容・名前は見えません)",
|
||||
"다른 앱에는 아이디 · 비밀번호를 넘기지 않습니다.\n\n• 받아서 열기: 임시 폴더로 받은 뒤 엽니다. 어느 앱에서나 열리지만 다 받을 때까지 기다려야 합니다.\n• 주소로 열기: 받지 않고 바로 재생합니다. 그 앱이 아이디 · 비밀번호를 물으면 넣어 주세요 (VLC 등). 묻지 않는 앱에서는 열리지 않습니다.": "他のアプリには ID・パスワードを渡しません。\n\n• ダウンロードして開く: 一時フォルダーにダウンロードしてから開きます。どのアプリでも開けますが、ダウンロードが終わるまで待つ必要があります。\n• アドレスで開く: ダウンロードせずにすぐ再生します。アプリが ID・パスワードを尋ねたら入力してください (VLC など)。尋ねないアプリでは開けません。",
|
||||
"다른 앱에는 아이디 · 비밀번호를 넘기지 않습니다. 받아서 열기는 어느 앱에서나 열리고, 주소로 열기는 아이디를 묻는 앱 (VLC 등) 만 열립니다": "他のアプリには ID・パスワードを渡しません。ダウンロードして開くはどのアプリでも開け、アドレスで開くは ID を尋ねるアプリ (VLC など) でだけ開けます",
|
||||
"다른 앱에서 동영상을 열 때": "他のアプリで動画を開いたとき",
|
||||
|
|
@ -1198,6 +1210,7 @@
|
|||
"마스터 비밀번호 정하기": "マスターパスワードを設定",
|
||||
"마스터 비밀번호가 필요합니다": "マスターパスワードが必要です",
|
||||
"마스터 비밀번호가 필요해 기다리는 중": "マスターパスワードが必要なため待機中",
|
||||
"마스터 비밀번호로 열기": "マスターパスワードで開く",
|
||||
"마스터 비밀번호로 잠겨 있습니다": "マスターパスワードでロックされています",
|
||||
"마스터 비밀번호를 끌까요?": "マスターパスワードをオフにしますか?",
|
||||
"마스터 비밀번호를 넣어야 저장된 비밀번호를 쓸 수 있습니다": "保存したパスワードを使うにはマスターパスワードを入力してください",
|
||||
|
|
@ -1205,6 +1218,8 @@
|
|||
"마스터 비밀번호를 묻는 때": "マスターパスワードを尋ねるとき",
|
||||
"마스터 비밀번호를 초기화했습니다": "マスターパスワードを初期化しました",
|
||||
"마스터 비밀번호와 같게 정할 수 없습니다": "マスターパスワードと同じにはできません",
|
||||
"마스터로 여는 열쇠가 없습니다 (마스터를 초기화했으면 없어집니다) - 보관함 비밀번호로 여세요": "マスターで開く鍵がありません (マスターを初期化すると消えます) - 保管庫のパスワードで開いてください",
|
||||
"마스터로도 열기": "マスターでも開く",
|
||||
"마지막": "最後",
|
||||
"마지막 인코딩 설정을 기억합니다": "最後のエンコード設定を記憶します",
|
||||
"마지막 장이라 계속 보기를 멈췄습니다.": "最後のページなので連続表示を停止しました。",
|
||||
|
|
@ -1245,6 +1260,7 @@
|
|||
"매번 묻기 (기본)": "毎回確認 (既定)",
|
||||
"매일 종일": "毎日 終日",
|
||||
"먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "まず mDNS で問い合わせ (名前を知らせる NAS)、続いてこの端末が接続しているネットワーク (Wi-Fi · 有線) の機器に SMB (445) で接続だけ試し、すぐに閉じます。ファイルやアカウント情報は送りません。",
|
||||
"먼저 환경 설정 > 보안 에서 마스터 비밀번호를 정하세요": "先に 設定 > セキュリティ でマスターパスワードを設定してください",
|
||||
"멈추고 옮기기": "止めて移す",
|
||||
"멈춤": "停止",
|
||||
"멈춤 (시작 안 됨)": "停滞 (開始されません)",
|
||||
|
|
@ -1357,6 +1373,7 @@
|
|||
"바뀐 때: {0}": "更新日時: {0}",
|
||||
"바뀐 지 … 일 안": "更新から … 日以内",
|
||||
"바뀐 지 … 일 지남": "更新から … 日以上",
|
||||
"바로": "すぐ",
|
||||
"바로 끔": "すぐ終了",
|
||||
"바로 시작 (기본)": "すぐ開始 (既定)",
|
||||
"바로 시작: 켜진 동기화를 모두 시작 · 골라서 시작: 켤 때 고르기 · 시작 안 함: 모니터링 > lsync 의 시작 버튼으로": "すぐ開始: オンの同期をすべて開始 · 選んで開始: 起動時に選ぶ · 開始しない: モニタリング > lsync の開始ボタンで",
|
||||
|
|
@ -1450,6 +1467,20 @@
|
|||
"변환": "変換",
|
||||
"변환기 안에서 오류가 났습니다": "変換エンジン内でエラーが発生しました",
|
||||
"보관한 설정으로 되살리고 다시 시작": "保管した設定で復元して再起動",
|
||||
"보관함 비밀번호": "保管庫のパスワード",
|
||||
"보관함 비밀번호 바꾸기": "保管庫のパスワードを変更",
|
||||
"보관함 비밀번호를 바꿨습니다": "保管庫のパスワードを変更しました",
|
||||
"보관함 열기": "保管庫を開く",
|
||||
"보관함 열쇠 한 벌을 이 기기의 안전 저장소에 두고, 마스터 비밀번호로도 열 수 있게 합니다. 보관함 비밀번호를 잊어도 열 수 있지만, 마스터 비밀번호를 아는 사람도 열 수 있게 됩니다. 마스터를 초기화하면 이 길만 없어지고 보관함 비밀번호는 그대로입니다.": "保管庫の鍵の写しをこの端末の安全な保存先に置き、マスターパスワードでも開けるようにします。保管庫のパスワードを忘れても開けますが、マスターパスワードを知っている人も開けるようになります。マスターを初期化するとこの方法だけがなくなり、保管庫のパスワードはそのままです。",
|
||||
"보관함 자동 잠금": "保管庫の自動ロック",
|
||||
"보관함 폴더를 열 수 없습니다: {0}": "保管庫のフォルダーを開けません: {0}",
|
||||
"보관함에 넣기": "保管庫に入れる",
|
||||
"보관함에 넣기 · {0}개": "保管庫に入れる・{0} 件",
|
||||
"보관함에서 지우기": "保管庫から削除",
|
||||
"보관함을 만들지 못했습니다: {0}": "保管庫を作成できませんでした: {0}",
|
||||
"보관함을 여는 중… (비밀번호 확인)": "保管庫を開いています… (パスワード確認)",
|
||||
"보관함을 열지 못했습니다: {0}": "保管庫を開けませんでした: {0}",
|
||||
"보관함이 비어 있습니다": "保管庫は空です",
|
||||
"보관해 둔 설정을 되살렸습니다: {0}": "保管した設定を復元しました: {0}",
|
||||
"보기": "表示",
|
||||
"보기 {0}초 · Ctrl+휠 확대/축소": "表示 {0}秒・Ctrl+ホイールで拡大/縮小",
|
||||
|
|
@ -1536,6 +1567,7 @@
|
|||
"비밀번호가 걸린 압축은 안을 미리 볼 수 없습니다 - [압축 풀기] 로 푸세요": "パスワード付きアーカイブの中はプレビューできません - [展開] で展開してください",
|
||||
"비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)": "パスワード付きのファイルがあります (展開時に確認します)",
|
||||
"비밀번호가 걸린 파일입니다 - [압축 풀기] 로 푸세요": "パスワード付きのファイルです - [展開] で展開してください",
|
||||
"비밀번호가 여러 번 틀려 30초 뒤에 다시 넣을 수 있습니다": "パスワードを何度も間違えたため、30 秒後に再入力できます",
|
||||
"비밀번호가 저장된 기능을 쓸 때": "パスワードを保存した機能を使うとき",
|
||||
"비밀번호가 틀렸습니다": "パスワードが違います",
|
||||
"비밀번호가 틀렸습니다 ({0}번)": "パスワードが違います ({0} 回)",
|
||||
|
|
@ -1543,6 +1575,8 @@
|
|||
"비밀번호를 넣으세요": "パスワードを入力してください",
|
||||
"비밀번호를 안전 저장소에 저장했습니다": "パスワードを安全な保存領域に保存しました",
|
||||
"비밀번호를 잊었습니다": "パスワードを忘れました",
|
||||
"비밀번호를 잊으면 되살릴 수 없습니다 (뒷문 없음)": "パスワードを忘れると復元できません (裏口なし)",
|
||||
"비밀번호를 잊으면 되살릴 수 없음을 압니다": "パスワードを忘れると復元できないことを理解しました",
|
||||
"비밀번호를 잊으면 이 ZIP 을 풀 수 없습니다 (앱이 기억하지 않습니다). 파일 이름 목록은 암호화되지 않습니다.": "パスワードを忘れるとこの ZIP は展開できません (アプリは記憶しません)。ファイル名の一覧は暗号化されません。",
|
||||
"비밀번호에 한글 등 영문 밖 글자가 있으면 512MB 넘는 파일 ({0}) 은 묶을 수 없습니다. 영문 · 숫자 · 기호 비밀번호를 쓰거나 파일을 나눠 주세요.": "パスワードに英字以外の文字 (ハングルなど) があると、512MB を超えるファイル ({0}) はまとめられません。英数字・記号のパスワードを使うか、ファイルを分けてください。",
|
||||
"비어 있는 압축 파일입니다.": "空の圧縮ファイルです。",
|
||||
|
|
@ -1595,6 +1629,7 @@
|
|||
"새 버전을 확인하지 못했습니다": "新しいバージョンを確認できませんでした。",
|
||||
"새 버전을 확인할 수 없습니다: {0}": "新しいバージョンを確認できません: {0}",
|
||||
"새 버전이 예전 기본값을 바꿨으면 켤 때 한 번 무엇이 바뀌었는지 알려 줍니다": "新しいバージョンが以前の既定値を変更した場合、起動時に一度だけ何が変わったかを知らせます",
|
||||
"새 보관함 만들기": "新しい保管庫を作成",
|
||||
"새 비밀번호": "新しいパスワード",
|
||||
"새 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 예전 비밀번호로 돌아갑니다.": "新しいパスワードはまだ保存されていません。アプリを終了すると以前のパスワードに戻ります。",
|
||||
"새 즐겨찾기 추가": "新しいブックマークを追加",
|
||||
|
|
@ -1825,6 +1860,9 @@
|
|||
"알리지 않음": "知らせない",
|
||||
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知が JJ_MKVMaker の名前で表示され、クリックするとアプリが開きます (管理者権限なしで現在のユーザーのみ)",
|
||||
"암호 ZIP 으로 묶는 중 · {0}": "パスワード付き ZIP にまとめています · {0}",
|
||||
"암호 보관함": "暗号化保管庫",
|
||||
"암호 보관함은 초기화되지 않습니다 - 보관함 파일은 그대로 남고 보관함 비밀번호로만 열 수 있습니다. [마스터로도 열기] 를 켰다면 마스터를 초기화할 때 그 길만 없어집니다": "暗号化保管庫は初期化されません - 保管庫のファイルはそのまま残り、保管庫のパスワードでのみ開けます。[マスターでも開く] をオンにしていた場合、マスターを初期化するとその方法だけがなくなります",
|
||||
"암호 보관함은 환경 설정 > 보안 에서 켭니다 (처음 꺼짐)": "暗号化保管庫は 設定 > セキュリティ でオンにします (既定はオフ)",
|
||||
"암호가 걸린 문서입니다": "パスワードで保護された文書です",
|
||||
"암호가 맞지 않습니다": "パスワードが正しくありません",
|
||||
"암호로 묶기": "パスワード付きでまとめる",
|
||||
|
|
@ -1874,6 +1912,7 @@
|
|||
"앱을 닫아 진행 중이던 작업이 멈췄습니다. 닫아도 계속하려면 환경 설정에서 '백그라운드로 실행' 을 켜세요.": "アプリを閉じたため実行中の作業が停止しました。閉じても続けるには、設定で「バックグラウンドで実行」をオンにしてください。",
|
||||
"앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)": "アプリを新しくインストールしたため設定がありません。保存した v{0} の設定 (動画リスト · ブックマークを含む) を復元しますか?\n(字幕 API キー · パスワードは保存していないので再入力してください)",
|
||||
"앱을 지우면 다음은 되살릴 수 없습니다:": "アプリを削除すると次のものは復元できません:",
|
||||
"앱이 뒤로 간 뒤 이만큼 지나 돌아오면 다시 비밀번호를 묻습니다": "アプリがバックグラウンドに移ってからこの時間が過ぎて戻ると、再びパスワードを求めます",
|
||||
"앱이 켜져 있는 동안 늘 동기화합니다.": "アプリの起動中は常に同期します。",
|
||||
"약 {0} 남음": "残り約 {0}",
|
||||
"약 {0}~{1}분": "約 {0}~{1} 分",
|
||||
|
|
@ -2016,6 +2055,7 @@
|
|||
"원본 옆에 \"이름_x2\" (ZIP 은 ZIP 옆 \"ZIP이름_AI\" 폴더)": "元のファイルの横に「名前_x2」(ZIP は ZIP の横の「ZIP名_AI」フォルダー)",
|
||||
"원본 옆으로": "元のファイルの横へ",
|
||||
"원본 유지 (재인코딩 없음)": "元のまま(再エンコードなし)",
|
||||
"원본 지우기 (이동)": "元のファイルを削除 (移動)",
|
||||
"원본 크기": "元のサイズ",
|
||||
"원본 파일 지우기 (--remove-source-files)": "元ファイルを削除 (--remove-source-files)",
|
||||
"원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다.": "元のファイルを削除できませんでした (権限がないか使用中)。移動先にはコピーされ、元のファイルは残っています。",
|
||||
|
|
@ -2159,10 +2199,13 @@
|
|||
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "この場所 (ネットワークドライブ・ネットワーク共有・USB メモリなど) にはごみ箱がないため、すぐに削除されます。",
|
||||
"이 이동 버튼 지우기": "この移動ボタンを削除",
|
||||
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "この移動を次に行うときにこのオプションを使います。今すぐ新しいオプションで実行しますか?",
|
||||
"이 종류는 앱 안에서 볼 수 없습니다 - [꺼내기] 로 꺼내서 보세요": "この種類はアプリ内で表示できません - [取り出す] で取り出して見てください",
|
||||
"이 쪽을 그리지 못했습니다": "このページを描画できませんでした",
|
||||
"이 파일에 맞는 자막 우선 (영상 해시)": "このファイルに合う字幕を優先(動画ハッシュ)",
|
||||
"이 파일용": "このファイル用",
|
||||
"이 파일은 바뀌었거나 깨져 풀 수 없습니다": "このファイルは変更されたか壊れているため復号できません",
|
||||
"이 파일을 열 수 있는 앱이 없습니다.": "このファイルを開けるアプリがありません。",
|
||||
"이 판이 모르는 보관함 형식입니다 - 앱을 새 판으로 올려 주세요 (보관함은 그대로 둡니다)": "このバージョンが知らない保管庫の形式です - アプリを新しいバージョンに更新してください (保管庫はそのままです)",
|
||||
"이 페이지 번역": "このページを翻訳",
|
||||
"이 페이지 지우기": "このページを削除",
|
||||
"이 페이지를 {0}(으)로 번역": "このページを{0}に翻訳",
|
||||
|
|
@ -2175,6 +2218,7 @@
|
|||
"이 폴더를 rsync 원본 · 대상으로 고르기": "このフォルダーを rsync の元 · 対象に選ぶ",
|
||||
"이 폴더를 볼 권한이 없습니다": "このフォルダーを表示する権限がありません",
|
||||
"이 폴더를 읽을 권한이 없습니다": "このフォルダーを読む権限がありません",
|
||||
"이 폴더를 지우면 보관함도 사라집니다. 폴더를 통째로 복사하면 그대로 백업이 됩니다 (비밀번호로 열림)": "このフォルダーを削除すると保管庫も消えます。フォルダーごとコピーすればそのままバックアップになります (パスワードで開けます)",
|
||||
"이 폴더에 동영상이 없습니다.": "このフォルダーに動画がありません。",
|
||||
"이 폴더에 빈 .nomedia 파일을 만듭니다. 갤러리 · 음악 앱이 이 폴더 (안의 폴더 포함) 의 그림 · 동영상 · 음악을 보이지 않게 됩니다.": "このフォルダーに空の .nomedia ファイルを作ります。ギャラリー・音楽アプリはこのフォルダー (サブフォルダーを含む) の画像・動画・音楽を表示しなくなります。",
|
||||
"이 폴더에 새 폴더": "このフォルダーに新しいフォルダー",
|
||||
|
|
@ -2300,6 +2344,7 @@
|
|||
"자동: 그래픽카드 (GPU) 가 있으면 사진용, CPU 뿐이면 가볍고 빠른 만화용. 원본은 덮어쓰지 않고 새 파일로 저장합니다": "自動: グラフィックカード (GPU) があれば写真用、CPU だけなら軽く速いマンガ用。元のファイルは上書きせず新しいファイルで保存します",
|
||||
"자동: 잰 결과 {0} 가 가장 빠릅니다": "自動: 計測の結果 {0} が最速です",
|
||||
"자동: 처음 그릴 때 짧게 재서 가장 빠른 장치를 고릅니다": "自動: 初回の描画時に短く計測して最速の装置を選びます",
|
||||
"자리: {0}": "場所: {0}",
|
||||
"자릿수": "桁数",
|
||||
"자막": "字幕",
|
||||
"자막 (AI · 인터넷)": "字幕(AI・インターネット)",
|
||||
|
|
@ -2362,6 +2407,7 @@
|
|||
"잘못된 간격: {0}": "不正な間隔: {0}",
|
||||
"잘못된 범위: {0}": "不正な範囲: {0}",
|
||||
"잘못된 칸: {0}": "不正な項目: {0}",
|
||||
"잠그기": "ロック",
|
||||
"재개": "再開",
|
||||
"재생": "再生",
|
||||
"재생 목록": "再生リスト",
|
||||
|
|
@ -2772,6 +2818,7 @@
|
|||
"파일 폴더": "ファイル フォルダー",
|
||||
"파일 확인 중…": "ファイルを確認中…",
|
||||
"파일만 골랐을 때": "ファイルだけを選んだとき",
|
||||
"파일을 AES-256 으로 암호화해 보관합니다 (이름까지 숨김). 보관함 비밀번호를 잊으면 되살릴 수 없습니다": "ファイルを AES-256 で暗号化して保管します (名前も隠します)。保管庫のパスワードを忘れると復元できません",
|
||||
"파일을 복사하지 못했습니다": "ファイルをコピーできませんでした",
|
||||
"파일을 분석할 수 없습니다: {0}": "ファイルを分析できません: {0}",
|
||||
"파일을 읽을 수 없습니다": "ファイルを読み込めません",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
"\"{0}\" 에 처음 연결합니다": "首次连接\"{0}\"",
|
||||
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "这些项目不在“{0}”中,只在目标“{1}”中。包含删除同步后会从目标中删除 (无法撤销)。",
|
||||
"\"{0}\" 은(는) 비밀번호가 걸려 있습니다.": "“{0}”设有密码。",
|
||||
"\"{0}\" 을 보관함에서 지웁니다. 되돌릴 수 없습니다 (먼저 [꺼내기] 를 하지 않았으면 내용이 사라집니다)": "将从保管库中删除“{0}”。此操作无法撤销 (若未先 [导出],内容将丢失)",
|
||||
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "将“{0}”移到“{1}”文件夹吗?",
|
||||
"\"{0}\" 을(를) 지울까요?": "要删除“{0}”吗?",
|
||||
"\"{0}\" 을(를) 지울까요? 되돌릴 수 없습니다.": "删除\"{0}\"吗? 此操作无法撤销。",
|
||||
|
|
@ -85,6 +86,7 @@
|
|||
"16진": "十六进制",
|
||||
"16진 보기 · {0}": "十六进制查看 · {0}",
|
||||
"1배 (선명하게)": "1倍 (锐化)",
|
||||
"1분 (기본)": "1 分钟 (默认)",
|
||||
"1분 안": "1 分钟以内",
|
||||
"2/2 · 해상도 올리는 중": "2/2 · 正在提高分辨率",
|
||||
"5초 뒤로": "后退 5 秒",
|
||||
|
|
@ -576,6 +578,7 @@
|
|||
"{0}개 맞춤": "已同步 {0} 个",
|
||||
"{0}개 맞춤 · 같은 이름의 폴더가 있어 건너뜀 {1}개": "已同步 {0} 项 · 因同名文件夹跳过 {1} 项",
|
||||
"{0}개 목록에서 숨기기": "从列表中隐藏 {0} 项",
|
||||
"{0}개 보관함에 넣기": "将 {0} 个放入保管库",
|
||||
"{0}개 보임": "显示 {0} 项",
|
||||
"{0}개 삭제": "删除 {0} 项",
|
||||
"{0}개 선택": "已选 {0} 个",
|
||||
|
|
@ -604,6 +607,7 @@
|
|||
"{0}개는 휴지통에 들어가지 않아 영구 삭제했습니다 (Windows 가 묻고 고른 대로).": "{0} 个项目无法放入回收站,已永久删除 (按 Windows 询问时的选择)。",
|
||||
"{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "已将 {0} 个导入“其他书签”。",
|
||||
"{0}개를 묶었습니다: {1}": "已压缩 {0} 个文件: {1}",
|
||||
"{0}개를 보관함에 넣었습니다": "已将 {0} 个放入保管库",
|
||||
"{0}개를 영구 삭제할까요?": "要永久删除 {0} 项吗?",
|
||||
"{0}개를 영구 삭제했습니다.": "已永久删除 {0} 个项目。",
|
||||
"{0}개를 옮겼습니다 → {1}": "已移动 {0} 个 → {1}",
|
||||
|
|
@ -874,6 +878,9 @@
|
|||
"길게 누르면 {0}": "长按: {0}",
|
||||
"길이": "时长",
|
||||
"길이 {0}": "时长 {0}",
|
||||
"꺼내기": "导出",
|
||||
"꺼내지 못했습니다: {0}": "无法导出: {0}",
|
||||
"꺼냈습니다: {0}": "已导出: {0}",
|
||||
"끄기": "关闭",
|
||||
"끄면 (기본) Tor 화면을 떠날 때 (다른 화면 · 앱이 뒤로) 지웁니다. 일반 브라우저와는 늘 따로입니다.": "关闭时(默认)在离开 Tor 页面时(切换到其他页面或应用转到后台)清除。始终与普通浏览器分开。",
|
||||
"끄면 더 안전하지만 많은 사이트가 보이지 않습니다 (다음에 열 때부터).": "关闭更安全,但很多网站将无法显示 (从下次打开起)。",
|
||||
|
|
@ -949,7 +956,10 @@
|
|||
"넘기는 방향: 오른쪽 → 왼쪽 (만화)": "翻页方向: 右 → 左 (漫画)",
|
||||
"넘기는 방향: 왼쪽 → 오른쪽": "翻页方向: 左 → 右",
|
||||
"넘으면 오래 보지 않은 것부터 지웁니다": "超出时先删除最久未查看的",
|
||||
"넣기": "放入",
|
||||
"넣기는 파일 탐색기에서 파일을 길게 눌러 [보관함에 넣기]": "要放入,请在文件管理器中长按文件并选择 [放入保管库]",
|
||||
"넣으면 AES-256 으로 암호화합니다. 비우면 보통 ZIP": "填写后使用 AES-256 加密。留空则为普通 ZIP",
|
||||
"넣은 날": "放入日期",
|
||||
"넣지 못했습니다: {0}": "无法加入: {0}",
|
||||
"네덜란드어": "荷兰语",
|
||||
"네트워크 · DNS": "网络 · DNS",
|
||||
|
|
@ -973,6 +983,7 @@
|
|||
"늘 받아서 열기": "始终下载后打开",
|
||||
"늘 이름 바꾸기": "总是重命名",
|
||||
"늘 주소로 열기": "始终按地址打开",
|
||||
"다 넣고 확인한 뒤에 탐색기의 지우기 (확인 · 휴지통) 로 지웁니다. 끄면 원본은 그대로 (복사)": "全部放入并确认后,通过文件管理器的删除 (确认 · 回收站) 删除原文件。关闭时保留原文件 (复制)",
|
||||
"다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)": "下载完成后立即放入制作 MKV 的视频列表,并从下载列表移除(与下载界面的复选框相同)",
|
||||
"다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)": "下载完成后自动加入制作 MKV 的视频列表,并从此下载列表移除(已下载的文件保留)",
|
||||
"다 받은 {0} 은 이미 MKV 목록에 있어 다운로드 목록에서 뺐습니다 (받은 파일은 그대로)": "下载完成的 {0} 已在 MKV 列表中,已从下载列表移除(文件保留)",
|
||||
|
|
@ -983,6 +994,7 @@
|
|||
"다른 버전 ({0}) 을 쓰다가 v{1} 로 돌아왔습니다. 그 버전은 이 버전의 설정 일부를 모를 수 있습니다.\nv{1} 를 마지막으로 쓸 때 보관해 둔 설정 (동영상 목록 · 즐겨찾기 포함) 으로 되돌릴까요?": "使用其他版本 ({0}) 后回到了 v{1}。该版本可能不认识此版本的部分设置。\n要恢复最后一次使用 v{1} 时保存的设置 (包括视频列表 · 书签) 吗?",
|
||||
"다른 버전 고르기": "选择其他版本",
|
||||
"다른 브라우저의 즐겨찾기 가져오기": "导入其他浏览器的书签",
|
||||
"다른 앱도 이 폴더의 암호화된 파일을 볼 수 있습니다 (내용 · 이름은 보이지 않습니다)": "其他应用也能看到此文件夹中的加密文件 (看不到内容和名称)",
|
||||
"다른 앱에는 아이디 · 비밀번호를 넘기지 않습니다.\n\n• 받아서 열기: 임시 폴더로 받은 뒤 엽니다. 어느 앱에서나 열리지만 다 받을 때까지 기다려야 합니다.\n• 주소로 열기: 받지 않고 바로 재생합니다. 그 앱이 아이디 · 비밀번호를 물으면 넣어 주세요 (VLC 등). 묻지 않는 앱에서는 열리지 않습니다.": "不会把用户名和密码传给其他应用。\n\n• 下载后打开: 先下载到临时文件夹再打开。任何应用都能打开,但需要等待下载完成。\n• 按地址打开: 不下载直接播放。如果应用询问用户名和密码,请输入 (VLC 等)。不询问的应用无法打开。",
|
||||
"다른 앱에는 아이디 · 비밀번호를 넘기지 않습니다. 받아서 열기는 어느 앱에서나 열리고, 주소로 열기는 아이디를 묻는 앱 (VLC 등) 만 열립니다": "不会把用户名和密码传给其他应用。下载后打开适用于任何应用,按地址打开仅适用于会询问登录信息的应用 (VLC 等)",
|
||||
"다른 앱에서 동영상을 열 때": "从其他应用打开视频时",
|
||||
|
|
@ -1198,6 +1210,7 @@
|
|||
"마스터 비밀번호 정하기": "设置主密码",
|
||||
"마스터 비밀번호가 필요합니다": "需要主密码",
|
||||
"마스터 비밀번호가 필요해 기다리는 중": "需要主密码,正在等待",
|
||||
"마스터 비밀번호로 열기": "用主密码打开",
|
||||
"마스터 비밀번호로 잠겨 있습니다": "已用主密码锁定",
|
||||
"마스터 비밀번호를 끌까요?": "要关闭主密码吗?",
|
||||
"마스터 비밀번호를 넣어야 저장된 비밀번호를 쓸 수 있습니다": "需要输入主密码才能使用已保存的密码",
|
||||
|
|
@ -1205,6 +1218,8 @@
|
|||
"마스터 비밀번호를 묻는 때": "何时询问主密码",
|
||||
"마스터 비밀번호를 초기화했습니다": "已重置主密码",
|
||||
"마스터 비밀번호와 같게 정할 수 없습니다": "不能与主密码相同",
|
||||
"마스터로 여는 열쇠가 없습니다 (마스터를 초기화했으면 없어집니다) - 보관함 비밀번호로 여세요": "没有用于主密码打开的密钥 (重置主密码后会被删除) - 请使用保管库密码打开",
|
||||
"마스터로도 열기": "也可用主密码打开",
|
||||
"마지막": "上次",
|
||||
"마지막 인코딩 설정을 기억합니다": "记住上次的编码设置",
|
||||
"마지막 장이라 계속 보기를 멈췄습니다.": "已到最后一页,连续浏览已停止。",
|
||||
|
|
@ -1245,6 +1260,7 @@
|
|||
"매번 묻기 (기본)": "每次询问 (默认)",
|
||||
"매일 종일": "每天 全天",
|
||||
"먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "先通过 mDNS 询问 (会公布名称的 NAS),再仅尝试与本设备所在网络 (Wi-Fi · 有线) 中的设备建立 SMB (445) 连接并立即关闭。不会发送文件或账户信息。",
|
||||
"먼저 환경 설정 > 보안 에서 마스터 비밀번호를 정하세요": "请先在 设置 > 安全 中设置主密码",
|
||||
"멈추고 옮기기": "停止并移动",
|
||||
"멈춤": "已停止",
|
||||
"멈춤 (시작 안 됨)": "停滞 (未开始)",
|
||||
|
|
@ -1357,6 +1373,7 @@
|
|||
"바뀐 때: {0}": "修改时间: {0}",
|
||||
"바뀐 지 … 일 안": "… 天内修改",
|
||||
"바뀐 지 … 일 지남": "超过 … 天未修改",
|
||||
"바로": "立即",
|
||||
"바로 끔": "立即关闭",
|
||||
"바로 시작 (기본)": "立即开始 (默认)",
|
||||
"바로 시작: 켜진 동기화를 모두 시작 · 골라서 시작: 켤 때 고르기 · 시작 안 함: 모니터링 > lsync 의 시작 버튼으로": "立即开始: 开始所有已启用的同步 · 选择后开始: 启动时选择 · 不开始: 用 监控 > lsync 的开始按钮",
|
||||
|
|
@ -1450,6 +1467,20 @@
|
|||
"변환": "转换",
|
||||
"변환기 안에서 오류가 났습니다": "转换器内部出错",
|
||||
"보관한 설정으로 되살리고 다시 시작": "用保存的设置恢复并重新启动",
|
||||
"보관함 비밀번호": "保管库密码",
|
||||
"보관함 비밀번호 바꾸기": "更改保管库密码",
|
||||
"보관함 비밀번호를 바꿨습니다": "已更改保管库密码",
|
||||
"보관함 열기": "打开保管库",
|
||||
"보관함 열쇠 한 벌을 이 기기의 안전 저장소에 두고, 마스터 비밀번호로도 열 수 있게 합니다. 보관함 비밀번호를 잊어도 열 수 있지만, 마스터 비밀번호를 아는 사람도 열 수 있게 됩니다. 마스터를 초기화하면 이 길만 없어지고 보관함 비밀번호는 그대로입니다.": "在本设备的安全存储中保存一份保管库密钥,使主密码也能打开保管库。即使忘记保管库密码也能打开,但知道主密码的人也能打开。重置主密码只会移除此方式,保管库密码保持不变。",
|
||||
"보관함 자동 잠금": "保管库自动锁定",
|
||||
"보관함 폴더를 열 수 없습니다: {0}": "无法打开保管库文件夹: {0}",
|
||||
"보관함에 넣기": "放入保管库",
|
||||
"보관함에 넣기 · {0}개": "放入保管库 · {0} 个",
|
||||
"보관함에서 지우기": "从保管库删除",
|
||||
"보관함을 만들지 못했습니다: {0}": "无法创建保管库: {0}",
|
||||
"보관함을 여는 중… (비밀번호 확인)": "正在打开保管库… (验证密码)",
|
||||
"보관함을 열지 못했습니다: {0}": "无法打开保管库: {0}",
|
||||
"보관함이 비어 있습니다": "保管库为空",
|
||||
"보관해 둔 설정을 되살렸습니다: {0}": "已恢复保存的设置: {0}",
|
||||
"보기": "查看",
|
||||
"보기 {0}초 · Ctrl+휠 확대/축소": "显示 {0} 秒 · Ctrl+滚轮缩放",
|
||||
|
|
@ -1536,6 +1567,7 @@
|
|||
"비밀번호가 걸린 압축은 안을 미리 볼 수 없습니다 - [압축 풀기] 로 푸세요": "无法预览有密码的压缩包 - 请用 [解压]",
|
||||
"비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)": "部分文件设有密码 (解压时会询问)",
|
||||
"비밀번호가 걸린 파일입니다 - [압축 풀기] 로 푸세요": "此文件有密码 - 请用 [解压]",
|
||||
"비밀번호가 여러 번 틀려 30초 뒤에 다시 넣을 수 있습니다": "密码错误次数过多,30 秒后可再次输入",
|
||||
"비밀번호가 저장된 기능을 쓸 때": "使用保存了密码的功能时",
|
||||
"비밀번호가 틀렸습니다": "密码错误",
|
||||
"비밀번호가 틀렸습니다 ({0}번)": "密码错误 ({0} 次)",
|
||||
|
|
@ -1543,6 +1575,8 @@
|
|||
"비밀번호를 넣으세요": "请输入密码",
|
||||
"비밀번호를 안전 저장소에 저장했습니다": "已将密码保存到安全存储",
|
||||
"비밀번호를 잊었습니다": "忘记密码",
|
||||
"비밀번호를 잊으면 되살릴 수 없습니다 (뒷문 없음)": "忘记密码将无法恢复 (没有后门)",
|
||||
"비밀번호를 잊으면 되살릴 수 없음을 압니다": "我了解忘记密码将无法恢复",
|
||||
"비밀번호를 잊으면 이 ZIP 을 풀 수 없습니다 (앱이 기억하지 않습니다). 파일 이름 목록은 암호화되지 않습니다.": "忘记密码将无法解压此 ZIP (应用不会记住密码)。文件名列表不会加密。",
|
||||
"비밀번호에 한글 등 영문 밖 글자가 있으면 512MB 넘는 파일 ({0}) 은 묶을 수 없습니다. 영문 · 숫자 · 기호 비밀번호를 쓰거나 파일을 나눠 주세요.": "密码含有英文以外的字符 (如韩文) 时,无法压缩超过 512MB 的文件 ({0})。请使用英文字母、数字和符号组成的密码,或拆分文件。",
|
||||
"비어 있는 압축 파일입니다.": "压缩文件为空。",
|
||||
|
|
@ -1595,6 +1629,7 @@
|
|||
"새 버전을 확인하지 못했습니다": "未能检查新版本。",
|
||||
"새 버전을 확인할 수 없습니다: {0}": "无法检查新版本: {0}",
|
||||
"새 버전이 예전 기본값을 바꿨으면 켤 때 한 번 무엇이 바뀌었는지 알려 줍니다": "如果新版本更改了旧的默认值,启动时提示一次更改了什么",
|
||||
"새 보관함 만들기": "创建新保管库",
|
||||
"새 비밀번호": "新密码",
|
||||
"새 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 예전 비밀번호로 돌아갑니다.": "新密码尚未保存。关闭应用后会恢复为旧密码。",
|
||||
"새 즐겨찾기 추가": "添加新书签",
|
||||
|
|
@ -1825,6 +1860,9 @@
|
|||
"알리지 않음": "不提醒",
|
||||
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知以 JJ_MKVMaker 名义显示,点击即可打开应用 (仅当前用户,无需管理员权限)",
|
||||
"암호 ZIP 으로 묶는 중 · {0}": "正在创建加密 ZIP · {0}",
|
||||
"암호 보관함": "加密保管库",
|
||||
"암호 보관함은 초기화되지 않습니다 - 보관함 파일은 그대로 남고 보관함 비밀번호로만 열 수 있습니다. [마스터로도 열기] 를 켰다면 마스터를 초기화할 때 그 길만 없어집니다": "加密保管库不会被重置 - 保管库文件保留,只能用保管库密码打开。若开启了 [也可用主密码打开],重置主密码时只会移除该方式",
|
||||
"암호 보관함은 환경 설정 > 보안 에서 켭니다 (처음 꺼짐)": "请在 设置 > 安全 中开启加密保管库 (默认关闭)",
|
||||
"암호가 걸린 문서입니다": "这是受密码保护的文档",
|
||||
"암호가 맞지 않습니다": "密码不正确",
|
||||
"암호로 묶기": "加密压缩",
|
||||
|
|
@ -1874,6 +1912,7 @@
|
|||
"앱을 닫아 진행 중이던 작업이 멈췄습니다. 닫아도 계속하려면 환경 설정에서 '백그라운드로 실행' 을 켜세요.": "关闭应用后,正在进行的任务已停止。如需关闭后继续,请在设置中开启“在后台运行”。",
|
||||
"앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)": "应用为全新安装,没有设置。要恢复保存的 v{0} 设置 (包括视频列表 · 书签) 吗?\n(字幕 API 密钥 · 密码未保存,请重新输入)",
|
||||
"앱을 지우면 다음은 되살릴 수 없습니다:": "删除应用后以下内容无法恢复:",
|
||||
"앱이 뒤로 간 뒤 이만큼 지나 돌아오면 다시 비밀번호를 묻습니다": "应用切到后台超过此时间后返回时,会再次要求输入密码",
|
||||
"앱이 켜져 있는 동안 늘 동기화합니다.": "应用运行期间始终同步。",
|
||||
"약 {0} 남음": "约剩 {0}",
|
||||
"약 {0}~{1}분": "约 {0}~{1} 分钟",
|
||||
|
|
@ -2016,6 +2055,7 @@
|
|||
"원본 옆에 \"이름_x2\" (ZIP 은 ZIP 옆 \"ZIP이름_AI\" 폴더)": "在原文件旁保存为“名称_x2” (ZIP 则保存在 ZIP 旁的“ZIP名称_AI”文件夹)",
|
||||
"원본 옆으로": "保存在原文件旁",
|
||||
"원본 유지 (재인코딩 없음)": "保持原样(不重新编码)",
|
||||
"원본 지우기 (이동)": "删除原文件 (移动)",
|
||||
"원본 크기": "原始尺寸",
|
||||
"원본 파일 지우기 (--remove-source-files)": "删除源文件 (--remove-source-files)",
|
||||
"원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다.": "无法删除源文件(没有权限或正在使用)。已复制到目标,源文件仍保留。",
|
||||
|
|
@ -2159,10 +2199,13 @@
|
|||
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "此位置 (网络驱动器 · 网络共享 · U 盘等) 没有回收站,会直接删除。",
|
||||
"이 이동 버튼 지우기": "删除此移动按钮",
|
||||
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "下次执行此移动时将使用这些选项。现在用新选项执行吗?",
|
||||
"이 종류는 앱 안에서 볼 수 없습니다 - [꺼내기] 로 꺼내서 보세요": "此类型无法在应用内查看 - 请用 [导出] 导出后查看",
|
||||
"이 쪽을 그리지 못했습니다": "无法渲染此页",
|
||||
"이 파일에 맞는 자막 우선 (영상 해시)": "优先匹配此文件的字幕(视频哈希)",
|
||||
"이 파일용": "适用于此文件",
|
||||
"이 파일은 바뀌었거나 깨져 풀 수 없습니다": "此文件已被更改或损坏,无法解密",
|
||||
"이 파일을 열 수 있는 앱이 없습니다.": "没有可以打开此文件的应用。",
|
||||
"이 판이 모르는 보관함 형식입니다 - 앱을 새 판으로 올려 주세요 (보관함은 그대로 둡니다)": "此版本不认识该保管库格式 - 请将应用更新到新版本 (保管库保持不变)",
|
||||
"이 페이지 번역": "翻译此页",
|
||||
"이 페이지 지우기": "删除此页",
|
||||
"이 페이지를 {0}(으)로 번역": "将此页面翻译为{0}",
|
||||
|
|
@ -2175,6 +2218,7 @@
|
|||
"이 폴더를 rsync 원본 · 대상으로 고르기": "选择此文件夹作为 rsync 源 · 目标",
|
||||
"이 폴더를 볼 권한이 없습니다": "没有查看此文件夹的权限",
|
||||
"이 폴더를 읽을 권한이 없습니다": "没有读取此文件夹的权限",
|
||||
"이 폴더를 지우면 보관함도 사라집니다. 폴더를 통째로 복사하면 그대로 백업이 됩니다 (비밀번호로 열림)": "删除此文件夹将同时删除保管库。整个文件夹复制即为完整备份 (可用密码打开)",
|
||||
"이 폴더에 동영상이 없습니다.": "此文件夹中没有视频。",
|
||||
"이 폴더에 빈 .nomedia 파일을 만듭니다. 갤러리 · 음악 앱이 이 폴더 (안의 폴더 포함) 의 그림 · 동영상 · 음악을 보이지 않게 됩니다.": "在此文件夹中创建空的 .nomedia 文件。图库和音乐应用将不再显示此文件夹 (含子文件夹) 中的图片、视频和音乐。",
|
||||
"이 폴더에 새 폴더": "在此文件夹中新建文件夹",
|
||||
|
|
@ -2300,6 +2344,7 @@
|
|||
"자동: 그래픽카드 (GPU) 가 있으면 사진용, CPU 뿐이면 가볍고 빠른 만화용. 원본은 덮어쓰지 않고 새 파일로 저장합니다": "自动: 有显卡 (GPU) 时用照片模型,仅有 CPU 时用轻量快速的漫画模型。不会覆盖原文件,另存为新文件",
|
||||
"자동: 잰 결과 {0} 가 가장 빠릅니다": "自动: 测得 {0} 最快",
|
||||
"자동: 처음 그릴 때 짧게 재서 가장 빠른 장치를 고릅니다": "自动: 首次绘制时短暂测试并选择最快的处理器",
|
||||
"자리: {0}": "位置: {0}",
|
||||
"자릿수": "位数",
|
||||
"자막": "字幕",
|
||||
"자막 (AI · 인터넷)": "字幕(AI · 网络)",
|
||||
|
|
@ -2362,6 +2407,7 @@
|
|||
"잘못된 간격: {0}": "无效的间隔: {0}",
|
||||
"잘못된 범위: {0}": "无效的范围: {0}",
|
||||
"잘못된 칸: {0}": "无效的字段: {0}",
|
||||
"잠그기": "锁定",
|
||||
"재개": "继续",
|
||||
"재생": "播放",
|
||||
"재생 목록": "播放列表",
|
||||
|
|
@ -2772,6 +2818,7 @@
|
|||
"파일 폴더": "文件夹",
|
||||
"파일 확인 중…": "正在检查文件…",
|
||||
"파일만 골랐을 때": "只选择文件时",
|
||||
"파일을 AES-256 으로 암호화해 보관합니다 (이름까지 숨김). 보관함 비밀번호를 잊으면 되살릴 수 없습니다": "使用 AES-256 加密保存文件 (名称也隐藏)。忘记保管库密码将无法恢复",
|
||||
"파일을 복사하지 못했습니다": "无法复制文件",
|
||||
"파일을 분석할 수 없습니다: {0}": "无法分析文件: {0}",
|
||||
"파일을 읽을 수 없습니다": "无法读取文件",
|
||||
|
|
|
|||
|
|
@ -285,11 +285,16 @@ class MasterLock extends ChangeNotifier {
|
|||
return true;
|
||||
}
|
||||
|
||||
/// X-plore 비교 5-2 암호 보관함 [마스터로도 열기] 가 안전 저장소에 둔 보관함 열쇠 한 벌 (마스터를 초기화하면 이 길만 없앤다 - 관리자 결정 10/21)
|
||||
static const vaultCopyKey = 'vault.masterCopy';
|
||||
|
||||
Future<void> clearMaster() async {
|
||||
_master = null;
|
||||
_askStored = null;
|
||||
await secrets.delete(masterKey);
|
||||
await secrets.delete(askKey);
|
||||
// 마스터가 없어지면 [마스터로도 열기] 길도 없앤다 (보관함 비밀번호로는 그대로 열림)
|
||||
await secrets.delete(vaultCopyKey);
|
||||
unlocked = true;
|
||||
declined = false;
|
||||
notifyListeners();
|
||||
|
|
|
|||
|
|
@ -485,6 +485,16 @@ class AppSettings {
|
|||
/// 비어 있으면 (처음) 스타일대로 (83) - ui/explorer_keys.dart
|
||||
Map<String, String> explorerKeyOverrides = {};
|
||||
|
||||
/// X-plore 비교 5-2 암호 보관함 (사용자 결정 10/21, 관리자 결정): 처음 끔. 보관함 자체는 설정이 아니라 그 폴더 (vault.json · *.jjv) 에 있다 -
|
||||
/// 설정을 처음 상태로 돌려도 보관함은 그대로 (끄기만 됨)
|
||||
bool vaultEnabled = false;
|
||||
|
||||
/// [마스터로도 열기] - 보관함 열쇠 한 벌을 안전 저장소 (마스터 비밀번호로 막힘) 에 둔다. 처음 끔. 마스터를 초기화하면 그 한 벌은 지워진다
|
||||
bool vaultMasterCopy = false;
|
||||
|
||||
/// 앱이 뒤로 간 뒤 이만큼 (초) 지나면 다시 잠금 (처음 60)
|
||||
int vaultAutoLockSeconds = 60;
|
||||
|
||||
/// 84: 폴더 → "기준|desc" (예: "size|1"). 오래된 것부터 지워 300개까지
|
||||
Map<String, String> explorerFolderSorts = {};
|
||||
|
||||
|
|
@ -886,6 +896,9 @@ class AppSettings {
|
|||
'explorerRemoteThumbs': explorerRemoteThumbs,
|
||||
'explorerRemoteThumbCacheMb': explorerRemoteThumbCacheMb,
|
||||
'explorerKeyOverrides': explorerKeyOverrides,
|
||||
'vaultEnabled': vaultEnabled,
|
||||
'vaultMasterCopy': vaultMasterCopy,
|
||||
'vaultAutoLockSeconds': vaultAutoLockSeconds,
|
||||
'explorerFolderSorts': explorerFolderSorts,
|
||||
'explorerShowHidden': explorerShowHidden,
|
||||
'explorerHiddenItems': explorerHiddenItems,
|
||||
|
|
@ -1130,6 +1143,9 @@ class AppSettings {
|
|||
..explorerSortPerFolder = j['explorerSortPerFolder'] == true
|
||||
..explorerSortByKind = j['explorerSortByKind'] == true
|
||||
..explorerRemoteThumbs = const {'off', 'wifi', 'always'}.contains(j['explorerRemoteThumbs']) ? j['explorerRemoteThumbs'] as String : 'wifi'
|
||||
..vaultEnabled = j['vaultEnabled'] == true
|
||||
..vaultMasterCopy = j['vaultMasterCopy'] == true
|
||||
..vaultAutoLockSeconds = j['vaultAutoLockSeconds'] is num ? (j['vaultAutoLockSeconds'] as num).round().clamp(0, 3600) : 60
|
||||
..explorerKeyOverrides = j['explorerKeyOverrides'] is Map
|
||||
? {for (final e in (j['explorerKeyOverrides'] as Map).entries) if (e.key is String && e.value is String) e.key as String: e.value as String}
|
||||
: {}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import '../l10n/tr.dart';
|
|||
import 'explorer_bookmarks.dart';
|
||||
import 'explorer_search.dart';
|
||||
import 'remote_thumb_view.dart';
|
||||
import 'vault_page.dart' show openVault;
|
||||
import 'app_manager_page.dart' show openAppManager;
|
||||
import 'dlna_page.dart' show openDlna;
|
||||
import 'explorer_keys.dart' show defaultExplorerKeys, effectiveExplorerKeys;
|
||||
|
|
@ -204,7 +205,9 @@ enum ExplorerButton {
|
|||
// X-plore 비교 e: 휴지통 (Android 앱 휴지통 · Windows 는 시스템 휴지통 열기)
|
||||
trash(Icons.restore_from_trash_outlined, '휴지통'),
|
||||
// X-plore 비교 5-2 (사용자 결정 10/21): 앱 관리자 (Android 만)
|
||||
apps(Icons.apps, '앱 관리자');
|
||||
apps(Icons.apps, '앱 관리자'),
|
||||
// X-plore 비교 5-2 (사용자 결정 10/21): 암호 보관함 (환경 설정 > 보안 에서 켰을 때 열림)
|
||||
vault(Icons.enhanced_encryption_outlined, '암호 보관함');
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
|
@ -2104,6 +2107,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
if (!e.isDir && !video && !audio) ('text', Icons.article_outlined, tr('글 뷰어로 보기')),
|
||||
// X-plore 비교 5-1 ④ (관리자 지시 10/21): SQLite (읽기 전용)
|
||||
if (!e.isDir && !inArchive && sqliteExtensions.contains(ext)) ('sqlite', Icons.table_chart_outlined, tr('SQLite 보기 (읽기 전용)')),
|
||||
// X-plore 비교 5-2 (사용자 결정 10/21): 암호 보관함에 넣기 - 이 기기의 파일만 (폴더째 넣기는 2단계)
|
||||
if (c.settings.vaultEnabled && !dav && !inArchive && targets.every((x) => !vIsDirSync(x)))
|
||||
('vaultAdd', Icons.enhanced_encryption_outlined, many ? trf('{0}개 보관함에 넣기', [targets.length]) : tr('보관함에 넣기')),
|
||||
if (!e.isDir) ('openWith', Icons.open_in_new, tr('다른 앱으로 열기')),
|
||||
if (!e.isDir && !video) ('openDefault', Icons.launch, tr('기본 앱으로 열기')),
|
||||
if (video && !dav) ('addMkv', Icons.playlist_add, tr('MKV 목록에 추가')),
|
||||
|
|
@ -2189,6 +2195,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
await _viewText(e.path);
|
||||
case 'sqlite':
|
||||
await openSqliteViewer(context, c, e.path);
|
||||
case 'vaultAdd':
|
||||
await openVault(context, c, add: targets, deleteOriginals: (paths) => _delete(pane, paths));
|
||||
await _refreshAll([vDirname(e.path)]);
|
||||
case 'openWith':
|
||||
final local = await _fetch(e.path);
|
||||
if (local != null && !await c.services.shell.openWith(local, choose: true)) _snack(_noAppMessage(e.path));
|
||||
|
|
@ -2672,6 +2681,12 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
await _wifiShare(pane.current);
|
||||
case ExplorerButton.apps:
|
||||
await openAppManager(context);
|
||||
case ExplorerButton.vault:
|
||||
if (!c.settings.vaultEnabled) {
|
||||
_snack(tr('암호 보관함은 환경 설정 > 보안 에서 켭니다 (처음 꺼짐)'));
|
||||
} else {
|
||||
await openVault(context, c);
|
||||
}
|
||||
case ExplorerButton.trash:
|
||||
await _openTrash();
|
||||
case ExplorerButton.sort:
|
||||
|
|
|
|||
|
|
@ -207,6 +207,13 @@ Future<void> showSuperReset(BuildContext context, AppController c, MasterLock lo
|
|||
// 145: 파일 탐색기 즐겨찾기 (73) 는 설정 안에 있어 함께 지워진다 - 예전 글은 "즐겨찾기는 그대로"
|
||||
subtitle: Text(tr('서버 목록 (WebDAV · SFTP · SMB · FTP) 과 그 비밀번호 · API 키, 파일 탐색기 즐겨찾기도 지워집니다. 동영상 목록 · 웹 즐겨찾기 · 파일은 그대로입니다')),
|
||||
),
|
||||
// X-plore 비교 5-2 (관리자 결정 10/21): 보관함은 초기화 대상이 아님 (풀 수 있는 열쇠가 없음)
|
||||
if (c.settings.vaultEnabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(tr('암호 보관함은 초기화되지 않습니다 - 보관함 파일은 그대로 남고 보관함 비밀번호로만 열 수 있습니다. [마스터로도 열기] 를 켰다면 마스터를 초기화할 때 그 길만 없어집니다'),
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import '../l10n/tr.dart';
|
|||
import 'confirm.dart';
|
||||
import 'master_prompt.dart';
|
||||
import 'setting_tile.dart';
|
||||
import 'vault_page.dart' show openVault;
|
||||
|
||||
/// 124: 환경 설정 > 보안 - 최상 비밀번호 (모두 초기화하는 열쇠) · 마스터 비밀번호 (저장된 비밀번호 · 설정 · 비밀번호가 필요한 기능)
|
||||
/// · 마스터를 묻는 때. 이미 정한 것을 바꾸거나 끄려면 지금 마스터 (또는 최상) 를 넣는다.
|
||||
|
|
@ -18,7 +19,7 @@ class SecuritySettings extends StatelessWidget {
|
|||
final lock = MasterLock.instance;
|
||||
if (lock == null) return const SizedBox();
|
||||
return ListenableBuilder(
|
||||
listenable: lock,
|
||||
listenable: Listenable.merge([lock, c]),
|
||||
builder: (context, _) {
|
||||
// 바꾸기 · 끄기 전에 지금 비밀번호 (마스터 또는 최상)
|
||||
Future<bool> current() => askMaster(context, c, lock,
|
||||
|
|
@ -119,6 +120,34 @@ class SecuritySettings extends StatelessWidget {
|
|||
]),
|
||||
]),
|
||||
),
|
||||
// X-plore 비교 5-2 암호 보관함 (사용자 결정 10/21, 처음 끔)
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.enhanced_encryption_outlined),
|
||||
title: Text(tr('암호 보관함')),
|
||||
subtitle: Text(tr('파일을 AES-256 으로 암호화해 보관합니다 (이름까지 숨김). 보관함 비밀번호를 잊으면 되살릴 수 없습니다')),
|
||||
trailing: Switch(value: c.settings.vaultEnabled, onChanged: (v) => c.updateSettings((x) => x.vaultEnabled = v)),
|
||||
),
|
||||
if (c.settings.vaultEnabled) ...[
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 56, right: 16),
|
||||
title: Text(tr('보관함 열기')),
|
||||
subtitle: Text(tr('넣기는 파일 탐색기에서 파일을 길게 눌러 [보관함에 넣기]')),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => openVault(context, c),
|
||||
),
|
||||
SettingTile(
|
||||
title: Text(tr('보관함 자동 잠금')),
|
||||
subtitle: Text(tr('앱이 뒤로 간 뒤 이만큼 지나 돌아오면 다시 비밀번호를 묻습니다')),
|
||||
trailing: DropdownButton<int>(
|
||||
value: c.settings.vaultAutoLockSeconds,
|
||||
items: [
|
||||
for (final v in {0, 30, 60, 300, 900, c.settings.vaultAutoLockSeconds}.toList()..sort())
|
||||
DropdownMenuItem(value: v, child: Text(v == 0 ? tr('바로') : v == 60 ? tr('1분 (기본)') : v < 60 ? trf('{0}초', [v]) : trf('{0}분', [v ~/ 60]))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.vaultAutoLockSeconds = v!),
|
||||
),
|
||||
),
|
||||
],
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,587 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../app/app_controller.dart';
|
||||
import '../app/master_lock.dart';
|
||||
import '../core/file_ops.dart' show formatSize;
|
||||
import '../core/playlist.dart' show isAudioFile, isVideoFile;
|
||||
import '../core/reader_sources.dart';
|
||||
import '../core/secret_gate.dart';
|
||||
import '../core/vault_crypto.dart';
|
||||
import '../core/vault_store.dart';
|
||||
import '../core/vfs.dart' show vDisplay;
|
||||
import '../l10n/tr.dart';
|
||||
import 'player_page.dart';
|
||||
import 'reader_page.dart' show openReader;
|
||||
import 'theme.dart';
|
||||
|
||||
/// X-plore 비교 5-2 암호 보관함 화면 (사용자 결정 10/21, 관리자 결정 - 설계안 위키 프로젝트/암호 보관함 · 루트 설계안 (10-21))
|
||||
Future<void> openVault(BuildContext context, AppController c,
|
||||
{List<String> add = const [], Future<void> Function(List<String> paths)? deleteOriginals}) =>
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => VaultPage(c: c, add: add, deleteOriginals: deleteOriginals)));
|
||||
|
||||
/// 보관함 폴더 (Android: Download/JJ_MKVMaker/vault · Windows: 문서\JJ_MKVMaker\vault). 시험에서 바꿈
|
||||
Future<Directory> Function() vaultDirOf = () async {
|
||||
if (Platform.isAndroid) {
|
||||
final root = await const MethodChannel('jj_mkvmaker/android').invokeMethod<String>('storageRoot') ?? '/storage/emulated/0';
|
||||
return Directory(p.join(root, 'Download', 'JJ_MKVMaker', 'vault'));
|
||||
}
|
||||
return Directory(p.join((await getApplicationDocumentsDirectory()).path, 'JJ_MKVMaker', 'vault'));
|
||||
};
|
||||
|
||||
/// 꺼낼 폴더 고르기 (시험에서 바꿈)
|
||||
Future<String?> Function() vaultPickFolder = () => FilePicker.getDirectoryPath();
|
||||
|
||||
/// 시험: PBKDF2 반복 수 (새 보관함 · 비밀번호 바꾸기)
|
||||
int vaultIterationsForTest = VaultCrypto.defaultIterations;
|
||||
|
||||
class VaultPage extends StatefulWidget {
|
||||
final AppController c;
|
||||
|
||||
/// 열면서 넣을 파일 (탐색기 [보관함에 넣기])
|
||||
final List<String> add;
|
||||
|
||||
/// [원본 지우기 (이동)] 일 때 탐색기의 지우기 (확인 · 휴지통)
|
||||
final Future<void> Function(List<String> paths)? deleteOriginals;
|
||||
const VaultPage({super.key, required this.c, this.add = const [], this.deleteOriginals});
|
||||
|
||||
@override
|
||||
State<VaultPage> createState() => _VaultPageState();
|
||||
}
|
||||
|
||||
enum _Stage { checking, create, locked, opening, open }
|
||||
|
||||
class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
||||
_Stage _stage = _Stage.checking;
|
||||
Directory? _dir;
|
||||
Vault? _vault;
|
||||
String? _error;
|
||||
int _fails = 0;
|
||||
DateTime? _waitUntil, _hiddenAt;
|
||||
final _pw = TextEditingController(), _pw2 = TextEditingController();
|
||||
bool _understood = false;
|
||||
bool _busy = false;
|
||||
late List<String> _pendingAdd = [...widget.add];
|
||||
String _sort = 'added';
|
||||
|
||||
AppController get c => widget.c;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
unawaited(_check());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_lock();
|
||||
_pw.dispose();
|
||||
_pw2.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 앱이 뒤로 간 뒤 정한 시간이 지나 돌아오면 다시 잠금
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.paused || state == AppLifecycleState.hidden) {
|
||||
_hiddenAt ??= DateTime.now();
|
||||
} else if (state == AppLifecycleState.resumed) {
|
||||
final at = _hiddenAt;
|
||||
_hiddenAt = null;
|
||||
if (at != null && _vault != null && DateTime.now().difference(at).inSeconds >= c.settings.vaultAutoLockSeconds) {
|
||||
setState(_lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _lock() {
|
||||
_vault = null;
|
||||
if (_stage == _Stage.open) _stage = _Stage.locked;
|
||||
unawaited(_clearViewDir());
|
||||
}
|
||||
|
||||
Future<Directory> _viewDir() async => Directory(p.join(await c.services.storage.tempDirectory(), 'jj_vault_view'));
|
||||
|
||||
Future<void> _clearViewDir() async {
|
||||
try {
|
||||
final d = await _viewDir();
|
||||
if (await d.exists()) await d.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _check() async {
|
||||
try {
|
||||
final d = await vaultDirOf();
|
||||
final has = await Vault.exists(d);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_dir = d;
|
||||
_stage = has ? _Stage.locked : _Stage.create;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _error = trf('보관함 폴더를 열 수 없습니다: {0}', [e]));
|
||||
}
|
||||
}
|
||||
|
||||
void _snack(String t) => ScaffoldMessenger.maybeOf(context)?.showSnackBar(SnackBar(content: Text(t)));
|
||||
|
||||
Future<void> _create() async {
|
||||
if (_pw.text.isEmpty || _pw.text != _pw2.text || !_understood) return;
|
||||
setState(() {
|
||||
_busy = true;
|
||||
_stage = _Stage.opening;
|
||||
});
|
||||
try {
|
||||
final v = await Vault.create(_dir!, _pw.text, iterations: vaultIterationsForTest);
|
||||
_pw.clear();
|
||||
_pw2.clear();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_vault = v;
|
||||
_stage = _Stage.open;
|
||||
});
|
||||
await _addPending();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_stage = _Stage.create;
|
||||
_error = trf('보관함을 만들지 못했습니다: {0}', [e]);
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _unlock() async {
|
||||
final wait = _waitUntil;
|
||||
if (wait != null && DateTime.now().isBefore(wait)) return;
|
||||
setState(() {
|
||||
_stage = _Stage.opening;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final v = await Vault.unlock(_dir!, _pw.text);
|
||||
_pw.clear();
|
||||
_fails = 0;
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_vault = v;
|
||||
_stage = _Stage.open;
|
||||
});
|
||||
await _addPending();
|
||||
} on VaultAuthException {
|
||||
_fails++;
|
||||
if (_fails >= 5) {
|
||||
_waitUntil = DateTime.now().add(const Duration(seconds: 30));
|
||||
_fails = 0;
|
||||
Timer(const Duration(seconds: 30), () => mounted ? setState(() => _waitUntil = null) : null);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_stage = _Stage.locked;
|
||||
_error = _waitUntil != null ? tr('비밀번호가 여러 번 틀려 30초 뒤에 다시 넣을 수 있습니다') : tr('비밀번호가 틀렸습니다');
|
||||
});
|
||||
}
|
||||
} on VaultFormatException {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_stage = _Stage.locked;
|
||||
_error = tr('이 판이 모르는 보관함 형식입니다 - 앱을 새 판으로 올려 주세요 (보관함은 그대로 둡니다)');
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_stage = _Stage.locked;
|
||||
_error = trf('보관함을 열지 못했습니다: {0}', [e]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [마스터로도 열기]: 마스터 비밀번호 확인 뒤 안전 저장소의 열쇠로
|
||||
Future<void> _unlockWithMaster() async {
|
||||
final lock = MasterLock.instance;
|
||||
if (lock == null || !await SecretGate.pass(force: true)) return;
|
||||
final key = (await lock.secrets.readAll())[MasterLock.vaultCopyKey];
|
||||
if (key == null) {
|
||||
_snack(tr('마스터로 여는 열쇠가 없습니다 (마스터를 초기화했으면 없어집니다) - 보관함 비밀번호로 여세요'));
|
||||
return;
|
||||
}
|
||||
setState(() => _stage = _Stage.opening);
|
||||
try {
|
||||
final v = await Vault.unlockWithKey(_dir!, base64.decode(key));
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_vault = v;
|
||||
_stage = _Stage.open;
|
||||
});
|
||||
await _addPending();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_stage = _Stage.locked;
|
||||
_error = trf('보관함을 열지 못했습니다: {0}', [e]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 탐색기에서 고른 것 넣기 - [원본 지우기 (이동)] 체크 (처음 끔)
|
||||
Future<void> _addPending() async {
|
||||
final paths = _pendingAdd;
|
||||
if (paths.isEmpty || _vault == null) return;
|
||||
_pendingAdd = [];
|
||||
var move = false;
|
||||
final go = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, set) => AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(trf('보관함에 넣기 · {0}개', [paths.length])),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
for (final x in paths.take(8)) Text('· ${p.basename(x)}', maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
if (paths.length > 8) Text(trf('… 외 {0}개', [paths.length - 8])),
|
||||
const SizedBox(height: 8),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: move,
|
||||
onChanged: widget.deleteOriginals == null ? null : (v) => set(() => move = v ?? false),
|
||||
title: Text(tr('원본 지우기 (이동)')),
|
||||
subtitle: Text(tr('다 넣고 확인한 뒤에 탐색기의 지우기 (확인 · 휴지통) 로 지웁니다. 끄면 원본은 그대로 (복사)')),
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('넣기'))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (go != true || !mounted) return;
|
||||
setState(() => _busy = true);
|
||||
final done = <String>[];
|
||||
try {
|
||||
for (final x in paths) {
|
||||
await _vault!.add(x);
|
||||
done.add(x);
|
||||
}
|
||||
} catch (e) {
|
||||
_snack(trf('넣지 못했습니다: {0}', [e]));
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
if (done.isNotEmpty) _snack(trf('{0}개를 보관함에 넣었습니다', [done.length]));
|
||||
if (move && done.isNotEmpty && widget.deleteOriginals != null) await widget.deleteOriginals!(done);
|
||||
}
|
||||
|
||||
Future<void> _view(VaultEntry e) async {
|
||||
try {
|
||||
final d = await _viewDir();
|
||||
await d.create(recursive: true);
|
||||
final out = p.join(d.path, 'v_${DateTime.now().microsecondsSinceEpoch}${p.extension(e.name)}');
|
||||
setState(() => _busy = true);
|
||||
await _vault!.export(e, out);
|
||||
if (mounted) setState(() => _busy = false);
|
||||
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;
|
||||
if (create != null) {
|
||||
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));
|
||||
} catch (err) {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
_snack(err is VaultAuthException ? tr('이 파일은 바뀌었거나 깨져 풀 수 없습니다') : trf('열 수 없습니다: {0}', [err]));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _export(VaultEntry e) async {
|
||||
final dir = await vaultPickFolder();
|
||||
if (dir == null || !mounted) return;
|
||||
var out = p.join(dir, e.name);
|
||||
for (var i = 2; await File(out).exists(); i++) {
|
||||
out = p.join(dir, '${p.basenameWithoutExtension(e.name)} ($i)${p.extension(e.name)}');
|
||||
}
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
await _vault!.export(e, out);
|
||||
_snack(trf('꺼냈습니다: {0}', [vDisplay(out)]));
|
||||
} catch (err) {
|
||||
_snack(err is VaultAuthException ? tr('이 파일은 바뀌었거나 깨져 풀 수 없습니다') : trf('꺼내지 못했습니다: {0}', [err]));
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _remove(VaultEntry e) async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(tr('보관함에서 지우기')),
|
||||
content: Text(trf('"{0}" 을 보관함에서 지웁니다. 되돌릴 수 없습니다 (먼저 [꺼내기] 를 하지 않았으면 내용이 사라집니다)', [e.name])),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('지우기'))),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true) return;
|
||||
await _vault!.remove(e);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _rename(VaultEntry e) async {
|
||||
final ctl = TextEditingController(text: e.name);
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(tr('이름 변경')),
|
||||
content: TextField(controller: ctl, autofocus: true),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('취소'))),
|
||||
FilledButton(onPressed: () => Navigator.pop(ctx, ctl.text.trim()), child: Text(tr('확인'))),
|
||||
],
|
||||
),
|
||||
);
|
||||
ctl.dispose();
|
||||
if (name == null || name.isEmpty || name == e.name) return;
|
||||
await _vault!.rename(e, name);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _changePassword() async {
|
||||
final a = TextEditingController(), b = TextEditingController();
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, set) => AlertDialog(
|
||||
title: Text(tr('보관함 비밀번호 바꾸기')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
TextField(controller: a, obscureText: true, decoration: InputDecoration(labelText: tr('새 비밀번호')), onChanged: (_) => set(() {})),
|
||||
TextField(controller: b, obscureText: true, decoration: InputDecoration(labelText: tr('한 번 더')), onChanged: (_) => set(() {})),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||
FilledButton(onPressed: a.text.isNotEmpty && a.text == b.text ? () => Navigator.pop(ctx, true) : null, child: Text(tr('바꾸기'))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (ok == true && mounted) {
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
await _vault!.changePassword(a.text, iterations: vaultIterationsForTest);
|
||||
_snack(tr('보관함 비밀번호를 바꿨습니다'));
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
a.dispose();
|
||||
b.dispose();
|
||||
}
|
||||
|
||||
/// [마스터로도 열기] 켜고 끄기 (마스터 비밀번호가 있어야 켬)
|
||||
Future<void> _toggleMasterCopy(bool on) async {
|
||||
final lock = MasterLock.instance;
|
||||
if (lock == null) return;
|
||||
if (on) {
|
||||
if (!lock.hasMaster) {
|
||||
_snack(tr('먼저 환경 설정 > 보안 에서 마스터 비밀번호를 정하세요'));
|
||||
return;
|
||||
}
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(tr('마스터로도 열기')),
|
||||
content: Text(tr('보관함 열쇠 한 벌을 이 기기의 안전 저장소에 두고, 마스터 비밀번호로도 열 수 있게 합니다. '
|
||||
'보관함 비밀번호를 잊어도 열 수 있지만, 마스터 비밀번호를 아는 사람도 열 수 있게 됩니다. '
|
||||
'마스터를 초기화하면 이 길만 없어지고 보관함 비밀번호는 그대로입니다.')),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('켜기'))),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true || !await SecretGate.pass(force: true)) return;
|
||||
await lock.secrets.write(MasterLock.vaultCopyKey, base64.encode(_vault!.keyForMasterCopy));
|
||||
} else {
|
||||
await lock.secrets.delete(MasterLock.vaultCopyKey);
|
||||
}
|
||||
await c.updateSettings((x) => x.vaultMasterCopy = on);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr('암호 보관함')),
|
||||
actions: [
|
||||
if (_stage == _Stage.open) ...[
|
||||
IconButton(tooltip: tr('잠그기'), icon: const Icon(Icons.lock_outline), onPressed: () => setState(_lock)),
|
||||
PopupMenuButton<String>(
|
||||
tooltip: tr('더 보기'),
|
||||
onSelected: (w) => switch (w) {
|
||||
'pw' => _changePassword(),
|
||||
'master' => _toggleMasterCopy(!c.settings.vaultMasterCopy),
|
||||
_ => null,
|
||||
},
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(value: 'pw', child: Text(tr('보관함 비밀번호 바꾸기'))),
|
||||
CheckedPopupMenuItem(value: 'master', checked: c.settings.vaultMasterCopy, child: Text(tr('마스터로도 열기'))),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
body: Stack(children: [
|
||||
switch (_stage) {
|
||||
_Stage.checking => const Center(child: CircularProgressIndicator()),
|
||||
_Stage.create => _createForm(),
|
||||
_Stage.locked => _unlockForm(),
|
||||
_Stage.opening => Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 12),
|
||||
Text(tr('보관함을 여는 중… (비밀번호 확인)')),
|
||||
]),
|
||||
),
|
||||
_Stage.open => _list(),
|
||||
},
|
||||
if (_busy && _stage == _Stage.open) const Positioned(left: 0, right: 0, top: 0, child: LinearProgressIndicator()),
|
||||
]),
|
||||
);
|
||||
|
||||
Widget _note(String t, {Color? color}) =>
|
||||
Padding(padding: const EdgeInsets.only(top: 6), child: Text(t, style: TextStyle(fontSize: 12.5, color: color ?? JjColors.textDim)));
|
||||
|
||||
Widget _createForm() => ListView(padding: const EdgeInsets.all(16), children: [
|
||||
Text(tr('새 보관함 만들기'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
_note(trf('자리: {0}', [vDisplay(_dir?.path ?? '')])),
|
||||
_note(tr('다른 앱도 이 폴더의 암호화된 파일을 볼 수 있습니다 (내용 · 이름은 보이지 않습니다)')),
|
||||
_note(tr('이 폴더를 지우면 보관함도 사라집니다. 폴더를 통째로 복사하면 그대로 백업이 됩니다 (비밀번호로 열림)')),
|
||||
_note(tr('비밀번호를 잊으면 되살릴 수 없습니다 (뒷문 없음)'), color: JjColors.danger),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _pw, obscureText: true, decoration: InputDecoration(labelText: tr('보관함 비밀번호')), onChanged: (_) => setState(() {})),
|
||||
TextField(controller: _pw2, obscureText: true, decoration: InputDecoration(labelText: tr('한 번 더')), onChanged: (_) => setState(() {})),
|
||||
if (_pw2.text.isNotEmpty && _pw.text != _pw2.text) _note(tr('두 번 넣은 비밀번호가 다릅니다'), color: JjColors.danger),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: _understood,
|
||||
onChanged: (v) => setState(() => _understood = v ?? false),
|
||||
title: Text(tr('비밀번호를 잊으면 되살릴 수 없음을 압니다')),
|
||||
),
|
||||
if (_error != null) _note(_error!, color: JjColors.danger),
|
||||
const SizedBox(height: 8),
|
||||
FilledButton(
|
||||
onPressed: !_busy && _understood && _pw.text.isNotEmpty && _pw.text == _pw2.text ? _create : null,
|
||||
child: Text(tr('만들기')),
|
||||
),
|
||||
]);
|
||||
|
||||
Widget _unlockForm() {
|
||||
final waiting = _waitUntil != null && DateTime.now().isBefore(_waitUntil!);
|
||||
return ListView(padding: const EdgeInsets.all(16), children: [
|
||||
const Icon(Icons.lock_outline, size: 48, color: JjColors.textDim),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _pw,
|
||||
obscureText: true,
|
||||
autofocus: true,
|
||||
enabled: !waiting,
|
||||
decoration: InputDecoration(labelText: tr('보관함 비밀번호')),
|
||||
onSubmitted: (_) => _unlock(),
|
||||
),
|
||||
if (_error != null) _note(_error!, color: JjColors.danger),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton(onPressed: waiting ? null : _unlock, child: Text(tr('열기'))),
|
||||
if (c.settings.vaultMasterCopy)
|
||||
TextButton(onPressed: waiting ? null : _unlockWithMaster, child: Text(tr('마스터 비밀번호로 열기'))),
|
||||
_note(trf('자리: {0}', [vDisplay(_dir?.path ?? '')])),
|
||||
]);
|
||||
}
|
||||
|
||||
String _day(DateTime d) => '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
|
||||
|
||||
Widget _list() {
|
||||
final list = [..._vault!.entries];
|
||||
if (_sort == 'name') {
|
||||
list.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
} else {
|
||||
list.sort((a, b) => b.added.compareTo(a.added));
|
||||
}
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Text(tr('넣기는 파일 탐색기에서 파일을 길게 눌러 [보관함에 넣기]'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
DropdownButton<String>(
|
||||
value: _sort,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'added', child: Text(tr('넣은 날'))),
|
||||
DropdownMenuItem(value: 'name', child: Text(tr('이름'))),
|
||||
],
|
||||
onChanged: (v) => setState(() => _sort = v!),
|
||||
),
|
||||
]),
|
||||
),
|
||||
if (list.isEmpty) Expanded(child: Center(child: Text(tr('보관함이 비어 있습니다')))),
|
||||
if (list.isNotEmpty)
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: list.length,
|
||||
itemBuilder: (_, i) {
|
||||
final e = list[i];
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.lock, color: JjColors.accent),
|
||||
title: Text(e.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text('${formatSize(e.size)} · ${_day(e.added)}'),
|
||||
onTap: _busy ? null : () => _view(e),
|
||||
trailing: PopupMenuButton<String>(
|
||||
tooltip: tr('더 보기'),
|
||||
enabled: !_busy,
|
||||
onSelected: (w) => switch (w) {
|
||||
'view' => _view(e),
|
||||
'export' => _export(e),
|
||||
'rename' => _rename(e),
|
||||
'remove' => _remove(e),
|
||||
_ => null,
|
||||
},
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(value: 'view', child: Text(tr('보기'))),
|
||||
PopupMenuItem(value: 'export', child: Text(tr('꺼내기'))),
|
||||
PopupMenuItem(value: 'rename', child: Text(tr('이름 변경'))),
|
||||
PopupMenuItem(value: 'remove', child: Text(tr('보관함에서 지우기'))),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/master_lock.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/services/secret_store.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/vault_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// X-plore 비교 5-2 암호 보관함 화면 (관리자 결정 10/21): 만들기 창의 알림 · 넣기 (복사 / 원본 지우기) · 잠금 · 틀린 비밀번호 ·
|
||||
/// 꺼내기 · 지우기 · [마스터로도 열기] (마스터를 초기화하면 그 길만 없어짐)
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
final was = vaultDirOf;
|
||||
final wasPick = vaultPickFolder;
|
||||
setUp(() {
|
||||
tmp = Directory.systemTemp.createTempSync('jj_vaultui_');
|
||||
vaultDirOf = () async => Directory(p.join(tmp.path, 'vault'));
|
||||
vaultPickFolder = () async => p.join(tmp.path, 'out');
|
||||
Directory(p.join(tmp.path, 'out')).createSync();
|
||||
vaultIterationsForTest = 1000;
|
||||
});
|
||||
tearDown(() {
|
||||
vaultDirOf = was;
|
||||
vaultPickFolder = wasPick;
|
||||
MasterLock.instance = null;
|
||||
tmp.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
AppController ctl() {
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.vaultEnabled = true;
|
||||
return c;
|
||||
}
|
||||
|
||||
Future<void> until(WidgetTester t, bool Function() ok, {int rounds = 200}) async {
|
||||
for (var i = 0; i < rounds && !ok(); i++) {
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
|
||||
await t.pump();
|
||||
}
|
||||
// 닫히는 창 · 메뉴의 막 (barrier) 이 사라질 때까지
|
||||
for (var i = 0; i < 8; i++) {
|
||||
await t.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
}
|
||||
|
||||
/// 진행 막대 · 글자 칸 깜빡임이 있으면 pumpAndSettle 이 끝나지 않는다 - 정한 만큼만
|
||||
Future<void> settle(WidgetTester t) async {
|
||||
for (var i = 0; i < 12; i++) {
|
||||
await t.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> menu(WidgetTester t, String row, String item) async {
|
||||
// 꺼내기 · 넣기 중에는 메뉴 단추가 꺼져 있다
|
||||
await until(t, () => find.byType(LinearProgressIndicator).evaluate().isEmpty);
|
||||
await t.tap(find.descendant(of: find.widgetWithText(ListTile, row), matching: find.byType(PopupMenuButton<String>)));
|
||||
await settle(t);
|
||||
await t.tap(find.text(item).last);
|
||||
await t.pump();
|
||||
}
|
||||
|
||||
testWidgets('만들기 (알림 · 체크해야 켜짐) → 넣기 (원본 지우기 체크 → 탐색기 지우기 부름) → 잠금 → 틀린 비밀번호 → 열기 → 꺼내기 → 지우기', (t) async {
|
||||
t.view.physicalSize = const Size(900, 1400);
|
||||
t.view.devicePixelRatio = 1;
|
||||
addTearDown(t.view.reset);
|
||||
final src = File(p.join(tmp.path, '비밀 문서.pdf'))..writeAsBytesSync(List.generate(3000, (i) => i % 256));
|
||||
final deleted = <List<String>>[];
|
||||
final c = ctl();
|
||||
await t.pumpWidget(MaterialApp(
|
||||
home: VaultPage(c: c, add: [src.path], deleteOriginals: (paths) async => deleted.add(paths))));
|
||||
await until(t, () => find.text('새 보관함 만들기').evaluate().isNotEmpty);
|
||||
expect(find.textContaining('다른 앱도 이 폴더의 암호화된 파일을 볼 수 있습니다'), findsOneWidget);
|
||||
expect(find.textContaining('이 폴더를 지우면 보관함도 사라집니다'), findsOneWidget);
|
||||
expect(find.textContaining('되살릴 수 없습니다'), findsWidgets);
|
||||
final fields = find.byType(TextField);
|
||||
await t.enterText(fields.at(0), 'pw1');
|
||||
await t.enterText(fields.at(1), 'pw1');
|
||||
await t.pump();
|
||||
expect(t.widget<FilledButton>(find.widgetWithText(FilledButton, '만들기')).onPressed, isNull, reason: '체크해야 켜짐');
|
||||
await t.tap(find.text('비밀번호를 잊으면 되살릴 수 없음을 압니다'));
|
||||
await t.pump();
|
||||
await t.tap(find.widgetWithText(FilledButton, '만들기'));
|
||||
await until(t, () => find.textContaining('보관함에 넣기 ·').evaluate().isNotEmpty);
|
||||
// 넣기 창: 원본 지우기 (처음 끔) 를 켜고 넣기
|
||||
expect(t.widget<CheckboxListTile>(find.byType(CheckboxListTile)).value, isFalse, reason: '처음은 복사');
|
||||
await t.tap(find.text('원본 지우기 (이동)'));
|
||||
await t.pump();
|
||||
await t.tap(find.widgetWithText(FilledButton, '넣기'));
|
||||
await until(t, () => deleted.isNotEmpty);
|
||||
expect(deleted.single, [src.path], reason: '다 넣은 뒤 탐색기의 지우기로');
|
||||
expect(find.text('비밀 문서.pdf'), findsOneWidget);
|
||||
expect(Directory(p.join(tmp.path, 'vault')).listSync().map((f) => p.basename(f.path)).join(), isNot(contains('비밀')));
|
||||
// 잠금 → 틀린 비밀번호 → 맞는 비밀번호
|
||||
await t.tap(find.byTooltip('잠그기'));
|
||||
await t.pump();
|
||||
await t.enterText(find.byType(TextField), 'wrong');
|
||||
await t.tap(find.widgetWithText(FilledButton, '열기'));
|
||||
await until(t, () => find.text('비밀번호가 틀렸습니다').evaluate().isNotEmpty);
|
||||
expect(find.text('비밀번호가 틀렸습니다'), findsOneWidget);
|
||||
await t.enterText(find.byType(TextField), 'pw1');
|
||||
await t.tap(find.widgetWithText(FilledButton, '열기'));
|
||||
await until(t, () => find.text('비밀 문서.pdf').evaluate().isNotEmpty);
|
||||
// 꺼내기 (원본과 같은 내용)
|
||||
await menu(t, '비밀 문서.pdf', '꺼내기');
|
||||
await until(t, () => File(p.join(tmp.path, 'out', '비밀 문서.pdf')).existsSync());
|
||||
expect(File(p.join(tmp.path, 'out', '비밀 문서.pdf')).readAsBytesSync(), src.readAsBytesSync());
|
||||
// 지우기 (확인)
|
||||
await menu(t, '비밀 문서.pdf', '보관함에서 지우기');
|
||||
await settle(t);
|
||||
await t.tap(find.widgetWithText(FilledButton, '지우기'));
|
||||
await until(t, () => find.text('보관함이 비어 있습니다').evaluate().isNotEmpty);
|
||||
expect(find.text('보관함이 비어 있습니다'), findsOneWidget);
|
||||
await t.pumpWidget(const SizedBox());
|
||||
});
|
||||
|
||||
testWidgets('틀린 비밀번호 5번이면 30초 기다림 · [마스터로도 열기] 켜기 → 마스터로 열기 · 마스터를 초기화하면 그 길만 없어짐', (t) async {
|
||||
t.view.physicalSize = const Size(900, 1400);
|
||||
t.view.devicePixelRatio = 1;
|
||||
addTearDown(t.view.reset);
|
||||
final secrets = MemorySecretStore();
|
||||
final lock = MasterLock(secrets);
|
||||
MasterLock.instance = lock;
|
||||
await t.runAsync(() => lock.setMaster('master'));
|
||||
final c = ctl();
|
||||
// 먼저 만들어 둔다
|
||||
await t.pumpWidget(MaterialApp(home: VaultPage(c: c)));
|
||||
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.text('보관함이 비어 있습니다').evaluate().isNotEmpty);
|
||||
// 마스터로도 열기 켜기
|
||||
await t.tap(find.descendant(of: find.byType(AppBar), matching: find.byType(PopupMenuButton<String>)));
|
||||
await settle(t);
|
||||
await t.tap(find.text('마스터로도 열기'));
|
||||
await settle(t);
|
||||
expect(find.textContaining('마스터를 초기화하면 이 길만 없어지고'), findsOneWidget);
|
||||
await t.tap(find.widgetWithText(FilledButton, '켜기'));
|
||||
await until(t, () => c.settings.vaultMasterCopy);
|
||||
expect((await t.runAsync(secrets.readAll))!.containsKey(MasterLock.vaultCopyKey), isTrue);
|
||||
// 잠그고 마스터로 열기
|
||||
await t.tap(find.byTooltip('잠그기'));
|
||||
await t.pump();
|
||||
await t.tap(find.text('마스터 비밀번호로 열기'));
|
||||
await until(t, () => find.text('보관함이 비어 있습니다').evaluate().isNotEmpty);
|
||||
expect(find.text('보관함이 비어 있습니다'), findsOneWidget);
|
||||
// 마스터 초기화 → 그 길만 없어짐 (보관함 비밀번호로는 열림)
|
||||
await t.runAsync(lock.clearMaster);
|
||||
expect((await t.runAsync(secrets.readAll))!.containsKey(MasterLock.vaultCopyKey), isFalse);
|
||||
await t.tap(find.byTooltip('잠그기'));
|
||||
await t.pump();
|
||||
await t.tap(find.text('마스터 비밀번호로 열기'));
|
||||
await until(t, () => find.textContaining('마스터로 여는 열쇠가 없습니다').evaluate().isNotEmpty);
|
||||
expect(find.textContaining('마스터로 여는 열쇠가 없습니다'), findsOneWidget);
|
||||
// 틀린 비밀번호 5번
|
||||
for (var i = 0; i < 5; i++) {
|
||||
await t.enterText(find.byType(TextField), 'nope$i');
|
||||
await t.tap(find.widgetWithText(FilledButton, '열기'));
|
||||
await t.pump();
|
||||
// 여는 중 (비밀번호 확인) 이 끝나 다시 잠금 화면이 될 때까지
|
||||
await until(t, () => find.byType(TextField).evaluate().isNotEmpty);
|
||||
}
|
||||
expect(find.textContaining('30초 뒤에 다시'), findsOneWidget);
|
||||
expect(t.widget<FilledButton>(find.widgetWithText(FilledButton, '열기')).onPressed, isNull);
|
||||
await t.pump(const Duration(seconds: 31));
|
||||
await t.enterText(find.byType(TextField), 'pw');
|
||||
await t.tap(find.widgetWithText(FilledButton, '열기'));
|
||||
await until(t, () => find.text('보관함이 비어 있습니다').evaluate().isNotEmpty);
|
||||
expect(find.text('보관함이 비어 있습니다'), findsOneWidget);
|
||||
await t.pumpWidget(const SizedBox());
|
||||
});
|
||||
testWidgets('파일 탐색기: [보관함에 넣기] 는 보관함을 켰을 때 · 이 기기의 파일에만 (폴더는 2단계)', (t) async {
|
||||
t.view.physicalSize = const Size(1400, 900);
|
||||
t.view.devicePixelRatio = 1;
|
||||
addTearDown(t.view.reset);
|
||||
final dir = Directory(p.join(tmp.path, 'files'))..createSync();
|
||||
File(p.join(dir.path, 'a.txt')).writeAsStringSync('a');
|
||||
Directory(p.join(dir.path, 'sub')).createSync();
|
||||
for (final on in [false, true]) {
|
||||
final c = ctl()
|
||||
..settings.vaultEnabled = on
|
||||
..settings.explorerPaths = [dir.path, dir.path]
|
||||
..settings.explorerLayout = 'split';
|
||||
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), c: c))));
|
||||
await until(t, () => find.text('a.txt').evaluate().isNotEmpty);
|
||||
await t.longPress(find.text('a.txt').last);
|
||||
await settle(t);
|
||||
expect(find.text('보관함에 넣기'), on ? findsOneWidget : findsNothing, reason: '켬 = $on');
|
||||
await t.tapAt(const Offset(5, 5));
|
||||
await settle(t);
|
||||
if (on) {
|
||||
await t.longPress(find.text('sub').last);
|
||||
await settle(t);
|
||||
expect(find.text('보관함에 넣기'), findsNothing, reason: '폴더는 아직');
|
||||
await t.tapAt(const Offset(5, 5));
|
||||
await settle(t);
|
||||
}
|
||||
await t.pumpWidget(const SizedBox());
|
||||
await t.pump(const Duration(seconds: 1));
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue