WIP 78 파일 탐색기 일괄 이름 바꾸기: 이름 틀 [N] · 번호 [C] · 찾아 바꾸기 · 대소문자, 미리 보기 · 겹침 검사, 실패하면 되돌림 · [되돌리기] (검토 대기)
- 관리자 지시 (F7): 번호 [C] · 찾아 바꾸기 · 대소문자, 미리 보기, 되돌리기 (예: "Show S01E01…"). 데이터를 바꾸는 기능이라 미리 보기 · 확인 · 되돌리기를 둔다. - 여럿을 고르고 [이름 변경] (버튼 · F2 · 길게 누르기 아이콘 줄 "n개 이름 일괄 바꾸기") → 일괄 이름 바꾸기 창. 하나면 예전 이름 변경 그대로. - 77 에서는 여럿일 때 [이름 변경] 이 꺼져 있었다 → 이제 일괄 창을 연다 (77 시험의 기대값을 바꿈). - 규칙 (차례대로): 이름 틀 (확장자 뺀 이름, [N] 원래 이름 · [C] 번호 시작 · 간격 · 자릿수) → 찾아 바꾸기 (대소문자 무시 기본) → 대소문자 (그대로 · 모두 작게 · 모두 크게 · 단어 첫 글자만). 파일 확장자는 그대로 (대소문자 바꾸기만 따름). - 미리 보기: 원래 → 새 이름, 문제 (쓸 수 없는 이름 · 고른 것끼리 같은 이름 · 고르지 않은 것과 같은 이름, 대소문자 무시) 는 빨갛게 보이고 하나라도 있으면 [바꾸기] 를 막는다. 고른 것끼리 맞바꾸기 (a↔b) 는 된다. - 번호는 목록에 보이는 차례대로. - 바꾸기는 모두 임시 이름을 거쳐 두 단계로 (맞바꾸기 때문). 도중에 실패하면 이미 바꾼 것을 원래 이름으로 되돌리고 창에 이유 · 되돌리지 못한 것을 보인다. - 끝나면 아래 알림 "n개의 이름을 바꿨습니다" + [되돌리기] (10초). - 내 판단: 규칙은 창을 열 때마다 처음 값 ([N] · 1 · 1 · 2자리) - 설정에 저장하지 않음. - 시험: batch_rename_test (규칙 · 미리 보기 · 맞바꾸기 · 실패 되돌림 · 되돌리기 · 창), explorer_test 78 (고르기 → 바꾸기 → 되돌리기). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음6
parent
e39068cc9f
commit
9032e8c4d7
|
|
@ -36,6 +36,7 @@
|
|||
"\"{0}\" 을(를) 지울까요?": "Delete \"{0}\"?",
|
||||
"\"{0}\" 을(를) 지울까요? 되돌릴 수 없습니다.": "Delete \"{0}\"? This cannot be undone.",
|
||||
"\"{0}\" 을(를) 휴지통으로 보낼까요?": "Move \"{0}\" to the Recycle Bin?",
|
||||
"\"{0}\" 의 이름을 바꾸지 못해 멈췄습니다: {1}": "Stopped: couldn't rename \"{0}\": {1}",
|
||||
"\"{0}\" 이 비어 있어, 맞추면 대상 \"{1}\" 의 아래 항목이 모두 지워집니다 (되돌릴 수 없음). 원본을 일부러 비운 경우에만 누르세요.": "\"{0}\" is empty, so syncing deletes all the items below from the target \"{1}\" (cannot be undone). Press only if you emptied the source on purpose.",
|
||||
"\"{0}\" 이동": "Move \"{0}\"",
|
||||
"\"{0}\" 찾기 · {1}개": "Search \"{0}\" · {1}",
|
||||
|
|
@ -356,6 +357,7 @@
|
|||
"ZIP · CBZ 를 만화로 보기": "Read ZIP · CBZ as comics",
|
||||
"ZIP 옆 새 폴더 \"{0}\" 로": "To a new folder \"{0}\" next to the ZIP",
|
||||
"ZIP 이 있는 폴더로": "To the ZIP's folder",
|
||||
"[N] 원래 이름 · [C] 번호 예: Show S01E[C]": "[N] original name · [C] counter e.g. Show S01E[C]",
|
||||
"[서버 설정 고치기] 에서 비밀번호를 넣으면 이 기기의 안전 저장소에 남아 다음부터는 다시 넣지 않아도 됩니다.": "Enter the password in [Edit server settings]. It is kept in this device's secure storage, so you won't need to enter it again.",
|
||||
"[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.": "Check the user name and password in [Edit server settings].",
|
||||
"[허용] 을 누르면 Android 설정이 열립니다. JJ_MKVMaker 를 허용하고 돌아오면, 읽지 못했던 동영상을 저절로 다시 분석합니다.": "Tap [Allow] to open Android settings. After you allow JJ_MKVMaker and come back, videos that couldn't be read are analyzed again automatically.",
|
||||
|
|
@ -472,6 +474,7 @@
|
|||
"{0}개 보임": "{0} shown",
|
||||
"{0}개 삭제": "Delete {0} items",
|
||||
"{0}개 선택": "{0} selected",
|
||||
"{0}개 이름 일괄 바꾸기": "Rename {0} items",
|
||||
"{0}개 중 {1}개를 지우지 못했습니다": "Couldn't delete {1} of {0}",
|
||||
"{0}개 지우고 맞추기": "Delete {0} and sync",
|
||||
"{0}개 폴더를 lsync (실시간 동기화) 로 옮겼습니다.": "Moved {0} folders to lsync (live sync).",
|
||||
|
|
@ -498,6 +501,7 @@
|
|||
"{0}개를 찾아 여기서 멈췄습니다. 이름을 더 자세히 넣거나 아래 폴더로 들어가 찾으세요.": "Stopped after finding {0}. Enter a more specific name or search inside a subfolder.",
|
||||
"{0}개를 풀었습니다: {1}": "Extracted {0}: {1}",
|
||||
"{0}개에서 멈췄습니다": "Stopped at {0}",
|
||||
"{0}개의 이름을 바꿨습니다.": "Renamed {0} items.",
|
||||
"{0}배": "{0}×",
|
||||
"{0}배로 올려 새 파일로 저장합니다. 원본 · ZIP 은 그대로입니다. 화면을 닫아도 계속하며 작업 알림에 남은 장과 예상 시간이 보입니다.": "Upscales {0}× and saves new files. Originals and ZIPs stay unchanged. It keeps going after you close the screen, and the job notification shows pages left and the estimated time.",
|
||||
"{0}분": "{0} min",
|
||||
|
|
@ -549,12 +553,14 @@
|
|||
"가져온 즐겨찾기": "Imported bookmarks",
|
||||
"가져온 즐겨찾기 ({0})": "Imported bookmarks ({0})",
|
||||
"가져올 즐겨찾기가 없습니다.": "No bookmarks to import.",
|
||||
"간격": "Step",
|
||||
"감지한 언어: {0} (자동 감지)": "Detected language: {0} (auto-detect)",
|
||||
"감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "Detected language: {0} - if it is wrong, make them again with another language",
|
||||
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "Downloaded only if the detected language differs from the subtitle language {0}",
|
||||
"같은 시리즈 (번호만 다른 파일)": "Same series (files that differ only by number)",
|
||||
"같은 이름이 이미 있습니다": "An item with the same name already exists",
|
||||
"같은 이름이 이미 있습니다: {0}": "Already exists with the same name: {0}",
|
||||
"같은 이름이 이미 있음": "Name already exists",
|
||||
"같은 이름이 있는지 확인하는 중 …": "Checking for items with the same name …",
|
||||
"같은 이름이 있을 때": "When a file with the same name exists",
|
||||
"같은 이름이라 건너뜀: {0}": "Skipped (same name): {0}",
|
||||
|
|
@ -796,6 +802,7 @@
|
|||
"다른 폴더로 이동": "Move to another folder",
|
||||
"다른 프로그램이 쓰고 있거나 권한이 없을 수 있습니다. 나머지는 지웠습니다.": "Another program may be using them, or you may lack permission. The rest were deleted.",
|
||||
"다른 프로그램이 쓰고 있습니다": "Another program is using it",
|
||||
"다른 항목과 같은 이름": "Same name as another item",
|
||||
"다른 화면으로 가거나 앱이 뒤로 가면 Tor 화면의 페이지 · 연결은 바로 정리합니다. Tor 자체는 이 시간 동안 켜 두어 다시 들어오면 바로 쓸 수 있습니다.": "When you go to another screen or the app goes to the background, the Tor screen's pages and connections are cleared right away. Tor itself keeps running for this long so you can use it again immediately.",
|
||||
"다른 화면으로 가면 페이지 소리 멈춤": "Pause page sound when leaving the browser",
|
||||
"다시 만들기 대기로": "Back to waiting for remake",
|
||||
|
|
@ -841,6 +848,7 @@
|
|||
"다음 페이지": "Next page",
|
||||
"다음부터 묻지 않기 (환경 설정 > 실행 에서 바꿀 수 있음)": "Don't ask again (can be changed in Settings > Run)",
|
||||
"단계": "Steps",
|
||||
"단어 첫 글자만 크게 (Abc)": "Capitalize words (Abc)",
|
||||
"단축키 해제": "Unregister hotkey",
|
||||
"닫기": "Close",
|
||||
"닫힘 (실행 안 함)": "Closed (not run)",
|
||||
|
|
@ -864,6 +872,8 @@
|
|||
"대상으로 옮긴 파일을 원본에서 지웁니다 (이동)": "Files transferred to the target are deleted from the source (move)",
|
||||
"대상이 원본과 같거나 원본 안에 있습니다": "The target is the same as or inside the source",
|
||||
"대상이 원본과 같거나 원본 안에 있습니다 (맞출 때마다 원본이 바뀝니다). 다른 폴더를 고르세요.": "The target is the same as or inside the source (the source would change every sync). Choose another folder.",
|
||||
"대소문자 그대로": "Keep case",
|
||||
"대소문자 무시": "Ignore case",
|
||||
"더 보기": "More",
|
||||
"더블클릭 · 연결 프로그램으로 JJ_MKVMaker 를 골랐을 때 (오른쪽 클릭 메뉴는 그대로)": "When JJ_MKVMaker is chosen by double-click · default app (the right-click menu stays)",
|
||||
"더한 언어 (AI 자동 번역)": "Added language (AI auto-translated)",
|
||||
|
|
@ -914,6 +924,7 @@
|
|||
"되돌리기 계속": "Continue rollback",
|
||||
"되돌리기: AI 모델 {0} 을 복사해 두었습니다: {1}": "Rollback: copied AI models ({0}): {1}",
|
||||
"되돌리지 못함: {0}": "Could not restore: {0}",
|
||||
"되돌리지 못했습니다: {0}": "Couldn't undo: {0}",
|
||||
"되돌릴 수 없습니다.": "This can't be undone.",
|
||||
"되살리고 다시 시작": "Restore and restart",
|
||||
"되살리기": "Restore",
|
||||
|
|
@ -1018,11 +1029,13 @@
|
|||
"모두 받았습니다. 512×512 한 장에 폰은 약 1분, PC 는 장치에 따라 1 ~ 5분 걸립니다.": "All downloaded. A 512×512 image takes about 1 min on a phone and 1–5 min on a PC depending on the hardware.",
|
||||
"모두 선택": "Select all",
|
||||
"모두 시작 (멈춘 것 {0}개)": "Start all ({0} paused)",
|
||||
"모두 작게 (abc)": "lowercase (abc)",
|
||||
"모두 종료": "Quit all",
|
||||
"모두 지우기": "Clear all",
|
||||
"모두 초기화": "Reset all",
|
||||
"모두 취소 ({0}개)": "Cancel all ({0})",
|
||||
"모두 취소 (대기 {0}개 포함)": "Cancel all (including {0} waiting)",
|
||||
"모두 크게 (ABC)": "UPPERCASE (ABC)",
|
||||
"모두 풀기": "Extract all",
|
||||
"모든 작업 취소": "Cancel all jobs",
|
||||
"목": "Thu",
|
||||
|
|
@ -1044,6 +1057,7 @@
|
|||
"문서 미리보기": "Document preview",
|
||||
"문서 미리보기 컴포넌트를 먼저 설치하세요": "Install the document preview component first",
|
||||
"문자셋 경고": "Charset warning",
|
||||
"문제 {0}개 - 고치기 전에는 바꾸지 않습니다": "{0} problems - nothing is renamed until they're fixed",
|
||||
"묻기": "Ask",
|
||||
"물어보지 않기": "Never ask",
|
||||
"미리 볼 수 없습니다: {0}": "Cannot preview: {0}",
|
||||
|
|
@ -1052,9 +1066,12 @@
|
|||
"미지정": "Unspecified",
|
||||
"바꾸거나 끄려면 지금 마스터 비밀번호 (또는 최상 비밀번호) 를 넣으세요.": "To change or turn it off, enter the current master password (or the top password).",
|
||||
"바꾸기": "Change",
|
||||
"바꾸기 ({0}개)": "Rename ({0})",
|
||||
"바꾸는 정도 {0}%": "Change amount {0}%",
|
||||
"바꾸면 바로 저장됩니다": "Saved automatically",
|
||||
"바꾼 것이 없습니다 (원본 그대로)": "Nothing changed (original as is)",
|
||||
"바꿀 글 (비우면 지움)": "Replace with (empty = remove)",
|
||||
"바뀌는 것 {0}개": "{0} will change",
|
||||
"바뀌는 것 없음 (양쪽이 같습니다)": "Nothing to change (both sides are the same)",
|
||||
"바뀐 것을 살펴보는 간격": "Interval for checking changes",
|
||||
"바뀐 날": "Modified",
|
||||
|
|
@ -1140,6 +1157,7 @@
|
|||
"번역 시작": "Start translation",
|
||||
"번역 응답의 개수가 다릅니다 ({0} / {1})": "The number of translation results does not match ({0} / {1})",
|
||||
"번역할 언어 (jj_mkv\\파일명_언어코드.srt 로 저장 후 MKV 에 추가)": "Languages to translate to (saved as jj_mkv\\filename_languagecode.srt and added to the MKV)",
|
||||
"번호 시작": "Start at",
|
||||
"범위 밖 값: {0} ({1}~{2})": "Value out of range: {0} ({1}~{2})",
|
||||
"베트남어": "Vietnamese",
|
||||
"벵골어": "Bengali",
|
||||
|
|
@ -1396,6 +1414,7 @@
|
|||
"실행해 보지 못함 ({0})": "Could not run it ({0})",
|
||||
"싱크": "Sync",
|
||||
"쓰레드 확인 (AI · 변환 작업이 멈출 때까지)": "Thread check (until AI · conversion jobs stop)",
|
||||
"쓸 수 없는 이름": "Invalid name",
|
||||
"쓸 수 없는 이름입니다": "This name cannot be used",
|
||||
"아랍어": "Arabic",
|
||||
"아래 프로그램이 없어 일부 기능을 쓸 수 없습니다. 공식 배포처에서 내려받아 프로그램 폴더에 설치할까요?": "Some features are unavailable because the programs below are missing. Download them from the official sources and install them in the program folder?",
|
||||
|
|
@ -1559,6 +1578,7 @@
|
|||
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "Too many requests. Try again in a moment.",
|
||||
"우르두어": "Urdu",
|
||||
"우크라이나어": "Ukrainian",
|
||||
"원래 이름으로 되돌리지 못한 것: {0}": "Couldn't restore the original names of: {0}",
|
||||
"원래 자리에 같은 이름이 이미 있습니다": "An item with the same name already exists in the original place",
|
||||
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "The original folder no longer exists, so it was not restored (restore it from the Recycle Bin yourself)",
|
||||
"원문": "Full text",
|
||||
|
|
@ -1743,6 +1763,8 @@
|
|||
"이름 바꾸기 (예: 이름 (2))": "Keep both (e.g. name (2))",
|
||||
"이름 바꾸기 · 이동": "Rename · move",
|
||||
"이름 변경": "Rename",
|
||||
"이름 일괄 바꾸기 · {0}개": "Batch rename · {0} items",
|
||||
"이름 틀 (확장자는 그대로)": "Name pattern (extension kept)",
|
||||
"이름을 바꿀 수 없습니다: {0}": "Cannot rename: {0}",
|
||||
"이름을 바꿀 항목 하나를 고르세요.": "Choose one item to rename.",
|
||||
"이름을 바꿉니다": "renames it",
|
||||
|
|
@ -1751,6 +1773,7 @@
|
|||
"이미 동영상 목록에 있습니다 ({0}개).": "Already in the video list ({0}).",
|
||||
"이미 만든 MKV 가 있습니다 ({0}개)": "Some MKVs already exist ({0})",
|
||||
"이미 목록에 있는 동영상입니다 ({0}개).": "These videos are already in the list ({0}).",
|
||||
"이미 바꾼 것은 모두 원래 이름으로 되돌렸습니다.": "Everything already renamed was changed back.",
|
||||
"이미 받는 중이거나 받을 수 없는 주소입니다.": "Already downloading or a URL that cannot be downloaded.",
|
||||
"이미 실행 중인 창이 응답하지 않습니다.\n\n[예] 그래도 열기 - 같은 데이터 위에 창이 두 개가 되어 설정 · 목록이 서로 덮어쓸 수 있습니다.\n[아니요] 닫기 - 응답하지 않는 창을 작업 관리자에서 끝낸 뒤 다시 실행하세요.": "A window that is already running is not responding.\n\n[Yes] Open anyway - two windows will share the same data, and settings or lists may overwrite each other.\n[No] Close - end the unresponsive window in Task Manager, then run the app again.",
|
||||
"이미 이 폴더에 있습니다": "Already in this folder",
|
||||
|
|
@ -1803,6 +1826,7 @@
|
|||
"자동: 그래픽카드 (GPU) 가 있으면 사진용, CPU 뿐이면 가볍고 빠른 만화용. 원본은 덮어쓰지 않고 새 파일로 저장합니다": "Auto: the photo model with a graphics card (GPU), the light and fast comic model with CPU only. Originals are never overwritten; results are saved as new files",
|
||||
"자동: 잰 결과 {0} 가 가장 빠릅니다": "Auto: measured {0} as the fastest",
|
||||
"자동: 처음 그릴 때 짧게 재서 가장 빠른 장치를 고릅니다": "Auto: a short test on the first drawing picks the fastest processor",
|
||||
"자릿수": "Digits",
|
||||
"자막": "Subtitles",
|
||||
"자막 (AI · 인터넷)": "Subtitles (AI · online)",
|
||||
"자막 (S)": "Subtitles (S)",
|
||||
|
|
@ -2074,6 +2098,7 @@
|
|||
"찾는 즐겨찾기가 없습니다.": "No matching bookmarks.",
|
||||
"찾은 것이 없습니다.": "Nothing found.",
|
||||
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "No subtitles found. Try the original English title or add languages.",
|
||||
"찾을 글": "Find",
|
||||
"찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "Not found (already deleted or moved)",
|
||||
"채도": "Saturation",
|
||||
"채우기 (가운데 잘라냄)": "Fill (crop the center)",
|
||||
|
|
@ -2236,6 +2261,7 @@
|
|||
"폴더는 공유할 수 없습니다. 파일을 고르세요.": "Folders can't be shared. Choose files.",
|
||||
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "Folders are shown, but the videos and files inside need this permission. Turn on JJ_MKVMaker on the permission screen and come back.",
|
||||
"폴더를 만들 수 없습니다: {0}": "Cannot create the folder: {0}",
|
||||
"폴더를 읽는 중…": "Reading folders…",
|
||||
"폴더를 자기 안으로 복사 · 이동할 수 없습니다": "Cannot copy or move a folder into itself",
|
||||
"폴더를 자기 안으로 복사할 수 없습니다": "Cannot copy a folder into itself",
|
||||
"폴더를 자기 안으로 옮길 수 없습니다": "Cannot move a folder into itself",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
"\"{0}\" 을(를) 지울까요?": "「{0}」を削除しますか?",
|
||||
"\"{0}\" 을(를) 지울까요? 되돌릴 수 없습니다.": "「{0}」を削除しますか? 元に戻せません。",
|
||||
"\"{0}\" 을(를) 휴지통으로 보낼까요?": "「{0}」をごみ箱に移動しますか?",
|
||||
"\"{0}\" 의 이름을 바꾸지 못해 멈췄습니다: {1}": "「{0}」の名前を変更できず中止しました: {1}",
|
||||
"\"{0}\" 이 비어 있어, 맞추면 대상 \"{1}\" 의 아래 항목이 모두 지워집니다 (되돌릴 수 없음). 원본을 일부러 비운 경우에만 누르세요.": "「{0}」が空のため、同期すると対象「{1}」の以下の項目がすべて削除されます (元に戻せません)。元フォルダーを意図的に空にした場合のみ押してください。",
|
||||
"\"{0}\" 이동": "「{0}」を移動",
|
||||
"\"{0}\" 찾기 · {1}개": "「{0}」を検索 · {1}件",
|
||||
|
|
@ -356,6 +357,7 @@
|
|||
"ZIP · CBZ 를 만화로 보기": "ZIP・CBZ をマンガとして表示",
|
||||
"ZIP 옆 새 폴더 \"{0}\" 로": "ZIP の隣の新しいフォルダー「{0}」へ",
|
||||
"ZIP 이 있는 폴더로": "ZIP のあるフォルダーへ",
|
||||
"[N] 원래 이름 · [C] 번호 예: Show S01E[C]": "[N] 元の名前 · [C] 番号 例: Show S01E[C]",
|
||||
"[서버 설정 고치기] 에서 비밀번호를 넣으면 이 기기의 안전 저장소에 남아 다음부터는 다시 넣지 않아도 됩니다.": "[サーバー設定を修正] でパスワードを入力すると、この端末の安全な保存領域に残り、次回からは入力不要です。",
|
||||
"[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.": "[サーバー設定を修正] でユーザー名・パスワードを確認してください。",
|
||||
"[허용] 을 누르면 Android 설정이 열립니다. JJ_MKVMaker 를 허용하고 돌아오면, 읽지 못했던 동영상을 저절로 다시 분석합니다.": "[許可] を押すと Android の設定が開きます。JJ_MKVMaker を許可して戻ると、読み取れなかった動画を自動的に再分析します。",
|
||||
|
|
@ -472,6 +474,7 @@
|
|||
"{0}개 보임": "{0} 件表示",
|
||||
"{0}개 삭제": "{0} 件を削除",
|
||||
"{0}개 선택": "{0}個選択",
|
||||
"{0}개 이름 일괄 바꾸기": "{0} 件の名前を一括変更",
|
||||
"{0}개 중 {1}개를 지우지 못했습니다": "{0} 個中 {1} 個を削除できませんでした",
|
||||
"{0}개 지우고 맞추기": "{0} 個を削除して同期",
|
||||
"{0}개 폴더를 lsync (실시간 동기화) 로 옮겼습니다.": "{0} 個のフォルダーを lsync (リアルタイム同期) へ移しました。",
|
||||
|
|
@ -498,6 +501,7 @@
|
|||
"{0}개를 찾아 여기서 멈췄습니다. 이름을 더 자세히 넣거나 아래 폴더로 들어가 찾으세요.": "{0} 件見つけたところで止めました。名前をより詳しく入力するか、下のフォルダーに入って探してください。",
|
||||
"{0}개를 풀었습니다: {1}": "{0} 個を展開しました: {1}",
|
||||
"{0}개에서 멈췄습니다": "{0} 件で停止しました",
|
||||
"{0}개의 이름을 바꿨습니다.": "{0} 件の名前を変更しました。",
|
||||
"{0}배": "{0} 倍",
|
||||
"{0}배로 올려 새 파일로 저장합니다. 원본 · ZIP 은 그대로입니다. 화면을 닫아도 계속하며 작업 알림에 남은 장과 예상 시간이 보입니다.": "{0} 倍に高解像度化して新しいファイルで保存します。元のファイル・ZIP はそのままです。画面を閉じても続き、作業通知に残りの枚数と予想時間が表示されます。",
|
||||
"{0}분": "{0} 分",
|
||||
|
|
@ -549,12 +553,14 @@
|
|||
"가져온 즐겨찾기": "取り込んだブックマーク",
|
||||
"가져온 즐겨찾기 ({0})": "取り込んだブックマーク({0})",
|
||||
"가져올 즐겨찾기가 없습니다.": "取り込むブックマークがありません。",
|
||||
"간격": "間隔",
|
||||
"감지한 언어: {0} (자동 감지)": "検出した言語: {0} (自動検出)",
|
||||
"감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "検出した言語: {0} - 違っていれば別の言語で作り直してください",
|
||||
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "検出した言語が作成する言語と異なる場合のみダウンロード {0}",
|
||||
"같은 시리즈 (번호만 다른 파일)": "同じシリーズ(番号だけ違うファイル)",
|
||||
"같은 이름이 이미 있습니다": "同じ名前のものが既にあります",
|
||||
"같은 이름이 이미 있습니다: {0}": "同じ名前が既にあります: {0}",
|
||||
"같은 이름이 이미 있음": "同じ名前が既にあります",
|
||||
"같은 이름이 있는지 확인하는 중 …": "同じ名前があるか確認中 …",
|
||||
"같은 이름이 있을 때": "同じ名前があるとき",
|
||||
"같은 이름이라 건너뜀: {0}": "同じ名前のためスキップ: {0}",
|
||||
|
|
@ -796,6 +802,7 @@
|
|||
"다른 폴더로 이동": "他のフォルダーに移動",
|
||||
"다른 프로그램이 쓰고 있거나 권한이 없을 수 있습니다. 나머지는 지웠습니다.": "他のプログラムが使用中か、権限がない可能性があります。残りは削除しました。",
|
||||
"다른 프로그램이 쓰고 있습니다": "他のプログラムが使用中です",
|
||||
"다른 항목과 같은 이름": "他の項目と同じ名前",
|
||||
"다른 화면으로 가거나 앱이 뒤로 가면 Tor 화면의 페이지 · 연결은 바로 정리합니다. Tor 자체는 이 시간 동안 켜 두어 다시 들어오면 바로 쓸 수 있습니다.": "別の画面に移るかアプリがバックグラウンドに回ると、Tor 画面のページ · 接続はすぐに片付けます。Tor 自体はこの時間だけ動かしておき、戻ったらすぐ使えます。",
|
||||
"다른 화면으로 가면 페이지 소리 멈춤": "他の画面に移るとページの音を止める",
|
||||
"다시 만들기 대기로": "作り直し待ちに戻す",
|
||||
|
|
@ -841,6 +848,7 @@
|
|||
"다음 페이지": "次のページ",
|
||||
"다음부터 묻지 않기 (환경 설정 > 실행 에서 바꿀 수 있음)": "次回から確認しない(環境設定 > 実行 で変更できます)",
|
||||
"단계": "ステップ",
|
||||
"단어 첫 글자만 크게 (Abc)": "単語の先頭だけ大文字 (Abc)",
|
||||
"단축키 해제": "ショートカット解除",
|
||||
"닫기": "閉じる",
|
||||
"닫힘 (실행 안 함)": "閉じた (実行しない)",
|
||||
|
|
@ -864,6 +872,8 @@
|
|||
"대상으로 옮긴 파일을 원본에서 지웁니다 (이동)": "対象へ移したファイルを元から削除します (移動)",
|
||||
"대상이 원본과 같거나 원본 안에 있습니다": "対象が元と同じか、元の中にあります",
|
||||
"대상이 원본과 같거나 원본 안에 있습니다 (맞출 때마다 원본이 바뀝니다). 다른 폴더를 고르세요.": "対象が元と同じか元の中にあります (同期するたびに元が変わります)。別のフォルダーを選んでください。",
|
||||
"대소문자 그대로": "大文字・小文字はそのまま",
|
||||
"대소문자 무시": "大文字・小文字を区別しない",
|
||||
"더 보기": "その他",
|
||||
"더블클릭 · 연결 프로그램으로 JJ_MKVMaker 를 골랐을 때 (오른쪽 클릭 메뉴는 그대로)": "ダブルクリック・既定のアプリで JJ_MKVMaker を選んだとき(右クリックメニューはそのまま)",
|
||||
"더한 언어 (AI 자동 번역)": "追加した言語(AI 自動翻訳)",
|
||||
|
|
@ -914,6 +924,7 @@
|
|||
"되돌리기 계속": "戻す操作を続ける",
|
||||
"되돌리기: AI 모델 {0} 을 복사해 두었습니다: {1}": "戻す: AI モデル {0} をコピーしました: {1}",
|
||||
"되돌리지 못함: {0}": "元に戻せません: {0}",
|
||||
"되돌리지 못했습니다: {0}": "元に戻せませんでした: {0}",
|
||||
"되돌릴 수 없습니다.": "元に戻せません。",
|
||||
"되살리고 다시 시작": "復元して再起動",
|
||||
"되살리기": "元に戻す",
|
||||
|
|
@ -1018,11 +1029,13 @@
|
|||
"모두 받았습니다. 512×512 한 장에 폰은 약 1분, PC 는 장치에 따라 1 ~ 5분 걸립니다.": "すべてダウンロード済みです。512×512 1 枚にスマホで約 1 分、PC では装置により 1〜5 分かかります。",
|
||||
"모두 선택": "すべて選択",
|
||||
"모두 시작 (멈춘 것 {0}개)": "すべて開始 (停止中 {0} 件)",
|
||||
"모두 작게 (abc)": "すべて小文字 (abc)",
|
||||
"모두 종료": "すべて終了",
|
||||
"모두 지우기": "すべて削除",
|
||||
"모두 초기화": "すべてリセット",
|
||||
"모두 취소 ({0}개)": "すべてキャンセル ({0} 件)",
|
||||
"모두 취소 (대기 {0}개 포함)": "すべてキャンセル (待機 {0} 件を含む)",
|
||||
"모두 크게 (ABC)": "すべて大文字 (ABC)",
|
||||
"모두 풀기": "すべて展開",
|
||||
"모든 작업 취소": "すべての作業をキャンセル",
|
||||
"목": "木",
|
||||
|
|
@ -1044,6 +1057,7 @@
|
|||
"문서 미리보기": "文書プレビュー",
|
||||
"문서 미리보기 컴포넌트를 먼저 설치하세요": "先にドキュメントプレビューのコンポーネントをインストールしてください",
|
||||
"문자셋 경고": "文字コードの警告",
|
||||
"문제 {0}개 - 고치기 전에는 바꾸지 않습니다": "問題 {0} 件 - 直すまで変更しません",
|
||||
"묻기": "確認する",
|
||||
"물어보지 않기": "尋ねない",
|
||||
"미리 볼 수 없습니다: {0}": "プレビューできません: {0}",
|
||||
|
|
@ -1052,9 +1066,12 @@
|
|||
"미지정": "未指定",
|
||||
"바꾸거나 끄려면 지금 마스터 비밀번호 (또는 최상 비밀번호) 를 넣으세요.": "変更・オフにするには現在のマスターパスワード (または最上位パスワード) を入力してください。",
|
||||
"바꾸기": "変更",
|
||||
"바꾸기 ({0}개)": "変更 ({0} 件)",
|
||||
"바꾸는 정도 {0}%": "変える度合い {0}%",
|
||||
"바꾸면 바로 저장됩니다": "変更するとすぐ保存されます",
|
||||
"바꾼 것이 없습니다 (원본 그대로)": "変更なし(元のまま)",
|
||||
"바꿀 글 (비우면 지움)": "置き換える文字 (空なら削除)",
|
||||
"바뀌는 것 {0}개": "変更されるもの {0} 件",
|
||||
"바뀌는 것 없음 (양쪽이 같습니다)": "変更なし (両方同じです)",
|
||||
"바뀐 것을 살펴보는 간격": "変更を確認する間隔",
|
||||
"바뀐 날": "更新日時",
|
||||
|
|
@ -1140,6 +1157,7 @@
|
|||
"번역 시작": "翻訳開始",
|
||||
"번역 응답의 개수가 다릅니다 ({0} / {1})": "翻訳結果の数が一致しません ({0} / {1})",
|
||||
"번역할 언어 (jj_mkv\\파일명_언어코드.srt 로 저장 후 MKV 에 추가)": "翻訳する言語(jj_mkv\\ファイル名_言語コード.srt に保存して MKV に追加)",
|
||||
"번호 시작": "開始番号",
|
||||
"범위 밖 값: {0} ({1}~{2})": "範囲外の値: {0} ({1}~{2})",
|
||||
"베트남어": "ベトナム語",
|
||||
"벵골어": "ベンガル語",
|
||||
|
|
@ -1396,6 +1414,7 @@
|
|||
"실행해 보지 못함 ({0})": "実行できませんでした ({0})",
|
||||
"싱크": "同期",
|
||||
"쓰레드 확인 (AI · 변환 작업이 멈출 때까지)": "スレッド確認(AI・変換作業が止まるまで)",
|
||||
"쓸 수 없는 이름": "使えない名前",
|
||||
"쓸 수 없는 이름입니다": "使用できない名前です",
|
||||
"아랍어": "アラビア語",
|
||||
"아래 프로그램이 없어 일부 기능을 쓸 수 없습니다. 공식 배포처에서 내려받아 프로그램 폴더에 설치할까요?": "以下のプログラムがないため一部の機能が使えません。公式配布元からダウンロードしてプログラムフォルダーにインストールしますか?",
|
||||
|
|
@ -1559,6 +1578,7 @@
|
|||
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "リクエストが多すぎます。しばらくしてからもう一度お試しください。",
|
||||
"우르두어": "ウルドゥー語",
|
||||
"우크라이나어": "ウクライナ語",
|
||||
"원래 이름으로 되돌리지 못한 것: {0}": "元の名前に戻せなかったもの: {0}",
|
||||
"원래 자리에 같은 이름이 이미 있습니다": "元の場所に同じ名前が既にあります",
|
||||
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "元のフォルダーがないため元に戻せませんでした (ごみ箱から直接復元してください)",
|
||||
"원문": "原文",
|
||||
|
|
@ -1743,6 +1763,8 @@
|
|||
"이름 바꾸기 (예: 이름 (2))": "名前を変える (例: 名前 (2))",
|
||||
"이름 바꾸기 · 이동": "名前の変更・移動",
|
||||
"이름 변경": "名前の変更",
|
||||
"이름 일괄 바꾸기 · {0}개": "名前の一括変更 · {0} 件",
|
||||
"이름 틀 (확장자는 그대로)": "名前の形式 (拡張子はそのまま)",
|
||||
"이름을 바꿀 수 없습니다: {0}": "名前を変更できません: {0}",
|
||||
"이름을 바꿀 항목 하나를 고르세요.": "名前を変更する項目を 1 つ選んでください。",
|
||||
"이름을 바꿉니다": "名前を変えます",
|
||||
|
|
@ -1751,6 +1773,7 @@
|
|||
"이미 동영상 목록에 있습니다 ({0}개).": "既に動画リストにあります({0}個)。",
|
||||
"이미 만든 MKV 가 있습니다 ({0}개)": "作成済みの MKV があります ({0}件)",
|
||||
"이미 목록에 있는 동영상입니다 ({0}개).": "既にリストにある動画です({0}個)。",
|
||||
"이미 바꾼 것은 모두 원래 이름으로 되돌렸습니다.": "変更済みのものはすべて元の名前に戻しました。",
|
||||
"이미 받는 중이거나 받을 수 없는 주소입니다.": "既にダウンロード中か、ダウンロードできない URL です。",
|
||||
"이미 실행 중인 창이 응답하지 않습니다.\n\n[예] 그래도 열기 - 같은 데이터 위에 창이 두 개가 되어 설정 · 목록이 서로 덮어쓸 수 있습니다.\n[아니요] 닫기 - 응답하지 않는 창을 작업 관리자에서 끝낸 뒤 다시 실행하세요.": "すでに実行中のウィンドウが応答しません。\n\n[はい] それでも開く - 同じデータの上にウィンドウが 2 つになり、設定 · 一覧が互いに上書きされることがあります。\n[いいえ] 閉じる - 応答しないウィンドウをタスク マネージャーで終了してから、もう一度実行してください。",
|
||||
"이미 이 폴더에 있습니다": "既にこのフォルダーにあります",
|
||||
|
|
@ -1803,6 +1826,7 @@
|
|||
"자동: 그래픽카드 (GPU) 가 있으면 사진용, CPU 뿐이면 가볍고 빠른 만화용. 원본은 덮어쓰지 않고 새 파일로 저장합니다": "自動: グラフィックカード (GPU) があれば写真用、CPU だけなら軽く速いマンガ用。元のファイルは上書きせず新しいファイルで保存します",
|
||||
"자동: 잰 결과 {0} 가 가장 빠릅니다": "自動: 計測の結果 {0} が最速です",
|
||||
"자동: 처음 그릴 때 짧게 재서 가장 빠른 장치를 고릅니다": "自動: 初回の描画時に短く計測して最速の装置を選びます",
|
||||
"자릿수": "桁数",
|
||||
"자막": "字幕",
|
||||
"자막 (AI · 인터넷)": "字幕(AI・インターネット)",
|
||||
"자막 (S)": "字幕(S)",
|
||||
|
|
@ -2074,6 +2098,7 @@
|
|||
"찾는 즐겨찾기가 없습니다.": "該当するブックマークがありません。",
|
||||
"찾은 것이 없습니다.": "見つかりませんでした。",
|
||||
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "字幕が見つかりませんでした。タイトルを英語の原題に変えるか、言語を追加してみてください。",
|
||||
"찾을 글": "検索する文字",
|
||||
"찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "見つかりません (すでに削除または移動されました)",
|
||||
"채도": "彩度",
|
||||
"채우기 (가운데 잘라냄)": "フィル(中央を切り抜き)",
|
||||
|
|
@ -2236,6 +2261,7 @@
|
|||
"폴더는 공유할 수 없습니다. 파일을 고르세요.": "フォルダーは共有できません。ファイルを選んでください。",
|
||||
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "フォルダーは見えても、中の動画・ファイルはこの権限が必要です。許可画面で JJ_MKVMaker をオンにして戻ってください。",
|
||||
"폴더를 만들 수 없습니다: {0}": "フォルダーを作成できません: {0}",
|
||||
"폴더를 읽는 중…": "フォルダーを読み込み中…",
|
||||
"폴더를 자기 안으로 복사 · 이동할 수 없습니다": "フォルダーを自分自身の中にコピー · 移動できません",
|
||||
"폴더를 자기 안으로 복사할 수 없습니다": "フォルダーを自分自身の中にコピーできません",
|
||||
"폴더를 자기 안으로 옮길 수 없습니다": "フォルダーを自分自身の中に移動できません",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
"\"{0}\" 을(를) 지울까요?": "要删除“{0}”吗?",
|
||||
"\"{0}\" 을(를) 지울까요? 되돌릴 수 없습니다.": "删除\"{0}\"吗? 此操作无法撤销。",
|
||||
"\"{0}\" 을(를) 휴지통으로 보낼까요?": "将“{0}”移到回收站吗?",
|
||||
"\"{0}\" 의 이름을 바꾸지 못해 멈췄습니다: {1}": "无法重命名“{0}”,已停止: {1}",
|
||||
"\"{0}\" 이 비어 있어, 맞추면 대상 \"{1}\" 의 아래 항목이 모두 지워집니다 (되돌릴 수 없음). 원본을 일부러 비운 경우에만 누르세요.": "“{0}”为空,同步后会删除目标“{1}”中的以下所有项目 (无法撤销)。仅在你有意清空源文件夹时按下。",
|
||||
"\"{0}\" 이동": "移动“{0}”",
|
||||
"\"{0}\" 찾기 · {1}개": "搜索\"{0}\" · {1}个",
|
||||
|
|
@ -356,6 +357,7 @@
|
|||
"ZIP · CBZ 를 만화로 보기": "将 ZIP · CBZ 作为漫画阅读",
|
||||
"ZIP 옆 새 폴더 \"{0}\" 로": "到 ZIP 旁的新文件夹“{0}”",
|
||||
"ZIP 이 있는 폴더로": "到 ZIP 所在文件夹",
|
||||
"[N] 원래 이름 · [C] 번호 예: Show S01E[C]": "[N] 原名称 · [C] 序号 例如: Show S01E[C]",
|
||||
"[서버 설정 고치기] 에서 비밀번호를 넣으면 이 기기의 안전 저장소에 남아 다음부터는 다시 넣지 않아도 됩니다.": "在 [修改服务器设置] 中输入密码后会保存在本设备的安全存储中,以后无需再次输入。",
|
||||
"[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.": "请在 [修改服务器设置] 中检查用户名和密码。",
|
||||
"[허용] 을 누르면 Android 설정이 열립니다. JJ_MKVMaker 를 허용하고 돌아오면, 읽지 못했던 동영상을 저절로 다시 분석합니다.": "点按 [允许] 将打开 Android 设置。允许 JJ_MKVMaker 后返回,无法读取的视频会自动重新分析。",
|
||||
|
|
@ -472,6 +474,7 @@
|
|||
"{0}개 보임": "显示 {0} 项",
|
||||
"{0}개 삭제": "删除 {0} 项",
|
||||
"{0}개 선택": "已选 {0} 个",
|
||||
"{0}개 이름 일괄 바꾸기": "批量重命名 {0} 项",
|
||||
"{0}개 중 {1}개를 지우지 못했습니다": "{0} 个中有 {1} 个无法删除",
|
||||
"{0}개 지우고 맞추기": "删除 {0} 个并同步",
|
||||
"{0}개 폴더를 lsync (실시간 동기화) 로 옮겼습니다.": "已将 {0} 个文件夹移到 lsync (实时同步)。",
|
||||
|
|
@ -498,6 +501,7 @@
|
|||
"{0}개를 찾아 여기서 멈췄습니다. 이름을 더 자세히 넣거나 아래 폴더로 들어가 찾으세요.": "找到 {0} 个后已停止。请输入更具体的名称或进入下级文件夹查找。",
|
||||
"{0}개를 풀었습니다: {1}": "已解压 {0} 个: {1}",
|
||||
"{0}개에서 멈췄습니다": "已在 {0} 个处暂停",
|
||||
"{0}개의 이름을 바꿨습니다.": "已重命名 {0} 项。",
|
||||
"{0}배": "{0} 倍",
|
||||
"{0}배로 올려 새 파일로 저장합니다. 원본 · ZIP 은 그대로입니다. 화면을 닫아도 계속하며 작업 알림에 남은 장과 예상 시간이 보입니다.": "提高到 {0} 倍并另存为新文件。原文件和 ZIP 保持不变。关闭画面后仍会继续,任务通知中会显示剩余页数和预计时间。",
|
||||
"{0}분": "{0} 分钟",
|
||||
|
|
@ -549,12 +553,14 @@
|
|||
"가져온 즐겨찾기": "导入的书签",
|
||||
"가져온 즐겨찾기 ({0})": "导入的书签({0})",
|
||||
"가져올 즐겨찾기가 없습니다.": "没有可导入的书签。",
|
||||
"간격": "间隔",
|
||||
"감지한 언어: {0} (자동 감지)": "检测到的语言:{0}(自动检测)",
|
||||
"감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "检测到的语言:{0} - 如有误,请用其他语言重新生成",
|
||||
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "仅当检测到的语言与要生成的语言不同时下载 {0}",
|
||||
"같은 시리즈 (번호만 다른 파일)": "同一系列(仅编号不同的文件)",
|
||||
"같은 이름이 이미 있습니다": "已存在同名项目",
|
||||
"같은 이름이 이미 있습니다: {0}": "已存在同名项目: {0}",
|
||||
"같은 이름이 이미 있음": "同名项已存在",
|
||||
"같은 이름이 있는지 확인하는 중 …": "正在检查是否有同名项目 …",
|
||||
"같은 이름이 있을 때": "存在同名项目时",
|
||||
"같은 이름이라 건너뜀: {0}": "因同名已跳过: {0}",
|
||||
|
|
@ -796,6 +802,7 @@
|
|||
"다른 폴더로 이동": "移动到其他文件夹",
|
||||
"다른 프로그램이 쓰고 있거나 권한이 없을 수 있습니다. 나머지는 지웠습니다.": "可能有其他程序正在使用或没有权限。其余已删除。",
|
||||
"다른 프로그램이 쓰고 있습니다": "其他程序正在使用",
|
||||
"다른 항목과 같은 이름": "与其他项同名",
|
||||
"다른 화면으로 가거나 앱이 뒤로 가면 Tor 화면의 페이지 · 연결은 바로 정리합니다. Tor 자체는 이 시간 동안 켜 두어 다시 들어오면 바로 쓸 수 있습니다.": "切换到其他页面或应用转到后台时,会立即清理 Tor 页面的网页和连接。Tor 本身会在这段时间内保持运行,返回后可立即使用。",
|
||||
"다른 화면으로 가면 페이지 소리 멈춤": "离开浏览器时暂停网页声音",
|
||||
"다시 만들기 대기로": "改回等待重新制作",
|
||||
|
|
@ -841,6 +848,7 @@
|
|||
"다음 페이지": "下一页",
|
||||
"다음부터 묻지 않기 (환경 설정 > 실행 에서 바꿀 수 있음)": "下次不再询问(可在 设置 > 运行 中更改)",
|
||||
"단계": "步数",
|
||||
"단어 첫 글자만 크게 (Abc)": "单词首字母大写 (Abc)",
|
||||
"단축키 해제": "注销快捷键",
|
||||
"닫기": "关闭",
|
||||
"닫힘 (실행 안 함)": "已关闭 (未执行)",
|
||||
|
|
@ -864,6 +872,8 @@
|
|||
"대상으로 옮긴 파일을 원본에서 지웁니다 (이동)": "已传到目标的文件从源中删除 (移动)",
|
||||
"대상이 원본과 같거나 원본 안에 있습니다": "目标与源相同或位于源内",
|
||||
"대상이 원본과 같거나 원본 안에 있습니다 (맞출 때마다 원본이 바뀝니다). 다른 폴더를 고르세요.": "目标与源相同或位于源内 (每次同步都会改变源)。请选择其他文件夹。",
|
||||
"대소문자 그대로": "保持大小写",
|
||||
"대소문자 무시": "忽略大小写",
|
||||
"더 보기": "更多",
|
||||
"더블클릭 · 연결 프로그램으로 JJ_MKVMaker 를 골랐을 때 (오른쪽 클릭 메뉴는 그대로)": "双击 · 默认打开方式选择 JJ_MKVMaker 时(右键菜单不变)",
|
||||
"더한 언어 (AI 자동 번역)": "已添加的语言(AI 自动翻译)",
|
||||
|
|
@ -914,6 +924,7 @@
|
|||
"되돌리기 계속": "继续回退",
|
||||
"되돌리기: AI 모델 {0} 을 복사해 두었습니다: {1}": "回退: 已复制 AI 模型 {0}: {1}",
|
||||
"되돌리지 못함: {0}": "无法恢复: {0}",
|
||||
"되돌리지 못했습니다: {0}": "无法撤销: {0}",
|
||||
"되돌릴 수 없습니다.": "无法撤销。",
|
||||
"되살리고 다시 시작": "恢复并重新启动",
|
||||
"되살리기": "恢复",
|
||||
|
|
@ -1018,11 +1029,13 @@
|
|||
"모두 받았습니다. 512×512 한 장에 폰은 약 1분, PC 는 장치에 따라 1 ~ 5분 걸립니다.": "已全部下载。生成一张 512×512 图片,手机约 1 分钟,电脑视硬件需 1~5 分钟。",
|
||||
"모두 선택": "全选",
|
||||
"모두 시작 (멈춘 것 {0}개)": "全部开始 (已暂停 {0} 个)",
|
||||
"모두 작게 (abc)": "全部小写 (abc)",
|
||||
"모두 종료": "全部退出",
|
||||
"모두 지우기": "全部清除",
|
||||
"모두 초기화": "全部重置",
|
||||
"모두 취소 ({0}개)": "全部取消({0} 个)",
|
||||
"모두 취소 (대기 {0}개 포함)": "全部取消 (包括等待中的 {0} 个)",
|
||||
"모두 크게 (ABC)": "全部大写 (ABC)",
|
||||
"모두 풀기": "全部解压",
|
||||
"모든 작업 취소": "取消所有任务",
|
||||
"목": "四",
|
||||
|
|
@ -1044,6 +1057,7 @@
|
|||
"문서 미리보기": "文档预览",
|
||||
"문서 미리보기 컴포넌트를 먼저 설치하세요": "请先安装文档预览组件",
|
||||
"문자셋 경고": "字符集警告",
|
||||
"문제 {0}개 - 고치기 전에는 바꾸지 않습니다": "{0} 个问题 - 修正前不会重命名",
|
||||
"묻기": "询问",
|
||||
"물어보지 않기": "不询问",
|
||||
"미리 볼 수 없습니다: {0}": "无法预览: {0}",
|
||||
|
|
@ -1052,9 +1066,12 @@
|
|||
"미지정": "未指定",
|
||||
"바꾸거나 끄려면 지금 마스터 비밀번호 (또는 최상 비밀번호) 를 넣으세요.": "如需更改或关闭,请输入当前主密码 (或最高密码)。",
|
||||
"바꾸기": "更改",
|
||||
"바꾸기 ({0}개)": "重命名 ({0} 个)",
|
||||
"바꾸는 정도 {0}%": "改变程度 {0}%",
|
||||
"바꾸면 바로 저장됩니다": "更改后立即保存",
|
||||
"바꾼 것이 없습니다 (원본 그대로)": "未做更改(保持原样)",
|
||||
"바꿀 글 (비우면 지움)": "替换为 (留空则删除)",
|
||||
"바뀌는 것 {0}개": "将更改 {0} 个",
|
||||
"바뀌는 것 없음 (양쪽이 같습니다)": "没有更改 (两边相同)",
|
||||
"바뀐 것을 살펴보는 간격": "检查更改的间隔",
|
||||
"바뀐 날": "修改时间",
|
||||
|
|
@ -1140,6 +1157,7 @@
|
|||
"번역 시작": "开始翻译",
|
||||
"번역 응답의 개수가 다릅니다 ({0} / {1})": "翻译结果的数量不一致({0} / {1})",
|
||||
"번역할 언어 (jj_mkv\\파일명_언어코드.srt 로 저장 후 MKV 에 추가)": "要翻译的语言(保存为 jj_mkv\\文件名_语言代码.srt 后加入 MKV)",
|
||||
"번호 시작": "起始序号",
|
||||
"범위 밖 값: {0} ({1}~{2})": "超出范围的值: {0}({1}~{2})",
|
||||
"베트남어": "越南语",
|
||||
"벵골어": "孟加拉语",
|
||||
|
|
@ -1396,6 +1414,7 @@
|
|||
"실행해 보지 못함 ({0})": "无法运行({0})",
|
||||
"싱크": "同步",
|
||||
"쓰레드 확인 (AI · 변환 작업이 멈출 때까지)": "线程检查(直到 AI · 转换任务停止)",
|
||||
"쓸 수 없는 이름": "无效名称",
|
||||
"쓸 수 없는 이름입니다": "不能使用此名称",
|
||||
"아랍어": "阿拉伯语",
|
||||
"아래 프로그램이 없어 일부 기능을 쓸 수 없습니다. 공식 배포처에서 내려받아 프로그램 폴더에 설치할까요?": "由于缺少以下程序,部分功能无法使用。要从官方来源下载并安装到程序文件夹吗?",
|
||||
|
|
@ -1559,6 +1578,7 @@
|
|||
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "请求过多。请稍后再试。",
|
||||
"우르두어": "乌尔都语",
|
||||
"우크라이나어": "乌克兰语",
|
||||
"원래 이름으로 되돌리지 못한 것: {0}": "未能恢复原名称: {0}",
|
||||
"원래 자리에 같은 이름이 이미 있습니다": "原位置已存在同名项目",
|
||||
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "原文件夹已不存在,未能恢复 (请在回收站中手动还原)",
|
||||
"원문": "原文",
|
||||
|
|
@ -1743,6 +1763,8 @@
|
|||
"이름 바꾸기 (예: 이름 (2))": "重命名 (例: 名称 (2))",
|
||||
"이름 바꾸기 · 이동": "重命名 · 移动",
|
||||
"이름 변경": "重命名",
|
||||
"이름 일괄 바꾸기 · {0}개": "批量重命名 · {0} 项",
|
||||
"이름 틀 (확장자는 그대로)": "名称模式 (保留扩展名)",
|
||||
"이름을 바꿀 수 없습니다: {0}": "无法重命名: {0}",
|
||||
"이름을 바꿀 항목 하나를 고르세요.": "请选择一个要重命名的项目。",
|
||||
"이름을 바꿉니다": "重命名",
|
||||
|
|
@ -1751,6 +1773,7 @@
|
|||
"이미 동영상 목록에 있습니다 ({0}개).": "已在视频列表中({0} 个)。",
|
||||
"이미 만든 MKV 가 있습니다 ({0}개)": "已有制作好的 MKV({0} 个)",
|
||||
"이미 목록에 있는 동영상입니다 ({0}개).": "这些视频已在列表中({0} 个)。",
|
||||
"이미 바꾼 것은 모두 원래 이름으로 되돌렸습니다.": "已重命名的项均已恢复原名称。",
|
||||
"이미 받는 중이거나 받을 수 없는 주소입니다.": "已在下载中,或该地址无法下载。",
|
||||
"이미 실행 중인 창이 응답하지 않습니다.\n\n[예] 그래도 열기 - 같은 데이터 위에 창이 두 개가 되어 설정 · 목록이 서로 덮어쓸 수 있습니다.\n[아니요] 닫기 - 응답하지 않는 창을 작업 관리자에서 끝낸 뒤 다시 실행하세요.": "已在运行的窗口没有响应。\n\n[是] 仍然打开 - 同一数据上将有两个窗口,设置和列表可能互相覆盖。\n[否] 关闭 - 请在任务管理器中结束无响应的窗口后重新运行。",
|
||||
"이미 이 폴더에 있습니다": "已在此文件夹中",
|
||||
|
|
@ -1803,6 +1826,7 @@
|
|||
"자동: 그래픽카드 (GPU) 가 있으면 사진용, CPU 뿐이면 가볍고 빠른 만화용. 원본은 덮어쓰지 않고 새 파일로 저장합니다": "自动: 有显卡 (GPU) 时用照片模型,仅有 CPU 时用轻量快速的漫画模型。不会覆盖原文件,另存为新文件",
|
||||
"자동: 잰 결과 {0} 가 가장 빠릅니다": "自动: 测得 {0} 最快",
|
||||
"자동: 처음 그릴 때 짧게 재서 가장 빠른 장치를 고릅니다": "自动: 首次绘制时短暂测试并选择最快的处理器",
|
||||
"자릿수": "位数",
|
||||
"자막": "字幕",
|
||||
"자막 (AI · 인터넷)": "字幕(AI · 网络)",
|
||||
"자막 (S)": "字幕(S)",
|
||||
|
|
@ -2074,6 +2098,7 @@
|
|||
"찾는 즐겨찾기가 없습니다.": "没有匹配的书签。",
|
||||
"찾은 것이 없습니다.": "未找到。",
|
||||
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "未找到字幕。请尝试改用英文原名或添加语言。",
|
||||
"찾을 글": "查找",
|
||||
"찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "找不到 (已被删除或移动)",
|
||||
"채도": "饱和度",
|
||||
"채우기 (가운데 잘라냄)": "填充(裁切中间)",
|
||||
|
|
@ -2236,6 +2261,7 @@
|
|||
"폴더는 공유할 수 없습니다. 파일을 고르세요.": "文件夹无法分享。请选择文件。",
|
||||
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "虽然能看到文件夹,但其中的视频和文件需要此权限。请在权限界面开启 JJ_MKVMaker 后返回。",
|
||||
"폴더를 만들 수 없습니다: {0}": "无法创建文件夹: {0}",
|
||||
"폴더를 읽는 중…": "正在读取文件夹…",
|
||||
"폴더를 자기 안으로 복사 · 이동할 수 없습니다": "不能将文件夹复制或移动到其自身内部",
|
||||
"폴더를 자기 안으로 복사할 수 없습니다": "不能将文件夹复制到其自身内部",
|
||||
"폴더를 자기 안으로 옮길 수 없습니다": "不能将文件夹移动到其自身内部",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../l10n/tr.dart';
|
||||
import 'file_ops.dart';
|
||||
import 'vfs.dart';
|
||||
|
||||
/// 78: 일괄 이름 바꾸기 규칙 (Total Commander · X-plore 참고).
|
||||
/// 차례: 이름 틀 ([N] 원래 이름 · [C] 번호) → 찾아 바꾸기 → 대소문자. 파일의 확장자는 그대로 둔다.
|
||||
class RenameRule {
|
||||
/// 이름 틀 (확장자 뺀 이름). [N] = 원래 이름, [C] = 번호. 예: "Show S01E[C]"
|
||||
final String template;
|
||||
|
||||
/// 번호: 시작 · 간격 · 자릿수 (2 → 01, 02 …)
|
||||
final int start, step, digits;
|
||||
|
||||
/// 찾아 바꾸기 (비우면 안 함)
|
||||
final String find, replace;
|
||||
final bool ignoreCase;
|
||||
|
||||
/// 'keep' · 'lower' · 'upper' · 'title' (단어 첫 글자만 크게)
|
||||
final String caseMode;
|
||||
|
||||
const RenameRule({
|
||||
this.template = '[N]',
|
||||
this.start = 1,
|
||||
this.step = 1,
|
||||
this.digits = 2,
|
||||
this.find = '',
|
||||
this.replace = '',
|
||||
this.ignoreCase = true,
|
||||
this.caseMode = 'keep',
|
||||
});
|
||||
|
||||
/// 바꾸는 것이 하나도 없는 규칙
|
||||
bool get isIdentity => template == '[N]' && find.isEmpty && caseMode == 'keep';
|
||||
|
||||
/// [name] (확장자 포함) 의 새 이름. [index] 는 0 부터 (번호 = start + index × step)
|
||||
String apply(String name, {required bool isDir, required int index}) {
|
||||
final ext = isDir ? '' : p.extension(name);
|
||||
final base = isDir || ext.isEmpty ? name : name.substring(0, name.length - ext.length);
|
||||
final n = start + index * step;
|
||||
final counter = n < 0 ? '-${(-n).toString().padLeft(digits, '0')}' : n.toString().padLeft(digits, '0');
|
||||
var out = template.replaceAll(RegExp(r'\[N\]', caseSensitive: false), base).replaceAll(RegExp(r'\[C\]', caseSensitive: false), counter);
|
||||
if (find.isNotEmpty) {
|
||||
out = out.replaceAll(RegExp(RegExp.escape(find), caseSensitive: !ignoreCase), replace);
|
||||
}
|
||||
out = switch (caseMode) {
|
||||
'lower' => out.toLowerCase(),
|
||||
'upper' => out.toUpperCase(),
|
||||
'title' => out.toLowerCase().replaceAllMapped(RegExp(r'(^|[\s._\-\[(])(\p{L})', unicode: true), (m) => '${m[1]}${m[2]!.toUpperCase()}'),
|
||||
_ => out,
|
||||
};
|
||||
// 확장자도 대소문자 바꾸기를 따른다 (찾아 바꾸기는 이름에만)
|
||||
final e = switch (caseMode) { 'lower' => ext.toLowerCase(), 'upper' => ext.toUpperCase(), _ => ext };
|
||||
return out.trim() + e;
|
||||
}
|
||||
}
|
||||
|
||||
/// 미리 보기 한 줄
|
||||
class RenameRow {
|
||||
final String path;
|
||||
final String newName;
|
||||
|
||||
/// 문제 (없으면 null): 쓸 수 없는 이름 · 겹침 · 이미 있음
|
||||
final String? problem;
|
||||
const RenameRow(this.path, this.newName, this.problem);
|
||||
|
||||
String get oldName => vBasename(path);
|
||||
bool get changed => newName != oldName;
|
||||
}
|
||||
|
||||
/// 미리 보기: [paths] 차례대로 번호를 매긴다. [siblings] 는 폴더마다 지금 있는 이름 (고르지 않은 것과 겹치는지 보려고)
|
||||
List<RenameRow> planRename(List<String> paths, RenameRule rule,
|
||||
{required bool Function(String path) isDir, required Map<String, Set<String>> siblings}) {
|
||||
bool same(String a, String b) => a.toLowerCase() == b.toLowerCase(); // Windows · 대부분의 NAS 는 대소문자를 가리지 않는다
|
||||
final names = [for (var i = 0; i < paths.length; i++) rule.apply(vBasename(paths[i]), isDir: isDir(paths[i]), index: i)];
|
||||
final rows = <RenameRow>[];
|
||||
for (var i = 0; i < paths.length; i++) {
|
||||
final dir = vDirname(paths[i]);
|
||||
final name = names[i];
|
||||
String? problem;
|
||||
if (name.isEmpty || name == '.' || name == '..' || name.contains(RegExp(r'[\\/:*?"<>|]'))) {
|
||||
problem = tr('쓸 수 없는 이름');
|
||||
} else if ([for (var k = 0; k < paths.length; k++) if (k != i && vDirname(paths[k]) == dir && same(names[k], name)) k].isNotEmpty) {
|
||||
problem = tr('다른 항목과 같은 이름');
|
||||
} else if (!same(name, vBasename(paths[i])) &&
|
||||
(siblings[dir] ?? const {}).any((s) => same(s, name) && !paths.any((x) => vDirname(x) == dir && same(vBasename(x), s)))) {
|
||||
// 고른 것끼리 맞바꾸는 것 (a→b, b→a) 은 괜찮다 - 고르지 않은 것과 겹치면 안 됨
|
||||
problem = tr('같은 이름이 이미 있음');
|
||||
}
|
||||
rows.add(RenameRow(paths[i], name, problem));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// 일괄 이름 바꾸기 실패 (바꾼 것은 되돌림)
|
||||
class BatchRenameException implements Exception {
|
||||
final String path;
|
||||
final Object error;
|
||||
|
||||
/// 되돌리지 못한 것 (있으면 알린다)
|
||||
final List<String> stuck;
|
||||
const BatchRenameException(this.path, this.error, this.stuck);
|
||||
@override
|
||||
String toString() => '$path: $error';
|
||||
}
|
||||
|
||||
/// 바꾸기: 먼저 모두 임시 이름으로, 그다음 새 이름으로 (맞바꾸기 a↔b 도 되게).
|
||||
/// 도중에 실패하면 이미 움직인 것을 원래 이름으로 되돌리고 (되돌리기도 임시 이름을 거쳐) [BatchRenameException].
|
||||
/// 돌려주는 값: (원래 경로, 새 경로) - 되돌리기에 쓴다
|
||||
Future<List<(String, String)>> runBatchRename(List<(String path, String newName)> jobs,
|
||||
{Future<String> Function(String path, String name)? renamer}) async {
|
||||
final ren = renamer ?? FileOps.rename;
|
||||
final todo = [for (final j in jobs) if (vBasename(j.$1) != j.$2) j];
|
||||
final stamp = DateTime.now().microsecondsSinceEpoch.toRadixString(36);
|
||||
// 항목마다 지금 있는 곳
|
||||
final now = [for (final j in todo) j.$1];
|
||||
|
||||
Future<Never> rollback(String at, Object e) async {
|
||||
final stuck = <String>[];
|
||||
final moved = [for (var i = 0; i < todo.length; i++) if (now[i] != todo[i].$1) i];
|
||||
// 되돌리기도 두 단계 (맞바꾼 이름끼리 부딪히지 않게)
|
||||
for (final i in moved) {
|
||||
try {
|
||||
now[i] = await ren(now[i], '~jjback_${stamp}_$i');
|
||||
} catch (_) {}
|
||||
}
|
||||
for (final i in moved) {
|
||||
try {
|
||||
now[i] = await ren(now[i], vBasename(todo[i].$1));
|
||||
} catch (_) {
|
||||
stuck.add(now[i]);
|
||||
}
|
||||
}
|
||||
throw BatchRenameException(at, e, stuck);
|
||||
}
|
||||
|
||||
for (var i = 0; i < todo.length; i++) {
|
||||
try {
|
||||
now[i] = await ren(now[i], '~jjren_${stamp}_$i');
|
||||
} catch (e) {
|
||||
await rollback(todo[i].$1, e);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < todo.length; i++) {
|
||||
try {
|
||||
now[i] = await ren(now[i], todo[i].$2);
|
||||
} catch (e) {
|
||||
await rollback(todo[i].$1, e);
|
||||
}
|
||||
}
|
||||
return [for (var i = 0; i < todo.length; i++) (todo[i].$1, now[i])];
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../core/batch_rename.dart';
|
||||
import '../core/vfs.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// 78: 일괄 이름 바꾸기 창 - 규칙을 넣으면 바로 미리 보기 (원래 → 새 이름, 문제는 빨갛게), [바꾸기] 뒤 결과.
|
||||
/// 돌려주는 값: 바꾼 것 (원래 경로, 새 경로) - 탐색기가 [되돌리기] 에 쓴다. 취소하면 null.
|
||||
/// 실패하면 이미 바꾼 것은 되돌리고 창에 이유를 보인다.
|
||||
Future<List<(String, String)>?> showBatchRename(BuildContext context, List<String> paths, {required bool Function(String) isDir}) =>
|
||||
showDialog<List<(String, String)>>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => _BatchRenameDialog(paths: paths, isDir: isDir),
|
||||
);
|
||||
|
||||
class _BatchRenameDialog extends StatefulWidget {
|
||||
final List<String> paths;
|
||||
final bool Function(String) isDir;
|
||||
const _BatchRenameDialog({required this.paths, required this.isDir});
|
||||
|
||||
@override
|
||||
State<_BatchRenameDialog> createState() => _BatchRenameDialogState();
|
||||
}
|
||||
|
||||
class _BatchRenameDialogState extends State<_BatchRenameDialog> {
|
||||
final _template = TextEditingController(text: '[N]');
|
||||
final _start = TextEditingController(text: '1');
|
||||
final _step = TextEditingController(text: '1');
|
||||
final _digits = TextEditingController(text: '2');
|
||||
final _find = TextEditingController();
|
||||
final _replace = TextEditingController();
|
||||
bool _ignoreCase = true;
|
||||
String _case = 'keep';
|
||||
|
||||
/// 폴더마다 지금 있는 이름 (읽는 중이면 비어 있음)
|
||||
final _siblings = <String, Set<String>>{};
|
||||
bool _loading = true;
|
||||
bool _running = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSiblings();
|
||||
}
|
||||
|
||||
Future<void> _loadSiblings() async {
|
||||
for (final dir in {for (final x in widget.paths) vDirname(x)}) {
|
||||
try {
|
||||
_siblings[dir] = {for (final e in await vList(dir)) vBasename(e.path)};
|
||||
} catch (_) {
|
||||
_siblings[dir] = {};
|
||||
}
|
||||
}
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final x in [_template, _start, _step, _digits, _find, _replace]) {
|
||||
x.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
RenameRule get _rule => RenameRule(
|
||||
template: _template.text.isEmpty ? '[N]' : _template.text,
|
||||
start: int.tryParse(_start.text.trim()) ?? 1,
|
||||
step: int.tryParse(_step.text.trim()) ?? 1,
|
||||
digits: (int.tryParse(_digits.text.trim()) ?? 1).clamp(1, 9),
|
||||
find: _find.text,
|
||||
replace: _replace.text,
|
||||
ignoreCase: _ignoreCase,
|
||||
caseMode: _case,
|
||||
);
|
||||
|
||||
void _insert(String tag) {
|
||||
final t = _template;
|
||||
final sel = t.selection.isValid ? t.selection : TextSelection.collapsed(offset: t.text.length);
|
||||
t.value = TextEditingValue(
|
||||
text: t.text.replaceRange(sel.start, sel.end, tag),
|
||||
selection: TextSelection.collapsed(offset: sel.start + tag.length),
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _run(List<RenameRow> rows) async {
|
||||
setState(() {
|
||||
_running = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final done = await runBatchRename([for (final r in rows) (r.path, r.newName)]);
|
||||
if (mounted) Navigator.pop(context, done);
|
||||
} on BatchRenameException catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_running = false;
|
||||
_error = [
|
||||
trf('"{0}" 의 이름을 바꾸지 못해 멈췄습니다: {1}', [vBasename(e.path), e.error]),
|
||||
if (e.stuck.isEmpty)
|
||||
tr('이미 바꾼 것은 모두 원래 이름으로 되돌렸습니다.')
|
||||
else
|
||||
trf('원래 이름으로 되돌리지 못한 것: {0}', [e.stuck.map(vBasename).join(', ')]),
|
||||
].join('\n');
|
||||
});
|
||||
// 되돌린 뒤의 실제 이름으로 다시 본다
|
||||
_siblings.clear();
|
||||
await _loadSiblings();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _num(TextEditingController c, String label) => SizedBox(
|
||||
width: 72,
|
||||
child: TextField(
|
||||
controller: c,
|
||||
keyboardType: const TextInputType.numberWithOptions(signed: true),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^-?\d{0,6}'))],
|
||||
decoration: InputDecoration(labelText: label, isDense: true),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rule = _rule;
|
||||
final rows = planRename(widget.paths, rule, isDir: widget.isDir, siblings: _siblings);
|
||||
final changed = rows.where((r) => r.changed).length;
|
||||
final bad = rows.where((r) => r.problem != null).length;
|
||||
final canRun = !_loading && !_running && bad == 0 && changed > 0;
|
||||
return AlertDialog(
|
||||
title: Text(trf('이름 일괄 바꾸기 · {0}개', [widget.paths.length])),
|
||||
content: SizedBox(
|
||||
width: 640,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
// 이름 틀 + [N] [C] 넣기
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _template,
|
||||
enabled: !_running,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr('이름 틀 (확장자는 그대로)'),
|
||||
helperText: tr('[N] 원래 이름 · [C] 번호 예: Show S01E[C]'),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
OutlinedButton(onPressed: _running ? null : () => _insert('[N]'), child: const Text('[N]')),
|
||||
const SizedBox(width: 4),
|
||||
OutlinedButton(onPressed: _running ? null : () => _insert('[C]'), child: const Text('[C]')),
|
||||
]),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(spacing: 10, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.end, children: [
|
||||
_num(_start, tr('번호 시작')),
|
||||
_num(_step, tr('간격')),
|
||||
_num(_digits, tr('자릿수')),
|
||||
DropdownButton<String>(
|
||||
value: _case,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'keep', child: Text(tr('대소문자 그대로'))),
|
||||
DropdownMenuItem(value: 'lower', child: Text(tr('모두 작게 (abc)'))),
|
||||
DropdownMenuItem(value: 'upper', child: Text(tr('모두 크게 (ABC)'))),
|
||||
DropdownMenuItem(value: 'title', child: Text(tr('단어 첫 글자만 크게 (Abc)'))),
|
||||
],
|
||||
onChanged: _running ? null : (v) => setState(() => _case = v!),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(spacing: 10, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
controller: _find,
|
||||
enabled: !_running,
|
||||
decoration: InputDecoration(labelText: tr('찾을 글'), isDense: true),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
controller: _replace,
|
||||
enabled: !_running,
|
||||
decoration: InputDecoration(labelText: tr('바꿀 글 (비우면 지움)'), isDense: true),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Checkbox(value: _ignoreCase, onChanged: _running ? null : (v) => setState(() => _ignoreCase = v!)),
|
||||
Text(tr('대소문자 무시')),
|
||||
]),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
_loading
|
||||
? tr('폴더를 읽는 중…')
|
||||
: bad > 0
|
||||
? trf('문제 {0}개 - 고치기 전에는 바꾸지 않습니다', [bad])
|
||||
: trf('바뀌는 것 {0}개', [changed]),
|
||||
style: TextStyle(fontSize: 12, color: bad > 0 ? JjColors.danger : JjColors.textDim),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 미리 보기
|
||||
Flexible(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 280),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: rows.length,
|
||||
itemBuilder: (_, i) {
|
||||
final r = rows[i];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Expanded(
|
||||
child: Text(r.oldName,
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 6), child: Text('→', style: TextStyle(fontSize: 12))),
|
||||
Expanded(
|
||||
child: Text(
|
||||
r.problem == null ? r.newName : '${r.newName} · ${r.problem}',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: r.problem != null ? JjColors.danger : (r.changed ? JjColors.text : JjColors.textDim),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(_error!, style: const TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: _running ? null : () => Navigator.pop(context), child: Text(tr('취소'))),
|
||||
FilledButton(
|
||||
onPressed: canRun ? () => _run(rows) : null,
|
||||
child: _running
|
||||
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: Text(trf('바꾸기 ({0}개)', [changed])),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,8 @@ import '../core/reader_sources.dart';
|
|||
import '../l10n/tr.dart';
|
||||
import 'explorer_bookmarks.dart';
|
||||
import 'explorer_search.dart';
|
||||
import 'batch_rename_dialog.dart';
|
||||
import '../core/batch_rename.dart';
|
||||
|
||||
/// 파일 탐색기 (X-plore 참고): 두 창 (트리 목록) + 가운데 기능 버튼 줄.
|
||||
///
|
||||
|
|
@ -1336,7 +1338,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
: <(String, IconData, String, String, bool)>[
|
||||
(dual ? 'copy' : 'clipCopy', Icons.copy_outlined, tr('복사'), dual ? tr('다른 창으로 복사') : tr('복사 (붙여넣기로)'), true),
|
||||
(dual ? 'move' : 'clipMove', Icons.drive_file_move_outline, tr('이동'), dual ? tr('다른 창으로 이동') : tr('이동 (붙여넣기로)'), true),
|
||||
('rename', Icons.drive_file_rename_outline, tr('이름 변경'), tr('이름 변경'), !many),
|
||||
// 78: 여럿이면 일괄 이름 바꾸기
|
||||
('rename', Icons.drive_file_rename_outline, tr('이름 변경'), many ? trf('{0}개 이름 일괄 바꾸기', [targets.length]) : tr('이름 변경'), true),
|
||||
('delete', Icons.delete_outline, tr('삭제'), many ? trf('{0}개 삭제', [targets.length]) : tr('삭제'), true),
|
||||
if (c.services.shell.canShare) ('share', Icons.share_outlined, tr('공유'), tr('공유 (다른 앱으로 보내기)'), targets.any((x) => !vIsDirSync(x))),
|
||||
];
|
||||
|
|
@ -1393,7 +1396,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
setState(() => _selecting = true);
|
||||
if (!_isMarked(pane, e.path)) await _toggleMark(pane, e);
|
||||
case 'rename':
|
||||
await _rename(pane, e.path);
|
||||
if (_isMarked(pane, e.path) && pane.marked.length > 1) {
|
||||
await _batchRename(pane, pane.marked.toList());
|
||||
} else {
|
||||
await _rename(pane, e.path);
|
||||
}
|
||||
case 'copy':
|
||||
await _transfer(pane, _isMarked(pane, e.path) ? pane.marked.toList() : [e.path], move: false);
|
||||
case 'move':
|
||||
|
|
@ -1561,10 +1568,12 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
}
|
||||
case ExplorerButton.rename:
|
||||
final t = _targets(pane);
|
||||
if (t.length != 1) {
|
||||
if (t.isEmpty) {
|
||||
_snack(tr('이름을 바꿀 항목 하나를 고르세요.'));
|
||||
return;
|
||||
}
|
||||
// 78: 여럿을 골랐으면 일괄 이름 바꾸기
|
||||
if (t.length > 1) return _batchRename(pane, t);
|
||||
await _rename(pane, t.first);
|
||||
case ExplorerButton.copy:
|
||||
await _transfer(pane, _targets(pane), move: false);
|
||||
|
|
@ -1659,6 +1668,50 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 78: 일괄 이름 바꾸기 - 번호는 목록에 보이는 차례대로. 끝나면 [되돌리기]
|
||||
Future<void> _batchRename(_Pane pane, List<String> paths) async {
|
||||
int at(String x) {
|
||||
final i = (pane.cache[vDirname(x)] ?? const <FileEntry>[]).indexWhere((e) => samePath(e.path, x));
|
||||
return i < 0 ? 1 << 30 : i;
|
||||
}
|
||||
|
||||
final ordered = [...paths]..sort((a, b) {
|
||||
final d = vDirname(a).compareTo(vDirname(b));
|
||||
return d != 0 ? d : at(a).compareTo(at(b));
|
||||
});
|
||||
final done = await showBatchRename(context, ordered, isDir: vIsDirSync);
|
||||
if (done == null || done.isEmpty || !mounted) return;
|
||||
await _afterBatchRename(pane, done);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(trf('{0}개의 이름을 바꿨습니다.', [done.length])),
|
||||
duration: const Duration(seconds: 10),
|
||||
action: SnackBarAction(
|
||||
label: tr('되돌리기'),
|
||||
onPressed: () async {
|
||||
try {
|
||||
final back = await runBatchRename([for (final (orig, now) in done) (now, vBasename(orig))]);
|
||||
if (mounted) await _afterBatchRename(pane, back);
|
||||
} on BatchRenameException catch (e) {
|
||||
_snack(trf('되돌리지 못했습니다: {0}', [e]));
|
||||
}
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
/// 바꾼 뒤: 목록을 다시 읽고, 표시 · 초점을 새 이름으로
|
||||
Future<void> _afterBatchRename(_Pane pane, List<(String, String)> moved) async {
|
||||
await _refreshAll({for (final (a, _) in moved) vDirname(a)});
|
||||
for (final x in _panes) {
|
||||
for (final (from, to) in moved) {
|
||||
if (x.marked.remove(from)) x.marked.add(to);
|
||||
if (x.focused != null && samePath(x.focused!, from)) x.focused = to;
|
||||
}
|
||||
x.changed();
|
||||
}
|
||||
}
|
||||
|
||||
/// Shift+Delete 로 불렀는지 (휴지통을 거치지 않고 지우기를 미리 골라 둔다 - 65)
|
||||
bool _shiftDelete = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/core/batch_rename.dart';
|
||||
import 'package:jj_mkvmaker/core/file_ops.dart';
|
||||
import 'package:jj_mkvmaker/ui/batch_rename_dialog.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 78: 일괄 이름 바꾸기 - 번호 [C] · 찾아 바꾸기 · 대소문자, 미리 보기 (겹침 · 이미 있음), 맞바꾸기, 실패하면 되돌림, 되돌리기
|
||||
void main() {
|
||||
group('규칙', () {
|
||||
test('드라마 회차: "Show S01E[C]" - 확장자는 그대로, 번호는 자릿수만큼', () {
|
||||
const r = RenameRule(template: 'Show S01E[C]');
|
||||
expect(r.apply('아무거나.mkv', isDir: false, index: 0), 'Show S01E01.mkv');
|
||||
expect(r.apply('x.srt', isDir: false, index: 9), 'Show S01E10.srt');
|
||||
expect(const RenameRule(template: '[C] - [N]', start: 5, step: 5, digits: 3).apply('a.mp4', isDir: false, index: 2), '015 - a.mp4');
|
||||
});
|
||||
|
||||
test('찾아 바꾸기 (대소문자 무시 · 가리기) · 대소문자 바꾸기 · 폴더는 점 뒤도 이름', () {
|
||||
expect(const RenameRule(find: 'x264', replace: 'H264').apply('Movie.X264.mkv', isDir: false, index: 0), 'Movie.H264.mkv');
|
||||
expect(const RenameRule(find: 'x264', replace: '', ignoreCase: false).apply('Movie.X264.mkv', isDir: false, index: 0),
|
||||
'Movie.X264.mkv');
|
||||
expect(const RenameRule(caseMode: 'lower').apply('ABC.MKV', isDir: false, index: 0), 'abc.mkv');
|
||||
expect(const RenameRule(caseMode: 'upper').apply('abc.mkv', isDir: false, index: 0), 'ABC.MKV');
|
||||
expect(const RenameRule(caseMode: 'title').apply('the big show.mkv', isDir: false, index: 0), 'The Big Show.mkv');
|
||||
expect(const RenameRule(template: '[N]_v2').apply('Season.1', isDir: true, index: 0), 'Season.1_v2');
|
||||
expect(const RenameRule().isIdentity, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('미리 보기', () {
|
||||
final dir = p.join('C:', 'v');
|
||||
String at(String n) => p.join(dir, n);
|
||||
|
||||
test('같은 이름 겹침 · 고르지 않은 것과 겹침 · 쓸 수 없는 글자 · 맞바꾸기는 괜찮음', () {
|
||||
final rows = planRename([at('a.mkv'), at('b.mkv')], const RenameRule(template: 'same'),
|
||||
isDir: (_) => false, siblings: {dir: {'a.mkv', 'b.mkv'}});
|
||||
expect(rows.every((r) => r.problem == '다른 항목과 같은 이름'), isTrue);
|
||||
|
||||
final taken = planRename([at('a.mkv')], const RenameRule(template: 'c'), isDir: (_) => false, siblings: {dir: {'a.mkv', 'C.MKV'}});
|
||||
expect(taken.single.problem, '같은 이름이 이미 있음', reason: '대소문자만 다른 것도 겹친다');
|
||||
|
||||
final bad = planRename([at('a.mkv')], const RenameRule(template: 'a:b'), isDir: (_) => false, siblings: {dir: {'a.mkv'}});
|
||||
expect(bad.single.problem, '쓸 수 없는 이름');
|
||||
|
||||
// a.mkv → b.mkv, b.mkv → a.mkv (찾아 바꾸기로 맞바꾸지는 못하니 틀로 흉내: 번호 차례를 거꾸로)
|
||||
final swap = planRename([at('b.mkv'), at('a.mkv')], const RenameRule(find: 'b', replace: 'a'),
|
||||
isDir: (_) => false, siblings: {dir: {'a.mkv', 'b.mkv'}});
|
||||
expect(swap.first.newName, 'a.mkv');
|
||||
expect(swap.first.problem, '다른 항목과 같은 이름', reason: 'a.mkv 는 그대로 a.mkv 라 겹친다');
|
||||
});
|
||||
|
||||
test('대소문자만 바꾸는 것은 자기 자신과 겹치지 않는다', () {
|
||||
final rows = planRename([at('abc.mkv')], const RenameRule(caseMode: 'upper'), isDir: (_) => false, siblings: {dir: {'abc.mkv'}});
|
||||
expect(rows.single.problem, isNull);
|
||||
expect(rows.single.newName, 'ABC.MKV');
|
||||
});
|
||||
});
|
||||
|
||||
group('바꾸기 (실제 파일)', () {
|
||||
late Directory tmp;
|
||||
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_ren78_'));
|
||||
tearDown(() => tmp.deleteSync(recursive: true));
|
||||
String mk(String n, [String body = '']) => (File(p.join(tmp.path, n))..writeAsStringSync(body.isEmpty ? n : body)).path;
|
||||
List<String> names() => (tmp.listSync().map((e) => p.basename(e.path)).toList()..sort());
|
||||
|
||||
test('여러 개 · 맞바꾸기 (a↔b) · 되돌리기', () async {
|
||||
final a = mk('a.txt'), b = mk('b.txt'), c = mk('c.txt');
|
||||
final done = await runBatchRename([(a, 'b.txt'), (b, 'a.txt'), (c, 'Show 01.txt')]);
|
||||
expect(names(), ['Show 01.txt', 'a.txt', 'b.txt']);
|
||||
expect(File(p.join(tmp.path, 'b.txt')).readAsStringSync(), 'a.txt', reason: '내용이 바뀐 이름을 따라감');
|
||||
expect(File(p.join(tmp.path, 'a.txt')).readAsStringSync(), 'b.txt');
|
||||
// 되돌리기
|
||||
await runBatchRename([for (final (orig, now) in done) (now, p.basename(orig))]);
|
||||
expect(names(), ['a.txt', 'b.txt', 'c.txt']);
|
||||
expect(File(p.join(tmp.path, 'a.txt')).readAsStringSync(), 'a.txt');
|
||||
});
|
||||
|
||||
test('도중에 실패하면 바꾼 것을 모두 원래대로 (임시 이름이 남지 않음)', () async {
|
||||
final a = mk('a.txt'), b = mk('b.txt'), c = mk('c.txt');
|
||||
var calls = 0;
|
||||
Future<String> flaky(String path, String name) {
|
||||
// 2단계 (새 이름으로) 의 세 번째에서 실패
|
||||
if (++calls == 6) throw const FileSystemException('잠김');
|
||||
return FileOps.rename(path, name);
|
||||
}
|
||||
|
||||
await expectLater(
|
||||
runBatchRename([(a, 'x.txt'), (b, 'y.txt'), (c, 'z.txt')], renamer: flaky),
|
||||
throwsA(isA<BatchRenameException>().having((e) => e.stuck, '되돌리지 못한 것', isEmpty)),
|
||||
);
|
||||
expect(names(), ['a.txt', 'b.txt', 'c.txt']);
|
||||
});
|
||||
|
||||
testWidgets('창: 규칙을 넣으면 미리 보기, 문제가 있으면 [바꾸기] 꺼짐, 바꾸면 결과를 돌려준다', (tester) async {
|
||||
tester.view.physicalSize = const Size(1000, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final paths = [mk('ep_b.mkv'), mk('ep_a.mkv')];
|
||||
mk('Show 01.mkv', 'other');
|
||||
List<(String, String)>? result;
|
||||
final nav = GlobalKey<NavigatorState>();
|
||||
await tester.pumpWidget(MaterialApp(navigatorKey: nav, home: const Scaffold()));
|
||||
showBatchRename(nav.currentContext!, paths, isDir: (_) => false).then((r) => result = r);
|
||||
for (var i = 0; i < 20; i++) {
|
||||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
|
||||
await tester.pump();
|
||||
}
|
||||
expect(find.text('이름 일괄 바꾸기 · 2개'), findsOneWidget);
|
||||
expect(find.text('바뀌는 것 0개'), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.widgetWithText(TextField, '이름 틀 (확장자는 그대로)'), 'Show [C]');
|
||||
await tester.pump();
|
||||
// 첫 번째가 Show 01.mkv - 고르지 않은 파일과 겹친다
|
||||
expect(find.textContaining('같은 이름이 이미 있음'), findsOneWidget);
|
||||
expect(tester.widget<FilledButton>(find.widgetWithText(FilledButton, '바꾸기 (2개)')).onPressed, isNull);
|
||||
|
||||
await tester.enterText(find.widgetWithText(TextField, '번호 시작'), '2');
|
||||
await tester.pump();
|
||||
expect(find.text('바뀌는 것 2개'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(FilledButton, '바꾸기 (2개)'));
|
||||
for (var i = 0; i < 20 && result == null; i++) {
|
||||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
|
||||
await tester.pump();
|
||||
}
|
||||
expect(result, hasLength(2));
|
||||
expect(names(), ['Show 01.mkv', 'Show 02.mkv', 'Show 03.mkv']);
|
||||
expect(File(p.join(tmp.path, 'Show 02.mkv')).readAsStringSync(), 'ep_b.mkv', reason: '받은 차례대로 번호');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -434,7 +434,7 @@ void main() {
|
|||
await calm(tester);
|
||||
expect(clip, p.join(left, 'doc.txt'));
|
||||
|
||||
// 여럿: "2개 항목" · 이름 변경은 눌리지 않음 · 경로 복사는 둘 다
|
||||
// 여럿: "2개 항목" · 이름 변경은 일괄 이름 바꾸기 (78) · 경로 복사는 둘 다
|
||||
await act(tester, () => tester.tap(find.text('선택').first));
|
||||
await act(tester, () => tester.tap(markOf('doc.txt')));
|
||||
await act(tester, () => tester.tap(markOf('b.txt')));
|
||||
|
|
@ -442,9 +442,13 @@ void main() {
|
|||
await calm(tester);
|
||||
expect(find.text('2개 항목'), findsOneWidget);
|
||||
expect(find.byTooltip('2개 삭제'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('이름 변경').last);
|
||||
await tester.tap(find.byTooltip('2개 이름 일괄 바꾸기'));
|
||||
await calm(tester);
|
||||
expect(find.text('이름 일괄 바꾸기 · 2개'), findsOneWidget, reason: '78: 여럿이면 일괄 이름 바꾸기 창');
|
||||
await tester.tap(find.text('취소').last);
|
||||
await calm(tester);
|
||||
await tester.longPress(find.text('b.txt').first);
|
||||
await calm(tester);
|
||||
expect(find.text('2개 항목'), findsOneWidget, reason: '이름 변경은 눌리지 않아 메뉴 그대로');
|
||||
await tester.tap(find.text('경로 복사 (2개)'));
|
||||
await calm(tester);
|
||||
expect(clip!.split('\n'), unorderedEquals([p.join(left, 'doc.txt'), p.join(left, 'b.txt')]));
|
||||
|
|
@ -872,6 +876,30 @@ void main() {
|
|||
matching: find.byTooltip('고르기 취소')), findsNothing); // 창마다 하나만
|
||||
});
|
||||
|
||||
testWidgets('78: 여럿을 골라 [이름 변경] = 일괄 이름 바꾸기 → 목록 차례대로 번호 · [되돌리기]', (tester) async {
|
||||
File(p.join(left, 'b.txt')).writeAsStringSync('b');
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('선택').first));
|
||||
await act(tester, () => tester.tap(markOf('doc.txt')));
|
||||
await act(tester, () => tester.tap(markOf('b.txt')));
|
||||
await act(tester, () => tester.tap(find.text('이름 변경').first));
|
||||
await settle(tester, () => find.text('바뀌는 것 0개').evaluate().isNotEmpty);
|
||||
await tester.enterText(find.widgetWithText(TextField, '이름 틀 (확장자는 그대로)'), 'Ep[C]');
|
||||
await tester.pump();
|
||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '바꾸기 (2개)')));
|
||||
await settle(tester, () => File(p.join(left, 'Ep02.txt')).existsSync() && find.text('Ep02.txt').evaluate().isNotEmpty);
|
||||
// 목록 차례 (이름순: b.txt, doc.txt) 대로 번호
|
||||
expect(File(p.join(left, 'Ep01.txt')).readAsStringSync(), 'b');
|
||||
expect(File(p.join(left, 'Ep02.txt')).readAsStringSync(), 'hello');
|
||||
expect(find.text('2개의 이름을 바꿨습니다.'), findsOneWidget);
|
||||
await act(tester, () => tester.tap(find.text('되돌리기')));
|
||||
// 알림이 닫히는 움직임을 끝까지 (시험이 끝난 뒤에 끝나면 Scaffold 'mounted' 단언에 걸린다)
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await settle(tester, () => File(p.join(left, 'doc.txt')).existsSync() && find.text('doc.txt').evaluate().isNotEmpty);
|
||||
expect(File(p.join(left, 'b.txt')).readAsStringSync(), 'b');
|
||||
expect(File(p.join(left, 'Ep01.txt')).existsSync(), isFalse);
|
||||
});
|
||||
|
||||
testWidgets('79: 찾기 결과를 목록으로 - 이름 없이 종류 조건으로 찾고, 골라서 그대로 다른 창으로 복사', (tester) async {
|
||||
await open(tester);
|
||||
File(p.join(left, 'sub', 'big.mkv'))
|
||||
|
|
|
|||
Loading…
Reference in New Issue