WIP 88 SFTP 2/3: 화면 - 환경 설정 SFTP 서버 · 탐색기 탭 · 처음 키 확인 · 고르는 곳 · 라이선스 고지 (미완)
- 환경 설정 > 파일 탐색기 > SFTP (lib/ui/sftp_settings.dart): 추가 · 고치기 · 지우기 (쓰는 곳 알림), 서버 주소 · 포트 · 아이디 · 비밀번호 · 개인 키 파일 고르기 (내용만 안전 저장소에, 원래 경로는 저장하지 않음) · 키 암호 · 시작 폴더 · 오래된 ssh-rsa 허용, [연결 확인] 에서 처음 보는 키는 지문을 보이고 [믿고 연결], 믿은 키 보기 · [믿은 키 지우기] (마스터 비밀번호 뒤). - 파일 탐색기 · Rsync 화면: SFTP 서버 탭 (🔑), 탭 길게 누르기 = 고치기, 끝 버튼에 [SFTP 서버 추가] (좁으면 ⋮ 메뉴에). 처음 연결: 목록 대신 "처음 연결하는 서버입니다 · 지문" 안내와 [지문 확인하고 연결] → 확인 창 [믿고 연결] → 지문을 설정에 저장하고 읽음. 바뀐 키: 막고 예전 · 지금 지문, [서버 설정 고치기]. 경로 복사는 sftp://아이디@서버:포트/경로 (비밀번호 없음). - 실시간 동기화 · 모니터링의 폴더 고르기에 SFTP 서버도. 화면 경로 "🔑 서버 › 폴더". - WebDAV 에만 맞던 글 6개를 "WebDAV · SFTP" 로 (풀기 · ZIP · 분석 · MKV 목록 · Rsync 안내 · 빠른 보기). 탐색기 ⋮ 이름 "경로 입력 · 네트워크 서버". - 파일 고르기: 확장자 목록이 비면 모든 파일 (확장자가 없는 개인 키 파일 - Windows · Android). - 라이선스: dartssh2 4.1.0 (MIT) 를 두 고지에 (pointycastle · pinenacl · asn1lib 원문은 패키지 라이선스 화면), 판이 바뀌면 시험이 알림. - 번역 en · ja · zh-Hans (빠진 번역 0). - 시험: sftp_explorer_test (시험 서버로 처음 연결 → 지문 → 믿고 연결 → 목록 · 지문 저장), test/support/sftp_server.dart 로 서버 띄우기 공용, 글이 바뀐 시험 3곳 고침. 전체: 이 4곳 말고 통과, 새로 남은 임시 폴더 0 (move_settled 1번은 powershell 실행 "액세스 거부" - 다시 돌려 통과). - 내 판단: 끝 버튼 줄에 [SFTP 서버 추가] 아이콘을 하나 더 (WebDAV 와 따로 바로 보이게). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음7_88
parent
b5918b7aa2
commit
9a786f3496
|
|
@ -29,8 +29,10 @@
|
||||||
" 외 {0}개": " and {0} more",
|
" 외 {0}개": " and {0} more",
|
||||||
" 지금 설치됨 ({0}MB)": " Installed now ({0}MB)",
|
" 지금 설치됨 ({0}MB)": " Installed now ({0}MB)",
|
||||||
"\"{0}\" 검색 결과 {1}개": "{1} results for \"{0}\"",
|
"\"{0}\" 검색 결과 {1}개": "{1} results for \"{0}\"",
|
||||||
|
"\"{0}\" 서버의 키가 바뀌었습니다": "The key of server \"{0}\" has changed",
|
||||||
"\"{0}\" 안에 만듭니다": "Created inside \"{0}\"",
|
"\"{0}\" 안에 만듭니다": "Created inside \"{0}\"",
|
||||||
"\"{0}\" 에 맞는 항목이 없습니다": "No items match \"{0}\"",
|
"\"{0}\" 에 맞는 항목이 없습니다": "No items match \"{0}\"",
|
||||||
|
"\"{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}\" 에는 없고 대상 \"{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}\" 은(는) 비밀번호가 걸려 있습니다.": "\"{0}\" is password-protected.",
|
||||||
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "Move \"{0}\" to the \"{1}\" folder?",
|
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "Move \"{0}\" to the \"{1}\" folder?",
|
||||||
|
|
@ -260,10 +262,15 @@
|
||||||
"Rsync 확인 창 ({0}): {1}": "Rsync confirmation ({0}): {1}",
|
"Rsync 확인 창 ({0}): {1}": "Rsync confirmation ({0}): {1}",
|
||||||
"SD 카드": "SD card",
|
"SD 카드": "SD card",
|
||||||
"SD 카드가 꽂혀 있는지, 네트워크 (NAS · VPN) 가 연결되어 있는지, 폴더 권한이 있는지 확인한 뒤 [다시 시도] 를 누르세요.": "Check that the SD card is inserted, the network (NAS · VPN) is connected and you have permission for the folder, then press [Try again].",
|
"SD 카드가 꽂혀 있는지, 네트워크 (NAS · VPN) 가 연결되어 있는지, 폴더 권한이 있는지 확인한 뒤 [다시 시도] 를 누르세요.": "Check that the SD card is inserted, the network (NAS · VPN) is connected and you have permission for the folder, then press [Try again].",
|
||||||
|
"SFTP 서버 \"{0}\" 을(를) 지울까요?": "Delete SFTP server \"{0}\"?",
|
||||||
|
"SFTP 서버 고치기": "Edit SFTP server",
|
||||||
"SFTP 서버 설정이 없습니다 ({0})": "There are no settings for this SFTP server ({0})",
|
"SFTP 서버 설정이 없습니다 ({0})": "There are no settings for this SFTP server ({0})",
|
||||||
"SFTP 서버 주소가 없습니다": "The SFTP server address is empty",
|
"SFTP 서버 주소가 없습니다": "The SFTP server address is empty",
|
||||||
|
"SFTP 서버 추가": "Add SFTP server",
|
||||||
"SFTP 에서 받는 중": "Downloading from SFTP",
|
"SFTP 에서 받는 중": "Downloading from SFTP",
|
||||||
|
"SHA-1 서명 · 키 교환을 씁니다. 오래된 NAS 가 다른 방식을 모를 때만 켜세요 (보안이 약함)": "Uses SHA-1 signatures and key exchange. Turn on only when an old NAS supports nothing else (weaker security)",
|
||||||
"SRT 형식이 아니거나 비어 있습니다.": "Not in SRT format or empty.",
|
"SRT 형식이 아니거나 비어 있습니다.": "Not in SRT format or empty.",
|
||||||
|
"SSH 서버 (NAS · 리눅스) 의 폴더를 파일 탐색기 · Rsync 화면에서 열고 복사 · 동기화합니다 (위쪽 탭). 비밀번호 · 개인 키 · 키 암호는 이 기기의 안전 저장소에만 둡니다. 처음 연결할 때 서버의 키 지문을 확인합니다.": "Open, copy and sync folders on SSH servers (NAS, Linux) in the File explorer and Rsync screens (tabs at the top). Passwords, private keys and key passphrases are kept only in this device's secure storage. The server's key fingerprint is checked on the first connection.",
|
||||||
"Stable Diffusion v1.5": "Stable Diffusion v1.5",
|
"Stable Diffusion v1.5": "Stable Diffusion v1.5",
|
||||||
"Stable Diffusion v1.5 는 CreativeML OpenRAIL-M 라이선스입니다. 상업적 사용을 포함해 쓸 수 있지만, 다음 용도로는 쓸 수 없습니다: 법을 어기는 일, 미성년자를 해치거나 이용하는 일, 남을 해치려는 거짓 정보, 개인 정보로 사람을 해치는 일, 차별 · 괴롭힘, 의료 · 법률 판단을 대신하는 일 등 (라이선스 부속서 A). 만든 그림에 대한 책임은 쓰는 사람에게 있습니다.": "Stable Diffusion v1.5 is licensed under CreativeML OpenRAIL-M. You may use it, including commercially, but not for: breaking the law, harming or exploiting minors, false information meant to harm others, harming people with personal information, discrimination or harassment, replacing medical or legal judgment, etc. (license Attachment A). You are responsible for the images you make.",
|
"Stable Diffusion v1.5 는 CreativeML OpenRAIL-M 라이선스입니다. 상업적 사용을 포함해 쓸 수 있지만, 다음 용도로는 쓸 수 없습니다: 법을 어기는 일, 미성년자를 해치거나 이용하는 일, 남을 해치려는 거짓 정보, 개인 정보로 사람을 해치는 일, 차별 · 괴롭힘, 의료 · 법률 판단을 대신하는 일 등 (라이선스 부속서 A). 만든 그림에 대한 책임은 쓰는 사람에게 있습니다.": "Stable Diffusion v1.5 is licensed under CreativeML OpenRAIL-M. You may use it, including commercially, but not for: breaking the law, harming or exploiting minors, false information meant to harm others, harming people with personal information, discrimination or harassment, replacing medical or legal judgment, etc. (license Attachment A). You are responsible for the images you make.",
|
||||||
"TAESD (빠른 디코더)": "TAESD (fast decoder)",
|
"TAESD (빠른 디코더)": "TAESD (fast decoder)",
|
||||||
|
|
@ -324,7 +331,12 @@
|
||||||
"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 · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV / SFTP reads everything inside from the server, which can take a long time. Analyze?",
|
||||||
|
"WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "Videos on WebDAV / SFTP can't be added to the MKV list. Copy them to this device first.",
|
||||||
|
"WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV / SFTP files show only name, size and date",
|
||||||
|
"WebDAV · SFTP 폴더로는 풀 수 없습니다. 이 기기의 폴더에서 풀어 주세요.": "Can't extract into a WebDAV / SFTP folder. Extract into a folder on this device.",
|
||||||
|
"WebDAV · SFTP 항목은 ZIP 으로 묶을 수 없습니다. 이 기기의 파일 · 폴더를 골라 주세요.": "WebDAV / SFTP items can't be zipped. Choose files or folders on this device.",
|
||||||
|
"WebDAV · SFTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV / SFTP: the app syncs directly instead of rsync (compares size and modified time; follows -u, --delete and --remove-source-files).",
|
||||||
"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",
|
||||||
|
|
@ -332,11 +344,6 @@
|
||||||
"WebDAV 서버 추가": "Add WebDAV server",
|
"WebDAV 서버 추가": "Add WebDAV server",
|
||||||
"WebDAV 에서 받는 중": "Downloading from WebDAV",
|
"WebDAV 에서 받는 중": "Downloading from WebDAV",
|
||||||
"WebDAV 원본에 씁니다 (PC 의 폴더는 바뀌면 바로 맞춤). 짧을수록 빨리 맞추지만 서버 · 배터리를 더 씁니다.": "Used for WebDAV sources (PC folders sync as soon as they change). Shorter means faster syncing but more server and battery use.",
|
"WebDAV 원본에 씁니다 (PC 의 폴더는 바뀌면 바로 맞춤). 짧을수록 빨리 맞추지만 서버 · 배터리를 더 씁니다.": "Used for WebDAV sources (PC folders sync as soon as they change). Shorter means faster syncing but more server and battery use.",
|
||||||
"WebDAV 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "Videos on WebDAV can't be added to the MKV list. Copy them to this device first.",
|
|
||||||
"WebDAV 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV files show only name, size and date",
|
|
||||||
"WebDAV 폴더로는 풀 수 없습니다. 이 기기의 폴더에서 풀어 주세요.": "Can't extract into a WebDAV folder. Extract into a folder on this device.",
|
|
||||||
"WebDAV 항목은 ZIP 으로 묶을 수 없습니다. 이 기기의 파일 · 폴더를 골라 주세요.": "WebDAV items can't be zipped. Choose files or folders on this device.",
|
|
||||||
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV: the app syncs directly instead of rsync (compares size and modified time; follows -u, --delete and --remove-source-files).",
|
|
||||||
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --remove-source-files 만 따름).": "WebDAV: the app syncs by itself instead of rsync (compares size and modified time; of the options only -u and --remove-source-files apply).",
|
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --remove-source-files 만 따름).": "WebDAV: the app syncs by itself instead of rsync (compares size and modified time; of the options only -u and --remove-source-files apply).",
|
||||||
"WebM (동영상)": "WebM (video)",
|
"WebM (동영상)": "WebM (video)",
|
||||||
"WebP 등은 PNG 로 저장합니다": "WebP and similar are saved as PNG",
|
"WebP 등은 PNG 로 저장합니다": "WebP and similar are saved as PNG",
|
||||||
|
|
@ -576,6 +583,7 @@
|
||||||
"가로 16:9": "Landscape 16:9",
|
"가로 16:9": "Landscape 16:9",
|
||||||
"가로 고정": "Landscape only",
|
"가로 고정": "Landscape only",
|
||||||
"가로 고정 · 세로 고정, 또는 자동 (기기를 돌리는 대로, 기기의 자동 회전 설정을 따름)": "Lock landscape · lock portrait, or auto (follows how you hold the device and its auto-rotate setting)",
|
"가로 고정 · 세로 고정, 또는 자동 (기기를 돌리는 대로, 기기의 자동 회전 설정을 따름)": "Lock landscape · lock portrait, or auto (follows how you hold the device and its auto-rotate setting)",
|
||||||
|
"가리기": "Hide",
|
||||||
"가입 → 프로필 → API consumers → New consumer": "Sign up → Profile → API consumers → New consumer",
|
"가입 → 프로필 → API consumers → New consumer": "Sign up → Profile → API consumers → New consumer",
|
||||||
"가장 빠른 처리 장치를 찾는 중 (처음 한 번): {0}": "Finding the fastest processor (first time only): {0}",
|
"가장 빠른 처리 장치를 찾는 중 (처음 한 번): {0}": "Finding the fastest processor (first time only): {0}",
|
||||||
"가져오기": "Import",
|
"가져오기": "Import",
|
||||||
|
|
@ -610,6 +618,15 @@
|
||||||
"같은 확장자 모두": "All with the same extension",
|
"같은 확장자 모두": "All with the same extension",
|
||||||
"같은 확장자 모두 (.{0})": "All with the same extension (.{0})",
|
"같은 확장자 모두 (.{0})": "All with the same extension (.{0})",
|
||||||
"개수": "Count",
|
"개수": "Count",
|
||||||
|
"개인 키": "Private key",
|
||||||
|
"개인 키 바꾸기": "Change private key",
|
||||||
|
"개인 키 빼기": "Remove private key",
|
||||||
|
"개인 키 암호 (없으면 비움)": "Private key passphrase (leave empty if none)",
|
||||||
|
"개인 키 있음": "Private key set",
|
||||||
|
"개인 키 파일 고르기": "Choose private key file",
|
||||||
|
"개인 키 파일이 아닙니다 (OpenSSH · PEM 형식의 비밀 키를 고르세요, .pub 아님)": "This isn't a private key file (choose an OpenSSH or PEM private key, not .pub)",
|
||||||
|
"개인 키 파일이 아닙니다 (너무 큼)": "This isn't a private key file (too large)",
|
||||||
|
"개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "Private key added (the original file path isn't saved)",
|
||||||
"개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)": "Can't read the private key (OpenSSH or PEM format)",
|
"개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)": "Can't read the private key (OpenSSH or PEM format)",
|
||||||
"개인 키의 암호가 맞지 않습니다": "The private key passphrase is incorrect",
|
"개인 키의 암호가 맞지 않습니다": "The private key passphrase is incorrect",
|
||||||
"거꾸로": "Reverse",
|
"거꾸로": "Reverse",
|
||||||
|
|
@ -634,6 +651,7 @@
|
||||||
"경로 입력": "Enter path",
|
"경로 입력": "Enter path",
|
||||||
"경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "Enter path (e.g. D:\\Videos · \\\\NAS\\share)",
|
"경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "Enter path (e.g. D:\\Videos · \\\\NAS\\share)",
|
||||||
"경로 입력 · WebDAV": "Enter path · WebDAV",
|
"경로 입력 · WebDAV": "Enter path · WebDAV",
|
||||||
|
"경로 입력 · 네트워크 서버": "Enter path · Network servers",
|
||||||
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "The path is too long for the Recycle Bin (the Windows Recycle Bin does not accept paths over 260 characters).",
|
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "The path is too long for the Recycle Bin (the Windows Recycle Bin does not accept paths over 260 characters).",
|
||||||
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "The path is too long (the Windows Recycle Bin doesn't accept paths over 260 characters) - you can delete it permanently",
|
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "The path is too long (the Windows Recycle Bin doesn't accept paths over 260 characters) - you can delete it permanently",
|
||||||
"경로를 복사했습니다: {0}": "Copied path: {0}",
|
"경로를 복사했습니다: {0}": "Copied path: {0}",
|
||||||
|
|
@ -1131,6 +1149,10 @@
|
||||||
"미리보기: 목록에서 동영상을 고르세요": "Preview: choose a video in the list",
|
"미리보기: 목록에서 동영상을 고르세요": "Preview: choose a video in the list",
|
||||||
"미얀마어": "Burmese",
|
"미얀마어": "Burmese",
|
||||||
"미지정": "Unspecified",
|
"미지정": "Unspecified",
|
||||||
|
"믿고 연결": "Trust and connect",
|
||||||
|
"믿은 서버 키": "Trusted server key",
|
||||||
|
"믿은 키 지우기": "Forget trusted key",
|
||||||
|
"믿은 키를 지웠습니다. [연결 확인] 으로 새 키를 확인하세요": "Trusted key removed. Check the new key with [Test connection]",
|
||||||
"바깥 폴더 (../)": "Parent folder (../)",
|
"바깥 폴더 (../)": "Parent folder (../)",
|
||||||
"바꾸거나 끄려면 지금 마스터 비밀번호 (또는 최상 비밀번호) 를 넣으세요.": "To change or turn it off, enter the current master password (or the top password).",
|
"바꾸거나 끄려면 지금 마스터 비밀번호 (또는 최상 비밀번호) 를 넣으세요.": "To change or turn it off, enter the current master password (or the top password).",
|
||||||
"바꾸기": "Change",
|
"바꾸기": "Change",
|
||||||
|
|
@ -1243,6 +1265,7 @@
|
||||||
"보낼 것 {0} / 있는 것 {1}": "sending {0} / existing {1}",
|
"보낼 것 {0} / 있는 것 {1}": "sending {0} / existing {1}",
|
||||||
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "The folder you were viewing is gone, so moved to its parent folder",
|
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "The folder you were viewing is gone, so moved to its parent folder",
|
||||||
"보안": "Security",
|
"보안": "Security",
|
||||||
|
"보이기": "Show",
|
||||||
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "Choose the buttons to show and drag to reorder.",
|
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "Choose the buttons to show and drag to reorder.",
|
||||||
"보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "Adjustments require re-encoding, so the codec was changed from \"Keep original\" to H.264.",
|
"보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "Adjustments require re-encoding, so the codec was changed from \"Keep original\" to H.264.",
|
||||||
"보조 DNS (비워 둘 수 있음)": "Secondary DNS (may be empty)",
|
"보조 DNS (비워 둘 수 있음)": "Secondary DNS (may be empty)",
|
||||||
|
|
@ -1296,6 +1319,7 @@
|
||||||
"비공개 · 회원 전용 영상입니다.": "This is a private · members-only video.",
|
"비공개 · 회원 전용 영상입니다.": "This is a private · members-only video.",
|
||||||
"비교하지 못했습니다: {0}": "Couldn't compare: {0}",
|
"비교하지 못했습니다: {0}": "Couldn't compare: {0}",
|
||||||
"비밀번호": "Password",
|
"비밀번호": "Password",
|
||||||
|
"비밀번호 (개인 키만 쓰면 비움)": "Password (leave empty if using only a private key)",
|
||||||
"비밀번호 (선택)": "Password (optional)",
|
"비밀번호 (선택)": "Password (optional)",
|
||||||
"비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "Passwords · API keys (WebDAV · OpenSubtitles · AI services)",
|
"비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "Passwords · API keys (WebDAV · OpenSubtitles · AI services)",
|
||||||
"비밀번호 · 자막 API 키는 백업하지 않으므로 다시 넣어 주세요. 자동 백업은 하루에 한 번쯤 만들어져 그 뒤에 바꾼 것은 빠졌을 수 있습니다.": "Passwords and the subtitle API key aren't backed up, so please enter them again. Auto Backup runs about once a day, so changes made after it may be missing.",
|
"비밀번호 · 자막 API 키는 백업하지 않으므로 다시 넣어 주세요. 자동 백업은 하루에 한 번쯤 만들어져 그 뒤에 바꾼 것은 빠졌을 수 있습니다.": "Passwords and the subtitle API key aren't backed up, so please enter them again. Auto Backup runs about once a day, so changes made after it may be missing.",
|
||||||
|
|
@ -1367,9 +1391,13 @@
|
||||||
"서버 ({0}) 와의 연결이 끊겼습니다": "The connection to the server ({0}) was lost",
|
"서버 ({0}) 와의 연결이 끊겼습니다": "The connection to the server ({0}) was lost",
|
||||||
"서버 · 서비스 (기기 밖)": "Servers · services (off device)",
|
"서버 · 서비스 (기기 밖)": "Servers · services (off device)",
|
||||||
"서버 설정 고치기": "Edit server settings",
|
"서버 설정 고치기": "Edit server settings",
|
||||||
|
"서버 설정과 이 기기에 저장된 비밀번호 · 개인 키 · 믿은 서버 키를 지웁니다. 서버의 파일은 그대로입니다. 이 서버를 쓰는 동기화 · 복사는 다시 넣을 때까지 연결되지 않습니다.": "Deletes the server settings and the password, private key and trusted server key saved on this device. Files on the server stay as they are. Syncs and copies that use this server won't connect until you add it again.",
|
||||||
"서버 설정과 이 기기에 저장된 비밀번호를 지웁니다. 서버의 파일은 그대로입니다. 이 서버를 쓰는 동기화 · 복사는 다시 넣을 때까지 연결되지 않습니다.": "Deletes the server setting and the password saved on this device. Files on the server stay as they are. Syncs and copies using this server won't connect until you add it again.",
|
"서버 설정과 이 기기에 저장된 비밀번호를 지웁니다. 서버의 파일은 그대로입니다. 이 서버를 쓰는 동기화 · 복사는 다시 넣을 때까지 연결되지 않습니다.": "Deletes the server setting and the password saved on this device. Files on the server stay as they are. Syncs and copies using this server won't connect until you add it again.",
|
||||||
"서버 인증서를 확인할 수 없습니다": "Cannot verify the server certificate",
|
"서버 인증서를 확인할 수 없습니다": "Cannot verify the server certificate",
|
||||||
"서버 저장 공간 부족": "server storage full",
|
"서버 저장 공간 부족": "server storage full",
|
||||||
|
"서버 주소": "Server address",
|
||||||
|
"서버 키 아직 확인 안 함": "Server key not checked yet",
|
||||||
|
"서버 키: 아직 확인 안 함 ([연결 확인] 을 누르면 지문을 보이고 묻습니다)": "Server key: not checked yet ([Test connection] shows the fingerprint and asks)",
|
||||||
"서버가 https 주소로 옮기라고 합니다 ({0}). 서버 설정의 주소를 https:// 로 바꾸세요.": "The server asks to move to an https address ({0}). Change the server address to https://.",
|
"서버가 https 주소로 옮기라고 합니다 ({0}). 서버 설정의 주소를 https:// 로 바꾸세요.": "The server asks to move to an https address ({0}). Change the server address to https://.",
|
||||||
"서버가 거절했습니다 ({0}): {1}": "The server refused ({0}): {1}",
|
"서버가 거절했습니다 ({0}): {1}": "The server refused ({0}): {1}",
|
||||||
"서버가 그림을 돌려주지 않았습니다": "The server returned no image",
|
"서버가 그림을 돌려주지 않았습니다": "The server returned no image",
|
||||||
|
|
@ -1382,6 +1410,8 @@
|
||||||
"서버와 같은 네트워크에 있거나 VPN (예: Tailscale) 이 켜져 있어야 합니다. 켠 뒤 [다시 시도] 를 누르세요.": "You need to be on the same network as the server or have a VPN (e.g. Tailscale) on. Turn it on, then tap [Try again].",
|
"서버와 같은 네트워크에 있거나 VPN (예: Tailscale) 이 켜져 있어야 합니다. 켠 뒤 [다시 시도] 를 누르세요.": "You need to be on the same network as the server or have a VPN (e.g. Tailscale) on. Turn it on, then tap [Try again].",
|
||||||
"서버와 암호 방식을 맞추지 못했습니다. 오래된 서버면 서버 설정의 \"오래된 ssh-rsa 허용\" 을 켜 보세요": "Couldn't agree on an encryption method with the server. If it's an old server, try turning on \"Allow old ssh-rsa\" in the server settings",
|
"서버와 암호 방식을 맞추지 못했습니다. 오래된 서버면 서버 설정의 \"오래된 ssh-rsa 허용\" 을 켜 보세요": "Couldn't agree on an encryption method with the server. If it's an old server, try turning on \"Allow old ssh-rsa\" in the server settings",
|
||||||
"서버와 암호 방식을 맞추지 못했습니다: {0}": "Couldn't agree on an encryption method with the server: {0}",
|
"서버와 암호 방식을 맞추지 못했습니다: {0}": "Couldn't agree on an encryption method with the server: {0}",
|
||||||
|
"서버의 키 지문이 서버 (NAS 설정 · ssh-keygen -lf) 에서 본 것과 같은지 확인하세요. 믿으면 이 기기에 저장하고, 다음부터 키가 바뀌면 연결하지 않고 알립니다.": "Check that the server's key fingerprint matches what the server shows (NAS settings or ssh-keygen -lf). If you trust it, it's saved on this device, and if the key changes later you'll be told and not connected.",
|
||||||
|
"서버의 키가 바뀌었습니다": "The server's key has changed",
|
||||||
"서버의 키가 바뀌었습니다 - 중간에서 가로채는 공격일 수 있어 연결하지 않았습니다. 예전: {0} · 지금: {1}": "The server's key has changed. This could be an interception attack, so the connection was not made. Before: {0} · Now: {1}",
|
"서버의 키가 바뀌었습니다 - 중간에서 가로채는 공격일 수 있어 연결하지 않았습니다. 예전: {0} · 지금: {1}": "The server's key has changed. This could be an interception attack, so the connection was not made. Before: {0} · Now: {1}",
|
||||||
"서비스 고치기": "Edit service",
|
"서비스 고치기": "Edit service",
|
||||||
"서비스 설정과 저장된 API 키를 지웁니다.": "Deletes the service settings and its saved API key.",
|
"서비스 설정과 저장된 API 키를 지웁니다.": "Deletes the service settings and its saved API key.",
|
||||||
|
|
@ -1469,6 +1499,7 @@
|
||||||
"시작 · 웹 브라우저": "Startup · web browser",
|
"시작 · 웹 브라우저": "Startup · web browser",
|
||||||
"시작 메뉴에 등록": "Add to Start menu",
|
"시작 메뉴에 등록": "Add to Start menu",
|
||||||
"시작 안 함 (모니터링에서 시작)": "Don't start (start from Monitor)",
|
"시작 안 함 (모니터링에서 시작)": "Don't start (start from Monitor)",
|
||||||
|
"시작 폴더 (비우면 홈 폴더)": "Start folder (home folder if empty)",
|
||||||
"시작: {0} → {1}{2}": "Started: {0} → {1}{2}",
|
"시작: {0} → {1}{2}": "Started: {0} → {1}{2}",
|
||||||
"시작하는 중…": "Starting…",
|
"시작하는 중…": "Starting…",
|
||||||
"시작하지 못했습니다: {0}": "Could not start: {0}",
|
"시작하지 못했습니다: {0}": "Could not start: {0}",
|
||||||
|
|
@ -1631,6 +1662,7 @@
|
||||||
"예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 속도 제한은 파일 탐색기의 속도 제한을 함께 씀. 새로 맞추는 폴더 쌍에 씁니다 - 이미 실행한 쌍은 기억한 옵션을 쓰며 모니터링 > 복사 · rsync 에서 따로 고칩니다": "e.g. -avPog (archive · verbose · progress · owner · group). Uses the file explorer's speed limit too. Applies to new folder pairs - pairs already run keep their remembered options, edited in Monitoring > Copy · rsync",
|
"예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 속도 제한은 파일 탐색기의 속도 제한을 함께 씀. 새로 맞추는 폴더 쌍에 씁니다 - 이미 실행한 쌍은 기억한 옵션을 쓰며 모니터링 > 복사 · rsync 에서 따로 고칩니다": "e.g. -avPog (archive · verbose · progress · owner · group). Uses the file explorer's speed limit too. Applies to new folder pairs - pairs already run keep their remembered options, edited in Monitoring > Copy · rsync",
|
||||||
"예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 이동은 --remove-source-files 를 자동으로 붙임": "e.g. -avPog (archive · verbose · progress · owner · group). Move adds --remove-source-files automatically",
|
"예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 이동은 --remove-source-files 를 자동으로 붙임": "e.g. -avPog (archive · verbose · progress · owner · group). Move adds --remove-source-files automatically",
|
||||||
"예: /E /COPY:DAT /DCOPY:T /R:2 /W:2. 이동은 /MOVE 를 자동으로 붙임": "e.g. /E /COPY:DAT /DCOPY:T /R:2 /W:2. Move adds /MOVE automatically",
|
"예: /E /COPY:DAT /DCOPY:T /R:2 /W:2. 이동은 /MOVE 를 자동으로 붙임": "e.g. /E /COPY:DAT /DCOPY:T /R:2 /W:2. Move adds /MOVE automatically",
|
||||||
|
"예: /volume1/video · 탭의 맨 위가 됩니다": "e.g. /volume1/video · becomes the top of the tab",
|
||||||
"예: 0 9-17 * * 1-5 (평일 9시~17시 59분) · 0 0-5 * * * (매일 0~5시). 분은 무시하고 1시간 단위": "e.g. 0 9-17 * * 1-5 (weekdays 9:00-17:59) · 0 0-5 * * * (daily 0-5). Minutes are ignored, 1-hour units",
|
"예: 0 9-17 * * 1-5 (평일 9시~17시 59분) · 0 0-5 * * * (매일 0~5시). 분은 무시하고 1시간 단위": "e.g. 0 9-17 * * 1-5 (weekdays 9:00-17:59) · 0 0-5 * * * (daily 0-5). Minutes are ignored, 1-hour units",
|
||||||
"예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "e.g. Ctrl+Shift+X (Ctrl·Shift·Alt·Win + letter·digit·F1~F12)",
|
"예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "e.g. Ctrl+Shift+X (Ctrl·Shift·Alt·Win + letter·digit·F1~F12)",
|
||||||
"예: S01E · *.srt": "e.g. S01E · *.srt",
|
"예: S01E · *.srt": "e.g. S01E · *.srt",
|
||||||
|
|
@ -1638,12 +1670,15 @@
|
||||||
"예: http://100.x.x.x:8188 (ComfyUI)": "e.g. http://100.x.x.x:8188 (ComfyUI)",
|
"예: http://100.x.x.x:8188 (ComfyUI)": "e.g. http://100.x.x.x:8188 (ComfyUI)",
|
||||||
"예: https://api.openai.com (경로 /v1/images/generations 를 붙여 부름)": "e.g. https://api.openai.com (called with the path /v1/images/generations)",
|
"예: https://api.openai.com (경로 /v1/images/generations 를 붙여 부름)": "e.g. https://api.openai.com (called with the path /v1/images/generations)",
|
||||||
"예: https://nas.local:5006/home · https://cloud.example.com/remote.php/dav/files/아이디": "e.g. https://nas.local:5006/home · https://cloud.example.com/remote.php/dav/files/username",
|
"예: https://nas.local:5006/home · https://cloud.example.com/remote.php/dav/files/아이디": "e.g. https://nas.local:5006/home · https://cloud.example.com/remote.php/dav/files/username",
|
||||||
|
"예: nas.local · 192.168.0.10 · 100.x.y.z (Tailscale)": "e.g. nas.local · 192.168.0.10 · 100.x.y.z (Tailscale)",
|
||||||
"예상 시간": "Estimated time",
|
"예상 시간": "Estimated time",
|
||||||
|
"예전": "Before",
|
||||||
"예전 구조의 프로그램 파일 정리 ({0}개): {1}": "Cleaned up program files of the old layout ({0}): {1}",
|
"예전 구조의 프로그램 파일 정리 ({0}개): {1}": "Cleaned up program files of the old layout ({0}): {1}",
|
||||||
"예전 버전": "an earlier version",
|
"예전 버전": "an earlier version",
|
||||||
"예전 버전 v{0} 으로 되돌리기": "Roll back to earlier version v{0}",
|
"예전 버전 v{0} 으로 되돌리기": "Roll back to earlier version v{0}",
|
||||||
"예전 버전으로 되돌립니다. 지금 버전의 설정 (동영상 목록 · 즐겨찾기 포함) 은 보관해 두었다가, 나중에 이 버전으로 다시 오면 되살릴 수 있습니다.": "This rolls back to an earlier version. The current version's settings (including the video list and bookmarks) are kept and can be restored when you come back to this version later.",
|
"예전 버전으로 되돌립니다. 지금 버전의 설정 (동영상 목록 · 즐겨찾기 포함) 은 보관해 두었다가, 나중에 이 버전으로 다시 오면 되살릴 수 있습니다.": "This rolls back to an earlier version. The current version's settings (including the video list and bookmarks) are kept and can be restored when you come back to this version later.",
|
||||||
"오늘 받을 수 있는 자막 수를 모두 사용했습니다. ({0})": "You have used all of today's subtitle downloads. ({0})",
|
"오늘 받을 수 있는 자막 수를 모두 사용했습니다. ({0})": "You have used all of today's subtitle downloads. ({0})",
|
||||||
|
"오래된 ssh-rsa 허용": "Allow old ssh-rsa",
|
||||||
"오래된 작업 기록 파일 (app.log.1 등). 지금 쓰는 기록은 남깁니다": "Old job log files (app.log.1 etc.). The current log is kept",
|
"오래된 작업 기록 파일 (app.log.1 등). 지금 쓰는 기록은 남깁니다": "Old job log files (app.log.1 etc.). The current log is kept",
|
||||||
"오른쪽": "Right",
|
"오른쪽": "Right",
|
||||||
"오른쪽 90°": "Right 90°",
|
"오른쪽 90°": "Right 90°",
|
||||||
|
|
@ -2117,6 +2152,7 @@
|
||||||
"준비 중…": "Preparing…",
|
"준비 중…": "Preparing…",
|
||||||
"준비하는 중": "Preparing",
|
"준비하는 중": "Preparing",
|
||||||
"줄 삭제": "Delete line",
|
"줄 삭제": "Delete line",
|
||||||
|
"중간에서 가로채는 공격일 수 있어 연결하지 않았습니다. 서버를 다시 설치했거나 키를 바꾼 것이 확실할 때만, 서버 설정의 [믿은 키 지우기] 뒤 다시 연결해 새 키를 확인하세요.": "This could be an interception attack, so the connection was not made. Only if you're sure the server was reinstalled or its key was changed, use [Forget trusted key] in the server settings and connect again to check the new key.",
|
||||||
"중국어": "Chinese",
|
"중국어": "Chinese",
|
||||||
"중국어 간체": "Chinese (Simplified)",
|
"중국어 간체": "Chinese (Simplified)",
|
||||||
"중국어 번체": "Chinese (Traditional)",
|
"중국어 번체": "Chinese (Traditional)",
|
||||||
|
|
@ -2169,6 +2205,7 @@
|
||||||
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "This is the Dual App (cloned app). Android may not grant this permission to dual apps. If you can't turn it on, open the original JJ_MKVMaker icon without the badge.",
|
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "This is the Dual App (cloned app). Android may not grant this permission to dual apps. If you can't turn it on, open the original JJ_MKVMaker icon without the badge.",
|
||||||
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "The last run did not end normally (last check {0}:{1}). It has been noted in the job log.",
|
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "The last run did not end normally (last check {0}:{1}). It has been noted in the job log.",
|
||||||
"지난 작업 기록": "Old job logs",
|
"지난 작업 기록": "Old job logs",
|
||||||
|
"지문 확인하고 연결": "Check fingerprint and connect",
|
||||||
"지우고 초기화": "Delete and reset",
|
"지우고 초기화": "Delete and reset",
|
||||||
"지우기": "Delete",
|
"지우기": "Delete",
|
||||||
"지우기 포함": "with delete",
|
"지우기 포함": "with delete",
|
||||||
|
|
@ -2235,6 +2272,7 @@
|
||||||
"처음 시작 시": "At app start",
|
"처음 시작 시": "At app start",
|
||||||
"처음 쓸 때 내려받기 (기본)": "Download on first use (default)",
|
"처음 쓸 때 내려받기 (기본)": "Download on first use (default)",
|
||||||
"처음 아이콘 (파랑)": "Original icon (blue)",
|
"처음 아이콘 (파랑)": "Original icon (blue)",
|
||||||
|
"처음 연결하는 서버입니다": "First connection to this server",
|
||||||
"처음 연결하는 서버입니다. 서버의 키 지문이 맞는지 확인하세요: {0} ({1})": "This is the first connection to this server. Check that the server's key fingerprint is correct: {0} ({1})",
|
"처음 연결하는 서버입니다. 서버의 키 지문이 맞는지 확인하세요: {0} ({1})": "This is the first connection to this server. Check that the server's key fingerprint is correct: {0} ({1})",
|
||||||
"처음 찾을 때 끝까지 찾기 전의 결과입니다. 더 찾으려면 다시 찾으세요.": "These results are from before the search finished. Search again to find more.",
|
"처음 찾을 때 끝까지 찾기 전의 결과입니다. 더 찾으려면 다시 찾으세요.": "These results are from before the search finished. Search again to find more.",
|
||||||
"처음대로": "Reset to default",
|
"처음대로": "Reset to default",
|
||||||
|
|
@ -2372,6 +2410,7 @@
|
||||||
"편집 목록에 추가 (MKV 만들기 대상)": "Add to the edit list (Make MKV target)",
|
"편집 목록에 추가 (MKV 만들기 대상)": "Add to the edit list (Make MKV target)",
|
||||||
"평일 9~18시": "Weekdays 9-18",
|
"평일 9~18시": "Weekdays 9-18",
|
||||||
"포르투갈어": "Portuguese",
|
"포르투갈어": "Portuguese",
|
||||||
|
"포트": "Port",
|
||||||
"포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)": "Included components and licenses (with full GPL · LGPL texts)",
|
"포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)": "Included components and licenses (with full GPL · LGPL texts)",
|
||||||
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "Included components and licenses (THIRD_PARTY_NOTICES.txt)",
|
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "Included components and licenses (THIRD_PARTY_NOTICES.txt)",
|
||||||
"폰 세로 목록 크기": "List size (portrait phone)",
|
"폰 세로 목록 크기": "List size (portrait phone)",
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,10 @@
|
||||||
" 외 {0}개": " ほか {0} 件",
|
" 외 {0}개": " ほか {0} 件",
|
||||||
" 지금 설치됨 ({0}MB)": " インストール済み({0}MB)",
|
" 지금 설치됨 ({0}MB)": " インストール済み({0}MB)",
|
||||||
"\"{0}\" 검색 결과 {1}개": "「{0}」の検索結果 {1}件",
|
"\"{0}\" 검색 결과 {1}개": "「{0}」の検索結果 {1}件",
|
||||||
|
"\"{0}\" 서버의 키가 바뀌었습니다": "サーバー「{0}」の鍵が変わりました",
|
||||||
"\"{0}\" 안에 만듭니다": "「{0}」の中に作成します",
|
"\"{0}\" 안에 만듭니다": "「{0}」の中に作成します",
|
||||||
"\"{0}\" 에 맞는 항목이 없습니다": "「{0}」に合う項目はありません",
|
"\"{0}\" 에 맞는 항목이 없습니다": "「{0}」に合う項目はありません",
|
||||||
|
"\"{0}\" 에 처음 연결합니다": "「{0}」に初めて接続します",
|
||||||
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "「{0}」にはなく、対象「{1}」にだけある項目です。削除を含めて同期すると対象から削除されます (元に戻せません)。",
|
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "「{0}」にはなく、対象「{1}」にだけある項目です。削除を含めて同期すると対象から削除されます (元に戻せません)。",
|
||||||
"\"{0}\" 은(는) 비밀번호가 걸려 있습니다.": "「{0}」にはパスワードが設定されています。",
|
"\"{0}\" 은(는) 비밀번호가 걸려 있습니다.": "「{0}」にはパスワードが設定されています。",
|
||||||
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "「{0}」を「{1}」フォルダーに移動しますか?",
|
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "「{0}」を「{1}」フォルダーに移動しますか?",
|
||||||
|
|
@ -260,10 +262,15 @@
|
||||||
"Rsync 확인 창 ({0}): {1}": "Rsync 確認ウィンドウ ({0}): {1}",
|
"Rsync 확인 창 ({0}): {1}": "Rsync 確認ウィンドウ ({0}): {1}",
|
||||||
"SD 카드": "SD カード",
|
"SD 카드": "SD カード",
|
||||||
"SD 카드가 꽂혀 있는지, 네트워크 (NAS · VPN) 가 연결되어 있는지, 폴더 권한이 있는지 확인한 뒤 [다시 시도] 를 누르세요.": "SD カードが挿さっているか、ネットワーク (NAS・VPN) が接続されているか、フォルダーの権限があるかを確認してから [再試行] を押してください。",
|
"SD 카드가 꽂혀 있는지, 네트워크 (NAS · VPN) 가 연결되어 있는지, 폴더 권한이 있는지 확인한 뒤 [다시 시도] 를 누르세요.": "SD カードが挿さっているか、ネットワーク (NAS・VPN) が接続されているか、フォルダーの権限があるかを確認してから [再試行] を押してください。",
|
||||||
|
"SFTP 서버 \"{0}\" 을(를) 지울까요?": "SFTP サーバー「{0}」を削除しますか?",
|
||||||
|
"SFTP 서버 고치기": "SFTP サーバーを編集",
|
||||||
"SFTP 서버 설정이 없습니다 ({0})": "この SFTP サーバーの設定がありません ({0})",
|
"SFTP 서버 설정이 없습니다 ({0})": "この SFTP サーバーの設定がありません ({0})",
|
||||||
"SFTP 서버 주소가 없습니다": "SFTP サーバーのアドレスがありません",
|
"SFTP 서버 주소가 없습니다": "SFTP サーバーのアドレスがありません",
|
||||||
|
"SFTP 서버 추가": "SFTP サーバーを追加",
|
||||||
"SFTP 에서 받는 중": "SFTP から受信中",
|
"SFTP 에서 받는 중": "SFTP から受信中",
|
||||||
|
"SHA-1 서명 · 키 교환을 씁니다. 오래된 NAS 가 다른 방식을 모를 때만 켜세요 (보안이 약함)": "SHA-1 の署名・鍵交換を使います。古い NAS が他の方式に対応していない場合のみオンにしてください (セキュリティが弱い)",
|
||||||
"SRT 형식이 아니거나 비어 있습니다.": "SRT 形式ではないか、空です。",
|
"SRT 형식이 아니거나 비어 있습니다.": "SRT 形式ではないか、空です。",
|
||||||
|
"SSH 서버 (NAS · 리눅스) 의 폴더를 파일 탐색기 · Rsync 화면에서 열고 복사 · 동기화합니다 (위쪽 탭). 비밀번호 · 개인 키 · 키 암호는 이 기기의 안전 저장소에만 둡니다. 처음 연결할 때 서버의 키 지문을 확인합니다.": "SSH サーバー (NAS・Linux) のフォルダーをファイルエクスプローラー・Rsync 画面で開き、コピー・同期します (上部のタブ)。パスワード・秘密鍵・鍵のパスフレーズはこの端末の安全なストレージにのみ保存します。初回接続時にサーバーの鍵のフィンガープリントを確認します。",
|
||||||
"Stable Diffusion v1.5": "Stable Diffusion v1.5",
|
"Stable Diffusion v1.5": "Stable Diffusion v1.5",
|
||||||
"Stable Diffusion v1.5 는 CreativeML OpenRAIL-M 라이선스입니다. 상업적 사용을 포함해 쓸 수 있지만, 다음 용도로는 쓸 수 없습니다: 법을 어기는 일, 미성년자를 해치거나 이용하는 일, 남을 해치려는 거짓 정보, 개인 정보로 사람을 해치는 일, 차별 · 괴롭힘, 의료 · 법률 판단을 대신하는 일 등 (라이선스 부속서 A). 만든 그림에 대한 책임은 쓰는 사람에게 있습니다.": "Stable Diffusion v1.5 は CreativeML OpenRAIL-M ライセンスです。商用を含めて使えますが、次の用途には使えません: 法律に違反すること、未成年者を害したり利用したりすること、他人を害する目的の虚偽情報、個人情報で人を害すること、差別・嫌がらせ、医療・法律の判断の代わりにすること など (ライセンス付属書 A)。作成した画像の責任は使う人にあります。",
|
"Stable Diffusion v1.5 는 CreativeML OpenRAIL-M 라이선스입니다. 상업적 사용을 포함해 쓸 수 있지만, 다음 용도로는 쓸 수 없습니다: 법을 어기는 일, 미성년자를 해치거나 이용하는 일, 남을 해치려는 거짓 정보, 개인 정보로 사람을 해치는 일, 차별 · 괴롭힘, 의료 · 법률 판단을 대신하는 일 등 (라이선스 부속서 A). 만든 그림에 대한 책임은 쓰는 사람에게 있습니다.": "Stable Diffusion v1.5 は CreativeML OpenRAIL-M ライセンスです。商用を含めて使えますが、次の用途には使えません: 法律に違反すること、未成年者を害したり利用したりすること、他人を害する目的の虚偽情報、個人情報で人を害すること、差別・嫌がらせ、医療・法律の判断の代わりにすること など (ライセンス付属書 A)。作成した画像の責任は使う人にあります。",
|
||||||
"TAESD (빠른 디코더)": "TAESD (高速デコーダー)",
|
"TAESD (빠른 디코더)": "TAESD (高速デコーダー)",
|
||||||
|
|
@ -324,7 +331,12 @@
|
||||||
"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 · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV・SFTP はサーバーから中身をすべて読むため、時間がかかることがあります。分析しますか?",
|
||||||
|
"WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "WebDAV・SFTP の動画は MKV リストに追加できません。先にこの端末へコピーしてください。",
|
||||||
|
"WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV・SFTP のファイルは名前・サイズ・日付のみ表示します",
|
||||||
|
"WebDAV · SFTP 폴더로는 풀 수 없습니다. 이 기기의 폴더에서 풀어 주세요.": "WebDAV・SFTP フォルダーには展開できません。この端末のフォルダーで展開してください。",
|
||||||
|
"WebDAV · SFTP 항목은 ZIP 으로 묶을 수 없습니다. 이 기기의 파일 · 폴더를 골라 주세요.": "WebDAV・SFTP の項目は ZIP にまとめられません。この端末のファイル・フォルダーを選んでください。",
|
||||||
|
"WebDAV · SFTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV・SFTP: rsync の代わりにアプリが直接同期します (サイズ・更新日時で比較、オプションのうち -u・--delete・--remove-source-files に従う)。",
|
||||||
"WebDAV 서버 \"{0}\" 을(를) 지울까요?": "WebDAV サーバー「{0}」を削除しますか?",
|
"WebDAV 서버 \"{0}\" 을(를) 지울까요?": "WebDAV サーバー「{0}」を削除しますか?",
|
||||||
"WebDAV 서버 고치기": "WebDAV サーバーを編集",
|
"WebDAV 서버 고치기": "WebDAV サーバーを編集",
|
||||||
"WebDAV 서버 목록과 그 저장된 비밀번호도 지워집니다. 동영상 목록 · 즐겨찾기 · 파일은 그대로입니다": "WebDAV サーバー一覧とその保存したパスワードも消えます。動画リスト・お気に入り・ファイルはそのままです",
|
"WebDAV 서버 목록과 그 저장된 비밀번호도 지워집니다. 동영상 목록 · 즐겨찾기 · 파일은 그대로입니다": "WebDAV サーバー一覧とその保存したパスワードも消えます。動画リスト・お気に入り・ファイルはそのままです",
|
||||||
|
|
@ -332,11 +344,6 @@
|
||||||
"WebDAV 서버 추가": "WebDAV サーバーを追加",
|
"WebDAV 서버 추가": "WebDAV サーバーを追加",
|
||||||
"WebDAV 에서 받는 중": "WebDAV からダウンロード中",
|
"WebDAV 에서 받는 중": "WebDAV からダウンロード中",
|
||||||
"WebDAV 원본에 씁니다 (PC 의 폴더는 바뀌면 바로 맞춤). 짧을수록 빨리 맞추지만 서버 · 배터리를 더 씁니다.": "WebDAV の元に使います (PC のフォルダーは変わるとすぐ同期)。短いほど早く同期しますが、サーバー・バッテリーをより使います。",
|
"WebDAV 원본에 씁니다 (PC 의 폴더는 바뀌면 바로 맞춤). 짧을수록 빨리 맞추지만 서버 · 배터리를 더 씁니다.": "WebDAV の元に使います (PC のフォルダーは変わるとすぐ同期)。短いほど早く同期しますが、サーバー・バッテリーをより使います。",
|
||||||
"WebDAV 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "WebDAV の動画は MKV リストに追加できません。先にこの端末へコピーしてください。",
|
|
||||||
"WebDAV 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV のファイルは名前・サイズ・日付のみ表示します",
|
|
||||||
"WebDAV 폴더로는 풀 수 없습니다. 이 기기의 폴더에서 풀어 주세요.": "WebDAV フォルダーには展開できません。この端末のフォルダーで展開してください。",
|
|
||||||
"WebDAV 항목은 ZIP 으로 묶을 수 없습니다. 이 기기의 파일 · 폴더를 골라 주세요.": "WebDAV の項目は ZIP にまとめられません。この端末のファイル・フォルダーを選んでください。",
|
|
||||||
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV: rsync の代わりにアプリが直接同期します (サイズ・更新日時で比較、オプションのうち -u・--delete・--remove-source-files に従う)。",
|
|
||||||
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --remove-source-files 만 따름).": "WebDAV: rsync の代わりにアプリが直接同期します (サイズ・更新日時で比較、オプションは -u・--remove-source-files のみ反映)。",
|
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --remove-source-files 만 따름).": "WebDAV: rsync の代わりにアプリが直接同期します (サイズ・更新日時で比較、オプションは -u・--remove-source-files のみ反映)。",
|
||||||
"WebM (동영상)": "WebM(動画)",
|
"WebM (동영상)": "WebM(動画)",
|
||||||
"WebP 등은 PNG 로 저장합니다": "WebP などは PNG で保存します",
|
"WebP 등은 PNG 로 저장합니다": "WebP などは PNG で保存します",
|
||||||
|
|
@ -576,6 +583,7 @@
|
||||||
"가로 16:9": "横 16:9",
|
"가로 16:9": "横 16:9",
|
||||||
"가로 고정": "横向き固定",
|
"가로 고정": "横向き固定",
|
||||||
"가로 고정 · 세로 고정, 또는 자동 (기기를 돌리는 대로, 기기의 자동 회전 설정을 따름)": "横向き固定・縦向き固定、または自動(端末の向きに合わせ、端末の自動回転設定に従う)",
|
"가로 고정 · 세로 고정, 또는 자동 (기기를 돌리는 대로, 기기의 자동 회전 설정을 따름)": "横向き固定・縦向き固定、または自動(端末の向きに合わせ、端末の自動回転設定に従う)",
|
||||||
|
"가리기": "隠す",
|
||||||
"가입 → 프로필 → API consumers → New consumer": "登録 → プロフィール → API consumers → New consumer",
|
"가입 → 프로필 → API consumers → New consumer": "登録 → プロフィール → API consumers → New consumer",
|
||||||
"가장 빠른 처리 장치를 찾는 중 (처음 한 번): {0}": "最も速い処理装置を探しています (初回のみ): {0}",
|
"가장 빠른 처리 장치를 찾는 중 (처음 한 번): {0}": "最も速い処理装置を探しています (初回のみ): {0}",
|
||||||
"가져오기": "取り込む",
|
"가져오기": "取り込む",
|
||||||
|
|
@ -610,6 +618,15 @@
|
||||||
"같은 확장자 모두": "同じ拡張子をすべて",
|
"같은 확장자 모두": "同じ拡張子をすべて",
|
||||||
"같은 확장자 모두 (.{0})": "同じ拡張子をすべて (.{0})",
|
"같은 확장자 모두 (.{0})": "同じ拡張子をすべて (.{0})",
|
||||||
"개수": "枚数",
|
"개수": "枚数",
|
||||||
|
"개인 키": "秘密鍵",
|
||||||
|
"개인 키 바꾸기": "秘密鍵を変更",
|
||||||
|
"개인 키 빼기": "秘密鍵を外す",
|
||||||
|
"개인 키 암호 (없으면 비움)": "秘密鍵のパスフレーズ (なければ空欄)",
|
||||||
|
"개인 키 있음": "秘密鍵あり",
|
||||||
|
"개인 키 파일 고르기": "秘密鍵ファイルを選択",
|
||||||
|
"개인 키 파일이 아닙니다 (OpenSSH · PEM 형식의 비밀 키를 고르세요, .pub 아님)": "秘密鍵ファイルではありません (OpenSSH・PEM 形式の秘密鍵を選んでください。.pub ではありません)",
|
||||||
|
"개인 키 파일이 아닙니다 (너무 큼)": "秘密鍵ファイルではありません (大きすぎます)",
|
||||||
|
"개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "秘密鍵を登録しました (元のファイルのパスは保存しません)",
|
||||||
"개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)": "秘密鍵を読み込めません (OpenSSH・PEM 形式)",
|
"개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)": "秘密鍵を読み込めません (OpenSSH・PEM 形式)",
|
||||||
"개인 키의 암호가 맞지 않습니다": "秘密鍵のパスフレーズが正しくありません",
|
"개인 키의 암호가 맞지 않습니다": "秘密鍵のパスフレーズが正しくありません",
|
||||||
"거꾸로": "逆順",
|
"거꾸로": "逆順",
|
||||||
|
|
@ -634,6 +651,7 @@
|
||||||
"경로 입력": "パスを入力",
|
"경로 입력": "パスを入力",
|
||||||
"경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "パスを入力 (例: D:\\動画・\\\\NAS\\共有)",
|
"경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "パスを入力 (例: D:\\動画・\\\\NAS\\共有)",
|
||||||
"경로 입력 · WebDAV": "パス入力 · WebDAV",
|
"경로 입력 · WebDAV": "パス入力 · WebDAV",
|
||||||
|
"경로 입력 · 네트워크 서버": "パス入力・ネットワークサーバー",
|
||||||
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "パスが長すぎてごみ箱に入れられません (Windows のごみ箱は 260 文字を超えるパスを受け付けません)。",
|
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "パスが長すぎてごみ箱に入れられません (Windows のごみ箱は 260 文字を超えるパスを受け付けません)。",
|
||||||
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "パスが長すぎます (Windows のごみ箱は 260 文字を超えるパスを受け付けません) - 完全に削除はできます",
|
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "パスが長すぎます (Windows のごみ箱は 260 文字を超えるパスを受け付けません) - 完全に削除はできます",
|
||||||
"경로를 복사했습니다: {0}": "パスをコピーしました: {0}",
|
"경로를 복사했습니다: {0}": "パスをコピーしました: {0}",
|
||||||
|
|
@ -1131,6 +1149,10 @@
|
||||||
"미리보기: 목록에서 동영상을 고르세요": "プレビュー: リストで動画を選んでください",
|
"미리보기: 목록에서 동영상을 고르세요": "プレビュー: リストで動画を選んでください",
|
||||||
"미얀마어": "ミャンマー語",
|
"미얀마어": "ミャンマー語",
|
||||||
"미지정": "未指定",
|
"미지정": "未指定",
|
||||||
|
"믿고 연결": "信頼して接続",
|
||||||
|
"믿은 서버 키": "信頼したサーバー鍵",
|
||||||
|
"믿은 키 지우기": "信頼した鍵を削除",
|
||||||
|
"믿은 키를 지웠습니다. [연결 확인] 으로 새 키를 확인하세요": "信頼した鍵を削除しました。[接続確認] で新しい鍵を確認してください",
|
||||||
"바깥 폴더 (../)": "外側のフォルダー (../)",
|
"바깥 폴더 (../)": "外側のフォルダー (../)",
|
||||||
"바꾸거나 끄려면 지금 마스터 비밀번호 (또는 최상 비밀번호) 를 넣으세요.": "変更・オフにするには現在のマスターパスワード (または最上位パスワード) を入力してください。",
|
"바꾸거나 끄려면 지금 마스터 비밀번호 (또는 최상 비밀번호) 를 넣으세요.": "変更・オフにするには現在のマスターパスワード (または最上位パスワード) を入力してください。",
|
||||||
"바꾸기": "変更",
|
"바꾸기": "変更",
|
||||||
|
|
@ -1243,6 +1265,7 @@
|
||||||
"보낼 것 {0} / 있는 것 {1}": "送る方 {0} / 既存 {1}",
|
"보낼 것 {0} / 있는 것 {1}": "送る方 {0} / 既存 {1}",
|
||||||
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "見ていたフォルダーがなくなったため上のフォルダーに移動しました",
|
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "見ていたフォルダーがなくなったため上のフォルダーに移動しました",
|
||||||
"보안": "セキュリティ",
|
"보안": "セキュリティ",
|
||||||
|
"보이기": "表示",
|
||||||
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "表示するボタンを選び、ドラッグで順序を変えます。",
|
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "表示するボタンを選び、ドラッグで順序を変えます。",
|
||||||
"보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "補正には再エンコードが必要なため、コーデックを「元のまま」から H.264 に変更しました。",
|
"보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "補正には再エンコードが必要なため、コーデックを「元のまま」から H.264 に変更しました。",
|
||||||
"보조 DNS (비워 둘 수 있음)": "代替 DNS (空欄可)",
|
"보조 DNS (비워 둘 수 있음)": "代替 DNS (空欄可)",
|
||||||
|
|
@ -1296,6 +1319,7 @@
|
||||||
"비공개 · 회원 전용 영상입니다.": "非公開・メンバー限定の動画です。",
|
"비공개 · 회원 전용 영상입니다.": "非公開・メンバー限定の動画です。",
|
||||||
"비교하지 못했습니다: {0}": "比較できませんでした: {0}",
|
"비교하지 못했습니다: {0}": "比較できませんでした: {0}",
|
||||||
"비밀번호": "パスワード",
|
"비밀번호": "パスワード",
|
||||||
|
"비밀번호 (개인 키만 쓰면 비움)": "パスワード (秘密鍵だけを使う場合は空欄)",
|
||||||
"비밀번호 (선택)": "パスワード(任意)",
|
"비밀번호 (선택)": "パスワード(任意)",
|
||||||
"비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "パスワード・API キー (WebDAV・OpenSubtitles・AI サービス)",
|
"비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "パスワード・API キー (WebDAV・OpenSubtitles・AI サービス)",
|
||||||
"비밀번호 · 자막 API 키는 백업하지 않으므로 다시 넣어 주세요. 자동 백업은 하루에 한 번쯤 만들어져 그 뒤에 바꾼 것은 빠졌을 수 있습니다.": "パスワード・字幕 API キーはバックアップされないので、もう一度入力してください。自動バックアップは 1 日に 1 回程度作成されるため、その後に変更したものは含まれていない場合があります。",
|
"비밀번호 · 자막 API 키는 백업하지 않으므로 다시 넣어 주세요. 자동 백업은 하루에 한 번쯤 만들어져 그 뒤에 바꾼 것은 빠졌을 수 있습니다.": "パスワード・字幕 API キーはバックアップされないので、もう一度入力してください。自動バックアップは 1 日に 1 回程度作成されるため、その後に変更したものは含まれていない場合があります。",
|
||||||
|
|
@ -1367,9 +1391,13 @@
|
||||||
"서버 ({0}) 와의 연결이 끊겼습니다": "サーバー ({0}) との接続が切れました",
|
"서버 ({0}) 와의 연결이 끊겼습니다": "サーバー ({0}) との接続が切れました",
|
||||||
"서버 · 서비스 (기기 밖)": "サーバー・サービス (端末外)",
|
"서버 · 서비스 (기기 밖)": "サーバー・サービス (端末外)",
|
||||||
"서버 설정 고치기": "サーバー設定を修正",
|
"서버 설정 고치기": "サーバー設定を修正",
|
||||||
|
"서버 설정과 이 기기에 저장된 비밀번호 · 개인 키 · 믿은 서버 키를 지웁니다. 서버의 파일은 그대로입니다. 이 서버를 쓰는 동기화 · 복사는 다시 넣을 때까지 연결되지 않습니다.": "サーバー設定と、この端末に保存されたパスワード・秘密鍵・信頼したサーバー鍵を削除します。サーバーのファイルはそのままです。このサーバーを使う同期・コピーは、再登録するまで接続されません。",
|
||||||
"서버 설정과 이 기기에 저장된 비밀번호를 지웁니다. 서버의 파일은 그대로입니다. 이 서버를 쓰는 동기화 · 복사는 다시 넣을 때까지 연결되지 않습니다.": "サーバー設定とこの端末に保存したパスワードを削除します。サーバーのファイルはそのままです。このサーバーを使う同期・コピーは、再度追加するまで接続できません。",
|
"서버 설정과 이 기기에 저장된 비밀번호를 지웁니다. 서버의 파일은 그대로입니다. 이 서버를 쓰는 동기화 · 복사는 다시 넣을 때까지 연결되지 않습니다.": "サーバー設定とこの端末に保存したパスワードを削除します。サーバーのファイルはそのままです。このサーバーを使う同期・コピーは、再度追加するまで接続できません。",
|
||||||
"서버 인증서를 확인할 수 없습니다": "サーバー証明書を確認できません",
|
"서버 인증서를 확인할 수 없습니다": "サーバー証明書を確認できません",
|
||||||
"서버 저장 공간 부족": "サーバーの空き容量不足",
|
"서버 저장 공간 부족": "サーバーの空き容量不足",
|
||||||
|
"서버 주소": "サーバーアドレス",
|
||||||
|
"서버 키 아직 확인 안 함": "サーバー鍵は未確認",
|
||||||
|
"서버 키: 아직 확인 안 함 ([연결 확인] 을 누르면 지문을 보이고 묻습니다)": "サーバー鍵: 未確認 ([接続確認] を押すとフィンガープリントを表示して確認します)",
|
||||||
"서버가 https 주소로 옮기라고 합니다 ({0}). 서버 설정의 주소를 https:// 로 바꾸세요.": "サーバーが https アドレスへの移動を求めています ({0})。サーバー設定のアドレスを https:// に変更してください。",
|
"서버가 https 주소로 옮기라고 합니다 ({0}). 서버 설정의 주소를 https:// 로 바꾸세요.": "サーバーが https アドレスへの移動を求めています ({0})。サーバー設定のアドレスを https:// に変更してください。",
|
||||||
"서버가 거절했습니다 ({0}): {1}": "サーバーが拒否しました ({0}): {1}",
|
"서버가 거절했습니다 ({0}): {1}": "サーバーが拒否しました ({0}): {1}",
|
||||||
"서버가 그림을 돌려주지 않았습니다": "サーバーが画像を返しませんでした",
|
"서버가 그림을 돌려주지 않았습니다": "サーバーが画像を返しませんでした",
|
||||||
|
|
@ -1382,6 +1410,8 @@
|
||||||
"서버와 같은 네트워크에 있거나 VPN (예: Tailscale) 이 켜져 있어야 합니다. 켠 뒤 [다시 시도] 를 누르세요.": "サーバーと同じネットワークにいるか、VPN (例: Tailscale) をオンにする必要があります。オンにしてから [再試行] を押してください。",
|
"서버와 같은 네트워크에 있거나 VPN (예: Tailscale) 이 켜져 있어야 합니다. 켠 뒤 [다시 시도] 를 누르세요.": "サーバーと同じネットワークにいるか、VPN (例: Tailscale) をオンにする必要があります。オンにしてから [再試行] を押してください。",
|
||||||
"서버와 암호 방식을 맞추지 못했습니다. 오래된 서버면 서버 설정의 \"오래된 ssh-rsa 허용\" 을 켜 보세요": "サーバーと暗号方式を合わせられませんでした。古いサーバーの場合はサーバー設定の「古い ssh-rsa を許可」をオンにしてみてください",
|
"서버와 암호 방식을 맞추지 못했습니다. 오래된 서버면 서버 설정의 \"오래된 ssh-rsa 허용\" 을 켜 보세요": "サーバーと暗号方式を合わせられませんでした。古いサーバーの場合はサーバー設定の「古い ssh-rsa を許可」をオンにしてみてください",
|
||||||
"서버와 암호 방식을 맞추지 못했습니다: {0}": "サーバーと暗号方式を合わせられませんでした: {0}",
|
"서버와 암호 방식을 맞추지 못했습니다: {0}": "サーバーと暗号方式を合わせられませんでした: {0}",
|
||||||
|
"서버의 키 지문이 서버 (NAS 설정 · ssh-keygen -lf) 에서 본 것과 같은지 확인하세요. 믿으면 이 기기에 저장하고, 다음부터 키가 바뀌면 연결하지 않고 알립니다.": "サーバーの鍵のフィンガープリントがサーバー側 (NAS の設定・ssh-keygen -lf) で確認したものと同じか確認してください。信頼するとこの端末に保存し、以後鍵が変わった場合は接続せずにお知らせします。",
|
||||||
|
"서버의 키가 바뀌었습니다": "サーバーの鍵が変わりました",
|
||||||
"서버의 키가 바뀌었습니다 - 중간에서 가로채는 공격일 수 있어 연결하지 않았습니다. 예전: {0} · 지금: {1}": "サーバーの鍵が変わりました。中間者攻撃の可能性があるため接続しませんでした。以前: {0} · 現在: {1}",
|
"서버의 키가 바뀌었습니다 - 중간에서 가로채는 공격일 수 있어 연결하지 않았습니다. 예전: {0} · 지금: {1}": "サーバーの鍵が変わりました。中間者攻撃の可能性があるため接続しませんでした。以前: {0} · 現在: {1}",
|
||||||
"서비스 고치기": "サービスを編集",
|
"서비스 고치기": "サービスを編集",
|
||||||
"서비스 설정과 저장된 API 키를 지웁니다.": "サービス設定と保存した API キーを削除します。",
|
"서비스 설정과 저장된 API 키를 지웁니다.": "サービス設定と保存した API キーを削除します。",
|
||||||
|
|
@ -1469,6 +1499,7 @@
|
||||||
"시작 · 웹 브라우저": "起動・ウェブブラウザー",
|
"시작 · 웹 브라우저": "起動・ウェブブラウザー",
|
||||||
"시작 메뉴에 등록": "スタートメニューに登録",
|
"시작 메뉴에 등록": "スタートメニューに登録",
|
||||||
"시작 안 함 (모니터링에서 시작)": "開始しない (モニタリングで開始)",
|
"시작 안 함 (모니터링에서 시작)": "開始しない (モニタリングで開始)",
|
||||||
|
"시작 폴더 (비우면 홈 폴더)": "開始フォルダー (空欄ならホームフォルダー)",
|
||||||
"시작: {0} → {1}{2}": "開始: {0} → {1}{2}",
|
"시작: {0} → {1}{2}": "開始: {0} → {1}{2}",
|
||||||
"시작하는 중…": "開始中…",
|
"시작하는 중…": "開始中…",
|
||||||
"시작하지 못했습니다: {0}": "開始できませんでした: {0}",
|
"시작하지 못했습니다: {0}": "開始できませんでした: {0}",
|
||||||
|
|
@ -1631,6 +1662,7 @@
|
||||||
"예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 속도 제한은 파일 탐색기의 속도 제한을 함께 씀. 새로 맞추는 폴더 쌍에 씁니다 - 이미 실행한 쌍은 기억한 옵션을 쓰며 모니터링 > 복사 · rsync 에서 따로 고칩니다": "例: -avPog (アーカイブ・詳細・進行状況・所有者・グループ)。速度制限はファイルエクスプローラーの速度制限も使います。新しいフォルダーの組に使います - すでに実行した組は記憶したオプションを使い、モニタリング > コピー・rsync で個別に変更します",
|
"예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 속도 제한은 파일 탐색기의 속도 제한을 함께 씀. 새로 맞추는 폴더 쌍에 씁니다 - 이미 실행한 쌍은 기억한 옵션을 쓰며 모니터링 > 복사 · rsync 에서 따로 고칩니다": "例: -avPog (アーカイブ・詳細・進行状況・所有者・グループ)。速度制限はファイルエクスプローラーの速度制限も使います。新しいフォルダーの組に使います - すでに実行した組は記憶したオプションを使い、モニタリング > コピー・rsync で個別に変更します",
|
||||||
"예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 이동은 --remove-source-files 를 자동으로 붙임": "例: -avPog (アーカイブ · 詳細 · 進捗 · 所有者 · グループ)。移動では --remove-source-files を自動で付加",
|
"예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 이동은 --remove-source-files 를 자동으로 붙임": "例: -avPog (アーカイブ · 詳細 · 進捗 · 所有者 · グループ)。移動では --remove-source-files を自動で付加",
|
||||||
"예: /E /COPY:DAT /DCOPY:T /R:2 /W:2. 이동은 /MOVE 를 자동으로 붙임": "例: /E /COPY:DAT /DCOPY:T /R:2 /W:2。移動では /MOVE を自動で付加",
|
"예: /E /COPY:DAT /DCOPY:T /R:2 /W:2. 이동은 /MOVE 를 자동으로 붙임": "例: /E /COPY:DAT /DCOPY:T /R:2 /W:2。移動では /MOVE を自動で付加",
|
||||||
|
"예: /volume1/video · 탭의 맨 위가 됩니다": "例: /volume1/video · タブの最上位になります",
|
||||||
"예: 0 9-17 * * 1-5 (평일 9시~17시 59분) · 0 0-5 * * * (매일 0~5시). 분은 무시하고 1시간 단위": "例: 0 9-17 * * 1-5 (平日 9 時~17 時 59 分) · 0 0-5 * * * (毎日 0~5 時)。分は無視し 1 時間単位",
|
"예: 0 9-17 * * 1-5 (평일 9시~17시 59분) · 0 0-5 * * * (매일 0~5시). 분은 무시하고 1시간 단위": "例: 0 9-17 * * 1-5 (平日 9 時~17 時 59 分) · 0 0-5 * * * (毎日 0~5 時)。分は無視し 1 時間単位",
|
||||||
"예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "例: Ctrl+Shift+X(Ctrl・Shift・Alt・Win + 英字・数字・F1〜F12)",
|
"예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "例: Ctrl+Shift+X(Ctrl・Shift・Alt・Win + 英字・数字・F1〜F12)",
|
||||||
"예: S01E · *.srt": "例: S01E · *.srt",
|
"예: S01E · *.srt": "例: S01E · *.srt",
|
||||||
|
|
@ -1638,12 +1670,15 @@
|
||||||
"예: http://100.x.x.x:8188 (ComfyUI)": "例: http://100.x.x.x:8188 (ComfyUI)",
|
"예: http://100.x.x.x:8188 (ComfyUI)": "例: http://100.x.x.x:8188 (ComfyUI)",
|
||||||
"예: https://api.openai.com (경로 /v1/images/generations 를 붙여 부름)": "例: https://api.openai.com (パス /v1/images/generations を付けて呼び出し)",
|
"예: https://api.openai.com (경로 /v1/images/generations 를 붙여 부름)": "例: https://api.openai.com (パス /v1/images/generations を付けて呼び出し)",
|
||||||
"예: https://nas.local:5006/home · https://cloud.example.com/remote.php/dav/files/아이디": "例: https://nas.local:5006/home · https://cloud.example.com/remote.php/dav/files/ユーザー名",
|
"예: https://nas.local:5006/home · https://cloud.example.com/remote.php/dav/files/아이디": "例: https://nas.local:5006/home · https://cloud.example.com/remote.php/dav/files/ユーザー名",
|
||||||
|
"예: nas.local · 192.168.0.10 · 100.x.y.z (Tailscale)": "例: nas.local · 192.168.0.10 · 100.x.y.z (Tailscale)",
|
||||||
"예상 시간": "予想時間",
|
"예상 시간": "予想時間",
|
||||||
|
"예전": "以前",
|
||||||
"예전 구조의 프로그램 파일 정리 ({0}개): {1}": "旧構成のプログラムファイルを整理({0}個): {1}",
|
"예전 구조의 프로그램 파일 정리 ({0}개): {1}": "旧構成のプログラムファイルを整理({0}個): {1}",
|
||||||
"예전 버전": "以前のバージョン",
|
"예전 버전": "以前のバージョン",
|
||||||
"예전 버전 v{0} 으로 되돌리기": "以前のバージョン v{0} に戻す",
|
"예전 버전 v{0} 으로 되돌리기": "以前のバージョン v{0} に戻す",
|
||||||
"예전 버전으로 되돌립니다. 지금 버전의 설정 (동영상 목록 · 즐겨찾기 포함) 은 보관해 두었다가, 나중에 이 버전으로 다시 오면 되살릴 수 있습니다.": "以前のバージョンに戻します。現在のバージョンの設定 (動画リスト · ブックマークを含む) は保管しておき、あとでこのバージョンに戻ると復元できます。",
|
"예전 버전으로 되돌립니다. 지금 버전의 설정 (동영상 목록 · 즐겨찾기 포함) 은 보관해 두었다가, 나중에 이 버전으로 다시 오면 되살릴 수 있습니다.": "以前のバージョンに戻します。現在のバージョンの設定 (動画リスト · ブックマークを含む) は保管しておき、あとでこのバージョンに戻ると復元できます。",
|
||||||
"오늘 받을 수 있는 자막 수를 모두 사용했습니다. ({0})": "今日ダウンロードできる字幕数をすべて使いました。({0})",
|
"오늘 받을 수 있는 자막 수를 모두 사용했습니다. ({0})": "今日ダウンロードできる字幕数をすべて使いました。({0})",
|
||||||
|
"오래된 ssh-rsa 허용": "古い ssh-rsa を許可",
|
||||||
"오래된 작업 기록 파일 (app.log.1 등). 지금 쓰는 기록은 남깁니다": "古い作業記録ファイル(app.log.1 など)。現在の記録は残します",
|
"오래된 작업 기록 파일 (app.log.1 등). 지금 쓰는 기록은 남깁니다": "古い作業記録ファイル(app.log.1 など)。現在の記録は残します",
|
||||||
"오른쪽": "右",
|
"오른쪽": "右",
|
||||||
"오른쪽 90°": "右 90°",
|
"오른쪽 90°": "右 90°",
|
||||||
|
|
@ -2117,6 +2152,7 @@
|
||||||
"준비 중…": "準備中…",
|
"준비 중…": "準備中…",
|
||||||
"준비하는 중": "準備中",
|
"준비하는 중": "準備中",
|
||||||
"줄 삭제": "行を削除",
|
"줄 삭제": "行を削除",
|
||||||
|
"중간에서 가로채는 공격일 수 있어 연결하지 않았습니다. 서버를 다시 설치했거나 키를 바꾼 것이 확실할 때만, 서버 설정의 [믿은 키 지우기] 뒤 다시 연결해 새 키를 확인하세요.": "中間者攻撃の可能性があるため接続しませんでした。サーバーを再インストールした、または鍵を変更したことが確かな場合のみ、サーバー設定の [信頼した鍵を削除] の後に再接続して新しい鍵を確認してください。",
|
||||||
"중국어": "中国語",
|
"중국어": "中国語",
|
||||||
"중국어 간체": "中国語(簡体字)",
|
"중국어 간체": "中国語(簡体字)",
|
||||||
"중국어 번체": "中国語(繁体字)",
|
"중국어 번체": "中国語(繁体字)",
|
||||||
|
|
@ -2169,6 +2205,7 @@
|
||||||
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "これはデュアルアプリ (複製したアプリ) です。Android はデュアルアプリにこの権限を与えない場合があります。許可画面でオンにできない場合は、バッジのない元の JJ_MKVMaker アイコンから開いてください。",
|
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "これはデュアルアプリ (複製したアプリ) です。Android はデュアルアプリにこの権限を与えない場合があります。許可画面でオンにできない場合は、バッジのない元の JJ_MKVMaker アイコンから開いてください。",
|
||||||
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "前回の実行が正常に終了しませんでした(最終確認 {0}:{1})。作業記録に残しました。",
|
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "前回の実行が正常に終了しませんでした(最終確認 {0}:{1})。作業記録に残しました。",
|
||||||
"지난 작업 기록": "過去の作業記録",
|
"지난 작업 기록": "過去の作業記録",
|
||||||
|
"지문 확인하고 연결": "フィンガープリントを確認して接続",
|
||||||
"지우고 초기화": "削除して初期化",
|
"지우고 초기화": "削除して初期化",
|
||||||
"지우기": "削除",
|
"지우기": "削除",
|
||||||
"지우기 포함": "削除あり",
|
"지우기 포함": "削除あり",
|
||||||
|
|
@ -2235,6 +2272,7 @@
|
||||||
"처음 시작 시": "起動時",
|
"처음 시작 시": "起動時",
|
||||||
"처음 쓸 때 내려받기 (기본)": "初めて使うときにダウンロード (既定)",
|
"처음 쓸 때 내려받기 (기본)": "初めて使うときにダウンロード (既定)",
|
||||||
"처음 아이콘 (파랑)": "最初のアイコン(青)",
|
"처음 아이콘 (파랑)": "最初のアイコン(青)",
|
||||||
|
"처음 연결하는 서버입니다": "初めて接続するサーバーです",
|
||||||
"처음 연결하는 서버입니다. 서버의 키 지문이 맞는지 확인하세요: {0} ({1})": "初めて接続するサーバーです。サーバーの鍵のフィンガープリントが正しいか確認してください: {0} ({1})",
|
"처음 연결하는 서버입니다. 서버의 키 지문이 맞는지 확인하세요: {0} ({1})": "初めて接続するサーバーです。サーバーの鍵のフィンガープリントが正しいか確認してください: {0} ({1})",
|
||||||
"처음 찾을 때 끝까지 찾기 전의 결과입니다. 더 찾으려면 다시 찾으세요.": "最初の検索が終わる前の結果です。さらに探すには検索し直してください。",
|
"처음 찾을 때 끝까지 찾기 전의 결과입니다. 더 찾으려면 다시 찾으세요.": "最初の検索が終わる前の結果です。さらに探すには検索し直してください。",
|
||||||
"처음대로": "既定に戻す",
|
"처음대로": "既定に戻す",
|
||||||
|
|
@ -2372,6 +2410,7 @@
|
||||||
"편집 목록에 추가 (MKV 만들기 대상)": "編集リストに追加(MKV 作成の対象)",
|
"편집 목록에 추가 (MKV 만들기 대상)": "編集リストに追加(MKV 作成の対象)",
|
||||||
"평일 9~18시": "平日 9~18 時",
|
"평일 9~18시": "平日 9~18 時",
|
||||||
"포르투갈어": "ポルトガル語",
|
"포르투갈어": "ポルトガル語",
|
||||||
|
"포트": "ポート",
|
||||||
"포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)": "含まれるコンポーネントとライセンス (GPL・LGPL 全文を含む)",
|
"포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)": "含まれるコンポーネントとライセンス (GPL・LGPL 全文を含む)",
|
||||||
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "含まれるコンポーネントとライセンス(THIRD_PARTY_NOTICES.txt)",
|
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "含まれるコンポーネントとライセンス(THIRD_PARTY_NOTICES.txt)",
|
||||||
"폰 세로 목록 크기": "縦向きスマホのリストの大きさ",
|
"폰 세로 목록 크기": "縦向きスマホのリストの大きさ",
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,10 @@
|
||||||
" 외 {0}개": " 等 {0} 个",
|
" 외 {0}개": " 等 {0} 个",
|
||||||
" 지금 설치됨 ({0}MB)": " 已安装({0}MB)",
|
" 지금 설치됨 ({0}MB)": " 已安装({0}MB)",
|
||||||
"\"{0}\" 검색 결과 {1}개": "“{0}”的搜索结果 {1} 个",
|
"\"{0}\" 검색 결과 {1}개": "“{0}”的搜索结果 {1} 个",
|
||||||
|
"\"{0}\" 서버의 키가 바뀌었습니다": "服务器\"{0}\"的密钥已更改",
|
||||||
"\"{0}\" 안에 만듭니다": "在“{0}”中创建",
|
"\"{0}\" 안에 만듭니다": "在“{0}”中创建",
|
||||||
"\"{0}\" 에 맞는 항목이 없습니다": "没有与“{0}”匹配的项目",
|
"\"{0}\" 에 맞는 항목이 없습니다": "没有与“{0}”匹配的项目",
|
||||||
|
"\"{0}\" 에 처음 연결합니다": "首次连接\"{0}\"",
|
||||||
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "这些项目不在“{0}”中,只在目标“{1}”中。包含删除同步后会从目标中删除 (无法撤销)。",
|
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "这些项目不在“{0}”中,只在目标“{1}”中。包含删除同步后会从目标中删除 (无法撤销)。",
|
||||||
"\"{0}\" 은(는) 비밀번호가 걸려 있습니다.": "“{0}”设有密码。",
|
"\"{0}\" 은(는) 비밀번호가 걸려 있습니다.": "“{0}”设有密码。",
|
||||||
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "将“{0}”移到“{1}”文件夹吗?",
|
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "将“{0}”移到“{1}”文件夹吗?",
|
||||||
|
|
@ -260,10 +262,15 @@
|
||||||
"Rsync 확인 창 ({0}): {1}": "Rsync 确认窗口 ({0}): {1}",
|
"Rsync 확인 창 ({0}): {1}": "Rsync 确认窗口 ({0}): {1}",
|
||||||
"SD 카드": "SD 卡",
|
"SD 카드": "SD 卡",
|
||||||
"SD 카드가 꽂혀 있는지, 네트워크 (NAS · VPN) 가 연결되어 있는지, 폴더 권한이 있는지 확인한 뒤 [다시 시도] 를 누르세요.": "请确认 SD 卡已插入、网络 (NAS · VPN) 已连接、拥有文件夹权限,然后按 [重试]。",
|
"SD 카드가 꽂혀 있는지, 네트워크 (NAS · VPN) 가 연결되어 있는지, 폴더 권한이 있는지 확인한 뒤 [다시 시도] 를 누르세요.": "请确认 SD 卡已插入、网络 (NAS · VPN) 已连接、拥有文件夹权限,然后按 [重试]。",
|
||||||
|
"SFTP 서버 \"{0}\" 을(를) 지울까요?": "要删除 SFTP 服务器\"{0}\"吗?",
|
||||||
|
"SFTP 서버 고치기": "编辑 SFTP 服务器",
|
||||||
"SFTP 서버 설정이 없습니다 ({0})": "没有此 SFTP 服务器的设置 ({0})",
|
"SFTP 서버 설정이 없습니다 ({0})": "没有此 SFTP 服务器的设置 ({0})",
|
||||||
"SFTP 서버 주소가 없습니다": "SFTP 服务器地址为空",
|
"SFTP 서버 주소가 없습니다": "SFTP 服务器地址为空",
|
||||||
|
"SFTP 서버 추가": "添加 SFTP 服务器",
|
||||||
"SFTP 에서 받는 중": "正在从 SFTP 下载",
|
"SFTP 에서 받는 중": "正在从 SFTP 下载",
|
||||||
|
"SHA-1 서명 · 키 교환을 씁니다. 오래된 NAS 가 다른 방식을 모를 때만 켜세요 (보안이 약함)": "使用 SHA-1 签名和密钥交换。仅当旧 NAS 不支持其他方式时开启 (安全性较弱)",
|
||||||
"SRT 형식이 아니거나 비어 있습니다.": "不是 SRT 格式或为空。",
|
"SRT 형식이 아니거나 비어 있습니다.": "不是 SRT 格式或为空。",
|
||||||
|
"SSH 서버 (NAS · 리눅스) 의 폴더를 파일 탐색기 · Rsync 화면에서 열고 복사 · 동기화합니다 (위쪽 탭). 비밀번호 · 개인 키 · 키 암호는 이 기기의 안전 저장소에만 둡니다. 처음 연결할 때 서버의 키 지문을 확인합니다.": "在文件资源管理器和 Rsync 界面中打开、复制和同步 SSH 服务器 (NAS · Linux) 上的文件夹 (顶部标签)。密码、私钥和私钥密码仅保存在本设备的安全存储中。首次连接时会确认服务器的密钥指纹。",
|
||||||
"Stable Diffusion v1.5": "Stable Diffusion v1.5",
|
"Stable Diffusion v1.5": "Stable Diffusion v1.5",
|
||||||
"Stable Diffusion v1.5 는 CreativeML OpenRAIL-M 라이선스입니다. 상업적 사용을 포함해 쓸 수 있지만, 다음 용도로는 쓸 수 없습니다: 법을 어기는 일, 미성년자를 해치거나 이용하는 일, 남을 해치려는 거짓 정보, 개인 정보로 사람을 해치는 일, 차별 · 괴롭힘, 의료 · 법률 판단을 대신하는 일 등 (라이선스 부속서 A). 만든 그림에 대한 책임은 쓰는 사람에게 있습니다.": "Stable Diffusion v1.5 采用 CreativeML OpenRAIL-M 许可证。可用于包括商业用途在内的场合,但不得用于: 违法行为、伤害或利用未成年人、意在伤害他人的虚假信息、利用个人信息伤害他人、歧视 · 骚扰、代替医疗 · 法律判断等 (许可证附件 A)。生成图片的责任由使用者承担。",
|
"Stable Diffusion v1.5 는 CreativeML OpenRAIL-M 라이선스입니다. 상업적 사용을 포함해 쓸 수 있지만, 다음 용도로는 쓸 수 없습니다: 법을 어기는 일, 미성년자를 해치거나 이용하는 일, 남을 해치려는 거짓 정보, 개인 정보로 사람을 해치는 일, 차별 · 괴롭힘, 의료 · 법률 판단을 대신하는 일 등 (라이선스 부속서 A). 만든 그림에 대한 책임은 쓰는 사람에게 있습니다.": "Stable Diffusion v1.5 采用 CreativeML OpenRAIL-M 许可证。可用于包括商业用途在内的场合,但不得用于: 违法行为、伤害或利用未成年人、意在伤害他人的虚假信息、利用个人信息伤害他人、歧视 · 骚扰、代替医疗 · 法律判断等 (许可证附件 A)。生成图片的责任由使用者承担。",
|
||||||
"TAESD (빠른 디코더)": "TAESD (快速解码器)",
|
"TAESD (빠른 디코더)": "TAESD (快速解码器)",
|
||||||
|
|
@ -324,7 +331,12 @@
|
||||||
"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 · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV · SFTP 需要从服务器读取全部内容,可能需要较长时间。要分析吗?",
|
||||||
|
"WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "无法将 WebDAV · SFTP 上的视频加入 MKV 列表。请先复制到本设备。",
|
||||||
|
"WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV · SFTP 文件仅显示名称、大小和日期",
|
||||||
|
"WebDAV · SFTP 폴더로는 풀 수 없습니다. 이 기기의 폴더에서 풀어 주세요.": "无法解压到 WebDAV · SFTP 文件夹。请解压到本设备的文件夹。",
|
||||||
|
"WebDAV · SFTP 항목은 ZIP 으로 묶을 수 없습니다. 이 기기의 파일 · 폴더를 골라 주세요.": "WebDAV · SFTP 项无法压缩为 ZIP。请选择本设备上的文件或文件夹。",
|
||||||
|
"WebDAV · SFTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV · SFTP: 由应用直接同步而非 rsync (比较大小 · 修改时间,遵循 -u · --delete · --remove-source-files)。",
|
||||||
"WebDAV 서버 \"{0}\" 을(를) 지울까요?": "删除 WebDAV 服务器“{0}”吗?",
|
"WebDAV 서버 \"{0}\" 을(를) 지울까요?": "删除 WebDAV 服务器“{0}”吗?",
|
||||||
"WebDAV 서버 고치기": "编辑 WebDAV 服务器",
|
"WebDAV 서버 고치기": "编辑 WebDAV 服务器",
|
||||||
"WebDAV 서버 목록과 그 저장된 비밀번호도 지워집니다. 동영상 목록 · 즐겨찾기 · 파일은 그대로입니다": "WebDAV 服务器列表及其保存的密码也会被删除。视频列表 · 收藏 · 文件保持不变",
|
"WebDAV 서버 목록과 그 저장된 비밀번호도 지워집니다. 동영상 목록 · 즐겨찾기 · 파일은 그대로입니다": "WebDAV 服务器列表及其保存的密码也会被删除。视频列表 · 收藏 · 文件保持不变",
|
||||||
|
|
@ -332,11 +344,6 @@
|
||||||
"WebDAV 서버 추가": "添加 WebDAV 服务器",
|
"WebDAV 서버 추가": "添加 WebDAV 服务器",
|
||||||
"WebDAV 에서 받는 중": "正在从 WebDAV 下载",
|
"WebDAV 에서 받는 중": "正在从 WebDAV 下载",
|
||||||
"WebDAV 원본에 씁니다 (PC 의 폴더는 바뀌면 바로 맞춤). 짧을수록 빨리 맞추지만 서버 · 배터리를 더 씁니다.": "用于 WebDAV 源 (电脑上的文件夹一有更改就同步)。间隔越短同步越快,但会更多占用服务器和电池。",
|
"WebDAV 원본에 씁니다 (PC 의 폴더는 바뀌면 바로 맞춤). 짧을수록 빨리 맞추지만 서버 · 배터리를 더 씁니다.": "用于 WebDAV 源 (电脑上的文件夹一有更改就同步)。间隔越短同步越快,但会更多占用服务器和电池。",
|
||||||
"WebDAV 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "无法将 WebDAV 上的视频加入 MKV 列表。请先复制到本设备。",
|
|
||||||
"WebDAV 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV 文件仅显示名称、大小和日期",
|
|
||||||
"WebDAV 폴더로는 풀 수 없습니다. 이 기기의 폴더에서 풀어 주세요.": "无法解压到 WebDAV 文件夹。请解压到本设备的文件夹。",
|
|
||||||
"WebDAV 항목은 ZIP 으로 묶을 수 없습니다. 이 기기의 파일 · 폴더를 골라 주세요.": "WebDAV 项无法压缩为 ZIP。请选择本设备上的文件或文件夹。",
|
|
||||||
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV: 由应用直接同步而非 rsync (比较大小 · 修改时间,遵循 -u · --delete · --remove-source-files)。",
|
|
||||||
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --remove-source-files 만 따름).": "WebDAV: 由应用代替 rsync 直接同步 (比较大小和修改时间,选项中仅 -u・--remove-source-files 生效)。",
|
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --remove-source-files 만 따름).": "WebDAV: 由应用代替 rsync 直接同步 (比较大小和修改时间,选项中仅 -u・--remove-source-files 生效)。",
|
||||||
"WebM (동영상)": "WebM(视频)",
|
"WebM (동영상)": "WebM(视频)",
|
||||||
"WebP 등은 PNG 로 저장합니다": "WebP 等将保存为 PNG",
|
"WebP 등은 PNG 로 저장합니다": "WebP 等将保存为 PNG",
|
||||||
|
|
@ -576,6 +583,7 @@
|
||||||
"가로 16:9": "横屏 16:9",
|
"가로 16:9": "横屏 16:9",
|
||||||
"가로 고정": "固定横屏",
|
"가로 고정": "固定横屏",
|
||||||
"가로 고정 · 세로 고정, 또는 자동 (기기를 돌리는 대로, 기기의 자동 회전 설정을 따름)": "锁定横屏 · 锁定竖屏,或自动(随设备方向旋转,遵循设备的自动旋转设置)",
|
"가로 고정 · 세로 고정, 또는 자동 (기기를 돌리는 대로, 기기의 자동 회전 설정을 따름)": "锁定横屏 · 锁定竖屏,或自动(随设备方向旋转,遵循设备的自动旋转设置)",
|
||||||
|
"가리기": "隐藏",
|
||||||
"가입 → 프로필 → API consumers → New consumer": "注册 → 个人资料 → API consumers → New consumer",
|
"가입 → 프로필 → API consumers → New consumer": "注册 → 个人资料 → API consumers → New consumer",
|
||||||
"가장 빠른 처리 장치를 찾는 중 (처음 한 번): {0}": "正在寻找最快的处理器 (仅首次): {0}",
|
"가장 빠른 처리 장치를 찾는 중 (처음 한 번): {0}": "正在寻找最快的处理器 (仅首次): {0}",
|
||||||
"가져오기": "导入",
|
"가져오기": "导入",
|
||||||
|
|
@ -610,6 +618,15 @@
|
||||||
"같은 확장자 모두": "所有相同扩展名",
|
"같은 확장자 모두": "所有相同扩展名",
|
||||||
"같은 확장자 모두 (.{0})": "所有相同扩展名(.{0})",
|
"같은 확장자 모두 (.{0})": "所有相同扩展名(.{0})",
|
||||||
"개수": "数量",
|
"개수": "数量",
|
||||||
|
"개인 키": "私钥",
|
||||||
|
"개인 키 바꾸기": "更换私钥",
|
||||||
|
"개인 키 빼기": "移除私钥",
|
||||||
|
"개인 키 암호 (없으면 비움)": "私钥密码 (没有则留空)",
|
||||||
|
"개인 키 있음": "已设置私钥",
|
||||||
|
"개인 키 파일 고르기": "选择私钥文件",
|
||||||
|
"개인 키 파일이 아닙니다 (OpenSSH · PEM 형식의 비밀 키를 고르세요, .pub 아님)": "这不是私钥文件 (请选择 OpenSSH · PEM 格式的私钥,而不是 .pub)",
|
||||||
|
"개인 키 파일이 아닙니다 (너무 큼)": "这不是私钥文件 (文件过大)",
|
||||||
|
"개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "已添加私钥 (不保存原文件路径)",
|
||||||
"개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)": "无法读取私钥 (OpenSSH · PEM 格式)",
|
"개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)": "无法读取私钥 (OpenSSH · PEM 格式)",
|
||||||
"개인 키의 암호가 맞지 않습니다": "私钥密码不正确",
|
"개인 키의 암호가 맞지 않습니다": "私钥密码不正确",
|
||||||
"거꾸로": "倒序",
|
"거꾸로": "倒序",
|
||||||
|
|
@ -634,6 +651,7 @@
|
||||||
"경로 입력": "输入路径",
|
"경로 입력": "输入路径",
|
||||||
"경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "输入路径 (例: D:\\视频 · \\\\NAS\\共享)",
|
"경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "输入路径 (例: D:\\视频 · \\\\NAS\\共享)",
|
||||||
"경로 입력 · WebDAV": "输入路径 · WebDAV",
|
"경로 입력 · WebDAV": "输入路径 · WebDAV",
|
||||||
|
"경로 입력 · 네트워크 서버": "输入路径 · 网络服务器",
|
||||||
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "路径过长,无法放入回收站 (Windows 回收站不接受超过 260 个字符的路径)。",
|
"경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "路径过长,无法放入回收站 (Windows 回收站不接受超过 260 个字符的路径)。",
|
||||||
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "路径过长 (Windows 回收站不接受超过 260 个字符的路径) - 可以永久删除",
|
"경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "路径过长 (Windows 回收站不接受超过 260 个字符的路径) - 可以永久删除",
|
||||||
"경로를 복사했습니다: {0}": "已复制路径: {0}",
|
"경로를 복사했습니다: {0}": "已复制路径: {0}",
|
||||||
|
|
@ -1131,6 +1149,10 @@
|
||||||
"미리보기: 목록에서 동영상을 고르세요": "预览: 请在列表中选择视频",
|
"미리보기: 목록에서 동영상을 고르세요": "预览: 请在列表中选择视频",
|
||||||
"미얀마어": "缅甸语",
|
"미얀마어": "缅甸语",
|
||||||
"미지정": "未指定",
|
"미지정": "未指定",
|
||||||
|
"믿고 연결": "信任并连接",
|
||||||
|
"믿은 서버 키": "已信任的服务器密钥",
|
||||||
|
"믿은 키 지우기": "清除已信任的密钥",
|
||||||
|
"믿은 키를 지웠습니다. [연결 확인] 으로 새 키를 확인하세요": "已清除信任的密钥。请用 [测试连接] 确认新密钥",
|
||||||
"바깥 폴더 (../)": "上级文件夹 (../)",
|
"바깥 폴더 (../)": "上级文件夹 (../)",
|
||||||
"바꾸거나 끄려면 지금 마스터 비밀번호 (또는 최상 비밀번호) 를 넣으세요.": "如需更改或关闭,请输入当前主密码 (或最高密码)。",
|
"바꾸거나 끄려면 지금 마스터 비밀번호 (또는 최상 비밀번호) 를 넣으세요.": "如需更改或关闭,请输入当前主密码 (或最高密码)。",
|
||||||
"바꾸기": "更改",
|
"바꾸기": "更改",
|
||||||
|
|
@ -1243,6 +1265,7 @@
|
||||||
"보낼 것 {0} / 있는 것 {1}": "要发送 {0} / 已有 {1}",
|
"보낼 것 {0} / 있는 것 {1}": "要发送 {0} / 已有 {1}",
|
||||||
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "正在查看的文件夹已不存在,已转到上级文件夹",
|
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "正在查看的文件夹已不存在,已转到上级文件夹",
|
||||||
"보안": "安全",
|
"보안": "安全",
|
||||||
|
"보이기": "显示",
|
||||||
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "选择要显示的按钮,拖动以调整顺序。",
|
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "选择要显示的按钮,拖动以调整顺序。",
|
||||||
"보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "调整需要重新编码,因此编解码器已从“保持原样”改为 H.264。",
|
"보정은 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "调整需要重新编码,因此编解码器已从“保持原样”改为 H.264。",
|
||||||
"보조 DNS (비워 둘 수 있음)": "备用 DNS (可留空)",
|
"보조 DNS (비워 둘 수 있음)": "备用 DNS (可留空)",
|
||||||
|
|
@ -1296,6 +1319,7 @@
|
||||||
"비공개 · 회원 전용 영상입니다.": "这是私享 · 会员专享视频。",
|
"비공개 · 회원 전용 영상입니다.": "这是私享 · 会员专享视频。",
|
||||||
"비교하지 못했습니다: {0}": "无法比较: {0}",
|
"비교하지 못했습니다: {0}": "无法比较: {0}",
|
||||||
"비밀번호": "密码",
|
"비밀번호": "密码",
|
||||||
|
"비밀번호 (개인 키만 쓰면 비움)": "密码 (仅使用私钥时留空)",
|
||||||
"비밀번호 (선택)": "密码(可选)",
|
"비밀번호 (선택)": "密码(可选)",
|
||||||
"비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "密码 · API 密钥 (WebDAV · OpenSubtitles · AI 服务)",
|
"비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "密码 · API 密钥 (WebDAV · OpenSubtitles · AI 服务)",
|
||||||
"비밀번호 · 자막 API 키는 백업하지 않으므로 다시 넣어 주세요. 자동 백업은 하루에 한 번쯤 만들어져 그 뒤에 바꾼 것은 빠졌을 수 있습니다.": "密码 · 字幕 API 密钥不会备份,请重新输入。自动备份大约每天创建一次,之后更改的内容可能缺失。",
|
"비밀번호 · 자막 API 키는 백업하지 않으므로 다시 넣어 주세요. 자동 백업은 하루에 한 번쯤 만들어져 그 뒤에 바꾼 것은 빠졌을 수 있습니다.": "密码 · 字幕 API 密钥不会备份,请重新输入。自动备份大约每天创建一次,之后更改的内容可能缺失。",
|
||||||
|
|
@ -1367,9 +1391,13 @@
|
||||||
"서버 ({0}) 와의 연결이 끊겼습니다": "与服务器 ({0}) 的连接已断开",
|
"서버 ({0}) 와의 연결이 끊겼습니다": "与服务器 ({0}) 的连接已断开",
|
||||||
"서버 · 서비스 (기기 밖)": "服务器 · 服务 (设备外)",
|
"서버 · 서비스 (기기 밖)": "服务器 · 服务 (设备外)",
|
||||||
"서버 설정 고치기": "修改服务器设置",
|
"서버 설정 고치기": "修改服务器设置",
|
||||||
|
"서버 설정과 이 기기에 저장된 비밀번호 · 개인 키 · 믿은 서버 키를 지웁니다. 서버의 파일은 그대로입니다. 이 서버를 쓰는 동기화 · 복사는 다시 넣을 때까지 연결되지 않습니다.": "删除服务器设置以及本设备保存的密码、私钥和已信任的服务器密钥。服务器上的文件保持不变。使用此服务器的同步 · 复制在重新添加前无法连接。",
|
||||||
"서버 설정과 이 기기에 저장된 비밀번호를 지웁니다. 서버의 파일은 그대로입니다. 이 서버를 쓰는 동기화 · 복사는 다시 넣을 때까지 연결되지 않습니다.": "删除服务器设置和本设备上保存的密码。服务器上的文件保持不变。使用此服务器的同步 · 复制在重新添加之前无法连接。",
|
"서버 설정과 이 기기에 저장된 비밀번호를 지웁니다. 서버의 파일은 그대로입니다. 이 서버를 쓰는 동기화 · 복사는 다시 넣을 때까지 연결되지 않습니다.": "删除服务器设置和本设备上保存的密码。服务器上的文件保持不变。使用此服务器的同步 · 复制在重新添加之前无法连接。",
|
||||||
"서버 인증서를 확인할 수 없습니다": "无法验证服务器证书",
|
"서버 인증서를 확인할 수 없습니다": "无法验证服务器证书",
|
||||||
"서버 저장 공간 부족": "服务器存储空间不足",
|
"서버 저장 공간 부족": "服务器存储空间不足",
|
||||||
|
"서버 주소": "服务器地址",
|
||||||
|
"서버 키 아직 확인 안 함": "尚未确认服务器密钥",
|
||||||
|
"서버 키: 아직 확인 안 함 ([연결 확인] 을 누르면 지문을 보이고 묻습니다)": "服务器密钥: 尚未确认 (点击 [测试连接] 会显示指纹并询问)",
|
||||||
"서버가 https 주소로 옮기라고 합니다 ({0}). 서버 설정의 주소를 https:// 로 바꾸세요.": "服务器要求改用 https 地址 ({0})。请将服务器设置中的地址改为 https://。",
|
"서버가 https 주소로 옮기라고 합니다 ({0}). 서버 설정의 주소를 https:// 로 바꾸세요.": "服务器要求改用 https 地址 ({0})。请将服务器设置中的地址改为 https://。",
|
||||||
"서버가 거절했습니다 ({0}): {1}": "服务器拒绝 ({0}): {1}",
|
"서버가 거절했습니다 ({0}): {1}": "服务器拒绝 ({0}): {1}",
|
||||||
"서버가 그림을 돌려주지 않았습니다": "服务器没有返回图片",
|
"서버가 그림을 돌려주지 않았습니다": "服务器没有返回图片",
|
||||||
|
|
@ -1382,6 +1410,8 @@
|
||||||
"서버와 같은 네트워크에 있거나 VPN (예: Tailscale) 이 켜져 있어야 합니다. 켠 뒤 [다시 시도] 를 누르세요.": "需要与服务器在同一网络,或打开 VPN (如 Tailscale)。打开后点 [重试]。",
|
"서버와 같은 네트워크에 있거나 VPN (예: Tailscale) 이 켜져 있어야 합니다. 켠 뒤 [다시 시도] 를 누르세요.": "需要与服务器在同一网络,或打开 VPN (如 Tailscale)。打开后点 [重试]。",
|
||||||
"서버와 암호 방식을 맞추지 못했습니다. 오래된 서버면 서버 설정의 \"오래된 ssh-rsa 허용\" 을 켜 보세요": "无法与服务器协商加密方式。如果是旧服务器,请在服务器设置中开启\"允许旧的 ssh-rsa\"",
|
"서버와 암호 방식을 맞추지 못했습니다. 오래된 서버면 서버 설정의 \"오래된 ssh-rsa 허용\" 을 켜 보세요": "无法与服务器协商加密方式。如果是旧服务器,请在服务器设置中开启\"允许旧的 ssh-rsa\"",
|
||||||
"서버와 암호 방식을 맞추지 못했습니다: {0}": "无法与服务器协商加密方式: {0}",
|
"서버와 암호 방식을 맞추지 못했습니다: {0}": "无法与服务器协商加密方式: {0}",
|
||||||
|
"서버의 키 지문이 서버 (NAS 설정 · ssh-keygen -lf) 에서 본 것과 같은지 확인하세요. 믿으면 이 기기에 저장하고, 다음부터 키가 바뀌면 연결하지 않고 알립니다.": "请确认服务器的密钥指纹与服务器端 (NAS 设置 · ssh-keygen -lf) 显示的一致。信任后会保存在本设备上,之后密钥若有变化将不连接并提示。",
|
||||||
|
"서버의 키가 바뀌었습니다": "服务器的密钥已更改",
|
||||||
"서버의 키가 바뀌었습니다 - 중간에서 가로채는 공격일 수 있어 연결하지 않았습니다. 예전: {0} · 지금: {1}": "服务器的密钥已更改。可能是中间人攻击,因此未连接。之前: {0} · 现在: {1}",
|
"서버의 키가 바뀌었습니다 - 중간에서 가로채는 공격일 수 있어 연결하지 않았습니다. 예전: {0} · 지금: {1}": "服务器的密钥已更改。可能是中间人攻击,因此未连接。之前: {0} · 现在: {1}",
|
||||||
"서비스 고치기": "编辑服务",
|
"서비스 고치기": "编辑服务",
|
||||||
"서비스 설정과 저장된 API 키를 지웁니다.": "删除服务设置及保存的 API 密钥。",
|
"서비스 설정과 저장된 API 키를 지웁니다.": "删除服务设置及保存的 API 密钥。",
|
||||||
|
|
@ -1469,6 +1499,7 @@
|
||||||
"시작 · 웹 브라우저": "启动 · 网页浏览器",
|
"시작 · 웹 브라우저": "启动 · 网页浏览器",
|
||||||
"시작 메뉴에 등록": "添加到开始菜单",
|
"시작 메뉴에 등록": "添加到开始菜单",
|
||||||
"시작 안 함 (모니터링에서 시작)": "不开始 (在监控中开始)",
|
"시작 안 함 (모니터링에서 시작)": "不开始 (在监控中开始)",
|
||||||
|
"시작 폴더 (비우면 홈 폴더)": "起始文件夹 (留空则为主目录)",
|
||||||
"시작: {0} → {1}{2}": "开始: {0} → {1}{2}",
|
"시작: {0} → {1}{2}": "开始: {0} → {1}{2}",
|
||||||
"시작하는 중…": "正在开始…",
|
"시작하는 중…": "正在开始…",
|
||||||
"시작하지 못했습니다: {0}": "无法开始:{0}",
|
"시작하지 못했습니다: {0}": "无法开始:{0}",
|
||||||
|
|
@ -1631,6 +1662,7 @@
|
||||||
"예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 속도 제한은 파일 탐색기의 속도 제한을 함께 씀. 새로 맞추는 폴더 쌍에 씁니다 - 이미 실행한 쌍은 기억한 옵션을 쓰며 모니터링 > 복사 · rsync 에서 따로 고칩니다": "例: -avPog (归档 · 详细 · 进度 · 所有者 · 组)。速度限制同样使用文件浏览器的速度限制。用于新的文件夹组合 - 已执行过的组合使用记住的选项,请在 监视 > 复制 · rsync 中单独修改",
|
"예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 속도 제한은 파일 탐색기의 속도 제한을 함께 씀. 새로 맞추는 폴더 쌍에 씁니다 - 이미 실행한 쌍은 기억한 옵션을 쓰며 모니터링 > 복사 · rsync 에서 따로 고칩니다": "例: -avPog (归档 · 详细 · 进度 · 所有者 · 组)。速度限制同样使用文件浏览器的速度限制。用于新的文件夹组合 - 已执行过的组合使用记住的选项,请在 监视 > 复制 · rsync 中单独修改",
|
||||||
"예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 이동은 --remove-source-files 를 자동으로 붙임": "例: -avPog (归档 · 详细 · 进度 · 所有者 · 组)。移动时自动加上 --remove-source-files",
|
"예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 이동은 --remove-source-files 를 자동으로 붙임": "例: -avPog (归档 · 详细 · 进度 · 所有者 · 组)。移动时自动加上 --remove-source-files",
|
||||||
"예: /E /COPY:DAT /DCOPY:T /R:2 /W:2. 이동은 /MOVE 를 자동으로 붙임": "例: /E /COPY:DAT /DCOPY:T /R:2 /W:2。移动时自动加上 /MOVE",
|
"예: /E /COPY:DAT /DCOPY:T /R:2 /W:2. 이동은 /MOVE 를 자동으로 붙임": "例: /E /COPY:DAT /DCOPY:T /R:2 /W:2。移动时自动加上 /MOVE",
|
||||||
|
"예: /volume1/video · 탭의 맨 위가 됩니다": "例如: /volume1/video · 将成为标签的最上层",
|
||||||
"예: 0 9-17 * * 1-5 (평일 9시~17시 59분) · 0 0-5 * * * (매일 0~5시). 분은 무시하고 1시간 단위": "例: 0 9-17 * * 1-5 (工作日 9:00~17:59) · 0 0-5 * * * (每天 0~5 点)。忽略分钟,以 1 小时为单位",
|
"예: 0 9-17 * * 1-5 (평일 9시~17시 59분) · 0 0-5 * * * (매일 0~5시). 분은 무시하고 1시간 단위": "例: 0 9-17 * * 1-5 (工作日 9:00~17:59) · 0 0-5 * * * (每天 0~5 点)。忽略分钟,以 1 小时为单位",
|
||||||
"예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "例: Ctrl+Shift+X(Ctrl·Shift·Alt·Win + 字母·数字·F1~F12)",
|
"예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "例: Ctrl+Shift+X(Ctrl·Shift·Alt·Win + 字母·数字·F1~F12)",
|
||||||
"예: S01E · *.srt": "例如: S01E · *.srt",
|
"예: S01E · *.srt": "例如: S01E · *.srt",
|
||||||
|
|
@ -1638,12 +1670,15 @@
|
||||||
"예: http://100.x.x.x:8188 (ComfyUI)": "例: http://100.x.x.x:8188 (ComfyUI)",
|
"예: http://100.x.x.x:8188 (ComfyUI)": "例: http://100.x.x.x:8188 (ComfyUI)",
|
||||||
"예: https://api.openai.com (경로 /v1/images/generations 를 붙여 부름)": "例: https://api.openai.com (调用时附加路径 /v1/images/generations)",
|
"예: https://api.openai.com (경로 /v1/images/generations 를 붙여 부름)": "例: https://api.openai.com (调用时附加路径 /v1/images/generations)",
|
||||||
"예: https://nas.local:5006/home · https://cloud.example.com/remote.php/dav/files/아이디": "例: https://nas.local:5006/home · https://cloud.example.com/remote.php/dav/files/用户名",
|
"예: https://nas.local:5006/home · https://cloud.example.com/remote.php/dav/files/아이디": "例: https://nas.local:5006/home · https://cloud.example.com/remote.php/dav/files/用户名",
|
||||||
|
"예: nas.local · 192.168.0.10 · 100.x.y.z (Tailscale)": "例如: nas.local · 192.168.0.10 · 100.x.y.z (Tailscale)",
|
||||||
"예상 시간": "预计时间",
|
"예상 시간": "预计时间",
|
||||||
|
"예전": "之前",
|
||||||
"예전 구조의 프로그램 파일 정리 ({0}개): {1}": "已清理旧结构的程序文件({0} 个): {1}",
|
"예전 구조의 프로그램 파일 정리 ({0}개): {1}": "已清理旧结构的程序文件({0} 个): {1}",
|
||||||
"예전 버전": "以前的版本",
|
"예전 버전": "以前的版本",
|
||||||
"예전 버전 v{0} 으로 되돌리기": "回退到以前的版本 v{0}",
|
"예전 버전 v{0} 으로 되돌리기": "回退到以前的版本 v{0}",
|
||||||
"예전 버전으로 되돌립니다. 지금 버전의 설정 (동영상 목록 · 즐겨찾기 포함) 은 보관해 두었다가, 나중에 이 버전으로 다시 오면 되살릴 수 있습니다.": "将回退到以前的版本。当前版本的设置 (包括视频列表 · 书签) 会被保存,以后回到此版本时可以恢复。",
|
"예전 버전으로 되돌립니다. 지금 버전의 설정 (동영상 목록 · 즐겨찾기 포함) 은 보관해 두었다가, 나중에 이 버전으로 다시 오면 되살릴 수 있습니다.": "将回退到以前的版本。当前版本的设置 (包括视频列表 · 书签) 会被保存,以后回到此版本时可以恢复。",
|
||||||
"오늘 받을 수 있는 자막 수를 모두 사용했습니다. ({0})": "今天可下载的字幕数已用完。({0})",
|
"오늘 받을 수 있는 자막 수를 모두 사용했습니다. ({0})": "今天可下载的字幕数已用完。({0})",
|
||||||
|
"오래된 ssh-rsa 허용": "允许旧的 ssh-rsa",
|
||||||
"오래된 작업 기록 파일 (app.log.1 등). 지금 쓰는 기록은 남깁니다": "旧的任务记录文件(app.log.1 等)。当前记录会保留",
|
"오래된 작업 기록 파일 (app.log.1 등). 지금 쓰는 기록은 남깁니다": "旧的任务记录文件(app.log.1 等)。当前记录会保留",
|
||||||
"오른쪽": "右",
|
"오른쪽": "右",
|
||||||
"오른쪽 90°": "向右 90°",
|
"오른쪽 90°": "向右 90°",
|
||||||
|
|
@ -2117,6 +2152,7 @@
|
||||||
"준비 중…": "准备中…",
|
"준비 중…": "准备中…",
|
||||||
"준비하는 중": "正在准备",
|
"준비하는 중": "正在准备",
|
||||||
"줄 삭제": "删除行",
|
"줄 삭제": "删除行",
|
||||||
|
"중간에서 가로채는 공격일 수 있어 연결하지 않았습니다. 서버를 다시 설치했거나 키를 바꾼 것이 확실할 때만, 서버 설정의 [믿은 키 지우기] 뒤 다시 연결해 새 키를 확인하세요.": "可能是中间人攻击,因此未连接。仅当确定服务器已重装或更换了密钥时,请在服务器设置中 [清除已信任的密钥] 后重新连接并确认新密钥。",
|
||||||
"중국어": "中文",
|
"중국어": "中文",
|
||||||
"중국어 간체": "简体中文",
|
"중국어 간체": "简体中文",
|
||||||
"중국어 번체": "繁体中文",
|
"중국어 번체": "繁体中文",
|
||||||
|
|
@ -2169,6 +2205,7 @@
|
||||||
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "当前是双开应用 (复制的应用)。Android 可能不会给双开应用此权限。如果无法开启,请从没有徽标的原 JJ_MKVMaker 图标打开。",
|
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "当前是双开应用 (复制的应用)。Android 可能不会给双开应用此权限。如果无法开启,请从没有徽标的原 JJ_MKVMaker 图标打开。",
|
||||||
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "上次运行未正常结束(最后检查 {0}:{1})。已记录在任务记录中。",
|
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "上次运行未正常结束(最后检查 {0}:{1})。已记录在任务记录中。",
|
||||||
"지난 작업 기록": "旧任务记录",
|
"지난 작업 기록": "旧任务记录",
|
||||||
|
"지문 확인하고 연결": "确认指纹并连接",
|
||||||
"지우고 초기화": "删除并重置",
|
"지우고 초기화": "删除并重置",
|
||||||
"지우기": "删除",
|
"지우기": "删除",
|
||||||
"지우기 포함": "含删除",
|
"지우기 포함": "含删除",
|
||||||
|
|
@ -2235,6 +2272,7 @@
|
||||||
"처음 시작 시": "启动时",
|
"처음 시작 시": "启动时",
|
||||||
"처음 쓸 때 내려받기 (기본)": "首次使用时下载 (默认)",
|
"처음 쓸 때 내려받기 (기본)": "首次使用时下载 (默认)",
|
||||||
"처음 아이콘 (파랑)": "最初的图标(蓝色)",
|
"처음 아이콘 (파랑)": "最初的图标(蓝色)",
|
||||||
|
"처음 연결하는 서버입니다": "首次连接此服务器",
|
||||||
"처음 연결하는 서버입니다. 서버의 키 지문이 맞는지 확인하세요: {0} ({1})": "这是首次连接此服务器。请确认服务器的密钥指纹正确: {0} ({1})",
|
"처음 연결하는 서버입니다. 서버의 키 지문이 맞는지 확인하세요: {0} ({1})": "这是首次连接此服务器。请确认服务器的密钥指纹正确: {0} ({1})",
|
||||||
"처음 찾을 때 끝까지 찾기 전의 결과입니다. 더 찾으려면 다시 찾으세요.": "这些是首次搜索完成前的结果。如需查找更多,请重新搜索。",
|
"처음 찾을 때 끝까지 찾기 전의 결과입니다. 더 찾으려면 다시 찾으세요.": "这些是首次搜索完成前的结果。如需查找更多,请重新搜索。",
|
||||||
"처음대로": "恢复默认",
|
"처음대로": "恢复默认",
|
||||||
|
|
@ -2372,6 +2410,7 @@
|
||||||
"편집 목록에 추가 (MKV 만들기 대상)": "加入编辑列表(制作 MKV 的对象)",
|
"편집 목록에 추가 (MKV 만들기 대상)": "加入编辑列表(制作 MKV 的对象)",
|
||||||
"평일 9~18시": "工作日 9~18 点",
|
"평일 9~18시": "工作日 9~18 点",
|
||||||
"포르투갈어": "葡萄牙语",
|
"포르투갈어": "葡萄牙语",
|
||||||
|
"포트": "端口",
|
||||||
"포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)": "包含的组件和许可证 (含 GPL · LGPL 全文)",
|
"포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)": "包含的组件和许可证 (含 GPL · LGPL 全文)",
|
||||||
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "包含的组件与许可(THIRD_PARTY_NOTICES.txt)",
|
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "包含的组件与许可(THIRD_PARTY_NOTICES.txt)",
|
||||||
"폰 세로 목록 크기": "竖屏手机列表大小",
|
"폰 세로 목록 크기": "竖屏手机列表大小",
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ WHISPER_CPP_LICENSE.txt · ONNXRUNTIME_LICENSES.txt 는 실행 파일 옆에도
|
||||||
- cp949_codec BSD https://pub.dev/packages/cp949_codec
|
- cp949_codec BSD https://pub.dev/packages/cp949_codec
|
||||||
- archive 4.3.0 (ZIP 풀기 · 묶기) MIT https://pub.dev/packages/archive
|
- archive 4.3.0 (ZIP 풀기 · 묶기) MIT https://pub.dev/packages/archive
|
||||||
Copyright (c) 2013-2021 Brendan Duncan.
|
Copyright (c) 2013-2021 Brendan Duncan.
|
||||||
|
- dartssh2 4.1.0 (SFTP) MIT https://pub.dev/packages/dartssh2
|
||||||
|
Copyright (c) 2022 xuty. 함께 쓰는 pointycastle (MIT) · pinenacl (MIT) · asn1lib (BSD-3-Clause) 의 원문은 "환경 설정 > 라이선스"
|
||||||
- Microsoft Visual C++ 런타임 (msvcp140.dll, vcruntime140*.dll)
|
- Microsoft Visual C++ 런타임 (msvcp140.dll, vcruntime140*.dll)
|
||||||
Microsoft 재배포 가능 파일 (Visual Studio 라이선스의 Distributable Code)
|
Microsoft 재배포 가능 파일 (Visual Studio 라이선스의 Distributable Code)
|
||||||
- 그 밖의 Dart 패키지 라이선스: 프로그램의 "환경 설정 > 라이선스" 화면
|
- 그 밖의 Dart 패키지 라이선스: 프로그램의 "환경 설정 > 라이선스" 화면
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ JJ_MKVMaker (Android) - 포함된 오픈 소스 구성 요소 및 라이선스
|
||||||
- file_picker · android_file_picker MIT https://pub.dev/packages/file_picker
|
- file_picker · android_file_picker MIT https://pub.dev/packages/file_picker
|
||||||
- archive 4.3.0 (ZIP 풀기 · 묶기) MIT https://pub.dev/packages/archive
|
- archive 4.3.0 (ZIP 풀기 · 묶기) MIT https://pub.dev/packages/archive
|
||||||
Copyright (c) 2013-2021 Brendan Duncan.
|
Copyright (c) 2013-2021 Brendan Duncan.
|
||||||
|
- dartssh2 4.1.0 (SFTP) MIT https://pub.dev/packages/dartssh2
|
||||||
|
Copyright (c) 2022 xuty. 함께 쓰는 pointycastle (MIT) · pinenacl (MIT) · asn1lib (BSD-3-Clause) 의 원문은 [패키지 라이선스]
|
||||||
- charset (Apache-2.0) · cp949_codec (BSD) · 그 밖의 Dart 패키지: [패키지 라이선스]
|
- charset (Apache-2.0) · cp949_codec (BSD) · 그 밖의 Dart 패키지: [패키지 라이선스]
|
||||||
|
|
||||||
[앱에 들어 있는 별도 실행 파일 · 라이브러리]
|
[앱에 들어 있는 별도 실행 파일 · 라이브러리]
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,9 @@ class DesktopStorageService implements StorageService {
|
||||||
final files = await FilePicker.pickFiles(
|
final files = await FilePicker.pickFiles(
|
||||||
dialogTitle: title,
|
dialogTitle: title,
|
||||||
initialDirectory: initialDirectory,
|
initialDirectory: initialDirectory,
|
||||||
type: FileType.custom,
|
// 확장자 목록이 비면 모든 파일 (88: 확장자가 없는 개인 키 파일)
|
||||||
allowedExtensions: extensions,
|
type: extensions.isEmpty ? FileType.any : FileType.custom,
|
||||||
|
allowedExtensions: extensions.isEmpty ? null : extensions,
|
||||||
);
|
);
|
||||||
return files.map((f) => f.path).whereType<String>().toList();
|
return files.map((f) => f.path).whereType<String>().toList();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,9 @@ class _FileBrowserState extends State<_FileBrowser> {
|
||||||
|
|
||||||
static String _volumeName((String, String, bool) v) => v.$3 ? (v.$2.isEmpty ? tr('SD 카드') : v.$2) : tr('내장 저장소');
|
static String _volumeName((String, String, bool) v) => v.$3 ? (v.$2.isEmpty ? tr('SD 카드') : v.$2) : tr('내장 저장소');
|
||||||
|
|
||||||
bool _match(String path) => widget.extensions.contains(p.extension(path).replaceFirst('.', '').toLowerCase());
|
/// 확장자 목록이 비면 모든 파일 (88: 확장자가 없는 개인 키 파일 id_ed25519 등)
|
||||||
|
bool _match(String path) =>
|
||||||
|
widget.extensions.isEmpty || widget.extensions.contains(p.extension(path).replaceFirst('.', '').toLowerCase());
|
||||||
|
|
||||||
void _open(String dir) {
|
void _open(String dir) {
|
||||||
final dirs = <Directory>[];
|
final dirs = <Directory>[];
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
|
||||||
if (otherDir != null && !isDav(otherDir)) (otherDir, tr('다른 창 폴더')),
|
if (otherDir != null && !isDav(otherDir)) (otherDir, tr('다른 창 폴더')),
|
||||||
];
|
];
|
||||||
if (choices.isEmpty) {
|
if (choices.isEmpty) {
|
||||||
say(tr('WebDAV 폴더로는 풀 수 없습니다. 이 기기의 폴더에서 풀어 주세요.'));
|
say(tr('WebDAV · SFTP 폴더로는 풀 수 없습니다. 이 기기의 폴더에서 풀어 주세요.'));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var dest = choices.first.$1;
|
var dest = choices.first.$1;
|
||||||
|
|
@ -309,7 +309,7 @@ Future<String?> createZipFlow(BuildContext context, AppController c, List<String
|
||||||
void say(String t) => messenger?.showSnackBar(SnackBar(content: Text(t)));
|
void say(String t) => messenger?.showSnackBar(SnackBar(content: Text(t)));
|
||||||
if (sources.isEmpty) return null;
|
if (sources.isEmpty) return null;
|
||||||
if (sources.any(isDav) || isDav(dir)) {
|
if (sources.any(isDav) || isDav(dir)) {
|
||||||
say(tr('WebDAV 항목은 ZIP 으로 묶을 수 없습니다. 이 기기의 파일 · 폴더를 골라 주세요.'));
|
say(tr('WebDAV · SFTP 항목은 ZIP 으로 묶을 수 없습니다. 이 기기의 파일 · 폴더를 골라 주세요.'));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final name = TextEditingController(
|
final name = TextEditingController(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import '../core/sync_tools.dart';
|
||||||
import '../core/file_ops.dart';
|
import '../core/file_ops.dart';
|
||||||
import '../core/playlist.dart' show isAudioFile, isVideoFile;
|
import '../core/playlist.dart' show isAudioFile, isVideoFile;
|
||||||
import '../core/vfs.dart';
|
import '../core/vfs.dart';
|
||||||
|
import '../core/sftp.dart' show SftpHostKeyException, SftpRegistry;
|
||||||
import '../core/webdav.dart' show DavRegistry;
|
import '../core/webdav.dart' show DavRegistry;
|
||||||
import '../platform/android/android_storage.dart';
|
import '../platform/android/android_storage.dart';
|
||||||
import 'app_actions.dart';
|
import 'app_actions.dart';
|
||||||
|
|
@ -35,6 +36,7 @@ import '../platform/windows/recycle_bin.dart';
|
||||||
import '../core/sync_preview.dart';
|
import '../core/sync_preview.dart';
|
||||||
import 'path_label.dart';
|
import 'path_label.dart';
|
||||||
import 'sync_preview_view.dart';
|
import 'sync_preview_view.dart';
|
||||||
|
import 'sftp_settings.dart';
|
||||||
import 'webdav_settings.dart';
|
import 'webdav_settings.dart';
|
||||||
import 'reader_page.dart';
|
import 'reader_page.dart';
|
||||||
import '../app/component_store.dart';
|
import '../app/component_store.dart';
|
||||||
|
|
@ -223,6 +225,9 @@ class _Pane extends ChangeNotifier {
|
||||||
final cache = <String, List<FileEntry>>{};
|
final cache = <String, List<FileEntry>>{};
|
||||||
final loading = <String>{};
|
final loading = <String>{};
|
||||||
final errors = <String, String>{};
|
final errors = <String, String>{};
|
||||||
|
|
||||||
|
/// 88: 처음 보는 (또는 바뀐) SFTP 서버 키 - 오류 안내에 지문 · [지문 확인하고 연결]
|
||||||
|
final hostKeyErrors = <String, SftpHostKeyException>{};
|
||||||
final marked = <String>{};
|
final marked = <String>{};
|
||||||
|
|
||||||
/// 74: 범위 고르기의 시작 (마지막으로 표시한 것)
|
/// 74: 범위 고르기의 시작 (마지막으로 표시한 것)
|
||||||
|
|
@ -317,6 +322,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
List<(String, String)> get _volumes => [
|
List<(String, String)> get _volumes => [
|
||||||
..._local,
|
..._local,
|
||||||
for (final s in c.settings.webdavServers) ('$davScheme${s.id}/', s.label),
|
for (final s in c.settings.webdavServers) ('$davScheme${s.id}/', s.label),
|
||||||
|
// 88: SFTP 서버
|
||||||
|
for (final s in c.settings.sftpServers) ('$sftpScheme${s.id}/', s.label),
|
||||||
];
|
];
|
||||||
late final List<_Pane> _panes = [_Pane(''), _Pane('')];
|
late final List<_Pane> _panes = [_Pane(''), _Pane('')];
|
||||||
int _active = 0;
|
int _active = 0;
|
||||||
|
|
@ -631,8 +638,36 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
if (err == null) return null;
|
if (err == null) return null;
|
||||||
final where = pane.errors[dir] != null ? dir : pane.root;
|
final where = pane.errors[dir] != null ? dir : pane.root;
|
||||||
final dav = isDav(where);
|
final dav = isDav(where);
|
||||||
final server = dav ? DavRegistry.server(DavPath.parse(where).server) : null;
|
final server = isWebDav(where) ? DavRegistry.server(DavPath.parse(where).server) : null;
|
||||||
final (title, body) = explainFileError(err, dav: dav, noPassword: server != null && server.password.isEmpty);
|
final sftp = isSftp(where) ? SftpRegistry.server(DavPath.parse(where).server) : null;
|
||||||
|
// 88: 처음 보는 SFTP 서버 키 - 지문을 보이고 [지문 확인하고 연결] (바뀐 키는 막고 서버 설정으로)
|
||||||
|
final hostKey = pane.hostKeyErrors[where] ?? pane.hostKeyErrors[pane.root];
|
||||||
|
if (sftp != null && hostKey != null) {
|
||||||
|
return _notice(
|
||||||
|
icon: hostKey.changed ? Icons.gpp_bad_outlined : Icons.fingerprint,
|
||||||
|
title: '${_displayPath(where)}: ${hostKey.changed ? tr('서버의 키가 바뀌었습니다') : tr('처음 연결하는 서버입니다')}',
|
||||||
|
body: hostKey.message,
|
||||||
|
actions: [
|
||||||
|
if (!hostKey.changed)
|
||||||
|
FilledButton.tonal(
|
||||||
|
onPressed: () async {
|
||||||
|
if (!await askTrustHostKey(context, sftp.label, hostKey)) return;
|
||||||
|
await trustSftpHostKey(c, sftp.id, hostKey);
|
||||||
|
pane.hostKeyErrors.remove(where);
|
||||||
|
pane.errors.remove(where);
|
||||||
|
pane.changed();
|
||||||
|
await _load(pane, where, force: true);
|
||||||
|
},
|
||||||
|
child: Text(tr('지문 확인하고 연결')),
|
||||||
|
),
|
||||||
|
TextButton(onPressed: () => _editSftp(pane, pane.root), child: Text(tr('서버 설정 고치기'))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final (title, body) = explainFileError(err,
|
||||||
|
dav: dav,
|
||||||
|
noPassword: (server != null && server.password.isEmpty) ||
|
||||||
|
(sftp != null && sftp.password.isEmpty && sftp.privateKey.isEmpty));
|
||||||
final locked = isLockedError(err);
|
final locked = isLockedError(err);
|
||||||
// 50: 막힌 폴더는 다시 해도 같으니 [다시 시도] 없이 [위 폴더로]
|
// 50: 막힌 폴더는 다시 해도 같으니 [다시 시도] 없이 [위 폴더로]
|
||||||
if (err == androidRestrictedError) {
|
if (err == androidRestrictedError) {
|
||||||
|
|
@ -669,7 +704,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
},
|
},
|
||||||
child: Text(locked ? tr('마스터 비밀번호 넣기') : tr('다시 시도')),
|
child: Text(locked ? tr('마스터 비밀번호 넣기') : tr('다시 시도')),
|
||||||
),
|
),
|
||||||
if (dav && !locked) TextButton(onPressed: () => _editDav(pane, pane.root), child: Text(tr('서버 설정 고치기'))),
|
if (dav && !locked)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => sftp != null ? _editSftp(pane, pane.root) : _editDav(pane, pane.root), child: Text(tr('서버 설정 고치기'))),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -678,6 +715,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
String _copyablePath(String path) {
|
String _copyablePath(String path) {
|
||||||
if (!isDav(path)) return path;
|
if (!isDav(path)) return path;
|
||||||
final d = DavPath.parse(path);
|
final d = DavPath.parse(path);
|
||||||
|
// 88: SFTP 는 sftp://아이디@서버:포트/경로 (시작 폴더를 비웠으면 홈 기준 ~/)
|
||||||
|
if (d.isSftp) {
|
||||||
|
final s = SftpRegistry.server(d.server);
|
||||||
|
if (s == null) return _displayPath(path);
|
||||||
|
final base = s.root.isEmpty ? '/~' : (s.root.startsWith('/') ? s.root : '/${s.root}');
|
||||||
|
return 'sftp://${s.user.isEmpty ? '' : '${s.user}@'}${s.host}${s.port == 22 ? '' : ':${s.port}'}$base${d.isRoot ? '' : d.rel}';
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return DavRegistry.client(d.server).uriOf(d.rel).replace(userInfo: '').toString();
|
return DavRegistry.client(d.server).uriOf(d.rel).replace(userInfo: '').toString();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|
@ -689,8 +733,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
String _displayPath(String path) {
|
String _displayPath(String path) {
|
||||||
if (isDav(path)) {
|
if (isDav(path)) {
|
||||||
final d = DavPath.parse(path);
|
final d = DavPath.parse(path);
|
||||||
final label = DavRegistry.server(d.server)?.label ?? 'WebDAV';
|
return ['${d.isSftp ? '🔑' : '☁'} ${d.serverLabel}', ...d.rel.split('/').where((s) => s.isNotEmpty)].join(' › ');
|
||||||
return ['☁ $label', ...d.rel.split('/').where((s) => s.isNotEmpty)].join(' › ');
|
|
||||||
}
|
}
|
||||||
if (!Platform.isAndroid) return path;
|
if (!Platform.isAndroid) return path;
|
||||||
String? best;
|
String? best;
|
||||||
|
|
@ -720,7 +763,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
void _dropRemovedServers() {
|
void _dropRemovedServers() {
|
||||||
if (!_ready) return;
|
if (!_ready) return;
|
||||||
for (final pane in _panes) {
|
for (final pane in _panes) {
|
||||||
if (isDav(pane.root) && DavRegistry.server(DavPath.parse(pane.root).server) == null) {
|
if (isDav(pane.root) && !DavPath.parse(pane.root).serverKnown) {
|
||||||
pane.marked.clear();
|
pane.marked.clear();
|
||||||
pane.cache.removeWhere((k, _) => isDav(k));
|
pane.cache.removeWhere((k, _) => isDav(k));
|
||||||
pane.expanded.removeWhere(isDav);
|
pane.expanded.removeWhere(isDav);
|
||||||
|
|
@ -734,7 +777,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
|
|
||||||
/// 다시 열 수 있는 폴더: 로컬은 있으면, WebDAV 는 그 서버가 아직 설정에 있으면 (열 때 확인)
|
/// 다시 열 수 있는 폴더: 로컬은 있으면, WebDAV 는 그 서버가 아직 설정에 있으면 (열 때 확인)
|
||||||
bool _reachable(String path) => isDav(path)
|
bool _reachable(String path) => isDav(path)
|
||||||
? DavRegistry.server(DavPath.parse(path).server) != null
|
? DavPath.parse(path).serverKnown
|
||||||
: Directory(path).existsSync();
|
: Directory(path).existsSync();
|
||||||
|
|
||||||
/// [_reachable] 을 화면을 멈추지 않고 (2초 넘게 걸리면 있다고 보고 열어 본다)
|
/// [_reachable] 을 화면을 멈추지 않고 (2초 넘게 걸리면 있다고 보고 열어 본다)
|
||||||
|
|
@ -748,7 +791,10 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
String _volumeOf(String path) {
|
String _volumeOf(String path) {
|
||||||
if (isDav(path)) return '$davScheme${DavPath.parse(path).server}/';
|
if (isDav(path)) {
|
||||||
|
final d = DavPath.parse(path);
|
||||||
|
return '${d.scheme}${d.server}/';
|
||||||
|
}
|
||||||
// 37: Windows 네트워크 공유 (\\NAS\공유) 는 그 공유가 맨 위
|
// 37: Windows 네트워크 공유 (\\NAS\공유) 는 그 공유가 맨 위
|
||||||
if (Platform.isWindows && path.startsWith(r'\\')) return uncRoot(path);
|
if (Platform.isWindows && path.startsWith(r'\\')) return uncRoot(path);
|
||||||
String best = _local.first.$1;
|
String best = _local.first.$1;
|
||||||
|
|
@ -781,9 +827,15 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
} else {
|
} else {
|
||||||
pane.errors.remove(dir);
|
pane.errors.remove(dir);
|
||||||
}
|
}
|
||||||
|
pane.hostKeyErrors.remove(dir);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
pane.cache[dir] = const [];
|
pane.cache[dir] = const [];
|
||||||
pane.errors[dir] = isAndroidRestricted(dir) ? androidRestrictedError : '$e';
|
pane.errors[dir] = isAndroidRestricted(dir) ? androidRestrictedError : '$e';
|
||||||
|
if (e is SftpHostKeyException) {
|
||||||
|
pane.hostKeyErrors[dir] = e;
|
||||||
|
} else {
|
||||||
|
pane.hostKeyErrors.remove(dir);
|
||||||
|
}
|
||||||
// 37-2: 보던 폴더가 밖에서 지워졌으면 남아 있는 가장 가까운 상위 폴더로 (제목 · 경로 칸도 그 폴더로)
|
// 37-2: 보던 폴더가 밖에서 지워졌으면 남아 있는 가장 가까운 상위 폴더로 (제목 · 경로 칸도 그 폴더로)
|
||||||
if (!isDav(dir) && samePath(dir, pane.current) && !samePath(dir, pane.root) && !Directory(dir).existsSync()) {
|
if (!isDav(dir) && samePath(dir, pane.current) && !samePath(dir, pane.root) && !Directory(dir).existsSync()) {
|
||||||
var up = vDirname(dir);
|
var up = vDirname(dir);
|
||||||
|
|
@ -1346,7 +1398,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: Text(tr('저장 공간 분석')),
|
title: Text(tr('저장 공간 분석')),
|
||||||
content: Text(tr('WebDAV 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?')),
|
content: Text(tr('WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?')),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||||
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('분석'))),
|
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('분석'))),
|
||||||
|
|
@ -1797,7 +1849,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
|
|
||||||
Future<void> _addMkv(List<String> paths) async {
|
Future<void> _addMkv(List<String> paths) async {
|
||||||
if (paths.any(isDav)) {
|
if (paths.any(isDav)) {
|
||||||
_snack(tr('WebDAV 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.'));
|
_snack(tr('WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final videos = <String>[];
|
final videos = <String>[];
|
||||||
|
|
@ -2617,7 +2669,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
if (_viaDav([l, r]))
|
if (_viaDav([l, r]))
|
||||||
Text(tr('WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).'),
|
Text(tr('WebDAV · SFTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).'),
|
||||||
style: const TextStyle(fontSize: 12, color: JjColors.accent)),
|
style: const TextStyle(fontSize: 12, color: JjColors.accent)),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
|
@ -4003,16 +4055,18 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
|
|
||||||
/// 202: 좁은 창의 끝 버튼 - WebDAV 서버 추가 · 경로 입력
|
/// 202: 좁은 창의 끝 버튼 - WebDAV 서버 추가 · 경로 입력
|
||||||
Widget _headerMenu(_Pane pane) => PopupMenuButton<String>(
|
Widget _headerMenu(_Pane pane) => PopupMenuButton<String>(
|
||||||
tooltip: tr('경로 입력 · WebDAV'),
|
tooltip: tr('경로 입력 · 네트워크 서버'),
|
||||||
icon: const Icon(Icons.more_vert, size: 18),
|
icon: const Icon(Icons.more_vert, size: 18),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
onSelected: (v) => v == 'path' ? _enterPath(pane) : _editDav(pane, null),
|
onSelected: (v) => switch (v) { 'path' => _enterPath(pane), 'sftp' => _editSftp(pane, null), _ => _editDav(pane, null) },
|
||||||
itemBuilder: (_) => [
|
itemBuilder: (_) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'path',
|
value: 'path',
|
||||||
child: ListTile(dense: true, leading: const Icon(Icons.edit_location_alt_outlined), title: Text(tr('경로 입력')))),
|
child: ListTile(dense: true, leading: const Icon(Icons.edit_location_alt_outlined), title: Text(tr('경로 입력')))),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'dav', child: ListTile(dense: true, leading: const Icon(Icons.cloud_outlined), title: Text(tr('WebDAV 서버 추가')))),
|
value: 'dav', child: ListTile(dense: true, leading: const Icon(Icons.cloud_outlined), title: Text(tr('WebDAV 서버 추가')))),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'sftp', child: ListTile(dense: true, leading: const Icon(Icons.vpn_key_outlined), title: Text(tr('SFTP 서버 추가')))),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -4094,7 +4148,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
foregroundColor: samePath(pane.root, path) ? JjColors.accent : null,
|
foregroundColor: samePath(pane.root, path) ? JjColors.accent : null,
|
||||||
),
|
),
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
isDav(path)
|
isSftp(path)
|
||||||
|
? Icons.vpn_key_outlined
|
||||||
|
: isDav(path)
|
||||||
? Icons.cloud_outlined
|
? Icons.cloud_outlined
|
||||||
: _networkDrives.contains(path)
|
: _networkDrives.contains(path)
|
||||||
? Icons.lan_outlined
|
? Icons.lan_outlined
|
||||||
|
|
@ -4115,7 +4171,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
_goTo(pane, path);
|
_goTo(pane, path);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: isDav(path) ? () => _editDav(pane, path) : null,
|
onLongPress: isSftp(path) ? () => _editSftp(pane, path) : isDav(path) ? () => _editDav(pane, path) : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -4144,6 +4200,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
onPressed: () => _editDav(pane, null),
|
onPressed: () => _editDav(pane, null),
|
||||||
),
|
),
|
||||||
// 37: 경로를 직접 넣어 가기 (Windows 는 \\NAS\공유 도)
|
// 37: 경로를 직접 넣어 가기 (Windows 는 \\NAS\공유 도)
|
||||||
|
// 88: SFTP 서버 추가
|
||||||
|
IconButton(
|
||||||
|
tooltip: tr('SFTP 서버 추가'),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Icons.vpn_key_outlined, size: 16),
|
||||||
|
onPressed: () => _editSftp(pane, null),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: tr('경로 입력'),
|
tooltip: tr('경로 입력'),
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
|
|
@ -4199,6 +4262,19 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
await _goTo(pane, '$davScheme${saved.id}/');
|
await _goTo(pane, '$davScheme${saved.id}/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 88: SFTP 서버 추가 (없으면 [root] 는 null) · 고치기 (탭 길게 누르기). 저장하면 그 서버로 간다.
|
||||||
|
Future<void> _editSftp(_Pane pane, String? root) async {
|
||||||
|
setState(() => _active = _panes.indexOf(pane));
|
||||||
|
final old = root == null ? null : SftpRegistry.server(DavPath.parse(root).server);
|
||||||
|
final saved = await editSftpServer(context, c, old: old);
|
||||||
|
if (saved == null || !mounted) return;
|
||||||
|
for (final x in _panes) {
|
||||||
|
x.cache.removeWhere((k, _) => isSftp(k) && DavPath.parse(k).server == saved.id);
|
||||||
|
x.hostKeyErrors.removeWhere((k, _) => isSftp(k) && DavPath.parse(k).server == saved.id);
|
||||||
|
}
|
||||||
|
await _goTo(pane, '$sftpScheme${saved.id}/');
|
||||||
|
}
|
||||||
|
|
||||||
/// 한 줄. [list]: "폴더 + 파일 목록" 의 오른쪽 목록 (트리 아님). [compact]: 폴더 트리 (열 · 표시 버튼 없이)
|
/// 한 줄. [list]: "폴더 + 파일 목록" 의 오른쪽 목록 (트리 아님). [compact]: 폴더 트리 (열 · 표시 버튼 없이)
|
||||||
Widget _rowView(_Pane pane, _Row row, {bool list = false, bool compact = false}) {
|
Widget _rowView(_Pane pane, _Row row, {bool list = false, bool compact = false}) {
|
||||||
final e = row.entry;
|
final e = row.entry;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../core/secret_gate.dart';
|
import '../core/secret_gate.dart';
|
||||||
|
import '../core/sftp.dart' show SftpRegistry;
|
||||||
import '../core/vfs.dart';
|
import '../core/vfs.dart';
|
||||||
import '../core/webdav.dart';
|
import '../core/webdav.dart';
|
||||||
import '../l10n/tr.dart';
|
import '../l10n/tr.dart';
|
||||||
|
|
@ -15,10 +16,15 @@ Future<String?> pickFolder(BuildContext context, String title, [String? initial]
|
||||||
? showAndroidFolderBrowser(context, title: title, initialDirectory: initial)
|
? showAndroidFolderBrowser(context, title: title, initialDirectory: initial)
|
||||||
: FilePicker.getDirectoryPath(dialogTitle: title, initialDirectory: initial);
|
: FilePicker.getDirectoryPath(dialogTitle: title, initialDirectory: initial);
|
||||||
|
|
||||||
/// 124: 이 기기 폴더 또는 WebDAV 서버의 폴더 고르기 (서버가 없으면 바로 이 기기 폴더).
|
/// 124: 이 기기 폴더 또는 WebDAV · SFTP (88) 서버의 폴더 고르기 (서버가 없으면 바로 이 기기 폴더).
|
||||||
/// 환경 설정에서 추가한 서버가 실시간 동기화 · 모니터링의 고르는 곳에도 바로 보이게.
|
/// 환경 설정에서 추가한 서버가 실시간 동기화 · 모니터링의 고르는 곳에도 바로 보이게.
|
||||||
Future<String?> pickFolderOrDav(BuildContext context, String title, [String? initial]) async {
|
Future<String?> pickFolderOrDav(BuildContext context, String title, [String? initial]) async {
|
||||||
final servers = DavRegistry.servers;
|
// (맨 위 경로, 이름, 설명, SFTP 인지)
|
||||||
|
final servers = [
|
||||||
|
for (final s in DavRegistry.servers) ('$davScheme${s.id}/', s.label, s.url, false),
|
||||||
|
for (final s in SftpRegistry.servers)
|
||||||
|
('$sftpScheme${s.id}/', s.label, '${s.user.isEmpty ? '' : '${s.user}@'}${s.host}${s.port == 22 ? '' : ':${s.port}'}', true),
|
||||||
|
];
|
||||||
if (servers.isEmpty) return pickFolder(context, title, initial);
|
if (servers.isEmpty) return pickFolder(context, title, initial);
|
||||||
final where = await showDialog<String>(
|
final where = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -29,17 +35,18 @@ Future<String?> pickFolderOrDav(BuildContext context, String title, [String? ini
|
||||||
onPressed: () => Navigator.pop(ctx, ''),
|
onPressed: () => Navigator.pop(ctx, ''),
|
||||||
child: ListTile(leading: const Icon(Icons.folder_outlined), title: Text(tr('이 기기의 폴더'))),
|
child: ListTile(leading: const Icon(Icons.folder_outlined), title: Text(tr('이 기기의 폴더'))),
|
||||||
),
|
),
|
||||||
for (final s in servers)
|
for (final (root, label, info, sftp) in servers)
|
||||||
SimpleDialogOption(
|
SimpleDialogOption(
|
||||||
onPressed: () => Navigator.pop(ctx, s.id),
|
onPressed: () => Navigator.pop(ctx, root),
|
||||||
child: ListTile(leading: const Icon(Icons.cloud_outlined), title: Text(s.label), subtitle: Text(s.url)),
|
child: ListTile(
|
||||||
|
leading: Icon(sftp ? Icons.vpn_key_outlined : Icons.cloud_outlined), title: Text(label), subtitle: Text(info)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (where == null || !context.mounted) return null;
|
if (where == null || !context.mounted) return null;
|
||||||
if (where.isEmpty) return pickFolder(context, title, initial != null && !isDav(initial) ? initial : null);
|
if (where.isEmpty) return pickFolder(context, title, initial != null && !isDav(initial) ? initial : null);
|
||||||
final start = initial != null && isDav(initial) && DavPath.parse(initial).server == where ? initial : '$davScheme$where/';
|
final start = initial != null && isDav(initial) && vNorm(initial).startsWith(where) ? initial : where;
|
||||||
return showDialog<String>(context: context, builder: (_) => _DavFolderDialog(title: title, start: start));
|
return showDialog<String>(context: context, builder: (_) => _DavFolderDialog(title: title, start: start));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import '../core/vfs.dart';
|
import '../core/vfs.dart';
|
||||||
import '../core/webdav.dart' show DavRegistry;
|
|
||||||
import '../l10n/tr.dart';
|
import '../l10n/tr.dart';
|
||||||
|
|
||||||
/// 화면에 보일 경로 (67): Android 는 "내장 저장소 › Download › 폴더", WebDAV 는 "☁ 서버 › 폴더", Windows 는 "D: › 폴더"
|
/// 화면에 보일 경로 (67): Android 는 "내장 저장소 › Download › 폴더", WebDAV 는 "☁ 서버 › 폴더", Windows 는 "D: › 폴더"
|
||||||
String friendlyPath(String path) {
|
String friendlyPath(String path) {
|
||||||
if (isDav(path)) {
|
if (isDav(path)) {
|
||||||
final d = DavPath.parse(path);
|
final d = DavPath.parse(path);
|
||||||
final label = DavRegistry.server(d.server)?.label ?? 'WebDAV';
|
return ['${d.isSftp ? '🔑' : '☁'} ${d.serverLabel}', ...d.rel.split('/').where((s) => s.isNotEmpty)].join(' › ');
|
||||||
return ['☁ $label', ...d.rel.split('/').where((s) => s.isNotEmpty)].join(' › ');
|
|
||||||
}
|
}
|
||||||
if (Platform.isAndroid || path.startsWith('/storage/')) {
|
if (Platform.isAndroid || path.startsWith('/storage/')) {
|
||||||
final m = RegExp(r'^/storage/(emulated/\d+|[^/]+)(/.*)?$').firstMatch(path);
|
final m = RegExp(r'^/storage/(emulated/\d+|[^/]+)(/.*)?$').firstMatch(path);
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ class _QuickLookState extends State<QuickLook> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return _info([if (dav) tr('WebDAV 파일은 이름 · 크기 · 날짜만 봅니다')]);
|
return _info([if (dav) tr('WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _info(List<String> lines) => Center(
|
Widget _info(List<String> lines) => Center(
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import 'app_actions.dart';
|
||||||
import 'cef_setup.dart';
|
import 'cef_setup.dart';
|
||||||
import 'cleanup_dialog.dart';
|
import 'cleanup_dialog.dart';
|
||||||
import 'copy_sync_settings.dart';
|
import 'copy_sync_settings.dart';
|
||||||
|
import 'sftp_settings.dart';
|
||||||
import 'webdav_settings.dart';
|
import 'webdav_settings.dart';
|
||||||
import 'explorer_look.dart' show ExplorerStyle;
|
import 'explorer_look.dart' show ExplorerStyle;
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
@ -842,6 +843,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
CopySyncSettings(c: c),
|
CopySyncSettings(c: c),
|
||||||
// WebDAV 서버 (탐색기 · Rsync 화면 위쪽 탭)
|
// WebDAV 서버 (탐색기 · Rsync 화면 위쪽 탭)
|
||||||
WebDavSettings(c: c),
|
WebDavSettings(c: c),
|
||||||
|
// 88: SFTP 서버 (WebDAV 옆 탭)
|
||||||
|
SftpSettings(c: c),
|
||||||
ViewerSettings(c: c),
|
ViewerSettings(c: c),
|
||||||
]),
|
]),
|
||||||
// Rsync 화면: rsync 옵션 · 가져오기 · 실시간 동기화 (lsync) · 백그라운드로 실행
|
// Rsync 화면: rsync 옵션 · 가져오기 · 실시간 동기화 (lsync) · 백그라운드로 실행
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,365 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../app/app_controller.dart';
|
||||||
|
import '../core/secret_gate.dart';
|
||||||
|
import '../core/sftp.dart';
|
||||||
|
import '../core/webdav.dart' show DavLockedException;
|
||||||
|
import '../l10n/tr.dart';
|
||||||
|
import 'confirm.dart';
|
||||||
|
import 'setting_tile.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
import 'webdav_settings.dart' show davServerUsers;
|
||||||
|
|
||||||
|
/// 88: 환경 설정 > 파일 탐색기 > SFTP: 서버 목록 (추가 · 고치기 · 지우기 · 연결 확인).
|
||||||
|
/// 서버마다 파일 탐색기 · Rsync 화면 위쪽 (WebDAV 옆) 에 탭이 생긴다.
|
||||||
|
class SftpSettings extends StatelessWidget {
|
||||||
|
final AppController c;
|
||||||
|
const SftpSettings({super.key, required this.c});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final servers = c.settings.sftpServers;
|
||||||
|
return Column(children: [
|
||||||
|
SettingTile(
|
||||||
|
leading: const Icon(Icons.vpn_key_outlined),
|
||||||
|
title: const Text('SFTP'),
|
||||||
|
subtitle: Text(tr('SSH 서버 (NAS · 리눅스) 의 폴더를 파일 탐색기 · Rsync 화면에서 열고 복사 · 동기화합니다 (위쪽 탭). '
|
||||||
|
'비밀번호 · 개인 키 · 키 암호는 이 기기의 안전 저장소에만 둡니다. 처음 연결할 때 서버의 키 지문을 확인합니다.')),
|
||||||
|
trailing: TextButton.icon(
|
||||||
|
onPressed: () => editSftpServer(context, c),
|
||||||
|
icon: const Icon(Icons.add, size: 18),
|
||||||
|
label: Text(tr('추가')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final s in servers)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16),
|
||||||
|
child: SettingTile(
|
||||||
|
dense: true,
|
||||||
|
leading: const Icon(Icons.vpn_key, color: JjColors.accent),
|
||||||
|
title: Text(s.label),
|
||||||
|
subtitle: Text(
|
||||||
|
[
|
||||||
|
'${s.user.isEmpty ? '' : '${s.user}@'}${s.host}${s.port == 22 ? '' : ':${s.port}'}',
|
||||||
|
if (s.root.isNotEmpty) s.root,
|
||||||
|
if (s.privateKey.isNotEmpty) tr('개인 키'),
|
||||||
|
if (s.legacyRsa) '⚠ ${tr('오래된 ssh-rsa 허용')}',
|
||||||
|
if (s.hostKeyFingerprint.isEmpty) tr('서버 키 아직 확인 안 함'),
|
||||||
|
].join(' · '),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis),
|
||||||
|
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: tr('고치기'),
|
||||||
|
icon: const Icon(Icons.edit_outlined, size: 18),
|
||||||
|
onPressed: () => editSftpServer(context, c, old: s),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: tr('지우기'),
|
||||||
|
icon: const Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
|
||||||
|
onPressed: () => deleteSftpServer(context, c, s),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 서버 지우기 (쓰는 곳을 알린 뒤 확인). 지웠으면 true
|
||||||
|
Future<bool> deleteSftpServer(BuildContext context, AppController c, SftpServer s) async {
|
||||||
|
final users = davServerUsers(c.settings, s.id);
|
||||||
|
final ok = await confirmAction(
|
||||||
|
context,
|
||||||
|
title: trf('SFTP 서버 "{0}" 을(를) 지울까요?', [s.label]),
|
||||||
|
body: tr('서버 설정과 이 기기에 저장된 비밀번호 · 개인 키 · 믿은 서버 키를 지웁니다. 서버의 파일은 그대로입니다. '
|
||||||
|
'이 서버를 쓰는 동기화 · 복사는 다시 넣을 때까지 연결되지 않습니다.') +
|
||||||
|
(users.isEmpty
|
||||||
|
? ''
|
||||||
|
: '\n\n${trf('이 서버를 쓰는 것 {0}개 (지워도 목록에는 남고, 실행하면 서버가 없다고 알립니다):', [users.length])}\n'
|
||||||
|
'${users.take(8).map((u) => '• $u').join('\n')}${users.length > 8 ? '\n…' : ''}'),
|
||||||
|
ok: tr('지우기'),
|
||||||
|
);
|
||||||
|
if (!ok) return false;
|
||||||
|
await c.updateSettings((x) => x.sftpServers = [for (final y in x.sftpServers) if (y.id != s.id) y]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 처음 보는 서버 키를 믿을지 묻는다 (TOFU). 믿으면 true. 바뀐 키는 여기서 믿지 않는다 (서버 고치기에서 저장된 키를 지운 뒤)
|
||||||
|
Future<bool> askTrustHostKey(BuildContext context, String serverLabel, SftpHostKeyException e) async {
|
||||||
|
if (e.changed) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
icon: const Icon(Icons.gpp_bad_outlined, color: JjColors.danger),
|
||||||
|
title: Text(trf('"{0}" 서버의 키가 바뀌었습니다', [serverLabel])),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 520,
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(tr('중간에서 가로채는 공격일 수 있어 연결하지 않았습니다. 서버를 다시 설치했거나 키를 바꾼 것이 확실할 때만, '
|
||||||
|
'서버 설정의 [믿은 키 지우기] 뒤 다시 연결해 새 키를 확인하세요.')),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
SelectableText('${tr('예전')}: ${e.previous}\n${tr('지금')}: ${e.fingerprint} (${e.type})',
|
||||||
|
style: const TextStyle(fontFamily: 'monospace', fontSize: 12)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
actions: [FilledButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('닫기')))],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final ok = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
icon: const Icon(Icons.fingerprint),
|
||||||
|
title: Text(trf('"{0}" 에 처음 연결합니다', [serverLabel])),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 520,
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(tr('서버의 키 지문이 서버 (NAS 설정 · ssh-keygen -lf) 에서 본 것과 같은지 확인하세요. 믿으면 이 기기에 저장하고, '
|
||||||
|
'다음부터 키가 바뀌면 연결하지 않고 알립니다.')),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
SelectableText('${e.fingerprint}\n(${e.type})', style: const TextStyle(fontFamily: 'monospace', fontSize: 12)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('취소'))),
|
||||||
|
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('믿고 연결'))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return ok == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 믿은 서버 키를 설정에 저장 (탐색기에서 처음 연결할 때)
|
||||||
|
Future<void> trustSftpHostKey(AppController c, String id, SftpHostKeyException e) =>
|
||||||
|
c.updateSettings((x) => x.sftpServers = [
|
||||||
|
for (final y in x.sftpServers) y.id == id ? y.copyWith(hostKeyType: e.type, hostKeyFingerprint: e.fingerprint) : y,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/// SFTP 서버 추가 · 고치기 창. 저장한 서버를 돌려준다 (취소면 null)
|
||||||
|
Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpServer? old}) async {
|
||||||
|
// 저장된 비밀번호 · 키가 칸에 채워지므로 먼저 마스터 비밀번호 (정해 두었으면)
|
||||||
|
if (old != null && (old.password.isNotEmpty || old.privateKey.isNotEmpty) && !await SecretGate.pass(force: true)) return null;
|
||||||
|
if (!context.mounted) return null;
|
||||||
|
final name = TextEditingController(text: old?.name ?? '');
|
||||||
|
final host = TextEditingController(text: old?.host ?? '');
|
||||||
|
final port = TextEditingController(text: '${old?.port ?? 22}');
|
||||||
|
final user = TextEditingController(text: old?.user ?? '');
|
||||||
|
final pass = TextEditingController(text: old?.password ?? '');
|
||||||
|
final passphrase = TextEditingController(text: old?.keyPassphrase ?? '');
|
||||||
|
final root = TextEditingController(text: old?.root ?? '');
|
||||||
|
var key = old?.privateKey ?? '';
|
||||||
|
var legacy = old?.legacyRsa ?? false;
|
||||||
|
var keyType = old?.hostKeyType ?? '';
|
||||||
|
var fingerprint = old?.hostKeyFingerprint ?? '';
|
||||||
|
var hide = true;
|
||||||
|
String? result;
|
||||||
|
var passed = false;
|
||||||
|
var testing = false;
|
||||||
|
|
||||||
|
SftpServer current() => SftpServer(
|
||||||
|
id: old?.id ?? 'sftp${DateTime.now().microsecondsSinceEpoch}',
|
||||||
|
name: name.text.trim(),
|
||||||
|
host: host.text.trim(),
|
||||||
|
port: int.tryParse(port.text.trim()) ?? 22,
|
||||||
|
user: user.text.trim(),
|
||||||
|
root: root.text.trim(),
|
||||||
|
password: pass.text,
|
||||||
|
privateKey: key,
|
||||||
|
keyPassphrase: passphrase.text,
|
||||||
|
legacyRsa: legacy,
|
||||||
|
hostKeyType: keyType,
|
||||||
|
hostKeyFingerprint: fingerprint,
|
||||||
|
);
|
||||||
|
|
||||||
|
final saved = await showDialog<SftpServer>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => StatefulBuilder(
|
||||||
|
builder: (ctx, set) {
|
||||||
|
Future<void> test() async {
|
||||||
|
FocusScope.of(ctx).unfocus();
|
||||||
|
set(() {
|
||||||
|
testing = true;
|
||||||
|
result = null;
|
||||||
|
});
|
||||||
|
for (var round = 0; round < 2; round++) {
|
||||||
|
final client = SftpRemote(current());
|
||||||
|
try {
|
||||||
|
final items = await client.list('/');
|
||||||
|
final (free, _) = await client.quota();
|
||||||
|
passed = true;
|
||||||
|
result = trf('연결됨: 항목 {0}개{1}', [items.length, free == null ? '' : ' · ${tr('남은 용량')} ${(free / (1 << 30)).toStringAsFixed(1)}GB']);
|
||||||
|
} on SftpHostKeyException catch (e) {
|
||||||
|
passed = false;
|
||||||
|
result = e.message;
|
||||||
|
// 처음 보는 키: 묻고 믿으면 이 창의 값에 넣고 한 번 더 (저장은 [저장] 때)
|
||||||
|
if (!e.changed && ctx.mounted && await askTrustHostKey(ctx, current().label, e)) {
|
||||||
|
keyType = e.type;
|
||||||
|
fingerprint = e.fingerprint;
|
||||||
|
result = null;
|
||||||
|
client.close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} on DavLockedException catch (e) {
|
||||||
|
passed = false;
|
||||||
|
result = e.message;
|
||||||
|
} catch (e) {
|
||||||
|
passed = false;
|
||||||
|
result = '$e';
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ctx.mounted) set(() => testing = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pickKey() async {
|
||||||
|
final files = await c.services.storage.pickFiles(title: tr('개인 키 파일 고르기'), extensions: const []);
|
||||||
|
if (files.isEmpty) return;
|
||||||
|
try {
|
||||||
|
final f = File(files.first);
|
||||||
|
// 개인 키는 작다 - 큰 파일은 키가 아니다 (실수로 다른 파일을 고름)
|
||||||
|
if (await f.length() > 64 * 1024) throw FormatException(tr('개인 키 파일이 아닙니다 (너무 큼)'));
|
||||||
|
final text = await f.readAsString();
|
||||||
|
if (!text.contains('PRIVATE KEY')) throw FormatException(tr('개인 키 파일이 아닙니다 (OpenSSH · PEM 형식의 비밀 키를 고르세요, .pub 아님)'));
|
||||||
|
set(() {
|
||||||
|
key = text;
|
||||||
|
result = tr('개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)');
|
||||||
|
passed = true;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
set(() {
|
||||||
|
result = e is FormatException ? e.message : '$e';
|
||||||
|
passed = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final ok = host.text.trim().isNotEmpty && (int.tryParse(port.text.trim()) ?? 0) > 0;
|
||||||
|
return AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: Text(old == null ? tr('SFTP 서버 추가') : tr('SFTP 서버 고치기')),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 520,
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
TextField(controller: name, decoration: InputDecoration(labelText: tr('이름 (탭에 보임, 비우면 주소)'))),
|
||||||
|
Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: host,
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
decoration: InputDecoration(labelText: tr('서버 주소'), helperText: tr('예: nas.local · 192.168.0.10 · 100.x.y.z (Tailscale)')),
|
||||||
|
onChanged: (_) => set(() {}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: 80,
|
||||||
|
child: TextField(
|
||||||
|
controller: port,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: InputDecoration(labelText: tr('포트')),
|
||||||
|
onChanged: (_) => set(() {}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
TextField(controller: user, decoration: InputDecoration(labelText: tr('아이디'))),
|
||||||
|
TextField(
|
||||||
|
controller: pass,
|
||||||
|
obscureText: hide,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: tr('비밀번호 (개인 키만 쓰면 비움)'),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
tooltip: hide ? tr('보이기') : tr('가리기'),
|
||||||
|
icon: Icon(hide ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 18),
|
||||||
|
onPressed: () => set(() => hide = !hide),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
icon: const Icon(Icons.key_outlined, size: 16),
|
||||||
|
label: Text(key.isEmpty ? tr('개인 키 파일 고르기') : tr('개인 키 바꾸기')),
|
||||||
|
onPressed: pickKey,
|
||||||
|
),
|
||||||
|
if (key.isNotEmpty) ...[
|
||||||
|
Text(tr('개인 키 있음'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||||
|
TextButton(onPressed: () => set(() => key = ''), child: Text(tr('개인 키 빼기'))),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
if (key.isNotEmpty)
|
||||||
|
TextField(
|
||||||
|
controller: passphrase,
|
||||||
|
obscureText: hide,
|
||||||
|
decoration: InputDecoration(labelText: tr('개인 키 암호 (없으면 비움)')),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: root,
|
||||||
|
decoration: InputDecoration(labelText: tr('시작 폴더 (비우면 홈 폴더)'), helperText: tr('예: /volume1/video · 탭의 맨 위가 됩니다')),
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
value: legacy,
|
||||||
|
onChanged: (v) => set(() => legacy = v ?? false),
|
||||||
|
title: Text(tr('오래된 ssh-rsa 허용')),
|
||||||
|
subtitle: Text(tr('SHA-1 서명 · 키 교환을 씁니다. 오래된 NAS 가 다른 방식을 모를 때만 켜세요 (보안이 약함)')),
|
||||||
|
),
|
||||||
|
// 믿은 서버 키 (처음 연결 때 확인)
|
||||||
|
Text(
|
||||||
|
fingerprint.isEmpty
|
||||||
|
? tr('서버 키: 아직 확인 안 함 ([연결 확인] 을 누르면 지문을 보이고 묻습니다)')
|
||||||
|
: '${tr('믿은 서버 키')}: $fingerprint${keyType.isEmpty ? '' : ' ($keyType)'}',
|
||||||
|
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||||
|
if (fingerprint.isNotEmpty)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: TextButton(
|
||||||
|
// 바뀐 키를 새로 믿으려면: 마스터 비밀번호 (정해 두었으면) 뒤 지우고 다시 확인
|
||||||
|
onPressed: () async {
|
||||||
|
if (!await SecretGate.pass(force: true)) return;
|
||||||
|
set(() {
|
||||||
|
fingerprint = '';
|
||||||
|
keyType = '';
|
||||||
|
result = tr('믿은 키를 지웠습니다. [연결 확인] 으로 새 키를 확인하세요');
|
||||||
|
passed = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text(tr('믿은 키 지우기')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (testing) const LinearProgressIndicator(),
|
||||||
|
if (result != null)
|
||||||
|
Text(result!, style: TextStyle(fontSize: 12, color: passed ? Colors.greenAccent : Colors.redAccent)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
if (old != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (await deleteSftpServer(ctx, c, old) && ctx.mounted) Navigator.pop(ctx);
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.redAccent),
|
||||||
|
child: Text(tr('지우기')),
|
||||||
|
),
|
||||||
|
TextButton(onPressed: !ok || testing ? null : test, child: Text(tr('연결 확인'))),
|
||||||
|
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('취소'))),
|
||||||
|
FilledButton(onPressed: ok ? () => Navigator.pop(ctx, current()) : null, child: Text(tr('저장'))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (saved == null) return null;
|
||||||
|
await c.updateSettings((x) => x.sftpServers = [
|
||||||
|
for (final y in x.sftpServers) y.id == saved.id ? saved : y,
|
||||||
|
if (!x.sftpServers.any((y) => y.id == saved.id)) saved,
|
||||||
|
]);
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
@ -110,8 +110,8 @@ void main() {
|
||||||
// 좁은 창 (위아래 두 창 · 폰): 끝 버튼은 [⋮] 하나로 늘 보임 → 경로 입력
|
// 좁은 창 (위아래 두 창 · 폰): 끝 버튼은 [⋮] 하나로 늘 보임 → 경로 입력
|
||||||
tester.view.physicalSize = const Size(560, 900);
|
tester.view.physicalSize = const Size(560, 900);
|
||||||
await calm(tester);
|
await calm(tester);
|
||||||
expect(find.byTooltip('경로 입력 · WebDAV'), findsWidgets);
|
expect(find.byTooltip('경로 입력 · 네트워크 서버'), findsWidgets);
|
||||||
await tester.tap(find.byTooltip('경로 입력 · WebDAV').first);
|
await tester.tap(find.byTooltip('경로 입력 · 네트워크 서버').first);
|
||||||
await calm(tester);
|
await calm(tester);
|
||||||
await tester.tap(find.text('경로 입력').last);
|
await tester.tap(find.text('경로 입력').last);
|
||||||
await calm(tester);
|
await calm(tester);
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ void main() {
|
||||||
await tester.drag(tabs, const Offset(-400, 0));
|
await tester.drag(tabs, const Offset(-400, 0));
|
||||||
await settle(tester, () => false, rounds: 10);
|
await settle(tester, () => false, rounds: 10);
|
||||||
expect(tester.getRect(find.text('SD 카드').first).right, lessThanOrEqualTo(tester.getRect(tabs).right + 1), reason: '밀면 끝까지 보임');
|
expect(tester.getRect(find.text('SD 카드').first).right, lessThanOrEqualTo(tester.getRect(tabs).right + 1), reason: '밀면 끝까지 보임');
|
||||||
expect(find.byTooltip('경로 입력 · WebDAV'), findsOneWidget, reason: '끝 버튼은 [⋮] 하나');
|
expect(find.byTooltip('경로 입력 · 네트워크 서버'), findsOneWidget, reason: '끝 버튼은 [⋮] 하나');
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ void main() {
|
||||||
// 72: 비교 - 보낼 것 · 받는 쪽 (WebDAV) 에만 있는 것 (지우기 없으니 그대로 둠)
|
// 72: 비교 - 보낼 것 · 받는 쪽 (WebDAV) 에만 있는 것 (지우기 없으니 그대로 둠)
|
||||||
expect(find.textContaining('→ + inner.txt'), findsOneWidget);
|
expect(find.textContaining('→ + inner.txt'), findsOneWidget);
|
||||||
expect(find.textContaining('= note.txt'), findsOneWidget);
|
expect(find.textContaining('= note.txt'), findsOneWidget);
|
||||||
expect(find.textContaining('WebDAV: rsync 대신 앱이 직접 맞춥니다'), findsOneWidget);
|
expect(find.textContaining('WebDAV · SFTP: rsync 대신 앱이 직접 맞춥니다'), findsOneWidget);
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '실행')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '실행')));
|
||||||
final got = File(p.join(remote.path, '원격', 'inner.txt'));
|
final got = File(p.join(remote.path, '원격', 'inner.txt'));
|
||||||
await settle(tester, () => got.existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty, rounds: 200);
|
await settle(tester, () => got.existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty, rounds: 200);
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,11 @@ void main() {
|
||||||
// ZIP 풀기 · 묶기는 archive 패키지 (MIT) - 두 고지에 이름으로
|
// ZIP 풀기 · 묶기는 archive 패키지 (MIT) - 두 고지에 이름으로
|
||||||
for (final t in [notices, android]) {
|
for (final t in [notices, android]) {
|
||||||
expect(t, contains('archive 4.3.0'));
|
expect(t, contains('archive 4.3.0'));
|
||||||
|
// 88: SFTP (dartssh2, MIT)
|
||||||
|
expect(t, contains('dartssh2 4.1.0'));
|
||||||
}
|
}
|
||||||
|
expect(File('pubspec.lock').readAsStringSync(), matches(RegExp(r'\n dartssh2:\r?\n(?: .*\r?\n){1,8}? version: "4\.1\.0"')),
|
||||||
|
reason: 'dartssh2 판이 바뀌면 고지의 판 번호도 고친다');
|
||||||
expect(File('pubspec.lock').readAsStringSync(), matches(RegExp(r'\n archive:\r?\n(?: .*\r?\n){1,8}? version: "4\.3\.0"')),
|
expect(File('pubspec.lock').readAsStringSync(), matches(RegExp(r'\n archive:\r?\n(?: .*\r?\n){1,8}? version: "4\.3\.0"')),
|
||||||
reason: 'archive 판이 바뀌면 고지의 판 번호도 고친다');
|
reason: 'archive 판이 바뀌면 고지의 판 번호도 고친다');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
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/core/sftp.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/ui/explorer_page.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import 'support/sftp_server.dart';
|
||||||
|
|
||||||
|
/// 88: 파일 탐색기의 SFTP 탭 - 처음 연결하면 서버 키 지문을 보이고 [지문 확인하고 연결] → [믿고 연결] 뒤 목록 (시험 서버 paramiko)
|
||||||
|
void main() {
|
||||||
|
late Directory tmp;
|
||||||
|
SftpTestServer? server;
|
||||||
|
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_sftp88_ui_'));
|
||||||
|
tearDown(() async {
|
||||||
|
SftpRegistry.configure(const []);
|
||||||
|
await server?.stop();
|
||||||
|
server = null;
|
||||||
|
for (var i = 0;; i++) {
|
||||||
|
try {
|
||||||
|
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
|
||||||
|
break;
|
||||||
|
} on FileSystemException {
|
||||||
|
if (i > 30) rethrow;
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> settle(WidgetTester tester, bool Function() done, {int rounds = 300}) async {
|
||||||
|
for (var i = 0; i < rounds; i++) {
|
||||||
|
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
|
||||||
|
await tester.pump();
|
||||||
|
if (done()) break;
|
||||||
|
}
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('처음 연결: 지문 안내 → [지문 확인하고 연결] → [믿고 연결] → 목록 · 지문은 설정에 저장', (tester) async {
|
||||||
|
final root = Directory(p.join(tmp.path, 'root'))..createSync();
|
||||||
|
File(p.join(root.path, 'hello.txt')).writeAsStringSync('hi');
|
||||||
|
final s = await tester.runAsync(() => SftpTestServer.start(root: root.path, hostKeyFile: p.join(tmp.path, 'hk')));
|
||||||
|
server = s;
|
||||||
|
final local = Directory(p.join(tmp.path, 'local'))..createSync();
|
||||||
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
await tester.runAsync(() => c.updateSettings((x) => x
|
||||||
|
..sftpServers = [SftpServer(id: 's1', name: 'NAS', host: '127.0.0.1', port: s!.port, user: 'jj', password: 'pw')]
|
||||||
|
..explorerPaths = ['sftp://s1/', local.path]));
|
||||||
|
tester.view.physicalSize = const Size(1600, 900);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
|
||||||
|
await settle(tester, () => find.text('지문 확인하고 연결').evaluate().isNotEmpty);
|
||||||
|
expect(find.textContaining('처음 연결하는 서버입니다'), findsWidgets);
|
||||||
|
expect(find.textContaining(s!.fingerprint), findsWidgets, reason: '안내에 지문');
|
||||||
|
expect(find.text('hello.txt'), findsNothing, reason: '믿기 전에는 열지 않는다');
|
||||||
|
// 탭: 🔑 아이콘 · 서버 이름
|
||||||
|
expect(find.byIcon(Icons.vpn_key_outlined), findsWidgets);
|
||||||
|
await tester.tap(find.text('지문 확인하고 연결'));
|
||||||
|
await settle(tester, () => find.text('믿고 연결').evaluate().isNotEmpty);
|
||||||
|
expect(find.textContaining(s.fingerprint), findsWidgets, reason: '확인 창에도 지문');
|
||||||
|
await tester.tap(find.text('믿고 연결'));
|
||||||
|
await settle(tester, () => find.text('hello.txt').evaluate().isNotEmpty);
|
||||||
|
expect(find.text('hello.txt'), findsOneWidget);
|
||||||
|
expect(c.settings.sftpServers.single.hostKeyFingerprint, s.fingerprint);
|
||||||
|
expect(c.settings.sftpServers.single.hostKeyType, 'ssh-ed25519');
|
||||||
|
// 화면 내리기 · 연결 닫기
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
await tester.runAsync(() async => SftpRegistry.configure(const []));
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
}, skip: !sftpTestServerAvailable);
|
||||||
|
}
|
||||||
|
|
@ -9,12 +9,12 @@ import 'package:jj_mkvmaker/core/vfs.dart';
|
||||||
import 'package:jj_mkvmaker/services/secret_store.dart';
|
import 'package:jj_mkvmaker/services/secret_store.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import 'support/sftp_server.dart';
|
||||||
|
|
||||||
/// 88: SFTP (dartssh2). 실제 서버 시험은 tool/sftp_test_server.py (paramiko) 를 띄운다 -
|
/// 88: SFTP (dartssh2). 실제 서버 시험은 tool/sftp_test_server.py (paramiko) 를 띄운다 -
|
||||||
/// 파이썬 (JJ_SFTP_PY, 없으면 ..\tools\sftp_test_py 의 venv) 이 없으면 건너뛴다.
|
/// 파이썬 (JJ_SFTP_PY, 없으면 ..\tools\sftp_test_py 의 venv) 이 없으면 건너뛴다.
|
||||||
void main() {
|
void main() {
|
||||||
final py = Platform.environment['JJ_SFTP_PY'] ??
|
final hasServer = sftpTestServerAvailable;
|
||||||
p.normalize(p.join(Directory.current.path, '..', 'tools', 'sftp_test_py', Platform.isWindows ? 'Scripts/python.exe' : 'bin/python'));
|
|
||||||
final hasServer = File(py).existsSync();
|
|
||||||
final keygen = Platform.isWindows ? r'C:\Windows\System32\OpenSSH\ssh-keygen.exe' : '/usr/bin/ssh-keygen';
|
final keygen = Platform.isWindows ? r'C:\Windows\System32\OpenSSH\ssh-keygen.exe' : '/usr/bin/ssh-keygen';
|
||||||
|
|
||||||
group('설정 · 경로 (서버 없이)', () {
|
group('설정 · 경로 (서버 없이)', () {
|
||||||
|
|
@ -83,26 +83,21 @@ void main() {
|
||||||
group('실제 SFTP 서버 (paramiko)', () {
|
group('실제 SFTP 서버 (paramiko)', () {
|
||||||
late Directory tmp;
|
late Directory tmp;
|
||||||
late Directory root;
|
late Directory root;
|
||||||
Process? server;
|
SftpTestServer? server;
|
||||||
late int port;
|
late int port;
|
||||||
late String fingerprint;
|
late String fingerprint;
|
||||||
|
|
||||||
Future<void> start({String? password = 'pw', String? pubkey, String keytype = 'ed25519', bool legacyOnly = false}) async {
|
Future<void> start({String? password = 'pw', String? pubkey, String keytype = 'ed25519', bool legacyOnly = false}) async {
|
||||||
final pr = await Process.start(py, [
|
final s = await SftpTestServer.start(
|
||||||
'tool/sftp_test_server.py',
|
root: root.path,
|
||||||
'--root', root.path,
|
hostKeyFile: p.join(tmp.path, 'hostkey_$keytype'),
|
||||||
'--hostkey', p.join(tmp.path, 'hostkey_$keytype'),
|
password: password,
|
||||||
'--keytype', keytype,
|
pubkey: pubkey,
|
||||||
'--user', 'jj',
|
keytype: keytype,
|
||||||
if (password != null) ...['--password', password],
|
legacyOnly: legacyOnly);
|
||||||
if (pubkey != null) ...['--pubkey', pubkey],
|
server = s;
|
||||||
if (legacyOnly) '--legacy-only',
|
port = s.port;
|
||||||
]);
|
fingerprint = s.fingerprint;
|
||||||
server = pr;
|
|
||||||
final lines = pr.stdout.transform(utf8.decoder).transform(const LineSplitter()).asBroadcastStream();
|
|
||||||
pr.stderr.drain<void>();
|
|
||||||
port = int.parse((await lines.firstWhere((l) => l.startsWith('PORT '))).split(' ')[1]);
|
|
||||||
fingerprint = (await lines.firstWhere((l) => l.startsWith('FINGERPRINT '))).split(' ')[1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
|
|
@ -113,11 +108,7 @@ void main() {
|
||||||
SftpRegistry.configure(const []);
|
SftpRegistry.configure(const []);
|
||||||
final s = server;
|
final s = server;
|
||||||
server = null;
|
server = null;
|
||||||
if (s != null) {
|
await s?.stop();
|
||||||
await s.stdin.close().catchError((_) {});
|
|
||||||
s.kill();
|
|
||||||
await s.exitCode.timeout(const Duration(seconds: 5), onTimeout: () => -1);
|
|
||||||
}
|
|
||||||
for (var i = 0;; i++) {
|
for (var i = 0;; i++) {
|
||||||
try {
|
try {
|
||||||
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
|
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// 88: SFTP 시험 서버 (tool/sftp_test_server.py, paramiko 3.5.1). 파이썬은 JJ_SFTP_PY, 없으면 ..\tools\sftp_test_py 의 venv
|
||||||
|
final sftpTestPython = Platform.environment['JJ_SFTP_PY'] ??
|
||||||
|
p.normalize(p.join(Directory.current.path, '..', 'tools', 'sftp_test_py', Platform.isWindows ? 'Scripts/python.exe' : 'bin/python'));
|
||||||
|
|
||||||
|
/// 시험 서버를 띄울 수 있는지 (없으면 그 시험은 건너뛴다)
|
||||||
|
bool get sftpTestServerAvailable => File(sftpTestPython).existsSync();
|
||||||
|
|
||||||
|
class SftpTestServer {
|
||||||
|
final Process process;
|
||||||
|
final int port;
|
||||||
|
|
||||||
|
/// 서버 호스트 키의 지문 ("SHA256:…")
|
||||||
|
final String fingerprint;
|
||||||
|
SftpTestServer._(this.process, this.port, this.fingerprint);
|
||||||
|
|
||||||
|
/// [root] 폴더를 내놓는 서버. [hostKeyFile] 이 없으면 만든다 (같은 파일이면 같은 키)
|
||||||
|
static Future<SftpTestServer> start({
|
||||||
|
required String root,
|
||||||
|
required String hostKeyFile,
|
||||||
|
String user = 'jj',
|
||||||
|
String? password = 'pw',
|
||||||
|
String? pubkey,
|
||||||
|
String keytype = 'ed25519',
|
||||||
|
bool legacyOnly = false,
|
||||||
|
}) async {
|
||||||
|
final pr = await Process.start(sftpTestPython, [
|
||||||
|
'tool/sftp_test_server.py',
|
||||||
|
'--root', root,
|
||||||
|
'--hostkey', hostKeyFile,
|
||||||
|
'--keytype', keytype,
|
||||||
|
'--user', user,
|
||||||
|
if (password != null) ...['--password', password],
|
||||||
|
if (pubkey != null) ...['--pubkey', pubkey],
|
||||||
|
if (legacyOnly) '--legacy-only',
|
||||||
|
]);
|
||||||
|
final lines = pr.stdout.transform(utf8.decoder).transform(const LineSplitter()).asBroadcastStream();
|
||||||
|
pr.stderr.drain<void>();
|
||||||
|
final port = int.parse((await lines.firstWhere((l) => l.startsWith('PORT '))).split(' ')[1]);
|
||||||
|
final fp = (await lines.firstWhere((l) => l.startsWith('FINGERPRINT '))).split(' ')[1];
|
||||||
|
return SftpTestServer._(pr, port, fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 끝내기 (표준 입력을 닫으면 서버가 끝난다)
|
||||||
|
Future<void> stop() async {
|
||||||
|
await process.stdin.close().catchError((_) {});
|
||||||
|
process.kill();
|
||||||
|
await process.exitCode.timeout(const Duration(seconds: 5), onTimeout: () => -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue