파일 탐색기 고르기 도구 (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
RyuJongJin 2026-10-10 05:18:35 +09:00
parent fe4ca6adb2
commit a7ef14382c
6 changed files with 307 additions and 23 deletions

View File

@ -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",

View File

@ -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} 件 (サイズ除く)",
"폴더 고르기": "フォルダーを選択", "폴더 고르기": "フォルダーを選択",
"폴더 만들기": "フォルダーを作成", "폴더 만들기": "フォルダーを作成",
"폴더 복사 · 이동 방법": "フォルダーのコピー · 移動方法", "폴더 복사 · 이동 방법": "フォルダーのコピー · 移動方法",

View File

@ -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} 个文件夹(不计大小)",
"폴더 고르기": "选择文件夹", "폴더 고르기": "选择文件夹",
"폴더 만들기": "新建文件夹", "폴더 만들기": "新建文件夹",
"폴더 복사 · 이동 방법": "文件夹复制 · 移动方式", "폴더 복사 · 이동 방법": "文件夹复制 · 移动方式",

View File

@ -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;

View File

@ -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 는 기본이 휴지통 (시험에서는 영구 삭제를 골라 휴지통을 어지럽히지 않음)

View File

@ -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