통합: 묶음5 의 새 끝 (e81fa6e: 243~246 · 233 · 242 · 241 · 251 · 252 · 시험 흔들림 calm) 을 합침
관리자 지시 (10/12). 겹친 곳 없이 자동으로 합쳐짐 (번역 json · app_actions · home_page · settings_page). 번역 json 은 정렬 · 모양 그대로 확인, 빠진 번역 0, analyze 0, 전체 시험 646 통과. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>fix_mkv
commit
55f5676cf5
|
|
@ -439,10 +439,12 @@
|
||||||
"{0} 이 비어 있습니다 (카드 · USB · 디스크를 넣은 뒤 다시 누르세요).": "{0} is empty (insert a card, USB drive or disc and tap again).",
|
"{0} 이 비어 있습니다 (카드 · USB · 디스크를 넣은 뒤 다시 누르세요).": "{0} is empty (insert a card, USB drive or disc and tap again).",
|
||||||
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg does not have the {0} encoder.",
|
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg does not have the {0} encoder.",
|
||||||
"{0} 전체": "All of {0}",
|
"{0} 전체": "All of {0}",
|
||||||
|
"{0} 접기": "Collapse {0}",
|
||||||
"{0} 제거": "Remove {0}",
|
"{0} 제거": "Remove {0}",
|
||||||
"{0} 추가": "Add {0}",
|
"{0} 추가": "Add {0}",
|
||||||
"{0} 파일": "{0} file",
|
"{0} 파일": "{0} file",
|
||||||
"{0} 페이지를 지울까요? (저장해야 파일에 반영됩니다)": "Delete page {0}? (Saved to the file only when you save)",
|
"{0} 페이지를 지울까요? (저장해야 파일에 반영됩니다)": "Delete page {0}? (Saved to the file only when you save)",
|
||||||
|
"{0} 펼치기": "Expand {0}",
|
||||||
"{0} 폴더가 없거나 열 수 없어 내역에서 뺐습니다": "Folder {0} is missing or can't be opened, so it was removed from history",
|
"{0} 폴더가 없거나 열 수 없어 내역에서 뺐습니다": "Folder {0} is missing or can't be opened, so it was removed from history",
|
||||||
"{0}% 에서 2분 넘게 나아가지 않아 멈췄습니다. 망이 Tor 를 막고 있거나 느릴 수 있습니다. 잠시 뒤 [다시 시도] 하세요.": "Stopped because it made no progress past {0}% for over 2 minutes. The network may be blocking Tor or be slow. Press [Try again] in a moment.",
|
"{0}% 에서 2분 넘게 나아가지 않아 멈췄습니다. 망이 Tor 를 막고 있거나 느릴 수 있습니다. 잠시 뒤 [다시 시도] 하세요.": "Stopped because it made no progress past {0}% for over 2 minutes. The network may be blocking Tor or be slow. Press [Try again] in a moment.",
|
||||||
"{0}. {1} (대기)": "{0}. {1} (waiting)",
|
"{0}. {1} (대기)": "{0}. {1} (waiting)",
|
||||||
|
|
@ -926,6 +928,7 @@
|
||||||
"마지막으로: {0}": "To the last: {0}",
|
"마지막으로: {0}": "To the last: {0}",
|
||||||
"만든 그림 · 저장: {0}": "Made images · saved in: {0}",
|
"만든 그림 · 저장: {0}": "Made images · saved in: {0}",
|
||||||
"만들 자막 언어 (파일명_언어코드.srt)": "Subtitle languages to make (filename_languagecode.srt)",
|
"만들 자막 언어 (파일명_언어코드.srt)": "Subtitle languages to make (filename_languagecode.srt)",
|
||||||
|
"만들거나 쓰지 못했습니다": "Could not create or write",
|
||||||
"만들기": "Make",
|
"만들기": "Make",
|
||||||
"만들다 만 파일 {0}개": "{0} incomplete files",
|
"만들다 만 파일 {0}개": "{0} incomplete files",
|
||||||
"만들다 만 파일 {0}개를 지울까요?": "Delete {0} incomplete files?",
|
"만들다 만 파일 {0}개를 지울까요?": "Delete {0} incomplete files?",
|
||||||
|
|
@ -1003,7 +1006,7 @@
|
||||||
"바꾸거나 끄려면 지금 마스터 비밀번호 (또는 최상 비밀번호) 를 넣으세요.": "To change or turn it off, enter the current master password (or the top password).",
|
"바꾸거나 끄려면 지금 마스터 비밀번호 (또는 최상 비밀번호) 를 넣으세요.": "To change or turn it off, enter the current master password (or the top password).",
|
||||||
"바꾸기": "Change",
|
"바꾸기": "Change",
|
||||||
"바꾸는 정도 {0}%": "Change amount {0}%",
|
"바꾸는 정도 {0}%": "Change amount {0}%",
|
||||||
"바꾸면 바로 저장됩니다": "Changes are saved right away",
|
"바꾸면 바로 저장됩니다": "Saved automatically",
|
||||||
"바꾼 것이 없습니다 (원본 그대로)": "Nothing changed (original as is)",
|
"바꾼 것이 없습니다 (원본 그대로)": "Nothing changed (original as is)",
|
||||||
"바뀌는 것 없음 (양쪽이 같습니다)": "Nothing to change (both sides are the same)",
|
"바뀌는 것 없음 (양쪽이 같습니다)": "Nothing to change (both sides are the same)",
|
||||||
"바뀐 것을 살펴보는 간격": "Interval for checking changes",
|
"바뀐 것을 살펴보는 간격": "Interval for checking changes",
|
||||||
|
|
@ -1118,6 +1121,7 @@
|
||||||
"복사 실패 ({0}): {1}": "Copy failed ({0}): {1}",
|
"복사 실패 ({0}): {1}": "Copy failed ({0}): {1}",
|
||||||
"복사 작업이라 칸의 원본 지우기 옵션 (--remove-source-files · /MOV) 은 쓰지 않습니다. 옮기려면 Rsync 화면에서 [원본 파일 지우기] 를 켜세요": "This is a copy task, so source-removing options in the field (--remove-source-files · /MOV) are not used. To move, turn on [Remove source files] on the Rsync screen",
|
"복사 작업이라 칸의 원본 지우기 옵션 (--remove-source-files · /MOV) 은 쓰지 않습니다. 옮기려면 Rsync 화면에서 [원본 파일 지우기] 를 켜세요": "This is a copy task, so source-removing options in the field (--remove-source-files · /MOV) are not used. To move, turn on [Remove source files] on the Rsync screen",
|
||||||
"복사(Ctrl+C)한 주소 자동 다운로드": "Auto-download copied (Ctrl+C) URLs",
|
"복사(Ctrl+C)한 주소 자동 다운로드": "Auto-download copied (Ctrl+C) URLs",
|
||||||
|
"복사로 맞춤": "Syncs by copying",
|
||||||
"복사하는 중": "Copying",
|
"복사하는 중": "Copying",
|
||||||
"복사할": "copy",
|
"복사할": "copy",
|
||||||
"복사할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "Choose items to copy (tap, or mark with the circle on the right).",
|
"복사할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "Choose items to copy (tap, or mark with the circle on the right).",
|
||||||
|
|
@ -1318,6 +1322,7 @@
|
||||||
"실시간 동기화: 원본 폴더": "Live sync: source folder",
|
"실시간 동기화: 원본 폴더": "Live sync: source folder",
|
||||||
"실시간 동기화가 멈췄습니다": "Live sync stopped",
|
"실시간 동기화가 멈췄습니다": "Live sync stopped",
|
||||||
"실시간 동기화가 없습니다. [추가] 하거나 복사 · rsync 탭에서 [lsync 로 이동] 하세요.": "No live sync. [Add] one, or use [Move to lsync] in the copy · rsync tab.",
|
"실시간 동기화가 없습니다. [추가] 하거나 복사 · rsync 탭에서 [lsync 로 이동] 하세요.": "No live sync. [Add] one, or use [Move to lsync] in the copy · rsync tab.",
|
||||||
|
"실시간 동기화는 원본을 지우지 않습니다 (복사로 맞춤). 원본을 비우려면 lsync 의 [최종 정리] 를 쓰세요.": "Real-time sync never deletes the source (it syncs by copying). To empty the source, use [Final cleanup] in lsync.",
|
||||||
"실시간 동기화를 지울까요?": "Delete this live sync?",
|
"실시간 동기화를 지울까요?": "Delete this live sync?",
|
||||||
"실시간 동기화의 [최종 정리]": "[Final cleanup] in live sync",
|
"실시간 동기화의 [최종 정리]": "[Final cleanup] in live sync",
|
||||||
"실제 실행: {0}": "Actually runs: {0}",
|
"실제 실행: {0}": "Actually runs: {0}",
|
||||||
|
|
@ -1461,6 +1466,7 @@
|
||||||
"올린 그림은 지워집니다. 다시 보려면 해상도 올리기를 다시 해야 합니다.": "The upscaled image will be deleted. To see it again you will need to upscale again.",
|
"올린 그림은 지워집니다. 다시 보려면 해상도 올리기를 다시 해야 합니다.": "The upscaled image will be deleted. To see it again you will need to upscale again.",
|
||||||
"올린 이 {0}": "Uploaded by {0}",
|
"올린 이 {0}": "Uploaded by {0}",
|
||||||
"옮겼습니다.": "moved.",
|
"옮겼습니다.": "moved.",
|
||||||
|
"옮기거나 이름을 바꾸지 못했습니다": "Could not move or rename",
|
||||||
"옮기기": "Move",
|
"옮기기": "Move",
|
||||||
"옮기는 중": "Moving",
|
"옮기는 중": "Moving",
|
||||||
"옮길": "move",
|
"옮길": "move",
|
||||||
|
|
@ -1469,10 +1475,10 @@
|
||||||
"옵션": "Options",
|
"옵션": "Options",
|
||||||
"옵션을 저장했습니다": "Options saved",
|
"옵션을 저장했습니다": "Options saved",
|
||||||
"완료": "Done",
|
"완료": "Done",
|
||||||
|
"완료 시 자동 동영상 추가": "Auto-add videos when done",
|
||||||
"완료 정리": "Clear completed",
|
"완료 정리": "Clear completed",
|
||||||
"완료: 성공 {0} / 전체 {1}": "Done: {0} succeeded / {1} total",
|
"완료: 성공 {0} / 전체 {1}": "Done: {0} succeeded / {1} total",
|
||||||
"완료: 성공 {0} · 취소 {1} · 실패 {2} / 전체 {3}": "Done: {0} succeeded · {1} cancelled · {2} failed / {3} total",
|
"완료: 성공 {0} · 취소 {1} · 실패 {2} / 전체 {3}": "Done: {0} succeeded · {1} cancelled · {2} failed / {3} total",
|
||||||
"완료시 자동 동영상추가": "Auto-add videos when done",
|
|
||||||
"외부": "External",
|
"외부": "External",
|
||||||
"외부 브라우저": "External browser",
|
"외부 브라우저": "External browser",
|
||||||
"외부 브라우저로 열기": "Open in external browser",
|
"외부 브라우저로 열기": "Open in external browser",
|
||||||
|
|
@ -1517,6 +1523,7 @@
|
||||||
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "This run removes source files, so it can run once the list of files to move is shown.",
|
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "This run removes source files, so it can run once the list of files to move is shown.",
|
||||||
"원본을 지우는 작업입니다 (이동)": "This task removes source files (move)",
|
"원본을 지우는 작업입니다 (이동)": "This task removes source files (move)",
|
||||||
"원본의 {0}배": "{0}× original",
|
"원본의 {0}배": "{0}× original",
|
||||||
|
"원본의 것은 모두 대상에 있습니다 (대상에만 있는 것 {0}개는 그대로 둠)": "Everything in the source is in the target ({0} items only in the target are left as they are)",
|
||||||
"원본의 빈 폴더 정리 중…": "Cleaning up empty folders in the source…",
|
"원본의 빈 폴더 정리 중…": "Cleaning up empty folders in the source…",
|
||||||
"원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.": "Moves all files from the source to the target (deleting them from the source), then deletes the empty folders left in the source.",
|
"원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.": "Moves all files from the source to the target (deleting them from the source), then deletes the empty folders left in the source.",
|
||||||
"원본이 대상 안에 있습니다 (환경 설정에서 막아 둠)": "The source is inside the target (blocked in settings)",
|
"원본이 대상 안에 있습니다 (환경 설정에서 막아 둠)": "The source is inside the target (blocked in settings)",
|
||||||
|
|
@ -1615,6 +1622,7 @@
|
||||||
"이 언어 지우기": "Remove this language",
|
"이 언어 지우기": "Remove this language",
|
||||||
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "This location (network drive, network share, USB memory etc.) has no Recycle Bin, so items are deleted right away.",
|
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "This location (network drive, network share, USB memory etc.) has no Recycle Bin, so items are deleted right away.",
|
||||||
"이 이동 버튼 지우기": "Delete this move button",
|
"이 이동 버튼 지우기": "Delete this move button",
|
||||||
|
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "These options will be used the next time this move runs. Run it now with the new options?",
|
||||||
"이 파일에 맞는 자막 우선 (영상 해시)": "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.",
|
"이 파일을 열 수 있는 앱이 없습니다.": "No app can open this file.",
|
||||||
|
|
@ -1866,8 +1874,8 @@
|
||||||
"좌 ⇄ 우": "Left ⇄ right",
|
"좌 ⇄ 우": "Left ⇄ right",
|
||||||
"좌우": "Side by side",
|
"좌우": "Side by side",
|
||||||
"좌우 (기본)": "Side by side (default)",
|
"좌우 (기본)": "Side by side (default)",
|
||||||
"좌우 ⇆": "Side ⇆",
|
"좌우 ⇆": "Side by side ⇆",
|
||||||
"좌우 ⇆ / 위아래 ⇅": "Side by side ⇆ / Top and bottom ⇅",
|
"좌우 ⇆ / 위아래 ⇅": "Side by side ⇆ / Stacked ⇅",
|
||||||
"좌우 두 폴더 맞추기 (rsync) · 모니터링 · 실시간 동기화 (lsync)": "Sync two folders (rsync) · monitoring · live sync (lsync)",
|
"좌우 두 폴더 맞추기 (rsync) · 모니터링 · 실시간 동기화 (lsync)": "Sync two folders (rsync) · monitoring · live sync (lsync)",
|
||||||
"좌우 맞추기 (세로로 밀어 봄)": "Fit width (scroll vertically)",
|
"좌우 맞추기 (세로로 밀어 봄)": "Fit width (scroll vertically)",
|
||||||
"좌우로 밀어 화면 이동": "Swipe left/right to switch screens",
|
"좌우로 밀어 화면 이동": "Swipe left/right to switch screens",
|
||||||
|
|
@ -1914,6 +1922,7 @@
|
||||||
"지금 관리자 권한으로 실행 중입니다. 끄면 (기본) Chrome 엔진을 시작하지 않고 Edge 를 권합니다. 켜면 샌드박스 없이 관리자 권한으로 웹 페이지를 돌립니다.": "The app is running as administrator now. When off (default), the Chrome engine isn't started and Edge is suggested. When on, web pages run as administrator without a sandbox.",
|
"지금 관리자 권한으로 실행 중입니다. 끄면 (기본) Chrome 엔진을 시작하지 않고 Edge 를 권합니다. 켜면 샌드박스 없이 관리자 권한으로 웹 페이지를 돌립니다.": "The app is running as administrator now. When off (default), the Chrome engine isn't started and Edge is suggested. When on, web pages run as administrator without a sandbox.",
|
||||||
"지금 끄기": "Stop now",
|
"지금 끄기": "Stop now",
|
||||||
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "The running copy uses the previous options. Stop it and run again with the new options?",
|
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "The running copy uses the previous options. Stop it and run again with the new options?",
|
||||||
|
"지금 도는 이동은 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "The running move is using the previous options. Stop it and run again with the new options?",
|
||||||
"지금 마스터 (또는 최상) 비밀번호": "Current master (or top) password",
|
"지금 마스터 (또는 최상) 비밀번호": "Current master (or top) password",
|
||||||
"지금 맞추기": "Sync now",
|
"지금 맞추기": "Sync now",
|
||||||
"지금 맞추는 중입니다. 끝난 뒤에 하세요.": "Syncing right now. Try again when it finishes.",
|
"지금 맞추는 중입니다. 끝난 뒤에 하세요.": "Syncing right now. Try again when it finishes.",
|
||||||
|
|
@ -1944,6 +1953,7 @@
|
||||||
"지우면 휴지통으로": "Delete to the Recycle Bin",
|
"지우면 휴지통으로": "Delete to the Recycle Bin",
|
||||||
"지우지 못한 파일: {0}": "Couldn't delete: {0}",
|
"지우지 못한 파일: {0}": "Couldn't delete: {0}",
|
||||||
"지우지 못함: {0}": "Could not delete: {0}",
|
"지우지 못함: {0}": "Could not delete: {0}",
|
||||||
|
"지우지 못했습니다 (권한이 없거나 사용 중)": "Could not delete (no permission or in use)",
|
||||||
"지우지 못했습니다: {0}": "Could not delete: {0}",
|
"지우지 못했습니다: {0}": "Could not delete: {0}",
|
||||||
"지우지 않고 맞춤 · 대상에만 있는 {0}개를 지울지 확인 필요": "Synced without deleting · confirm whether to delete {0} items only in the target",
|
"지우지 않고 맞춤 · 대상에만 있는 {0}개를 지울지 확인 필요": "Synced without deleting · confirm whether to delete {0} items only in the target",
|
||||||
"지우지 않기 (지우기 끄기)": "Don't delete (turn off delete)",
|
"지우지 않기 (지우기 끄기)": "Don't delete (turn off delete)",
|
||||||
|
|
@ -2089,7 +2099,9 @@
|
||||||
"파일 폴더": "File folder",
|
"파일 폴더": "File folder",
|
||||||
"파일 확인 중…": "Checking files…",
|
"파일 확인 중…": "Checking files…",
|
||||||
"파일만 골랐을 때": "When only files are selected",
|
"파일만 골랐을 때": "When only files are selected",
|
||||||
|
"파일을 복사하지 못했습니다": "Could not copy the file",
|
||||||
"파일을 분석할 수 없습니다: {0}": "Cannot analyze the file: {0}",
|
"파일을 분석할 수 없습니다: {0}": "Cannot analyze the file: {0}",
|
||||||
|
"파일을 읽지 못했습니다": "Could not read the file",
|
||||||
"파일이 깨졌습니다": "The file is damaged",
|
"파일이 깨졌습니다": "The file is damaged",
|
||||||
"파일이 깨졌습니다 · [다시 받기] 를 눌러 주세요": "The file is damaged · tap [Download again]",
|
"파일이 깨졌습니다 · [다시 받기] 를 눌러 주세요": "The file is damaged · tap [Download again]",
|
||||||
"파일이 보이지 않습니다: \"모든 파일에 대한 접근\" 권한이 없습니다": "Files are not shown: \"All files access\" is not allowed",
|
"파일이 보이지 않습니다: \"모든 파일에 대한 접근\" 권한이 없습니다": "Files are not shown: \"All files access\" is not allowed",
|
||||||
|
|
|
||||||
|
|
@ -439,10 +439,12 @@
|
||||||
"{0} 이 비어 있습니다 (카드 · USB · 디스크를 넣은 뒤 다시 누르세요).": "{0} は空です (カード・USB・ディスクを入れてからもう一度押してください)。",
|
"{0} 이 비어 있습니다 (카드 · USB · 디스크를 넣은 뒤 다시 누르세요).": "{0} は空です (カード・USB・ディスクを入れてからもう一度押してください)。",
|
||||||
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg に {0} エンコーダーがありません。",
|
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg に {0} エンコーダーがありません。",
|
||||||
"{0} 전체": "{0} 全体",
|
"{0} 전체": "{0} 全体",
|
||||||
|
"{0} 접기": "{0} を折りたたむ",
|
||||||
"{0} 제거": "{0} の削除",
|
"{0} 제거": "{0} の削除",
|
||||||
"{0} 추가": "{0} を追加",
|
"{0} 추가": "{0} を追加",
|
||||||
"{0} 파일": "{0} ファイル",
|
"{0} 파일": "{0} ファイル",
|
||||||
"{0} 페이지를 지울까요? (저장해야 파일에 반영됩니다)": "{0} ページを削除しますか? (保存するとファイルに反映されます)",
|
"{0} 페이지를 지울까요? (저장해야 파일에 반영됩니다)": "{0} ページを削除しますか? (保存するとファイルに反映されます)",
|
||||||
|
"{0} 펼치기": "{0} を展開",
|
||||||
"{0} 폴더가 없거나 열 수 없어 내역에서 뺐습니다": "{0} フォルダーが存在しないか開けないため、履歴から削除しました",
|
"{0} 폴더가 없거나 열 수 없어 내역에서 뺐습니다": "{0} フォルダーが存在しないか開けないため、履歴から削除しました",
|
||||||
"{0}% 에서 2분 넘게 나아가지 않아 멈췄습니다. 망이 Tor 를 막고 있거나 느릴 수 있습니다. 잠시 뒤 [다시 시도] 하세요.": "{0}% から 2 分以上進まなかったため停止しました。ネットワークが Tor をブロックしているか、遅い可能性があります。少し待ってから [再試行] してください。",
|
"{0}% 에서 2분 넘게 나아가지 않아 멈췄습니다. 망이 Tor 를 막고 있거나 느릴 수 있습니다. 잠시 뒤 [다시 시도] 하세요.": "{0}% から 2 分以上進まなかったため停止しました。ネットワークが Tor をブロックしているか、遅い可能性があります。少し待ってから [再試行] してください。",
|
||||||
"{0}. {1} (대기)": "{0}. {1}(待機)",
|
"{0}. {1} (대기)": "{0}. {1}(待機)",
|
||||||
|
|
@ -926,6 +928,7 @@
|
||||||
"마지막으로: {0}": "最後へ: {0}",
|
"마지막으로: {0}": "最後へ: {0}",
|
||||||
"만든 그림 · 저장: {0}": "作成した画像・保存先: {0}",
|
"만든 그림 · 저장: {0}": "作成した画像・保存先: {0}",
|
||||||
"만들 자막 언어 (파일명_언어코드.srt)": "作成する字幕の言語(ファイル名_言語コード.srt)",
|
"만들 자막 언어 (파일명_언어코드.srt)": "作成する字幕の言語(ファイル名_言語コード.srt)",
|
||||||
|
"만들거나 쓰지 못했습니다": "作成または書き込みできませんでした",
|
||||||
"만들기": "作成",
|
"만들기": "作成",
|
||||||
"만들다 만 파일 {0}개": "未完成のファイル {0} 個",
|
"만들다 만 파일 {0}개": "未完成のファイル {0} 個",
|
||||||
"만들다 만 파일 {0}개를 지울까요?": "未完成のファイル {0} 個を削除しますか?",
|
"만들다 만 파일 {0}개를 지울까요?": "未完成のファイル {0} 個を削除しますか?",
|
||||||
|
|
@ -1118,6 +1121,7 @@
|
||||||
"복사 실패 ({0}): {1}": "コピー失敗 ({0}): {1}",
|
"복사 실패 ({0}): {1}": "コピー失敗 ({0}): {1}",
|
||||||
"복사 작업이라 칸의 원본 지우기 옵션 (--remove-source-files · /MOV) 은 쓰지 않습니다. 옮기려면 Rsync 화면에서 [원본 파일 지우기] 를 켜세요": "コピー作業のため、欄の元ファイル削除オプション (--remove-source-files · /MOV) は使いません。移動するには Rsync 画面で [元ファイルを削除] をオンにしてください",
|
"복사 작업이라 칸의 원본 지우기 옵션 (--remove-source-files · /MOV) 은 쓰지 않습니다. 옮기려면 Rsync 화면에서 [원본 파일 지우기] 를 켜세요": "コピー作業のため、欄の元ファイル削除オプション (--remove-source-files · /MOV) は使いません。移動するには Rsync 画面で [元ファイルを削除] をオンにしてください",
|
||||||
"복사(Ctrl+C)한 주소 자동 다운로드": "コピー(Ctrl+C)した URL を自動ダウンロード",
|
"복사(Ctrl+C)한 주소 자동 다운로드": "コピー(Ctrl+C)した URL を自動ダウンロード",
|
||||||
|
"복사로 맞춤": "コピーで合わせる",
|
||||||
"복사하는 중": "コピー中",
|
"복사하는 중": "コピー中",
|
||||||
"복사할": "コピーする",
|
"복사할": "コピーする",
|
||||||
"복사할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "コピーする項目を選んでください (タップするか右の丸で印を付ける)。",
|
"복사할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "コピーする項目を選んでください (タップするか右の丸で印を付ける)。",
|
||||||
|
|
@ -1318,6 +1322,7 @@
|
||||||
"실시간 동기화: 원본 폴더": "リアルタイム同期: 元フォルダー",
|
"실시간 동기화: 원본 폴더": "リアルタイム同期: 元フォルダー",
|
||||||
"실시간 동기화가 멈췄습니다": "リアルタイム同期が停止しました",
|
"실시간 동기화가 멈췄습니다": "リアルタイム同期が停止しました",
|
||||||
"실시간 동기화가 없습니다. [추가] 하거나 복사 · rsync 탭에서 [lsync 로 이동] 하세요.": "リアルタイム同期はありません。[追加] するか、コピー · rsync タブで [lsync へ移動] してください。",
|
"실시간 동기화가 없습니다. [추가] 하거나 복사 · rsync 탭에서 [lsync 로 이동] 하세요.": "リアルタイム同期はありません。[追加] するか、コピー · rsync タブで [lsync へ移動] してください。",
|
||||||
|
"실시간 동기화는 원본을 지우지 않습니다 (복사로 맞춤). 원본을 비우려면 lsync 의 [최종 정리] 를 쓰세요.": "リアルタイム同期は元を削除しません (コピーで合わせます)。元を空にするには lsync の [最終整理] を使ってください。",
|
||||||
"실시간 동기화를 지울까요?": "このリアルタイム同期を削除しますか?",
|
"실시간 동기화를 지울까요?": "このリアルタイム同期を削除しますか?",
|
||||||
"실시간 동기화의 [최종 정리]": "リアルタイム同期の [最終整理]",
|
"실시간 동기화의 [최종 정리]": "リアルタイム同期の [最終整理]",
|
||||||
"실제 실행: {0}": "実際の実行: {0}",
|
"실제 실행: {0}": "実際の実行: {0}",
|
||||||
|
|
@ -1461,6 +1466,7 @@
|
||||||
"올린 그림은 지워집니다. 다시 보려면 해상도 올리기를 다시 해야 합니다.": "高解像度化した画像は削除されます。もう一度見るには高解像度化をやり直す必要があります。",
|
"올린 그림은 지워집니다. 다시 보려면 해상도 올리기를 다시 해야 합니다.": "高解像度化した画像は削除されます。もう一度見るには高解像度化をやり直す必要があります。",
|
||||||
"올린 이 {0}": "投稿者 {0}",
|
"올린 이 {0}": "投稿者 {0}",
|
||||||
"옮겼습니다.": "移動しました。",
|
"옮겼습니다.": "移動しました。",
|
||||||
|
"옮기거나 이름을 바꾸지 못했습니다": "移動または名前の変更ができませんでした",
|
||||||
"옮기기": "移動",
|
"옮기기": "移動",
|
||||||
"옮기는 중": "移動中",
|
"옮기는 중": "移動中",
|
||||||
"옮길": "移す",
|
"옮길": "移す",
|
||||||
|
|
@ -1469,10 +1475,10 @@
|
||||||
"옵션": "オプション",
|
"옵션": "オプション",
|
||||||
"옵션을 저장했습니다": "オプションを保存しました",
|
"옵션을 저장했습니다": "オプションを保存しました",
|
||||||
"완료": "完了",
|
"완료": "完了",
|
||||||
|
"완료 시 자동 동영상 추가": "完了時に動画を自動追加",
|
||||||
"완료 정리": "完了を整理",
|
"완료 정리": "完了を整理",
|
||||||
"완료: 성공 {0} / 전체 {1}": "完了: 成功 {0} / 全体 {1}",
|
"완료: 성공 {0} / 전체 {1}": "完了: 成功 {0} / 全体 {1}",
|
||||||
"완료: 성공 {0} · 취소 {1} · 실패 {2} / 전체 {3}": "完了: 成功 {0} · 取消 {1} · 失敗 {2} / 全 {3}",
|
"완료: 성공 {0} · 취소 {1} · 실패 {2} / 전체 {3}": "完了: 成功 {0} · 取消 {1} · 失敗 {2} / 全 {3}",
|
||||||
"완료시 자동 동영상추가": "完了時に動画を自動追加",
|
|
||||||
"외부": "外部",
|
"외부": "外部",
|
||||||
"외부 브라우저": "外部ブラウザー",
|
"외부 브라우저": "外部ブラウザー",
|
||||||
"외부 브라우저로 열기": "外部ブラウザーで開く",
|
"외부 브라우저로 열기": "外部ブラウザーで開く",
|
||||||
|
|
@ -1517,6 +1523,7 @@
|
||||||
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "元のファイルを削除する実行のため、移動する一覧が表示されてから実行できます。",
|
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "元のファイルを削除する実行のため、移動する一覧が表示されてから実行できます。",
|
||||||
"원본을 지우는 작업입니다 (이동)": "元のファイルを削除する作業です (移動)",
|
"원본을 지우는 작업입니다 (이동)": "元のファイルを削除する作業です (移動)",
|
||||||
"원본의 {0}배": "元の {0}倍",
|
"원본의 {0}배": "元の {0}倍",
|
||||||
|
"원본의 것은 모두 대상에 있습니다 (대상에만 있는 것 {0}개는 그대로 둠)": "元のものはすべて対象にあります (対象にだけある {0} 件はそのまま)",
|
||||||
"원본의 빈 폴더 정리 중…": "元の空フォルダーを整理中…",
|
"원본의 빈 폴더 정리 중…": "元の空フォルダーを整理中…",
|
||||||
"원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.": "元のファイルをすべて対象へ移し (元から削除)、元に残った空フォルダーを削除します。",
|
"원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.": "元のファイルをすべて対象へ移し (元から削除)、元に残った空フォルダーを削除します。",
|
||||||
"원본이 대상 안에 있습니다 (환경 설정에서 막아 둠)": "元が対象の中にあります (環境設定で禁止)",
|
"원본이 대상 안에 있습니다 (환경 설정에서 막아 둠)": "元が対象の中にあります (環境設定で禁止)",
|
||||||
|
|
@ -1615,6 +1622,7 @@
|
||||||
"이 언어 지우기": "この言語を削除",
|
"이 언어 지우기": "この言語を削除",
|
||||||
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "この場所 (ネットワークドライブ・ネットワーク共有・USB メモリなど) にはごみ箱がないため、すぐに削除されます。",
|
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "この場所 (ネットワークドライブ・ネットワーク共有・USB メモリなど) にはごみ箱がないため、すぐに削除されます。",
|
||||||
"이 이동 버튼 지우기": "この移動ボタンを削除",
|
"이 이동 버튼 지우기": "この移動ボタンを削除",
|
||||||
|
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "この移動を次に行うときにこのオプションを使います。今すぐ新しいオプションで実行しますか?",
|
||||||
"이 파일에 맞는 자막 우선 (영상 해시)": "このファイルに合う字幕を優先(動画ハッシュ)",
|
"이 파일에 맞는 자막 우선 (영상 해시)": "このファイルに合う字幕を優先(動画ハッシュ)",
|
||||||
"이 파일용": "このファイル用",
|
"이 파일용": "このファイル用",
|
||||||
"이 파일을 열 수 있는 앱이 없습니다.": "このファイルを開けるアプリがありません。",
|
"이 파일을 열 수 있는 앱이 없습니다.": "このファイルを開けるアプリがありません。",
|
||||||
|
|
@ -1914,6 +1922,7 @@
|
||||||
"지금 관리자 권한으로 실행 중입니다. 끄면 (기본) Chrome 엔진을 시작하지 않고 Edge 를 권합니다. 켜면 샌드박스 없이 관리자 권한으로 웹 페이지를 돌립니다.": "現在、管理者権限で実行中です。オフ (既定) の場合は Chrome エンジンを起動せず Edge をおすすめします。オンにすると、サンドボックスなしで管理者権限のまま Web ページを動かします。",
|
"지금 관리자 권한으로 실행 중입니다. 끄면 (기본) Chrome 엔진을 시작하지 않고 Edge 를 권합니다. 켜면 샌드박스 없이 관리자 권한으로 웹 페이지를 돌립니다.": "現在、管理者権限で実行中です。オフ (既定) の場合は Chrome エンジンを起動せず Edge をおすすめします。オンにすると、サンドボックスなしで管理者権限のまま Web ページを動かします。",
|
||||||
"지금 끄기": "今すぐ終了",
|
"지금 끄기": "今すぐ終了",
|
||||||
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "実行中のコピーは前のオプションで動いています。止めて新しいオプションで再実行しますか?",
|
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "実行中のコピーは前のオプションで動いています。止めて新しいオプションで再実行しますか?",
|
||||||
|
"지금 도는 이동은 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "実行中の移動は前のオプションで動いています。止めて新しいオプションでやり直しますか?",
|
||||||
"지금 마스터 (또는 최상) 비밀번호": "現在のマスター (または最上位) パスワード",
|
"지금 마스터 (또는 최상) 비밀번호": "現在のマスター (または最上位) パスワード",
|
||||||
"지금 맞추기": "今すぐ同期",
|
"지금 맞추기": "今すぐ同期",
|
||||||
"지금 맞추는 중입니다. 끝난 뒤에 하세요.": "同期中です。終わってから行ってください。",
|
"지금 맞추는 중입니다. 끝난 뒤에 하세요.": "同期中です。終わってから行ってください。",
|
||||||
|
|
@ -1944,6 +1953,7 @@
|
||||||
"지우면 휴지통으로": "削除するとごみ箱へ",
|
"지우면 휴지통으로": "削除するとごみ箱へ",
|
||||||
"지우지 못한 파일: {0}": "削除できなかったファイル: {0}",
|
"지우지 못한 파일: {0}": "削除できなかったファイル: {0}",
|
||||||
"지우지 못함: {0}": "削除できません: {0}",
|
"지우지 못함: {0}": "削除できません: {0}",
|
||||||
|
"지우지 못했습니다 (권한이 없거나 사용 중)": "削除できませんでした (権限がないか使用中)",
|
||||||
"지우지 못했습니다: {0}": "削除できませんでした: {0}",
|
"지우지 못했습니다: {0}": "削除できませんでした: {0}",
|
||||||
"지우지 않고 맞춤 · 대상에만 있는 {0}개를 지울지 확인 필요": "削除せずに同期・対象にだけある {0} 個を削除するか確認が必要",
|
"지우지 않고 맞춤 · 대상에만 있는 {0}개를 지울지 확인 필요": "削除せずに同期・対象にだけある {0} 個を削除するか確認が必要",
|
||||||
"지우지 않기 (지우기 끄기)": "削除しない (削除をオフ)",
|
"지우지 않기 (지우기 끄기)": "削除しない (削除をオフ)",
|
||||||
|
|
@ -2089,7 +2099,9 @@
|
||||||
"파일 폴더": "ファイル フォルダー",
|
"파일 폴더": "ファイル フォルダー",
|
||||||
"파일 확인 중…": "ファイルを確認中…",
|
"파일 확인 중…": "ファイルを確認中…",
|
||||||
"파일만 골랐을 때": "ファイルだけを選んだとき",
|
"파일만 골랐을 때": "ファイルだけを選んだとき",
|
||||||
|
"파일을 복사하지 못했습니다": "ファイルをコピーできませんでした",
|
||||||
"파일을 분석할 수 없습니다: {0}": "ファイルを分析できません: {0}",
|
"파일을 분석할 수 없습니다: {0}": "ファイルを分析できません: {0}",
|
||||||
|
"파일을 읽지 못했습니다": "ファイルを読み取れませんでした",
|
||||||
"파일이 깨졌습니다": "ファイルが壊れています",
|
"파일이 깨졌습니다": "ファイルが壊れています",
|
||||||
"파일이 깨졌습니다 · [다시 받기] 를 눌러 주세요": "ファイルが壊れています・[再ダウンロード] を押してください",
|
"파일이 깨졌습니다 · [다시 받기] 를 눌러 주세요": "ファイルが壊れています・[再ダウンロード] を押してください",
|
||||||
"파일이 보이지 않습니다: \"모든 파일에 대한 접근\" 권한이 없습니다": "ファイルが表示されません: 「すべてのファイルへのアクセス」権限がありません",
|
"파일이 보이지 않습니다: \"모든 파일에 대한 접근\" 권한이 없습니다": "ファイルが表示されません: 「すべてのファイルへのアクセス」権限がありません",
|
||||||
|
|
|
||||||
|
|
@ -439,10 +439,12 @@
|
||||||
"{0} 이 비어 있습니다 (카드 · USB · 디스크를 넣은 뒤 다시 누르세요).": "{0} 为空 (插入存储卡、U 盘或光盘后再点按)。",
|
"{0} 이 비어 있습니다 (카드 · USB · 디스크를 넣은 뒤 다시 누르세요).": "{0} 为空 (插入存储卡、U 盘或光盘后再点按)。",
|
||||||
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg 中没有 {0} 编码器。",
|
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg 中没有 {0} 编码器。",
|
||||||
"{0} 전체": "{0} 全部",
|
"{0} 전체": "{0} 全部",
|
||||||
|
"{0} 접기": "折叠 {0}",
|
||||||
"{0} 제거": "移除 {0}",
|
"{0} 제거": "移除 {0}",
|
||||||
"{0} 추가": "添加 {0}",
|
"{0} 추가": "添加 {0}",
|
||||||
"{0} 파일": "{0} 文件",
|
"{0} 파일": "{0} 文件",
|
||||||
"{0} 페이지를 지울까요? (저장해야 파일에 반영됩니다)": "删除第 {0} 页? (保存后才会写入文件)",
|
"{0} 페이지를 지울까요? (저장해야 파일에 반영됩니다)": "删除第 {0} 页? (保存后才会写入文件)",
|
||||||
|
"{0} 펼치기": "展开 {0}",
|
||||||
"{0} 폴더가 없거나 열 수 없어 내역에서 뺐습니다": "文件夹 {0} 不存在或无法打开,已从历史记录中移除",
|
"{0} 폴더가 없거나 열 수 없어 내역에서 뺐습니다": "文件夹 {0} 不存在或无法打开,已从历史记录中移除",
|
||||||
"{0}% 에서 2분 넘게 나아가지 않아 멈췄습니다. 망이 Tor 를 막고 있거나 느릴 수 있습니다. 잠시 뒤 [다시 시도] 하세요.": "在 {0}% 停滞超过 2 分钟,已停止。网络可能在阻止 Tor 或速度较慢。请稍后点 [重试]。",
|
"{0}% 에서 2분 넘게 나아가지 않아 멈췄습니다. 망이 Tor 를 막고 있거나 느릴 수 있습니다. 잠시 뒤 [다시 시도] 하세요.": "在 {0}% 停滞超过 2 分钟,已停止。网络可能在阻止 Tor 或速度较慢。请稍后点 [重试]。",
|
||||||
"{0}. {1} (대기)": "{0}. {1}(等待)",
|
"{0}. {1} (대기)": "{0}. {1}(等待)",
|
||||||
|
|
@ -926,6 +928,7 @@
|
||||||
"마지막으로: {0}": "到最后: {0}",
|
"마지막으로: {0}": "到最后: {0}",
|
||||||
"만든 그림 · 저장: {0}": "生成的图片 · 保存位置: {0}",
|
"만든 그림 · 저장: {0}": "生成的图片 · 保存位置: {0}",
|
||||||
"만들 자막 언어 (파일명_언어코드.srt)": "要生成的字幕语言(文件名_语言代码.srt)",
|
"만들 자막 언어 (파일명_언어코드.srt)": "要生成的字幕语言(文件名_语言代码.srt)",
|
||||||
|
"만들거나 쓰지 못했습니다": "无法创建或写入",
|
||||||
"만들기": "生成",
|
"만들기": "生成",
|
||||||
"만들다 만 파일 {0}개": "{0} 个未完成的文件",
|
"만들다 만 파일 {0}개": "{0} 个未完成的文件",
|
||||||
"만들다 만 파일 {0}개를 지울까요?": "删除 {0} 个未完成的文件吗?",
|
"만들다 만 파일 {0}개를 지울까요?": "删除 {0} 个未完成的文件吗?",
|
||||||
|
|
@ -1118,6 +1121,7 @@
|
||||||
"복사 실패 ({0}): {1}": "复制失败 ({0}): {1}",
|
"복사 실패 ({0}): {1}": "复制失败 ({0}): {1}",
|
||||||
"복사 작업이라 칸의 원본 지우기 옵션 (--remove-source-files · /MOV) 은 쓰지 않습니다. 옮기려면 Rsync 화면에서 [원본 파일 지우기] 를 켜세요": "这是复制任务,栏中的删除源文件选项(--remove-source-files · /MOV)不会使用。要移动,请在 Rsync 界面打开 [删除源文件]",
|
"복사 작업이라 칸의 원본 지우기 옵션 (--remove-source-files · /MOV) 은 쓰지 않습니다. 옮기려면 Rsync 화면에서 [원본 파일 지우기] 를 켜세요": "这是复制任务,栏中的删除源文件选项(--remove-source-files · /MOV)不会使用。要移动,请在 Rsync 界面打开 [删除源文件]",
|
||||||
"복사(Ctrl+C)한 주소 자동 다운로드": "自动下载复制(Ctrl+C)的地址",
|
"복사(Ctrl+C)한 주소 자동 다운로드": "自动下载复制(Ctrl+C)的地址",
|
||||||
|
"복사로 맞춤": "以复制同步",
|
||||||
"복사하는 중": "正在复制",
|
"복사하는 중": "正在复制",
|
||||||
"복사할": "复制",
|
"복사할": "复制",
|
||||||
"복사할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "请选择要复制的项目 (点按或用右侧圆圈标记)。",
|
"복사할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "请选择要复制的项目 (点按或用右侧圆圈标记)。",
|
||||||
|
|
@ -1318,6 +1322,7 @@
|
||||||
"실시간 동기화: 원본 폴더": "实时同步: 源文件夹",
|
"실시간 동기화: 원본 폴더": "实时同步: 源文件夹",
|
||||||
"실시간 동기화가 멈췄습니다": "实时同步已停止",
|
"실시간 동기화가 멈췄습니다": "实时同步已停止",
|
||||||
"실시간 동기화가 없습니다. [추가] 하거나 복사 · rsync 탭에서 [lsync 로 이동] 하세요.": "没有实时同步。请 [添加],或在 复制 · rsync 标签中 [移到 lsync]。",
|
"실시간 동기화가 없습니다. [추가] 하거나 복사 · rsync 탭에서 [lsync 로 이동] 하세요.": "没有实时同步。请 [添加],或在 复制 · rsync 标签中 [移到 lsync]。",
|
||||||
|
"실시간 동기화는 원본을 지우지 않습니다 (복사로 맞춤). 원본을 비우려면 lsync 의 [최종 정리] 를 쓰세요.": "实时同步不会删除源(以复制方式同步)。要清空源,请使用 lsync 的 [最终整理]。",
|
||||||
"실시간 동기화를 지울까요?": "删除此实时同步吗?",
|
"실시간 동기화를 지울까요?": "删除此实时同步吗?",
|
||||||
"실시간 동기화의 [최종 정리]": "实时同步的 [最终整理]",
|
"실시간 동기화의 [최종 정리]": "实时同步的 [最终整理]",
|
||||||
"실제 실행: {0}": "实际执行:{0}",
|
"실제 실행: {0}": "实际执行:{0}",
|
||||||
|
|
@ -1461,6 +1466,7 @@
|
||||||
"올린 그림은 지워집니다. 다시 보려면 해상도 올리기를 다시 해야 합니다.": "提升后的图片将被删除。要再次查看需要重新提升分辨率。",
|
"올린 그림은 지워집니다. 다시 보려면 해상도 올리기를 다시 해야 합니다.": "提升后的图片将被删除。要再次查看需要重新提升分辨率。",
|
||||||
"올린 이 {0}": "上传者 {0}",
|
"올린 이 {0}": "上传者 {0}",
|
||||||
"옮겼습니다.": "已移动。",
|
"옮겼습니다.": "已移动。",
|
||||||
|
"옮기거나 이름을 바꾸지 못했습니다": "无法移动或重命名",
|
||||||
"옮기기": "移动",
|
"옮기기": "移动",
|
||||||
"옮기는 중": "正在移动",
|
"옮기는 중": "正在移动",
|
||||||
"옮길": "移动",
|
"옮길": "移动",
|
||||||
|
|
@ -1469,10 +1475,10 @@
|
||||||
"옵션": "选项",
|
"옵션": "选项",
|
||||||
"옵션을 저장했습니다": "已保存选项",
|
"옵션을 저장했습니다": "已保存选项",
|
||||||
"완료": "完成",
|
"완료": "完成",
|
||||||
|
"완료 시 자동 동영상 추가": "完成后自动添加视频",
|
||||||
"완료 정리": "清理已完成",
|
"완료 정리": "清理已完成",
|
||||||
"완료: 성공 {0} / 전체 {1}": "完成: 成功 {0} / 共 {1}",
|
"완료: 성공 {0} / 전체 {1}": "完成: 成功 {0} / 共 {1}",
|
||||||
"완료: 성공 {0} · 취소 {1} · 실패 {2} / 전체 {3}": "完成:成功 {0} · 取消 {1} · 失败 {2} / 共 {3}",
|
"완료: 성공 {0} · 취소 {1} · 실패 {2} / 전체 {3}": "完成:成功 {0} · 取消 {1} · 失败 {2} / 共 {3}",
|
||||||
"완료시 자동 동영상추가": "完成后自动添加视频",
|
|
||||||
"외부": "外部",
|
"외부": "外部",
|
||||||
"외부 브라우저": "外部浏览器",
|
"외부 브라우저": "外部浏览器",
|
||||||
"외부 브라우저로 열기": "在外部浏览器中打开",
|
"외부 브라우저로 열기": "在外部浏览器中打开",
|
||||||
|
|
@ -1517,6 +1523,7 @@
|
||||||
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "此操作会删除源文件,需在显示要移动的列表后才能执行。",
|
"원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.": "此操作会删除源文件,需在显示要移动的列表后才能执行。",
|
||||||
"원본을 지우는 작업입니다 (이동)": "此任务会删除源文件(移动)",
|
"원본을 지우는 작업입니다 (이동)": "此任务会删除源文件(移动)",
|
||||||
"원본의 {0}배": "原始的 {0} 倍",
|
"원본의 {0}배": "原始的 {0} 倍",
|
||||||
|
"원본의 것은 모두 대상에 있습니다 (대상에만 있는 것 {0}개는 그대로 둠)": "源中的内容都已在目标中(仅在目标中的 {0} 项保持不变)",
|
||||||
"원본의 빈 폴더 정리 중…": "正在清理源中的空文件夹…",
|
"원본의 빈 폴더 정리 중…": "正在清理源中的空文件夹…",
|
||||||
"원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.": "将源中的所有文件移到目标 (从源中删除),然后删除源中剩下的空文件夹。",
|
"원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.": "将源中的所有文件移到目标 (从源中删除),然后删除源中剩下的空文件夹。",
|
||||||
"원본이 대상 안에 있습니다 (환경 설정에서 막아 둠)": "源位于目标内 (已在设置中阻止)",
|
"원본이 대상 안에 있습니다 (환경 설정에서 막아 둠)": "源位于目标内 (已在设置中阻止)",
|
||||||
|
|
@ -1615,6 +1622,7 @@
|
||||||
"이 언어 지우기": "删除此语言",
|
"이 언어 지우기": "删除此语言",
|
||||||
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "此位置 (网络驱动器 · 网络共享 · U 盘等) 没有回收站,会直接删除。",
|
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "此位置 (网络驱动器 · 网络共享 · U 盘等) 没有回收站,会直接删除。",
|
||||||
"이 이동 버튼 지우기": "删除此移动按钮",
|
"이 이동 버튼 지우기": "删除此移动按钮",
|
||||||
|
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "下次执行此移动时将使用这些选项。现在用新选项执行吗?",
|
||||||
"이 파일에 맞는 자막 우선 (영상 해시)": "优先匹配此文件的字幕(视频哈希)",
|
"이 파일에 맞는 자막 우선 (영상 해시)": "优先匹配此文件的字幕(视频哈希)",
|
||||||
"이 파일용": "适用于此文件",
|
"이 파일용": "适用于此文件",
|
||||||
"이 파일을 열 수 있는 앱이 없습니다.": "没有可以打开此文件的应用。",
|
"이 파일을 열 수 있는 앱이 없습니다.": "没有可以打开此文件的应用。",
|
||||||
|
|
@ -1914,6 +1922,7 @@
|
||||||
"지금 관리자 권한으로 실행 중입니다. 끄면 (기본) Chrome 엔진을 시작하지 않고 Edge 를 권합니다. 켜면 샌드박스 없이 관리자 권한으로 웹 페이지를 돌립니다.": "当前正以管理员权限运行。关闭时(默认)不启动 Chrome 引擎并建议使用 Edge。开启后将在无沙盒的情况下以管理员权限运行网页。",
|
"지금 관리자 권한으로 실행 중입니다. 끄면 (기본) Chrome 엔진을 시작하지 않고 Edge 를 권합니다. 켜면 샌드박스 없이 관리자 권한으로 웹 페이지를 돌립니다.": "当前正以管理员权限运行。关闭时(默认)不启动 Chrome 引擎并建议使用 Edge。开启后将在无沙盒的情况下以管理员权限运行网页。",
|
||||||
"지금 끄기": "立即关闭",
|
"지금 끄기": "立即关闭",
|
||||||
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "正在运行的复制使用的是之前的选项。停止并用新选项重新运行吗?",
|
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "正在运行的复制使用的是之前的选项。停止并用新选项重新运行吗?",
|
||||||
|
"지금 도는 이동은 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "正在进行的移动使用的是之前的选项。要停止并用新选项重新执行吗?",
|
||||||
"지금 마스터 (또는 최상) 비밀번호": "当前主密码 (或最高密码)",
|
"지금 마스터 (또는 최상) 비밀번호": "当前主密码 (或最高密码)",
|
||||||
"지금 맞추기": "立即同步",
|
"지금 맞추기": "立即同步",
|
||||||
"지금 맞추는 중입니다. 끝난 뒤에 하세요.": "正在同步。请等结束后再进行。",
|
"지금 맞추는 중입니다. 끝난 뒤에 하세요.": "正在同步。请等结束后再进行。",
|
||||||
|
|
@ -1944,6 +1953,7 @@
|
||||||
"지우면 휴지통으로": "删除时移到回收站",
|
"지우면 휴지통으로": "删除时移到回收站",
|
||||||
"지우지 못한 파일: {0}": "无法删除的文件: {0}",
|
"지우지 못한 파일: {0}": "无法删除的文件: {0}",
|
||||||
"지우지 못함: {0}": "无法删除: {0}",
|
"지우지 못함: {0}": "无法删除: {0}",
|
||||||
|
"지우지 못했습니다 (권한이 없거나 사용 중)": "无法删除(没有权限或正在使用)",
|
||||||
"지우지 못했습니다: {0}": "无法删除: {0}",
|
"지우지 못했습니다: {0}": "无法删除: {0}",
|
||||||
"지우지 않고 맞춤 · 대상에만 있는 {0}개를 지울지 확인 필요": "已同步但未删除 · 需确认是否删除仅在目标中的 {0} 个项目",
|
"지우지 않고 맞춤 · 대상에만 있는 {0}개를 지울지 확인 필요": "已同步但未删除 · 需确认是否删除仅在目标中的 {0} 个项目",
|
||||||
"지우지 않기 (지우기 끄기)": "不删除 (关闭删除)",
|
"지우지 않기 (지우기 끄기)": "不删除 (关闭删除)",
|
||||||
|
|
@ -2089,7 +2099,9 @@
|
||||||
"파일 폴더": "文件夹",
|
"파일 폴더": "文件夹",
|
||||||
"파일 확인 중…": "正在检查文件…",
|
"파일 확인 중…": "正在检查文件…",
|
||||||
"파일만 골랐을 때": "只选择文件时",
|
"파일만 골랐을 때": "只选择文件时",
|
||||||
|
"파일을 복사하지 못했습니다": "无法复制文件",
|
||||||
"파일을 분석할 수 없습니다: {0}": "无法分析文件: {0}",
|
"파일을 분석할 수 없습니다: {0}": "无法分析文件: {0}",
|
||||||
|
"파일을 읽지 못했습니다": "无法读取文件",
|
||||||
"파일이 깨졌습니다": "文件已损坏",
|
"파일이 깨졌습니다": "文件已损坏",
|
||||||
"파일이 깨졌습니다 · [다시 받기] 를 눌러 주세요": "文件已损坏 · 请点按 [重新下载]",
|
"파일이 깨졌습니다 · [다시 받기] 를 눌러 주세요": "文件已损坏 · 请点按 [重新下载]",
|
||||||
"파일이 보이지 않습니다: \"모든 파일에 대한 접근\" 권한이 없습니다": "文件不可见: 没有“所有文件访问”权限",
|
"파일이 보이지 않습니다: \"모든 파일에 대한 접근\" 권한이 없습니다": "文件不可见: 没有“所有文件访问”权限",
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ class CopyCenter extends ChangeNotifier {
|
||||||
if (job.leftInSource > 0) trf('{0}개는 원본에 남음 (건너뜀)', [job.leftInSource]),
|
if (job.leftInSource > 0) trf('{0}개는 원본에 남음 (건너뜀)', [job.leftInSource]),
|
||||||
if (job.pruned > 0) trf('빈 폴더 {0}개 지움', [job.pruned]),
|
if (job.pruned > 0) trf('빈 폴더 {0}개 지움', [job.pruned]),
|
||||||
].join(' · ')
|
].join(' · ')
|
||||||
: userErrorText(e), // 237: 예외 이름 없이 (원래 글은 작업 기록에)
|
: userErrorText(e, sources: t.sources), // 237 · 245: 예외 이름 없이 · 화면 언어로 (원래 글은 작업 기록에)
|
||||||
lastFiles: job.doneFiles,
|
lastFiles: job.doneFiles,
|
||||||
lastLeft: e == null ? job.leftInSource : 0,
|
lastLeft: e == null ? job.leftInSource : 0,
|
||||||
lastDetail: e == null || e is FileOpCancelled ? '' : '$e',
|
lastDetail: e == null || e is FileOpCancelled ? '' : '$e',
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class DownloadManager extends ChangeNotifier {
|
||||||
/// 받은 동영상을 편집 목록(MKV 만들기)에 넣는 함수 (새로 넣은 개수를 돌려줌). 앱이 연결해 준다.
|
/// 받은 동영상을 편집 목록(MKV 만들기)에 넣는 함수 (새로 넣은 개수를 돌려줌). 앱이 연결해 준다.
|
||||||
Future<int> Function(List<String> files)? addToEditList;
|
Future<int> Function(List<String> files)? addToEditList;
|
||||||
|
|
||||||
/// "완료시 자동 동영상추가" 를 켜고 끄는 함수 (설정에 저장). 앱이 연결해 준다.
|
/// "완료 시 자동 동영상 추가" 를 켜고 끄는 함수 (설정에 저장). 앱이 연결해 준다.
|
||||||
Future<void> Function(bool on)? setAutoAdd;
|
Future<void> Function(bool on)? setAutoAdd;
|
||||||
|
|
||||||
/// 설정이 바뀌었을 때 화면 갱신
|
/// 설정이 바뀌었을 때 화면 갱신
|
||||||
|
|
|
||||||
|
|
@ -247,10 +247,28 @@ class LiveSync extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
pending[k] = await diff(x);
|
pending[k] = await diff(x);
|
||||||
|
await _countOnlyTarget(x);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 244: 지우기 포함이 아닌 쌍에서 대상에만 있는 항목 수 (맞출 것이 없을 때 "같습니다" 대신 사실대로 보이려고)
|
||||||
|
final onlyTarget = <String, int>{};
|
||||||
|
|
||||||
|
Future<void> _countOnlyTarget(LiveSyncPair x) async {
|
||||||
|
final k = keyOf(x);
|
||||||
|
if (x.delete || (pending[k]?.isNotEmpty ?? true)) {
|
||||||
|
onlyTarget.remove(k);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 지우기 포함으로 셌을 때 "지울 것" (− ) 이 곧 대상에만 있는 것
|
||||||
|
try {
|
||||||
|
onlyTarget[k] = [for (final d in await diff(x.copyWith(delete: true), limit: 10000)) if (d.startsWith('− ')) d].length;
|
||||||
|
} catch (_) {
|
||||||
|
onlyTarget.remove(k); // 세지 못하면 예전 글
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static String keyOf(LiveSyncPair x) => '${x.source}=>${x.target}';
|
static String keyOf(LiveSyncPair x) => '${x.source}=>${x.target}';
|
||||||
|
|
||||||
/// 지우기를 확인하지 않은 쌍: 대상에만 있어 지워질 항목 (원본 기준 상대 경로). 확인 창에 보여 준다 (42)
|
/// 지우기를 확인하지 않은 쌍: 대상에만 있어 지워질 항목 (원본 기준 상대 경로). 확인 창에 보여 준다 (42)
|
||||||
|
|
@ -401,6 +419,7 @@ class LiveSync extends ChangeNotifier {
|
||||||
problems.remove(k);
|
problems.remove(k);
|
||||||
emptyDeletes.remove(k);
|
emptyDeletes.remove(k);
|
||||||
pending[k] = await diff(asked);
|
pending[k] = await diff(asked);
|
||||||
|
await _countOnlyTarget(asked);
|
||||||
if (n != 0) c.note(trf('실시간 동기화 ({0}): {1} → {2}', [method.label, x.source, x.target]));
|
if (n != 0) c.note(trf('실시간 동기화 ({0}): {1} → {2}', [method.label, x.source, x.target]));
|
||||||
} on SourceUnreadableException catch (e) {
|
} on SourceUnreadableException catch (e) {
|
||||||
// 68 · 70: 대상은 건드리지 않고 멈춤. 카드에 빨갛게 · 작업 알림에 · 로그에
|
// 68 · 70: 대상은 건드리지 않고 멈춤. 카드에 빨갛게 · 작업 알림에 · 로그에
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,39 @@ import '../l10n/tr.dart';
|
||||||
/// 복사 · 이동 한 번 (파일 탐색기). 방법 (현재 방식 · rsync · robocopy) 과 상관없이 같은 진행 상태를 알린다:
|
/// 복사 · 이동 한 번 (파일 탐색기). 방법 (현재 방식 · rsync · robocopy) 과 상관없이 같은 진행 상태를 알린다:
|
||||||
/// - 위쪽: 고른 항목 (폴더) 중 몇 번째인지 · 전체 진행률
|
/// - 위쪽: 고른 항목 (폴더) 중 몇 번째인지 · 전체 진행률
|
||||||
/// - 아래쪽: 지금 항목 (폴더) 의 파일 중 몇 개를 했는지
|
/// - 아래쪽: 지금 항목 (폴더) 의 파일 중 몇 개를 했는지
|
||||||
/// 215: rsync 가 실패했을 때 보일 글 - 원본을 못 지웠으면 쉬운 말 한 줄, 그리고 오류 · 권한 줄을 끝쪽부터 (전송한 파일 이름이 아니라).
|
|
||||||
/// 예전에는 출력의 앞쪽 8줄이라 파일 이름만 보이고 "Permission denied" · "rsync error … (code 23)" 가 빠지기 쉬웠다
|
|
||||||
/// 237: 사용자에게 보이는 실패 글 - "ProcessException:" 같은 예외 이름 · "Command:" 줄은 빼고 내용만.
|
/// 237: 사용자에게 보이는 실패 글 - "ProcessException:" 같은 예외 이름 · "Command:" 줄은 빼고 내용만.
|
||||||
/// 원래 글 ('$e') 은 작업 기록에 그대로 남긴다
|
/// 원래 글 ('$e') 은 작업 기록에 그대로 남긴다.
|
||||||
String userErrorText(Object e) => switch (e) {
|
/// 245: 앱이 직접 할 때의 파일 오류 (Dart 의 "Cannot delete file" 등 영어) 는 화면 언어로. [sources] 안의 파일을 못 지웠으면
|
||||||
|
/// rsync 때 (215) 와 같은 글 - 이동에서 원본만 남은 것
|
||||||
|
String userErrorText(Object e, {List<String> sources = const []}) => switch (e) {
|
||||||
ProcessException(:final message) when message.isNotEmpty => message,
|
ProcessException(:final message) when message.isNotEmpty => message,
|
||||||
FileSystemException(:final message, :final path, :final osError) =>
|
FileSystemException(:final message, :final path, :final osError) => [
|
||||||
[message, if (path != null && path.isNotEmpty) path, if (osError != null && osError.message.isNotEmpty) osError.message].join(' · '),
|
_fileErrorText(message, path, sources),
|
||||||
|
if (path != null && path.isNotEmpty) path,
|
||||||
|
if (osError != null && osError.message.isNotEmpty) osError.message,
|
||||||
|
].join(' · '),
|
||||||
_ => '$e'.replaceFirst(RegExp(r'^\w*(Exception|Error)(\([^)]*\))?: '), ''),
|
_ => '$e'.replaceFirst(RegExp(r'^\w*(Exception|Error)(\([^)]*\))?: '), ''),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
String _fileErrorText(String message, String? path, List<String> sources) {
|
||||||
|
final m = message.toLowerCase();
|
||||||
|
if (m.startsWith('cannot delete') || m.startsWith('deletion failed')) {
|
||||||
|
final inSource = path != null && sources.any((s) => isSameOrInside(path, s));
|
||||||
|
return inSource
|
||||||
|
? tr('원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다.')
|
||||||
|
: tr('지우지 못했습니다 (권한이 없거나 사용 중)');
|
||||||
|
}
|
||||||
|
if (m.startsWith('cannot copy')) return tr('파일을 복사하지 못했습니다');
|
||||||
|
if (m.startsWith('cannot rename')) return tr('옮기거나 이름을 바꾸지 못했습니다');
|
||||||
|
if (m.startsWith('cannot open') || m.startsWith('cannot read')) return tr('파일을 읽지 못했습니다');
|
||||||
|
if (m.startsWith('cannot create') || m.startsWith('creation failed') || m.startsWith('cannot write')) {
|
||||||
|
return tr('만들거나 쓰지 못했습니다');
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 215: rsync 가 실패했을 때 보일 글 - 원본을 못 지웠으면 쉬운 말 한 줄, 그리고 오류 · 권한 줄을 끝쪽부터 (전송한 파일 이름이 아니라).
|
||||||
|
/// 예전에는 출력의 앞쪽 8줄이라 파일 이름만 보이고 "Permission denied" · "rsync error … (code 23)" 가 빠지기 쉬웠다
|
||||||
String rsyncFailureText(int code, List<String> errLines, List<String> allLines) {
|
String rsyncFailureText(int code, List<String> errLines, List<String> allLines) {
|
||||||
final all = [...errLines, ...allLines];
|
final all = [...errLines, ...allLines];
|
||||||
final removeFailed = all.any((l) {
|
final removeFailed = all.any((l) {
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,22 @@ bool isCompact(BuildContext context) => MediaQuery.sizeOf(context).width < compa
|
||||||
/// 모든 화면의 위쪽 막대: [nav] [middle (남는 너비)] [actions].
|
/// 모든 화면의 위쪽 막대: [nav] [middle (남는 너비)] [actions].
|
||||||
/// 좁은 화면에서는 두 줄 - 위: [nav] … [actions], 아래: [middle] 이 한 줄을 다 쓴다.
|
/// 좁은 화면에서는 두 줄 - 위: [nav] … [actions], 아래: [middle] 이 한 줄을 다 쓴다.
|
||||||
/// [minMiddleWidth] (201): 한 줄일 때 middle 이 이보다 좁아지면 (폰 가로처럼 넓지만 버튼이 폭을 다 차지할 때) 두 줄로.
|
/// [minMiddleWidth] (201): 한 줄일 때 middle 이 이보다 좁아지면 (폰 가로처럼 넓지만 버튼이 폭을 다 차지할 때) 두 줄로.
|
||||||
/// 브라우저 주소 칸처럼 middle 이 꼭 보여야 하는 화면이 준다 (0 이면 폭만으로 정함)
|
/// 브라우저 주소 칸처럼 middle 이 꼭 보여야 하는 화면이 준다 (0 이면 폭만으로 정함).
|
||||||
|
/// [trailing] (242): middle 오른쪽에 붙는, 줄지 않는 버튼 묶음 - 이것 때문에 middle 이 [minMiddleWidth] 보다 좁아지면 두 줄
|
||||||
|
/// (어림한 폭이 실제보다 작아 넘친 버튼이 화면 크기 "100%" 를 덮던 것 - 실제로 재어 정한다)
|
||||||
class AppTopBar extends StatefulWidget {
|
class AppTopBar extends StatefulWidget {
|
||||||
final Widget nav;
|
final Widget nav;
|
||||||
final Widget middle;
|
final Widget middle;
|
||||||
|
final Widget? trailing;
|
||||||
final Widget actions;
|
final Widget actions;
|
||||||
final double minMiddleWidth;
|
final double minMiddleWidth;
|
||||||
const AppTopBar({super.key, required this.nav, required this.middle, required this.actions, this.minMiddleWidth = 0});
|
const AppTopBar(
|
||||||
|
{super.key,
|
||||||
|
required this.nav,
|
||||||
|
required this.middle,
|
||||||
|
this.trailing,
|
||||||
|
required this.actions,
|
||||||
|
this.minMiddleWidth = 0});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AppTopBar> createState() => _AppTopBarState();
|
State<AppTopBar> createState() => _AppTopBarState();
|
||||||
|
|
@ -63,7 +72,23 @@ class _AppTopBarState extends State<AppTopBar> {
|
||||||
height: appBarHeight,
|
height: appBarHeight,
|
||||||
color: JjColors.panel,
|
color: JjColors.panel,
|
||||||
padding: const EdgeInsets.only(left: 8, right: appBarRightPadding),
|
padding: const EdgeInsets.only(left: 8, right: appBarRightPadding),
|
||||||
child: Row(children: [nav, const SizedBox(width: 8), Expanded(child: KeyedSubtree(key: _mid, child: middle)), actions]),
|
// 242: middle 이 넘쳐도 오른쪽 (화면 크기 "100%" · 설정 · 종료) 을 덮어 그리지 않게.
|
||||||
|
// trailing 이 있으면 제 폭대로 재고 남는 폭을 middle 에 (모자라면 [_check] 가 두 줄로 - 그사이 넘침 오류 없이 잘라 그림)
|
||||||
|
child: widget.trailing == null
|
||||||
|
? Row(children: [
|
||||||
|
nav,
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(child: ClipRect(child: KeyedSubtree(key: _mid, child: middle))),
|
||||||
|
actions,
|
||||||
|
])
|
||||||
|
: ClipRect(
|
||||||
|
child: CustomMultiChildLayout(delegate: _BarLayout(), children: [
|
||||||
|
LayoutId(id: #nav, child: nav),
|
||||||
|
LayoutId(id: #mid, child: ClipRect(child: KeyedSubtree(key: _mid, child: middle))),
|
||||||
|
LayoutId(id: #trail, child: widget.trailing!),
|
||||||
|
LayoutId(id: #act, child: actions),
|
||||||
|
]),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
|
|
@ -78,12 +103,37 @@ class _AppTopBarState extends State<AppTopBar> {
|
||||||
actions,
|
actions,
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
SizedBox(height: 48, child: Padding(padding: const EdgeInsets.only(left: 6), child: middle)),
|
SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 6),
|
||||||
|
child: widget.trailing == null ? middle : Row(children: [Expanded(child: middle), widget.trailing!]))),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 242: [nav] [middle (남는 폭)] [trailing] [actions] - nav · trailing · actions 는 제 폭대로, 세로 가운데
|
||||||
|
class _BarLayout extends MultiChildLayoutDelegate {
|
||||||
|
@override
|
||||||
|
void performLayout(Size size) {
|
||||||
|
final loose = BoxConstraints(maxHeight: size.height);
|
||||||
|
final nav = layoutChild(#nav, loose);
|
||||||
|
final act = layoutChild(#act, loose);
|
||||||
|
final trail = layoutChild(#trail, loose);
|
||||||
|
final midW = (size.width - nav.width - 8 - trail.width - act.width).clamp(0.0, double.infinity);
|
||||||
|
final mid = layoutChild(#mid, BoxConstraints(minWidth: midW, maxWidth: midW, maxHeight: size.height));
|
||||||
|
double y(Size s) => (size.height - s.height) / 2;
|
||||||
|
positionChild(#nav, Offset(0, y(nav)));
|
||||||
|
positionChild(#mid, Offset(nav.width + 8, y(mid)));
|
||||||
|
positionChild(#trail, Offset(nav.width + 8 + midW, y(trail)));
|
||||||
|
positionChild(#act, Offset(size.width - act.width, y(act)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRelayout(_BarLayout oldDelegate) => false;
|
||||||
|
}
|
||||||
|
|
||||||
/// 앱 전체에서 쓰는 것 (환경 설정을 열 컨트롤러 · 종료 동작) 을 화면들에 내려 준다
|
/// 앱 전체에서 쓰는 것 (환경 설정을 열 컨트롤러 · 종료 동작) 을 화면들에 내려 준다
|
||||||
class AppScope extends InheritedWidget {
|
class AppScope extends InheritedWidget {
|
||||||
final AppController controller;
|
final AppController controller;
|
||||||
|
|
|
||||||
|
|
@ -151,13 +151,7 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
||||||
onChanged: (v) => c.updateSettings((x) => x.copyConflict = v ?? 'ask'),
|
onChanged: (v) => c.updateSettings((x) => x.copyConflict = v ?? 'ask'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 229: 폴더 안의 것을 맞출 때 (모니터링의 현재 방식 · WebDAV) - rsync 의 -u 와 같다
|
_skipNewerTile(),
|
||||||
SwitchListTile(
|
|
||||||
value: s.copySkipNewer,
|
|
||||||
onChanged: (v) => c.updateSettings((x) => x.copySkipNewer = v),
|
|
||||||
title: Text(tr('받는 쪽이 더 새것이면 건너뛰기')),
|
|
||||||
subtitle: Text(tr('현재 방식 · robocopy · WebDAV 로 복사 · 이동할 때 (rsync 의 -u, robocopy 의 /XO 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다')),
|
|
||||||
),
|
|
||||||
SettingTile(
|
SettingTile(
|
||||||
title: Text(tr('속도 제한 (KB/s)')),
|
title: Text(tr('속도 제한 (KB/s)')),
|
||||||
subtitle: Text(tr('0 = 제한 없음. 모든 방법에 적용: rsync --bwlimit, 현재 방식은 앱이 조절, robocopy 는 /IPG 로 비슷하게. '
|
subtitle: Text(tr('0 = 제한 없음. 모든 방법에 적용: rsync --bwlimit, 현재 방식은 앱이 조절, robocopy 는 /IPG 로 비슷하게. '
|
||||||
|
|
@ -178,6 +172,14 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rsync 화면: rsync 옵션 · rsync 가져오기 · 실시간 동기화 (lsync)
|
/// Rsync 화면: rsync 옵션 · rsync 가져오기 · 실시간 동기화 (lsync)
|
||||||
|
/// 229: 받는 쪽이 더 새것이면 건너뛰기 - 파일 탐색기 · Rsync 탭 두 곳에 같은 설정 (Rsync 화면 · 모니터링에서도 쓰여 찾기 쉽게)
|
||||||
|
Widget _skipNewerTile() => SwitchListTile(
|
||||||
|
value: c.settings.copySkipNewer,
|
||||||
|
onChanged: (v) => c.updateSettings((x) => x.copySkipNewer = v),
|
||||||
|
title: Text(tr('받는 쪽이 더 새것이면 건너뛰기')),
|
||||||
|
subtitle: Text(tr('현재 방식 · robocopy · WebDAV 로 복사 · 이동할 때 (rsync 의 -u, robocopy 의 /XO 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다')),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _rsyncPart() {
|
Widget _rsyncPart() {
|
||||||
final s = c.settings;
|
final s = c.settings;
|
||||||
final desk = Platform.isWindows;
|
final desk = Platform.isWindows;
|
||||||
|
|
@ -186,6 +188,7 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
||||||
_options(tr('rsync 옵션'), tr('예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 속도 제한은 파일 탐색기의 속도 제한을 함께 씀. '
|
_options(tr('rsync 옵션'), tr('예: -avPog (보관 · 자세히 · 진행 · 소유자 · 그룹). 속도 제한은 파일 탐색기의 속도 제한을 함께 씀. '
|
||||||
'새로 맞추는 폴더 쌍에 씁니다 - 이미 실행한 쌍은 기억한 옵션을 쓰며 모니터링 > 복사 · rsync 에서 따로 고칩니다'),
|
'새로 맞추는 폴더 쌍에 씁니다 - 이미 실행한 쌍은 기억한 옵션을 쓰며 모니터링 > 복사 · rsync 에서 따로 고칩니다'),
|
||||||
s.rsyncOptions, defaultRsyncOptions, (x, v) => x.rsyncOptions = v),
|
s.rsyncOptions, defaultRsyncOptions, (x, v) => x.rsyncOptions = v),
|
||||||
|
_skipNewerTile(),
|
||||||
SettingTile(
|
SettingTile(
|
||||||
title: Text(tr('rsync 가져오기')),
|
title: Text(tr('rsync 가져오기')),
|
||||||
subtitle: Text(_checking
|
subtitle: Text(_checking
|
||||||
|
|
|
||||||
|
|
@ -119,11 +119,12 @@ class _DownloadsPageState extends State<DownloadsPage> {
|
||||||
Container(
|
Container(
|
||||||
color: JjColors.bg,
|
color: JjColors.bg,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
child: Row(children: [
|
// 246: 좁으면 (폰 가로 등) 버튼들을 다음 줄로 - 예전에는 옆으로 미는 줄이라 앞쪽 글이 잘려 "전체 선택" 옆에 겹쳐 보였다
|
||||||
|
child: Wrap(alignment: WrapAlignment.spaceBetween, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||||
InkWell(
|
InkWell(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
onTap: d.tasks.isEmpty ? null : () => allSel ? d.selectNone() : d.selectAll(),
|
onTap: d.tasks.isEmpty ? null : () => allSel ? d.selectNone() : d.selectAll(),
|
||||||
child: Row(children: [
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
value: allSel ? true : (hasSel ? null : false),
|
value: allSel ? true : (hasSel ? null : false),
|
||||||
tristate: true,
|
tristate: true,
|
||||||
|
|
@ -136,12 +137,7 @@ class _DownloadsPageState extends State<DownloadsPage> {
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
// 좁으면 옆으로 밀어 볼 수 있게 (오른쪽 끝부터 보임)
|
Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
reverse: true,
|
|
||||||
child: Row(children: [
|
|
||||||
if (d.addToEditList != null) ...[
|
if (d.addToEditList != null) ...[
|
||||||
_btn(Icons.playlist_add, tr('동영상 추가'),
|
_btn(Icons.playlist_add, tr('동영상 추가'),
|
||||||
hasSel && d.selectedVideoFiles.isNotEmpty ? () => _addToEditList(context) : null),
|
hasSel && d.selectedVideoFiles.isNotEmpty ? () => _addToEditList(context) : null),
|
||||||
|
|
@ -160,7 +156,7 @@ class _DownloadsPageState extends State<DownloadsPage> {
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(right: 8),
|
padding: EdgeInsets.only(right: 8),
|
||||||
child: Text(tr('완료시 자동 동영상추가'), style: TextStyle(fontSize: 12)),
|
child: Text(tr('완료 시 자동 동영상 추가'), style: TextStyle(fontSize: 12)),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
|
@ -175,9 +171,7 @@ class _DownloadsPageState extends State<DownloadsPage> {
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
_btn(Icons.cleaning_services_outlined, tr('완료 정리'),
|
_btn(Icons.cleaning_services_outlined, tr('완료 정리'),
|
||||||
d.tasks.any((t) => !t.unfinished) ? d.cleanupFinished : null),
|
d.tasks.any((t) => !t.unfinished) ? d.cleanupFinished : null),
|
||||||
]),
|
]),
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
|
|
|
||||||
|
|
@ -3367,12 +3367,19 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
? (loading
|
? (loading
|
||||||
? const Center(
|
? const Center(
|
||||||
child: SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 1.5)))
|
child: SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 1.5)))
|
||||||
: InkResponse(
|
// 241: TalkBack 이 읽는 이름 ("… 펼치기" · "… 접기")
|
||||||
radius: chevronWidth / 2 + 4,
|
: Semantics(
|
||||||
containedInkWell: false,
|
button: true,
|
||||||
|
label: open ? trf('{0} 접기', [name]) : trf('{0} 펼치기', [name]),
|
||||||
|
excludeSemantics: true,
|
||||||
onTap: () => _toggle(pane, e),
|
onTap: () => _toggle(pane, e),
|
||||||
child: Center(
|
child: InkResponse(
|
||||||
child: Icon(open ? Icons.expand_more : Icons.chevron_right, size: 18, color: JjColors.textDim)),
|
radius: chevronWidth / 2 + 4,
|
||||||
|
containedInkWell: false,
|
||||||
|
onTap: () => _toggle(pane, e),
|
||||||
|
child: Center(
|
||||||
|
child: Icon(open ? Icons.expand_more : Icons.chevron_right, size: 18, color: JjColors.textDim)),
|
||||||
|
),
|
||||||
))
|
))
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,7 @@ class _TopBar extends StatelessWidget {
|
||||||
if (need() > w) usage = false;
|
if (need() > w) usage = false;
|
||||||
if (need() > w) dlBox = false;
|
if (need() > w) dlBox = false;
|
||||||
if (need() > w) mkvText = false;
|
if (need() > w) mkvText = false;
|
||||||
|
// 242: 다 줄여도 모자라는지는 어림 (need) 이 아니라 위 막대가 실제로 잰다 (오른쪽 묶음 [trailing] · minMiddleWidth)
|
||||||
|
|
||||||
// 7: 폰에는 "탐색기에서 끌어다 놓기" 가 없다
|
// 7: 폰에는 "탐색기에서 끌어다 놓기" 가 없다
|
||||||
final addTip = Platform.isAndroid ? tr('동영상 추가') : tr('동영상 추가 (탐색기에서 끌어다 놓아도 됩니다)');
|
final addTip = Platform.isAndroid ? tr('동영상 추가') : tr('동영상 추가 (탐색기에서 끌어다 놓아도 됩니다)');
|
||||||
|
|
@ -328,19 +329,21 @@ class _TopBar extends StatelessWidget {
|
||||||
{required bool text, required String tip, Color? color, bool ai = false}) {
|
{required bool text, required String tip, Color? color, bool ai = false}) {
|
||||||
final fg = ai ? aiColor : null;
|
final fg = ai ? aiColor : null;
|
||||||
final side = BorderSide(color: ai && onPressed != null ? aiColor.withValues(alpha: 0.7) : JjColors.border);
|
final side = BorderSide(color: ai && onPressed != null ? aiColor.withValues(alpha: 0.7) : JjColors.border);
|
||||||
|
// 241: 아이콘만 있는 버튼은 이름을 버튼 자체에 (바깥 Tooltip 은 TalkBack 이 읽는 이름이 되지 않았다)
|
||||||
|
if (!text) {
|
||||||
|
return IconButton.outlined(
|
||||||
|
tooltip: tip,
|
||||||
|
onPressed: onPressed,
|
||||||
|
icon: Icon(icon, size: 20, color: onPressed == null ? null : color ?? fg),
|
||||||
|
style: IconButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), side: side));
|
||||||
|
}
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: tip,
|
message: tip,
|
||||||
child: text
|
child: OutlinedButton.icon(
|
||||||
? OutlinedButton.icon(
|
style: ai ? OutlinedButton.styleFrom(foregroundColor: fg, side: side) : null,
|
||||||
style: ai ? OutlinedButton.styleFrom(foregroundColor: fg, side: side) : null,
|
onPressed: onPressed,
|
||||||
onPressed: onPressed,
|
icon: Icon(icon, size: 18, color: color ?? fg),
|
||||||
icon: Icon(icon, size: 18, color: color ?? fg),
|
label: Text(label)),
|
||||||
label: Text(label))
|
|
||||||
: IconButton.outlined(
|
|
||||||
onPressed: onPressed,
|
|
||||||
icon: Icon(icon, size: 20, color: onPressed == null ? null : color ?? fg),
|
|
||||||
style: IconButton.styleFrom(
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), side: side)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -407,6 +410,10 @@ class _TopBar extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppTopBar(
|
return AppTopBar(
|
||||||
|
// 242: 작업 중 (진행 표시 · 취소) 이 생기거나 없어지면 다시 잰다
|
||||||
|
key: ValueKey(('mkvTopBar', c.busy)),
|
||||||
|
// 242: 오른쪽 묶음 때문에 왼쪽 (동영상 추가 · AI) 이 아이콘 하나보다 좁아지면 두 줄
|
||||||
|
minMiddleWidth: 56,
|
||||||
// 모든 화면 공통: [JJ 홈] [MKV 화면] [뒤로] [다운로드 목록]
|
// 모든 화면 공통: [JJ 홈] [MKV 화면] [뒤로] [다운로드 목록]
|
||||||
nav: const AppNavButtons(onMkvPage: true),
|
nav: const AppNavButtons(onMkvPage: true),
|
||||||
// 화면 크기 · 환경 설정 · 종료: 모든 화면에서 같은 자리
|
// 화면 크기 · 환경 설정 · 종료: 모든 화면에서 같은 자리
|
||||||
|
|
@ -431,6 +438,12 @@ class _TopBar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// 오른쪽 묶음 (줄지 않음): 작업 · 재생 · MKV 만들기 · 결과 폴더 · 다운로드
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
if (c.busy) ...[
|
if (c.busy) ...[
|
||||||
JobIndicator(c: c, compact: !jobText, iconOnly: !jobText),
|
JobIndicator(c: c, compact: !jobText, iconOnly: !jobText),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|
@ -453,7 +466,8 @@ class _TopBar extends StatelessWidget {
|
||||||
],
|
],
|
||||||
// 체크한 동영상이 있으면 그것만, 없으면 "전체 만들기 / 취소" 를 묻는다
|
// 체크한 동영상이 있으면 그것만, 없으면 "전체 만들기 / 취소" 를 묻는다
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: trf('MKV 만들기{0}', [count]),
|
// 241: 아이콘만일 때는 버튼 자체의 tooltip (이름) 을 쓰고 바깥은 비운다 (두 번 뜨지 않게)
|
||||||
|
message: mkvText ? trf('MKV 만들기{0}', [count]) : '',
|
||||||
child: mkvText
|
child: mkvText
|
||||||
? FilledButton.icon(
|
? FilledButton.icon(
|
||||||
onPressed: !canBuild ? null : (batch.isEmpty ? () => _confirmBuildAll(context) : () => c.buildVideos(batch)),
|
onPressed: !canBuild ? null : (batch.isEmpty ? () => _confirmBuildAll(context) : () => c.buildVideos(batch)),
|
||||||
|
|
@ -461,6 +475,7 @@ class _TopBar extends StatelessWidget {
|
||||||
label: Text(trf('MKV 만들기{0}{1}', [count, c.busy && jobText ? tr(' (대기열)') : ''])),
|
label: Text(trf('MKV 만들기{0}{1}', [count, c.busy && jobText ? tr(' (대기열)') : ''])),
|
||||||
)
|
)
|
||||||
: IconButton.filled(
|
: IconButton.filled(
|
||||||
|
tooltip: trf('MKV 만들기{0}', [count]), // 241: TalkBack 이 읽는 이름
|
||||||
onPressed: !canBuild ? null : (batch.isEmpty ? () => _confirmBuildAll(context) : () => c.buildVideos(batch)),
|
onPressed: !canBuild ? null : (batch.isEmpty ? () => _confirmBuildAll(context) : () => c.buildVideos(batch)),
|
||||||
icon: Icon(c.busy ? Icons.playlist_add : Icons.play_arrow, size: 20),
|
icon: Icon(c.busy ? Icons.playlist_add : Icons.play_arrow, size: 20),
|
||||||
),
|
),
|
||||||
|
|
@ -611,7 +626,8 @@ class _EncodeBar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
if (compact) {
|
// 241 시험에서 찾음: 폭 720 ~ 840 (노트 10+ 가로 등) 에서 한 줄이 넘쳤다 - 960 보다 좁으면 폰처럼 줄을 바꿔 보인다 (내 판단)
|
||||||
|
if (compact || MediaQuery.sizeOf(context).width < 960) {
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
color: JjColors.panel,
|
color: JjColors.panel,
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,11 @@ class _DiskInfo extends StatelessWidget {
|
||||||
return Row(mainAxisSize: MainAxisSize.min, children: [
|
return Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
const Icon(Icons.storage, size: 14, color: JjColors.textDim),
|
const Icon(Icons.storage, size: 14, color: JjColors.textDim),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(trf('대상 디스크: 남은 {0} / 전체 {1}', [formatSize(free), formatSize(total)]), style: const TextStyle(fontSize: 12)),
|
// 241 시험에서 찾음: 좁은 화면 (폭 500) 에서 넘쳤다 - 글을 줄인다
|
||||||
|
Flexible(
|
||||||
|
child: Text(trf('대상 디스크: 남은 {0} / 전체 {1}', [formatSize(free), formatSize(total)]),
|
||||||
|
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 90,
|
width: 90,
|
||||||
|
|
@ -435,8 +439,13 @@ class _CopyCardState extends State<_CopyCard> {
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: Text(tr('옵션을 저장했습니다')),
|
title: Text(tr('옵션을 저장했습니다')),
|
||||||
content: Text(running
|
content: Text(running
|
||||||
? tr('지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?')
|
// 233: 이동 카드는 "이동" 으로
|
||||||
: tr('이 복사를 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?')),
|
? (t.move
|
||||||
|
? tr('지금 도는 이동은 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?')
|
||||||
|
: tr('지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?'))
|
||||||
|
: (t.move
|
||||||
|
? tr('이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?')
|
||||||
|
: tr('이 복사를 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?'))),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('현재 유지'))),
|
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('현재 유지'))),
|
||||||
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('설정 반영 후 실행'))),
|
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('설정 반영 후 실행'))),
|
||||||
|
|
@ -455,20 +464,30 @@ class _CopyCardState extends State<_CopyCard> {
|
||||||
/// lsync 로: 끝나지 않은 것 (실행 중 · 실패 · 취소 · 아직 안 함) 도 옮길 수 있다 - 남은 것은 lsync 가 이어서 맞춘다.
|
/// lsync 로: 끝나지 않은 것 (실행 중 · 실패 · 취소 · 아직 안 함) 도 옮길 수 있다 - 남은 것은 lsync 가 이어서 맞춘다.
|
||||||
/// 실행 중이면 물어본 뒤 멈추고 옮긴다.
|
/// 실행 중이면 물어본 뒤 멈추고 옮긴다.
|
||||||
Future<void> _toLive(CopyTask t) async {
|
Future<void> _toLive(CopyTask t) async {
|
||||||
if (center.isRunning(t.id)) {
|
final running = center.isRunning(t.id);
|
||||||
|
// 243: 이동 카드 - 실시간 동기화는 원본을 지우지 않는다 (107). 옮기기 전에 알린다
|
||||||
|
if (running || t.move) {
|
||||||
final ok = await showDialog<bool>(
|
final ok = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: Text(tr('lsync 로 이동')),
|
title: Text(tr('lsync 로 이동')),
|
||||||
content: Text(tr('지금 실행 중입니다. 멈추고 lsync (실시간 동기화) 로 옮길까요? 남은 것은 lsync 가 이어서 맞춥니다.')),
|
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
if (running) Text(tr('지금 실행 중입니다. 멈추고 lsync (실시간 동기화) 로 옮길까요? 남은 것은 lsync 가 이어서 맞춥니다.')),
|
||||||
|
if (running && t.move) const SizedBox(height: 10),
|
||||||
|
if (t.move)
|
||||||
|
Text(tr('실시간 동기화는 원본을 지우지 않습니다 (복사로 맞춤). 원본을 비우려면 lsync 의 [최종 정리] 를 쓰세요.'),
|
||||||
|
style: const TextStyle(color: Colors.orangeAccent)),
|
||||||
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||||
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('멈추고 옮기기'))),
|
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(running ? tr('멈추고 옮기기') : tr('옮기기'))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (ok != true) return;
|
if (ok != true || !mounted) return;
|
||||||
|
}
|
||||||
|
if (running) {
|
||||||
final job = center.jobs[t.id]!;
|
final job = center.jobs[t.id]!;
|
||||||
job.cancel();
|
job.cancel();
|
||||||
await job.done;
|
await job.done;
|
||||||
|
|
@ -942,7 +961,8 @@ class _LiveTabState extends State<_LiveTab> {
|
||||||
),
|
),
|
||||||
Chip(
|
Chip(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
label: Text([tr(CopyMethod.of(x.method).label), if (x.delete) tr('지우기 포함')].join(' · ')),
|
// 243: 실시간 동기화는 늘 복사로 맞춘다 (원본은 지우지 않음 - 원본을 비우려면 [최종 정리])
|
||||||
|
label: Text([tr(CopyMethod.of(x.method).label), tr('복사로 맞춤'), if (x.delete) tr('지우기 포함')].join(' · ')),
|
||||||
),
|
),
|
||||||
// 이번 실행 동안 멈춤 / 시작 (설정의 켜기 · 끄기는 오른쪽 스위치)
|
// 이번 실행 동안 멈춤 / 시작 (설정의 켜기 · 끄기는 오른쪽 스위치)
|
||||||
if (live != null && x.enabled)
|
if (live != null && x.enabled)
|
||||||
|
|
@ -1013,7 +1033,12 @@ class _LiveTabState extends State<_LiveTab> {
|
||||||
child: pend == null
|
child: pend == null
|
||||||
? Text(tr('맞출 것을 세는 중…'), style: const TextStyle(fontSize: 12, color: JjColors.textDim))
|
? Text(tr('맞출 것을 세는 중…'), style: const TextStyle(fontSize: 12, color: JjColors.textDim))
|
||||||
: pend.isEmpty
|
: pend.isEmpty
|
||||||
? Text(tr('맞출 것 없음 (원본과 대상이 같습니다)'), style: const TextStyle(fontSize: 12, color: Colors.greenAccent))
|
// 244: 대상에만 있는 것이 있으면 "같습니다" 가 아니라 사실대로
|
||||||
|
? Text(
|
||||||
|
(live?.onlyTarget[k] ?? 0) > 0
|
||||||
|
? trf('원본의 것은 모두 대상에 있습니다 (대상에만 있는 것 {0}개는 그대로 둠)', [live!.onlyTarget[k]])
|
||||||
|
: tr('맞출 것 없음 (원본과 대상이 같습니다)'),
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.greenAccent))
|
||||||
: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Text(
|
Text(
|
||||||
trf('맞출 것 {0}개{1}', [pend.length >= 500 ? '500+' : pend.length, active ? '' : tr(' · 다음 시작 시간에 맞춥니다')]),
|
trf('맞출 것 {0}개{1}', [pend.length >= 500 ? '500+' : pend.length, active ? '' : tr(' · 다음 시작 시간에 맞춥니다')]),
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,13 @@ class SettingTile extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final tile = _tile(context);
|
||||||
|
// 241: 오른쪽 스위치 · 고르기 상자가 제목을 이름으로 읽히게 (TalkBack 이 이름 없는 "켜짐" 만 읽었다).
|
||||||
|
// 줄 자체를 누르는 타일 ([onTap]) 은 누르는 곳이 둘이 되므로 합치지 않는다
|
||||||
|
return trailing != null && onTap == null ? MergeSemantics(child: tile) : tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _tile(BuildContext context) {
|
||||||
final t = trailing;
|
final t = trailing;
|
||||||
if (t == null || _small(t) || !isCompact(context)) {
|
if (t == null || _small(t) || !isCompact(context)) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
|
|
||||||
|
|
@ -329,8 +329,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
Text(tr('환경 설정'), style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
Text(tr('환경 설정'), style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Flexible(
|
Flexible(
|
||||||
|
// 251: 영어 등 긴 언어에서 잘리지 않게 두 줄까지
|
||||||
child: Text(tr('바꾸면 바로 저장됩니다'),
|
child: Text(tr('바꾸면 바로 저장됩니다'),
|
||||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
]),
|
]),
|
||||||
|
|
@ -796,7 +797,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
value: s.addFinishedDownloads,
|
value: s.addFinishedDownloads,
|
||||||
onChanged: (v) => c.updateSettings((x) => x.addFinishedDownloads = v),
|
onChanged: (v) => c.updateSettings((x) => x.addFinishedDownloads = v),
|
||||||
title: Text(tr('완료시 자동 동영상추가')),
|
title: Text(tr('완료 시 자동 동영상 추가')),
|
||||||
subtitle: Text(tr('다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)')),
|
subtitle: Text(tr('다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)')),
|
||||||
),
|
),
|
||||||
SettingTile(
|
SettingTile(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.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/platform_services.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/home_page.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/monitor_page.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/settings_page.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// 241: TalkBack 이 읽는 이름이 없는 아이콘 버튼이 없다 (아이콘 크기의 누르는 곳 - 넓은 목록 줄은 안의 글을 읽는다).
|
||||||
|
/// 바깥 Tooltip 으로 감싼 IconButton · 이름 없는 InkResponse 는 Android 에 이름이 가지 않았다
|
||||||
|
void main() {
|
||||||
|
late Directory tmp;
|
||||||
|
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_a11y_'));
|
||||||
|
tearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
|
||||||
|
Future<List<String>> unnamed(WidgetTester tester, Widget Function(AppController) page, double w) async {
|
||||||
|
final handle = tester.ensureSemantics();
|
||||||
|
// 설정 화면의 rsync 확인이 앱 폴더를 묻는다 (시험 환경에는 path_provider 가 없음)
|
||||||
|
const pp = MethodChannel('plugins.flutter.io/path_provider');
|
||||||
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, (_) async => tmp.path);
|
||||||
|
addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, null));
|
||||||
|
tester.view.physicalSize = Size(w, 900);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
File(p.join(tmp.path, 'a.txt')).writeAsStringSync('a');
|
||||||
|
Directory(p.join(tmp.path, 'sub')).createSync();
|
||||||
|
c.settings
|
||||||
|
..explorerPaths = [tmp.path, tmp.path]
|
||||||
|
..rsyncPaths = [tmp.path, tmp.path]
|
||||||
|
..copyTasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, 'sub'), method: 'rsync', options: '-av')]
|
||||||
|
..liveSyncPairs = [LiveSyncPair(tmp.path, p.join(tmp.path, 'x'))];
|
||||||
|
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: page(c))));
|
||||||
|
for (var i = 0; i < 20; i++) {
|
||||||
|
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
final out = <String>[];
|
||||||
|
void walk(SemanticsNode n) {
|
||||||
|
final d = n.getSemanticsData();
|
||||||
|
final tappable = d.flagsCollection.isButton || d.hasAction(SemanticsAction.tap);
|
||||||
|
if (!n.isMergedIntoParent &&
|
||||||
|
tappable &&
|
||||||
|
n.rect.width <= 72 &&
|
||||||
|
n.rect.height >= 20 &&
|
||||||
|
d.label.trim().isEmpty &&
|
||||||
|
d.tooltip.trim().isEmpty &&
|
||||||
|
d.value.trim().isEmpty) {
|
||||||
|
out.add('${n.rect} (위: ${n.parent?.getSemanticsData().label})');
|
||||||
|
}
|
||||||
|
n.visitChildren((x) {
|
||||||
|
walk(x);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(tester.binding.renderViews.first.owner!.semanticsOwner!.rootSemanticsNode!);
|
||||||
|
handle.dispose();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pages = <String, Widget Function(AppController)>{
|
||||||
|
'MKV': (c) => HomePage(c: c),
|
||||||
|
'파일 탐색기': (c) => ExplorerPage(c: c),
|
||||||
|
'Rsync': (c) => ExplorerPage(c: c, rsync: true),
|
||||||
|
'모니터링': (c) => MonitorPage(c: c),
|
||||||
|
'환경 설정': (c) => SettingsPage(c: c),
|
||||||
|
};
|
||||||
|
for (final MapEntry(key: name, value: page) in pages.entries) {
|
||||||
|
for (final w in [500.0, 800.0, 1600.0]) {
|
||||||
|
testWidgets('241: $name ($w) - 이름 없는 아이콘 버튼 없음', (tester) async {
|
||||||
|
expect(await unnamed(tester, page, w), isEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -12,6 +13,7 @@ import 'package:jj_mkvmaker/core/encode_options.dart';
|
||||||
import 'package:jj_mkvmaker/core/models.dart';
|
import 'package:jj_mkvmaker/core/models.dart';
|
||||||
import 'package:jj_mkvmaker/core/output_paths.dart';
|
import 'package:jj_mkvmaker/core/output_paths.dart';
|
||||||
import 'package:jj_mkvmaker/core/srt.dart';
|
import 'package:jj_mkvmaker/core/srt.dart';
|
||||||
|
import 'package:jj_mkvmaker/l10n/tr.dart';
|
||||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.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/platform/windows/process_media_tool.dart';
|
||||||
import 'package:jj_mkvmaker/services/ai_services.dart';
|
import 'package:jj_mkvmaker/services/ai_services.dart';
|
||||||
|
|
@ -374,7 +376,7 @@ void main() {
|
||||||
d.dispose();
|
d.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('다운로드 화면: "완료시 자동 동영상추가" 체크 → 설정 변경, 목록에서 빼기', (tester) async {
|
testWidgets('다운로드 화면: "완료 시 자동 동영상 추가" 체크 → 설정 변경, 목록에서 빼기', (tester) async {
|
||||||
tester.view.physicalSize = const Size(1500, 900);
|
tester.view.physicalSize = const Size(1500, 900);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
addTearDown(tester.view.reset);
|
addTearDown(tester.view.reset);
|
||||||
|
|
@ -391,7 +393,7 @@ void main() {
|
||||||
Checkbox box() => tester.widget<Checkbox>(
|
Checkbox box() => tester.widget<Checkbox>(
|
||||||
find.descendant(of: find.byTooltip(RegExp('MKV 만들기의 동영상 목록에 자동으로')), matching: find.byType(Checkbox)));
|
find.descendant(of: find.byTooltip(RegExp('MKV 만들기의 동영상 목록에 자동으로')), matching: find.byType(Checkbox)));
|
||||||
expect(box().value, isFalse);
|
expect(box().value, isFalse);
|
||||||
await tester.tap(find.text('완료시 자동 동영상추가'));
|
await tester.tap(find.text('완료 시 자동 동영상 추가'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(s.addFinishedDownloads, isTrue);
|
expect(s.addFinishedDownloads, isTrue);
|
||||||
expect(box().value, isTrue);
|
expect(box().value, isTrue);
|
||||||
|
|
@ -404,6 +406,38 @@ void main() {
|
||||||
d.dispose();
|
d.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 246 · P11 확인 (921): 영어 · 일본어는 글이 길어 체크가 아예 안 보였다 - 언어 · 폭마다
|
||||||
|
for (final lang in ['ko', 'en', 'ja']) {
|
||||||
|
for (final w in [800.0, 1200.0]) {
|
||||||
|
testWidgets('246: 다운로드 화면 ($lang, 폭 $w) - 버튼 줄의 글이 잘리거나 "전체 선택" 과 겹치지 않는다', (tester) async {
|
||||||
|
Map<String, String> dict(String code) =>
|
||||||
|
Map<String, String>.from(jsonDecode(File('assets/l10n/$code.json').readAsStringSync()) as Map);
|
||||||
|
setTranslations(lang, lang == 'ko' ? const {} : dict(lang), lang == 'ko' ? const {} : dict('en'));
|
||||||
|
addTearDown(() => setTranslations('ko', const {}));
|
||||||
|
tester.view.physicalSize = Size(w, 411);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
final s = AppSettings();
|
||||||
|
final d = DownloadManager(backends: [_Backend()], settings: () => s, readClipboard: () async => null);
|
||||||
|
addTearDown(d.dispose);
|
||||||
|
d.addToEditList = (files) async => files.length;
|
||||||
|
d.setAutoAdd = (on) async => s.addFinishedDownloads = on;
|
||||||
|
d.addPage('https://vimeo.com/1')!.title = '영상';
|
||||||
|
await tester.pumpWidget(MaterialApp(home: DownloadsPage(d: d)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
final all = tester.getRect(find.text(tr('전체 선택')));
|
||||||
|
final auto = tester.getRect(find.text(tr('완료 시 자동 동영상 추가')));
|
||||||
|
expect(auto.overlaps(all), isFalse, reason: '$auto · $all');
|
||||||
|
expect([auto.left >= 0, auto.right <= w], [true, true], reason: '화면 안에 다 보임 $auto');
|
||||||
|
for (final t in ['일시정지', '재개', '다운로드 취소', '삭제', '완료 정리']) {
|
||||||
|
final r = tester.getRect(find.text(tr(t)).first);
|
||||||
|
expect([r.left >= 0, r.right <= w], [true, true], reason: '$t $r');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
testWidgets('환경 설정 · 종료 버튼: 모든 화면에서 같은 자리', (tester) async {
|
testWidgets('환경 설정 · 종료 버튼: 모든 화면에서 같은 자리', (tester) async {
|
||||||
tester.view.physicalSize = const Size(1500, 900);
|
tester.view.physicalSize = const Size(1500, 900);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
|
|
@ -581,4 +615,43 @@ void main() {
|
||||||
expect(b.status, JobStatus.cancelled);
|
expect(b.status, JobStatus.cancelled);
|
||||||
expect(find.text('모두 취소 (2개)'), findsNothing);
|
expect(find.text('모두 취소 (2개)'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('242: 폰 가로 (노트 10+ - 내비게이션 막대를 뺀 약 800×411) 에서 작업 중이어도 위 막대 버튼이 화면 크기 "100%" 를 덮지 않는다', (tester) async {
|
||||||
|
final w = double.parse(Platform.environment['JJ_W'] ?? '800');
|
||||||
|
tester.view.physicalSize = Size(w, 411);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
final tool = _HoldTool();
|
||||||
|
final dir = Directory.systemTemp.createTempSync('jj_batch242_');
|
||||||
|
addTearDown(() => dir.deleteSync(recursive: true));
|
||||||
|
final file = (File(p.join(dir.path, 'a.mp4'))..writeAsBytesSync([0])).path;
|
||||||
|
final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
|
||||||
|
late Future<void> building;
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
await c.init();
|
||||||
|
await c.addVideos([file]);
|
||||||
|
c.selected = c.videos.first;
|
||||||
|
building = c.buildVideos([...c.videos]);
|
||||||
|
for (var i = 0; i < 200 && tool.runs.isEmpty; i++) {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(c.busy, isTrue);
|
||||||
|
await tester.pumpWidget(MaterialApp(home: HomePage(c: c)));
|
||||||
|
// 첫 그림에서 재어 보고 모자라면 다음 그림부터 두 줄 (첫 그림도 넘침 오류 없이 잘라 그림)
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
expect(tester.takeException(), isNull, reason: '두 줄로 바뀐 뒤에는 넘침 없음');
|
||||||
|
final pct = tester.getRect(find.text('${(c.settings.uiScale * 100).round()}%'));
|
||||||
|
for (final tip in ['작업 취소', '결과 폴더 열기: ${outputDirFor(file)}']) {
|
||||||
|
final f = find.byTooltip(tip);
|
||||||
|
expect(f, findsWidgets, reason: tip);
|
||||||
|
final r = tester.getRect(f.first);
|
||||||
|
expect(r.overlaps(pct), isFalse, reason: '$tip 버튼 $r 이 "100%" $pct 를 덮음');
|
||||||
|
}
|
||||||
|
c.cancel();
|
||||||
|
await tester.runAsync(() => building);
|
||||||
|
await tester.pump();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -274,9 +274,37 @@ void main() {
|
||||||
expect(userErrorText(const ProcessException('rsync', ['-a'], '원본 파일을 지우지 못했습니다', 23)), '원본 파일을 지우지 못했습니다');
|
expect(userErrorText(const ProcessException('rsync', ['-a'], '원본 파일을 지우지 못했습니다', 23)), '원본 파일을 지우지 못했습니다');
|
||||||
expect(userErrorText(const FileSystemException('지울 수 없음', r'C:\x', OSError('액세스가 거부되었습니다', 5))), r'지울 수 없음 · C:\x · 액세스가 거부되었습니다');
|
expect(userErrorText(const FileSystemException('지울 수 없음', r'C:\x', OSError('액세스가 거부되었습니다', 5))), r'지울 수 없음 · C:\x · 액세스가 거부되었습니다');
|
||||||
expect(userErrorText(Exception('끊김')), '끊김');
|
expect(userErrorText(Exception('끊김')), '끊김');
|
||||||
|
// 245: 앱이 직접 할 때의 Dart 파일 오류 (영어) 도 rsync 때와 같은 글 - 원본을 못 지웠으면
|
||||||
|
expect(userErrorText(const FileSystemException('Cannot delete file', r'C:\s\a.txt', OSError('사용 중', 32)), sources: [r'C:\s']),
|
||||||
|
r'원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다. · C:\s\a.txt · 사용 중');
|
||||||
|
expect(userErrorText(const FileSystemException('Cannot delete file', r'D:\t\x.txt'), sources: [r'C:\s']).startsWith('지우지 못했습니다'), isTrue);
|
||||||
|
expect(userErrorText(const FileSystemException('Cannot copy file to D:\\t', r'C:\s\a.txt')).startsWith('파일을 복사하지 못했습니다'), isTrue);
|
||||||
expect(userErrorText('그냥 글'), '그냥 글');
|
expect(userErrorText('그냥 글'), '그냥 글');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('243: 이동 카드를 lsync 로 옮길 때 "원본을 지우지 않음 (복사로 맞춤)" 을 알리고, 취소하면 옮기지 않는다', (tester) async {
|
||||||
|
final src = p.join(tmp.path, 'mv243'), dst = p.join(tmp.path, 'mv243d');
|
||||||
|
Directory(src).createSync();
|
||||||
|
Directory(dst).createSync();
|
||||||
|
c.settings.copyTasks = [CopyTask(id: '1', sources: [src], dest: dst, contents: true, move: true, method: 'builtin')];
|
||||||
|
tester.view.physicalSize = const Size(1500, 1000);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c)));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.text('lsync 로 이동').first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.textContaining('실시간 동기화는 원본을 지우지 않습니다 (복사로 맞춤)'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('취소').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(c.settings.liveSyncPairs, isEmpty, reason: '취소하면 옮기지 않음');
|
||||||
|
await tester.tap(find.text('lsync 로 이동').first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.widgetWithText(FilledButton, '옮기기'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(c.settings.liveSyncPairs.map((x) => x.source), [src]);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('105: 원본이 대상 안 + 창에서 [원본 파일 지우기] 로 지우기 옵션이 생기면 막는다', (tester) async {
|
testWidgets('105: 원본이 대상 안 + 창에서 [원본 파일 지우기] 로 지우기 옵션이 생기면 막는다', (tester) async {
|
||||||
final outer = p.join(tmp.path, 'O'), inner = p.join(outer, 'in');
|
final outer = p.join(tmp.path, 'O'), inner = p.join(outer, 'in');
|
||||||
File(p.join(inner, 'n.txt'))
|
File(p.join(inner, 'n.txt'))
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import 'support/fake_recycle_bin.dart';
|
||||||
import 'support/recycle_leftovers.dart';
|
import 'support/recycle_leftovers.dart';
|
||||||
import 'support/elevated.dart';
|
import 'support/elevated.dart';
|
||||||
|
|
||||||
|
import 'explorer_test.dart' show calm;
|
||||||
/// 48 · 154 · 155: 같은 이름이 있으면 덮어쓰기 · 건너뛰기 · 이름 바꾸기를 고르고, 확인 창에 항목 이름이 보인다
|
/// 48 · 154 · 155: 같은 이름이 있으면 덮어쓰기 · 건너뛰기 · 이름 바꾸기를 고르고, 확인 창에 항목 이름이 보인다
|
||||||
void main() {
|
void main() {
|
||||||
late Directory tmp;
|
late Directory tmp;
|
||||||
|
|
@ -165,7 +166,7 @@ void main() {
|
||||||
await tester.runAsync(() => tester.tap(find.text('doc.txt').first));
|
await tester.runAsync(() => tester.tap(find.text('doc.txt').first));
|
||||||
await settle(tester, () => false, rounds: 10);
|
await settle(tester, () => false, rounds: 10);
|
||||||
await tester.runAsync(() => tester.tap(find.text('삭제').first));
|
await tester.runAsync(() => tester.tap(find.text('삭제').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '휴지통으로')));
|
await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '휴지통으로')));
|
||||||
await settle(tester, () => find.text('되돌리기').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('되돌리기').evaluate().isNotEmpty);
|
||||||
expect(File(p.join(left, 'doc.txt')).existsSync(), isFalse);
|
expect(File(p.join(left, 'doc.txt')).existsSync(), isFalse);
|
||||||
|
|
@ -179,7 +180,7 @@ void main() {
|
||||||
if (bin != null) expect(bin!.items, isEmpty, reason: '가짜 휴지통에서 되돌림');
|
if (bin != null) expect(bin!.items, isEmpty, reason: '가짜 휴지통에서 되돌림');
|
||||||
// 알림 (10초) 이 시험이 끝난 뒤에 닫히지 않게 정리
|
// 알림 (10초) 이 시험이 끝난 뒤에 닫히지 않게 정리
|
||||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
}, skip: !Platform.isWindows);
|
}, skip: !Platform.isWindows);
|
||||||
|
|
||||||
testWidgets('144: 긴 경로는 휴지통에 넣지 못한 이유 한 줄과 [영구 삭제] - 누르면 지운다 · [그대로 두기] 면 남는다', (tester) async {
|
testWidgets('144: 긴 경로는 휴지통에 넣지 못한 이유 한 줄과 [영구 삭제] - 누르면 지운다 · [그대로 두기] 면 남는다', (tester) async {
|
||||||
|
|
@ -199,7 +200,7 @@ void main() {
|
||||||
await tester.runAsync(() => tester.tap(find.text('deep').first));
|
await tester.runAsync(() => tester.tap(find.text('deep').first));
|
||||||
await settle(tester, () => false, rounds: 10);
|
await settle(tester, () => false, rounds: 10);
|
||||||
await tester.runAsync(() => tester.tap(find.text('삭제').first));
|
await tester.runAsync(() => tester.tap(find.text('삭제').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '휴지통으로')));
|
await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '휴지통으로')));
|
||||||
await settle(tester, () => find.textContaining('경로가 너무 길어').evaluate().isNotEmpty);
|
await settle(tester, () => find.textContaining('경로가 너무 길어').evaluate().isNotEmpty);
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +211,7 @@ void main() {
|
||||||
expect(find.text('· deep'), findsOneWidget);
|
expect(find.text('· deep'), findsOneWidget);
|
||||||
expect(find.textContaining('영구 삭제로 지울 수 있습니다'), findsNothing, reason: '긴 설명을 항목마다 되풀이하지 않음');
|
expect(find.textContaining('영구 삭제로 지울 수 있습니다'), findsNothing, reason: '긴 설명을 항목마다 되풀이하지 않음');
|
||||||
await tester.tap(find.text('그대로 두기'));
|
await tester.tap(find.text('그대로 두기'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(Directory(lp(deep)).existsSync(), isTrue);
|
expect(Directory(lp(deep)).existsSync(), isTrue);
|
||||||
// 다시 → [영구 삭제]
|
// 다시 → [영구 삭제]
|
||||||
await deleteDeep();
|
await deleteDeep();
|
||||||
|
|
@ -249,7 +250,7 @@ void main() {
|
||||||
expect(find.byType(ExplorerPage), findsOneWidget);
|
expect(find.byType(ExplorerPage), findsOneWidget);
|
||||||
expect(find.textContaining(p.join(tmp.path, 'left')), findsNothing);
|
expect(find.textContaining(p.join(tmp.path, 'left')), findsNothing);
|
||||||
await back(); // 3) 맨 위에서는 화면 닫기
|
await back(); // 3) 맨 위에서는 화면 닫기
|
||||||
await tester.pumpAndSettle(); // 닫히는 전환 (가짜 시간을 흘려야 끝난다)
|
await calm(tester); // 닫히는 전환 (가짜 시간을 흘려야 끝난다)
|
||||||
expect(find.byType(ExplorerPage), findsNothing);
|
expect(find.byType(ExplorerPage), findsNothing);
|
||||||
expect(find.text('앞 화면'), findsOneWidget);
|
expect(find.text('앞 화면'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
@ -287,14 +288,14 @@ void main() {
|
||||||
expect(find.descendant(of: find.byType(AlertDialog), matching: find.text('이미 이 폴더에 있습니다')), findsOneWidget);
|
expect(find.descendant(of: find.byType(AlertDialog), matching: find.text('이미 이 폴더에 있습니다')), findsOneWidget);
|
||||||
expect(find.textContaining('doc.txt 은(는) 이미 이 폴더에 있어 옮길 것이 없습니다'), findsOneWidget);
|
expect(find.textContaining('doc.txt 은(는) 이미 이 폴더에 있어 옮길 것이 없습니다'), findsOneWidget);
|
||||||
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
|
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
|
||||||
// 같은 폴더로 [복사] 는 사본을 만들지 묻는다: [취소] → 그대로, [사본 만들기] → "doc (2).txt"
|
// 같은 폴더로 [복사] 는 사본을 만들지 묻는다: [취소] → 그대로, [사본 만들기] → "doc (2).txt"
|
||||||
await tester.runAsync(() => tester.tap(find.text('복사').first));
|
await tester.runAsync(() => tester.tap(find.text('복사').first));
|
||||||
await settle(tester, () => find.byType(AlertDialog).evaluate().isNotEmpty, rounds: 30);
|
await settle(tester, () => find.byType(AlertDialog).evaluate().isNotEmpty, rounds: 30);
|
||||||
expect(find.textContaining('같은 폴더에 사본을 만들까요? (이름 (2))'), findsOneWidget);
|
expect(find.textContaining('같은 폴더에 사본을 만들까요? (이름 (2))'), findsOneWidget);
|
||||||
await tester.tap(find.text('취소'));
|
await tester.tap(find.text('취소'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(File(p.join(left, 'doc (2).txt')).existsSync(), isFalse);
|
expect(File(p.join(left, 'doc (2).txt')).existsSync(), isFalse);
|
||||||
await tester.runAsync(() => tester.tap(find.text('복사').first));
|
await tester.runAsync(() => tester.tap(find.text('복사').first));
|
||||||
await settle(tester, () => find.byType(AlertDialog).evaluate().isNotEmpty, rounds: 30);
|
await settle(tester, () => find.byType(AlertDialog).evaluate().isNotEmpty, rounds: 30);
|
||||||
|
|
@ -346,7 +347,7 @@ void main() {
|
||||||
expect(find.text('더 보기'), findsOneWidget);
|
expect(find.text('더 보기'), findsOneWidget);
|
||||||
expect(find.text('숨은 항목 표시'), findsNothing, reason: '들어가지 않는 버튼은 줄에 없음');
|
expect(find.text('숨은 항목 표시'), findsNothing, reason: '들어가지 않는 버튼은 줄에 없음');
|
||||||
await tester.tap(find.text('더 보기'));
|
await tester.tap(find.text('더 보기'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.text('숨은 항목 표시'), findsOneWidget);
|
expect(find.text('숨은 항목 표시'), findsOneWidget);
|
||||||
await tester.runAsync(() => tester.tap(find.text('숨은 항목 표시')));
|
await tester.runAsync(() => tester.tap(find.text('숨은 항목 표시')));
|
||||||
await settle(tester, () => c.settings.explorerShowHidden != before, rounds: 20);
|
await settle(tester, () => c.settings.explorerShowHidden != before, rounds: 20);
|
||||||
|
|
@ -391,7 +392,7 @@ void main() {
|
||||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
|
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
|
||||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||||
await tester.tap(find.byTooltip('경로 입력').first);
|
await tester.tap(find.byTooltip('경로 입력').first);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.enterText(find.byType(TextField).last, p.join(tmp.path, 'nope'));
|
await tester.enterText(find.byType(TextField).last, p.join(tmp.path, 'nope'));
|
||||||
await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||||||
await settle(tester, () => find.text('폴더가 없거나 열 수 없습니다').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('폴더가 없거나 열 수 없습니다').evaluate().isNotEmpty);
|
||||||
|
|
@ -431,7 +432,7 @@ void main() {
|
||||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
|
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
|
||||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||||
await tester.tap(find.byTooltip('경로 입력').first);
|
await tester.tap(find.byTooltip('경로 입력').first);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.widgetWithText(TextButton, 'WebDAV 서버 추가'), findsNothing);
|
expect(find.widgetWithText(TextButton, 'WebDAV 서버 추가'), findsNothing);
|
||||||
await tester.enterText(find.byType(TextField).last, 'smb://nas/영상');
|
await tester.enterText(find.byType(TextField).last, 'smb://nas/영상');
|
||||||
await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||||||
|
|
@ -444,7 +445,7 @@ void main() {
|
||||||
expect(find.widgetWithText(AlertDialog, 'WebDAV 서버 추가'), findsOneWidget);
|
expect(find.widgetWithText(AlertDialog, 'WebDAV 서버 추가'), findsOneWidget);
|
||||||
expect(find.textContaining('SMB'), findsNothing, reason: '경로 창은 닫힘');
|
expect(find.textContaining('SMB'), findsNothing, reason: '경로 창은 닫힘');
|
||||||
await tester.tap(find.text('취소').last);
|
await tester.tap(find.text('취소').last);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.byType(AlertDialog), findsNothing);
|
expect(find.byType(AlertDialog), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import 'explorer_test.dart' show calm;
|
||||||
void main() {
|
void main() {
|
||||||
late Directory tmp;
|
late Directory tmp;
|
||||||
late AppController c;
|
late AppController c;
|
||||||
|
|
@ -93,10 +94,10 @@ void main() {
|
||||||
expect(tester.getRect(find.byWidget(e.widget).first).right, lessThanOrEqualTo(1280));
|
expect(tester.getRect(find.byWidget(e.widget).first).right, lessThanOrEqualTo(1280));
|
||||||
}
|
}
|
||||||
await tester.tap(pathBtn.first);
|
await tester.tap(pathBtn.first);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.byType(AlertDialog), findsOneWidget);
|
expect(find.byType(AlertDialog), findsOneWidget);
|
||||||
await tester.tap(find.text('취소').last);
|
await tester.tap(find.text('취소').last);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
// 정상 드라이브 (T:) 가 빈 드라이브들보다 앞
|
// 정상 드라이브 (T:) 가 빈 드라이브들보다 앞
|
||||||
expect(tester.getTopLeft(find.text('T:').first).dx, lessThan(tester.getTopLeft(find.textContaining('G: ·').first).dx));
|
expect(tester.getTopLeft(find.text('T:').first).dx, lessThan(tester.getTopLeft(find.textContaining('G: ·').first).dx));
|
||||||
// 휠 (위 · 아래) 로 탭 줄이 옆으로 (왼쪽 창의 T: 탭이 왼쪽으로 밀린다)
|
// 휠 (위 · 아래) 로 탭 줄이 옆으로 (왼쪽 창의 T: 탭이 왼쪽으로 밀린다)
|
||||||
|
|
@ -108,15 +109,15 @@ void main() {
|
||||||
expect(tester.getTopLeft(find.text('T:').first).dx, lessThan(before - 10));
|
expect(tester.getTopLeft(find.text('T:').first).dx, lessThan(before - 10));
|
||||||
// 좁은 창 (위아래 두 창 · 폰): 끝 버튼은 [⋮] 하나로 늘 보임 → 경로 입력
|
// 좁은 창 (위아래 두 창 · 폰): 끝 버튼은 [⋮] 하나로 늘 보임 → 경로 입력
|
||||||
tester.view.physicalSize = const Size(560, 900);
|
tester.view.physicalSize = const Size(560, 900);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.byTooltip('경로 입력 · WebDAV'), findsWidgets);
|
expect(find.byTooltip('경로 입력 · WebDAV'), findsWidgets);
|
||||||
await tester.tap(find.byTooltip('경로 입력 · WebDAV').first);
|
await tester.tap(find.byTooltip('경로 입력 · WebDAV').first);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.tap(find.text('경로 입력').last);
|
await tester.tap(find.text('경로 입력').last);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.byType(AlertDialog), findsOneWidget);
|
expect(find.byType(AlertDialog), findsOneWidget);
|
||||||
await tester.tap(find.text('취소').last);
|
await tester.tap(find.text('취소').last);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(tester.takeException(), isNull, reason: '넘침 (RenderFlex overflow) 없음');
|
expect(tester.takeException(), isNull, reason: '넘침 (RenderFlex overflow) 없음');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
import 'explorer_test.dart' show act, settle;
|
import 'explorer_test.dart' show act, settle, calm;
|
||||||
|
|
||||||
/// 23: 앞의 복사가 끝나지 않아도 다음 복사를 함께 시작한다 (막지 않음, 진행 막대가 둘)
|
/// 23: 앞의 복사가 끝나지 않아도 다음 복사를 함께 시작한다 (막지 않음, 진행 막대가 둘)
|
||||||
void main() {
|
void main() {
|
||||||
|
|
@ -161,7 +161,7 @@ void main() {
|
||||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c, rsync: true))));
|
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c, rsync: true))));
|
||||||
await settle(tester, () => find.text('sub').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('sub').evaluate().isNotEmpty);
|
||||||
await tester.longPress(find.text('sub'));
|
await tester.longPress(find.text('sub'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
for (final t in ['rsync 에 고르기', '열기', '다른 창에서 열기', '이름 변경', '삭제', '정보']) {
|
for (final t in ['rsync 에 고르기', '열기', '다른 창에서 열기', '이름 변경', '삭제', '정보']) {
|
||||||
expect(find.text(t), findsOneWidget, reason: t);
|
expect(find.text(t), findsOneWidget, reason: t);
|
||||||
}
|
}
|
||||||
|
|
@ -194,7 +194,7 @@ void main() {
|
||||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
|
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
|
||||||
await settle(tester, () => find.text('top.txt').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('top.txt').evaluate().isNotEmpty);
|
||||||
await tester.tap(find.byTooltip('경로 입력').first);
|
await tester.tap(find.byTooltip('경로 입력').first);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.enterText(find.byType(TextField).last, p.join(tmp.path, 'none'));
|
await tester.enterText(find.byType(TextField).last, p.join(tmp.path, 'none'));
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||||||
await settle(tester, () => find.textContaining('폴더가 없거나 열 수 없습니다').evaluate().isNotEmpty, rounds: 20);
|
await settle(tester, () => find.textContaining('폴더가 없거나 열 수 없습니다').evaluate().isNotEmpty, rounds: 20);
|
||||||
|
|
@ -232,7 +232,7 @@ void main() {
|
||||||
..createSync(recursive: true)
|
..createSync(recursive: true)
|
||||||
..writeAsBytesSync([1, 2, 3]);
|
..writeAsBytesSync([1, 2, 3]);
|
||||||
await tester.tap(find.byTooltip('경로 입력').first);
|
await tester.tap(find.byTooltip('경로 입력').first);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.enterText(find.byType(TextField).last, made);
|
await tester.enterText(find.byType(TextField).last, made);
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||||||
await settle(tester, () => find.text('pg1.png').evaluate().isNotEmpty, rounds: 300);
|
await settle(tester, () => find.text('pg1.png').evaluate().isNotEmpty, rounds: 300);
|
||||||
|
|
@ -308,7 +308,7 @@ void main() {
|
||||||
await act(tester, () => tester.tap(find.byTooltip(tooltip)));
|
await act(tester, () => tester.tap(find.byTooltip(tooltip)));
|
||||||
} else {
|
} else {
|
||||||
await tester.tap(find.text('더 보기'));
|
await tester.tap(find.text('더 보기'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.text(label), findsOneWidget, reason: '[더 보기] 메뉴에 지금 배치가 보임: $label');
|
expect(find.text(label), findsOneWidget, reason: '[더 보기] 메뉴에 지금 배치가 보임: $label');
|
||||||
await act(tester, () => tester.tap(find.text(label)));
|
await act(tester, () => tester.tap(find.text(label)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'support/fake_recycle_bin.dart';
|
import 'support/fake_recycle_bin.dart';
|
||||||
|
|
||||||
|
import 'explorer_test.dart' show calm;
|
||||||
/// 180: 복사 · 이동이 시작 · 진행 · 끝나도 목록의 자리와 스크롤은 그대로 (다음 것을 누르려다 엉뚱한 것을 누르지 않게)
|
/// 180: 복사 · 이동이 시작 · 진행 · 끝나도 목록의 자리와 스크롤은 그대로 (다음 것을 누르려다 엉뚱한 것을 누르지 않게)
|
||||||
void main() {
|
void main() {
|
||||||
late Directory tmp;
|
late Directory tmp;
|
||||||
|
|
@ -62,7 +63,7 @@ void main() {
|
||||||
// 왼쪽 목록을 조금 내려 둔다
|
// 왼쪽 목록을 조금 내려 둔다
|
||||||
final list = find.ancestor(of: find.text('f05.txt'), matching: find.byType(Scrollable)).first;
|
final list = find.ancestor(of: find.text('f05.txt'), matching: find.byType(Scrollable)).first;
|
||||||
await tester.drag(find.text('f05.txt'), const Offset(0, -200));
|
await tester.drag(find.text('f05.txt'), const Offset(0, -200));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
final probe = find.text('f07.txt');
|
final probe = find.text('f07.txt');
|
||||||
expect(probe, findsOneWidget);
|
expect(probe, findsOneWidget);
|
||||||
final where = tester.getTopLeft(probe);
|
final where = tester.getTopLeft(probe);
|
||||||
|
|
@ -84,7 +85,7 @@ void main() {
|
||||||
await act(tester, () => tester.tap(find.text('새로 고침').first));
|
await act(tester, () => tester.tap(find.text('새로 고침').first));
|
||||||
} else {
|
} else {
|
||||||
await act(tester, () => tester.tap(find.text('복사').first));
|
await act(tester, () => tester.tap(find.text('복사').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
||||||
}
|
}
|
||||||
// 시작 · 진행 · 끝 (진행 막대가 내려갈 때까지) 내내 같은 자리
|
// 시작 · 진행 · 끝 (진행 막대가 내려갈 때까지) 내내 같은 자리
|
||||||
|
|
@ -126,7 +127,7 @@ void main() {
|
||||||
await settle(tester, () => find.text('f05.txt').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('f05.txt').evaluate().isNotEmpty);
|
||||||
final list = find.ancestor(of: find.text('f05.txt'), matching: find.byType(Scrollable)).first;
|
final list = find.ancestor(of: find.text('f05.txt'), matching: find.byType(Scrollable)).first;
|
||||||
await tester.drag(find.text('f05.txt'), const Offset(0, -200));
|
await tester.drag(find.text('f05.txt'), const Offset(0, -200));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
final at = tester.state<ScrollableState>(list).position.pixels;
|
final at = tester.state<ScrollableState>(list).position.pixels;
|
||||||
expect(at, greaterThan(0));
|
expect(at, greaterThan(0));
|
||||||
final w = tester.getSize(list).width;
|
final w = tester.getSize(list).width;
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ void main() {
|
||||||
|
|
||||||
// 길게 누르기 → 메뉴 → 다른 앱으로 열기 (고르기 창)
|
// 길게 누르기 → 메뉴 → 다른 앱으로 열기 (고르기 창)
|
||||||
await tester.longPress(find.text('doc.txt'));
|
await tester.longPress(find.text('doc.txt'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.text('다른 창으로 복사'), findsOneWidget);
|
expect(find.text('다른 창으로 복사'), findsOneWidget);
|
||||||
expect(find.text('삭제'), findsWidgets);
|
expect(find.text('삭제'), findsWidgets);
|
||||||
await act(tester, () => tester.tap(find.text('다른 앱으로 열기')));
|
await act(tester, () => tester.tap(find.text('다른 앱으로 열기')));
|
||||||
|
|
@ -123,7 +123,7 @@ void main() {
|
||||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||||
await settle(tester, () => false, rounds: 15);
|
await settle(tester, () => false, rounds: 15);
|
||||||
await act(tester, () => tester.tap(find.text('복사').first));
|
await act(tester, () => tester.tap(find.text('복사').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
||||||
await settle(tester, () => File(p.join(right, 'doc.txt')).existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty);
|
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(right, 'doc.txt')).readAsStringSync(), 'hello');
|
||||||
|
|
@ -138,17 +138,17 @@ void main() {
|
||||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||||
await settle(tester, () => false, rounds: 15);
|
await settle(tester, () => false, rounds: 15);
|
||||||
await act(tester, () => tester.tap(find.text('복사').first));
|
await act(tester, () => tester.tap(find.text('복사').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.textContaining('방법: robocopy'), findsOneWidget);
|
expect(find.textContaining('방법: robocopy'), findsOneWidget);
|
||||||
expect(find.text('환경 설정의 /MOV 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)'), findsOneWidget);
|
expect(find.text('환경 설정의 /MOV 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)'), findsOneWidget);
|
||||||
await act(tester, () => tester.tap(find.text('취소').last));
|
await act(tester, () => tester.tap(find.text('취소').last));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await act(tester, () => tester.tap(find.text('이동').first));
|
await act(tester, () => tester.tap(find.text('이동').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.textContaining('옮길까요?'), findsOneWidget);
|
expect(find.textContaining('옮길까요?'), findsOneWidget);
|
||||||
expect(find.textContaining('복사에서는 쓰지 않습니다'), findsNothing);
|
expect(find.textContaining('복사에서는 쓰지 않습니다'), findsNothing);
|
||||||
await act(tester, () => tester.tap(find.text('취소').last));
|
await act(tester, () => tester.tap(find.text('취소').last));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
|
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
|
||||||
expect(File(p.join(right, 'doc.txt')).existsSync(), isFalse);
|
expect(File(p.join(right, 'doc.txt')).existsSync(), isFalse);
|
||||||
}, skip: !Platform.isWindows);
|
}, skip: !Platform.isWindows);
|
||||||
|
|
@ -192,17 +192,17 @@ void main() {
|
||||||
|
|
||||||
// 이 배치에도 복사 · 이동: 길게 눌러 [복사] 로 담고 → 다른 폴더를 골라 [붙여넣기]
|
// 이 배치에도 복사 · 이동: 길게 눌러 [복사] 로 담고 → 다른 폴더를 골라 [붙여넣기]
|
||||||
await tester.longPress(find.text('inner.txt'));
|
await tester.longPress(find.text('inner.txt'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.text('다른 창으로 복사'), findsNothing);
|
expect(find.text('다른 창으로 복사'), findsNothing);
|
||||||
await tester.tap(find.text('복사').last);
|
await tester.tap(find.text('복사').last);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await act(tester, () => tester.tap(find.text('..')));
|
await act(tester, () => tester.tap(find.text('..')));
|
||||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||||
// 73 (관리자): 담아 둔 것이 있으면 [붙여넣기] 는 좁아도 [더 보기] 로 가지 않고 줄에 남는다
|
// 73 (관리자): 담아 둔 것이 있으면 [붙여넣기] 는 좁아도 [더 보기] 로 가지 않고 줄에 남는다
|
||||||
expect(find.text('더 보기'), findsOneWidget, reason: '이 너비에서는 줄이 넘친다');
|
expect(find.text('더 보기'), findsOneWidget, reason: '이 너비에서는 줄이 넘친다');
|
||||||
expect(find.text('붙여넣기'), findsOneWidget);
|
expect(find.text('붙여넣기'), findsOneWidget);
|
||||||
await act(tester, () => tester.tap(find.text('붙여넣기')));
|
await act(tester, () => tester.tap(find.text('붙여넣기')));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
||||||
await settle(tester, () => File(p.join(left, 'inner.txt')).existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty);
|
await settle(tester, () => File(p.join(left, 'inner.txt')).existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty);
|
||||||
expect(File(p.join(left, 'inner.txt')).readAsStringSync(), 'inner');
|
expect(File(p.join(left, 'inner.txt')).readAsStringSync(), 'inner');
|
||||||
|
|
@ -220,7 +220,7 @@ void main() {
|
||||||
expect(find.text('확장자'), findsOneWidget);
|
expect(find.text('확장자'), findsOneWidget);
|
||||||
// 알림 (복사 끝 등) 이 시험이 끝난 뒤에 닫히지 않게 정리
|
// 알림 (복사 끝 등) 이 시험이 끝난 뒤에 닫히지 않게 정리
|
||||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('선택 모드: 파일 누르기 = 선택 · 취소, 폴더 누르기 = 자기 자신과 안의 것 모두 선택 · 다시 누르면 모두 취소', (tester) async {
|
testWidgets('선택 모드: 파일 누르기 = 선택 · 취소, 폴더 누르기 = 자기 자신과 안의 것 모두 선택 · 다시 누르면 모두 취소', (tester) async {
|
||||||
|
|
@ -278,9 +278,9 @@ void main() {
|
||||||
await act(tester, () => tester.tap(markOf('sub')));
|
await act(tester, () => tester.tap(markOf('sub')));
|
||||||
await settle(tester, () => find.textContaining('3개 표시함').evaluate().isNotEmpty);
|
await settle(tester, () => find.textContaining('3개 표시함').evaluate().isNotEmpty);
|
||||||
await act(tester, () => tester.tap(find.text('삭제').first));
|
await act(tester, () => tester.tap(find.text('삭제').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.tap(find.text('휴지통을 거치지 않고 영구 삭제 (Shift+Delete)'));
|
await tester.tap(find.text('휴지통을 거치지 않고 영구 삭제 (Shift+Delete)'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '영구 삭제')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '영구 삭제')));
|
||||||
await settle(tester, () => find.textContaining('지우지 못했습니다').evaluate().isNotEmpty);
|
await settle(tester, () => find.textContaining('지우지 못했습니다').evaluate().isNotEmpty);
|
||||||
expect(find.text('3개 중 1개를 지우지 못했습니다'), findsOneWidget);
|
expect(find.text('3개 중 1개를 지우지 못했습니다'), findsOneWidget);
|
||||||
|
|
@ -289,43 +289,43 @@ void main() {
|
||||||
expect(Directory(p.join(left, 'sub')).existsSync(), isFalse);
|
expect(Directory(p.join(left, 'sub')).existsSync(), isFalse);
|
||||||
expect(locked.existsSync(), isTrue);
|
expect(locked.existsSync(), isTrue);
|
||||||
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
}, skip: !Platform.isWindows);
|
}, skip: !Platform.isWindows);
|
||||||
|
|
||||||
testWidgets('73: 즐겨찾기 - 지금 폴더 넣기 (이름 고침) · 한 번에 가기 · 빼기 (폴더는 그대로)', (tester) async {
|
testWidgets('73: 즐겨찾기 - 지금 폴더 넣기 (이름 고침) · 한 번에 가기 · 빼기 (폴더는 그대로)', (tester) async {
|
||||||
await open(tester);
|
await open(tester);
|
||||||
// 창 위의 ★ (왼쪽 창)
|
// 창 위의 ★ (왼쪽 창)
|
||||||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.text('아직 없습니다. 아래에서 지금 폴더를 넣으세요.'), findsOneWidget);
|
expect(find.text('아직 없습니다. 아래에서 지금 폴더를 넣으세요.'), findsOneWidget);
|
||||||
await tester.tap(find.text('지금 폴더 추가'));
|
await tester.tap(find.text('지금 폴더 추가'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.enterText(find.byType(TextField).last, '왼쪽 폴더');
|
await tester.enterText(find.byType(TextField).last, '왼쪽 폴더');
|
||||||
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect([for (final b in c.settings.folderBookmarks) (b.name, b.path)], [('왼쪽 폴더', left)]);
|
expect([for (final b in c.settings.folderBookmarks) (b.name, b.path)], [('왼쪽 폴더', left)]);
|
||||||
expect(find.text('왼쪽 폴더'), findsOneWidget);
|
expect(find.text('왼쪽 폴더'), findsOneWidget);
|
||||||
await tester.tap(find.text('닫기'));
|
await tester.tap(find.text('닫기'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.byIcon(Icons.star), findsOneWidget, reason: '즐겨찾기한 폴더를 보는 창의 ★ 는 채움');
|
expect(find.byIcon(Icons.star), findsOneWidget, reason: '즐겨찾기한 폴더를 보는 창의 ★ 는 채움');
|
||||||
|
|
||||||
// 오른쪽 폴더를 즐겨찾기에 두고 왼쪽 창에서 한 번에 가기
|
// 오른쪽 폴더를 즐겨찾기에 두고 왼쪽 창에서 한 번에 가기
|
||||||
await c.updateSettings((x) => x.folderBookmarks = [...x.folderBookmarks, FolderBookmark(name: 'R', path: right)]);
|
await c.updateSettings((x) => x.folderBookmarks = [...x.folderBookmarks, FolderBookmark(name: 'R', path: right)]);
|
||||||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await act(tester, () => tester.tap(find.text('R')));
|
await act(tester, () => tester.tap(find.text('R')));
|
||||||
await settle(tester, () => false, rounds: 20);
|
await settle(tester, () => false, rounds: 20);
|
||||||
expect(find.byIcon(Icons.star), findsNWidgets(2), reason: '두 창 모두 즐겨찾기한 폴더 (왼쪽 창이 R 로 감)');
|
expect(find.byIcon(Icons.star), findsNWidgets(2), reason: '두 창 모두 즐겨찾기한 폴더 (왼쪽 창이 R 로 감)');
|
||||||
|
|
||||||
// 빼기: 즐겨찾기에서만 (폴더는 그대로)
|
// 빼기: 즐겨찾기에서만 (폴더는 그대로)
|
||||||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.tap(find.byTooltip('즐겨찾기에서 빼기 (폴더는 그대로)').first);
|
await tester.tap(find.byTooltip('즐겨찾기에서 빼기 (폴더는 그대로)').first);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect([for (final b in c.settings.folderBookmarks) b.name], ['R']);
|
expect([for (final b in c.settings.folderBookmarks) b.name], ['R']);
|
||||||
expect(Directory(left).existsSync(), isTrue);
|
expect(Directory(left).existsSync(), isTrue);
|
||||||
await tester.tap(find.text('닫기'));
|
await tester.tap(find.text('닫기'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('73: Rsync 화면 - "왼쪽 + 오른쪽" 짝을 넣고 한 번에 연다', (tester) async {
|
testWidgets('73: Rsync 화면 - "왼쪽 + 오른쪽" 짝을 넣고 한 번에 연다', (tester) async {
|
||||||
|
|
@ -336,19 +336,19 @@ void main() {
|
||||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c, rsync: true))));
|
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c, rsync: true))));
|
||||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.tap(find.text('지금 짝 추가'));
|
await tester.tap(find.text('지금 짝 추가'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
final pair = c.settings.folderBookmarks.single;
|
final pair = c.settings.folderBookmarks.single;
|
||||||
expect((pair.path, pair.right, pair.name), (left, right, 'left ⇄ right'));
|
expect((pair.path, pair.right, pair.name), (left, right, 'left ⇄ right'));
|
||||||
await tester.tap(find.text('닫기'));
|
await tester.tap(find.text('닫기'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
// 반대 짝을 넣어 두고 열면 두 창이 함께 바뀐다
|
// 반대 짝을 넣어 두고 열면 두 창이 함께 바뀐다
|
||||||
await c.updateSettings((x) => x.folderBookmarks = [...x.folderBookmarks, FolderBookmark(name: '거꾸로', path: right, right: left)]);
|
await c.updateSettings((x) => x.folderBookmarks = [...x.folderBookmarks, FolderBookmark(name: '거꾸로', path: right, right: left)]);
|
||||||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await act(tester, () => tester.tap(find.text('거꾸로')));
|
await act(tester, () => tester.tap(find.text('거꾸로')));
|
||||||
await settle(tester, () => c.settings.rsyncPaths.length == 2 && c.settings.rsyncPaths[0] == right && c.settings.rsyncPaths[1] == left,
|
await settle(tester, () => c.settings.rsyncPaths.length == 2 && c.settings.rsyncPaths[0] == right && c.settings.rsyncPaths[1] == left,
|
||||||
rounds: 100);
|
rounds: 100);
|
||||||
|
|
@ -368,9 +368,9 @@ void main() {
|
||||||
|
|
||||||
Future<void> tool(String label) async {
|
Future<void> tool(String label) async {
|
||||||
await tester.tap(find.text('고르기 도구').first);
|
await tester.tap(find.text('고르기 도구').first);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.tap(find.textContaining(label).last);
|
await tester.tap(find.textContaining(label).last);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 같은 확장자 모두 (.srt) - 마지막으로 누른 a.srt 기준
|
// 같은 확장자 모두 (.srt) - 마지막으로 누른 a.srt 기준
|
||||||
|
|
@ -386,7 +386,7 @@ void main() {
|
||||||
await tool('패턴으로 고르기');
|
await tool('패턴으로 고르기');
|
||||||
await tester.enterText(find.byType(TextField).last, '*.MKV; doc.*');
|
await tester.enterText(find.byType(TextField).last, '*.MKV; doc.*');
|
||||||
await tester.tap(find.widgetWithText(FilledButton, '고르기'));
|
await tester.tap(find.widgetWithText(FilledButton, '고르기'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.text('2개 표시함 · 13B'), findsOneWidget);
|
expect(find.text('2개 표시함 · 13B'), findsOneWidget);
|
||||||
|
|
||||||
// 범위: a.srt 를 표시한 뒤 Shift + c.mkv → a.srt · b.srt · c.mkv (보이는 순서)
|
// 범위: a.srt 를 표시한 뒤 Shift + c.mkv → a.srt · b.srt · c.mkv (보이는 순서)
|
||||||
|
|
@ -404,9 +404,9 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await act(tester, () => tester.tap(markOf('b.srt')));
|
await act(tester, () => tester.tap(markOf('b.srt')));
|
||||||
await tester.longPress(find.text('doc.txt').first);
|
await tester.longPress(find.text('doc.txt').first);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.tap(find.text('여기까지 범위 고르기'));
|
await tester.tap(find.text('여기까지 범위 고르기'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.textContaining('3개 표시함'), findsOneWidget, reason: 'b.srt · c.mkv · doc.txt');
|
expect(find.textContaining('3개 표시함'), findsOneWidget, reason: 'b.srt · c.mkv · doc.txt');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -548,14 +548,14 @@ void main() {
|
||||||
await settle(tester, () => find.textContaining('2개 표시함').evaluate().isNotEmpty);
|
await settle(tester, () => find.textContaining('2개 표시함').evaluate().isNotEmpty);
|
||||||
expect(find.textContaining('2개 표시함'), findsOneWidget);
|
expect(find.textContaining('2개 표시함'), findsOneWidget);
|
||||||
await act(tester, () => tester.tap(find.text('삭제').first));
|
await act(tester, () => tester.tap(find.text('삭제').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
// 지울 항목 이름이 확인 창에 보인다. 65: Windows 는 기본이 휴지통 (시험에서는 영구 삭제를 골라 휴지통을 어지럽히지 않음)
|
// 지울 항목 이름이 확인 창에 보인다. 65: Windows 는 기본이 휴지통 (시험에서는 영구 삭제를 골라 휴지통을 어지럽히지 않음)
|
||||||
expect(find.textContaining('2개 항목을 휴지통으로 보낼까요?'), findsOneWidget);
|
expect(find.textContaining('2개 항목을 휴지통으로 보낼까요?'), findsOneWidget);
|
||||||
expect(find.widgetWithText(FilledButton, '휴지통으로'), findsOneWidget);
|
expect(find.widgetWithText(FilledButton, '휴지통으로'), findsOneWidget);
|
||||||
expect(find.textContaining('· sub'), findsOneWidget);
|
expect(find.textContaining('· sub'), findsOneWidget);
|
||||||
expect(find.textContaining('· doc.txt'), findsOneWidget);
|
expect(find.textContaining('· doc.txt'), findsOneWidget);
|
||||||
await tester.tap(find.text('휴지통을 거치지 않고 영구 삭제 (Shift+Delete)'));
|
await tester.tap(find.text('휴지통을 거치지 않고 영구 삭제 (Shift+Delete)'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.textContaining('2개 항목을 지울까요? 되돌릴 수 없습니다.'), findsOneWidget);
|
expect(find.textContaining('2개 항목을 지울까요? 되돌릴 수 없습니다.'), findsOneWidget);
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '영구 삭제')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '영구 삭제')));
|
||||||
await settle(tester, () => find.text('doc.txt').evaluate().isEmpty);
|
await settle(tester, () => find.text('doc.txt').evaluate().isEmpty);
|
||||||
|
|
@ -564,14 +564,14 @@ void main() {
|
||||||
// 새 폴더 → 이름 변경 (지운 뒤 창들이 목록을 다시 읽는 동안은 진행 막대가 돌아 pumpAndSettle 이 끝나지 않는다 - 무거울 때 흔들림)
|
// 새 폴더 → 이름 변경 (지운 뒤 창들이 목록을 다시 읽는 동안은 진행 막대가 돌아 pumpAndSettle 이 끝나지 않는다 - 무거울 때 흔들림)
|
||||||
await idle(tester);
|
await idle(tester);
|
||||||
await act(tester, () => tester.tap(find.text('새 폴더').first));
|
await act(tester, () => tester.tap(find.text('새 폴더').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await act(tester, () => tester.enterText(find.byType(TextField), 'films'));
|
await act(tester, () => tester.enterText(find.byType(TextField), 'films'));
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||||||
await settle(tester, () => find.text('films').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('films').evaluate().isNotEmpty);
|
||||||
expect(Directory(p.join(left, 'films')).existsSync(), isTrue);
|
expect(Directory(p.join(left, 'films')).existsSync(), isTrue);
|
||||||
await idle(tester);
|
await idle(tester);
|
||||||
await act(tester, () => tester.tap(find.text('이름 변경').first));
|
await act(tester, () => tester.tap(find.text('이름 변경').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await act(tester, () => tester.enterText(find.byType(TextField), 'movies'));
|
await act(tester, () => tester.enterText(find.byType(TextField), 'movies'));
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||||||
await settle(tester, () => find.text('movies').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('movies').evaluate().isNotEmpty);
|
||||||
|
|
@ -581,33 +581,33 @@ void main() {
|
||||||
testWidgets('창 배치: 한 창 · 버튼 줄 숨김, 버튼 구성: 버튼 빼기 → 설정에 저장', (tester) async {
|
testWidgets('창 배치: 한 창 · 버튼 줄 숨김, 버튼 구성: 버튼 빼기 → 설정에 저장', (tester) async {
|
||||||
await open(tester);
|
await open(tester);
|
||||||
await tester.tap(find.byTooltip('창 배치 · 버튼 구성'));
|
await tester.tap(find.byTooltip('창 배치 · 버튼 구성'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.tap(find.text('창 배치').last);
|
await tester.tap(find.text('창 배치').last);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.tap(find.widgetWithText(ChoiceChip, '한 창'));
|
await tester.tap(find.widgetWithText(ChoiceChip, '한 창'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.tap(find.widgetWithText(ChoiceChip, '숨김'));
|
await tester.tap(find.widgetWithText(ChoiceChip, '숨김'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect([c.settings.explorerLayout, c.settings.explorerToolbar], ['single', 'hidden']);
|
expect([c.settings.explorerLayout, c.settings.explorerToolbar], ['single', 'hidden']);
|
||||||
expect(find.text('상위 폴더'), findsNothing); // 버튼 줄 숨김
|
expect(find.text('상위 폴더'), findsNothing); // 버튼 줄 숨김
|
||||||
expect(find.text('doc.txt'), findsOneWidget); // 한 창 (왼쪽만)
|
expect(find.text('doc.txt'), findsOneWidget); // 한 창 (왼쪽만)
|
||||||
|
|
||||||
// ⋮ 메뉴에서 버튼 구성: "찾기" 빼기
|
// ⋮ 메뉴에서 버튼 구성: "찾기" 빼기
|
||||||
await c.updateSettings((s) => s.explorerToolbar = 'middle');
|
await c.updateSettings((s) => s.explorerToolbar = 'middle');
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.tap(find.byTooltip('창 배치 · 버튼 구성'));
|
await tester.tap(find.byTooltip('창 배치 · 버튼 구성'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.tap(find.text('버튼 구성').last);
|
await tester.tap(find.text('버튼 구성').last);
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
// 처음 값 순서 (자주 쓰는 것부터) 에서 뒤쪽이라 목록을 내려 보이게 한 뒤
|
// 처음 값 순서 (자주 쓰는 것부터) 에서 뒤쪽이라 목록을 내려 보이게 한 뒤
|
||||||
await tester.scrollUntilVisible(find.widgetWithText(CheckboxListTile, 'MKV 목록에 추가'), 80,
|
await tester.scrollUntilVisible(find.widgetWithText(CheckboxListTile, 'MKV 목록에 추가'), 80,
|
||||||
scrollable: find.descendant(of: find.byType(AlertDialog), matching: find.byType(Scrollable)).first);
|
scrollable: find.descendant(of: find.byType(AlertDialog), matching: find.byType(Scrollable)).first);
|
||||||
await tester.tap(find.widgetWithText(CheckboxListTile, 'MKV 목록에 추가'));
|
await tester.tap(find.widgetWithText(CheckboxListTile, 'MKV 목록에 추가'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
await tester.tap(find.widgetWithText(FilledButton, '확인'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(c.settings.explorerButtons, isNot(contains('addMkv')));
|
expect(c.settings.explorerButtons, isNot(contains('addMkv')));
|
||||||
expect(c.settings.explorerButtons, contains('copy'));
|
expect(c.settings.explorerButtons, contains('copy'));
|
||||||
expect(find.text('MKV 목록에 추가'), findsNothing);
|
expect(find.text('MKV 목록에 추가'), findsNothing);
|
||||||
|
|
@ -673,7 +673,7 @@ void main() {
|
||||||
expect(find.text('$rs/ → $ls/'), findsOneWidget);
|
expect(find.text('$rs/ → $ls/'), findsOneWidget);
|
||||||
expect(find.textContaining('-avPog -u'), findsNWidgets(2));
|
expect(find.textContaining('-avPog -u'), findsNWidgets(2));
|
||||||
await tester.tap(find.text('취소'));
|
await tester.tap(find.text('취소'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(c.settings.copyTasks, isEmpty);
|
expect(c.settings.copyTasks, isEmpty);
|
||||||
expect(find.text('원본 파일 지우기 (--remove-source-files)'), findsNothing); // 양쪽에는 없음
|
expect(find.text('원본 파일 지우기 (--remove-source-files)'), findsNothing); // 양쪽에는 없음
|
||||||
// → 는 한 방향 · -u 없이 · 원본 파일 지우기를 고를 수 있다 (고르면 원본 폴더 남김 / 지움)
|
// → 는 한 방향 · -u 없이 · 원본 파일 지우기를 고를 수 있다 (고르면 원본 폴더 남김 / 지움)
|
||||||
|
|
@ -684,13 +684,13 @@ void main() {
|
||||||
expect(find.textContaining('-u'), findsNothing);
|
expect(find.textContaining('-u'), findsNothing);
|
||||||
expect(find.text('빈 폴더 지움 · 원본 폴더는 남김'), findsNothing);
|
expect(find.text('빈 폴더 지움 · 원본 폴더는 남김'), findsNothing);
|
||||||
await tester.tap(find.text('원본 파일 지우기 (--remove-source-files)'));
|
await tester.tap(find.text('원본 파일 지우기 (--remove-source-files)'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.textContaining('-avPog --remove-source-files'), findsOneWidget);
|
expect(find.textContaining('-avPog --remove-source-files'), findsOneWidget);
|
||||||
expect(find.text('빈 폴더 지움 · 원본 폴더는 남김'), findsOneWidget);
|
expect(find.text('빈 폴더 지움 · 원본 폴더는 남김'), findsOneWidget);
|
||||||
expect(find.text('빈 폴더 지움 · 원본 폴더도 지움'), findsOneWidget);
|
expect(find.text('빈 폴더 지움 · 원본 폴더도 지움'), findsOneWidget);
|
||||||
expect(find.widgetWithText(FilledButton, '이동'), findsOneWidget);
|
expect(find.widgetWithText(FilledButton, '이동'), findsOneWidget);
|
||||||
await tester.tap(find.text('취소'));
|
await tester.tap(find.text('취소'));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
// 고른 폴더는 기억 (다음에 열 때 그 폴더부터)
|
// 고른 폴더는 기억 (다음에 열 때 그 폴더부터)
|
||||||
expect(c.settings.rsyncPaths, [p.join(left, 'sub'), p.join(right, 'rsub')]);
|
expect(c.settings.rsyncPaths, [p.join(left, 'sub'), p.join(right, 'rsub')]);
|
||||||
|
|
||||||
|
|
@ -700,7 +700,7 @@ void main() {
|
||||||
await act(tester, () => tester.tap(find.text('rsub')));
|
await act(tester, () => tester.tap(find.text('rsub')));
|
||||||
await settle(tester, () => false, rounds: 5); // 다시 골라 지금 폴더로
|
await settle(tester, () => false, rounds: 5); // 다시 골라 지금 폴더로
|
||||||
await act(tester, () => tester.tap(find.text('새 폴더')));
|
await act(tester, () => tester.tap(find.text('새 폴더')));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
await tester.enterText(find.byType(TextField), '새로만든');
|
await tester.enterText(find.byType(TextField), '새로만든');
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
|
||||||
// 만든 폴더가 보이고 골라질 때까지 (전체 시험 중 바쁠 때는 고르기가 조금 늦다)
|
// 만든 폴더가 보이고 골라질 때까지 (전체 시험 중 바쁠 때는 고르기가 조금 늦다)
|
||||||
|
|
@ -743,10 +743,21 @@ Future<void> doubleTap(WidgetTester tester, Finder f) async {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 창들이 목록을 다시 읽는 중 (맨 위 진행 막대) 이 끝날 때까지 실제 시간으로 기다린다.
|
/// 창들이 목록을 다시 읽는 중 (맨 위 진행 막대 · 도는 표시) 이 끝날 때까지 실제 시간으로 기다린다.
|
||||||
/// 그사이 pumpAndSettle 을 하면 막대가 계속 움직여, 디스크 읽기가 늦을 때 (전체 시험이 무거울 때) 시간 초과가 난다
|
/// 그사이 pumpAndSettle 을 하면 막대가 계속 움직여, 디스크 읽기가 늦을 때 (전체 시험이 무거울 때) 시간 초과가 난다.
|
||||||
Future<void> idle(WidgetTester tester) =>
|
/// 값이 있는 막대 (디스크 용량 · 진행률) 는 움직이지 않으므로 세지 않는다
|
||||||
settle(tester, () => find.byType(LinearProgressIndicator).evaluate().isEmpty && find.byType(CircularProgressIndicator).evaluate().isEmpty);
|
Future<void> idle(WidgetTester tester) => settle(
|
||||||
|
tester,
|
||||||
|
() => find
|
||||||
|
.byWidgetPredicate((w) => w is ProgressIndicator && w.value == null)
|
||||||
|
.evaluate()
|
||||||
|
.isEmpty);
|
||||||
|
|
||||||
|
/// 탐색기 시험의 pumpAndSettle: 목록 읽기가 끝난 뒤에 (경로를 바꾸거나 다시 읽은 뒤 무거울 때 흔들리던 것을 한 번에)
|
||||||
|
Future<void> calm(WidgetTester tester) async {
|
||||||
|
await idle(tester);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
/// 실제 파일 작업 (비동기 IO) 이 끝나기를 기다리며 화면을 갱신
|
/// 실제 파일 작업 (비동기 IO) 이 끝나기를 기다리며 화면을 갱신
|
||||||
// 끝나면 바로 나온다 - 넉넉한 횟수는 전체 시험 중 PC 가 바쁠 때 (실제 파일 입출력이 느려짐) 흔들리지 않게
|
// 끝나면 바로 나온다 - 넉넉한 횟수는 전체 시험 중 PC 가 바쁠 때 (실제 파일 입출력이 느려짐) 흔들리지 않게
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
import 'explorer_test.dart' show act, doubleTap, settle;
|
import 'explorer_test.dart' show act, doubleTap, settle, calm;
|
||||||
import 'support/dav_server.dart';
|
import 'support/dav_server.dart';
|
||||||
|
|
||||||
class _Shell extends NoopShell {
|
class _Shell extends NoopShell {
|
||||||
|
|
@ -76,7 +76,7 @@ void main() {
|
||||||
// 서버가 없으면 위쪽 저장 장치 옆에 "WebDAV" 탭 (창마다)
|
// 서버가 없으면 위쪽 저장 장치 옆에 "WebDAV" 탭 (창마다)
|
||||||
expect(find.text('WebDAV'), findsNWidgets(2));
|
expect(find.text('WebDAV'), findsNWidgets(2));
|
||||||
await act(tester, () => tester.tap(find.text('WebDAV').last));
|
await act(tester, () => tester.tap(find.text('WebDAV').last));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.text('WebDAV 서버 추가'), findsOneWidget);
|
expect(find.text('WebDAV 서버 추가'), findsOneWidget);
|
||||||
await tester.enterText(find.widgetWithText(TextField, '이름 (탭에 보임, 비우면 주소)'), '집 NAS');
|
await tester.enterText(find.widgetWithText(TextField, '이름 (탭에 보임, 비우면 주소)'), '집 NAS');
|
||||||
await tester.enterText(find.widgetWithText(TextField, '주소'), server.url);
|
await tester.enterText(find.widgetWithText(TextField, '주소'), server.url);
|
||||||
|
|
@ -108,7 +108,7 @@ void main() {
|
||||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||||
await settle(tester, () => false, rounds: 15);
|
await settle(tester, () => false, rounds: 15);
|
||||||
await act(tester, () => tester.tap(find.text('복사').first));
|
await act(tester, () => tester.tap(find.text('복사').first));
|
||||||
await tester.pumpAndSettle();
|
await calm(tester);
|
||||||
expect(find.textContaining('☁ 집 NAS/원격'), findsOneWidget);
|
expect(find.textContaining('☁ 집 NAS/원격'), findsOneWidget);
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
||||||
final up = File(p.join(remote.path, '원격', 'doc.txt'));
|
final up = File(p.join(remote.path, '원격', 'doc.txt'));
|
||||||
|
|
@ -170,7 +170,8 @@ void main() {
|
||||||
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||||
Future<void> openBookmark() async {
|
Future<void> openBookmark() async {
|
||||||
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||||
await tester.pumpAndSettle();
|
// pumpAndSettle 이 아니라 창이 뜰 때까지 - 목록을 읽는 중 (진행 막대) 이면 무거울 때 시간 초과
|
||||||
|
await settle(tester, () => find.text('NAS 원격').evaluate().isNotEmpty);
|
||||||
await act(tester, () => tester.tap(find.text('NAS 원격')));
|
await act(tester, () => tester.tap(find.text('NAS 원격')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import 'package:jj_mkvmaker/core/cron_window.dart';
|
||||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.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/platform/windows/process_media_tool.dart';
|
||||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/copy_sync_settings.dart' show CopySyncSettings;
|
||||||
import 'package:jj_mkvmaker/ui/monitor_page.dart';
|
import 'package:jj_mkvmaker/ui/monitor_page.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
|
@ -237,6 +238,31 @@ void main() {
|
||||||
expect(live.watching.map((x) => x.source), [b.path]);
|
expect(live.watching.map((x) => x.source), [b.path]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('244: 맞출 것이 없어도 대상에만 있는 것이 있으면 "같습니다" 가 아니라 그 수를 센다', () async {
|
||||||
|
final tmp = Directory.systemTemp.createTempSync('jj_only_');
|
||||||
|
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
final a = Directory(p.join(tmp.path, 'a'))..createSync();
|
||||||
|
final t = Directory(p.join(tmp.path, 't'))..createSync();
|
||||||
|
final now = DateTime.now();
|
||||||
|
for (final d in [a, t]) {
|
||||||
|
(File(p.join(d.path, 'same.txt'))..writeAsStringSync('s')).setLastModifiedSync(now);
|
||||||
|
}
|
||||||
|
File(p.join(t.path, 'only1.txt')).writeAsStringSync('1');
|
||||||
|
Directory(p.join(t.path, 'onlydir')).createSync();
|
||||||
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
final pair = LiveSyncPair(a.path, t.path, method: 'builtin');
|
||||||
|
c.settings.liveSyncPairs = [pair];
|
||||||
|
final live = LiveSync(c);
|
||||||
|
addTearDown(live.dispose);
|
||||||
|
await live.refreshPending(pair);
|
||||||
|
expect(live.pending[LiveSync.keyOf(pair)], isEmpty);
|
||||||
|
expect(live.onlyTarget[LiveSync.keyOf(pair)], 2);
|
||||||
|
File(p.join(t.path, 'only1.txt')).deleteSync();
|
||||||
|
Directory(p.join(t.path, 'onlydir')).deleteSync();
|
||||||
|
await live.refreshPending(pair);
|
||||||
|
expect(live.onlyTarget[LiveSync.keyOf(pair)], 0, reason: '정말 같으면 0 - "같습니다"');
|
||||||
|
});
|
||||||
|
|
||||||
test('dispose 뒤에 맞추기가 끝나도 알리지 않는다 (앱을 끄는 중 - used after disposed 오류 없음)', () async {
|
test('dispose 뒤에 맞추기가 끝나도 알리지 않는다 (앱을 끄는 중 - used after disposed 오류 없음)', () async {
|
||||||
final tmp = Directory.systemTemp.createTempSync('jj_disp_');
|
final tmp = Directory.systemTemp.createTempSync('jj_disp_');
|
||||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
|
@ -275,6 +301,7 @@ void main() {
|
||||||
await tester.tap(find.text('저장'));
|
await tester.tap(find.text('저장'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('옵션을 저장했습니다'), findsOneWidget);
|
expect(find.text('옵션을 저장했습니다'), findsOneWidget);
|
||||||
|
expect(find.text('이 복사를 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?'), findsOneWidget);
|
||||||
expect(find.text('설정 반영 후 실행'), findsOneWidget);
|
expect(find.text('설정 반영 후 실행'), findsOneWidget);
|
||||||
await tester.tap(find.text('현재 유지'));
|
await tester.tap(find.text('현재 유지'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
@ -288,4 +315,30 @@ void main() {
|
||||||
expect(find.text('지금 맞추기'), findsOneWidget);
|
expect(find.text('지금 맞추기'), findsOneWidget);
|
||||||
expect(find.text('복사 · rsync 로 이동'), findsOneWidget);
|
expect(find.text('복사 · rsync 로 이동'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('233: 이동 카드의 저장 창은 "이 이동을" · 229 설정은 Rsync 탭에도 있다', (tester) async {
|
||||||
|
final tmp = Directory.systemTemp.createTempSync('jj_mon233_');
|
||||||
|
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
c.settings.copyTasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, '..'), move: true, method: 'builtin')];
|
||||||
|
tester.view.physicalSize = const Size(1500, 1000);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c)));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.enterText(find.widgetWithText(TextField, '속도 제한'), '500');
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.text('저장'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('현재 유지'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// Rsync 탭의 설정에도 같은 스위치
|
||||||
|
await tester.pumpWidget(MaterialApp(home: Scaffold(body: SingleChildScrollView(child: CopySyncSettings(c: c, rsync: true)))));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('받는 쪽이 더 새것이면 건너뛰기'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('받는 쪽이 더 새것이면 건너뛰기'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(c.settings.copySkipNewer, isFalse);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue