이동 버튼 여러 개 (표시 이름 · 폴더, 환경 설정 > MKV 만들기), 세부 정보 오른쪽 아래에 표시 이름으로 위로 쌓기. 예전 이동 폴더는 '이동' 버튼으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
RyuJongJin 2026-10-04 20:52:11 +09:00
parent 22364ec16a
commit d7efce87a6
8 changed files with 201 additions and 51 deletions

View File

@ -160,14 +160,17 @@
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg does not have the {0} encoder.", "{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg does not have the {0} encoder.",
"{0} 추가": "Add {0}", "{0} 추가": "Add {0}",
"{0}. {1} (대기)": "{0}. {1} (waiting)", "{0}. {1} (대기)": "{0}. {1} (waiting)",
"{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: moves this video to {1}",
"{0}: 이미 이동 폴더에 있습니다": "{0}: already in the move folder", "{0}: 이미 이동 폴더에 있습니다": "{0}: already in the move folder",
"{0}: 작업 중이라 옮기지 않았습니다": "{0}: not moved because it is being processed", "{0}: 작업 중이라 옮기지 않았습니다": "{0}: not moved because it is being processed",
"{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: moves the {1} checked videos to {2}",
"{0}개": "{0}", "{0}개": "{0}",
"{0}개 · {1}": "{0} · {1}", "{0}개 · {1}": "{0} · {1}",
"{0}개 · 주소 {1}개": "{0} · {1} links", "{0}개 · 주소 {1}개": "{0} · {1} links",
"{0}개 선택": "{0} selected", "{0}개 선택": "{0} selected",
"{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "Imported {0} into \"Other bookmarks\".", "{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "Imported {0} into \"Other bookmarks\".",
"{0}개를 옮겼습니다 → {1}": "Moved {0} → {1}", "{0}개를 옮겼습니다 → {1}": "Moved {0} → {1}",
"{0}개를 옮겼습니다 → {1} ({2})": "Moved {0} → {1} ({2})",
"{0}분 {1}초": "{0} min {1} s", "{0}분 {1}초": "{0} min {1} s",
"{0}시간 {1}분": "{0} h {1} min", "{0}시간 {1}분": "{0} h {1} min",
"{0}줄": "{0} lines", "{0}줄": "{0} lines",
@ -493,6 +496,7 @@
"업데이트 · 설치하고 남은 파일": "Update · install leftovers", "업데이트 · 설치하고 남은 파일": "Update · install leftovers",
"업데이트 실패: {0}": "Update failed: {0}", "업데이트 실패: {0}": "Update failed: {0}",
"없음": "None", "없음": "None",
"없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "None: one is created when you first press [Move] in the details",
"여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "Drop here to add to the Make MKV video list", "여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "Drop here to add to the Make MKV video list",
"여러 동영상을 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)",
"연도": "Year", "연도": "Year",
@ -560,6 +564,7 @@
"이 버전에는 Windows 용 zip 이 없습니다.": "This version has no zip for Windows.", "이 버전에는 Windows 용 zip 이 없습니다.": "This version has no zip for Windows.",
"이 브라우저의 즐겨찾기 파일을 찾지 못했습니다.": "Could not find this browser's bookmarks file.", "이 브라우저의 즐겨찾기 파일을 찾지 못했습니다.": "Could not find this browser's bookmarks file.",
"이 언어 지우기": "Remove this language", "이 언어 지우기": "Remove this language",
"이 이동 버튼 지우기": "Delete this move button",
"이 파일에 맞는 자막 우선 (영상 해시)": "Prefer subtitles that match this file (video hash)", "이 파일에 맞는 자막 우선 (영상 해시)": "Prefer subtitles that match this file (video hash)",
"이 파일용": "For this file", "이 파일용": "For this file",
"이 폴더 선택": "Choose this folder", "이 폴더 선택": "Choose this folder",
@ -569,11 +574,16 @@
"이 폴더에는 고를 파일이 없습니다": "There are no files to choose in this folder", "이 폴더에는 고를 파일이 없습니다": "There are no files to choose in this folder",
"이동": "Move", "이동": "Move",
"이동 ({0})": "Move ({0})", "이동 ({0})": "Move ({0})",
"이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)": "Move buttons (bottom right of the details, stacked upward)",
"이동 버튼 추가": "Add move button",
"이동 버튼의 폴더": "Folder of the move button",
"이동 폴더": "Move folder", "이동 폴더": "Move folder",
"이동 폴더 (환경 설정에서 바꿀 수 있음)": "Move folder (can be changed in Settings)", "이동 폴더 (환경 설정에서 바꿀 수 있음)": "Move folder (can be changed in Settings)",
"이동: {0} → {1}{2}": "Moved: {0} → {1}{2}", "이동: {0} → {1}{2}": "Moved: {0} → {1}{2}",
"이동: 옮길 폴더를 골라 이동 버튼을 만듭니다 (환경 설정 > MKV 만들기 > 이동 버튼에서 여러 개 만들 수 있음)": "Move: choose a folder to create a move button (you can make several in Settings > Make MKV > Move buttons)",
"이동: 이 동영상을 이동 폴더로 옮깁니다{0}": "Move: moves this video to the move folder{0}", "이동: 이 동영상을 이동 폴더로 옮깁니다{0}": "Move: moves this video to the move folder{0}",
"이동: 체크한 동영상 {0}개를 이동 폴더로 옮깁니다": "Move: moves the {0} checked videos to the move folder", "이동: 체크한 동영상 {0}개를 이동 폴더로 옮깁니다": "Move: moves the {0} checked videos to the move folder",
"이동할 폴더가 정해지지 않았습니다 (환경 설정 > MKV 만들기 > 이동 버튼)": "No move folder is set (Settings > Make MKV > Move buttons)",
"이동할 폴더가 정해지지 않았습니다 (환경 설정 > 저장 위치)": "No move folder is set (Settings > Save locations)", "이동할 폴더가 정해지지 않았습니다 (환경 설정 > 저장 위치)": "No move folder is set (Settings > Save locations)",
"이동할 폴더를 만들 수 없습니다: {0} ({1})": "Cannot create the move folder: {0} ({1})", "이동할 폴더를 만들 수 없습니다: {0} ({1})": "Cannot create the move folder: {0} ({1})",
"이름": "Name", "이름": "Name",
@ -799,6 +809,8 @@
"폴더: {0}": "Folder: {0}", "폴더: {0}": "Folder: {0}",
"폴더가 아닙니다: {0}": "Not a folder: {0}", "폴더가 아닙니다: {0}": "Not a folder: {0}",
"폴란드어": "Polish", "폴란드어": "Polish",
"표시 이름": "Display name",
"표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다": "You can edit display names right here. Pressing a video row three times moves it to the first button's folder",
"표준": "Standard", "표준": "Standard",
"푸는 중": "Extracting", "푸는 중": "Extracting",
"프랑스어": "French", "프랑스어": "French",

View File

@ -160,14 +160,17 @@
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg に {0} エンコーダーがありません。", "{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg に {0} エンコーダーがありません。",
"{0} 추가": "{0} を追加", "{0} 추가": "{0} を追加",
"{0}. {1} (대기)": "{0}. {1}(待機)", "{0}. {1} (대기)": "{0}. {1}(待機)",
"{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: この動画を {1} に移動します",
"{0}: 이미 이동 폴더에 있습니다": "{0}: 既に移動フォルダーにあります", "{0}: 이미 이동 폴더에 있습니다": "{0}: 既に移動フォルダーにあります",
"{0}: 작업 중이라 옮기지 않았습니다": "{0}: 作業中のため移動しませんでした", "{0}: 작업 중이라 옮기지 않았습니다": "{0}: 作業中のため移動しませんでした",
"{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: チェックした動画 {1}個を {2} に移動します",
"{0}개": "{0}個", "{0}개": "{0}個",
"{0}개 · {1}": "{0}個・{1}", "{0}개 · {1}": "{0}個・{1}",
"{0}개 · 주소 {1}개": "{0}個・URL {1}個", "{0}개 · 주소 {1}개": "{0}個・URL {1}個",
"{0}개 선택": "{0}個選択", "{0}개 선택": "{0}個選択",
"{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "{0}個を「その他のブックマーク」に取り込みました。", "{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "{0}個を「その他のブックマーク」に取り込みました。",
"{0}개를 옮겼습니다 → {1}": "{0}個を移動しました → {1}", "{0}개를 옮겼습니다 → {1}": "{0}個を移動しました → {1}",
"{0}개를 옮겼습니다 → {1} ({2})": "{0}個を移動しました → {1}({2})",
"{0}분 {1}초": "{0}分 {1}秒", "{0}분 {1}초": "{0}分 {1}秒",
"{0}시간 {1}분": "{0}時間 {1}分", "{0}시간 {1}분": "{0}時間 {1}分",
"{0}줄": "{0}行", "{0}줄": "{0}行",
@ -493,6 +496,7 @@
"업데이트 · 설치하고 남은 파일": "更新・インストールの残り", "업데이트 · 설치하고 남은 파일": "更新・インストールの残り",
"업데이트 실패: {0}": "更新失敗: {0}", "업데이트 실패: {0}": "更新失敗: {0}",
"없음": "なし", "없음": "なし",
"없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "なし: 詳細の[移動]を初めて押したときにフォルダーを選んで作成します",
"여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "ここにドロップすると MKV 作成の動画リストに追加します", "여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "ここにドロップすると MKV 作成の動画リストに追加します",
"여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)": "複数の動画を MKV にするとき同時に処理する数(再エンコードは CPU を多く使います)", "여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)": "複数の動画を MKV にするとき同時に処理する数(再エンコードは CPU を多く使います)",
"연도": "年", "연도": "年",
@ -560,6 +564,7 @@
"이 버전에는 Windows 용 zip 이 없습니다.": "このバージョンには Windows 用 zip がありません。", "이 버전에는 Windows 용 zip 이 없습니다.": "このバージョンには Windows 用 zip がありません。",
"이 브라우저의 즐겨찾기 파일을 찾지 못했습니다.": "このブラウザーのブックマークファイルが見つかりませんでした。", "이 브라우저의 즐겨찾기 파일을 찾지 못했습니다.": "このブラウザーのブックマークファイルが見つかりませんでした。",
"이 언어 지우기": "この言語を削除", "이 언어 지우기": "この言語を削除",
"이 이동 버튼 지우기": "この移動ボタンを削除",
"이 파일에 맞는 자막 우선 (영상 해시)": "このファイルに合う字幕を優先(動画ハッシュ)", "이 파일에 맞는 자막 우선 (영상 해시)": "このファイルに合う字幕を優先(動画ハッシュ)",
"이 파일용": "このファイル用", "이 파일용": "このファイル用",
"이 폴더 선택": "このフォルダーを選択", "이 폴더 선택": "このフォルダーを選択",
@ -569,11 +574,16 @@
"이 폴더에는 고를 파일이 없습니다": "このフォルダーには選べるファイルがありません", "이 폴더에는 고를 파일이 없습니다": "このフォルダーには選べるファイルがありません",
"이동": "移動", "이동": "移動",
"이동 ({0})": "移動({0})", "이동 ({0})": "移動({0})",
"이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)": "移動ボタン(詳細の右下、上に 1つずつ)",
"이동 버튼 추가": "移動ボタンを追加",
"이동 버튼의 폴더": "移動ボタンのフォルダー",
"이동 폴더": "移動フォルダー", "이동 폴더": "移動フォルダー",
"이동 폴더 (환경 설정에서 바꿀 수 있음)": "移動フォルダー(環境設定で変更できます)", "이동 폴더 (환경 설정에서 바꿀 수 있음)": "移動フォルダー(環境設定で変更できます)",
"이동: {0} → {1}{2}": "移動: {0} → {1}{2}", "이동: {0} → {1}{2}": "移動: {0} → {1}{2}",
"이동: 옮길 폴더를 골라 이동 버튼을 만듭니다 (환경 설정 > MKV 만들기 > 이동 버튼에서 여러 개 만들 수 있음)": "移動: 移動先フォルダーを選んで移動ボタンを作ります(環境設定 > MKV 作成 > 移動ボタンで複数作れます)",
"이동: 이 동영상을 이동 폴더로 옮깁니다{0}": "移動: この動画を移動フォルダーに移します{0}", "이동: 이 동영상을 이동 폴더로 옮깁니다{0}": "移動: この動画を移動フォルダーに移します{0}",
"이동: 체크한 동영상 {0}개를 이동 폴더로 옮깁니다": "移動: チェックした動画 {0}個を移動フォルダーに移します", "이동: 체크한 동영상 {0}개를 이동 폴더로 옮깁니다": "移動: チェックした動画 {0}個を移動フォルダーに移します",
"이동할 폴더가 정해지지 않았습니다 (환경 설정 > MKV 만들기 > 이동 버튼)": "移動先フォルダーが決まっていません(環境設定 > MKV 作成 > 移動ボタン)",
"이동할 폴더가 정해지지 않았습니다 (환경 설정 > 저장 위치)": "移動フォルダーが決まっていません(環境設定 > 保存先)", "이동할 폴더가 정해지지 않았습니다 (환경 설정 > 저장 위치)": "移動フォルダーが決まっていません(環境設定 > 保存先)",
"이동할 폴더를 만들 수 없습니다: {0} ({1})": "移動フォルダーを作成できません: {0}({1})", "이동할 폴더를 만들 수 없습니다: {0} ({1})": "移動フォルダーを作成できません: {0}({1})",
"이름": "名前", "이름": "名前",
@ -799,6 +809,8 @@
"폴더: {0}": "フォルダー: {0}", "폴더: {0}": "フォルダー: {0}",
"폴더가 아닙니다: {0}": "フォルダーではありません: {0}", "폴더가 아닙니다: {0}": "フォルダーではありません: {0}",
"폴란드어": "ポーランド語", "폴란드어": "ポーランド語",
"표시 이름": "表示名",
"표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다": "表示名はその場で変更できます。動画の行を 3回押すと最初のボタンのフォルダーに移動します",
"표준": "標準", "표준": "標準",
"푸는 중": "展開中", "푸는 중": "展開中",
"프랑스어": "フランス語", "프랑스어": "フランス語",

View File

@ -160,14 +160,17 @@
"{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg 中没有 {0} 编码器。", "{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg 中没有 {0} 编码器。",
"{0} 추가": "添加 {0}", "{0} 추가": "添加 {0}",
"{0}. {1} (대기)": "{0}. {1}(等待)", "{0}. {1} (대기)": "{0}. {1}(等待)",
"{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: 将此视频移到 {1}",
"{0}: 이미 이동 폴더에 있습니다": "{0}: 已在移动文件夹中", "{0}: 이미 이동 폴더에 있습니다": "{0}: 已在移动文件夹中",
"{0}: 작업 중이라 옮기지 않았습니다": "{0}: 正在处理,未移动", "{0}: 작업 중이라 옮기지 않았습니다": "{0}: 正在处理,未移动",
"{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: 将勾选的 {1} 个视频移到 {2}",
"{0}개": "{0} 个", "{0}개": "{0} 个",
"{0}개 · {1}": "{0} 个 · {1}", "{0}개 · {1}": "{0} 个 · {1}",
"{0}개 · 주소 {1}개": "{0} 个 · 地址 {1} 个", "{0}개 · 주소 {1}개": "{0} 个 · 地址 {1} 个",
"{0}개 선택": "已选 {0} 个", "{0}개 선택": "已选 {0} 个",
"{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "已将 {0} 个导入“其他书签”。", "{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "已将 {0} 个导入“其他书签”。",
"{0}개를 옮겼습니다 → {1}": "已移动 {0} 个 → {1}", "{0}개를 옮겼습니다 → {1}": "已移动 {0} 个 → {1}",
"{0}개를 옮겼습니다 → {1} ({2})": "已移动 {0} 个 → {1}({2})",
"{0}분 {1}초": "{0} 分 {1} 秒", "{0}분 {1}초": "{0} 分 {1} 秒",
"{0}시간 {1}분": "{0} 小时 {1} 分", "{0}시간 {1}분": "{0} 小时 {1} 分",
"{0}줄": "{0} 行", "{0}줄": "{0} 行",
@ -493,6 +496,7 @@
"업데이트 · 설치하고 남은 파일": "更新 · 安装残留", "업데이트 · 설치하고 남은 파일": "更新 · 安装残留",
"업데이트 실패: {0}": "更新失败: {0}", "업데이트 실패: {0}": "更新失败: {0}",
"없음": "无", "없음": "无",
"없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "无: 首次在详细信息中点击[移动]时选择文件夹并创建",
"여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "拖放到此处即可加入制作 MKV 的视频列表", "여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "拖放到此处即可加入制作 MKV 的视频列表",
"여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)": "将多个视频制作成 MKV 时同时处理的数量(重新编码会占用大量 CPU)", "여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)": "将多个视频制作成 MKV 时同时处理的数量(重新编码会占用大量 CPU)",
"연도": "年份", "연도": "年份",
@ -560,6 +564,7 @@
"이 버전에는 Windows 용 zip 이 없습니다.": "此版本没有 Windows 版 zip。", "이 버전에는 Windows 용 zip 이 없습니다.": "此版本没有 Windows 版 zip。",
"이 브라우저의 즐겨찾기 파일을 찾지 못했습니다.": "未找到此浏览器的书签文件。", "이 브라우저의 즐겨찾기 파일을 찾지 못했습니다.": "未找到此浏览器的书签文件。",
"이 언어 지우기": "删除此语言", "이 언어 지우기": "删除此语言",
"이 이동 버튼 지우기": "删除此移动按钮",
"이 파일에 맞는 자막 우선 (영상 해시)": "优先匹配此文件的字幕(视频哈希)", "이 파일에 맞는 자막 우선 (영상 해시)": "优先匹配此文件的字幕(视频哈希)",
"이 파일용": "适用于此文件", "이 파일용": "适用于此文件",
"이 폴더 선택": "选择此文件夹", "이 폴더 선택": "选择此文件夹",
@ -569,11 +574,16 @@
"이 폴더에는 고를 파일이 없습니다": "此文件夹中没有可选择的文件", "이 폴더에는 고를 파일이 없습니다": "此文件夹中没有可选择的文件",
"이동": "移动", "이동": "移动",
"이동 ({0})": "移动({0})", "이동 ({0})": "移动({0})",
"이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)": "移动按钮(详细信息右下角,向上依次排列)",
"이동 버튼 추가": "添加移动按钮",
"이동 버튼의 폴더": "移动按钮的文件夹",
"이동 폴더": "移动目标文件夹", "이동 폴더": "移动目标文件夹",
"이동 폴더 (환경 설정에서 바꿀 수 있음)": "移动目标文件夹(可在设置中更改)", "이동 폴더 (환경 설정에서 바꿀 수 있음)": "移动目标文件夹(可在设置中更改)",
"이동: {0} → {1}{2}": "已移动: {0} → {1}{2}", "이동: {0} → {1}{2}": "已移动: {0} → {1}{2}",
"이동: 옮길 폴더를 골라 이동 버튼을 만듭니다 (환경 설정 > MKV 만들기 > 이동 버튼에서 여러 개 만들 수 있음)": "移动: 选择目标文件夹以创建移动按钮(可在 设置 > 制作 MKV > 移动按钮 中创建多个)",
"이동: 이 동영상을 이동 폴더로 옮깁니다{0}": "移动: 将此视频移到移动目标文件夹{0}", "이동: 이 동영상을 이동 폴더로 옮깁니다{0}": "移动: 将此视频移到移动目标文件夹{0}",
"이동: 체크한 동영상 {0}개를 이동 폴더로 옮깁니다": "移动: 将勾选的 {0} 个视频移到移动目标文件夹", "이동: 체크한 동영상 {0}개를 이동 폴더로 옮깁니다": "移动: 将勾选的 {0} 个视频移到移动目标文件夹",
"이동할 폴더가 정해지지 않았습니다 (환경 설정 > MKV 만들기 > 이동 버튼)": "尚未设置移动目标文件夹(设置 > 制作 MKV > 移动按钮)",
"이동할 폴더가 정해지지 않았습니다 (환경 설정 > 저장 위치)": "尚未设置移动目标文件夹(设置 > 保存位置)", "이동할 폴더가 정해지지 않았습니다 (환경 설정 > 저장 위치)": "尚未设置移动目标文件夹(设置 > 保存位置)",
"이동할 폴더를 만들 수 없습니다: {0} ({1})": "无法创建移动目标文件夹: {0}({1})", "이동할 폴더를 만들 수 없습니다: {0} ({1})": "无法创建移动目标文件夹: {0}({1})",
"이름": "名称", "이름": "名称",
@ -799,6 +809,8 @@
"폴더: {0}": "文件夹: {0}", "폴더: {0}": "文件夹: {0}",
"폴더가 아닙니다: {0}": "不是文件夹: {0}", "폴더가 아닙니다: {0}": "不是文件夹: {0}",
"폴란드어": "波兰语", "폴란드어": "波兰语",
"표시 이름": "显示名称",
"표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다": "显示名称可直接修改。连续点击视频行三次会移到第一个按钮的文件夹",
"표준": "标准", "표준": "标准",
"푸는 중": "正在解压", "푸는 중": "正在解压",
"프랑스어": "法语", "프랑스어": "法语",

View File

@ -425,12 +425,11 @@ class AppController extends ChangeNotifier {
return n; return n;
} }
/// 동영상을 이동 폴더 (환경 설정) 로 옮긴다. 옆에 있는 같은 이름의 자막 파일 (a.srt · a.ko.smi …) 과 /// 동영상을 [dest] (환경 설정의 이동 버튼 폴더) 로 옮긴다. 옆에 있는 같은 이름의 자막 파일 (a.srt · a.ko.smi …) 과
/// 만든 결과물 (jj_mkv 의 a.mkv · a_AI.srt · a_ko.srt …) 도 함께 (결과물은 이동 폴더의 jj_mkv 로). /// 만든 결과물 (jj_mkv 의 a.mkv · a_AI.srt · a_ko.srt …) 도 함께 (결과물은 이동 폴더의 jj_mkv 로).
/// 옮긴 동영상은 목록에서 뺀다. 작업 중인 것은 옮기지 않는다. 반환: (옮긴 수, 못 옮긴 이유들) /// 옮긴 동영상은 목록에서 뺀다. 작업 중인 것은 옮기지 않는다. 반환: (옮긴 수, 못 옮긴 이유들)
Future<(int, List<String>)> moveVideos(List<VideoItem> targets) async { Future<(int, List<String>)> moveVideos(List<VideoItem> targets, String dest) async {
final dest = settings.moveTargetDir; if (dest.isEmpty) return (0, [tr('이동할 폴더가 정해지지 않았습니다 (환경 설정 > MKV 만들기 > 이동 버튼)')]);
if (dest == null || dest.isEmpty) return (0, [tr('이동할 폴더가 정해지지 않았습니다 (환경 설정 > 저장 위치)')]);
var moved = 0; var moved = 0;
final errors = <String>[]; final errors = <String>[];
try { try {

View File

@ -8,13 +8,34 @@ import '../core/download_detect.dart';
import '../core/encode_options.dart'; import '../core/encode_options.dart';
import '../core/playlist.dart'; import '../core/playlist.dart';
/// MKV 세부 정보의 이동 버튼 하나: 표시 이름 · 옮길 폴더
class MoveTarget {
final String name;
final String dir;
const MoveTarget(this.name, this.dir);
MoveTarget copyWith({String? name, String? dir}) => MoveTarget(name ?? this.name, dir ?? this.dir);
Map<String, Object?> toJson() => {'name': name, 'dir': dir};
factory MoveTarget.fromJson(Map<Object?, Object?> j) =>
MoveTarget((j['name'] as String?)?.trim().isNotEmpty == true ? j['name'] as String : '이동', j['dir'] as String);
@override
bool operator ==(Object other) => other is MoveTarget && other.name == name && other.dir == dir;
@override
int get hashCode => Object.hash(name, dir);
}
/// 환경 설정 (settings.json 에 저장) /// 환경 설정 (settings.json 에 저장)
class AppSettings { class AppSettings {
/// MKV·자막 저장 위치. null 이면 동영상이 있는 폴더 아래 jj_mkv /// MKV·자막 저장 위치. null 이면 동영상이 있는 폴더 아래 jj_mkv
String? mkvOutputRoot; String? mkvOutputRoot;
/// MKV 목록의 [이동] · 세 번 누르기로 동영상을 옮길 폴더. null 이면 처음 옮길 때 고른다 /// MKV 세부 정보 오른쪽 아래의 이동 버튼들 (표시 이름 · 폴더). 세 번 누르기는 첫 번째 버튼의 폴더로.
String? moveTargetDir; /// 비어 있으면 [이동] 을 처음 누를 때 폴더를 골라 하나 만든다.
List<MoveTarget> moveTargets = [];
/// 웹 브라우저에서 YouTube 광고 자동 건너뛰기 (건너뛰기 버튼 누르기 · 건너뛸 수 없는 광고는 빨리 감기 · 소리 끄기) /// 웹 브라우저에서 YouTube 광고 자동 건너뛰기 (건너뛰기 버튼 누르기 · 건너뛸 수 없는 광고는 빨리 감기 · 소리 끄기)
bool youtubeAdSkip = true; bool youtubeAdSkip = true;
@ -130,7 +151,7 @@ class AppSettings {
Map<String, Object?> toJson() => { Map<String, Object?> toJson() => {
'mkvOutputRoot': mkvOutputRoot, 'mkvOutputRoot': mkvOutputRoot,
'moveTargetDir': moveTargetDir, 'moveTargets': [for (final t in moveTargets) t.toJson()],
'youtubeAdSkip': youtubeAdSkip, 'youtubeAdSkip': youtubeAdSkip,
'youtubeAdHide': youtubeAdHide, 'youtubeAdHide': youtubeAdHide,
'downloadRoot': downloadRoot, 'downloadRoot': downloadRoot,
@ -193,7 +214,12 @@ class AppSettings {
final e = (j['encode'] as Map?) ?? const {}; final e = (j['encode'] as Map?) ?? const {};
return AppSettings() return AppSettings()
..mkvOutputRoot = j['mkvOutputRoot'] as String? ..mkvOutputRoot = j['mkvOutputRoot'] as String?
..moveTargetDir = j['moveTargetDir'] as String? ..moveTargets = [
for (final x in (j['moveTargets'] as List?) ?? const [])
if (x is Map && x['dir'] is String) MoveTarget.fromJson(x),
// 예전 판의 이동 폴더 하나 → "이동" 버튼으로
if (j['moveTargets'] == null && j['moveTargetDir'] is String) MoveTarget('이동', j['moveTargetDir'] as String),
]
..youtubeAdSkip = j['youtubeAdSkip'] as bool? ?? true ..youtubeAdSkip = j['youtubeAdSkip'] as bool? ?? true
..youtubeAdHide = j['youtubeAdHide'] as bool? ?? true ..youtubeAdHide = j['youtubeAdHide'] as bool? ?? true
..downloadRoot = j['downloadRoot'] as String? ..downloadRoot = j['downloadRoot'] as String?

View File

@ -14,6 +14,9 @@ import '../core/output_paths.dart';
import 'ai_dialog.dart'; import 'ai_dialog.dart';
import 'app_actions.dart'; import 'app_actions.dart';
import 'downloads_page.dart'; import 'downloads_page.dart';
import 'package:path/path.dart' as p;
import '../app/settings.dart' show MoveTarget;
import 'folder_picker.dart'; import 'folder_picker.dart';
import 'player_page.dart'; import 'player_page.dart';
import 'subtitle_editor_page.dart'; import 'subtitle_editor_page.dart';
@ -707,25 +710,41 @@ class _VideoDetail extends StatelessWidget {
// 이 동영상을 처리하는 중일 때만 잠금 (다른 작업 중이면 대기열에 넣는다) // 이 동영상을 처리하는 중일 때만 잠금 (다른 작업 중이면 대기열에 넣는다)
final locked = v.status == JobStatus.running; final locked = v.status == JobStatus.running;
// 오른쪽 아래 [이동]: 체크한 동영상이 있으면 그것들, 없으면 지금 보고 있는 이 동영상 // 오른쪽 아래 이동 버튼들 (환경 설정 > MKV 만들기 > 이동 버튼): 표시 이름으로, 위로 하나씩.
final moveTargets = c.checked.isEmpty ? [v] : c.batchTargets; // 체크한 동영상이 있으면 그것들, 없으면 지금 보고 있는 이 동영상을 그 버튼의 폴더로 옮긴다.
final moving = c.checked.isEmpty ? [v] : c.batchTargets;
final canMove = moving.any((x) => x.status != JobStatus.running);
final count = c.checked.isEmpty ? '' : ' (${c.checked.length})';
final buttons = c.settings.moveTargets;
Widget button(MoveTarget? t) => Tooltip(
message: t == null
? tr('이동: 옮길 폴더를 골라 이동 버튼을 만듭니다 (환경 설정 > MKV 만들기 > 이동 버튼에서 여러 개 만들 수 있음)')
: c.checked.isEmpty
? trf('{0}: 이 동영상을 {1} 로 옮깁니다', [t.name, t.dir])
: trf('{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다', [t.name, c.checked.length, t.dir]),
child: FloatingActionButton.extended(
heroTag: null,
onPressed: canMove ? () => moveToTarget(context, c, moving, target: t) : null,
icon: const Icon(Icons.drive_file_move_outline),
label: Text('${t?.name ?? tr('이동')}$count'),
),
);
return Stack(children: [ return Stack(children: [
_list(context, info, video, audioCount, locked), _list(context, info, video, audioCount, locked),
Positioned( Positioned(
right: 16, right: 16,
bottom: 16, bottom: 16,
child: Tooltip( child: Column(
message: c.checked.isEmpty mainAxisSize: MainAxisSize.min,
? trf('이동: 이 동영상을 이동 폴더로 옮깁니다{0}', [c.settings.moveTargetDir == null ? '' : ' (${c.settings.moveTargetDir})']) crossAxisAlignment: CrossAxisAlignment.end,
: trf('이동: 체크한 동영상 {0}개를 이동 폴더로 옮깁니다', [c.checked.length]), children: [
child: FloatingActionButton.extended( if (buttons.isEmpty) button(null),
heroTag: null, // 첫 번째 버튼이 맨 아래 (예전 [이동] 자리), 다음 버튼은 그 위로
onPressed: moveTargets.any((x) => x.status != JobStatus.running) for (var i = buttons.length - 1; i >= 0; i--) ...[
? () => moveToTarget(context, c, moveTargets) button(buttons[i]),
: null, if (i > 0) const SizedBox(height: 10),
icon: const Icon(Icons.drive_file_move_outline), ],
label: Text(c.checked.isEmpty ? tr('이동') : trf('이동 ({0})', [c.checked.length])), ],
),
), ),
), ),
]); ]);
@ -1044,19 +1063,23 @@ class _LogStrip extends StatelessWidget {
); );
} }
/// 동영상을 이동 폴더로 옮긴다 (MKV 목록의 [이동] · 세 번 누르기). 이동 폴더를 아직 안 정했으면 먼저 고른다. /// 동영상을 이동 버튼의 폴더로 옮긴다 (세부 정보의 이동 버튼 · 세 번 누르기). [target] 이 없으면 첫 번째 버튼,
Future<void> moveToTarget(BuildContext context, AppController c, List<VideoItem> targets) async { /// 이동 버튼이 하나도 없으면 폴더를 골라 하나 만든다 (표시 이름은 폴더 이름).
if (targets.isEmpty) return; Future<void> moveToTarget(BuildContext context, AppController c, List<VideoItem> videos, {MoveTarget? target}) async {
if (videos.isEmpty) return;
final messenger = ScaffoldMessenger.maybeOf(context); final messenger = ScaffoldMessenger.maybeOf(context);
if (c.settings.moveTargetDir == null) { var t = target ?? c.settings.moveTargets.firstOrNull;
if (t == null) {
final d = await pickFolder(context, tr('이동 폴더 (환경 설정에서 바꿀 수 있음)')); final d = await pickFolder(context, tr('이동 폴더 (환경 설정에서 바꿀 수 있음)'));
if (d == null) return; if (d == null) return;
await c.updateSettings((x) => x.moveTargetDir = d); t = MoveTarget(p.basename(d).isEmpty ? tr('이동') : p.basename(d), d);
final add = t;
await c.updateSettings((x) => x.moveTargets = [...x.moveTargets, add]);
} }
final (moved, errors) = await c.moveVideos(targets); final (moved, errors) = await c.moveVideos(videos, t.dir);
messenger?.showSnackBar(SnackBar( messenger?.showSnackBar(SnackBar(
content: Text([ content: Text([
if (moved > 0) trf('{0}개를 옮겼습니다 → {1}', [moved, c.settings.moveTargetDir]), if (moved > 0) trf('{0}개를 옮겼습니다 → {1} ({2})', [moved, t.name, t.dir]),
...errors, ...errors,
].join('\n')), ].join('\n')),
)); ));

View File

@ -4,6 +4,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:path/path.dart' as p;
import '../app/app_controller.dart'; import '../app/app_controller.dart';
import '../core/download_detect.dart'; import '../core/download_detect.dart';
@ -373,15 +374,31 @@ class _SettingsPageState extends State<SettingsPage> {
}, },
onReset: () => c.updateSettings((x) => x.mkvOutputRoot = null), onReset: () => c.updateSettings((x) => x.mkvOutputRoot = null),
), ),
_folderTile( _subTitle(tr('이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)')),
title: tr('이동 폴더'), for (var i = 0; i < s.moveTargets.length; i++) _moveTargetTile(i, s.moveTargets[i]),
value: s.moveTargetDir, Padding(
defaultText: tr('정하지 않음: MKV 목록에서 [이동] 을 처음 누를 때 고릅니다'), padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
onPick: () async { child: Row(children: [
final d = await _pickDir(tr('이동 폴더'), s.moveTargetDir); OutlinedButton.icon(
if (d != null) await c.updateSettings((x) => x.moveTargetDir = d); onPressed: () async {
}, final d = await _pickDir(tr('이동 버튼의 폴더'));
onReset: () => c.updateSettings((x) => x.moveTargetDir = null), if (d == null) return;
final name = p.basename(d).isEmpty ? tr('이동') : p.basename(d);
await c.updateSettings((x) => x.moveTargets = [...x.moveTargets, MoveTarget(name, d)]);
},
icon: const Icon(Icons.add, size: 18),
label: Text(tr('이동 버튼 추가')),
),
const SizedBox(width: 12),
Expanded(
child: Text(
s.moveTargets.isEmpty
? tr('없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다')
: tr('표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
),
),
]),
), ),
]), ]),
_group('subtitle', Icons.subtitles_outlined, tr('자막 (AI · 인터넷)'), [ _group('subtitle', Icons.subtitles_outlined, tr('자막 (AI · 인터넷)'), [
@ -709,6 +726,45 @@ class _SettingsPageState extends State<SettingsPage> {
); );
} }
/// 이동 버튼 하나: 표시 이름 (바로 고침) · 폴더 · 순서 올리기 · 폴더 바꾸기 · 지우기
Widget _moveTargetTile(int i, MoveTarget t) {
final s = c.settings;
void update(List<MoveTarget> Function(List<MoveTarget>) f) =>
c.updateSettings((x) => x.moveTargets = f([...x.moveTargets]));
return ListTile(
key: ValueKey('move_${i}_${t.dir}'),
leading: const Icon(Icons.drive_file_move_outline, color: JjColors.accent),
title: TextFormField(
initialValue: t.name,
decoration: InputDecoration(isDense: true, labelText: tr('표시 이름')),
onChanged: (v) => update((l) => l..[i] = t.copyWith(name: v.trim().isEmpty ? tr('이동') : v.trim())),
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(t.dir, maxLines: 2, overflow: TextOverflow.ellipsis),
),
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
IconButton(
tooltip: tr('위로'),
icon: const Icon(Icons.arrow_upward, size: 18),
onPressed: i == 0 ? null : () => update((l) => l..insert(i - 1, l.removeAt(i))),
),
OutlinedButton(
onPressed: () async {
final d = await _pickDir(tr('이동 버튼의 폴더'), t.dir);
if (d != null) update((l) => l..[i] = t.copyWith(dir: d));
},
child: Text(tr('폴더 선택')),
),
IconButton(
tooltip: tr('이 이동 버튼 지우기'),
icon: const Icon(Icons.delete_outline),
onPressed: s.moveTargets.length > i ? () => update((l) => l..removeAt(i)) : null,
),
]),
);
}
/// 묶음 이름 → 위치 (바로가기 버튼으로 그 묶음까지 스크롤) /// 묶음 이름 → 위치 (바로가기 버튼으로 그 묶음까지 스크롤)
final _groupKeys = <String, GlobalKey>{}; final _groupKeys = <String, GlobalKey>{};

View File

@ -27,7 +27,6 @@ void main() {
test('이동: 동영상 + 옆의 같은 이름 자막을 옮기고 목록에서 뺀다. 같은 이름이 있으면 (2)', () async { test('이동: 동영상 + 옆의 같은 이름 자막을 옮기고 목록에서 뺀다. 같은 이름이 있으면 (2)', () async {
final c = _controller(); final c = _controller();
final dest = p.join(dir.path, 'done'); final dest = p.join(dir.path, 'done');
c.settings.moveTargetDir = dest;
final a = VideoItem(make('a.mp4').path) final a = VideoItem(make('a.mp4').path)
..subtitles.add(SubtitleEntry.external(path: make('a.ko.srt').path, language: undetermined)) ..subtitles.add(SubtitleEntry.external(path: make('a.ko.srt').path, language: undetermined))
..subtitles.add(SubtitleEntry.external(path: make('other.srt').path, language: undetermined)); ..subtitles.add(SubtitleEntry.external(path: make('other.srt').path, language: undetermined));
@ -43,7 +42,7 @@ void main() {
c.videos.addAll([a, b, busy]); c.videos.addAll([a, b, busy]);
c.selected = a; c.selected = a;
final (moved, errors) = await c.moveVideos([a, b, busy]); final (moved, errors) = await c.moveVideos([a, b, busy], dest);
expect(moved, 2); expect(moved, 2);
expect(errors.single, contains('작업 중')); expect(errors.single, contains('작업 중'));
expect(File(p.join(dest, 'a.mp4')).existsSync(), isTrue); expect(File(p.join(dest, 'a.mp4')).existsSync(), isTrue);
@ -80,25 +79,28 @@ void main() {
final c = _controller(); final c = _controller();
final a = VideoItem(make('a.mp4').path); final a = VideoItem(make('a.mp4').path);
c.videos.add(a); c.videos.add(a);
final (moved, errors) = await c.moveVideos([a]); final (moved, errors) = await c.moveVideos([a], '');
expect(moved, 0); expect(moved, 0);
expect(errors.single, contains('이동할 폴더')); expect(errors.single, contains('이동할 폴더'));
expect(File(a.path).existsSync(), isTrue); expect(File(a.path).existsSync(), isTrue);
}); });
test('이동 폴더 설정 저장', () { test('이동 버튼 설정 저장 (표시 이름 · 폴더 · 순서), 예전 이동 폴더 하나는 "이동" 버튼으로', () {
final s = AppSettings()..moveTargetDir = r'D:\done'; final s = AppSettings()
expect(AppSettings.fromJson(s.toJson()).moveTargetDir, r'D:\done'); ..moveTargets = const [MoveTarget('완료', r'D:\done'), MoveTarget('보관', r'E:\archive')];
expect(AppSettings().moveTargetDir, isNull); expect(AppSettings.fromJson(s.toJson()).moveTargets, s.moveTargets);
expect(AppSettings().moveTargets, isEmpty);
expect(AppSettings.fromJson({'moveTargetDir': r'D:\old'}).moveTargets, const [MoveTarget('이동', r'D:\old')]);
}); });
testWidgets('MKV 목록: 세 번 누르면 이동 · 세부 정보의 [이동] 은 체크한 것 (없으면 보고 있는 것)', (tester) async { testWidgets('MKV 목록: 세 번 누르면 첫 번째 이동 버튼으로 · 세부 정보의 이동 버튼 (표시 이름) 은 그 폴더로', (tester) async {
tester.view.physicalSize = const Size(1600, 900); tester.view.physicalSize = const Size(1600, 900);
tester.view.devicePixelRatio = 1; tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset); addTearDown(tester.view.reset);
final c = _controller(); final c = _controller();
final dest = p.join(dir.path, 'done'); final dest = p.join(dir.path, 'done');
c.settings.moveTargetDir = dest; final archive = p.join(dir.path, 'archive');
c.settings.moveTargets = [MoveTarget('완료함', dest), MoveTarget('보관함', archive)];
final a = VideoItem(make('a.mp4').path); final a = VideoItem(make('a.mp4').path);
final b = VideoItem(make('b.mp4').path); final b = VideoItem(make('b.mp4').path);
final d = VideoItem(make('d.mp4').path); final d = VideoItem(make('d.mp4').path);
@ -124,13 +126,21 @@ void main() {
expect(File(p.join(dest, 'b.mp4')).existsSync(), isTrue); expect(File(p.join(dest, 'b.mp4')).existsSync(), isTrue);
expect(c.videos.map((v) => v.fileName), ['a.mp4', 'd.mp4']); expect(c.videos.map((v) => v.fileName), ['a.mp4', 'd.mp4']);
// 체크 없음: [이동] → 보고 있는 동영상 (a) // 세부 정보 오른쪽 아래: 표시 이름 버튼 두 개 (첫 번째가 아래, 두 번째가 그 위)
c.select(a); c.select(a);
await tester.pump(); await tester.pump();
expect(find.text('이동'), findsOneWidget); expect(find.text('완료함'), findsOneWidget);
await tester.tap(find.text('이동')); expect(find.text('보관함'), findsOneWidget);
expect(find.text('이동'), findsNothing);
expect(tester.getCenter(find.text('보관함')).dy, lessThan(tester.getCenter(find.text('완료함')).dy));
// 체크 없음: [보관함] → 보고 있는 동영상 (a) 를 보관 폴더로
await tester.tap(find.text('보관함'));
await settleIo(); await settleIo();
expect(File(p.join(dest, 'a.mp4')).existsSync(), isTrue); expect(File(p.join(archive, 'a.mp4')).existsSync(), isTrue);
expect(c.videos.map((v) => v.fileName), ['d.mp4']); expect(c.videos.map((v) => v.fileName), ['d.mp4']);
// 체크하면 버튼에 개수
c.toggleChecked(d);
await tester.pump();
expect(find.text('완료함 (1)'), findsOneWidget);
}); });
} }