파일 탐색기 고르기 도구 (74) · 붙여넣기 줄에 남기기 · WebDAV 즐겨찾기 시험 (73 보충)
74: 고르기 막대에 "n개 표시함 · 합계 크기" (폴더는 개수만). [고르기 도구 ▾]: 반전 · 패턴으로 고르기 (*.srt, ?, ;, 대소문자 무시) · 같은 확장자 모두. 범위: PC Shift+누르기, 폰은 첫 항목을 표시한 뒤 마지막 항목을 길게 눌러 [여기까지 범위 고르기] (시작점은 창마다). 73 보충: 복사 · 이동으로 담아 둔 것이 있으면 [붙여넣기] 는 좁아도 줄에 (pinned). WebDAV 폴더 즐겨찾기 - 마스터 문을 지나야 열림. explorer_test · explorer_webdav_test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>fix_mkv
parent
fe4ca6adb2
commit
a7ef14382c
|
|
@ -29,6 +29,7 @@
|
||||||
" 지금 설치됨 ({0}MB)": " Installed now ({0}MB)",
|
" 지금 설치됨 ({0}MB)": " Installed now ({0}MB)",
|
||||||
"\"{0}\" 검색 결과 {1}개": "{1} results for \"{0}\"",
|
"\"{0}\" 검색 결과 {1}개": "{1} results for \"{0}\"",
|
||||||
"\"{0}\" 안에 만듭니다": "Created inside \"{0}\"",
|
"\"{0}\" 안에 만듭니다": "Created inside \"{0}\"",
|
||||||
|
"\"{0}\" 에 맞는 항목이 없습니다": "No items match \"{0}\"",
|
||||||
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "These items are not in \"{0}\" but only in the target \"{1}\". Syncing with delete removes them from the target (cannot be undone).",
|
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "These items are not in \"{0}\" but only in the target \"{1}\". Syncing with delete removes them from the target (cannot be undone).",
|
||||||
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "Move \"{0}\" to the \"{1}\" folder?",
|
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "Move \"{0}\" to the \"{1}\" folder?",
|
||||||
"\"{0}\" 을(를) 지울까요?": "Delete \"{0}\"?",
|
"\"{0}\" 을(를) 지울까요?": "Delete \"{0}\"?",
|
||||||
|
|
@ -51,6 +52,7 @@
|
||||||
"(설명 없음)": "(no description)",
|
"(설명 없음)": "(no description)",
|
||||||
"(지워짐)": "(deleted)",
|
"(지워짐)": "(deleted)",
|
||||||
"(현재 방식은 옵션 없음)": "(no options for the built-in method)",
|
"(현재 방식은 옵션 없음)": "(no options for the built-in method)",
|
||||||
|
"* 는 아무 글자, ? 는 한 글자. 여러 개는 ; 로 (예: *.srt; *.ass)": "* matches any text, ? one character. Separate several with ; (e.g. *.srt; *.ass)",
|
||||||
"+ 여기에 새 줄": "+ New line here",
|
"+ 여기에 새 줄": "+ New line here",
|
||||||
"-1 은 아무렇게나": "-1 = random",
|
"-1 은 아무렇게나": "-1 = random",
|
||||||
".{0} 재생 프로그램 (exe)": ".{0} player (exe)",
|
".{0} 재생 프로그램 (exe)": ".{0} player (exe)",
|
||||||
|
|
@ -434,6 +436,8 @@
|
||||||
"같은 폴더입니다": "Same folder",
|
"같은 폴더입니다": "Same folder",
|
||||||
"같은 항목을 같은 곳으로 복사 · 이동하는 중입니다.": "The same items are already being copied/moved to the same place.",
|
"같은 항목을 같은 곳으로 복사 · 이동하는 중입니다.": "The same items are already being copied/moved to the same place.",
|
||||||
"같은 화질이면 H.264 먼저 받기": "Prefer H.264 at the same quality",
|
"같은 화질이면 H.264 먼저 받기": "Prefer H.264 at the same quality",
|
||||||
|
"같은 확장자 모두": "All with the same extension",
|
||||||
|
"같은 확장자 모두 (.{0})": "All with the same extension (.{0})",
|
||||||
"개수": "Count",
|
"개수": "Count",
|
||||||
"거꾸로": "Reverse",
|
"거꾸로": "Reverse",
|
||||||
"거꾸로 된 범위: {0}": "Reversed range: {0}",
|
"거꾸로 된 범위: {0}": "Reversed range: {0}",
|
||||||
|
|
@ -463,6 +467,7 @@
|
||||||
"계속 보기를 멈췄습니다.": "Slideshow stopped.",
|
"계속 보기를 멈췄습니다.": "Slideshow stopped.",
|
||||||
"계속 편집": "Keep editing",
|
"계속 편집": "Keep editing",
|
||||||
"고르기": "Choose",
|
"고르기": "Choose",
|
||||||
|
"고르기 도구": "Selection tools",
|
||||||
"고르기 취소": "Unselect",
|
"고르기 취소": "Unselect",
|
||||||
"고르지 않음": "Not chosen",
|
"고르지 않음": "Not chosen",
|
||||||
"고른 것 시작": "Start selected",
|
"고른 것 시작": "Start selected",
|
||||||
|
|
@ -871,6 +876,7 @@
|
||||||
"바탕 그림 고르기 (그림 → 그림)": "Choose a base picture (image → image)",
|
"바탕 그림 고르기 (그림 → 그림)": "Choose a base picture (image → image)",
|
||||||
"바탕 그림 빼기": "Remove base picture",
|
"바탕 그림 빼기": "Remove base picture",
|
||||||
"반대로 90° 회전": "Rotate 90° (counterclockwise)",
|
"반대로 90° 회전": "Rotate 90° (counterclockwise)",
|
||||||
|
"반전 (고른 것 ↔ 안 고른 것)": "Invert (selected ↔ not selected)",
|
||||||
"받기": "Download",
|
"받기": "Download",
|
||||||
"받기 ({0})": "Download ({0})",
|
"받기 ({0})": "Download ({0})",
|
||||||
"받기 화면으로": "Go to downloads",
|
"받기 화면으로": "Go to downloads",
|
||||||
|
|
@ -1231,6 +1237,7 @@
|
||||||
"없음": "None",
|
"없음": "None",
|
||||||
"없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "None: one is created when you first press [Move] in the details",
|
"없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "None: one is created when you first press [Move] in the details",
|
||||||
"엔진": "Engine",
|
"엔진": "Engine",
|
||||||
|
"여기까지 범위 고르기": "Select range up to here",
|
||||||
"여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "Drop here to add to the Make MKV video list",
|
"여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "Drop here to add to the Make MKV video list",
|
||||||
"여러 개를 골랐을 때": "When several are selected",
|
"여러 개를 골랐을 때": "When several are selected",
|
||||||
"여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)": "How many to process at once when making several videos into MKV (re-encoding uses a lot of CPU)",
|
"여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)": "How many to process at once when making several videos into MKV (re-encoding uses a lot of CPU)",
|
||||||
|
|
@ -1875,6 +1882,8 @@
|
||||||
"팝업 보기": "Pop-up view",
|
"팝업 보기": "Pop-up view",
|
||||||
"패키지 라이선스": "Package licenses",
|
"패키지 라이선스": "Package licenses",
|
||||||
"패키지에 {0} 이 없습니다": "{0} is missing from the package",
|
"패키지에 {0} 이 없습니다": "{0} is missing from the package",
|
||||||
|
"패턴으로 고르기": "Select by pattern",
|
||||||
|
"패턴으로 고르기 (예: *.srt)": "Select by pattern (e.g. *.srt)",
|
||||||
"페르시아어": "Persian",
|
"페르시아어": "Persian",
|
||||||
"페이지 열기": "Open page",
|
"페이지 열기": "Open page",
|
||||||
"페이지 지우기": "Delete page",
|
"페이지 지우기": "Delete page",
|
||||||
|
|
@ -1889,6 +1898,7 @@
|
||||||
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "Included components and licenses (THIRD_PARTY_NOTICES.txt)",
|
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "Included components and licenses (THIRD_PARTY_NOTICES.txt)",
|
||||||
"폴더": "Folder",
|
"폴더": "Folder",
|
||||||
"폴더 + 파일 목록": "Folders + file list",
|
"폴더 + 파일 목록": "Folders + file list",
|
||||||
|
"폴더 {0}개 (크기 빼고)": "{0} folders (size not counted)",
|
||||||
"폴더 고르기": "Choose folder",
|
"폴더 고르기": "Choose folder",
|
||||||
"폴더 만들기": "New folder",
|
"폴더 만들기": "New folder",
|
||||||
"폴더 복사 · 이동 방법": "Folder copy · move method",
|
"폴더 복사 · 이동 방법": "Folder copy · move method",
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
" 지금 설치됨 ({0}MB)": " インストール済み({0}MB)",
|
" 지금 설치됨 ({0}MB)": " インストール済み({0}MB)",
|
||||||
"\"{0}\" 검색 결과 {1}개": "「{0}」の検索結果 {1}件",
|
"\"{0}\" 검색 결과 {1}개": "「{0}」の検索結果 {1}件",
|
||||||
"\"{0}\" 안에 만듭니다": "「{0}」の中に作成します",
|
"\"{0}\" 안에 만듭니다": "「{0}」の中に作成します",
|
||||||
|
"\"{0}\" 에 맞는 항목이 없습니다": "「{0}」に合う項目はありません",
|
||||||
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "「{0}」にはなく、対象「{1}」にだけある項目です。削除を含めて同期すると対象から削除されます (元に戻せません)。",
|
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "「{0}」にはなく、対象「{1}」にだけある項目です。削除を含めて同期すると対象から削除されます (元に戻せません)。",
|
||||||
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "「{0}」を「{1}」フォルダーに移動しますか?",
|
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "「{0}」を「{1}」フォルダーに移動しますか?",
|
||||||
"\"{0}\" 을(를) 지울까요?": "「{0}」を削除しますか?",
|
"\"{0}\" 을(를) 지울까요?": "「{0}」を削除しますか?",
|
||||||
|
|
@ -51,6 +52,7 @@
|
||||||
"(설명 없음)": "(説明なし)",
|
"(설명 없음)": "(説明なし)",
|
||||||
"(지워짐)": "(削除)",
|
"(지워짐)": "(削除)",
|
||||||
"(현재 방식은 옵션 없음)": "(現在の方式にはオプションなし)",
|
"(현재 방식은 옵션 없음)": "(現在の方式にはオプションなし)",
|
||||||
|
"* 는 아무 글자, ? 는 한 글자. 여러 개는 ; 로 (예: *.srt; *.ass)": "* は任意の文字列、? は 1 文字。複数は ; で区切る (例: *.srt; *.ass)",
|
||||||
"+ 여기에 새 줄": "+ ここに新しい行",
|
"+ 여기에 새 줄": "+ ここに新しい行",
|
||||||
"-1 은 아무렇게나": "-1 はランダム",
|
"-1 은 아무렇게나": "-1 はランダム",
|
||||||
".{0} 재생 프로그램 (exe)": ".{0} 再生プログラム(exe)",
|
".{0} 재생 프로그램 (exe)": ".{0} 再生プログラム(exe)",
|
||||||
|
|
@ -434,6 +436,8 @@
|
||||||
"같은 폴더입니다": "同じフォルダーです",
|
"같은 폴더입니다": "同じフォルダーです",
|
||||||
"같은 항목을 같은 곳으로 복사 · 이동하는 중입니다.": "同じ項目を同じ場所へコピー・移動中です。",
|
"같은 항목을 같은 곳으로 복사 · 이동하는 중입니다.": "同じ項目を同じ場所へコピー・移動中です。",
|
||||||
"같은 화질이면 H.264 먼저 받기": "同じ画質なら H.264 を優先",
|
"같은 화질이면 H.264 먼저 받기": "同じ画質なら H.264 を優先",
|
||||||
|
"같은 확장자 모두": "同じ拡張子をすべて",
|
||||||
|
"같은 확장자 모두 (.{0})": "同じ拡張子をすべて (.{0})",
|
||||||
"개수": "枚数",
|
"개수": "枚数",
|
||||||
"거꾸로": "逆順",
|
"거꾸로": "逆順",
|
||||||
"거꾸로 된 범위: {0}": "逆順の範囲: {0}",
|
"거꾸로 된 범위: {0}": "逆順の範囲: {0}",
|
||||||
|
|
@ -463,6 +467,7 @@
|
||||||
"계속 보기를 멈췄습니다.": "連続表示を停止しました。",
|
"계속 보기를 멈췄습니다.": "連続表示を停止しました。",
|
||||||
"계속 편집": "編集を続ける",
|
"계속 편집": "編集を続ける",
|
||||||
"고르기": "選ぶ",
|
"고르기": "選ぶ",
|
||||||
|
"고르기 도구": "選択ツール",
|
||||||
"고르기 취소": "選択を解除",
|
"고르기 취소": "選択を解除",
|
||||||
"고르지 않음": "未選択",
|
"고르지 않음": "未選択",
|
||||||
"고른 것 시작": "選んだものを開始",
|
"고른 것 시작": "選んだものを開始",
|
||||||
|
|
@ -871,6 +876,7 @@
|
||||||
"바탕 그림 고르기 (그림 → 그림)": "元の画像を選択 (画像 → 画像)",
|
"바탕 그림 고르기 (그림 → 그림)": "元の画像を選択 (画像 → 画像)",
|
||||||
"바탕 그림 빼기": "元の画像を外す",
|
"바탕 그림 빼기": "元の画像を外す",
|
||||||
"반대로 90° 회전": "反対に 90° 回転",
|
"반대로 90° 회전": "反対に 90° 回転",
|
||||||
|
"반전 (고른 것 ↔ 안 고른 것)": "反転 (選択 ↔ 未選択)",
|
||||||
"받기": "ダウンロード",
|
"받기": "ダウンロード",
|
||||||
"받기 ({0})": "ダウンロード ({0})",
|
"받기 ({0})": "ダウンロード ({0})",
|
||||||
"받기 화면으로": "ダウンロード画面へ",
|
"받기 화면으로": "ダウンロード画面へ",
|
||||||
|
|
@ -1231,6 +1237,7 @@
|
||||||
"없음": "なし",
|
"없음": "なし",
|
||||||
"없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "なし: 詳細の[移動]を初めて押したときにフォルダーを選んで作成します",
|
"없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "なし: 詳細の[移動]を初めて押したときにフォルダーを選んで作成します",
|
||||||
"엔진": "エンジン",
|
"엔진": "エンジン",
|
||||||
|
"여기까지 범위 고르기": "ここまで範囲選択",
|
||||||
"여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "ここにドロップすると MKV 作成の動画リストに追加します",
|
"여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "ここにドロップすると MKV 作成の動画リストに追加します",
|
||||||
"여러 개를 골랐을 때": "複数選んだとき",
|
"여러 개를 골랐을 때": "複数選んだとき",
|
||||||
"여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)": "複数の動画を MKV にするとき同時に処理する数(再エンコードは CPU を多く使います)",
|
"여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)": "複数の動画を MKV にするとき同時に処理する数(再エンコードは CPU を多く使います)",
|
||||||
|
|
@ -1875,6 +1882,8 @@
|
||||||
"팝업 보기": "ポップアップ表示",
|
"팝업 보기": "ポップアップ表示",
|
||||||
"패키지 라이선스": "パッケージのライセンス",
|
"패키지 라이선스": "パッケージのライセンス",
|
||||||
"패키지에 {0} 이 없습니다": "パッケージに {0} がありません",
|
"패키지에 {0} 이 없습니다": "パッケージに {0} がありません",
|
||||||
|
"패턴으로 고르기": "パターンで選択",
|
||||||
|
"패턴으로 고르기 (예: *.srt)": "パターンで選択 (例: *.srt)",
|
||||||
"페르시아어": "ペルシャ語",
|
"페르시아어": "ペルシャ語",
|
||||||
"페이지 열기": "ページを開く",
|
"페이지 열기": "ページを開く",
|
||||||
"페이지 지우기": "ページを削除",
|
"페이지 지우기": "ページを削除",
|
||||||
|
|
@ -1889,6 +1898,7 @@
|
||||||
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "含まれるコンポーネントとライセンス(THIRD_PARTY_NOTICES.txt)",
|
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "含まれるコンポーネントとライセンス(THIRD_PARTY_NOTICES.txt)",
|
||||||
"폴더": "フォルダー",
|
"폴더": "フォルダー",
|
||||||
"폴더 + 파일 목록": "フォルダー + ファイル一覧",
|
"폴더 + 파일 목록": "フォルダー + ファイル一覧",
|
||||||
|
"폴더 {0}개 (크기 빼고)": "フォルダー {0} 件 (サイズ除く)",
|
||||||
"폴더 고르기": "フォルダーを選択",
|
"폴더 고르기": "フォルダーを選択",
|
||||||
"폴더 만들기": "フォルダーを作成",
|
"폴더 만들기": "フォルダーを作成",
|
||||||
"폴더 복사 · 이동 방법": "フォルダーのコピー · 移動方法",
|
"폴더 복사 · 이동 방법": "フォルダーのコピー · 移動方法",
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
" 지금 설치됨 ({0}MB)": " 已安装({0}MB)",
|
" 지금 설치됨 ({0}MB)": " 已安装({0}MB)",
|
||||||
"\"{0}\" 검색 결과 {1}개": "“{0}”的搜索结果 {1} 个",
|
"\"{0}\" 검색 결과 {1}개": "“{0}”的搜索结果 {1} 个",
|
||||||
"\"{0}\" 안에 만듭니다": "在“{0}”中创建",
|
"\"{0}\" 안에 만듭니다": "在“{0}”中创建",
|
||||||
|
"\"{0}\" 에 맞는 항목이 없습니다": "没有与“{0}”匹配的项目",
|
||||||
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "这些项目不在“{0}”中,只在目标“{1}”中。包含删除同步后会从目标中删除 (无法撤销)。",
|
"\"{0}\" 에는 없고 대상 \"{1}\" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).": "这些项目不在“{0}”中,只在目标“{1}”中。包含删除同步后会从目标中删除 (无法撤销)。",
|
||||||
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "将“{0}”移到“{1}”文件夹吗?",
|
"\"{0}\" 을(를) \"{1}\" 폴더로 옮길까요?": "将“{0}”移到“{1}”文件夹吗?",
|
||||||
"\"{0}\" 을(를) 지울까요?": "要删除“{0}”吗?",
|
"\"{0}\" 을(를) 지울까요?": "要删除“{0}”吗?",
|
||||||
|
|
@ -51,6 +52,7 @@
|
||||||
"(설명 없음)": "(无说明)",
|
"(설명 없음)": "(无说明)",
|
||||||
"(지워짐)": "(将删除)",
|
"(지워짐)": "(将删除)",
|
||||||
"(현재 방식은 옵션 없음)": "(当前方式没有选项)",
|
"(현재 방식은 옵션 없음)": "(当前方式没有选项)",
|
||||||
|
"* 는 아무 글자, ? 는 한 글자. 여러 개는 ; 로 (예: *.srt; *.ass)": "* 表示任意字符,? 表示一个字符。多个用 ; 分隔(例:*.srt; *.ass)",
|
||||||
"+ 여기에 새 줄": "+ 在此新增一行",
|
"+ 여기에 새 줄": "+ 在此新增一行",
|
||||||
"-1 은 아무렇게나": "-1 为随机",
|
"-1 은 아무렇게나": "-1 为随机",
|
||||||
".{0} 재생 프로그램 (exe)": ".{0} 播放程序(exe)",
|
".{0} 재생 프로그램 (exe)": ".{0} 播放程序(exe)",
|
||||||
|
|
@ -434,6 +436,8 @@
|
||||||
"같은 폴더입니다": "同一文件夹",
|
"같은 폴더입니다": "同一文件夹",
|
||||||
"같은 항목을 같은 곳으로 복사 · 이동하는 중입니다.": "正在将相同项目复制 · 移动到同一位置。",
|
"같은 항목을 같은 곳으로 복사 · 이동하는 중입니다.": "正在将相同项目复制 · 移动到同一位置。",
|
||||||
"같은 화질이면 H.264 먼저 받기": "相同画质时优先 H.264",
|
"같은 화질이면 H.264 먼저 받기": "相同画质时优先 H.264",
|
||||||
|
"같은 확장자 모두": "所有相同扩展名",
|
||||||
|
"같은 확장자 모두 (.{0})": "所有相同扩展名(.{0})",
|
||||||
"개수": "数量",
|
"개수": "数量",
|
||||||
"거꾸로": "倒序",
|
"거꾸로": "倒序",
|
||||||
"거꾸로 된 범위: {0}": "范围顺序颠倒: {0}",
|
"거꾸로 된 범위: {0}": "范围顺序颠倒: {0}",
|
||||||
|
|
@ -463,6 +467,7 @@
|
||||||
"계속 보기를 멈췄습니다.": "已停止连续浏览。",
|
"계속 보기를 멈췄습니다.": "已停止连续浏览。",
|
||||||
"계속 편집": "继续编辑",
|
"계속 편집": "继续编辑",
|
||||||
"고르기": "选择",
|
"고르기": "选择",
|
||||||
|
"고르기 도구": "选择工具",
|
||||||
"고르기 취소": "取消选择",
|
"고르기 취소": "取消选择",
|
||||||
"고르지 않음": "未选择",
|
"고르지 않음": "未选择",
|
||||||
"고른 것 시작": "开始所选",
|
"고른 것 시작": "开始所选",
|
||||||
|
|
@ -871,6 +876,7 @@
|
||||||
"바탕 그림 고르기 (그림 → 그림)": "选择基础图片 (图片 → 图片)",
|
"바탕 그림 고르기 (그림 → 그림)": "选择基础图片 (图片 → 图片)",
|
||||||
"바탕 그림 빼기": "移除基础图片",
|
"바탕 그림 빼기": "移除基础图片",
|
||||||
"반대로 90° 회전": "逆时针旋转 90°",
|
"반대로 90° 회전": "逆时针旋转 90°",
|
||||||
|
"반전 (고른 것 ↔ 안 고른 것)": "反选(已选 ↔ 未选)",
|
||||||
"받기": "下载",
|
"받기": "下载",
|
||||||
"받기 ({0})": "下载 ({0})",
|
"받기 ({0})": "下载 ({0})",
|
||||||
"받기 화면으로": "前往下载画面",
|
"받기 화면으로": "前往下载画面",
|
||||||
|
|
@ -1231,6 +1237,7 @@
|
||||||
"없음": "无",
|
"없음": "无",
|
||||||
"없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "无: 首次在详细信息中点击[移动]时选择文件夹并创建",
|
"없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "无: 首次在详细信息中点击[移动]时选择文件夹并创建",
|
||||||
"엔진": "引擎",
|
"엔진": "引擎",
|
||||||
|
"여기까지 범위 고르기": "选择到此处的范围",
|
||||||
"여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "拖放到此处即可加入制作 MKV 的视频列表",
|
"여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "拖放到此处即可加入制作 MKV 的视频列表",
|
||||||
"여러 개를 골랐을 때": "选择多个时",
|
"여러 개를 골랐을 때": "选择多个时",
|
||||||
"여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)": "将多个视频制作成 MKV 时同时处理的数量(重新编码会占用大量 CPU)",
|
"여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)": "将多个视频制作成 MKV 时同时处理的数量(重新编码会占用大量 CPU)",
|
||||||
|
|
@ -1875,6 +1882,8 @@
|
||||||
"팝업 보기": "弹出窗口观看",
|
"팝업 보기": "弹出窗口观看",
|
||||||
"패키지 라이선스": "软件包许可",
|
"패키지 라이선스": "软件包许可",
|
||||||
"패키지에 {0} 이 없습니다": "包中缺少 {0}",
|
"패키지에 {0} 이 없습니다": "包中缺少 {0}",
|
||||||
|
"패턴으로 고르기": "按模式选择",
|
||||||
|
"패턴으로 고르기 (예: *.srt)": "按模式选择(例:*.srt)",
|
||||||
"페르시아어": "波斯语",
|
"페르시아어": "波斯语",
|
||||||
"페이지 열기": "打开页面",
|
"페이지 열기": "打开页面",
|
||||||
"페이지 지우기": "删除页面",
|
"페이지 지우기": "删除页面",
|
||||||
|
|
@ -1889,6 +1898,7 @@
|
||||||
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "包含的组件与许可(THIRD_PARTY_NOTICES.txt)",
|
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "包含的组件与许可(THIRD_PARTY_NOTICES.txt)",
|
||||||
"폴더": "文件夹",
|
"폴더": "文件夹",
|
||||||
"폴더 + 파일 목록": "文件夹 + 文件列表",
|
"폴더 + 파일 목록": "文件夹 + 文件列表",
|
||||||
|
"폴더 {0}개 (크기 빼고)": "{0} 个文件夹(不计大小)",
|
||||||
"폴더 고르기": "选择文件夹",
|
"폴더 고르기": "选择文件夹",
|
||||||
"폴더 만들기": "新建文件夹",
|
"폴더 만들기": "新建文件夹",
|
||||||
"폴더 복사 · 이동 방법": "文件夹复制 · 移动方式",
|
"폴더 복사 · 이동 방법": "文件夹复制 · 移动方式",
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,9 @@ class _Pane extends ChangeNotifier {
|
||||||
final errors = <String, String>{};
|
final errors = <String, String>{};
|
||||||
final marked = <String>{};
|
final marked = <String>{};
|
||||||
|
|
||||||
|
/// 74: 범위 고르기의 시작 (마지막으로 표시한 것)
|
||||||
|
String? anchor;
|
||||||
|
|
||||||
/// 목록 스크롤 (폴더로 이동하면 그 폴더가 보이게)
|
/// 목록 스크롤 (폴더로 이동하면 그 폴더가 보이게)
|
||||||
final scroll = ScrollController();
|
final scroll = ScrollController();
|
||||||
|
|
||||||
|
|
@ -789,6 +792,12 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
await _pick(pane, e);
|
await _pick(pane, e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 74: Shift + 누르기 = 마지막으로 표시한 것부터 여기까지 (PC)
|
||||||
|
if (!row.isRoot && HardwareKeyboard.instance.isShiftPressed && _canRange(pane, e)) {
|
||||||
|
if (!_selecting) setState(() => _selecting = true);
|
||||||
|
_markRange(pane, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!row.isRoot && (_selecting || HardwareKeyboard.instance.isControlPressed)) {
|
if (!row.isRoot && (_selecting || HardwareKeyboard.instance.isControlPressed)) {
|
||||||
if (!_selecting) setState(() => _selecting = true);
|
if (!_selecting) setState(() => _selecting = true);
|
||||||
await _toggleMark(pane, e);
|
await _toggleMark(pane, e);
|
||||||
|
|
@ -825,6 +834,50 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 74: 범위 고르기를 할 수 있는지 - 마지막으로 표시한 것이 같은 폴더에 있고 다른 항목
|
||||||
|
bool _canRange(_Pane pane, FileEntry e) {
|
||||||
|
final a = pane.anchor;
|
||||||
|
return a != null && !samePath(a, e.path) && samePath(vDirname(a), vDirname(e.path)) && _isMarked(pane, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 74: 마지막으로 표시한 것부터 [to] 까지 (보이는 순서대로) 모두 표시
|
||||||
|
void _markRange(_Pane pane, FileEntry to) {
|
||||||
|
final list = pane.cache[vDirname(to.path)] ?? const <FileEntry>[];
|
||||||
|
final a = list.indexWhere((x) => samePath(x.path, pane.anchor!));
|
||||||
|
final b = list.indexWhere((x) => samePath(x.path, to.path));
|
||||||
|
if (a < 0 || b < 0) return;
|
||||||
|
setState(() {
|
||||||
|
for (var i = a < b ? a : b; i <= (a < b ? b : a); i++) {
|
||||||
|
pane.marked.add(list[i].path);
|
||||||
|
}
|
||||||
|
_normalizeMarks(pane);
|
||||||
|
pane.anchor = to.path;
|
||||||
|
pane.focused = to.path;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 74: 고르기 도구 - 이 폴더 ([dir]) 의 항목에서
|
||||||
|
void _invertMarks(_Pane pane, String dir) => setState(() {
|
||||||
|
for (final x in pane.cache[dir] ?? const <FileEntry>[]) {
|
||||||
|
if (!pane.marked.remove(x.path)) pane.marked.add(x.path);
|
||||||
|
}
|
||||||
|
_normalizeMarks(pane);
|
||||||
|
});
|
||||||
|
|
||||||
|
void _markWhere(_Pane pane, String dir, bool Function(FileEntry e) test) => setState(() {
|
||||||
|
pane.marked.addAll([for (final x in pane.cache[dir] ?? const <FileEntry>[]) if (test(x)) x.path]);
|
||||||
|
_normalizeMarks(pane);
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> _markPattern(_Pane pane, String dir) async {
|
||||||
|
final pattern = await showDialog<String>(context: context, builder: (_) => const _PatternDialog());
|
||||||
|
if (pattern == null || pattern.trim().isEmpty) return;
|
||||||
|
final re = globToRegExp(pattern);
|
||||||
|
final before = pane.marked.length;
|
||||||
|
_markWhere(pane, dir, (x) => re.hasMatch(vBasename(x.path)));
|
||||||
|
if (pane.marked.length == before) _snack(trf('"{0}" 에 맞는 항목이 없습니다', [pattern.trim()]));
|
||||||
|
}
|
||||||
|
|
||||||
/// 다른 표시 안에 든 표시는 뺀다 (폴더 하나로 안의 것까지 고른 것이므로)
|
/// 다른 표시 안에 든 표시는 뺀다 (폴더 하나로 안의 것까지 고른 것이므로)
|
||||||
void _normalizeMarks(_Pane pane) {
|
void _normalizeMarks(_Pane pane) {
|
||||||
final all = pane.marked.toList();
|
final all = pane.marked.toList();
|
||||||
|
|
@ -841,6 +894,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
setState(() {
|
setState(() {
|
||||||
pane.marked.removeWhere((m) => isSameOrInside(m, e.path)); // 안에 따로 고른 것은 이 폴더 하나로
|
pane.marked.removeWhere((m) => isSameOrInside(m, e.path)); // 안에 따로 고른 것은 이 폴더 하나로
|
||||||
pane.marked.add(e.path);
|
pane.marked.add(e.path);
|
||||||
|
pane.anchor = e.path;
|
||||||
});
|
});
|
||||||
if (e.isDir) {
|
if (e.isDir) {
|
||||||
await _load(pane, e.path);
|
await _load(pane, e.path);
|
||||||
|
|
@ -1084,6 +1138,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
('info', Icons.info_outline, tr('정보')),
|
('info', Icons.info_outline, tr('정보')),
|
||||||
]
|
]
|
||||||
: <(String, IconData, String)>[
|
: <(String, IconData, String)>[
|
||||||
|
// 74: 폰에서 범위 고르기 - 첫 항목을 표시한 뒤 마지막 항목을 길게 눌러
|
||||||
|
if (_selecting && _canRange(pane, e)) ('range', Icons.unfold_more, tr('여기까지 범위 고르기')),
|
||||||
if (e.isDir) ('open', Icons.folder_open, tr('열기')),
|
if (e.isDir) ('open', Icons.folder_open, tr('열기')),
|
||||||
if (e.isDir && dual) ('openOther', Icons.vertical_split_outlined, tr('다른 창에서 열기')),
|
if (e.isDir && dual) ('openOther', Icons.vertical_split_outlined, tr('다른 창에서 열기')),
|
||||||
if (e.isDir) ('playFolder', Icons.play_circle_outline, tr('이 폴더의 동영상 재생')),
|
if (e.isDir) ('playFolder', Icons.play_circle_outline, tr('이 폴더의 동영상 재생')),
|
||||||
|
|
@ -1146,6 +1202,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
if (local != null && !await c.services.shell.openWith(local)) _snack(_noAppMessage(e.path));
|
if (local != null && !await c.services.shell.openWith(local)) _snack(_noAppMessage(e.path));
|
||||||
case 'addMkv':
|
case 'addMkv':
|
||||||
await _addMkv([e.path]);
|
await _addMkv([e.path]);
|
||||||
|
case 'range':
|
||||||
|
_markRange(pane, e);
|
||||||
case 'select':
|
case 'select':
|
||||||
setState(() => _selecting = true);
|
setState(() => _selecting = true);
|
||||||
if (!_isMarked(pane, e.path)) await _toggleMark(pane, e);
|
if (!_isMarked(pane, e.path)) await _toggleMark(pane, e);
|
||||||
|
|
@ -2662,6 +2720,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
selected: (b) =>
|
selected: (b) =>
|
||||||
(b == ExplorerButton.hidden && s.explorerShowHidden) || (b == ExplorerButton.select && _selecting),
|
(b == ExplorerButton.hidden && s.explorerShowHidden) || (b == ExplorerButton.select && _selecting),
|
||||||
twoPanes: dual || split,
|
twoPanes: dual || split,
|
||||||
|
pinned: {ExplorerButton.orient, if (_clip.isNotEmpty) ExplorerButton.paste},
|
||||||
);
|
);
|
||||||
final middle = bar != null && (widget.rsync || s.explorerToolbar == 'middle');
|
final middle = bar != null && (widget.rsync || s.explorerToolbar == 'middle');
|
||||||
final edge = bar != null && !widget.rsync && s.explorerToolbar == 'edge';
|
final edge = bar != null && !widget.rsync && s.explorerToolbar == 'edge';
|
||||||
|
|
@ -2706,14 +2765,68 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 선택 모드 막대: 고른 수 · 모두 고르기 · 선택 끝
|
/// 선택 모드 막대: 고른 수 · 모두 고르기 · 선택 끝
|
||||||
Widget _marksBar(_Pane pane, {String? listDir}) => Container(
|
Widget _marksBar(_Pane pane, {String? listDir}) {
|
||||||
|
final dir = listDir ?? pane.current;
|
||||||
|
// 74: 고른 개수와 합계 크기 (읽어 둔 목록 기준, 폴더 크기는 빼고)
|
||||||
|
var size = 0, folders = 0;
|
||||||
|
final byPath = {for (final l in pane.cache.values) for (final x in l) x.path: x};
|
||||||
|
for (final m in pane.marked) {
|
||||||
|
final x = byPath[m];
|
||||||
|
if (x == null) continue;
|
||||||
|
if (x.isDir) {
|
||||||
|
folders++;
|
||||||
|
} else {
|
||||||
|
size += x.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final focusedExt = pane.focused == null || byPath[pane.focused]?.isDir != false ? '' : extOf(pane.focused!);
|
||||||
|
return Container(
|
||||||
color: JjColors.panel,
|
color: JjColors.panel,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Icon(Icons.checklist, size: 18, color: JjColors.accent),
|
Icon(Icons.checklist, size: 18, color: JjColors.accent),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(trf('{0}개 표시함', [pane.marked.length]), style: const TextStyle(fontSize: 12)),
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
[
|
||||||
|
trf('{0}개 표시함', [pane.marked.length]),
|
||||||
|
if (size > 0) formatSize(size),
|
||||||
|
if (folders > 0) trf('폴더 {0}개 (크기 빼고)', [folders]),
|
||||||
|
].join(' · '),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
PopupMenuButton<String>(
|
||||||
|
tooltip: tr('고르기 도구'),
|
||||||
|
onSelected: (v) {
|
||||||
|
switch (v) {
|
||||||
|
case 'invert':
|
||||||
|
_invertMarks(pane, dir);
|
||||||
|
case 'pattern':
|
||||||
|
_markPattern(pane, dir);
|
||||||
|
case 'sameExt':
|
||||||
|
_markWhere(pane, dir, (x) => !x.isDir && extOf(x.path) == focusedExt);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (_) => [
|
||||||
|
PopupMenuItem(value: 'invert', child: Text(tr('반전 (고른 것 ↔ 안 고른 것)'))),
|
||||||
|
PopupMenuItem(value: 'pattern', child: Text(tr('패턴으로 고르기 (예: *.srt)'))),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'sameExt',
|
||||||
|
enabled: focusedExt.isNotEmpty,
|
||||||
|
child: Text(focusedExt.isEmpty ? tr('같은 확장자 모두') : trf('같은 확장자 모두 (.{0})', [focusedExt])),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
Text(tr('고르기 도구'), style: const TextStyle(fontSize: 13, color: JjColors.accent)),
|
||||||
|
const Icon(Icons.arrow_drop_down, size: 18, color: JjColors.accent),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => setState(() {
|
onPressed: () => setState(() {
|
||||||
final dir = listDir ?? pane.current;
|
final dir = listDir ?? pane.current;
|
||||||
|
|
@ -2726,6 +2839,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
TextButton(onPressed: () => _setSelecting(false), child: Text(tr('선택 끝'))),
|
TextButton(onPressed: () => _setSelecting(false), child: Text(tr('선택 끝'))),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 트리 창 (저장 장치 고르기 + 트리)
|
/// 트리 창 (저장 장치 고르기 + 트리)
|
||||||
Widget _paneView(int i, {bool foldersOnly = false}) {
|
Widget _paneView(int i, {bool foldersOnly = false}) {
|
||||||
|
|
@ -3128,13 +3242,17 @@ class _Toolbar extends StatelessWidget {
|
||||||
|
|
||||||
/// 창이 둘인지 (두 창 · 폴더 + 파일 목록). 하나면 [orient] 는 "두 창으로"
|
/// 창이 둘인지 (두 창 · 폴더 + 파일 목록). 하나면 [orient] 는 "두 창으로"
|
||||||
final bool twoPanes;
|
final bool twoPanes;
|
||||||
|
|
||||||
|
/// 좁아도 [더 보기] 로 보내지 않는 버튼 (창 배치 · 담아 둔 것이 있을 때 붙여넣기)
|
||||||
|
final Set<ExplorerButton> pinned;
|
||||||
const _Toolbar(
|
const _Toolbar(
|
||||||
{required this.buttons,
|
{required this.buttons,
|
||||||
required this.vertical,
|
required this.vertical,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
required this.selected,
|
required this.selected,
|
||||||
this.twoPanes = true});
|
this.twoPanes = true,
|
||||||
|
this.pinned = const {ExplorerButton.orient}});
|
||||||
|
|
||||||
/// 118: 지금 배치를 보여 준다 (누르면 반대로)
|
/// 118: 지금 배치를 보여 준다 (누르면 반대로)
|
||||||
String _orientLabel() => !twoPanes ? '두 창으로' : vertical ? '좌우 ⇆' : '위아래 ⇅';
|
String _orientLabel() => !twoPanes ? '두 창으로' : vertical ? '좌우 ⇆' : '위아래 ⇅';
|
||||||
|
|
@ -3250,10 +3368,14 @@ class _Toolbar extends StatelessWidget {
|
||||||
if (fit >= 1) {
|
if (fit >= 1) {
|
||||||
shown = buttons.take(fit).toList();
|
shown = buttons.take(fit).toList();
|
||||||
rest = buttons.skip(fit).toList();
|
rest = buttons.skip(fit).toList();
|
||||||
// 창 배치 버튼은 늘 줄에 (사용자가 가운데 아이콘 모음에 두라고 함, 10/9) - 넘치면 마지막 자리를 내준다
|
// 창 배치 버튼은 늘 줄에 (사용자가 가운데 아이콘 모음에 두라고 함, 10/9) - 넘치면 마지막 자리를 내준다.
|
||||||
if (rest.contains(ExplorerButton.orient)) {
|
// 73 (관리자): 복사 · 이동으로 담아 둔 것이 있으면 [붙여넣기] 도 (담아 두고 붙여넣기를 찾지 못하는 일이 없게)
|
||||||
rest = [shown.removeLast(), ...rest.where((b) => b != ExplorerButton.orient)];
|
for (final b in buttons.where((b) => pinned.contains(b) && rest.contains(b))) {
|
||||||
shown = [...shown, ExplorerButton.orient];
|
final i = shown.lastIndexWhere((x) => !pinned.contains(x));
|
||||||
|
if (i < 0) break;
|
||||||
|
final out = shown[i];
|
||||||
|
shown = [...shown.sublist(0, i), ...shown.sublist(i + 1), b];
|
||||||
|
rest = [out, ...rest.where((x) => x != b)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3482,6 +3604,46 @@ class _Thumb extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 146 · 37-2: 경로 입력 창 - 없는 폴더면 닫지 않고 칸 아래에 "폴더가 없거나 열 수 없습니다"
|
/// 146 · 37-2: 경로 입력 창 - 없는 폴더면 닫지 않고 칸 아래에 "폴더가 없거나 열 수 없습니다"
|
||||||
|
/// 74: 패턴 (예: *.srt, 여러 개는 ; 로) → 이름 전체에 맞는 정규식 (대소문자 무시)
|
||||||
|
RegExp globToRegExp(String pattern) {
|
||||||
|
final parts = pattern.split(RegExp(r'[;,]')).map((x) => x.trim()).where((x) => x.isNotEmpty);
|
||||||
|
String one(String g) => RegExp.escape(g).replaceAll(r'\*', '.*').replaceAll(r'\?', '.');
|
||||||
|
return RegExp('^(?:${parts.map(one).join('|')})\$', caseSensitive: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 74: 패턴 넣기
|
||||||
|
class _PatternDialog extends StatefulWidget {
|
||||||
|
const _PatternDialog();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_PatternDialog> createState() => _PatternDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PatternDialogState extends State<_PatternDialog> {
|
||||||
|
final _ctl = TextEditingController(text: '*.srt');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ctl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => AlertDialog(
|
||||||
|
title: Text(tr('패턴으로 고르기')),
|
||||||
|
content: TextField(
|
||||||
|
controller: _ctl,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: InputDecoration(helperText: tr('* 는 아무 글자, ? 는 한 글자. 여러 개는 ; 로 (예: *.srt; *.ass)')),
|
||||||
|
onSubmitted: (v) => Navigator.pop(context, v),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('취소'))),
|
||||||
|
FilledButton(onPressed: () => Navigator.pop(context, _ctl.text), child: Text(tr('고르기'))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class _PathDialog extends StatefulWidget {
|
class _PathDialog extends StatefulWidget {
|
||||||
const _PathDialog({required this.initial});
|
const _PathDialog({required this.initial});
|
||||||
final String initial;
|
final String initial;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||||
import 'package:jj_mkvmaker/app/settings.dart';
|
import 'package:jj_mkvmaker/app/settings.dart';
|
||||||
|
|
@ -164,14 +165,9 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
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 (관리자): 담아 둔 것이 있으면 [붙여넣기] 는 좁아도 [더 보기] 로 가지 않고 줄에 남는다
|
||||||
if (find.text('붙여넣기').evaluate().isEmpty) {
|
expect(find.text('더 보기'), findsOneWidget, reason: '이 너비에서는 줄이 넘친다');
|
||||||
// 아래 버튼 줄을 가리는 "담았습니다" 알림을 먼저 닫는다
|
expect(find.text('붙여넣기'), findsOneWidget);
|
||||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.tap(find.text('더 보기'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
}
|
|
||||||
await act(tester, () => tester.tap(find.text('붙여넣기')));
|
await act(tester, () => tester.tap(find.text('붙여넣기')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사')));
|
||||||
|
|
@ -201,9 +197,9 @@ void main() {
|
||||||
expect(find.byTooltip('표시 (여러 개 고르기)'), findsWidgets); // 동그라미가 보인다
|
expect(find.byTooltip('표시 (여러 개 고르기)'), findsWidgets); // 동그라미가 보인다
|
||||||
// 파일: 누르면 선택, 다시 누르면 취소 (실행하지 않음)
|
// 파일: 누르면 선택, 다시 누르면 취소 (실행하지 않음)
|
||||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||||
expect(find.text('1개 표시함'), findsOneWidget);
|
expect(find.textContaining('1개 표시함'), findsOneWidget);
|
||||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||||
expect(find.text('0개 표시함'), findsNWidgets(2)); // 두 창 모두 (각 창의 선택 수)
|
expect(find.textContaining('0개 표시함'), findsNWidgets(2)); // 두 창 모두 (각 창의 선택 수)
|
||||||
expect(shell.opened, isEmpty);
|
expect(shell.opened, isEmpty);
|
||||||
|
|
||||||
IconData markIcon(String name) => (tester
|
IconData markIcon(String name) => (tester
|
||||||
|
|
@ -216,7 +212,7 @@ void main() {
|
||||||
// 폴더: 누르면 자기 자신과 안의 것 모두 선택 (펼쳐서 ✔ 로 보여 줌)
|
// 폴더: 누르면 자기 자신과 안의 것 모두 선택 (펼쳐서 ✔ 로 보여 줌)
|
||||||
await act(tester, () => tester.tap(find.text('sub')));
|
await act(tester, () => tester.tap(find.text('sub')));
|
||||||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||||||
expect(find.text('1개 표시함'), findsOneWidget); // 폴더 하나로 안의 것까지
|
expect(find.textContaining('1개 표시함'), findsOneWidget); // 폴더 하나로 안의 것까지
|
||||||
expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(Icons.check_circle));
|
expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(Icons.check_circle));
|
||||||
// 안의 것 하나를 누르면 그것만 빠지고 나머지는 고른 채로
|
// 안의 것 하나를 누르면 그것만 빠지고 나머지는 고른 채로
|
||||||
await act(tester, () => tester.tap(find.text('inner.txt')));
|
await act(tester, () => tester.tap(find.text('inner.txt')));
|
||||||
|
|
@ -227,11 +223,11 @@ void main() {
|
||||||
// 폴더를 다시 누르면 다시 전체 선택, 한 번 더 누르면 자기 자신과 안의 것 모두 취소
|
// 폴더를 다시 누르면 다시 전체 선택, 한 번 더 누르면 자기 자신과 안의 것 모두 취소
|
||||||
await act(tester, () => tester.tap(find.text('sub')));
|
await act(tester, () => tester.tap(find.text('sub')));
|
||||||
await settle(tester, () => false, rounds: 5);
|
await settle(tester, () => false, rounds: 5);
|
||||||
expect(find.text('1개 표시함'), findsOneWidget);
|
expect(find.textContaining('1개 표시함'), findsOneWidget);
|
||||||
expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(Icons.check_circle));
|
expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(Icons.check_circle));
|
||||||
await act(tester, () => tester.tap(find.text('sub')));
|
await act(tester, () => tester.tap(find.text('sub')));
|
||||||
await settle(tester, () => false, rounds: 5);
|
await settle(tester, () => false, rounds: 5);
|
||||||
expect(find.text('0개 표시함'), findsNWidgets(2));
|
expect(find.textContaining('0개 표시함'), findsNWidgets(2));
|
||||||
expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(Icons.check_circle_outline));
|
expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(Icons.check_circle_outline));
|
||||||
// [선택 끝] → 동그라미가 사라진다
|
// [선택 끝] → 동그라미가 사라진다
|
||||||
await act(tester, () => tester.tap(find.text('선택 끝').first)); // 두 창 모두에 있음
|
await act(tester, () => tester.tap(find.text('선택 끝').first)); // 두 창 모두에 있음
|
||||||
|
|
@ -247,7 +243,7 @@ void main() {
|
||||||
await act(tester, () => tester.tap(markOf('doc.txt')));
|
await act(tester, () => tester.tap(markOf('doc.txt')));
|
||||||
await act(tester, () => tester.tap(markOf('locked.txt')));
|
await act(tester, () => tester.tap(markOf('locked.txt')));
|
||||||
await act(tester, () => tester.tap(markOf('sub')));
|
await act(tester, () => tester.tap(markOf('sub')));
|
||||||
await settle(tester, () => find.text('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 tester.pumpAndSettle();
|
||||||
await tester.tap(find.text('휴지통을 거치지 않고 영구 삭제 (Shift+Delete)'));
|
await tester.tap(find.text('휴지통을 거치지 않고 영구 삭제 (Shift+Delete)'));
|
||||||
|
|
@ -326,6 +322,67 @@ void main() {
|
||||||
expect(c.settings.rsyncPaths, [right, left]);
|
expect(c.settings.rsyncPaths, [right, left]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('74: 고르기 도구 - 개수 · 합계 크기, 반전, 패턴, 같은 확장자, 범위 (Shift · 길게 누르기)', (tester) async {
|
||||||
|
for (final n in ['a.srt', 'b.srt', 'c.mkv']) {
|
||||||
|
File(p.join(left, n)).writeAsStringSync('12345678'); // 8B
|
||||||
|
}
|
||||||
|
await open(tester);
|
||||||
|
await settle(tester, () => find.text('c.mkv').evaluate().isNotEmpty);
|
||||||
|
await act(tester, () => tester.tap(find.text('선택').first));
|
||||||
|
await act(tester, () => tester.tap(markOf('a.srt')));
|
||||||
|
await settle(tester, () => find.textContaining('1개 표시함').evaluate().isNotEmpty);
|
||||||
|
expect(find.text('1개 표시함 · 8B'), findsOneWidget);
|
||||||
|
|
||||||
|
Future<void> tool(String label) async {
|
||||||
|
await tester.tap(find.text('고르기 도구').first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.textContaining(label).last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 같은 확장자 모두 (.srt) - 마지막으로 누른 a.srt 기준
|
||||||
|
await tool('같은 확장자 모두');
|
||||||
|
expect(find.text('2개 표시함 · 16B'), findsOneWidget);
|
||||||
|
// 반전: 이 폴더의 나머지 (doc.txt · c.mkv · sub)
|
||||||
|
await tool('반전');
|
||||||
|
expect(find.textContaining('3개 표시함'), findsOneWidget);
|
||||||
|
expect(find.textContaining('폴더 1개 (크기 빼고)'), findsOneWidget);
|
||||||
|
// 패턴
|
||||||
|
await tester.tap(find.text('표시 지우기').first);
|
||||||
|
await tester.pump();
|
||||||
|
await tool('패턴으로 고르기');
|
||||||
|
await tester.enterText(find.byType(TextField).last, '*.MKV; doc.*');
|
||||||
|
await tester.tap(find.widgetWithText(FilledButton, '고르기'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('2개 표시함 · 13B'), findsOneWidget);
|
||||||
|
|
||||||
|
// 범위: a.srt 를 표시한 뒤 Shift + c.mkv → a.srt · b.srt · c.mkv (보이는 순서)
|
||||||
|
await tester.tap(find.text('표시 지우기').first);
|
||||||
|
await tester.pump();
|
||||||
|
await act(tester, () => tester.tap(markOf('a.srt')));
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
||||||
|
await act(tester, () => tester.tap(find.text('c.mkv').first));
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('3개 표시함 · 24B'), findsOneWidget);
|
||||||
|
|
||||||
|
// 폰: 첫 항목을 표시한 뒤 마지막 항목을 길게 눌러 [여기까지 범위 고르기]
|
||||||
|
await tester.tap(find.text('표시 지우기').first);
|
||||||
|
await tester.pump();
|
||||||
|
await act(tester, () => tester.tap(markOf('b.srt')));
|
||||||
|
await tester.longPress(find.text('doc.txt').first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('여기까지 범위 고르기'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.textContaining('3개 표시함'), findsOneWidget, reason: 'b.srt · c.mkv · doc.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('74: 패턴 (* · ? · 여러 개 · 대소문자 무시)', () {
|
||||||
|
final re = globToRegExp('*.srt; ep??.mkv');
|
||||||
|
expect(['a.srt', 'A.SRT', 'ep01.mkv'].every(re.hasMatch), isTrue);
|
||||||
|
expect(['a.srt.bak', 'ep1.mkv', 'x.ass'].any(re.hasMatch), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
test('73: 즐겨찾기 저장 · 읽기 (Rsync 짝 포함)', () {
|
test('73: 즐겨찾기 저장 · 읽기 (Rsync 짝 포함)', () {
|
||||||
final s = AppSettings()
|
final s = AppSettings()
|
||||||
..folderBookmarks = const [FolderBookmark(name: 'NAS/영화', path: r'\\nas\movies'), FolderBookmark(name: '백업', path: r'D:\a', right: r'E:\b')];
|
..folderBookmarks = const [FolderBookmark(name: 'NAS/영화', path: r'\\nas\movies'), FolderBookmark(name: '백업', path: r'D:\a', right: r'E:\b')];
|
||||||
|
|
@ -340,8 +397,8 @@ void main() {
|
||||||
await act(tester, () => tester.tap(markOf('sub')));
|
await act(tester, () => tester.tap(markOf('sub')));
|
||||||
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); // 폴더를 고르면 펼쳐서 안의 것도 ✔
|
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); // 폴더를 고르면 펼쳐서 안의 것도 ✔
|
||||||
await act(tester, () => tester.tap(markOf('doc.txt')));
|
await act(tester, () => tester.tap(markOf('doc.txt')));
|
||||||
await settle(tester, () => find.text('2개 표시함').evaluate().isNotEmpty);
|
await settle(tester, () => find.textContaining('2개 표시함').evaluate().isNotEmpty);
|
||||||
expect(find.text('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 tester.pumpAndSettle();
|
||||||
// 지울 항목 이름이 확인 창에 보인다. 65: Windows 는 기본이 휴지통 (시험에서는 영구 삭제를 골라 휴지통을 어지럽히지 않음)
|
// 지울 항목 이름이 확인 창에 보인다. 65: Windows 는 기본이 휴지통 (시험에서는 영구 삭제를 골라 휴지통을 어지럽히지 않음)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||||
|
import 'package:jj_mkvmaker/app/settings.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/secret_gate.dart';
|
||||||
import 'package:jj_mkvmaker/core/webdav.dart';
|
import 'package:jj_mkvmaker/core/webdav.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';
|
||||||
|
|
@ -147,6 +149,39 @@ void main() {
|
||||||
expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue, reason: '체크를 끄면 원본은 그대로 (복사)');
|
expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue, reason: '체크를 끄면 원본은 그대로 (복사)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('73: WebDAV 폴더도 즐겨찾기로 연다 - 비밀번호가 있는 서버는 마스터 문 (124) 을 지난다', (tester) async {
|
||||||
|
final s = DavServer(id: 'nas', name: '집 NAS', url: server.url, user: 'user', password: 'pass');
|
||||||
|
var asked = 0, allow = false;
|
||||||
|
SecretGate.check = (force) async {
|
||||||
|
asked++;
|
||||||
|
return allow;
|
||||||
|
};
|
||||||
|
addTearDown(() => SecretGate.check = null);
|
||||||
|
c.settings
|
||||||
|
..webdavServers = [s]
|
||||||
|
..explorerPaths = [left, left]
|
||||||
|
..folderBookmarks = [const FolderBookmark(name: 'NAS 원격', path: 'dav://nas/원격')];
|
||||||
|
DavRegistry.configure([s]);
|
||||||
|
await pump(tester, ExplorerPage(c: c));
|
||||||
|
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
|
||||||
|
Future<void> openBookmark() async {
|
||||||
|
await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await act(tester, () => tester.tap(find.text('NAS 원격')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마스터 문을 지나지 못하면 열지 않고 알린다 (저장된 비밀번호를 쓰지 않음)
|
||||||
|
await openBookmark();
|
||||||
|
await settle(tester, () => find.textContaining('마스터 비밀번호가 필요합니다').evaluate().isNotEmpty, rounds: 200);
|
||||||
|
expect(asked, greaterThan(0));
|
||||||
|
expect(find.text('note.txt'), findsNothing);
|
||||||
|
// 지나면 그 폴더 목록
|
||||||
|
allow = true;
|
||||||
|
await openBookmark();
|
||||||
|
await settle(tester, () => find.text('note.txt').evaluate().isNotEmpty, rounds: 300);
|
||||||
|
expect(find.text('note.txt'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Rsync 화면: 로컬 ↔ WebDAV 폴더 → (앱이 직접 맞춤 · rsync 필요 없음)', (tester) async {
|
testWidgets('Rsync 화면: 로컬 ↔ WebDAV 폴더 → (앱이 직접 맞춤 · rsync 필요 없음)', (tester) async {
|
||||||
final s = DavServer(id: 'nas', name: '집 NAS', url: server.url, user: 'user', password: 'pass');
|
final s = DavServer(id: 'nas', name: '집 NAS', url: server.url, user: 'user', password: 'pass');
|
||||||
c.settings
|
c.settings
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue