파일 탐색기 (X-plore 참고): 두 창 트리 · 가운데 기능 버튼 · 창 배치 · 버튼 구성

- 위쪽 공통 버튼에 [파일 탐색기], 환경 설정 > 홈 화면에 "파일 탐색기" (앱을 켜면 바로)
- 두 창 (트리: 누르면 그 자리에서 펼치기 · 접기), 저장 장치 고르기 (Windows 드라이브 · Android 저장소)
- 동영상: 누르면 기본 동작 (내장 플레이어 또는 확장자별 재생 프로그램), 길게 누르기 · 오른쪽 클릭 메뉴
  (내장 플레이어로 재생 · 다른 앱으로 열기 · MKV 목록에 추가 · 이름 변경 · 복사 / 이동 · 삭제 · 정보)
- 썸네일 · 해상도 · 길이 (작업 중에는 썸네일을 만들지 않음), 그림 파일 미리 보기
- 가운데 버튼: 상위 폴더 · 새로 고침 · 재생 · MKV 목록에 추가 · 새 폴더 · 이름 변경 · 복사 · 이동 · 삭제 ·
  찾기 · 정렬 기준 · 숨은 항목 · 내역 · 창 배치 · 버튼 구성
- 창 배치: 두 창 / 한 창, 좌우 / 위아래 / 화면 모양 따라, 버튼 줄 두 창 사이 / 끝 / 숨김
- 버튼 구성: 보일 버튼 · 순서
- 다른 앱으로 열기: Windows "연결 프로그램" 창 · Android 앱 고르기 (FileProvider, SD 카드 포함)
- 여러 개 지울 때 확인 창에 이름 · 폴더를 보여 준다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음3
RyuJongJin 2026-10-05 04:14:05 +09:00
parent bba955f6bf
commit 99024d3c6e
20 changed files with 2363 additions and 17 deletions

View File

@ -64,6 +64,9 @@ class MainActivity : FlutterActivity() {
result.error("INSTALL", e.message ?: e.toString(), null) result.error("INSTALL", e.message ?: e.toString(), null)
} }
"openFolder" -> result.success(openFolder(call.argument<String>("path") ?: "")) "openFolder" -> result.success(openFolder(call.argument<String>("path") ?: ""))
"openWith" -> result.success(
openWith(call.argument<String>("path") ?: "", call.argument<Boolean>("choose") ?: false)
)
"setAppIcon" -> result.success(setAppIcon(call.argument<String>("id") ?: "")) "setAppIcon" -> result.success(setAppIcon(call.argument<String>("id") ?: ""))
"keepAlive" -> { "keepAlive" -> {
keepAlive(call.argument<String>("text") ?: "", call.argument<Int>("progress") ?: -1) keepAlive(call.argument<String>("text") ?: "", call.argument<Int>("progress") ?: -1)
@ -259,6 +262,36 @@ class MainActivity : FlutterActivity() {
return true return true
} }
/// 파일 탐색기: 파일을 다른 앱으로 연다. [choose] 면 늘 앱 고르기 창 (이 앱은 뺌), 아니면 기본 앱 (없으면 고르기 창).
/// SD 카드 · USB 파일도 넘길 수 있게 FileProvider 의 root 경로를 쓴다 (res/xml/jj_file_paths.xml).
private fun openWith(path: String, choose: Boolean): Boolean {
val f = File(path)
if (!f.isFile) return false
val uri = try {
androidx.core.content.FileProvider.getUriForFile(this, "$packageName.files", f)
} catch (e: Exception) {
return false
}
val mime = android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(f.extension.lowercase()) ?: "*/*"
val view = Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, mime)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
fun chooser(): Intent = Intent.createChooser(view, null)
.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(android.content.ComponentName(this, MainActivity::class.java)))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
return try {
startActivity(if (choose) chooser() else view)
true
} catch (e: Exception) {
try {
startActivity(chooser())
true
} catch (e2: Exception) {
false
}
}
}
private fun openFolder(path: String): Boolean { private fun openFolder(path: String): Boolean {
// 내장 저장소 (primary) 또는 SD 카드 · USB (/storage/XXXX-XXXX → 문서 ID "XXXX-XXXX:...") // 내장 저장소 (primary) 또는 SD 카드 · USB (/storage/XXXX-XXXX → 문서 ID "XXXX-XXXX:...")
val root = Environment.getExternalStorageDirectory().path val root = Environment.getExternalStorageDirectory().path

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- 업데이트 APK (MainActivity.installApk) --> <!-- 업데이트 APK (MainActivity.installApk) · 파일 탐색기에서 다른 앱으로 열기 (MainActivity.openWith, SD 카드 포함) -->
<paths> <paths>
<cache-path name="updates" path="updates/"/> <cache-path name="updates" path="updates/"/>
<root-path name="root" path="."/>
</paths> </paths>

View File

@ -23,7 +23,9 @@
" 지금 설치됨 ({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}\" 안에 만듭니다": "Created inside \"{0}\"", "\"{0}\" 안에 만듭니다": "Created inside \"{0}\"",
"\"{0}\" 을(를) 지울까요? 되돌릴 수 없습니다.": "Delete \"{0}\"? This cannot be undone.",
"\"{0}\" 이동": "Move \"{0}\"", "\"{0}\" 이동": "Move \"{0}\"",
"\"{0}\" 찾기 · {1}개": "Search \"{0}\" · {1}",
"\"동영상 추가\" 로 파일을 선택하세요": "Choose files with \"Add videos\"", "\"동영상 추가\" 로 파일을 선택하세요": "Choose files with \"Add videos\"",
"\"외부 브라우저로 열기\" 에 쓸 브라우저 (이 PC 에 설치된 것)": "Browser for \"Open in external browser\" (installed on this PC)", "\"외부 브라우저로 열기\" 에 쓸 브라우저 (이 PC 에 설치된 것)": "Browser for \"Open in external browser\" (installed on this PC)",
"# Netscape HTTP Cookie File\n# JJ_MKVMaker 앱 안 브라우저에서 내보냄\n\n": "# Netscape HTTP Cookie File\n# Exported from the JJ_MKVMaker in-app browser\n\n", "# Netscape HTTP Cookie File\n# JJ_MKVMaker 앱 안 브라우저에서 내보냄\n\n": "# Netscape HTTP Cookie File\n# Exported from the JJ_MKVMaker in-app browser\n\n",
@ -96,6 +98,8 @@
"MKV 만들기{0}{1}": "Make MKV{0}{1}", "MKV 만들기{0}{1}": "Make MKV{0}{1}",
"MKV 만들기에 적용됩니다 (영상을 다시 인코딩)": "Applied when making MKVs (the video is re-encoded)", "MKV 만들기에 적용됩니다 (영상을 다시 인코딩)": "Applied when making MKVs (the video is re-encoded)",
"MKV 만들기의 동영상 목록에 {0}개를 추가했습니다.{1}": "Added {0} to the Make MKV video list.{1}", "MKV 만들기의 동영상 목록에 {0}개를 추가했습니다.{1}": "Added {0} to the Make MKV video list.{1}",
"MKV 목록에 {0}개를 추가했습니다.": "Added {0} to the MKV list.",
"MKV 목록에 추가": "Add to MKV list",
"MKV 완성": "MKV done", "MKV 완성": "MKV done",
"MKV 화면": "MKV screen", "MKV 화면": "MKV screen",
"MKV 화면 (기본)": "MKV screen (default)", "MKV 화면 (기본)": "MKV screen (default)",
@ -170,6 +174,10 @@
"{0}개 · {1}": "{0} · {1}", "{0}개 · {1}": "{0} · {1}",
"{0}개 · 주소 {1}개": "{0} · {1} links", "{0}개 · 주소 {1}개": "{0} · {1} links",
"{0}개 선택": "{0} selected", "{0}개 선택": "{0} selected",
"{0}개 표시함": "{0} marked",
"{0}개 항목을 {1}": "{0} items {1}",
"{0}개 항목을 다음 폴더로 {1}:\n{2}": "{0} items to this folder — {1}\n{2}",
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "Delete {0} items? This cannot be undone.",
"{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "Imported {0} into \"Other bookmarks\".", "{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "Imported {0} into \"Other bookmarks\".",
"{0}개를 옮겼습니다 → {1}": "Moved {0} → {1}", "{0}개를 옮겼습니다 → {1}": "Moved {0} → {1}",
"{0}개를 옮겼습니다 → {1} ({2})": "Moved {0} → {1} ({2})", "{0}개를 옮겼습니다 → {1} ({2})": "Moved {0} → {1} ({2})",
@ -178,6 +186,7 @@
"{0}줄": "{0} lines", "{0}줄": "{0} lines",
"{0}초": "{0} s", "{0}초": "{0} s",
"· 백그라운드로: 창만 숨기고 다운로드 · 변환을 계속합니다 ({0} 또는 트레이 아이콘으로 다시 열기)\n· 모두 종료: 다운로드 · 변환을 멈추고 프로그램을 끝냅니다": "· Background: only hides the window and keeps downloading · converting (reopen with {0} or the tray icon)\n· Quit all: stops downloads · conversions and closes the program", "· 백그라운드로: 창만 숨기고 다운로드 · 변환을 계속합니다 ({0} 또는 트레이 아이콘으로 다시 열기)\n· 모두 종료: 다운로드 · 변환을 멈추고 프로그램을 끝냅니다": "· Background: only hides the window and keeps downloading · converting (reopen with {0} or the tray icon)\n· Quit all: stops downloads · conversions and closes the program",
"… 외 {0}개": "… and {0} more",
"※ 다른 작업 중이면 대기열에 넣고 차례대로 합니다. 번역하는 동안 브라우저를 써도 됩니다.": "※ If another job is running, this is queued and done in order. You can use the browser while translating.", "※ 다른 작업 중이면 대기열에 넣고 차례대로 합니다. 번역하는 동안 브라우저를 써도 됩니다.": "※ If another job is running, this is queued and done in order. You can use the browser while translating.",
"※ 시간이 오래 걸립니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)": "※ This takes a long time (1 hour of video ≈ 15–30 min speech recognition + 10–20 min translation per language)", "※ 시간이 오래 걸립니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)": "※ This takes a long time (1 hour of video ≈ 15–30 min speech recognition + 10–20 min translation per language)",
"− 차갑게 · + 따뜻하게": "− cooler · + warmer", "− 차갑게 · + 따뜻하게": "− cooler · + warmer",
@ -199,6 +208,7 @@
"같은 시리즈 (번호만 다른 파일)": "Same series (files that differ only by number)", "같은 시리즈 (번호만 다른 파일)": "Same series (files that differ only by number)",
"같은 폴더의 모든 동영상": "All videos in the same folder", "같은 폴더의 모든 동영상": "All videos in the same folder",
"같은 폴더의 자막은 자동으로 추가됩니다": "Subtitles in the same folder are added automatically", "같은 폴더의 자막은 자동으로 추가됩니다": "Subtitles in the same folder are added automatically",
"거꾸로": "Reverse",
"건너뜀 (출력 폴더의 파일): {0}": "Skipped (file in the output folder): {0}", "건너뜀 (출력 폴더의 파일): {0}": "Skipped (file in the output folder): {0}",
"검색": "Search", "검색": "Search",
"검색 지우기": "Clear search", "검색 지우기": "Clear search",
@ -219,7 +229,9 @@
"글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "Text · button size. Change it any time with − · + on the top bar; press the number in the middle to return to this size (now {0}%)", "글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "Text · button size. Change it any time with − · + on the top bar; press the number in the middle to return to this size (now {0}%)",
"기계 번역": "Machine translation", "기계 번역": "Machine translation",
"기기": "device", "기기": "device",
"기능 버튼 줄": "Button bar",
"기본": "Default", "기본": "Default",
"기본 앱으로 열기": "Open with default app",
"기본 연결 프로그램": "Default app", "기본 연결 프로그램": "Default app",
"기본 크기 ({0}%) 로 · 기본 크기는 환경 설정 > 화면 에서": "Back to the default size ({0}%) · set the default size in Settings > Display", "기본 크기 ({0}%) 로 · 기본 크기는 환경 설정 > 화면 에서": "Back to the default size ({0}%) · set the default size in Settings > Display",
"기본 프로그램": "Default program", "기본 프로그램": "Default program",
@ -235,9 +247,11 @@
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "When off, page scripts do not run (fewer ads · trackers, but script-built sites such as YouTube will not show). Page translation · video detection also stop.", "끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "When off, page scripts do not run (fewer ads · trackers, but script-built sites such as YouTube will not show). Page translation · video detection also stop.",
"끌어다 놓은 항목에 동영상이 없습니다.": "There are no videos in the dropped items.", "끌어다 놓은 항목에 동영상이 없습니다.": "There are no videos in the dropped items.",
"끝": "End", "끝": "End",
"끝 (오른쪽 · 아래)": "Edge (right · bottom)",
"끝 = 현재": "End = now", "끝 = 현재": "End = now",
"끝 시간이 시작보다 빠른 줄: {0}{1}": "Lines whose end is before their start: {0}{1}", "끝 시간이 시작보다 빠른 줄: {0}{1}": "Lines whose end is before their start: {0}{1}",
"끝: {0}": "End: {0}", "끝: {0}": "End: {0}",
"끝나지 못했습니다: {0}": "Did not finish: {0}",
"나가기": "Exit", "나가기": "Exit",
"나머지는 대기했다가 차례로 받습니다": "The rest wait and download in turn", "나머지는 대기했다가 차례로 받습니다": "The rest wait and download in turn",
"나중에": "Later", "나중에": "Later",
@ -250,6 +264,8 @@
"내려받다 끊긴 모델 (.part) · 변환하다 남은 파일 (.tmp)": "Interrupted model downloads (.part) · leftovers from conversion (.tmp)", "내려받다 끊긴 모델 (.part) · 변환하다 남은 파일 (.tmp)": "Interrupted model downloads (.part) · leftovers from conversion (.tmp)",
"내려받은 Chrome 엔진 지우기": "Delete the downloaded Chrome engine", "내려받은 Chrome 엔진 지우기": "Delete the downloaded Chrome engine",
"내보냈습니다: {0}": "Exported: {0}", "내보냈습니다: {0}": "Exported: {0}",
"내역": "History",
"내역 (최근 폴더)": "History (recent folders)",
"내용": "Content", "내용": "Content",
"내용 편집 · 문자셋 변환": "Edit content · convert charset", "내용 편집 · 문자셋 변환": "Edit content · convert charset",
"내장": "Embedded", "내장": "Embedded",
@ -262,6 +278,7 @@
"내장 Chrome 엔진을 지웠습니다.": "Deleted the built-in Chrome engine.", "내장 Chrome 엔진을 지웠습니다.": "Deleted the built-in Chrome engine.",
"내장 저장소": "Internal storage", "내장 저장소": "Internal storage",
"내장 플레이어": "Built-in player", "내장 플레이어": "Built-in player",
"내장 플레이어로 재생": "Play in built-in player",
"네덜란드어": "Dutch", "네덜란드어": "Dutch",
"네팔어": "Nepali", "네팔어": "Nepali",
"노랑": "Yellow", "노랑": "Yellow",
@ -272,10 +289,14 @@
"다른 브라우저의 즐겨찾기 가져오기": "Import bookmarks from another browser", "다른 브라우저의 즐겨찾기 가져오기": "Import bookmarks from another browser",
"다른 앱에서 동영상을 열 때": "When opening a video from another app", "다른 앱에서 동영상을 열 때": "When opening a video from another app",
"다른 앱에서 열기: {0}": "Opened from another app: {0}", "다른 앱에서 열기: {0}": "Opened from another app: {0}",
"다른 앱으로 열기": "Open with another app",
"다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. 주소창 옆 번역 버튼으로 원문 · 번역을 바꿀 수 있습니다.": "Translates pages in other languages into the display language ({0}). The page text is sent to Google Translate. Use the translate button next to the address bar to switch between original · translation.", "다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. 주소창 옆 번역 버튼으로 원문 · 번역을 바꿀 수 있습니다.": "Translates pages in other languages into the display language ({0}). The page text is sent to Google Translate. Use the translate button next to the address bar to switch between original · translation.",
"다른 언어로 번역 (AI · 이 {0}에서)": "Translate to another language (AI · on this {0})", "다른 언어로 번역 (AI · 이 {0}에서)": "Translate to another language (AI · on this {0})",
"다른 이름으로 저장": "Save as", "다른 이름으로 저장": "Save as",
"다른 이름으로 저장: {0} ({1})": "Save as: {0} ({1})", "다른 이름으로 저장: {0} ({1})": "Save as: {0} ({1})",
"다른 창에서 열기": "Open in other pane",
"다른 창으로 복사": "Copy to other pane",
"다른 창으로 이동": "Move to other pane",
"다른 폴더로 이동": "Move to another folder", "다른 폴더로 이동": "Move to another folder",
"다시 만들기 대기로": "Back to waiting for remake", "다시 만들기 대기로": "Back to waiting for remake",
"다시 시작": "Restart", "다시 시작": "Restart",
@ -322,6 +343,7 @@
"동시 MKV 변환 수": "Simultaneous MKV conversions", "동시 MKV 변환 수": "Simultaneous MKV conversions",
"동시 다운로드 수": "Simultaneous downloads", "동시 다운로드 수": "Simultaneous downloads",
"동시에 처리할 개수 (0 = 무한)": "Number processed at once (0 = unlimited)", "동시에 처리할 개수 (0 = 무한)": "Number processed at once (0 = unlimited)",
"동영상": "Video",
"동영상 {0}개 · 전체 선택": "{0} videos · select all", "동영상 {0}개 · 전체 선택": "{0} videos · select all",
"동영상 목록 · 창 위치 저장": "Save video list · window position", "동영상 목록 · 창 위치 저장": "Save video list · window position",
"동영상 목록에 추가: {0}": "Added to the video list: {0}", "동영상 목록에 추가: {0}": "Added to the video list: {0}",
@ -337,6 +359,9 @@
"동영상을 끌어다 놓으면 목록에 추가됩니다": "Drag and drop videos to add them to the list", "동영상을 끌어다 놓으면 목록에 추가됩니다": "Drag and drop videos to add them to the list",
"동영상이 있는 폴더 아래 jj_mkv (기본)": "jj_mkv under the video's folder (default)", "동영상이 있는 폴더 아래 jj_mkv (기본)": "jj_mkv under the video's folder (default)",
"되살리기": "Restore", "되살리기": "Restore",
"두 창": "Two panes",
"두 창 배치": "Two-pane arrangement",
"두 창 사이": "Between panes",
"뒤로": "Back", "뒤로": "Back",
"등록": "Register", "등록": "Register",
"등록 중 일부가 실패했습니다.": "Some registrations failed.", "등록 중 일부가 실패했습니다.": "Some registrations failed.",
@ -373,6 +398,7 @@
"미지정": "Unspecified", "미지정": "Unspecified",
"바꾸면 바로 저장됩니다": "Changes are saved right away", "바꾸면 바로 저장됩니다": "Changes are saved right away",
"바꾼 것이 없습니다 (원본 그대로)": "Nothing changed (original as is)", "바꾼 것이 없습니다 (원본 그대로)": "Nothing changed (original as is)",
"바뀐 날": "Modified",
"바뀐 내용": "Changes", "바뀐 내용": "Changes",
"바로 재생 (기본)": "Play right away (default)", "바로 재생 (기본)": "Play right away (default)",
"바로 전 작업: {0}": "Previous job: {0}", "바로 전 작업: {0}": "Previous job: {0}",
@ -393,6 +419,7 @@
"백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)": "Background: hides only the window and keeps downloading · converting. Reopen with {0} or the tray icon.\nTo quit completely: right-click the tray icon > Quit (always quits whatever the setting)", "백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)": "Background: hides only the window and keeps downloading · converting. Reopen with {0} or the tray icon.\nTo quit completely: right-click the tray icon > Quit (always quits whatever the setting)",
"백그라운드로": "To background", "백그라운드로": "To background",
"백그라운드로 계속 (기본)": "Keep running in the background (default)", "백그라운드로 계속 (기본)": "Keep running in the background (default)",
"버튼 구성": "Button layout",
"번역 모델 내려받는 중 {0}%": "Downloading translation model {0}%", "번역 모델 내려받는 중 {0}%": "Downloading translation model {0}%",
"번역 모델 내려받는 중: {0}": "Downloading translation model: {0}", "번역 모델 내려받는 중: {0}": "Downloading translation model: {0}",
"번역 모델 불러오는 중": "Loading translation model", "번역 모델 불러오는 중": "Loading translation model",
@ -407,7 +434,14 @@
"변경 내용을 저장하지 않고 나갈까요?": "Leave without saving the changes?", "변경 내용을 저장하지 않고 나갈까요?": "Leave without saving the changes?",
"변환": "Convert", "변환": "Convert",
"보기 {0}초 · Ctrl+휠 확대/축소": "View {0} s · Ctrl+wheel to zoom", "보기 {0}초 · Ctrl+휠 확대/축소": "View {0} s · Ctrl+wheel to zoom",
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "Choose the buttons to show and drag to reorder.",
"복사": "Copy",
"복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).": "Copy · move works with two panes (Pane layout > Two panes).",
"복사 · 이동할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "Choose items to copy · move (tap, or mark with the circle on the right).",
"복사(Ctrl+C)한 주소 자동 다운로드": "Auto-download copied (Ctrl+C) URLs", "복사(Ctrl+C)한 주소 자동 다운로드": "Auto-download copied (Ctrl+C) URLs",
"복사하는 중": "Copying",
"복사할까요?": "copy?",
"복사했습니다.": "copied.",
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "This video is unavailable (deleted · region restricted).", "볼 수 없는 영상입니다 (삭제 · 지역 제한).": "This video is unavailable (deleted · region restricted).",
"분석 실패: {0} - {1}": "Analysis failed: {0} - {1}", "분석 실패: {0} - {1}": "Analysis failed: {0} - {1}",
"브라우저 엔진 (앱 안)": "Browser engine (in-app)", "브라우저 엔진 (앱 안)": "Browser engine (in-app)",
@ -425,6 +459,7 @@
"삭제 (중지하고 받던 파일 삭제)": "Delete (stop and delete the partial file)", "삭제 (중지하고 받던 파일 삭제)": "Delete (stop and delete the partial file)",
"삭제 (출력에서 제외)": "Delete (exclude from output)", "삭제 (출력에서 제외)": "Delete (exclude from output)",
"삭제: {0}": "Deleted: {0}", "삭제: {0}": "Deleted: {0}",
"상위 폴더": "Parent folder",
"새 버전 v{0}": "New version v{0}", "새 버전 v{0}": "New version v{0}",
"새 버전 확인: 지금 v{0} · 최신 v{1}": "Update check: current v{0} · latest v{1}", "새 버전 확인: 지금 v{0} · 최신 v{1}": "Update check: current v{0} · latest v{1}",
"새 버전을 확인할 수 없습니다: {0}": "Cannot check for a new version: {0}", "새 버전을 확인할 수 없습니다: {0}": "Cannot check for a new version: {0}",
@ -433,6 +468,7 @@
"새 폴더": "New folder", "새 폴더": "New folder",
"새 폴더 추가": "Add new folder", "새 폴더 추가": "Add new folder",
"새 폴더…": "New folder…", "새 폴더…": "New folder…",
"새로 고침": "Refresh",
"새로고침 (F5)": "Reload (F5)", "새로고침 (F5)": "Reload (F5)",
"색 보정": "Color correction", "색 보정": "Color correction",
"색온도": "Temperature", "색온도": "Temperature",
@ -463,6 +499,11 @@
"세로 고정": "Portrait lock", "세로 고정": "Portrait lock",
"수정": "Edit", "수정": "Edit",
"수정 · 이동": "Edit · move", "수정 · 이동": "Edit · move",
"숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "Even when hidden, you can change the pane layout · buttons from the ⋮ menu at the top.",
"숨김": "Hidden",
"숨은 항목 표시": "Show hidden items",
"숨은 항목을 감춥니다.": "Hidden items are hidden.",
"숨은 항목을 보입니다.": "Hidden items are shown.",
"쉬는 중": "Idle", "쉬는 중": "Idle",
"스웨덴어": "Swedish", "스웨덴어": "Swedish",
"스페인어": "Spanish", "스페인어": "Spanish",
@ -530,6 +571,9 @@
"오른쪽 90°": "Right 90°", "오른쪽 90°": "Right 90°",
"오픈 소스 라이선스": "Open source licenses", "오픈 소스 라이선스": "Open source licenses",
"올린 이 {0}": "Uploaded by {0}", "올린 이 {0}": "Uploaded by {0}",
"옮겼습니다.": "moved.",
"옮기는 중": "Moving",
"옮길까요?": "move?",
"완료": "Done", "완료": "Done",
"완료 정리": "Clear completed", "완료 정리": "Clear completed",
"완료: 성공 {0} / 전체 {1}": "Done: {0} succeeded / {1} total", "완료: 성공 {0} / 전체 {1}": "Done: {0} succeeded / {1} total",
@ -538,6 +582,7 @@
"외부 브라우저": "External browser", "외부 브라우저": "External browser",
"외부 브라우저로 열기": "Open in external browser", "외부 브라우저로 열기": "Open in external browser",
"외부 프로그램으로 재생: {0} ← {1}개": "Play with external program: {0} ← {1}", "외부 프로그램으로 재생: {0} ← {1}개": "Play with external program: {0} ← {1}",
"왼쪽": "Left",
"왼쪽 90°": "Left 90°", "왼쪽 90°": "Left 90°",
"왼쪽 손잡이로 순서 바꾸기 · 아이콘을 끌어 왼쪽 폴더나 목록의 폴더 위에 놓으면 그 폴더로 이동 · 오른쪽 클릭 메뉴": "Reorder with the handle on the left · drag the icon onto a folder on the left or in the list to move it there · right-click menu", "왼쪽 손잡이로 순서 바꾸기 · 아이콘을 끌어 왼쪽 폴더나 목록의 폴더 위에 놓으면 그 폴더로 이동 · 오른쪽 클릭 메뉴": "Reorder with the handle on the left · drag the icon onto a folder on the left or in the list to move it there · right-click menu",
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "Too many requests. Try again in a moment.", "요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "Too many requests. Try again in a moment.",
@ -561,6 +606,7 @@
"웹 페이지를 번역하지 못했습니다: {0}": "Could not translate the web page: {0}", "웹 페이지를 번역하지 못했습니다: {0}": "Could not translate the web page: {0}",
"위 폴더": "Parent folder", "위 폴더": "Parent folder",
"위로": "Up", "위로": "Up",
"위아래": "Top and bottom",
"음성": "Audio", "음성": "Audio",
"음성 {0}개": "{0} audio", "음성 {0}개": "{0} audio",
"음성 선택": "Choose audio", "음성 선택": "Choose audio",
@ -584,12 +630,15 @@
"이 이동 버튼 지우기": "Delete this move button", "이 이동 버튼 지우기": "Delete this move button",
"이 파일에 맞는 자막 우선 (영상 해시)": "Prefer subtitles that match this file (video hash)", "이 파일에 맞는 자막 우선 (영상 해시)": "Prefer subtitles that match this file (video hash)",
"이 파일용": "For this file", "이 파일용": "For this file",
"이 파일을 열 수 있는 앱이 없습니다.": "No app can open this file.",
"이 페이지를 {0}(으)로 번역": "Translate this page to {0}", "이 페이지를 {0}(으)로 번역": "Translate this page to {0}",
"이 폴더 선택": "Choose this folder", "이 폴더 선택": "Choose this folder",
"이 폴더 전체": "This whole folder", "이 폴더 전체": "This whole folder",
"이 폴더에 동영상이 없습니다.": "There are no videos in this folder.",
"이 폴더에 새 폴더": "New folder in this folder", "이 폴더에 새 폴더": "New folder in this folder",
"이 폴더에 현재 페이지 추가": "Add the current page to this folder", "이 폴더에 현재 페이지 추가": "Add the current page to this folder",
"이 폴더에는 고를 파일이 없습니다": "There are no files to choose in this folder", "이 폴더에는 고를 파일이 없습니다": "There are no files to choose in this folder",
"이 폴더의 동영상 재생": "Play videos in this folder",
"이동": "Move", "이동": "Move",
"이동 ({0})": "Move ({0})", "이동 ({0})": "Move ({0})",
"이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)": "Move buttons (bottom right of the details, stacked upward)", "이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)": "Move buttons (bottom right of the details, stacked upward)",
@ -606,6 +655,9 @@
"이동할 폴더를 만들 수 없습니다: {0} ({1})": "Cannot create the move folder: {0} ({1})", "이동할 폴더를 만들 수 없습니다: {0} ({1})": "Cannot create the move folder: {0} ({1})",
"이름": "Name", "이름": "Name",
"이름 바꾸기 · 이동": "Rename · move", "이름 바꾸기 · 이동": "Rename · move",
"이름 변경": "Rename",
"이름을 바꿀 수 없습니다: {0}": "Cannot rename: {0}",
"이름을 바꿀 항목 하나를 고르세요.": "Choose one item to rename.",
"이미 동영상 목록에 있습니다 ({0}개).": "Already in the video list ({0}).", "이미 동영상 목록에 있습니다 ({0}개).": "Already in the video list ({0}).",
"이미 목록에 있는 동영상입니다 ({0}개).": "These videos are already in the list ({0}).", "이미 목록에 있는 동영상입니다 ({0}개).": "These videos are already in the list ({0}).",
"이미 받는 중이거나 받을 수 없는 주소입니다.": "Already downloading or a URL that cannot be downloaded.", "이미 받는 중이거나 받을 수 없는 주소입니다.": "Already downloading or a URL that cannot be downloaded.",
@ -626,6 +678,7 @@
"일반": "General", "일반": "General",
"일본어": "Japanese", "일본어": "Japanese",
"일시정지": "Pause", "일시정지": "Pause",
"읽을 수 없음": "Cannot read",
"임시 파일 · 남은 조각 정리": "Clean temporary files · leftovers", "임시 파일 · 남은 조각 정리": "Clean temporary files · leftovers",
"자동": "Auto", "자동": "Auto",
"자동 감지": "Auto-detect", "자동 감지": "Auto-detect",
@ -708,12 +761,14 @@
"전체 화면": "Full screen", "전체 화면": "Full screen",
"전체 화면 (F)": "Full screen (F)", "전체 화면 (F)": "Full screen (F)",
"점검": "Check", "점검": "Check",
"정렬 기준": "Sort by",
"정리": "Clean up", "정리": "Clean up",
"정리 ({0})": "Clean up ({0})", "정리 ({0})": "Clean up ({0})",
"정리…": "Clean up…", "정리…": "Clean up…",
"정리할 파일을 찾는 중…": "Looking for files to clean up…", "정리할 파일을 찾는 중…": "Looking for files to clean up…",
"정리할 파일이 없었습니다.": "There was nothing to clean up.", "정리할 파일이 없었습니다.": "There was nothing to clean up.",
"정리했습니다: 파일 {0}개 · {1}": "Cleaned up: {0} files · {1}", "정리했습니다: 파일 {0}개 · {1}": "Cleaned up: {0} files · {1}",
"정보": "Info",
"정사각 1:1": "Square 1:1", "정사각 1:1": "Square 1:1",
"정지": "Stop", "정지": "Stop",
"정하지 않음: MKV 목록에서 [이동] 을 처음 누를 때 고릅니다": "Not set: you choose it the first time you press [Move] in the MKV list", "정하지 않음: MKV 목록에서 [이동] 을 처음 누를 때 고릅니다": "Not set: you choose it the first time you press [Move] in the MKV list",
@ -725,6 +780,8 @@
"종료 중입니다.": "Quitting.", "종료 중입니다.": "Quitting.",
"종료 창 닫힘": "Quit window closed", "종료 창 닫힘": "Quit window closed",
"종료할까요?": "Quit?", "종료할까요?": "Quit?",
"종류 (확장자)": "Type (extension)",
"좌우": "Side by side",
"주소": "Address", "주소": "Address",
"주소 입력 또는 검색 (Ctrl+L)": "Enter an address or search (Ctrl+L)", "주소 입력 또는 검색 (Ctrl+L)": "Enter an address or search (Ctrl+L)",
"준비 중": "Preparing", "준비 중": "Preparing",
@ -755,17 +812,25 @@
"지금: {0}": "Now: {0}", "지금: {0}": "Now: {0}",
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {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",
"지우지 못했습니다: {0}": "Could not delete: {0}",
"지울 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "Choose items to delete (tap, or mark with the circle on the right).",
"지원하지 않는 protobuf 형식 (wire type {0}, 위치 {1})": "Unsupported protobuf format (wire type {0}, position {1})", "지원하지 않는 protobuf 형식 (wire type {0}, 위치 {1})": "Unsupported protobuf format (wire type {0}, position {1})",
"지원하지 않는 문자셋: {0}": "Unsupported charset: {0}", "지원하지 않는 문자셋: {0}": "Unsupported charset: {0}",
"직접 입력": "Custom", "직접 입력": "Custom",
"직접 입력…": "Custom…", "직접 입력…": "Custom…",
"진행 중인 작업 없음": "No jobs in progress", "진행 중인 작업 없음": "No jobs in progress",
"진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?": "A job is in progress: {0}\nStop it and quit?", "진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?": "A job is in progress: {0}\nStop it and quit?",
"창": "Panes",
"창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "Icon of the window, taskbar, tray and the top-left button in the app (the program file and desktop shortcut icons stay the same)", "창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "Icon of the window, taskbar, tray and the top-left button in the app (the program file and desktop shortcut icons stay the same)",
"창 배치": "Pane layout",
"창 배치 · 버튼 구성": "Pane layout · buttons",
"창 보이기 / 숨기기 단축키": "Hotkey to show / hide the window", "창 보이기 / 숨기기 단축키": "Hotkey to show / hide the window",
"창 숨김 → 프로세스 종료": "Window hidden → process exit", "창 숨김 → 프로세스 종료": "Window hidden → process exit",
"창 종료": "Close window", "창 종료": "Close window",
"찾기": "Search",
"찾기: {0}": "Search: {0}",
"찾는 즐겨찾기가 없습니다.": "No matching bookmarks.", "찾는 즐겨찾기가 없습니다.": "No matching bookmarks.",
"찾은 것이 없습니다.": "Nothing found.",
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "No subtitles found. Try the original English title or add languages.", "찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "No subtitles found. Try the original English title or add languages.",
"채도": "Saturation", "채도": "Saturation",
"채우기 (가운데 잘라냄)": "Fill (crop the center)", "채우기 (가운데 잘라냄)": "Fill (crop the center)",
@ -784,6 +849,7 @@
"추가 ({0})": "Add ({0})", "추가 ({0})": "Add ({0})",
"추가 취소": "Cancel adding", "추가 취소": "Cancel adding",
"추가: {0} (내장 자막 {1}개, 같은 폴더 자막 {2}개)": "Added: {0} ({1} embedded subtitles, {2} subtitles in the same folder)", "추가: {0} (내장 자막 {1}개, 같은 폴더 자막 {2}개)": "Added: {0} ({1} embedded subtitles, {2} subtitles in the same folder)",
"추가할 동영상이 없습니다.": "No videos to add.",
"출력: {0}": "Output: {0}", "출력: {0}": "Output: {0}",
"취소": "Cancel", "취소": "Cancel",
"취소 · 실패한 다운로드가 남긴 .part · .ytdl · .aria2 · 합치기 전 조각 파일 (jj_yt-dlp · jj_aria2)": ".part · .ytdl · .aria2 · unmerged fragments left by cancelled · failed downloads (jj_yt-dlp · jj_aria2)", "취소 · 실패한 다운로드가 남긴 .part · .ytdl · .aria2 · 합치기 전 조각 파일 (jj_yt-dlp · jj_aria2)": ".part · .ytdl · .aria2 · unmerged fragments left by cancelled · failed downloads (jj_yt-dlp · jj_aria2)",
@ -791,6 +857,7 @@
"취소했습니다.": "Cancelled.", "취소했습니다.": "Cancelled.",
"켜져 있는 창에서 (기본)": "In the open window (default)", "켜져 있는 창에서 (기본)": "In the open window (default)",
"코덱": "Codec", "코덱": "Codec",
"크기": "Size",
"크메르어": "Khmer", "크메르어": "Khmer",
"타갈로그어": "Tagalog", "타갈로그어": "Tagalog",
"타밀어": "Tamil", "타밀어": "Tamil",
@ -804,9 +871,12 @@
"트랙 {0}": "Track {0}", "트랙 {0}": "Track {0}",
"트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "Reopen with the tray icon or the hotkey below", "트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "Reopen with the tray icon or the hotkey below",
"트레이 정리": "Clean up tray", "트레이 정리": "Clean up tray",
"파일 관리자에서 보기": "Show in file manager",
"파일 앱의 \"다음으로 열기\" · 공유에서 JJ_MKVMaker 를 골랐을 때": "When JJ_MKVMaker is chosen from \"Open with\" · Share in a file app", "파일 앱의 \"다음으로 열기\" · 공유에서 JJ_MKVMaker 를 골랐을 때": "When JJ_MKVMaker is chosen from \"Open with\" · Share in a file app",
"파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 \"동영상 추가\" 로 고르세요.": "The file location is unknown, so it will only be played. To make an MKV, choose it with \"Add videos\".", "파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 \"동영상 추가\" 로 고르세요.": "The file location is unknown, so it will only be played. To make an MKV, choose it with \"Add videos\".",
"파일 이름": "File name", "파일 이름": "File name",
"파일 탐색기": "File explorer",
"파일 탐색기 (지금 여기)": "File explorer (you are here)",
"파일 확인 중…": "Checking files…", "파일 확인 중…": "Checking files…",
"파일을 분석할 수 없습니다: {0}": "Cannot analyze the file: {0}", "파일을 분석할 수 없습니다: {0}": "Cannot analyze the file: {0}",
"팝업 끄기": "Close pop-up", "팝업 끄기": "Close pop-up",
@ -833,8 +903,10 @@
"폴더가 아닙니다: {0}": "Not a folder: {0}", "폴더가 아닙니다: {0}": "Not a folder: {0}",
"폴더를 만들 수 없습니다: {0}": "Cannot create the folder: {0}", "폴더를 만들 수 없습니다: {0}": "Cannot create the folder: {0}",
"폴란드어": "Polish", "폴란드어": "Polish",
"표시 (여러 개 고르기)": "Mark (select several)",
"표시 이름": "Display name", "표시 이름": "Display name",
"표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다": "You can edit display names right here. Pressing a video row three times moves it to the first button's folder", "표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다": "You can edit display names right here. Pressing a video row three times moves it to the first button's folder",
"표시 지우기": "Clear marks",
"표준": "Standard", "표준": "Standard",
"푸는 중": "Extracting", "푸는 중": "Extracting",
"프랑스어": "French", "프랑스어": "French",
@ -854,6 +926,7 @@
"필수 프로그램 점검": "Check required programs", "필수 프로그램 점검": "Check required programs",
"필수 프로그램이 모두 설치되어 있습니다.": "All required programs are installed.", "필수 프로그램이 모두 설치되어 있습니다.": "All required programs are installed.",
"하위 폴더가 없습니다": "No subfolders", "하위 폴더가 없습니다": "No subfolders",
"한 창": "One pane",
"한국어": "Korean", "한국어": "Korean",
"한글": "Hangul", "한글": "Hangul",
"해제": "Unset", "해제": "Unset",
@ -870,6 +943,7 @@
"화면": "Display", "화면": "Display",
"화면 · 색 보정": "Frame · color", "화면 · 색 보정": "Frame · color",
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "Translates all of the program's text on this device. It may take a few minutes.", "화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "Translates all of the program's text on this device. It may take a few minutes.",
"화면 모양 따라": "Follow screen shape",
"화면 방향": "Screen orientation", "화면 방향": "Screen orientation",
"화면 비율": "Aspect ratio", "화면 비율": "Aspect ratio",
"화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도": "Aspect ratio (landscape · portrait) · rotation · brightness · contrast · saturation · temperature", "화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도": "Aspect ratio (landscape · portrait) · rotation · brightness · contrast · saturation · temperature",

View File

@ -23,7 +23,9 @@
" 지금 설치됨 ({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}\" 찾기 · {1}개": "「{0}」を検索 · {1}件",
"\"동영상 추가\" 로 파일을 선택하세요": "「動画を追加」でファイルを選んでください", "\"동영상 추가\" 로 파일을 선택하세요": "「動画を追加」でファイルを選んでください",
"\"외부 브라우저로 열기\" 에 쓸 브라우저 (이 PC 에 설치된 것)": "「外部ブラウザーで開く」に使うブラウザー(この PC にインストール済み)", "\"외부 브라우저로 열기\" 에 쓸 브라우저 (이 PC 에 설치된 것)": "「外部ブラウザーで開く」に使うブラウザー(この PC にインストール済み)",
"# Netscape HTTP Cookie File\n# JJ_MKVMaker 앱 안 브라우저에서 내보냄\n\n": "# Netscape HTTP Cookie File\n# JJ_MKVMaker アプリ内ブラウザーから書き出し\n\n", "# Netscape HTTP Cookie File\n# JJ_MKVMaker 앱 안 브라우저에서 내보냄\n\n": "# Netscape HTTP Cookie File\n# JJ_MKVMaker アプリ内ブラウザーから書き出し\n\n",
@ -96,6 +98,8 @@
"MKV 만들기{0}{1}": "MKV 作成{0}{1}", "MKV 만들기{0}{1}": "MKV 作成{0}{1}",
"MKV 만들기에 적용됩니다 (영상을 다시 인코딩)": "MKV 作成に適用されます(映像を再エンコード)", "MKV 만들기에 적용됩니다 (영상을 다시 인코딩)": "MKV 作成に適用されます(映像を再エンコード)",
"MKV 만들기의 동영상 목록에 {0}개를 추가했습니다.{1}": "MKV 作成の動画リストに {0}個を追加しました。{1}", "MKV 만들기의 동영상 목록에 {0}개를 추가했습니다.{1}": "MKV 作成の動画リストに {0}個を追加しました。{1}",
"MKV 목록에 {0}개를 추가했습니다.": "MKV リストに {0} 件を追加しました。",
"MKV 목록에 추가": "MKV リストに追加",
"MKV 완성": "MKV 完成", "MKV 완성": "MKV 完成",
"MKV 화면": "MKV 画面", "MKV 화면": "MKV 画面",
"MKV 화면 (기본)": "MKV 画面(既定)", "MKV 화면 (기본)": "MKV 画面(既定)",
@ -170,6 +174,10 @@
"{0}개 · {1}": "{0}個・{1}", "{0}개 · {1}": "{0}個・{1}",
"{0}개 · 주소 {1}개": "{0}個・URL {1}個", "{0}개 · 주소 {1}개": "{0}個・URL {1}個",
"{0}개 선택": "{0}個選択", "{0}개 선택": "{0}個選択",
"{0}개 표시함": "{0} 件をマーク",
"{0}개 항목을 {1}": "{0} 件の項目を{1}",
"{0}개 항목을 다음 폴더로 {1}:\n{2}": "{0} 件の項目を次のフォルダーへ{1}\n{2}",
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "{0} 件の項目を削除しますか? 元に戻せません。",
"{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "{0}個を「その他のブックマーク」に取り込みました。", "{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "{0}個を「その他のブックマーク」に取り込みました。",
"{0}개를 옮겼습니다 → {1}": "{0}個を移動しました → {1}", "{0}개를 옮겼습니다 → {1}": "{0}個を移動しました → {1}",
"{0}개를 옮겼습니다 → {1} ({2})": "{0}個を移動しました → {1}({2})", "{0}개를 옮겼습니다 → {1} ({2})": "{0}個を移動しました → {1}({2})",
@ -178,6 +186,7 @@
"{0}줄": "{0}行", "{0}줄": "{0}行",
"{0}초": "{0}秒", "{0}초": "{0}秒",
"· 백그라운드로: 창만 숨기고 다운로드 · 변환을 계속합니다 ({0} 또는 트레이 아이콘으로 다시 열기)\n· 모두 종료: 다운로드 · 변환을 멈추고 프로그램을 끝냅니다": "· バックグラウンドへ: ウィンドウだけ隠してダウンロード・変換を続けます({0} またはトレイアイコンで再表示)\n· すべて終了: ダウンロード・変換を止めてプログラムを終了します", "· 백그라운드로: 창만 숨기고 다운로드 · 변환을 계속합니다 ({0} 또는 트레이 아이콘으로 다시 열기)\n· 모두 종료: 다운로드 · 변환을 멈추고 프로그램을 끝냅니다": "· バックグラウンドへ: ウィンドウだけ隠してダウンロード・変換を続けます({0} またはトレイアイコンで再表示)\n· すべて終了: ダウンロード・変換を止めてプログラムを終了します",
"… 외 {0}개": "… ほか {0} 件",
"※ 다른 작업 중이면 대기열에 넣고 차례대로 합니다. 번역하는 동안 브라우저를 써도 됩니다.": "※ 他の作業中ならキューに入れて順番に行います。翻訳中もブラウザーを使えます。", "※ 다른 작업 중이면 대기열에 넣고 차례대로 합니다. 번역하는 동안 브라우저를 써도 됩니다.": "※ 他の作業中ならキューに入れて順番に行います。翻訳中もブラウザーを使えます。",
"※ 시간이 오래 걸립니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)": "※ 時間がかかります(動画 1時間 ≈ 音声認識 15〜30分 + 言語ごとの翻訳 10〜20分)", "※ 시간이 오래 걸립니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)": "※ 時間がかかります(動画 1時間 ≈ 音声認識 15〜30分 + 言語ごとの翻訳 10〜20分)",
"− 차갑게 · + 따뜻하게": "− 寒色 · + 暖色", "− 차갑게 · + 따뜻하게": "− 寒色 · + 暖色",
@ -199,6 +208,7 @@
"같은 시리즈 (번호만 다른 파일)": "同じシリーズ(番号だけ違うファイル)", "같은 시리즈 (번호만 다른 파일)": "同じシリーズ(番号だけ違うファイル)",
"같은 폴더의 모든 동영상": "同じフォルダーのすべての動画", "같은 폴더의 모든 동영상": "同じフォルダーのすべての動画",
"같은 폴더의 자막은 자동으로 추가됩니다": "同じフォルダーの字幕は自動で追加されます", "같은 폴더의 자막은 자동으로 추가됩니다": "同じフォルダーの字幕は自動で追加されます",
"거꾸로": "逆順",
"건너뜀 (출력 폴더의 파일): {0}": "スキップ(出力フォルダーのファイル): {0}", "건너뜀 (출력 폴더의 파일): {0}": "スキップ(出力フォルダーのファイル): {0}",
"검색": "検索", "검색": "検索",
"검색 지우기": "検索をクリア", "검색 지우기": "検索をクリア",
@ -219,7 +229,9 @@
"글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字・ボタンの大きさです。上部バーの − ・ + でその都度変更でき、中央の数字を押すとこの大きさに戻ります(現在 {0}%)", "글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字・ボタンの大きさです。上部バーの − ・ + でその都度変更でき、中央の数字を押すとこの大きさに戻ります(現在 {0}%)",
"기계 번역": "機械翻訳", "기계 번역": "機械翻訳",
"기기": "端末", "기기": "端末",
"기능 버튼 줄": "機能ボタンの列",
"기본": "既定", "기본": "既定",
"기본 앱으로 열기": "既定のアプリで開く",
"기본 연결 프로그램": "既定のアプリ", "기본 연결 프로그램": "既定のアプリ",
"기본 크기 ({0}%) 로 · 기본 크기는 환경 설정 > 화면 에서": "既定の大きさ({0}%)に戻す・既定の大きさは 環境設定 > 画面 で", "기본 크기 ({0}%) 로 · 기본 크기는 환경 설정 > 화면 에서": "既定の大きさ({0}%)に戻す・既定の大きさは 環境設定 > 画面 で",
"기본 프로그램": "既定のプログラム", "기본 프로그램": "既定のプログラム",
@ -235,9 +247,11 @@
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "オフにするとページのスクリプトを実行しません (広告 · 追跡は減りますが、YouTube のようにスクリプトで作られたサイトは表示されません)。ページ翻訳 · 動画の検出も止まります。", "끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "オフにするとページのスクリプトを実行しません (広告 · 追跡は減りますが、YouTube のようにスクリプトで作られたサイトは表示されません)。ページ翻訳 · 動画の検出も止まります。",
"끌어다 놓은 항목에 동영상이 없습니다.": "ドロップした項目に動画がありません。", "끌어다 놓은 항목에 동영상이 없습니다.": "ドロップした項目に動画がありません。",
"끝": "終了", "끝": "終了",
"끝 (오른쪽 · 아래)": "端 (右 · 下)",
"끝 = 현재": "終了 = 現在", "끝 = 현재": "終了 = 現在",
"끝 시간이 시작보다 빠른 줄: {0}{1}": "終了時間が開始より早い行: {0}{1}", "끝 시간이 시작보다 빠른 줄: {0}{1}": "終了時間が開始より早い行: {0}{1}",
"끝: {0}": "終了: {0}", "끝: {0}": "終了: {0}",
"끝나지 못했습니다: {0}": "完了できませんでした: {0}",
"나가기": "終了する", "나가기": "終了する",
"나머지는 대기했다가 차례로 받습니다": "残りは待機して順番にダウンロードします", "나머지는 대기했다가 차례로 받습니다": "残りは待機して順番にダウンロードします",
"나중에": "後で", "나중에": "後で",
@ -250,6 +264,8 @@
"내려받다 끊긴 모델 (.part) · 변환하다 남은 파일 (.tmp)": "ダウンロードが途切れたモデル(.part)・変換の残り(.tmp)", "내려받다 끊긴 모델 (.part) · 변환하다 남은 파일 (.tmp)": "ダウンロードが途切れたモデル(.part)・変換の残り(.tmp)",
"내려받은 Chrome 엔진 지우기": "ダウンロードした Chrome エンジンを削除", "내려받은 Chrome 엔진 지우기": "ダウンロードした Chrome エンジンを削除",
"내보냈습니다: {0}": "書き出しました: {0}", "내보냈습니다: {0}": "書き出しました: {0}",
"내역": "履歴",
"내역 (최근 폴더)": "履歴 (最近のフォルダー)",
"내용": "内容", "내용": "内容",
"내용 편집 · 문자셋 변환": "内容の編集・文字コード変換", "내용 편집 · 문자셋 변환": "内容の編集・文字コード変換",
"내장": "内蔵", "내장": "内蔵",
@ -262,6 +278,7 @@
"내장 Chrome 엔진을 지웠습니다.": "内蔵 Chrome エンジンを削除しました。", "내장 Chrome 엔진을 지웠습니다.": "内蔵 Chrome エンジンを削除しました。",
"내장 저장소": "内部ストレージ", "내장 저장소": "内部ストレージ",
"내장 플레이어": "内蔵プレーヤー", "내장 플레이어": "内蔵プレーヤー",
"내장 플레이어로 재생": "内蔵プレーヤーで再生",
"네덜란드어": "オランダ語", "네덜란드어": "オランダ語",
"네팔어": "ネパール語", "네팔어": "ネパール語",
"노랑": "黄色", "노랑": "黄色",
@ -272,10 +289,14 @@
"다른 브라우저의 즐겨찾기 가져오기": "他のブラウザーのブックマークを取り込む", "다른 브라우저의 즐겨찾기 가져오기": "他のブラウザーのブックマークを取り込む",
"다른 앱에서 동영상을 열 때": "他のアプリで動画を開いたとき", "다른 앱에서 동영상을 열 때": "他のアプリで動画を開いたとき",
"다른 앱에서 열기: {0}": "他のアプリから開く: {0}", "다른 앱에서 열기: {0}": "他のアプリから開く: {0}",
"다른 앱으로 열기": "別のアプリで開く",
"다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. 주소창 옆 번역 버튼으로 원문 · 번역을 바꿀 수 있습니다.": "ほかの言語のページを表示言語 ({0}) に翻訳します。ページの文章は Google 翻訳に送られます。アドレスバー横の翻訳ボタンで原文 · 翻訳を切り替えられます。", "다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. 주소창 옆 번역 버튼으로 원문 · 번역을 바꿀 수 있습니다.": "ほかの言語のページを表示言語 ({0}) に翻訳します。ページの文章は Google 翻訳に送られます。アドレスバー横の翻訳ボタンで原文 · 翻訳を切り替えられます。",
"다른 언어로 번역 (AI · 이 {0}에서)": "他の言語に翻訳(AI・この{0}で)", "다른 언어로 번역 (AI · 이 {0}에서)": "他の言語に翻訳(AI・この{0}で)",
"다른 이름으로 저장": "名前を付けて保存", "다른 이름으로 저장": "名前を付けて保存",
"다른 이름으로 저장: {0} ({1})": "名前を付けて保存: {0}({1})", "다른 이름으로 저장: {0} ({1})": "名前を付けて保存: {0}({1})",
"다른 창에서 열기": "もう一方のペインで開く",
"다른 창으로 복사": "もう一方のペインへコピー",
"다른 창으로 이동": "もう一方のペインへ移動",
"다른 폴더로 이동": "他のフォルダーに移動", "다른 폴더로 이동": "他のフォルダーに移動",
"다시 만들기 대기로": "作り直し待ちに戻す", "다시 만들기 대기로": "作り直し待ちに戻す",
"다시 시작": "再起動", "다시 시작": "再起動",
@ -322,6 +343,7 @@
"동시 MKV 변환 수": "同時 MKV 変換数", "동시 MKV 변환 수": "同時 MKV 変換数",
"동시 다운로드 수": "同時ダウンロード数", "동시 다운로드 수": "同時ダウンロード数",
"동시에 처리할 개수 (0 = 무한)": "同時に処理する数(0 = 無制限)", "동시에 처리할 개수 (0 = 무한)": "同時に処理する数(0 = 無制限)",
"동영상": "動画",
"동영상 {0}개 · 전체 선택": "動画 {0}個・すべて選択", "동영상 {0}개 · 전체 선택": "動画 {0}個・すべて選択",
"동영상 목록 · 창 위치 저장": "動画リスト・ウィンドウ位置を保存", "동영상 목록 · 창 위치 저장": "動画リスト・ウィンドウ位置を保存",
"동영상 목록에 추가: {0}": "動画リストに追加: {0}", "동영상 목록에 추가: {0}": "動画リストに追加: {0}",
@ -337,6 +359,9 @@
"동영상을 끌어다 놓으면 목록에 추가됩니다": "動画をドラッグ&ドロップするとリストに追加されます", "동영상을 끌어다 놓으면 목록에 추가됩니다": "動画をドラッグ&ドロップするとリストに追加されます",
"동영상이 있는 폴더 아래 jj_mkv (기본)": "動画があるフォルダーの下の jj_mkv(既定)", "동영상이 있는 폴더 아래 jj_mkv (기본)": "動画があるフォルダーの下の jj_mkv(既定)",
"되살리기": "元に戻す", "되살리기": "元に戻す",
"두 창": "2 ペイン",
"두 창 배치": "2 ペインの配置",
"두 창 사이": "ペインの間",
"뒤로": "戻る", "뒤로": "戻る",
"등록": "登録", "등록": "登録",
"등록 중 일부가 실패했습니다.": "登録の一部に失敗しました。", "등록 중 일부가 실패했습니다.": "登録の一部に失敗しました。",
@ -373,6 +398,7 @@
"미지정": "未指定", "미지정": "未指定",
"바꾸면 바로 저장됩니다": "変更するとすぐ保存されます", "바꾸면 바로 저장됩니다": "変更するとすぐ保存されます",
"바꾼 것이 없습니다 (원본 그대로)": "変更なし(元のまま)", "바꾼 것이 없습니다 (원본 그대로)": "変更なし(元のまま)",
"바뀐 날": "更新日時",
"바뀐 내용": "変更内容", "바뀐 내용": "変更内容",
"바로 재생 (기본)": "すぐ再生(既定)", "바로 재생 (기본)": "すぐ再生(既定)",
"바로 전 작업: {0}": "直前の作業: {0}", "바로 전 작업: {0}": "直前の作業: {0}",
@ -393,6 +419,7 @@
"백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)": "バックグラウンド: ウィンドウだけ隠してダウンロード・変換は続けます。{0} またはトレイアイコンで再表示します。\n完全に終了するには トレイアイコンを右クリック > 終了(どの設定でも常に終了)", "백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)": "バックグラウンド: ウィンドウだけ隠してダウンロード・変換は続けます。{0} またはトレイアイコンで再表示します。\n完全に終了するには トレイアイコンを右クリック > 終了(どの設定でも常に終了)",
"백그라운드로": "バックグラウンドへ", "백그라운드로": "バックグラウンドへ",
"백그라운드로 계속 (기본)": "バックグラウンドで続ける(既定)", "백그라운드로 계속 (기본)": "バックグラウンドで続ける(既定)",
"버튼 구성": "ボタン構成",
"번역 모델 내려받는 중 {0}%": "翻訳モデルをダウンロード中 {0}%", "번역 모델 내려받는 중 {0}%": "翻訳モデルをダウンロード中 {0}%",
"번역 모델 내려받는 중: {0}": "翻訳モデルをダウンロード中: {0}", "번역 모델 내려받는 중: {0}": "翻訳モデルをダウンロード中: {0}",
"번역 모델 불러오는 중": "翻訳モデルを読み込み中", "번역 모델 불러오는 중": "翻訳モデルを読み込み中",
@ -407,7 +434,14 @@
"변경 내용을 저장하지 않고 나갈까요?": "変更を保存せずに終了しますか?", "변경 내용을 저장하지 않고 나갈까요?": "変更を保存せずに終了しますか?",
"변환": "変換", "변환": "変換",
"보기 {0}초 · Ctrl+휠 확대/축소": "表示 {0}秒・Ctrl+ホイールで拡大/縮小", "보기 {0}초 · Ctrl+휠 확대/축소": "表示 {0}秒・Ctrl+ホイールで拡大/縮小",
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "表示するボタンを選び、ドラッグで順序を変えます。",
"복사": "コピー",
"복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).": "コピー · 移動は 2 ペインで使います (ペインの配置 > 2 ペイン)。",
"복사 · 이동할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "コピー · 移動する項目を選んでください (タップ、または右の丸でマーク)。",
"복사(Ctrl+C)한 주소 자동 다운로드": "コピー(Ctrl+C)した URL を自動ダウンロード", "복사(Ctrl+C)한 주소 자동 다운로드": "コピー(Ctrl+C)した URL を自動ダウンロード",
"복사하는 중": "コピー中",
"복사할까요?": "コピーしますか?",
"복사했습니다.": "コピーしました。",
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "視聴できない動画です(削除・地域制限)。", "볼 수 없는 영상입니다 (삭제 · 지역 제한).": "視聴できない動画です(削除・地域制限)。",
"분석 실패: {0} - {1}": "分析失敗: {0} - {1}", "분석 실패: {0} - {1}": "分析失敗: {0} - {1}",
"브라우저 엔진 (앱 안)": "ブラウザーエンジン(アプリ内)", "브라우저 엔진 (앱 안)": "ブラウザーエンジン(アプリ内)",
@ -425,6 +459,7 @@
"삭제 (중지하고 받던 파일 삭제)": "削除(中止してダウンロード中のファイルを削除)", "삭제 (중지하고 받던 파일 삭제)": "削除(中止してダウンロード中のファイルを削除)",
"삭제 (출력에서 제외)": "削除(出力から除外)", "삭제 (출력에서 제외)": "削除(出力から除外)",
"삭제: {0}": "削除: {0}", "삭제: {0}": "削除: {0}",
"상위 폴더": "上のフォルダー",
"새 버전 v{0}": "新しいバージョン v{0}", "새 버전 v{0}": "新しいバージョン v{0}",
"새 버전 확인: 지금 v{0} · 최신 v{1}": "新しいバージョンの確認: 現在 v{0} · 最新 v{1}", "새 버전 확인: 지금 v{0} · 최신 v{1}": "新しいバージョンの確認: 現在 v{0} · 最新 v{1}",
"새 버전을 확인할 수 없습니다: {0}": "新しいバージョンを確認できません: {0}", "새 버전을 확인할 수 없습니다: {0}": "新しいバージョンを確認できません: {0}",
@ -433,6 +468,7 @@
"새 폴더": "新しいフォルダー", "새 폴더": "新しいフォルダー",
"새 폴더 추가": "新しいフォルダーを追加", "새 폴더 추가": "新しいフォルダーを追加",
"새 폴더…": "新しいフォルダー…", "새 폴더…": "新しいフォルダー…",
"새로 고침": "更新",
"새로고침 (F5)": "再読み込み(F5)", "새로고침 (F5)": "再読み込み(F5)",
"색 보정": "カラー補正", "색 보정": "カラー補正",
"색온도": "色温度", "색온도": "色温度",
@ -463,6 +499,11 @@
"세로 고정": "縦向き固定", "세로 고정": "縦向き固定",
"수정": "編集", "수정": "編集",
"수정 · 이동": "編集・移動", "수정 · 이동": "編集・移動",
"숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "非表示にしても、上の ⋮ メニューからペインの配置 · ボタン構成を変えられます。",
"숨김": "非表示",
"숨은 항목 표시": "隠し項目を表示",
"숨은 항목을 감춥니다.": "隠し項目を隠します。",
"숨은 항목을 보입니다.": "隠し項目を表示します。",
"쉬는 중": "休止中", "쉬는 중": "休止中",
"스웨덴어": "スウェーデン語", "스웨덴어": "スウェーデン語",
"스페인어": "スペイン語", "스페인어": "スペイン語",
@ -530,6 +571,9 @@
"오른쪽 90°": "右 90°", "오른쪽 90°": "右 90°",
"오픈 소스 라이선스": "オープンソースライセンス", "오픈 소스 라이선스": "オープンソースライセンス",
"올린 이 {0}": "投稿者 {0}", "올린 이 {0}": "投稿者 {0}",
"옮겼습니다.": "移動しました。",
"옮기는 중": "移動中",
"옮길까요?": "移動しますか?",
"완료": "完了", "완료": "完了",
"완료 정리": "完了を整理", "완료 정리": "完了を整理",
"완료: 성공 {0} / 전체 {1}": "完了: 成功 {0} / 全体 {1}", "완료: 성공 {0} / 전체 {1}": "完了: 成功 {0} / 全体 {1}",
@ -538,6 +582,7 @@
"외부 브라우저": "外部ブラウザー", "외부 브라우저": "外部ブラウザー",
"외부 브라우저로 열기": "外部ブラウザーで開く", "외부 브라우저로 열기": "外部ブラウザーで開く",
"외부 프로그램으로 재생: {0} ← {1}개": "外部プログラムで再生: {0} ← {1}個", "외부 프로그램으로 재생: {0} ← {1}개": "外部プログラムで再生: {0} ← {1}個",
"왼쪽": "左",
"왼쪽 90°": "左 90°", "왼쪽 90°": "左 90°",
"왼쪽 손잡이로 순서 바꾸기 · 아이콘을 끌어 왼쪽 폴더나 목록의 폴더 위에 놓으면 그 폴더로 이동 · 오른쪽 클릭 메뉴": "左のハンドルで並べ替え・アイコンを左のフォルダーかリスト内のフォルダーにドラッグするとそのフォルダーへ移動・右クリックメニュー", "왼쪽 손잡이로 순서 바꾸기 · 아이콘을 끌어 왼쪽 폴더나 목록의 폴더 위에 놓으면 그 폴더로 이동 · 오른쪽 클릭 메뉴": "左のハンドルで並べ替え・アイコンを左のフォルダーかリスト内のフォルダーにドラッグするとそのフォルダーへ移動・右クリックメニュー",
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "リクエストが多すぎます。しばらくしてからもう一度お試しください。", "요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "リクエストが多すぎます。しばらくしてからもう一度お試しください。",
@ -561,6 +606,7 @@
"웹 페이지를 번역하지 못했습니다: {0}": "ウェブページを翻訳できませんでした: {0}", "웹 페이지를 번역하지 못했습니다: {0}": "ウェブページを翻訳できませんでした: {0}",
"위 폴더": "上のフォルダー", "위 폴더": "上のフォルダー",
"위로": "上へ", "위로": "上へ",
"위아래": "上下",
"음성": "音声", "음성": "音声",
"음성 {0}개": "音声 {0}個", "음성 {0}개": "音声 {0}個",
"음성 선택": "音声を選択", "음성 선택": "音声を選択",
@ -584,12 +630,15 @@
"이 이동 버튼 지우기": "この移動ボタンを削除", "이 이동 버튼 지우기": "この移動ボタンを削除",
"이 파일에 맞는 자막 우선 (영상 해시)": "このファイルに合う字幕を優先(動画ハッシュ)", "이 파일에 맞는 자막 우선 (영상 해시)": "このファイルに合う字幕を優先(動画ハッシュ)",
"이 파일용": "このファイル用", "이 파일용": "このファイル用",
"이 파일을 열 수 있는 앱이 없습니다.": "このファイルを開けるアプリがありません。",
"이 페이지를 {0}(으)로 번역": "このページを{0}に翻訳", "이 페이지를 {0}(으)로 번역": "このページを{0}に翻訳",
"이 폴더 선택": "このフォルダーを選択", "이 폴더 선택": "このフォルダーを選択",
"이 폴더 전체": "このフォルダー全体", "이 폴더 전체": "このフォルダー全体",
"이 폴더에 동영상이 없습니다.": "このフォルダーに動画がありません。",
"이 폴더에 새 폴더": "このフォルダーに新しいフォルダー", "이 폴더에 새 폴더": "このフォルダーに新しいフォルダー",
"이 폴더에 현재 페이지 추가": "このフォルダーに現在のページを追加", "이 폴더에 현재 페이지 추가": "このフォルダーに現在のページを追加",
"이 폴더에는 고를 파일이 없습니다": "このフォルダーには選べるファイルがありません", "이 폴더에는 고를 파일이 없습니다": "このフォルダーには選べるファイルがありません",
"이 폴더의 동영상 재생": "このフォルダーの動画を再生",
"이동": "移動", "이동": "移動",
"이동 ({0})": "移動({0})", "이동 ({0})": "移動({0})",
"이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)": "移動ボタン(詳細の右下、上に 1つずつ)", "이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)": "移動ボタン(詳細の右下、上に 1つずつ)",
@ -606,6 +655,9 @@
"이동할 폴더를 만들 수 없습니다: {0} ({1})": "移動フォルダーを作成できません: {0}({1})", "이동할 폴더를 만들 수 없습니다: {0} ({1})": "移動フォルダーを作成できません: {0}({1})",
"이름": "名前", "이름": "名前",
"이름 바꾸기 · 이동": "名前の変更・移動", "이름 바꾸기 · 이동": "名前の変更・移動",
"이름 변경": "名前の変更",
"이름을 바꿀 수 없습니다: {0}": "名前を変更できません: {0}",
"이름을 바꿀 항목 하나를 고르세요.": "名前を変更する項目を 1 つ選んでください。",
"이미 동영상 목록에 있습니다 ({0}개).": "既に動画リストにあります({0}個)。", "이미 동영상 목록에 있습니다 ({0}개).": "既に動画リストにあります({0}個)。",
"이미 목록에 있는 동영상입니다 ({0}개).": "既にリストにある動画です({0}個)。", "이미 목록에 있는 동영상입니다 ({0}개).": "既にリストにある動画です({0}個)。",
"이미 받는 중이거나 받을 수 없는 주소입니다.": "既にダウンロード中か、ダウンロードできない URL です。", "이미 받는 중이거나 받을 수 없는 주소입니다.": "既にダウンロード中か、ダウンロードできない URL です。",
@ -626,6 +678,7 @@
"일반": "一般", "일반": "一般",
"일본어": "日本語", "일본어": "日本語",
"일시정지": "一時停止", "일시정지": "一時停止",
"읽을 수 없음": "読み取れません",
"임시 파일 · 남은 조각 정리": "一時ファイル・残った断片を整理", "임시 파일 · 남은 조각 정리": "一時ファイル・残った断片を整理",
"자동": "自動", "자동": "自動",
"자동 감지": "自動検出", "자동 감지": "自動検出",
@ -708,12 +761,14 @@
"전체 화면": "全画面", "전체 화면": "全画面",
"전체 화면 (F)": "全画面(F)", "전체 화면 (F)": "全画面(F)",
"점검": "点検", "점검": "点検",
"정렬 기준": "並べ替え",
"정리": "整理", "정리": "整理",
"정리 ({0})": "整理({0})", "정리 ({0})": "整理({0})",
"정리…": "整理…", "정리…": "整理…",
"정리할 파일을 찾는 중…": "整理するファイルを探しています…", "정리할 파일을 찾는 중…": "整理するファイルを探しています…",
"정리할 파일이 없었습니다.": "整理するファイルはありませんでした。", "정리할 파일이 없었습니다.": "整理するファイルはありませんでした。",
"정리했습니다: 파일 {0}개 · {1}": "整理しました: ファイル {0}個・{1}", "정리했습니다: 파일 {0}개 · {1}": "整理しました: ファイル {0}個・{1}",
"정보": "情報",
"정사각 1:1": "正方形 1:1", "정사각 1:1": "正方形 1:1",
"정지": "停止", "정지": "停止",
"정하지 않음: MKV 목록에서 [이동] 을 처음 누를 때 고릅니다": "未設定: MKV リストで[移動]を初めて押したときに選びます", "정하지 않음: MKV 목록에서 [이동] 을 처음 누를 때 고릅니다": "未設定: MKV リストで[移動]を初めて押したときに選びます",
@ -725,6 +780,8 @@
"종료 중입니다.": "終了しています。", "종료 중입니다.": "終了しています。",
"종료 창 닫힘": "終了ウィンドウを閉じました", "종료 창 닫힘": "終了ウィンドウを閉じました",
"종료할까요?": "終了しますか?", "종료할까요?": "終了しますか?",
"종류 (확장자)": "種類 (拡張子)",
"좌우": "左右",
"주소": "アドレス", "주소": "アドレス",
"주소 입력 또는 검색 (Ctrl+L)": "アドレスを入力または検索(Ctrl+L)", "주소 입력 또는 검색 (Ctrl+L)": "アドレスを入力または検索(Ctrl+L)",
"준비 중": "準備中", "준비 중": "準備中",
@ -755,17 +812,25 @@
"지금: {0}": "現在: {0}", "지금: {0}": "現在: {0}",
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "前回の実行が正常に終了しませんでした(最終確認 {0}:{1})。作業記録に残しました。", "지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "前回の実行が正常に終了しませんでした(最終確認 {0}:{1})。作業記録に残しました。",
"지난 작업 기록": "過去の作業記録", "지난 작업 기록": "過去の作業記録",
"지우지 못했습니다: {0}": "削除できませんでした: {0}",
"지울 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "削除する項目を選んでください (タップ、または右の丸でマーク)。",
"지원하지 않는 protobuf 형식 (wire type {0}, 위치 {1})": "対応していない protobuf 形式(wire type {0}、位置 {1})", "지원하지 않는 protobuf 형식 (wire type {0}, 위치 {1})": "対応していない protobuf 形式(wire type {0}、位置 {1})",
"지원하지 않는 문자셋: {0}": "対応していない文字コード: {0}", "지원하지 않는 문자셋: {0}": "対応していない文字コード: {0}",
"직접 입력": "直接入力", "직접 입력": "直接入力",
"직접 입력…": "直接入力…", "직접 입력…": "直接入力…",
"진행 중인 작업 없음": "進行中の作業なし", "진행 중인 작업 없음": "進行中の作業なし",
"진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?": "進行中の作業があります: {0}\n中止して終了しますか?", "진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?": "進行中の作業があります: {0}\n中止して終了しますか?",
"창": "ペイン",
"창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "ウィンドウ・タスクバー・トレイとアプリ内左上のボタンのアイコンです(実行ファイル・デスクトップのショートカットのアイコンはそのまま)", "창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "ウィンドウ・タスクバー・トレイとアプリ内左上のボタンのアイコンです(実行ファイル・デスクトップのショートカットのアイコンはそのまま)",
"창 배치": "ペインの配置",
"창 배치 · 버튼 구성": "ペインの配置 · ボタン構成",
"창 보이기 / 숨기기 단축키": "ウィンドウ表示 / 非表示のショートカット", "창 보이기 / 숨기기 단축키": "ウィンドウ表示 / 非表示のショートカット",
"창 숨김 → 프로세스 종료": "ウィンドウ非表示 → プロセス終了", "창 숨김 → 프로세스 종료": "ウィンドウ非表示 → プロセス終了",
"창 종료": "ウィンドウを閉じる", "창 종료": "ウィンドウを閉じる",
"찾기": "検索",
"찾기: {0}": "検索: {0}",
"찾는 즐겨찾기가 없습니다.": "該当するブックマークがありません。", "찾는 즐겨찾기가 없습니다.": "該当するブックマークがありません。",
"찾은 것이 없습니다.": "見つかりませんでした。",
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "字幕が見つかりませんでした。タイトルを英語の原題に変えるか、言語を追加してみてください。", "찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "字幕が見つかりませんでした。タイトルを英語の原題に変えるか、言語を追加してみてください。",
"채도": "彩度", "채도": "彩度",
"채우기 (가운데 잘라냄)": "フィル(中央を切り抜き)", "채우기 (가운데 잘라냄)": "フィル(中央を切り抜き)",
@ -784,6 +849,7 @@
"추가 ({0})": "追加({0})", "추가 ({0})": "追加({0})",
"추가 취소": "追加をキャンセル", "추가 취소": "追加をキャンセル",
"추가: {0} (내장 자막 {1}개, 같은 폴더 자막 {2}개)": "追加: {0}(内蔵字幕 {1}個、同じフォルダーの字幕 {2}個)", "추가: {0} (내장 자막 {1}개, 같은 폴더 자막 {2}개)": "追加: {0}(内蔵字幕 {1}個、同じフォルダーの字幕 {2}個)",
"추가할 동영상이 없습니다.": "追加する動画がありません。",
"출력: {0}": "出力: {0}", "출력: {0}": "出力: {0}",
"취소": "キャンセル", "취소": "キャンセル",
"취소 · 실패한 다운로드가 남긴 .part · .ytdl · .aria2 · 합치기 전 조각 파일 (jj_yt-dlp · jj_aria2)": "キャンセル・失敗したダウンロードが残した .part・.ytdl・.aria2・結合前の断片(jj_yt-dlp・jj_aria2)", "취소 · 실패한 다운로드가 남긴 .part · .ytdl · .aria2 · 합치기 전 조각 파일 (jj_yt-dlp · jj_aria2)": "キャンセル・失敗したダウンロードが残した .part・.ytdl・.aria2・結合前の断片(jj_yt-dlp・jj_aria2)",
@ -791,6 +857,7 @@
"취소했습니다.": "キャンセルしました。", "취소했습니다.": "キャンセルしました。",
"켜져 있는 창에서 (기본)": "開いているウィンドウで(既定)", "켜져 있는 창에서 (기본)": "開いているウィンドウで(既定)",
"코덱": "コーデック", "코덱": "コーデック",
"크기": "サイズ",
"크메르어": "クメール語", "크메르어": "クメール語",
"타갈로그어": "タガログ語", "타갈로그어": "タガログ語",
"타밀어": "タミル語", "타밀어": "タミル語",
@ -804,9 +871,12 @@
"트랙 {0}": "トラック {0}", "트랙 {0}": "トラック {0}",
"트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "トレイアイコンを押すか、下のショートカットで再表示します", "트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "トレイアイコンを押すか、下のショートカットで再表示します",
"트레이 정리": "トレイの後片付け", "트레이 정리": "トレイの後片付け",
"파일 관리자에서 보기": "ファイルマネージャーで表示",
"파일 앱의 \"다음으로 열기\" · 공유에서 JJ_MKVMaker 를 골랐을 때": "ファイルアプリの「アプリで開く」・共有で JJ_MKVMaker を選んだとき", "파일 앱의 \"다음으로 열기\" · 공유에서 JJ_MKVMaker 를 골랐을 때": "ファイルアプリの「アプリで開く」・共有で JJ_MKVMaker を選んだとき",
"파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 \"동영상 추가\" 로 고르세요.": "ファイルの場所がわからないため再生のみ行います。MKV を作成するには「動画を追加」で選んでください。", "파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 \"동영상 추가\" 로 고르세요.": "ファイルの場所がわからないため再生のみ行います。MKV を作成するには「動画を追加」で選んでください。",
"파일 이름": "ファイル名", "파일 이름": "ファイル名",
"파일 탐색기": "ファイルエクスプローラー",
"파일 탐색기 (지금 여기)": "ファイルエクスプローラー (現在地)",
"파일 확인 중…": "ファイルを確認中…", "파일 확인 중…": "ファイルを確認中…",
"파일을 분석할 수 없습니다: {0}": "ファイルを分析できません: {0}", "파일을 분석할 수 없습니다: {0}": "ファイルを分析できません: {0}",
"팝업 끄기": "ポップアップを閉じる", "팝업 끄기": "ポップアップを閉じる",
@ -833,8 +903,10 @@
"폴더가 아닙니다: {0}": "フォルダーではありません: {0}", "폴더가 아닙니다: {0}": "フォルダーではありません: {0}",
"폴더를 만들 수 없습니다: {0}": "フォルダーを作成できません: {0}", "폴더를 만들 수 없습니다: {0}": "フォルダーを作成できません: {0}",
"폴란드어": "ポーランド語", "폴란드어": "ポーランド語",
"표시 (여러 개 고르기)": "マーク (複数選択)",
"표시 이름": "表示名", "표시 이름": "表示名",
"표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다": "表示名はその場で変更できます。動画の行を 3回押すと最初のボタンのフォルダーに移動します", "표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다": "表示名はその場で変更できます。動画の行を 3回押すと最初のボタンのフォルダーに移動します",
"표시 지우기": "マークを解除",
"표준": "標準", "표준": "標準",
"푸는 중": "展開中", "푸는 중": "展開中",
"프랑스어": "フランス語", "프랑스어": "フランス語",
@ -854,6 +926,7 @@
"필수 프로그램 점검": "必須プログラムの点検", "필수 프로그램 점검": "必須プログラムの点検",
"필수 프로그램이 모두 설치되어 있습니다.": "必須プログラムはすべてインストールされています。", "필수 프로그램이 모두 설치되어 있습니다.": "必須プログラムはすべてインストールされています。",
"하위 폴더가 없습니다": "サブフォルダーがありません", "하위 폴더가 없습니다": "サブフォルダーがありません",
"한 창": "1 ペイン",
"한국어": "韓国語", "한국어": "韓国語",
"한글": "ハングル", "한글": "ハングル",
"해제": "解除", "해제": "解除",
@ -870,6 +943,7 @@
"화면": "画面", "화면": "画面",
"화면 · 색 보정": "画面・カラー補正", "화면 · 색 보정": "画面・カラー補正",
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "画面の文字をすべてこの端末で翻訳します。数分かかることがあります。", "화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "画面の文字をすべてこの端末で翻訳します。数分かかることがあります。",
"화면 모양 따라": "画面の形に合わせる",
"화면 방향": "画面の向き", "화면 방향": "画面の向き",
"화면 비율": "画面比率", "화면 비율": "画面比率",
"화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도": "画面比率(横・縦)・回転・明るさ・コントラスト・彩度・色温度", "화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도": "画面比率(横・縦)・回転・明るさ・コントラスト・彩度・色温度",

View File

@ -23,7 +23,9 @@
" 지금 설치됨 ({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}\" 찾기 · {1}개": "搜索\"{0}\" · {1}个",
"\"동영상 추가\" 로 파일을 선택하세요": "请用“添加视频”选择文件", "\"동영상 추가\" 로 파일을 선택하세요": "请用“添加视频”选择文件",
"\"외부 브라우저로 열기\" 에 쓸 브라우저 (이 PC 에 설치된 것)": "“在外部浏览器中打开”使用的浏览器(本机已安装)", "\"외부 브라우저로 열기\" 에 쓸 브라우저 (이 PC 에 설치된 것)": "“在外部浏览器中打开”使用的浏览器(本机已安装)",
"# Netscape HTTP Cookie File\n# JJ_MKVMaker 앱 안 브라우저에서 내보냄\n\n": "# Netscape HTTP Cookie File\n# 从 JJ_MKVMaker 内置浏览器导出\n\n", "# Netscape HTTP Cookie File\n# JJ_MKVMaker 앱 안 브라우저에서 내보냄\n\n": "# Netscape HTTP Cookie File\n# 从 JJ_MKVMaker 内置浏览器导出\n\n",
@ -96,6 +98,8 @@
"MKV 만들기{0}{1}": "制作 MKV{0}{1}", "MKV 만들기{0}{1}": "制作 MKV{0}{1}",
"MKV 만들기에 적용됩니다 (영상을 다시 인코딩)": "应用于制作 MKV(视频将重新编码)", "MKV 만들기에 적용됩니다 (영상을 다시 인코딩)": "应用于制作 MKV(视频将重新编码)",
"MKV 만들기의 동영상 목록에 {0}개를 추가했습니다.{1}": "已向制作 MKV 的视频列表添加 {0} 个。{1}", "MKV 만들기의 동영상 목록에 {0}개를 추가했습니다.{1}": "已向制作 MKV 的视频列表添加 {0} 个。{1}",
"MKV 목록에 {0}개를 추가했습니다.": "已将 {0} 个添加到 MKV 列表。",
"MKV 목록에 추가": "添加到 MKV 列表",
"MKV 완성": "MKV 完成", "MKV 완성": "MKV 完成",
"MKV 화면": "MKV 界面", "MKV 화면": "MKV 界面",
"MKV 화면 (기본)": "MKV 界面(默认)", "MKV 화면 (기본)": "MKV 界面(默认)",
@ -170,6 +174,10 @@
"{0}개 · {1}": "{0} 个 · {1}", "{0}개 · {1}": "{0} 个 · {1}",
"{0}개 · 주소 {1}개": "{0} 个 · 地址 {1} 个", "{0}개 · 주소 {1}개": "{0} 个 · 地址 {1} 个",
"{0}개 선택": "已选 {0} 个", "{0}개 선택": "已选 {0} 个",
"{0}개 표시함": "已标记 {0} 个",
"{0}개 항목을 {1}": "{0} 个项目{1}",
"{0}개 항목을 다음 폴더로 {1}:\n{2}": "将 {0} 个项目到以下文件夹 — {1}\n{2}",
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "删除 {0} 个项目吗? 此操作无法撤销。",
"{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "已将 {0} 个导入“其他书签”。", "{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "已将 {0} 个导入“其他书签”。",
"{0}개를 옮겼습니다 → {1}": "已移动 {0} 个 → {1}", "{0}개를 옮겼습니다 → {1}": "已移动 {0} 个 → {1}",
"{0}개를 옮겼습니다 → {1} ({2})": "已移动 {0} 个 → {1}({2})", "{0}개를 옮겼습니다 → {1} ({2})": "已移动 {0} 个 → {1}({2})",
@ -178,6 +186,7 @@
"{0}줄": "{0} 行", "{0}줄": "{0} 行",
"{0}초": "{0} 秒", "{0}초": "{0} 秒",
"· 백그라운드로: 창만 숨기고 다운로드 · 변환을 계속합니다 ({0} 또는 트레이 아이콘으로 다시 열기)\n· 모두 종료: 다운로드 · 변환을 멈추고 프로그램을 끝냅니다": "· 转到后台: 仅隐藏窗口,继续下载 · 转换(用 {0} 或托盘图标重新打开)\n· 全部退出: 停止下载 · 转换并关闭程序", "· 백그라운드로: 창만 숨기고 다운로드 · 변환을 계속합니다 ({0} 또는 트레이 아이콘으로 다시 열기)\n· 모두 종료: 다운로드 · 변환을 멈추고 프로그램을 끝냅니다": "· 转到后台: 仅隐藏窗口,继续下载 · 转换(用 {0} 或托盘图标重新打开)\n· 全部退出: 停止下载 · 转换并关闭程序",
"… 외 {0}개": "… 另外 {0} 个",
"※ 다른 작업 중이면 대기열에 넣고 차례대로 합니다. 번역하는 동안 브라우저를 써도 됩니다.": "※ 如有其他任务正在进行,将加入队列依次处理。翻译期间可以使用浏览器。", "※ 다른 작업 중이면 대기열에 넣고 차례대로 합니다. 번역하는 동안 브라우저를 써도 됩니다.": "※ 如有其他任务正在进行,将加入队列依次处理。翻译期间可以使用浏览器。",
"※ 시간이 오래 걸립니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)": "※ 耗时较长(1 小时视频 ≈ 语音识别 15~30 分钟 + 每种语言翻译 10~20 分钟)", "※ 시간이 오래 걸립니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)": "※ 耗时较长(1 小时视频 ≈ 语音识别 15~30 分钟 + 每种语言翻译 10~20 分钟)",
"− 차갑게 · + 따뜻하게": "− 偏冷 · + 偏暖", "− 차갑게 · + 따뜻하게": "− 偏冷 · + 偏暖",
@ -199,6 +208,7 @@
"같은 시리즈 (번호만 다른 파일)": "同一系列(仅编号不同的文件)", "같은 시리즈 (번호만 다른 파일)": "同一系列(仅编号不同的文件)",
"같은 폴더의 모든 동영상": "同一文件夹中的所有视频", "같은 폴더의 모든 동영상": "同一文件夹中的所有视频",
"같은 폴더의 자막은 자동으로 추가됩니다": "同一文件夹中的字幕会自动添加", "같은 폴더의 자막은 자동으로 추가됩니다": "同一文件夹中的字幕会自动添加",
"거꾸로": "倒序",
"건너뜀 (출력 폴더의 파일): {0}": "已跳过(输出文件夹中的文件): {0}", "건너뜀 (출력 폴더의 파일): {0}": "已跳过(输出文件夹中的文件): {0}",
"검색": "搜索", "검색": "搜索",
"검색 지우기": "清除搜索", "검색 지우기": "清除搜索",
@ -219,7 +229,9 @@
"글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字 · 按钮大小。可随时用顶部栏的 − · + 调整,点击中间的数字恢复为此大小(当前 {0}%)", "글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字 · 按钮大小。可随时用顶部栏的 − · + 调整,点击中间的数字恢复为此大小(当前 {0}%)",
"기계 번역": "机器翻译", "기계 번역": "机器翻译",
"기기": "设备", "기기": "设备",
"기능 버튼 줄": "功能按钮栏",
"기본": "默认", "기본": "默认",
"기본 앱으로 열기": "用默认应用打开",
"기본 연결 프로그램": "默认打开方式", "기본 연결 프로그램": "默认打开方式",
"기본 크기 ({0}%) 로 · 기본 크기는 환경 설정 > 화면 에서": "恢复默认大小({0}%)· 默认大小在 设置 > 显示 中设置", "기본 크기 ({0}%) 로 · 기본 크기는 환경 설정 > 화면 에서": "恢复默认大小({0}%)· 默认大小在 设置 > 显示 中设置",
"기본 프로그램": "默认程序", "기본 프로그램": "默认程序",
@ -235,9 +247,11 @@
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "关闭后不运行页面脚本 (广告 · 跟踪会减少,但 YouTube 等由脚本构建的网站将无法显示)。页面翻译 · 视频检测也会停止。", "끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "关闭后不运行页面脚本 (广告 · 跟踪会减少,但 YouTube 等由脚本构建的网站将无法显示)。页面翻译 · 视频检测也会停止。",
"끌어다 놓은 항목에 동영상이 없습니다.": "拖入的项目中没有视频。", "끌어다 놓은 항목에 동영상이 없습니다.": "拖入的项目中没有视频。",
"끝": "结束", "끝": "结束",
"끝 (오른쪽 · 아래)": "边缘 (右 · 下)",
"끝 = 현재": "结束 = 当前", "끝 = 현재": "结束 = 当前",
"끝 시간이 시작보다 빠른 줄: {0}{1}": "结束时间早于开始时间的行: {0}{1}", "끝 시간이 시작보다 빠른 줄: {0}{1}": "结束时间早于开始时间的行: {0}{1}",
"끝: {0}": "结束: {0}", "끝: {0}": "结束: {0}",
"끝나지 못했습니다: {0}": "未能完成: {0}",
"나가기": "退出", "나가기": "退出",
"나머지는 대기했다가 차례로 받습니다": "其余的等待并依次下载", "나머지는 대기했다가 차례로 받습니다": "其余的等待并依次下载",
"나중에": "以后再说", "나중에": "以后再说",
@ -250,6 +264,8 @@
"내려받다 끊긴 모델 (.part) · 변환하다 남은 파일 (.tmp)": "下载中断的模型(.part)· 转换残留(.tmp)", "내려받다 끊긴 모델 (.part) · 변환하다 남은 파일 (.tmp)": "下载中断的模型(.part)· 转换残留(.tmp)",
"내려받은 Chrome 엔진 지우기": "删除已下载的 Chrome 引擎", "내려받은 Chrome 엔진 지우기": "删除已下载的 Chrome 引擎",
"내보냈습니다: {0}": "已导出: {0}", "내보냈습니다: {0}": "已导出: {0}",
"내역": "历史",
"내역 (최근 폴더)": "历史 (最近的文件夹)",
"내용": "内容", "내용": "内容",
"내용 편집 · 문자셋 변환": "编辑内容 · 转换字符集", "내용 편집 · 문자셋 변환": "编辑内容 · 转换字符集",
"내장": "内嵌", "내장": "内嵌",
@ -262,6 +278,7 @@
"내장 Chrome 엔진을 지웠습니다.": "已删除内置 Chrome 引擎。", "내장 Chrome 엔진을 지웠습니다.": "已删除内置 Chrome 引擎。",
"내장 저장소": "内部存储", "내장 저장소": "内部存储",
"내장 플레이어": "内置播放器", "내장 플레이어": "内置播放器",
"내장 플레이어로 재생": "用内置播放器播放",
"네덜란드어": "荷兰语", "네덜란드어": "荷兰语",
"네팔어": "尼泊尔语", "네팔어": "尼泊尔语",
"노랑": "黄色", "노랑": "黄色",
@ -272,10 +289,14 @@
"다른 브라우저의 즐겨찾기 가져오기": "导入其他浏览器的书签", "다른 브라우저의 즐겨찾기 가져오기": "导入其他浏览器的书签",
"다른 앱에서 동영상을 열 때": "从其他应用打开视频时", "다른 앱에서 동영상을 열 때": "从其他应用打开视频时",
"다른 앱에서 열기: {0}": "从其他应用打开: {0}", "다른 앱에서 열기: {0}": "从其他应用打开: {0}",
"다른 앱으로 열기": "用其他应用打开",
"다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. 주소창 옆 번역 버튼으로 원문 · 번역을 바꿀 수 있습니다.": "将其他语言的页面翻译为界面语言 ({0})。页面文字会发送到 Google 翻译。可用地址栏旁的翻译按钮切换原文 · 译文。", "다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. 주소창 옆 번역 버튼으로 원문 · 번역을 바꿀 수 있습니다.": "将其他语言的页面翻译为界面语言 ({0})。页面文字会发送到 Google 翻译。可用地址栏旁的翻译按钮切换原文 · 译文。",
"다른 언어로 번역 (AI · 이 {0}에서)": "翻译为其他语言(AI · 在此{0}上)", "다른 언어로 번역 (AI · 이 {0}에서)": "翻译为其他语言(AI · 在此{0}上)",
"다른 이름으로 저장": "另存为", "다른 이름으로 저장": "另存为",
"다른 이름으로 저장: {0} ({1})": "另存为: {0}({1})", "다른 이름으로 저장: {0} ({1})": "另存为: {0}({1})",
"다른 창에서 열기": "在另一窗格中打开",
"다른 창으로 복사": "复制到另一窗格",
"다른 창으로 이동": "移动到另一窗格",
"다른 폴더로 이동": "移动到其他文件夹", "다른 폴더로 이동": "移动到其他文件夹",
"다시 만들기 대기로": "改回等待重新制作", "다시 만들기 대기로": "改回等待重新制作",
"다시 시작": "重新启动", "다시 시작": "重新启动",
@ -322,6 +343,7 @@
"동시 MKV 변환 수": "同时转换 MKV 数", "동시 MKV 변환 수": "同时转换 MKV 数",
"동시 다운로드 수": "同时下载数", "동시 다운로드 수": "同时下载数",
"동시에 처리할 개수 (0 = 무한)": "同时处理的数量(0 = 无限制)", "동시에 처리할 개수 (0 = 무한)": "同时处理的数量(0 = 无限制)",
"동영상": "视频",
"동영상 {0}개 · 전체 선택": "视频 {0} 个 · 全选", "동영상 {0}개 · 전체 선택": "视频 {0} 个 · 全选",
"동영상 목록 · 창 위치 저장": "保存视频列表 · 窗口位置", "동영상 목록 · 창 위치 저장": "保存视频列表 · 窗口位置",
"동영상 목록에 추가: {0}": "已加入视频列表: {0}", "동영상 목록에 추가: {0}": "已加入视频列表: {0}",
@ -337,6 +359,9 @@
"동영상을 끌어다 놓으면 목록에 추가됩니다": "拖放视频即可加入列表", "동영상을 끌어다 놓으면 목록에 추가됩니다": "拖放视频即可加入列表",
"동영상이 있는 폴더 아래 jj_mkv (기본)": "视频所在文件夹下的 jj_mkv(默认)", "동영상이 있는 폴더 아래 jj_mkv (기본)": "视频所在文件夹下的 jj_mkv(默认)",
"되살리기": "恢复", "되살리기": "恢复",
"두 창": "双窗格",
"두 창 배치": "双窗格排列",
"두 창 사이": "窗格之间",
"뒤로": "后退", "뒤로": "后退",
"등록": "注册", "등록": "注册",
"등록 중 일부가 실패했습니다.": "部分注册失败。", "등록 중 일부가 실패했습니다.": "部分注册失败。",
@ -373,6 +398,7 @@
"미지정": "未指定", "미지정": "未指定",
"바꾸면 바로 저장됩니다": "更改后立即保存", "바꾸면 바로 저장됩니다": "更改后立即保存",
"바꾼 것이 없습니다 (원본 그대로)": "未做更改(保持原样)", "바꾼 것이 없습니다 (원본 그대로)": "未做更改(保持原样)",
"바뀐 날": "修改时间",
"바뀐 내용": "更改内容", "바뀐 내용": "更改内容",
"바로 재생 (기본)": "立即播放(默认)", "바로 재생 (기본)": "立即播放(默认)",
"바로 전 작업: {0}": "上一个任务: {0}", "바로 전 작업: {0}": "上一个任务: {0}",
@ -393,6 +419,7 @@
"백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)": "后台: 仅隐藏窗口,下载 · 转换继续进行。用 {0} 或托盘图标重新打开。\n要完全退出: 右键托盘图标 > 退出(无论何种设置都会退出)", "백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)": "后台: 仅隐藏窗口,下载 · 转换继续进行。用 {0} 或托盘图标重新打开。\n要完全退出: 右键托盘图标 > 退出(无论何种设置都会退出)",
"백그라운드로": "转到后台", "백그라운드로": "转到后台",
"백그라운드로 계속 (기본)": "在后台继续(默认)", "백그라운드로 계속 (기본)": "在后台继续(默认)",
"버튼 구성": "按钮配置",
"번역 모델 내려받는 중 {0}%": "正在下载翻译模型 {0}%", "번역 모델 내려받는 중 {0}%": "正在下载翻译模型 {0}%",
"번역 모델 내려받는 중: {0}": "正在下载翻译模型: {0}", "번역 모델 내려받는 중: {0}": "正在下载翻译模型: {0}",
"번역 모델 불러오는 중": "正在加载翻译模型", "번역 모델 불러오는 중": "正在加载翻译模型",
@ -407,7 +434,14 @@
"변경 내용을 저장하지 않고 나갈까요?": "不保存更改就离开吗?", "변경 내용을 저장하지 않고 나갈까요?": "不保存更改就离开吗?",
"변환": "转换", "변환": "转换",
"보기 {0}초 · Ctrl+휠 확대/축소": "显示 {0} 秒 · Ctrl+滚轮缩放", "보기 {0}초 · Ctrl+휠 확대/축소": "显示 {0} 秒 · Ctrl+滚轮缩放",
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "选择要显示的按钮,拖动以调整顺序。",
"복사": "复制",
"복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).": "复制 · 移动需在双窗格中使用 (窗格布局 > 双窗格)。",
"복사 · 이동할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "请选择要复制 · 移动的项目 (点按,或用右侧圆圈标记)。",
"복사(Ctrl+C)한 주소 자동 다운로드": "自动下载复制(Ctrl+C)的地址", "복사(Ctrl+C)한 주소 자동 다운로드": "自动下载复制(Ctrl+C)的地址",
"복사하는 중": "正在复制",
"복사할까요?": "复制吗?",
"복사했습니다.": "已复制。",
"볼 수 없는 영상입니다 (삭제 · 지역 제한).": "该视频无法观看(已删除 · 地区限制)。", "볼 수 없는 영상입니다 (삭제 · 지역 제한).": "该视频无法观看(已删除 · 地区限制)。",
"분석 실패: {0} - {1}": "分析失败: {0} - {1}", "분석 실패: {0} - {1}": "分析失败: {0} - {1}",
"브라우저 엔진 (앱 안)": "浏览器引擎(应用内)", "브라우저 엔진 (앱 안)": "浏览器引擎(应用内)",
@ -425,6 +459,7 @@
"삭제 (중지하고 받던 파일 삭제)": "删除(停止并删除正在下载的文件)", "삭제 (중지하고 받던 파일 삭제)": "删除(停止并删除正在下载的文件)",
"삭제 (출력에서 제외)": "删除(从输出中排除)", "삭제 (출력에서 제외)": "删除(从输出中排除)",
"삭제: {0}": "已删除: {0}", "삭제: {0}": "已删除: {0}",
"상위 폴더": "上级文件夹",
"새 버전 v{0}": "新版本 v{0}", "새 버전 v{0}": "新版本 v{0}",
"새 버전 확인: 지금 v{0} · 최신 v{1}": "检查新版本: 当前 v{0} · 最新 v{1}", "새 버전 확인: 지금 v{0} · 최신 v{1}": "检查新版本: 当前 v{0} · 最新 v{1}",
"새 버전을 확인할 수 없습니다: {0}": "无法检查新版本: {0}", "새 버전을 확인할 수 없습니다: {0}": "无法检查新版本: {0}",
@ -433,6 +468,7 @@
"새 폴더": "新建文件夹", "새 폴더": "新建文件夹",
"새 폴더 추가": "添加新文件夹", "새 폴더 추가": "添加新文件夹",
"새 폴더…": "新建文件夹…", "새 폴더…": "新建文件夹…",
"새로 고침": "刷新",
"새로고침 (F5)": "刷新(F5)", "새로고침 (F5)": "刷新(F5)",
"색 보정": "色彩校正", "색 보정": "色彩校正",
"색온도": "色温", "색온도": "色温",
@ -463,6 +499,11 @@
"세로 고정": "锁定竖屏", "세로 고정": "锁定竖屏",
"수정": "编辑", "수정": "编辑",
"수정 · 이동": "编辑 · 移动", "수정 · 이동": "编辑 · 移动",
"숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "即使隐藏,也可以在顶部 ⋮ 菜单中更改窗格布局 · 按钮配置。",
"숨김": "隐藏",
"숨은 항목 표시": "显示隐藏项目",
"숨은 항목을 감춥니다.": "已隐藏隐藏项目。",
"숨은 항목을 보입니다.": "已显示隐藏项目。",
"쉬는 중": "空闲", "쉬는 중": "空闲",
"스웨덴어": "瑞典语", "스웨덴어": "瑞典语",
"스페인어": "西班牙语", "스페인어": "西班牙语",
@ -530,6 +571,9 @@
"오른쪽 90°": "向右 90°", "오른쪽 90°": "向右 90°",
"오픈 소스 라이선스": "开源许可", "오픈 소스 라이선스": "开源许可",
"올린 이 {0}": "上传者 {0}", "올린 이 {0}": "上传者 {0}",
"옮겼습니다.": "已移动。",
"옮기는 중": "正在移动",
"옮길까요?": "移动吗?",
"완료": "完成", "완료": "完成",
"완료 정리": "清理已完成", "완료 정리": "清理已完成",
"완료: 성공 {0} / 전체 {1}": "完成: 成功 {0} / 共 {1}", "완료: 성공 {0} / 전체 {1}": "完成: 成功 {0} / 共 {1}",
@ -538,6 +582,7 @@
"외부 브라우저": "外部浏览器", "외부 브라우저": "外部浏览器",
"외부 브라우저로 열기": "在外部浏览器中打开", "외부 브라우저로 열기": "在外部浏览器中打开",
"외부 프로그램으로 재생: {0} ← {1}개": "用外部程序播放: {0} ← {1} 个", "외부 프로그램으로 재생: {0} ← {1}개": "用外部程序播放: {0} ← {1} 个",
"왼쪽": "左侧",
"왼쪽 90°": "向左 90°", "왼쪽 90°": "向左 90°",
"왼쪽 손잡이로 순서 바꾸기 · 아이콘을 끌어 왼쪽 폴더나 목록의 폴더 위에 놓으면 그 폴더로 이동 · 오른쪽 클릭 메뉴": "用左侧手柄调整顺序 · 将图标拖到左侧文件夹或列表中的文件夹即可移入 · 右键菜单", "왼쪽 손잡이로 순서 바꾸기 · 아이콘을 끌어 왼쪽 폴더나 목록의 폴더 위에 놓으면 그 폴더로 이동 · 오른쪽 클릭 메뉴": "用左侧手柄调整顺序 · 将图标拖到左侧文件夹或列表中的文件夹即可移入 · 右键菜单",
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "请求过多。请稍后再试。", "요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "请求过多。请稍后再试。",
@ -561,6 +606,7 @@
"웹 페이지를 번역하지 못했습니다: {0}": "无法翻译网页: {0}", "웹 페이지를 번역하지 못했습니다: {0}": "无法翻译网页: {0}",
"위 폴더": "上级文件夹", "위 폴더": "上级文件夹",
"위로": "向上", "위로": "向上",
"위아래": "上下",
"음성": "音频", "음성": "音频",
"음성 {0}개": "音频 {0} 个", "음성 {0}개": "音频 {0} 个",
"음성 선택": "选择音轨", "음성 선택": "选择音轨",
@ -584,12 +630,15 @@
"이 이동 버튼 지우기": "删除此移动按钮", "이 이동 버튼 지우기": "删除此移动按钮",
"이 파일에 맞는 자막 우선 (영상 해시)": "优先匹配此文件的字幕(视频哈希)", "이 파일에 맞는 자막 우선 (영상 해시)": "优先匹配此文件的字幕(视频哈希)",
"이 파일용": "适用于此文件", "이 파일용": "适用于此文件",
"이 파일을 열 수 있는 앱이 없습니다.": "没有可以打开此文件的应用。",
"이 페이지를 {0}(으)로 번역": "将此页面翻译为{0}", "이 페이지를 {0}(으)로 번역": "将此页面翻译为{0}",
"이 폴더 선택": "选择此文件夹", "이 폴더 선택": "选择此文件夹",
"이 폴더 전체": "整个文件夹", "이 폴더 전체": "整个文件夹",
"이 폴더에 동영상이 없습니다.": "此文件夹中没有视频。",
"이 폴더에 새 폴더": "在此文件夹中新建文件夹", "이 폴더에 새 폴더": "在此文件夹中新建文件夹",
"이 폴더에 현재 페이지 추가": "将当前页面添加到此文件夹", "이 폴더에 현재 페이지 추가": "将当前页面添加到此文件夹",
"이 폴더에는 고를 파일이 없습니다": "此文件夹中没有可选择的文件", "이 폴더에는 고를 파일이 없습니다": "此文件夹中没有可选择的文件",
"이 폴더의 동영상 재생": "播放此文件夹中的视频",
"이동": "移动", "이동": "移动",
"이동 ({0})": "移动({0})", "이동 ({0})": "移动({0})",
"이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)": "移动按钮(详细信息右下角,向上依次排列)", "이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)": "移动按钮(详细信息右下角,向上依次排列)",
@ -606,6 +655,9 @@
"이동할 폴더를 만들 수 없습니다: {0} ({1})": "无法创建移动目标文件夹: {0}({1})", "이동할 폴더를 만들 수 없습니다: {0} ({1})": "无法创建移动目标文件夹: {0}({1})",
"이름": "名称", "이름": "名称",
"이름 바꾸기 · 이동": "重命名 · 移动", "이름 바꾸기 · 이동": "重命名 · 移动",
"이름 변경": "重命名",
"이름을 바꿀 수 없습니다: {0}": "无法重命名: {0}",
"이름을 바꿀 항목 하나를 고르세요.": "请选择一个要重命名的项目。",
"이미 동영상 목록에 있습니다 ({0}개).": "已在视频列表中({0} 个)。", "이미 동영상 목록에 있습니다 ({0}개).": "已在视频列表中({0} 个)。",
"이미 목록에 있는 동영상입니다 ({0}개).": "这些视频已在列表中({0} 个)。", "이미 목록에 있는 동영상입니다 ({0}개).": "这些视频已在列表中({0} 个)。",
"이미 받는 중이거나 받을 수 없는 주소입니다.": "已在下载中,或该地址无法下载。", "이미 받는 중이거나 받을 수 없는 주소입니다.": "已在下载中,或该地址无法下载。",
@ -626,6 +678,7 @@
"일반": "常规", "일반": "常规",
"일본어": "日语", "일본어": "日语",
"일시정지": "暂停", "일시정지": "暂停",
"읽을 수 없음": "无法读取",
"임시 파일 · 남은 조각 정리": "清理临时文件 · 残留碎片", "임시 파일 · 남은 조각 정리": "清理临时文件 · 残留碎片",
"자동": "自动", "자동": "自动",
"자동 감지": "自动检测", "자동 감지": "自动检测",
@ -708,12 +761,14 @@
"전체 화면": "全屏", "전체 화면": "全屏",
"전체 화면 (F)": "全屏(F)", "전체 화면 (F)": "全屏(F)",
"점검": "检查", "점검": "检查",
"정렬 기준": "排序方式",
"정리": "清理", "정리": "清理",
"정리 ({0})": "清理({0})", "정리 ({0})": "清理({0})",
"정리…": "清理…", "정리…": "清理…",
"정리할 파일을 찾는 중…": "正在查找可清理的文件…", "정리할 파일을 찾는 중…": "正在查找可清理的文件…",
"정리할 파일이 없었습니다.": "没有可清理的文件。", "정리할 파일이 없었습니다.": "没有可清理的文件。",
"정리했습니다: 파일 {0}개 · {1}": "已清理: 文件 {0} 个 · {1}", "정리했습니다: 파일 {0}개 · {1}": "已清理: 文件 {0} 个 · {1}",
"정보": "信息",
"정사각 1:1": "正方形 1:1", "정사각 1:1": "正方形 1:1",
"정지": "停止", "정지": "停止",
"정하지 않음: MKV 목록에서 [이동] 을 처음 누를 때 고릅니다": "未设置: 首次在 MKV 列表中点击[移动]时选择", "정하지 않음: MKV 목록에서 [이동] 을 처음 누를 때 고릅니다": "未设置: 首次在 MKV 列表中点击[移动]时选择",
@ -725,6 +780,8 @@
"종료 중입니다.": "正在退出。", "종료 중입니다.": "正在退出。",
"종료 창 닫힘": "退出窗口已关闭", "종료 창 닫힘": "退出窗口已关闭",
"종료할까요?": "要退出吗?", "종료할까요?": "要退出吗?",
"종류 (확장자)": "类型 (扩展名)",
"좌우": "左右",
"주소": "地址", "주소": "地址",
"주소 입력 또는 검색 (Ctrl+L)": "输入地址或搜索(Ctrl+L)", "주소 입력 또는 검색 (Ctrl+L)": "输入地址或搜索(Ctrl+L)",
"준비 중": "准备中", "준비 중": "准备中",
@ -755,17 +812,25 @@
"지금: {0}": "当前: {0}", "지금: {0}": "当前: {0}",
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "上次运行未正常结束(最后检查 {0}:{1})。已记录在任务记录中。", "지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "上次运行未正常结束(最后检查 {0}:{1})。已记录在任务记录中。",
"지난 작업 기록": "旧任务记录", "지난 작업 기록": "旧任务记录",
"지우지 못했습니다: {0}": "无法删除: {0}",
"지울 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "请选择要删除的项目 (点按,或用右侧圆圈标记)。",
"지원하지 않는 protobuf 형식 (wire type {0}, 위치 {1})": "不支持的 protobuf 格式(wire type {0},位置 {1})", "지원하지 않는 protobuf 형식 (wire type {0}, 위치 {1})": "不支持的 protobuf 格式(wire type {0},位置 {1})",
"지원하지 않는 문자셋: {0}": "不支持的字符集: {0}", "지원하지 않는 문자셋: {0}": "不支持的字符集: {0}",
"직접 입력": "手动输入", "직접 입력": "手动输入",
"직접 입력…": "手动输入…", "직접 입력…": "手动输入…",
"진행 중인 작업 없음": "没有进行中的任务", "진행 중인 작업 없음": "没有进行中的任务",
"진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?": "有正在进行的任务: {0}\n要停止并退出吗?", "진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?": "有正在进行的任务: {0}\n要停止并退出吗?",
"창": "窗格",
"창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "窗口、任务栏、托盘以及应用内左上角按钮的图标(程序文件和桌面快捷方式的图标不变)", "창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "窗口、任务栏、托盘以及应用内左上角按钮的图标(程序文件和桌面快捷方式的图标不变)",
"창 배치": "窗格布局",
"창 배치 · 버튼 구성": "窗格布局 · 按钮配置",
"창 보이기 / 숨기기 단축키": "显示 / 隐藏窗口的快捷键", "창 보이기 / 숨기기 단축키": "显示 / 隐藏窗口的快捷键",
"창 숨김 → 프로세스 종료": "窗口隐藏 → 进程退出", "창 숨김 → 프로세스 종료": "窗口隐藏 → 进程退出",
"창 종료": "关闭窗口", "창 종료": "关闭窗口",
"찾기": "搜索",
"찾기: {0}": "搜索: {0}",
"찾는 즐겨찾기가 없습니다.": "没有匹配的书签。", "찾는 즐겨찾기가 없습니다.": "没有匹配的书签。",
"찾은 것이 없습니다.": "未找到。",
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "未找到字幕。请尝试改用英文原名或添加语言。", "찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "未找到字幕。请尝试改用英文原名或添加语言。",
"채도": "饱和度", "채도": "饱和度",
"채우기 (가운데 잘라냄)": "填充(裁切中间)", "채우기 (가운데 잘라냄)": "填充(裁切中间)",
@ -784,6 +849,7 @@
"추가 ({0})": "添加({0})", "추가 ({0})": "添加({0})",
"추가 취소": "取消添加", "추가 취소": "取消添加",
"추가: {0} (내장 자막 {1}개, 같은 폴더 자막 {2}개)": "已添加: {0}(内嵌字幕 {1} 个,同文件夹字幕 {2} 个)", "추가: {0} (내장 자막 {1}개, 같은 폴더 자막 {2}개)": "已添加: {0}(内嵌字幕 {1} 个,同文件夹字幕 {2} 个)",
"추가할 동영상이 없습니다.": "没有可添加的视频。",
"출력: {0}": "输出: {0}", "출력: {0}": "输出: {0}",
"취소": "取消", "취소": "取消",
"취소 · 실패한 다운로드가 남긴 .part · .ytdl · .aria2 · 합치기 전 조각 파일 (jj_yt-dlp · jj_aria2)": "取消 · 失败的下载留下的 .part · .ytdl · .aria2 · 合并前的碎片文件(jj_yt-dlp · jj_aria2)", "취소 · 실패한 다운로드가 남긴 .part · .ytdl · .aria2 · 합치기 전 조각 파일 (jj_yt-dlp · jj_aria2)": "取消 · 失败的下载留下的 .part · .ytdl · .aria2 · 合并前的碎片文件(jj_yt-dlp · jj_aria2)",
@ -791,6 +857,7 @@
"취소했습니다.": "已取消。", "취소했습니다.": "已取消。",
"켜져 있는 창에서 (기본)": "在已打开的窗口中(默认)", "켜져 있는 창에서 (기본)": "在已打开的窗口中(默认)",
"코덱": "编解码器", "코덱": "编解码器",
"크기": "大小",
"크메르어": "高棉语", "크메르어": "高棉语",
"타갈로그어": "他加禄语", "타갈로그어": "他加禄语",
"타밀어": "泰米尔语", "타밀어": "泰米尔语",
@ -804,9 +871,12 @@
"트랙 {0}": "轨道 {0}", "트랙 {0}": "轨道 {0}",
"트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "点击托盘图标或用下方快捷键重新打开", "트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "点击托盘图标或用下方快捷键重新打开",
"트레이 정리": "清理托盘", "트레이 정리": "清理托盘",
"파일 관리자에서 보기": "在文件管理器中显示",
"파일 앱의 \"다음으로 열기\" · 공유에서 JJ_MKVMaker 를 골랐을 때": "在文件应用的“打开方式” · 分享中选择 JJ_MKVMaker 时", "파일 앱의 \"다음으로 열기\" · 공유에서 JJ_MKVMaker 를 골랐을 때": "在文件应用的“打开方式” · 分享中选择 JJ_MKVMaker 时",
"파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 \"동영상 추가\" 로 고르세요.": "无法确定文件位置,因此仅播放。要制作 MKV,请用“添加视频”选择。", "파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 \"동영상 추가\" 로 고르세요.": "无法确定文件位置,因此仅播放。要制作 MKV,请用“添加视频”选择。",
"파일 이름": "文件名", "파일 이름": "文件名",
"파일 탐색기": "文件浏览器",
"파일 탐색기 (지금 여기)": "文件浏览器 (当前位置)",
"파일 확인 중…": "正在检查文件…", "파일 확인 중…": "正在检查文件…",
"파일을 분석할 수 없습니다: {0}": "无法分析文件: {0}", "파일을 분석할 수 없습니다: {0}": "无法分析文件: {0}",
"팝업 끄기": "关闭弹出窗口", "팝업 끄기": "关闭弹出窗口",
@ -833,8 +903,10 @@
"폴더가 아닙니다: {0}": "不是文件夹: {0}", "폴더가 아닙니다: {0}": "不是文件夹: {0}",
"폴더를 만들 수 없습니다: {0}": "无法创建文件夹: {0}", "폴더를 만들 수 없습니다: {0}": "无法创建文件夹: {0}",
"폴란드어": "波兰语", "폴란드어": "波兰语",
"표시 (여러 개 고르기)": "标记 (多选)",
"표시 이름": "显示名称", "표시 이름": "显示名称",
"표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다": "显示名称可直接修改。连续点击视频行三次会移到第一个按钮的文件夹", "표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다": "显示名称可直接修改。连续点击视频行三次会移到第一个按钮的文件夹",
"표시 지우기": "清除标记",
"표준": "标准", "표준": "标准",
"푸는 중": "正在解压", "푸는 중": "正在解压",
"프랑스어": "法语", "프랑스어": "法语",
@ -854,6 +926,7 @@
"필수 프로그램 점검": "检查必需程序", "필수 프로그램 점검": "检查必需程序",
"필수 프로그램이 모두 설치되어 있습니다.": "所有必需程序均已安装。", "필수 프로그램이 모두 설치되어 있습니다.": "所有必需程序均已安装。",
"하위 폴더가 없습니다": "没有子文件夹", "하위 폴더가 없습니다": "没有子文件夹",
"한 창": "单窗格",
"한국어": "韩语", "한국어": "韩语",
"한글": "韩文", "한글": "韩文",
"해제": "解除", "해제": "解除",
@ -870,6 +943,7 @@
"화면": "显示", "화면": "显示",
"화면 · 색 보정": "画面 · 色彩", "화면 · 색 보정": "画面 · 色彩",
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "将在此设备上翻译所有界面文字。可能需要几分钟。", "화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "将在此设备上翻译所有界面文字。可能需要几分钟。",
"화면 모양 따라": "跟随屏幕形状",
"화면 방향": "屏幕方向", "화면 방향": "屏幕方向",
"화면 비율": "画面比例", "화면 비율": "画面比例",
"화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도": "画面比例(横屏 · 竖屏)· 旋转 · 亮度 · 对比度 · 饱和度 · 色温", "화면 비율 (가로 · 세로) · 회전 · 밝기 · 대비 · 채도 · 색온도": "画面比例(横屏 · 竖屏)· 旋转 · 亮度 · 对比度 · 饱和度 · 色温",

View File

@ -0,0 +1,48 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/explorer_page.dart';
import 'package:jj_mkvmaker/ui/theme.dart';
/// 파일 탐색기 화면을 실제 글꼴 · 실제 FFmpeg (썸네일 · 동영상 정보) 로 그려 PNG 로 저장 (JJ_SHOT_DIR).
/// 왼쪽 · 오른쪽 창 폴더: JJ_SHOT_LEFT · JJ_SHOT_RIGHT. 화면 배치 확인용.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('파일 탐색기 화면 그림', (tester) async {
final env = Platform.environment;
final out = env['JJ_SHOT_DIR'] ?? Directory.systemTemp.path;
final c = AppController(PlatformServices.create());
c.settings.explorerPaths = [env['JJ_SHOT_LEFT'] ?? Directory.current.path, env['JJ_SHOT_RIGHT'] ?? Directory.current.path];
final key = GlobalKey();
for (final (name, size, orient) in [
('wide', const Size(1600, 900), 'auto'),
('tall', const Size(900, 1400), 'auto'),
]) {
c.settings.explorerOrientation = orient;
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1;
await tester.runAsync(() => tester.pumpWidget(MaterialApp(
theme: buildTheme(),
home: RepaintBoundary(key: key, child: ExplorerPage(c: c)),
)));
// 목록 · 썸네일 · 동영상 정보가 나올 때까지
for (var i = 0; i < 40; i++) {
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 250)));
await tester.pump();
}
final boundary = key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final image = await tester.runAsync(() => boundary.toImage());
final bytes = await tester.runAsync(() => image!.toByteData(format: ui.ImageByteFormat.png));
File('$out/explorer_$name.png').writeAsBytesSync(bytes!.buffer.asUint8List());
await tester.pumpWidget(const SizedBox());
}
tester.view.reset();
});
}

View File

@ -1114,12 +1114,14 @@ class AppController extends ChangeNotifier {
/// 재생 준비. 확장자에 외부 프로그램이 지정되어 있으면 그것으로 열고 null, /// 재생 준비. 확장자에 외부 프로그램이 지정되어 있으면 그것으로 열고 null,
/// 아니면 내장 플레이어용 (목록, 시작 위치). /// 아니면 내장 플레이어용 (목록, 시작 위치).
/// [keepOrder]: 여러 개를 줄 때 이름순으로 다시 정렬하지 않고 준 순서대로 재생 /// [keepOrder]: 여러 개를 줄 때 이름순으로 다시 정렬하지 않고 준 순서대로 재생
Future<(List<String>, int)?> preparePlayback(List<String> files, {bool keepOrder = false}) async { /// [internal]: 외부 프로그램 설정과 상관없이 내장 플레이어로 (파일 탐색기의 "내장 플레이어로 재생")
Future<(List<String>, int)?> preparePlayback(List<String> files,
{bool keepOrder = false, bool internal = false}) async {
final videos = files.where(isVideoFile).toList(); final videos = files.where(isVideoFile).toList();
if (videos.isEmpty) return null; if (videos.isEmpty) return null;
final ext = p.extension(videos.first).replaceFirst('.', '').toLowerCase(); final ext = p.extension(videos.first).replaceFirst('.', '').toLowerCase();
final program = settings.externalPlayers[ext]; final program = settings.externalPlayers[ext];
if (program != null && program.isNotEmpty) { if (!internal && program != null && program.isNotEmpty) {
await services.shell.openExternal(program, videos); await services.shell.openExternal(program, videos);
_log(trf('외부 프로그램으로 재생: {0} ← {1}개', [program == 'system' ? tr('기본 연결 프로그램') : p.basename(program), videos.length])); _log(trf('외부 프로그램으로 재생: {0} ← {1}개', [program == 'system' ? tr('기본 연결 프로그램') : p.basename(program), videos.length]));
return null; return null;

View File

@ -91,7 +91,7 @@ class AppSettings {
/// 메인 화면 아래 작업 기록 창 보이기 /// 메인 화면 아래 작업 기록 창 보이기
bool showLog = true; bool showLog = true;
/// 처음 화면: 'home' = 지금처럼 MKV 화면 (기본), 'browser' = 웹 브라우저 ([homeUrl]) /// 처음 화면: 'home' = 지금처럼 MKV 화면 (기본), 'browser' = 웹 브라우저 ([homeUrl]), 'files' = 파일 탐색기
String startScreen = 'home'; String startScreen = 'home';
/// 화면 (글자 · 버튼) 크기 배율. [uiScale] 은 지금 크기 (위쪽 막대의 − · + 로 바꿈), /// 화면 (글자 · 버튼) 크기 배율. [uiScale] 은 지금 크기 (위쪽 막대의 − · + 로 바꿈),
@ -133,6 +133,30 @@ class AppSettings {
/// 앱 안 브라우저: 페이지의 JavaScript 실행 (끄면 스크립트 없이 글 · 그림만) /// 앱 안 브라우저: 페이지의 JavaScript 실행 (끄면 스크립트 없이 글 · 그림만)
bool webJavaScript = true; bool webJavaScript = true;
// ── 파일 탐색기 (X-plore 참고) ──
/// 창: 'dual' 두 창 (기본) · 'single' 한 창
String explorerLayout = 'dual';
/// 두 창 배치: 'auto' 화면 모양 따라 (가로로 넓으면 좌우, 세로로 길면 위아래) · 'side' 좌우 · 'stacked' 위아래
String explorerOrientation = 'auto';
/// 기능 버튼 줄: 'middle' 두 창 사이 (기본) · 'edge' 오른쪽 (위아래 배치면 아래) 끝 · 'hidden' 숨김
String explorerToolbar = 'middle';
/// 기능 버튼 줄에 보일 버튼과 순서 (ExplorerButton 이름). 비어 있으면 기본 구성.
List<String> explorerButtons = [];
/// 정렬 · 숨긴 항목
String explorerSort = 'name';
bool explorerSortDesc = false;
bool explorerShowHidden = false;
/// 마지막으로 연 폴더 (왼쪽 · 오른쪽 창)
List<String> explorerPaths = [];
/// 최근에 연 폴더 (내역, 최신이 앞)
List<String> explorerHistory = [];
/// 앱 안 브라우저의 데이터 폴더 (로그인 · 쿠키). 실행 중에 정해지며 저장하지 않음 /// 앱 안 브라우저의 데이터 폴더 (로그인 · 쿠키). 실행 중에 정해지며 저장하지 않음
String webViewDataDir = ''; String webViewDataDir = '';
@ -199,6 +223,15 @@ class AppSettings {
'externalBrowser': externalBrowser, 'externalBrowser': externalBrowser,
'webTranslate': webTranslate, 'webTranslate': webTranslate,
'webJavaScript': webJavaScript, 'webJavaScript': webJavaScript,
'explorerLayout': explorerLayout,
'explorerOrientation': explorerOrientation,
'explorerToolbar': explorerToolbar,
'explorerButtons': explorerButtons,
'explorerSort': explorerSort,
'explorerSortDesc': explorerSortDesc,
'explorerShowHidden': explorerShowHidden,
'explorerPaths': explorerPaths,
'explorerHistory': explorerHistory,
'autoCheckUpdates': autoCheckUpdates, 'autoCheckUpdates': autoCheckUpdates,
'lastUpdateCheck': lastUpdateCheck, 'lastUpdateCheck': lastUpdateCheck,
'lastUpdateCheckVersion': lastUpdateCheckVersion, 'lastUpdateCheckVersion': lastUpdateCheckVersion,
@ -273,6 +306,17 @@ class AppSettings {
..externalBrowser = j['externalBrowser'] as String? ?? 'system' ..externalBrowser = j['externalBrowser'] as String? ?? 'system'
..webTranslate = j['webTranslate'] as bool? ?? false ..webTranslate = j['webTranslate'] as bool? ?? false
..webJavaScript = j['webJavaScript'] as bool? ?? true ..webJavaScript = j['webJavaScript'] as bool? ?? true
..explorerLayout = j['explorerLayout'] == 'single' ? 'single' : 'dual'
..explorerOrientation = const ['side', 'stacked'].contains(j['explorerOrientation'])
? j['explorerOrientation'] as String
: 'auto'
..explorerToolbar = const ['edge', 'hidden'].contains(j['explorerToolbar']) ? j['explorerToolbar'] as String : 'middle'
..explorerButtons = [for (final x in (j['explorerButtons'] as List?) ?? const []) '$x']
..explorerSort = const ['date', 'size', 'type'].contains(j['explorerSort']) ? j['explorerSort'] as String : 'name'
..explorerSortDesc = j['explorerSortDesc'] as bool? ?? false
..explorerShowHidden = j['explorerShowHidden'] as bool? ?? false
..explorerPaths = [for (final x in (j['explorerPaths'] as List?) ?? const []) '$x']
..explorerHistory = [for (final x in (j['explorerHistory'] as List?) ?? const []) '$x']
..autoCheckUpdates = j['autoCheckUpdates'] as bool? ?? true ..autoCheckUpdates = j['autoCheckUpdates'] as bool? ?? true
..lastUpdateCheck = j['lastUpdateCheck'] as String? ?? '' ..lastUpdateCheck = j['lastUpdateCheck'] as String? ?? ''
..lastUpdateCheckVersion = j['lastUpdateCheckVersion'] as String? ?? '' ..lastUpdateCheckVersion = j['lastUpdateCheckVersion'] as String? ?? ''

284
lib/core/file_ops.dart Normal file
View File

@ -0,0 +1,284 @@
import 'dart:async';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'playlist.dart' show naturalCompare;
/// 파일 탐색기 (X-plore 참고) 의 화면과 상관없는 부분: 폴더 읽기 · 정렬 · 복사 / 이동 / 삭제 · 찾기.
/// 폴더 안의 항목 하나
class FileEntry {
final String path;
final bool isDir;
final int size;
final DateTime modified;
const FileEntry(this.path, {required this.isDir, this.size = 0, required this.modified});
String get name => p.basename(path);
bool get hidden => name.startsWith('.');
/// 확장자 (소문자, 점 없이)
String get ext => isDir ? '' : p.extension(path).replaceFirst('.', '').toLowerCase();
}
enum SortBy { name, date, size, type }
/// 정렬: 폴더 먼저, 이름은 숫자를 숫자로 비교 (file2 < file10)
List<FileEntry> sortEntries(List<FileEntry> list, SortBy by, {bool descending = false}) {
int cmp(FileEntry a, FileEntry b) {
if (a.isDir != b.isDir) return a.isDir ? -1 : 1; // 폴더는 거꾸로여도 늘 위
final r = switch (by) {
SortBy.name => 0,
SortBy.date => a.modified.compareTo(b.modified),
SortBy.size => a.size.compareTo(b.size),
SortBy.type => a.ext.compareTo(b.ext),
};
final v = r != 0 ? r : naturalCompare(a.name, b.name);
return descending ? -v : v;
}
return [...list]..sort(cmp);
}
/// 폴더 읽기 (읽을 수 없는 항목은 건너뜀). [showHidden] 이 아니면 점으로 시작하는 항목을 뺀다.
Future<List<FileEntry>> listEntries(String dir, {bool showHidden = false}) async {
final out = <FileEntry>[];
await for (final e in Directory(dir).list(followLinks: false).handleError((_) {})) {
try {
final st = await e.stat();
final isDir = st.type == FileSystemEntityType.directory;
final entry = FileEntry(e.path, isDir: isDir, size: isDir ? 0 : st.size, modified: st.modified);
if (!showHidden && entry.hidden) continue;
out.add(entry);
} catch (_) {}
}
return out;
}
/// 저장 장치 (맨 위 폴더): Windows 는 드라이브 (C:\ …), 그 밖은 주어진 목록
List<(String path, String label)> windowsDrives() => [
for (final c in 'CDEFGHIJKLMNOPQRSTUVWXYZAB'.split(''))
if (FileSystemEntity.isDirectorySync('$c:\\')) ('$c:\\', '$c:'),
];
/// 같은 이름이 있으면 "이름 (2).확장자" 로
String uniqueTarget(String dir, String name) {
var target = p.join(dir, name);
if (FileSystemEntity.typeSync(target) == FileSystemEntityType.notFound) return target;
final base = p.basenameWithoutExtension(name), ext = p.extension(name);
for (var i = 2;; i++) {
target = p.join(dir, '$base ($i)$ext');
if (FileSystemEntity.typeSync(target) == FileSystemEntityType.notFound) return target;
}
}
/// 같은 경로인지 (Windows 는 대소문자 무시)
bool samePath(String a, String b) {
final x = p.normalize(p.absolute(a)), y = p.normalize(p.absolute(b));
return Platform.isWindows ? x.toLowerCase() == y.toLowerCase() : x == y;
}
/// [a] 가 [b] 와 같거나 그 안에 있는지 (폴더를 자기 안으로 복사 · 이동하지 않게)
bool isSameOrInside(String a, String b) {
final x = p.normalize(p.absolute(a)), y = p.normalize(p.absolute(b));
return samePath(a, b) || p.isWithin(y, x) || (Platform.isWindows && p.isWithin(y.toLowerCase(), x.toLowerCase()));
}
/// 복사 · 이동 중 알림 (지금 파일, 지금까지 바이트, 전체 바이트)
typedef CopyProgress = void Function(String current, int done, int total);
class FileOpCancelled implements Exception {
const FileOpCancelled();
}
/// 복사 · 이동 · 삭제. [cancel] 을 부르면 다음 조각에서 멈춘다.
class FileOps {
bool _cancel = false;
void cancel() => _cancel = true;
void _check() {
if (_cancel) throw const FileOpCancelled();
}
/// 전체 크기 (진행률용)
static Future<int> totalSize(List<String> paths) async {
var total = 0;
for (final s in paths) {
final t = FileSystemEntity.typeSync(s, followLinks: false);
if (t == FileSystemEntityType.directory) {
await for (final e in Directory(s).list(recursive: true, followLinks: false).handleError((_) {})) {
if (e is File) {
try {
total += await e.length();
} catch (_) {}
}
}
} else if (t == FileSystemEntityType.file) {
total += await File(s).length();
}
}
return total;
}
/// [sources] 를 [destDir] 로 복사 (같은 이름이 있으면 "이름 (2)"). 만든 경로들을 돌려준다.
Future<List<String>> copy(List<String> sources, String destDir, {CopyProgress? onProgress}) async {
final total = await totalSize(sources);
var done = 0;
final made = <String>[];
Future<void> copyFile(File f, String target) async {
_check();
final out = File(target).openWrite();
try {
try {
await for (final chunk in f.openRead()) {
_check();
out.add(chunk);
done += chunk.length;
onProgress?.call(f.path, done, total);
}
} finally {
await out.close();
}
_check();
} catch (_) {
// 취소 · 실패: 만들다 만 파일을 남기지 않는다
try {
await File(target).delete();
} catch (_) {}
rethrow;
}
try {
await File(target).setLastModified(await f.lastModified());
} catch (_) {}
}
Future<void> copyAny(String src, String target) async {
if (FileSystemEntity.isDirectorySync(src)) {
await Directory(target).create(recursive: true);
await for (final e in Directory(src).list(followLinks: false)) {
await copyAny(e.path, p.join(target, p.basename(e.path)));
}
} else {
await copyFile(File(src), target);
}
}
for (final s in sources) {
if (FileSystemEntity.isDirectorySync(s) && isSameOrInside(destDir, s)) {
throw FileSystemException('폴더를 자기 안으로 복사할 수 없습니다', s);
}
final target = uniqueTarget(destDir, p.basename(s));
await copyAny(s, target);
made.add(target);
}
return made;
}
/// 이동: 같은 드라이브면 이름 바꾸기, 아니면 복사한 뒤 원본 삭제
Future<List<String>> move(List<String> sources, String destDir, {CopyProgress? onProgress}) async {
final made = <String>[];
for (final s in sources) {
_check();
if (isSameOrInside(destDir, s)) throw FileSystemException('폴더를 자기 안으로 옮길 수 없습니다', s);
if (samePath(p.dirname(s), destDir)) {
made.add(s); // 이미 그 폴더에 있음
continue;
}
final target = uniqueTarget(destDir, p.basename(s));
try {
final t = FileSystemEntity.typeSync(s);
if (t == FileSystemEntityType.directory) {
await Directory(s).rename(target);
} else {
await File(s).rename(target);
}
made.add(target);
} on FileSystemException {
// 다른 드라이브 · 저장 장치: 복사 후 삭제
final copied = await copy([s], destDir, onProgress: onProgress);
await delete([s]);
made.addAll(copied);
}
}
return made;
}
Future<void> delete(List<String> paths) async {
for (final s in paths) {
_check();
final t = FileSystemEntity.typeSync(s, followLinks: false);
if (t == FileSystemEntityType.directory) {
await Directory(s).delete(recursive: true);
} else if (t != FileSystemEntityType.notFound) {
await File(s).delete();
}
}
}
/// 이름 바꾸기 (같은 폴더 안). 새 경로를 돌려준다.
static Future<String> rename(String path, String newName) async {
final name = newName.trim();
if (name.isEmpty || name.contains(RegExp(r'[\\/:*?"<>|]'))) {
throw FileSystemException('쓸 수 없는 이름입니다', name);
}
final target = p.join(p.dirname(path), name);
if (target == path) return path;
if (FileSystemEntity.typeSync(target) != FileSystemEntityType.notFound &&
target.toLowerCase() != path.toLowerCase()) {
throw FileSystemException('같은 이름이 이미 있습니다', target);
}
final t = FileSystemEntity.typeSync(path);
return t == FileSystemEntityType.directory
? (await Directory(path).rename(target)).path
: (await File(path).rename(target)).path;
}
static Future<String> makeFolder(String dir, String name) async {
final n = name.trim();
if (n.isEmpty || n.contains(RegExp(r'[\\/:*?"<>|]'))) throw FileSystemException('쓸 수 없는 이름입니다', n);
final target = p.join(dir, n);
if (FileSystemEntity.typeSync(target) != FileSystemEntityType.notFound) {
throw FileSystemException('같은 이름이 이미 있습니다', target);
}
return (await Directory(target).create()).path;
}
}
/// [root] 아래에서 이름에 [query] 가 들어간 항목 찾기 (대소문자 무시, * ? 사용 가능). 찾는 대로 내보낸다.
Stream<FileEntry> searchFiles(String root, String query, {bool showHidden = false, int limit = 2000}) async* {
final q = query.trim().toLowerCase();
if (q.isEmpty) return;
final wild = q.contains('*') || q.contains('?');
final re = wild
? RegExp('^${RegExp.escape(q).replaceAll(r'\*', '.*').replaceAll(r'\?', '.')}\$')
: null;
var n = 0;
final pending = <String>[root];
while (pending.isNotEmpty) {
final dir = pending.removeAt(0);
List<FileEntry> items;
try {
items = await listEntries(dir, showHidden: showHidden);
} catch (_) {
continue;
}
for (final e in sortEntries(items, SortBy.name)) {
final name = e.name.toLowerCase();
if (re != null ? re.hasMatch(name) : name.contains(q)) {
yield e;
if (++n >= limit) return;
}
if (e.isDir) pending.add(e.path);
}
}
}
/// "1.2GB" · "350MB" · "12KB"
String formatSize(int bytes) {
const k = 1024;
if (bytes >= k * k * k) return '${(bytes / (k * k * k)).toStringAsFixed(1)}GB';
if (bytes >= k * k) return '${(bytes / (k * k)).toStringAsFixed(bytes < 10 * k * k ? 1 : 0)}MB';
if (bytes >= k) return '${(bytes / k).round()}KB';
return '${bytes}B';
}

View File

@ -32,6 +32,7 @@ import 'ui/app_actions.dart';
import 'ui/app_drop.dart'; import 'ui/app_drop.dart';
import 'ui/browser_page.dart'; import 'ui/browser_page.dart';
import 'ui/downloads_page.dart'; import 'ui/downloads_page.dart';
import 'ui/explorer_page.dart';
import 'ui/exit_dialog.dart'; import 'ui/exit_dialog.dart';
import 'ui/home_page.dart'; import 'ui/home_page.dart';
import 'ui/player_page.dart'; import 'ui/player_page.dart';
@ -283,10 +284,13 @@ Future<void> main(List<String> args) async {
content: Text(trf('지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 ' '{0}:{1}). ' '작업 기록에 남겨 두었습니다.', [crashed.$2.hour.toString().padLeft(2, '0'), crashed.$2.minute.toString().padLeft(2, '0')])), content: Text(trf('지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 ' '{0}:{1}). ' '작업 기록에 남겨 두었습니다.', [crashed.$2.hour.toString().padLeft(2, '0'), crashed.$2.minute.toString().padLeft(2, '0')])),
)); ));
} }
// 시작 화면을 "웹 브라우저" 로 정했으면 브라우저를 연다 (MKV 화면은 그 아래에 있음) // 시작 화면을 "웹 브라우저" · "파일 탐색기" 로 정했으면 그 화면을 연다 (MKV 화면은 그 아래에 있음)
if (controller.settings.startScreen == 'browser') { if (controller.settings.startScreen == 'browser') {
final nav = navigatorKey.currentState; final nav = navigatorKey.currentState;
if (nav != null) unawaited(BrowserPage.open(nav, c: controller, downloads: downloads, bookmarks: bookmarks)); if (nav != null) unawaited(BrowserPage.open(nav, c: controller, downloads: downloads, bookmarks: bookmarks));
} else if (controller.settings.startScreen == 'files') {
final nav = navigatorKey.currentState;
if (nav != null) unawaited(ExplorerPage.open(nav, c: controller));
} }
await checkRequiredTools(ctx, services.shell); await checkRequiredTools(ctx, services.shell);
final ctx2 = navigatorKey.currentContext; final ctx2 = navigatorKey.currentContext;
@ -439,6 +443,9 @@ Future<void> runAndroid(String dataDir) async {
if (controller.settings.startScreen == 'browser') { if (controller.settings.startScreen == 'browser') {
final nav = navigatorKey.currentState; final nav = navigatorKey.currentState;
if (nav != null) unawaited(BrowserPage.open(nav, c: controller, downloads: downloads, bookmarks: bookmarks)); if (nav != null) unawaited(BrowserPage.open(nav, c: controller, downloads: downloads, bookmarks: bookmarks));
} else if (controller.settings.startScreen == 'files') {
final nav = navigatorKey.currentState;
if (nav != null) unawaited(ExplorerPage.open(nav, c: controller));
} }
// 새 버전 확인 (하루 한 번, 환경 설정에서 끌 수 있음) // 새 버전 확인 (하루 한 번, 환경 설정에서 끌 수 있음)
final ctx = navigatorKey.currentContext; final ctx = navigatorKey.currentContext;

View File

@ -45,6 +45,22 @@ class AndroidShell extends NoopShell {
} catch (_) {} } catch (_) {}
} }
/// 다른 앱으로 열기 (MainActivity "openWith"): [choose] 면 늘 앱 고르기 창, 아니면 기본 앱 (없으면 고르기 창)
@override
Future<bool> openWith(String path, {bool choose = false}) async {
try {
return await _ch.invokeMethod<bool>('openWith', {'path': path, 'choose': choose}) ?? false;
} catch (_) {
return false;
}
}
/// 환경 설정의 확장자별 재생 프로그램: Android 는 'system' (기본 앱) 만 있다
@override
Future<void> openExternal(String program, List<String> files) async {
if (files.isNotEmpty) await openWith(files.first);
}
/// 파일이 있는 폴더 (폴더면 그 폴더) 를 파일 앱으로 /// 파일이 있는 폴더 (폴더면 그 폴더) 를 파일 앱으로
@override @override
Future<void> revealFile(String path) async { Future<void> revealFile(String path) async {

View File

@ -291,6 +291,21 @@ class DesktopShell with WindowListener implements AppShell {
Future<void> openUrl(String url, {String browser = 'system'}) => openInBrowser(url, browser); Future<void> openUrl(String url, {String browser = 'system'}) => openInBrowser(url, browser);
/// 탐색기로 폴더를 열고 파일을 선택. 파일이 없으면 폴더만 (그것도 없으면 가장 가까운 위 폴더). /// 탐색기로 폴더를 열고 파일을 선택. 파일이 없으면 폴더만 (그것도 없으면 가장 가까운 위 폴더).
/// 기본 연결 프로그램 (탐색기에서 두 번 누른 것과 같음) 또는 "연결 프로그램" 고르기 창
@override
Future<bool> openWith(String path, {bool choose = false}) async {
try {
if (choose) {
await Process.start('rundll32.exe', ['shell32.dll,OpenAs_RunDLL', path], mode: ProcessStartMode.detached);
} else {
await Process.start('explorer.exe', [path], mode: ProcessStartMode.detached);
}
return true;
} catch (_) {
return false;
}
}
@override @override
Future<void> revealFile(String path) async { Future<void> revealFile(String path) async {
if (File(path).existsSync()) { if (File(path).existsSync()) {

View File

@ -57,6 +57,10 @@ abstract class AppShell {
/// 파일이 있는 폴더를 파일 관리자(탐색기)로 열고 그 파일을 선택해 보여 준다 /// 파일이 있는 폴더를 파일 관리자(탐색기)로 열고 그 파일을 선택해 보여 준다
Future<void> revealFile(String path); Future<void> revealFile(String path);
/// 파일을 다른 프로그램으로 열기 (파일 탐색기). [choose]: 프로그램 고르기 창을 띄움, 아니면 기본 연결 프로그램.
/// 열지 못하면 false.
Future<bool> openWith(String path, {bool choose = false});
/// 설치된 외부 브라우저 ('chrome', 'firefox', 'edge', 'whale' 중) /// 설치된 외부 브라우저 ('chrome', 'firefox', 'edge', 'whale' 중)
List<String> installedBrowsers(); List<String> installedBrowsers();
@ -115,6 +119,8 @@ class NoopShell implements AppShell {
@override @override
Future<void> revealFile(String path) async {} Future<void> revealFile(String path) async {}
@override @override
Future<bool> openWith(String path, {bool choose = false}) async => false;
@override
List<String> installedBrowsers() => const []; List<String> installedBrowsers() => const [];
@override @override
Future<List<String>> missingTools() async => const []; Future<List<String>> missingTools() async => const [];

View File

@ -8,6 +8,7 @@ import '../services/app_shell.dart' show appIconButtonAsset;
import '../services/system_usage.dart'; import '../services/system_usage.dart';
import 'browser_page.dart'; import 'browser_page.dart';
import 'downloads_page.dart'; import 'downloads_page.dart';
import 'explorer_page.dart';
import 'settings_page.dart'; import 'settings_page.dart';
import 'theme.dart'; import 'theme.dart';
import '../l10n/tr.dart'; import '../l10n/tr.dart';
@ -38,28 +39,35 @@ class AppScope extends InheritedWidget {
} }
/// 모든 화면의 위쪽 막대 맨 왼쪽 (같은 자리 · 같은 순서): /// 모든 화면의 위쪽 막대 맨 왼쪽 (같은 자리 · 같은 순서):
/// [JJ] 홈 화면 (환경 설정의 "홈 화면": MKV 화면 또는 웹 브라우저) /// [JJ] 홈 화면 (환경 설정의 "홈 화면": MKV 화면 · 웹 브라우저 · 파일 탐색기)
/// [▤] MKV 화면으로 [←] 뒤로 [⇩] 다운로드 목록 /// [▤] MKV 화면으로 [🌐] 웹 브라우저 [📁] 파일 탐색기 [←] 뒤로 [⇩] 다운로드 목록
class AppNavButtons extends StatelessWidget { class AppNavButtons extends StatelessWidget {
/// 다운로드 목록 화면 자신 (그 버튼을 "지금 여기" 로 표시) /// 다운로드 목록 화면 자신 (그 버튼을 "지금 여기" 로 표시)
final bool onDownloadsPage; final bool onDownloadsPage;
/// 웹 브라우저 화면 자신 /// 웹 브라우저 화면 자신
final bool onBrowserPage; final bool onBrowserPage;
const AppNavButtons({super.key, this.onDownloadsPage = false, this.onBrowserPage = false});
static const double width = 5 * 40; /// 파일 탐색기 화면 자신
final bool onExplorerPage;
const AppNavButtons({super.key, this.onDownloadsPage = false, this.onBrowserPage = false, this.onExplorerPage = false});
static const double width = 6 * 40;
/// MKV 화면 (맨 처음 화면) 까지 돌아가기 /// MKV 화면 (맨 처음 화면) 까지 돌아가기
static void toMkv(BuildContext context) => Navigator.of(context).popUntil((r) => r.isFirst); static void toMkv(BuildContext context) => Navigator.of(context).popUntil((r) => r.isFirst);
/// 홈 화면으로: MKV 화면까지 돌아간 뒤, 홈 화면이 웹 브라우저면 브라우저를 연다 /// 홈 화면으로: MKV 화면까지 돌아간 뒤, 홈 화면이 웹 브라우저 · 파일 탐색기면 그 화면을 연다
static void toHome(BuildContext context) { static void toHome(BuildContext context) {
final scope = AppScope.maybeOf(context); final scope = AppScope.maybeOf(context);
final nav = Navigator.of(context); final nav = Navigator.of(context);
nav.popUntil((r) => r.isFirst); nav.popUntil((r) => r.isFirst);
if (scope != null && scope.controller.settings.startScreen == 'browser' && scope.bookmarks != null) { if (scope == null) return;
final start = scope.controller.settings.startScreen;
if (start == 'browser' && scope.bookmarks != null) {
BrowserPage.open(nav, c: scope.controller, downloads: scope.downloads, bookmarks: scope.bookmarks!); BrowserPage.open(nav, c: scope.controller, downloads: scope.downloads, bookmarks: scope.bookmarks!);
} else if (start == 'files') {
ExplorerPage.open(nav, c: scope.controller);
} }
} }
@ -68,7 +76,12 @@ class AppNavButtons extends StatelessWidget {
final scope = AppScope.maybeOf(context); final scope = AppScope.maybeOf(context);
final nav = Navigator.maybeOf(context); final nav = Navigator.maybeOf(context);
final atRoot = !(nav?.canPop() ?? false); final atRoot = !(nav?.canPop() ?? false);
final homeIsBrowser = scope?.controller.settings.startScreen == 'browser' && scope?.bookmarks != null; final start = scope?.controller.settings.startScreen;
final homeName = start == 'browser' && scope?.bookmarks != null
? tr('웹 브라우저')
: start == 'files'
? tr('파일 탐색기')
: tr('MKV 화면');
final downloads = scope?.downloads; final downloads = scope?.downloads;
Widget btn(Widget icon, String tip, VoidCallback? f, {bool here = false}) => SizedBox( Widget btn(Widget icon, String tip, VoidCallback? f, {bool here = false}) => SizedBox(
width: 40, width: 40,
@ -87,7 +100,7 @@ class AppNavButtons extends StatelessWidget {
btn( btn(
Image.asset(appIconButtonAsset(scope?.controller.settings.appIcon), Image.asset(appIconButtonAsset(scope?.controller.settings.appIcon),
width: 26, height: 26, filterQuality: FilterQuality.medium), width: 26, height: 26, filterQuality: FilterQuality.medium),
trf('홈 화면 ({0}) · 환경 설정에서 바꿈', [homeIsBrowser ? tr('웹 브라우저') : tr('MKV 화면')]), trf('홈 화면 ({0}) · 환경 설정에서 바꿈', [homeName]),
scope == null ? null : () => toHome(context), scope == null ? null : () => toHome(context),
), ),
btn( btn(
@ -106,6 +119,13 @@ class AppNavButtons extends StatelessWidget {
c: scope!.controller, downloads: scope.downloads, bookmarks: scope.bookmarks!), c: scope!.controller, downloads: scope.downloads, bookmarks: scope.bookmarks!),
here: onBrowserPage, here: onBrowserPage,
), ),
// 파일 탐색기: 웹 브라우저 다음
btn(
Icon(Icons.folder_copy_outlined, color: onExplorerPage ? JjColors.accent : null),
onExplorerPage ? tr('파일 탐색기 (지금 여기)') : tr('파일 탐색기'),
scope == null || onExplorerPage ? null : () => ExplorerPage.open(Navigator.of(context), c: scope.controller),
here: onExplorerPage,
),
btn(const Icon(Icons.arrow_back), tr('뒤로'), atRoot ? null : () => Navigator.maybePop(context)), btn(const Icon(Icons.arrow_back), tr('뒤로'), atRoot ? null : () => Navigator.maybePop(context)),
btn( btn(
Icon(Icons.download_for_offline_outlined, color: onDownloadsPage ? JjColors.accent : null), Icon(Icons.download_for_offline_outlined, color: onDownloadsPage ? JjColors.accent : null),

1359
lib/ui/explorer_page.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@ -15,11 +15,11 @@ import 'app_actions.dart';
import 'theme.dart'; import 'theme.dart';
import '../l10n/tr.dart'; import '../l10n/tr.dart';
/// 파일들을 재생 (외부 프로그램이 지정된 확장자면 그 프로그램으로) /// 파일들을 재생 (외부 프로그램이 지정된 확장자면 그 프로그램으로, [internal] 이면 늘 내장 플레이어)
Future<void> playFiles(BuildContext context, AppController c, List<String> files, Future<void> playFiles(BuildContext context, AppController c, List<String> files,
{bool keepOrder = false}) async { {bool keepOrder = false, bool internal = false}) async {
final create = c.services.createMediaPlayer; final create = c.services.createMediaPlayer;
final plan = await c.preparePlayback(files, keepOrder: keepOrder); final plan = await c.preparePlayback(files, keepOrder: keepOrder, internal: internal);
if (plan == null || create == null || !context.mounted) return; if (plan == null || create == null || !context.mounted) return;
final (list, start) = plan; final (list, start) = plan;
await Navigator.of(context).push(MaterialPageRoute<void>( await Navigator.of(context).push(MaterialPageRoute<void>(

View File

@ -308,6 +308,7 @@ class _SettingsPageState extends State<SettingsPage> {
items: [ items: [
DropdownMenuItem(value: 'home', child: Text(tr('MKV 화면 (기본)'))), DropdownMenuItem(value: 'home', child: Text(tr('MKV 화면 (기본)'))),
DropdownMenuItem(value: 'browser', child: Text(tr('웹 브라우저 (홈 주소)'))), DropdownMenuItem(value: 'browser', child: Text(tr('웹 브라우저 (홈 주소)'))),
DropdownMenuItem(value: 'files', child: Text(tr('파일 탐색기'))),
], ],
onChanged: (v) => c.updateSettings((x) => x.startScreen = v!), onChanged: (v) => c.updateSettings((x) => x.startScreen = v!),
), ),

View File

@ -189,7 +189,7 @@ void main() {
}); });
testWidgets('MKV 화면: 체크 → "자막 만들기 (2)" · "MKV 만들기 (2)" 글, 전체 선택', (tester) async { testWidgets('MKV 화면: 체크 → "자막 만들기 (2)" · "MKV 만들기 (2)" 글, 전체 선택', (tester) async {
tester.view.physicalSize = const Size(1500, 900); tester.view.physicalSize = const Size(1600, 900); // 위쪽 버튼 글이 다 보이는 너비
tester.view.devicePixelRatio = 1; tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset); addTearDown(tester.view.reset);
final c = AppController(PlatformServices( final c = AppController(PlatformServices(

178
test/explorer_test.dart Normal file
View File

@ -0,0 +1,178 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/app/settings.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/app_shell.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/explorer_page.dart';
import 'package:path/path.dart' as p;
/// 다른 앱으로 열기를 기록만 하는 셸 (실제로 프로그램을 띄우지 않게)
class _Shell extends NoopShell {
final opened = <(String, bool)>[];
@override
Future<bool> openWith(String path, {bool choose = false}) async {
opened.add((path, choose));
return true;
}
}
void main() {
late Directory tmp;
late String left, right;
late _Shell shell;
late AppController c;
setUp(() {
tmp = Directory.systemTemp.createTempSync('jj_explorer_');
left = p.join(tmp.path, 'left');
right = p.join(tmp.path, 'right');
File(p.join(left, 'doc.txt'))
..createSync(recursive: true)
..writeAsStringSync('hello');
File(p.join(left, 'sub', 'inner.txt'))
..createSync(recursive: true)
..writeAsStringSync('inner');
Directory(right).createSync();
shell = _Shell();
c = AppController(PlatformServices(
mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), shell: shell));
c.settings.explorerPaths = [left, right];
});
tearDown(() => tmp.deleteSync(recursive: true));
Future<void> open(WidgetTester tester) async {
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('doc.txt').evaluate().isNotEmpty);
}
testWidgets('두 창: 폴더 펼치기 · 파일 누르면 기본 앱 · 길게 눌러 다른 앱으로 · 다른 창으로 복사', (tester) async {
await open(tester);
// 두 창 + 가운데 버튼 줄
expect(find.text('상위 폴더'), findsOneWidget);
expect(find.text('doc.txt'), findsOneWidget);
// 폴더를 누르면 그 자리에서 펼친다
await act(tester, () => tester.tap(find.text('sub')));
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
expect(find.text('inner.txt'), findsOneWidget);
// 파일 (동영상 아님) 을 누르면 기본 앱으로
await act(tester, () => tester.tap(find.text('doc.txt')));
await settle(tester, () => shell.opened.isNotEmpty);
expect(shell.opened.last, (p.join(left, 'doc.txt'), false));
// 길게 누르기 → 메뉴 → 다른 앱으로 열기 (고르기 창)
await tester.longPress(find.text('doc.txt'));
await tester.pumpAndSettle();
expect(find.text('다른 창으로 복사'), findsOneWidget);
await act(tester, () => tester.tap(find.text('다른 앱으로 열기')));
await settle(tester, () => shell.opened.length == 2);
expect(shell.opened.last, (p.join(left, 'doc.txt'), true));
// doc.txt 를 고른 채 [복사] → 확인 → 오른쪽 창 폴더에 생긴다
await act(tester, () => tester.tap(find.text('doc.txt')));
await tester.pump();
await act(tester, () => tester.tap(find.text('복사').first));
await tester.pumpAndSettle();
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
await settle(tester, () => File(p.join(right, 'doc.txt')).existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty);
expect(File(p.join(right, 'doc.txt')).readAsStringSync(), 'hello');
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
});
testWidgets('여러 개 표시 → 삭제 · 새 폴더 · 이름 변경', (tester) async {
await open(tester);
// 동그라미로 doc.txt · sub 표시 (반드시 이름으로 찾은 줄의 버튼만: 목록에는 시험 폴더 위쪽의 실제 폴더도 보인다)
await act(tester, () => tester.tap(markOf('sub')));
await act(tester, () => tester.tap(markOf('doc.txt')));
expect(find.text('2개 표시함'), findsOneWidget);
await act(tester, () => tester.tap(find.text('삭제').first));
await tester.pumpAndSettle();
// 지울 항목 이름이 확인 창에 보인다
expect(find.textContaining('2개 항목을 지울까요?'), findsOneWidget);
expect(find.textContaining('· sub'), findsOneWidget);
expect(find.textContaining('· doc.txt'), findsOneWidget);
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '삭제')));
await settle(tester, () => find.text('doc.txt').evaluate().isEmpty);
expect(Directory(left).listSync(), isEmpty);
// 새 폴더 → 이름 변경
await act(tester, () => tester.tap(find.text('새 폴더').first));
await tester.pumpAndSettle();
await act(tester, () => tester.enterText(find.byType(TextField), 'films'));
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
await settle(tester, () => find.text('films').evaluate().isNotEmpty);
expect(Directory(p.join(left, 'films')).existsSync(), isTrue);
await act(tester, () => tester.tap(find.text('이름 변경').first));
await tester.pumpAndSettle();
await act(tester, () => tester.enterText(find.byType(TextField), 'movies'));
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
await settle(tester, () => find.text('movies').evaluate().isNotEmpty);
expect(Directory(p.join(left, 'movies')).existsSync(), isTrue);
});
testWidgets('창 배치: 한 창 · 버튼 줄 숨김, 버튼 구성: 버튼 빼기 → 설정에 저장', (tester) async {
await open(tester);
await tester.tap(find.text('창 배치').first);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(ChoiceChip, '한 창'));
await tester.pump();
await tester.tap(find.widgetWithText(ChoiceChip, '숨김'));
await tester.pump();
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
await tester.pumpAndSettle();
expect([c.settings.explorerLayout, c.settings.explorerToolbar], ['single', 'hidden']);
expect(find.text('상위 폴더'), findsNothing); // 버튼 줄 숨김
expect(find.text('doc.txt'), findsOneWidget); // 한 창 (왼쪽만)
// ⋮ 메뉴에서 버튼 구성: "찾기" 빼기
await c.updateSettings((s) => s.explorerToolbar = 'middle');
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('창 배치 · 버튼 구성'));
await tester.pumpAndSettle();
await tester.tap(find.text('버튼 구성').last);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(CheckboxListTile, 'MKV 목록에 추가'));
await tester.pump();
await tester.tap(find.widgetWithText(FilledButton, '확인'));
await tester.pumpAndSettle();
expect(c.settings.explorerButtons, isNot(contains('addMkv')));
expect(c.settings.explorerButtons, contains('copy'));
expect(find.text('MKV 목록에 추가'), findsNothing);
// 설정 저장 · 읽기
final back = AppSettings.fromJson(c.settings.toJson());
expect([back.explorerLayout, back.explorerToolbar, back.explorerButtons],
[c.settings.explorerLayout, c.settings.explorerToolbar, c.settings.explorerButtons]);
});
}
/// [name] 줄의 "표시" 동그라미 버튼
Finder markOf(String name) => find.descendant(
of: find.ancestor(of: find.text(name), matching: find.byType(InkWell)).first,
matching: find.byTooltip('표시 (여러 개 고르기)'),
);
/// 누르기 등을 실제 비동기 구역에서 (그 때 시작한 파일 작업이 테스트의 가짜 시간에 묶이지 않게)
Future<void> act(WidgetTester tester, Future<void> Function() f) async {
await tester.runAsync(f);
await tester.pump();
}
/// 실제 파일 작업 (비동기 IO) 이 끝나기를 기다리며 화면을 갱신
Future<void> settle(WidgetTester tester, bool Function() done) async {
for (var i = 0; i < 100; i++) {
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
await tester.pump();
if (done()) break;
}
await tester.pump(const Duration(milliseconds: 300));
}

110
test/file_ops_test.dart Normal file
View File

@ -0,0 +1,110 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/core/file_ops.dart';
import 'package:path/path.dart' as p;
void main() {
late Directory tmp;
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_fileops_'));
tearDown(() => tmp.deleteSync(recursive: true));
File file(String rel, [String text = 'x']) =>
File(p.join(tmp.path, rel))
..parent.createSync(recursive: true)
..writeAsStringSync(text);
test('폴더 읽기: 숨은 항목 · 정렬 (폴더 먼저, 숫자는 크기로, 크기 · 거꾸로)', () async {
file('a/ep10.mkv', '1234567890');
file('a/ep2.mkv', '12');
file('a/.hidden');
Directory(p.join(tmp.path, 'a', 'zfolder')).createSync();
final dir = p.join(tmp.path, 'a');
final list = await listEntries(dir);
expect(list.map((e) => e.name), isNot(contains('.hidden')));
expect((await listEntries(dir, showHidden: true)).map((e) => e.name), contains('.hidden'));
expect(sortEntries(list, SortBy.name).map((e) => e.name), ['zfolder', 'ep2.mkv', 'ep10.mkv']);
// 거꾸로여도 폴더는 위
expect(sortEntries(list, SortBy.size, descending: true).map((e) => e.name), ['zfolder', 'ep10.mkv', 'ep2.mkv']);
final e = list.firstWhere((x) => x.name == 'ep10.mkv');
expect([e.ext, e.size, e.isDir], ['mkv', 10, false]);
});
test('복사: 폴더째 · 같은 이름은 "이름 (2)" · 진행률 · 자기 안으로는 안 됨', () async {
file('src/movie.mp4', 'abcdef');
file('src/sub/s.srt', 'sub');
file('dst/movie.mp4', 'old');
final dst = p.join(tmp.path, 'dst');
var last = 0;
final made = await FileOps().copy(
[p.join(tmp.path, 'src', 'movie.mp4'), p.join(tmp.path, 'src', 'sub')], dst,
onProgress: (_, done, total) {
last = done;
expect(total, 9);
});
expect(made.map(p.basename), ['movie (2).mp4', 'sub']);
expect(File(p.join(dst, 'movie (2).mp4')).readAsStringSync(), 'abcdef');
expect(File(p.join(dst, 'movie.mp4')).readAsStringSync(), 'old');
expect(File(p.join(dst, 'sub', 's.srt')).readAsStringSync(), 'sub');
expect(last, 9);
expect(() => FileOps().copy([p.join(tmp.path, 'src')], p.join(tmp.path, 'src', 'sub')),
throwsA(isA<FileSystemException>()));
});
test('이동 · 삭제 · 이름 바꾸기 · 새 폴더', () async {
final f = file('a/x.mkv', 'video');
final b = Directory(p.join(tmp.path, 'b'))..createSync();
final moved = await FileOps().move([f.path], b.path);
expect(File(moved.single).readAsStringSync(), 'video');
expect(f.existsSync(), isFalse);
// 이미 그 폴더에 있으면 그대로
expect(await FileOps().move([moved.single], b.path), [moved.single]);
final renamed = await FileOps.rename(moved.single, 'y.mkv');
expect(p.basename(renamed), 'y.mkv');
expect(() => FileOps.rename(renamed, 'bad/name'), throwsA(isA<FileSystemException>()));
file('b/z.mkv');
expect(() => FileOps.rename(renamed, 'z.mkv'), throwsA(isA<FileSystemException>()));
final folder = await FileOps.makeFolder(b.path, '새 폴더');
expect(Directory(folder).existsSync(), isTrue);
expect(() => FileOps.makeFolder(b.path, '새 폴더'), throwsA(isA<FileSystemException>()));
await FileOps().delete([b.path]);
expect(b.existsSync(), isFalse);
});
test('복사 취소: 만들던 파일을 지우고 FileOpCancelled', () async {
final big = file('big.bin', 'x' * (2 * 1024 * 1024));
final dst = Directory(p.join(tmp.path, 'dst'))..createSync();
final ops = FileOps();
final f = ops.copy([big.path], dst.path, onProgress: (_, done, _) {
if (done > 0) ops.cancel();
});
await expectLater(f, throwsA(isA<FileOpCancelled>()));
expect(dst.listSync(), isEmpty);
});
test('찾기: 하위 폴더까지 · 대소문자 무시 · * 사용', () async {
file('a/Movie One.mkv');
file('a/b/movie two.mp4');
file('a/b/notes.txt');
final all = await searchFiles(tmp.path, 'MOVIE').map((e) => e.name).toList();
expect(all.toSet(), {'Movie One.mkv', 'movie two.mp4'});
expect(await searchFiles(tmp.path, '*.txt').map((e) => e.name).toList(), ['notes.txt']);
expect(await searchFiles(tmp.path, '').toList(), isEmpty);
});
test('같은 경로 · 안쪽 판단, 크기 글자', () {
final a = p.join(tmp.path, 'a');
expect(samePath(a, '$a${Platform.pathSeparator}'), isTrue);
expect(isSameOrInside(p.join(a, 'b'), a), isTrue);
expect(isSameOrInside(a, p.join(a, 'b')), isFalse);
expect(formatSize(500), '500B');
expect(formatSize(1536), '2KB');
expect(formatSize(5 * 1024 * 1024), '5.0MB');
expect(formatSize(3 * 1024 * 1024 * 1024), '3.0GB');
});
}