WIP MKV 화면 (15 지적 ①②③): 고른 것 없음 안내 · 폰 세로 버튼 줄 [›] · 목록은 내용만큼 + 설정 "폰 세로 목록 크기" (검토 대기)

① 동영상이 있는데 고른 것이 없으면 빈 목록 안내 ("동영상 추가로 파일을 선택하세요") 대신
   "고른 동영상이 없습니다 · 목록에서 동영상을 누르면 여기에 자세히 보입니다" (넓은 · 좁은 화면 모두).
   첫 줄 자동 고르기는 하지 않음 (관리자 판단).
② 폰 세로 (폭 412) 위 버튼 줄이 화면보다 길면 끝에 흐림 · [›] (밀었으면 [‹]), 누르면 한 화면쯤 민다
   (새 위젯 ScrollMore). 예전에는 세 번째 버튼이 잘린 채 더 있다는 표시가 없었다 (버그).
③ 폰 세로 목록 높이: 화면 32% 고정 → 관리자 판단대로 목록은 내용만큼, 자세히 칸 최소 높이를 남기고
   남는 공간은 목록에. 설정 "폰 세로 목록 크기" 자동 (처음 값) / 작게 (25%) / 크게 (55%), 모르는 값은 자동.
   - 내 판단: 자동의 자세히 칸 최소 = 고른 동영상이 있으면 62% (261 · 266 의 자막 버튼 보호, 예전 68% 와 비슷),
     없으면 안내 자리 120.
   - 내 판단: 작업 기록 칸이 비었으면 "작업 기록이 아직 없습니다 …" 를 적는다 (✕ 만 있는 빈 칸으로 보였다, 높이는 그대로).
- 시험 mkv_compact_list_test (412 · 1280, 작게 < 크게 < 자동, [›] 누르기, 빈 기록 안내), 번역 en · ja · zh-Hans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음6
RyuJongJin 2026-10-17 10:24:00 +09:00
parent a2cb535f1e
commit 8833c93bf8
8 changed files with 329 additions and 21 deletions

View File

@ -604,6 +604,7 @@
"고르지 않음": "Not chosen", "고르지 않음": "Not chosen",
"고른 것 시작": "Start selected", "고른 것 시작": "Start selected",
"고른 것 풀기": "Extract selected", "고른 것 풀기": "Extract selected",
"고른 동영상이 없습니다": "No video selected",
"고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "Best quality at or below the chosen size (nearest if the video lacks that size)", "고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "Best quality at or below the chosen size (nearest if the video lacks that size)",
"고지 문서": "Notices", "고지 문서": "Notices",
"고치기": "Edit", "고치기": "Edit",
@ -1019,6 +1020,7 @@
"목록": "list", "목록": "list",
"목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다": "Hides ad areas in lists · next to · on top of videos", "목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다": "Hides ad areas in lists · next to · on top of videos",
"목록 보기": "View list", "목록 보기": "View list",
"목록에서 동영상을 누르면 여기에 자세히 보입니다": "Tap a video in the list to see its details here",
"목록에서 빼기": "Remove from list", "목록에서 빼기": "Remove from list",
"목록에서 빼기 (파일은 그대로)": "Remove from list (file is kept)", "목록에서 빼기 (파일은 그대로)": "Remove from list (file is kept)",
"목록에서 제거": "Remove from list", "목록에서 제거": "Remove from list",
@ -1113,6 +1115,7 @@
"버전 목록을 읽을 수 없습니다 ({0})": "Cannot read the version list ({0})", "버전 목록을 읽을 수 없습니다 ({0})": "Cannot read the version list ({0})",
"버전 목록을 읽을 수 없습니다: {0}": "Cannot read the version list: {0}", "버전 목록을 읽을 수 없습니다: {0}": "Cannot read the version list: {0}",
"버튼 구성": "Button layout", "버튼 구성": "Button layout",
"버튼 더 보기": "More buttons",
"번역 모델 내려받는 중 {0}%": "Downloading translation model {0}%", "번역 모델 내려받는 중 {0}%": "Downloading translation model {0}%",
"번역 모델 내려받는 중: {0}": "Downloading translation model: {0}", "번역 모델 내려받는 중: {0}": "Downloading translation model: {0}",
"번역 모델 라이선스: CC BY-NC 4.0 (비상업적 이용만 허용)": "Translation model license: CC BY-NC 4.0 (non-commercial use only)", "번역 모델 라이선스: CC BY-NC 4.0 (비상업적 이용만 허용)": "Translation model license: CC BY-NC 4.0 (non-commercial use only)",
@ -1411,6 +1414,7 @@
"압축을 풀 수 없습니다: {0}": "Cannot extract the archive: {0}", "압축을 풀 수 없습니다: {0}": "Cannot extract the archive: {0}",
"앞으로": "Forward", "앞으로": "Forward",
"앞의 rsync 가 끝난 뒤에 하세요.": "Wait until the previous rsync finishes.", "앞의 rsync 가 끝난 뒤에 하세요.": "Wait until the previous rsync finishes.",
"앞의 버튼 보기": "Previous buttons",
"앞의 복사 · 이동이 끝난 뒤에 하세요.": "Wait until the current copy · move finishes.", "앞의 복사 · 이동이 끝난 뒤에 하세요.": "Wait until the current copy · move finishes.",
"앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "Icon in the app list, home screen and the top-left button in the app. After changing it, the icon on your home screen may disappear and you may need to drag it from the app list again", "앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "Icon in the app list, home screen and the top-left button in the app. After changing it, the icon on your home screen may disappear and you may need to drag it from the app list again",
"앱 목록이 열렸다면, 목록에서 JJ_MKVMaker 를 찾아 눌러 \"모든 파일에 대한 접근\" 을 켜고 돌아오세요.": "If a list of apps opened, find and tap JJ_MKVMaker in the list, turn on \"All files access\", and come back.", "앱 목록이 열렸다면, 목록에서 JJ_MKVMaker 를 찾아 눌러 \"모든 파일에 대한 접근\" 을 켜고 돌아오세요.": "If a list of apps opened, find and tap JJ_MKVMaker in the list, turn on \"All files access\", and come back.",
@ -1819,10 +1823,12 @@
"자세히": "Details", "자세히": "Details",
"자세히 닫기": "Hide details", "자세히 닫기": "Hide details",
"자체 서명 인증서를 쓰는 집 NAS 등 (믿을 수 있는 서버에서만)": "For a home NAS with a self-signed certificate, etc. (trusted servers only)", "자체 서명 인증서를 쓰는 집 NAS 등 (믿을 수 있는 서버에서만)": "For a home NAS with a self-signed certificate, etc. (trusted servers only)",
"작게": "Small",
"작업": "Jobs", "작업": "Jobs",
"작업 기록 · 받다 만 파일": "Activity log · unfinished downloads", "작업 기록 · 받다 만 파일": "Activity log · unfinished downloads",
"작업 기록 보기": "Show job log", "작업 기록 보기": "Show job log",
"작업 기록 숨기기": "Hide job log", "작업 기록 숨기기": "Hide job log",
"작업 기록이 아직 없습니다. MKV 만들기 · 자막 · 다운로드를 하면 여기에 차례로 남습니다.": "No activity yet. MKV builds, subtitles and downloads will be logged here in order.",
"작업 임시 파일": "Job temporary files", "작업 임시 파일": "Job temporary files",
"작업 중": "Working", "작업 중": "Working",
"작업 중지: {0}{1}": "Job stopped: {0}{1}", "작업 중지: {0}{1}": "Job stopped: {0}{1}",
@ -1917,6 +1923,7 @@
"정해 둠. 저장된 비밀번호 · 설정 · 비밀번호가 필요한 기능을 쓸 때 묻습니다 (앱을 끌 때까지 한 번)": "Set. Asked when using saved passwords, settings or features that need a password (once until the app is closed)", "정해 둠. 저장된 비밀번호 · 설정 · 비밀번호가 필요한 기능을 쓸 때 묻습니다 (앱을 끌 때까지 한 번)": "Set. Asked when using saved passwords, settings or features that need a password (once until the app is closed)",
"제목": "Title", "제목": "Title",
"조정 전": "Before", "조정 전": "Before",
"좁은 화면에서 동영상 목록이 차지하는 높이 (자동: 목록은 줄 수만큼, 자세히 칸 자리는 남김)": "Height of the video list on narrow screens (Auto: as tall as its rows, keeping room for the details)",
"종료": "Quit", "종료": "Quit",
"종료 (창 닫기 ✕ · 종료 버튼) 를 누르면": "When you press quit (close window ✕ · quit button)", "종료 (창 닫기 ✕ · 종료 버튼) 를 누르면": "When you press quit (close window ✕ · quit button)",
"종료 시작 {0}": "Quit started {0}", "종료 시작 {0}": "Quit started {0}",
@ -2111,6 +2118,7 @@
"쿠키 · 방문 기록 지우기": "Clear cookies and history", "쿠키 · 방문 기록 지우기": "Clear cookies and history",
"쿠키 · 방문 기록을 지웠습니다.": "Cleared cookies and history.", "쿠키 · 방문 기록을 지웠습니다.": "Cleared cookies and history.",
"쿠키 파일 지우기": "Delete cookie file", "쿠키 파일 지우기": "Delete cookie file",
"크게": "Large",
"크기": "Size", "크기": "Size",
"크기 · 보정을 원래대로 돌려 코덱도 \"원본 유지\" 로 되돌렸습니다.": "Size and adjustments are back to original, so the codec was set back to \"Keep original\".", "크기 · 보정을 원래대로 돌려 코덱도 \"원본 유지\" 로 되돌렸습니다.": "Size and adjustments are back to original, so the codec was set back to \"Keep original\".",
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "Changing the size requires re-encoding, so the codec was changed from \"Keep original\" to H.264.", "크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "Changing the size requires re-encoding, so the codec was changed from \"Keep original\" to H.264.",
@ -2182,6 +2190,7 @@
"포르투갈어": "Portuguese", "포르투갈어": "Portuguese",
"포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)": "Included components and licenses (with full GPL · LGPL texts)", "포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)": "Included components and licenses (with full GPL · LGPL texts)",
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "Included components and licenses (THIRD_PARTY_NOTICES.txt)", "포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "Included components and licenses (THIRD_PARTY_NOTICES.txt)",
"폰 세로 목록 크기": "List size (portrait phone)",
"폴더": "Folder", "폴더": "Folder",
"폴더 + 파일 목록": "Folders + file list", "폴더 + 파일 목록": "Folders + file list",
"폴더 {0}개 (크기 빼고)": "{0} folders (size not counted)", "폴더 {0}개 (크기 빼고)": "{0} folders (size not counted)",

View File

@ -604,6 +604,7 @@
"고르지 않음": "未選択", "고르지 않음": "未選択",
"고른 것 시작": "選んだものを開始", "고른 것 시작": "選んだものを開始",
"고른 것 풀기": "選択したものを展開", "고른 것 풀기": "選択したものを展開",
"고른 동영상이 없습니다": "動画が選択されていません",
"고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "選んだサイズ以下で最も良い画質(動画にないサイズなら近いもの)", "고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "選んだサイズ以下で最も良い画質(動画にないサイズなら近いもの)",
"고지 문서": "告知文書", "고지 문서": "告知文書",
"고치기": "編集", "고치기": "編集",
@ -1019,6 +1020,7 @@
"목록": "一覧", "목록": "一覧",
"목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다": "リスト・動画の横・動画の上に出る広告エリアを隠します", "목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다": "リスト・動画の横・動画の上に出る広告エリアを隠します",
"목록 보기": "リストを見る", "목록 보기": "リストを見る",
"목록에서 동영상을 누르면 여기에 자세히 보입니다": "リストの動画をタップすると、ここに詳細が表示されます",
"목록에서 빼기": "リストから外す", "목록에서 빼기": "リストから外す",
"목록에서 빼기 (파일은 그대로)": "リストから外す (ファイルはそのまま)", "목록에서 빼기 (파일은 그대로)": "リストから外す (ファイルはそのまま)",
"목록에서 제거": "リストから削除", "목록에서 제거": "リストから削除",
@ -1113,6 +1115,7 @@
"버전 목록을 읽을 수 없습니다 ({0})": "バージョン一覧を読めません ({0})", "버전 목록을 읽을 수 없습니다 ({0})": "バージョン一覧を読めません ({0})",
"버전 목록을 읽을 수 없습니다: {0}": "バージョン一覧を読めません: {0}", "버전 목록을 읽을 수 없습니다: {0}": "バージョン一覧を読めません: {0}",
"버튼 구성": "ボタン構成", "버튼 구성": "ボタン構成",
"버튼 더 보기": "ほかのボタンを表示",
"번역 모델 내려받는 중 {0}%": "翻訳モデルをダウンロード中 {0}%", "번역 모델 내려받는 중 {0}%": "翻訳モデルをダウンロード中 {0}%",
"번역 모델 내려받는 중: {0}": "翻訳モデルをダウンロード中: {0}", "번역 모델 내려받는 중: {0}": "翻訳モデルをダウンロード中: {0}",
"번역 모델 라이선스: CC BY-NC 4.0 (비상업적 이용만 허용)": "翻訳モデルのライセンス: CC BY-NC 4.0 (非営利利用のみ)", "번역 모델 라이선스: CC BY-NC 4.0 (비상업적 이용만 허용)": "翻訳モデルのライセンス: CC BY-NC 4.0 (非営利利用のみ)",
@ -1411,6 +1414,7 @@
"압축을 풀 수 없습니다: {0}": "圧縮ファイルを展開できません: {0}", "압축을 풀 수 없습니다: {0}": "圧縮ファイルを展開できません: {0}",
"앞으로": "進む", "앞으로": "進む",
"앞의 rsync 가 끝난 뒤에 하세요.": "前の rsync が終わってから行ってください。", "앞의 rsync 가 끝난 뒤에 하세요.": "前の rsync が終わってから行ってください。",
"앞의 버튼 보기": "前のボタンを表示",
"앞의 복사 · 이동이 끝난 뒤에 하세요.": "前のコピー · 移動が終わってから行ってください。", "앞의 복사 · 이동이 끝난 뒤에 하세요.": "前のコピー · 移動が終わってから行ってください。",
"앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "アプリ一覧・ホーム画面とアプリ内左上のボタンのアイコンです。変更するとホーム画面のアイコンが消えることがあり、アプリ一覧から再度ドラッグする必要がある場合があります", "앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "アプリ一覧・ホーム画面とアプリ内左上のボタンのアイコンです。変更するとホーム画面のアイコンが消えることがあり、アプリ一覧から再度ドラッグする必要がある場合があります",
"앱 목록이 열렸다면, 목록에서 JJ_MKVMaker 를 찾아 눌러 \"모든 파일에 대한 접근\" 을 켜고 돌아오세요.": "アプリ一覧が開いた場合は、一覧から JJ_MKVMaker を探してタップし、「すべてのファイルへのアクセス」をオンにして戻ってください。", "앱 목록이 열렸다면, 목록에서 JJ_MKVMaker 를 찾아 눌러 \"모든 파일에 대한 접근\" 을 켜고 돌아오세요.": "アプリ一覧が開いた場合は、一覧から JJ_MKVMaker を探してタップし、「すべてのファイルへのアクセス」をオンにして戻ってください。",
@ -1819,10 +1823,12 @@
"자세히": "詳細", "자세히": "詳細",
"자세히 닫기": "詳細を閉じる", "자세히 닫기": "詳細を閉じる",
"자체 서명 인증서를 쓰는 집 NAS 등 (믿을 수 있는 서버에서만)": "自己署名証明書を使う自宅 NAS など (信頼できるサーバーのみ)", "자체 서명 인증서를 쓰는 집 NAS 등 (믿을 수 있는 서버에서만)": "自己署名証明書を使う自宅 NAS など (信頼できるサーバーのみ)",
"작게": "小さく",
"작업": "作業", "작업": "作業",
"작업 기록 · 받다 만 파일": "作業記録・ダウンロード途中のファイル", "작업 기록 · 받다 만 파일": "作業記録・ダウンロード途中のファイル",
"작업 기록 보기": "作業記録を表示", "작업 기록 보기": "作業記録を表示",
"작업 기록 숨기기": "作業記録を隠す", "작업 기록 숨기기": "作業記録を隠す",
"작업 기록이 아직 없습니다. MKV 만들기 · 자막 · 다운로드를 하면 여기에 차례로 남습니다.": "作業記録はまだありません。MKV 作成・字幕・ダウンロードを行うと、ここに順に記録されます。",
"작업 임시 파일": "作業の一時ファイル", "작업 임시 파일": "作業の一時ファイル",
"작업 중": "作業中", "작업 중": "作業中",
"작업 중지: {0}{1}": "作業中止: {0}{1}", "작업 중지: {0}{1}": "作業中止: {0}{1}",
@ -1917,6 +1923,7 @@
"정해 둠. 저장된 비밀번호 · 설정 · 비밀번호가 필요한 기능을 쓸 때 묻습니다 (앱을 끌 때까지 한 번)": "設定済み。保存したパスワード・設定・パスワードが必要な機能を使うときに尋ねます (アプリを終了するまで 1 回)", "정해 둠. 저장된 비밀번호 · 설정 · 비밀번호가 필요한 기능을 쓸 때 묻습니다 (앱을 끌 때까지 한 번)": "設定済み。保存したパスワード・設定・パスワードが必要な機能を使うときに尋ねます (アプリを終了するまで 1 回)",
"제목": "タイトル", "제목": "タイトル",
"조정 전": "調整前", "조정 전": "調整前",
"좁은 화면에서 동영상 목록이 차지하는 높이 (자동: 목록은 줄 수만큼, 자세히 칸 자리는 남김)": "狭い画面で動画リストが占める高さ (自動: 行の数だけ、詳細欄の場所は残す)",
"종료": "終了", "종료": "終了",
"종료 (창 닫기 ✕ · 종료 버튼) 를 누르면": "終了(ウィンドウを閉じる ✕・終了ボタン)を押したとき", "종료 (창 닫기 ✕ · 종료 버튼) 를 누르면": "終了(ウィンドウを閉じる ✕・終了ボタン)を押したとき",
"종료 시작 {0}": "終了開始 {0}", "종료 시작 {0}": "終了開始 {0}",
@ -2111,6 +2118,7 @@
"쿠키 · 방문 기록 지우기": "Cookie・閲覧履歴を消去", "쿠키 · 방문 기록 지우기": "Cookie・閲覧履歴を消去",
"쿠키 · 방문 기록을 지웠습니다.": "Cookie・閲覧履歴を消去しました。", "쿠키 · 방문 기록을 지웠습니다.": "Cookie・閲覧履歴を消去しました。",
"쿠키 파일 지우기": "Cookie ファイルを削除", "쿠키 파일 지우기": "Cookie ファイルを削除",
"크게": "大きく",
"크기": "サイズ", "크기": "サイズ",
"크기 · 보정을 원래대로 돌려 코덱도 \"원본 유지\" 로 되돌렸습니다.": "サイズ・補正を元に戻したので、コーデックも「元のまま」に戻しました。", "크기 · 보정을 원래대로 돌려 코덱도 \"원본 유지\" 로 되돌렸습니다.": "サイズ・補正を元に戻したので、コーデックも「元のまま」に戻しました。",
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "サイズを変えると再エンコードが必要なため、コーデックを「元のまま」から H.264 に変更しました。", "크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "サイズを変えると再エンコードが必要なため、コーデックを「元のまま」から H.264 に変更しました。",
@ -2182,6 +2190,7 @@
"포르투갈어": "ポルトガル語", "포르투갈어": "ポルトガル語",
"포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)": "含まれるコンポーネントとライセンス (GPL・LGPL 全文を含む)", "포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)": "含まれるコンポーネントとライセンス (GPL・LGPL 全文を含む)",
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "含まれるコンポーネントとライセンス(THIRD_PARTY_NOTICES.txt)", "포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "含まれるコンポーネントとライセンス(THIRD_PARTY_NOTICES.txt)",
"폰 세로 목록 크기": "縦向きスマホのリストの大きさ",
"폴더": "フォルダー", "폴더": "フォルダー",
"폴더 + 파일 목록": "フォルダー + ファイル一覧", "폴더 + 파일 목록": "フォルダー + ファイル一覧",
"폴더 {0}개 (크기 빼고)": "フォルダー {0} 件 (サイズ除く)", "폴더 {0}개 (크기 빼고)": "フォルダー {0} 件 (サイズ除く)",

View File

@ -604,6 +604,7 @@
"고르지 않음": "未选择", "고르지 않음": "未选择",
"고른 것 시작": "开始所选", "고른 것 시작": "开始所选",
"고른 것 풀기": "解压所选", "고른 것 풀기": "解压所选",
"고른 동영상이 없습니다": "未选择视频",
"고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "不超过所选尺寸的最佳画质(视频没有该尺寸时取最接近的)", "고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "不超过所选尺寸的最佳画质(视频没有该尺寸时取最接近的)",
"고지 문서": "声明文件", "고지 문서": "声明文件",
"고치기": "编辑", "고치기": "编辑",
@ -1019,6 +1020,7 @@
"목록": "列表", "목록": "列表",
"목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다": "隐藏列表 · 视频旁 · 视频上方出现的广告区域", "목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다": "隐藏列表 · 视频旁 · 视频上方出现的广告区域",
"목록 보기": "查看列表", "목록 보기": "查看列表",
"목록에서 동영상을 누르면 여기에 자세히 보입니다": "点按列表中的视频即可在此查看详情",
"목록에서 빼기": "从列表中移除", "목록에서 빼기": "从列表中移除",
"목록에서 빼기 (파일은 그대로)": "从列表中移除 (保留文件)", "목록에서 빼기 (파일은 그대로)": "从列表中移除 (保留文件)",
"목록에서 제거": "从列表中移除", "목록에서 제거": "从列表中移除",
@ -1113,6 +1115,7 @@
"버전 목록을 읽을 수 없습니다 ({0})": "无法读取版本列表 ({0})", "버전 목록을 읽을 수 없습니다 ({0})": "无法读取版本列表 ({0})",
"버전 목록을 읽을 수 없습니다: {0}": "无法读取版本列表: {0}", "버전 목록을 읽을 수 없습니다: {0}": "无法读取版本列表: {0}",
"버튼 구성": "按钮配置", "버튼 구성": "按钮配置",
"버튼 더 보기": "更多按钮",
"번역 모델 내려받는 중 {0}%": "正在下载翻译模型 {0}%", "번역 모델 내려받는 중 {0}%": "正在下载翻译模型 {0}%",
"번역 모델 내려받는 중: {0}": "正在下载翻译模型: {0}", "번역 모델 내려받는 중: {0}": "正在下载翻译模型: {0}",
"번역 모델 라이선스: CC BY-NC 4.0 (비상업적 이용만 허용)": "翻译模型许可证:CC BY-NC 4.0(仅限非商业用途)", "번역 모델 라이선스: CC BY-NC 4.0 (비상업적 이용만 허용)": "翻译模型许可证:CC BY-NC 4.0(仅限非商业用途)",
@ -1411,6 +1414,7 @@
"압축을 풀 수 없습니다: {0}": "无法解压: {0}", "압축을 풀 수 없습니다: {0}": "无法解压: {0}",
"앞으로": "前进", "앞으로": "前进",
"앞의 rsync 가 끝난 뒤에 하세요.": "请等上一个 rsync 结束后再运行。", "앞의 rsync 가 끝난 뒤에 하세요.": "请等上一个 rsync 结束后再运行。",
"앞의 버튼 보기": "前面的按钮",
"앞의 복사 · 이동이 끝난 뒤에 하세요.": "请等待前一个复制 · 移动完成。", "앞의 복사 · 이동이 끝난 뒤에 하세요.": "请等待前一个复制 · 移动完成。",
"앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "应用列表、主屏幕以及应用内左上角按钮的图标。更改后主屏幕上的图标可能会消失,可能需要从应用列表重新拖放", "앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "应用列表、主屏幕以及应用内左上角按钮的图标。更改后主屏幕上的图标可能会消失,可能需要从应用列表重新拖放",
"앱 목록이 열렸다면, 목록에서 JJ_MKVMaker 를 찾아 눌러 \"모든 파일에 대한 접근\" 을 켜고 돌아오세요.": "如果打开的是应用列表,请在列表中找到并点按 JJ_MKVMaker,开启“所有文件访问权限”后返回。", "앱 목록이 열렸다면, 목록에서 JJ_MKVMaker 를 찾아 눌러 \"모든 파일에 대한 접근\" 을 켜고 돌아오세요.": "如果打开的是应用列表,请在列表中找到并点按 JJ_MKVMaker,开启“所有文件访问权限”后返回。",
@ -1819,10 +1823,12 @@
"자세히": "详情", "자세히": "详情",
"자세히 닫기": "收起详情", "자세히 닫기": "收起详情",
"자체 서명 인증서를 쓰는 집 NAS 등 (믿을 수 있는 서버에서만)": "用于使用自签名证书的家用 NAS 等 (仅限可信服务器)", "자체 서명 인증서를 쓰는 집 NAS 등 (믿을 수 있는 서버에서만)": "用于使用自签名证书的家用 NAS 等 (仅限可信服务器)",
"작게": "小",
"작업": "任务", "작업": "任务",
"작업 기록 · 받다 만 파일": "操作记录 · 未下载完的文件", "작업 기록 · 받다 만 파일": "操作记录 · 未下载完的文件",
"작업 기록 보기": "显示任务记录", "작업 기록 보기": "显示任务记录",
"작업 기록 숨기기": "隐藏任务记录", "작업 기록 숨기기": "隐藏任务记录",
"작업 기록이 아직 없습니다. MKV 만들기 · 자막 · 다운로드를 하면 여기에 차례로 남습니다.": "暂无工作记录。生成 MKV、字幕和下载后会按顺序记录在这里。",
"작업 임시 파일": "任务临时文件", "작업 임시 파일": "任务临时文件",
"작업 중": "处理中", "작업 중": "处理中",
"작업 중지: {0}{1}": "任务已停止: {0}{1}", "작업 중지: {0}{1}": "任务已停止: {0}{1}",
@ -1917,6 +1923,7 @@
"정해 둠. 저장된 비밀번호 · 설정 · 비밀번호가 필요한 기능을 쓸 때 묻습니다 (앱을 끌 때까지 한 번)": "已设置。使用已保存的密码 · 设置 · 需要密码的功能时询问 (在关闭应用前只问一次)", "정해 둠. 저장된 비밀번호 · 설정 · 비밀번호가 필요한 기능을 쓸 때 묻습니다 (앱을 끌 때까지 한 번)": "已设置。使用已保存的密码 · 设置 · 需要密码的功能时询问 (在关闭应用前只问一次)",
"제목": "标题", "제목": "标题",
"조정 전": "调整前", "조정 전": "调整前",
"좁은 화면에서 동영상 목록이 차지하는 높이 (자동: 목록은 줄 수만큼, 자세히 칸 자리는 남김)": "窄屏时视频列表所占高度 (自动: 按行数,并为详情区留出空间)",
"종료": "退出", "종료": "退出",
"종료 (창 닫기 ✕ · 종료 버튼) 를 누르면": "点击退出(关闭窗口 ✕ · 退出按钮)时", "종료 (창 닫기 ✕ · 종료 버튼) 를 누르면": "点击退出(关闭窗口 ✕ · 退出按钮)时",
"종료 시작 {0}": "开始退出 {0}", "종료 시작 {0}": "开始退出 {0}",
@ -2111,6 +2118,7 @@
"쿠키 · 방문 기록 지우기": "清除 Cookie · 浏览记录", "쿠키 · 방문 기록 지우기": "清除 Cookie · 浏览记录",
"쿠키 · 방문 기록을 지웠습니다.": "已清除 Cookie · 浏览记录。", "쿠키 · 방문 기록을 지웠습니다.": "已清除 Cookie · 浏览记录。",
"쿠키 파일 지우기": "删除 Cookie 文件", "쿠키 파일 지우기": "删除 Cookie 文件",
"크게": "大",
"크기": "大小", "크기": "大小",
"크기 · 보정을 원래대로 돌려 코덱도 \"원본 유지\" 로 되돌렸습니다.": "大小 · 调整已恢复原样,编解码器也恢复为“保持原样”。", "크기 · 보정을 원래대로 돌려 코덱도 \"원본 유지\" 로 되돌렸습니다.": "大小 · 调整已恢复原样,编解码器也恢复为“保持原样”。",
"크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "更改大小需要重新编码,因此编解码器已从“保持原样”改为 H.264。", "크기를 바꾸면 다시 인코딩해야 해서 코덱을 \"원본 유지\" 에서 H.264 로 바꿨습니다.": "更改大小需要重新编码,因此编解码器已从“保持原样”改为 H.264。",
@ -2182,6 +2190,7 @@
"포르투갈어": "葡萄牙语", "포르투갈어": "葡萄牙语",
"포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)": "包含的组件和许可证 (含 GPL · LGPL 全文)", "포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)": "包含的组件和许可证 (含 GPL · LGPL 全文)",
"포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "包含的组件与许可(THIRD_PARTY_NOTICES.txt)", "포함된 구성 요소와 라이선스 (THIRD_PARTY_NOTICES.txt)": "包含的组件与许可(THIRD_PARTY_NOTICES.txt)",
"폰 세로 목록 크기": "竖屏手机列表大小",
"폴더": "文件夹", "폴더": "文件夹",
"폴더 + 파일 목록": "文件夹 + 文件列表", "폴더 + 파일 목록": "文件夹 + 文件列表",
"폴더 {0}개 (크기 빼고)": "{0} 个文件夹(不计大小)", "폴더 {0}개 (크기 빼고)": "{0} 个文件夹(不计大小)",

View File

@ -314,6 +314,9 @@ class AppSettings {
/// 메인 화면 아래 작업 기록 창 보이기 /// 메인 화면 아래 작업 기록 창 보이기
bool showLog = true; bool showLog = true;
/// 폰 세로 (좁은 화면) MKV 목록 크기: 'auto' 목록은 내용만큼 · 자세히 칸에 최소 높이 (처음 값) · 'small' · 'large' (관리자 판단 10/17)
String mkvCompactList = 'auto';
/// 처음 화면: 'home' = 지금처럼 MKV 화면 (기본), 'browser' = 웹 브라우저 ([homeUrl]), 'files' = 파일 탐색기 /// 처음 화면: 'home' = 지금처럼 MKV 화면 (기본), 'browser' = 웹 브라우저 ([homeUrl]), 'files' = 파일 탐색기
String startScreen = 'home'; String startScreen = 'home';
@ -690,6 +693,7 @@ class AppSettings {
'maxParallelDownloads': maxParallelDownloads, 'maxParallelDownloads': maxParallelDownloads,
'maxParallelJobs': maxParallelJobs, 'maxParallelJobs': maxParallelJobs,
'showLog': showLog, 'showLog': showLog,
'mkvCompactList': mkvCompactList,
'startScreen': startScreen, 'startScreen': startScreen,
'uiScale': uiScale, 'uiScale': uiScale,
'uiScaleDefault': uiScaleDefault, 'uiScaleDefault': uiScaleDefault,
@ -877,6 +881,7 @@ class AppSettings {
..maxParallelDownloads = (j['maxParallelDownloads'] as num?)?.toInt() ?? 3 ..maxParallelDownloads = (j['maxParallelDownloads'] as num?)?.toInt() ?? 3
..maxParallelJobs = (j['maxParallelJobs'] as num?)?.toInt() ?? 5 ..maxParallelJobs = (j['maxParallelJobs'] as num?)?.toInt() ?? 5
..showLog = j['showLog'] as bool? ?? true ..showLog = j['showLog'] as bool? ?? true
..mkvCompactList = const ['auto', 'small', 'large'].contains(j['mkvCompactList']) ? j['mkvCompactList'] as String : 'auto'
..startScreen = j['startScreen'] as String? ?? 'home' ..startScreen = j['startScreen'] as String? ?? 'home'
..uiScale = clampUiScale(j['uiScale'] as num?) ..uiScale = clampUiScale(j['uiScale'] as num?)
..uiScaleDefault = clampUiScale(j['uiScaleDefault'] as num?) ..uiScaleDefault = clampUiScale(j['uiScaleDefault'] as num?)

View File

@ -15,6 +15,7 @@ import '../core/models.dart';
import '../core/output_paths.dart'; import '../core/output_paths.dart';
import 'ai_dialog.dart'; import 'ai_dialog.dart';
import 'app_actions.dart'; import 'app_actions.dart';
import 'scroll_more.dart';
import 'downloads_page.dart'; import 'downloads_page.dart';
import 'existing_output_dialog.dart'; import 'existing_output_dialog.dart';
import 'exit_guard.dart'; import 'exit_guard.dart';
@ -71,20 +72,7 @@ class HomePage extends StatelessWidget {
// 8: 좁은 화면에 동영상이 없으면 빈 목록 · 안내로 나누지 않고 한 화면에 안내 + [동영상 추가] // 8: 좁은 화면에 동영상이 없으면 빈 목록 · 안내로 나누지 않고 한 화면에 안내 + [동영상 추가]
? _EmptyHint(onAdd: c.pickVideos) ? _EmptyHint(onAdd: c.pickVideos)
: isCompact(context) : isCompact(context)
? Column( ? _CompactBody(c: c)
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: (MediaQuery.sizeOf(context).height * 0.32).clamp(160.0, 420.0),
child: _VideoList(c: c)),
const Divider(height: 1),
Expanded(
child: c.selected == null
? _EmptyHint(onAdd: c.pickVideos)
: _VideoDetail(c: c, v: c.selected!),
),
],
)
: Row( : Row(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@ -94,9 +82,7 @@ class HomePage extends StatelessWidget {
child: _VideoList(c: c)), child: _VideoList(c: c)),
const VerticalDivider(width: 1), const VerticalDivider(width: 1),
Expanded( Expanded(
child: c.selected == null child: c.selected == null ? const _PickHint() : _VideoDetail(c: c, v: c.selected!),
? _EmptyHint(onAdd: c.pickVideos)
: _VideoDetail(c: c, v: c.selected!),
), ),
], ],
), ),
@ -370,8 +356,9 @@ class _TopBar extends StatelessWidget {
return AppTopBar( return AppTopBar(
nav: const AppNavButtons(onMkvPage: true), nav: const AppNavButtons(onMkvPage: true),
actions: AppActions(c: c, onExit: onExit, showUsage: false), actions: AppActions(c: c, onExit: onExit, showUsage: false),
middle: SingleChildScrollView( // 15 지적: 폭 412 에서 세 번째 버튼이 잘렸는데 더 있다는 표시가 없었다 - 끝에 흐림 · [›]
scrollDirection: Axis.horizontal, middle: ScrollMore(
background: JjColors.panel,
child: Row(children: [ child: Row(children: [
Tooltip( Tooltip(
message: trf('MKV 만들기{0}', [count]), message: trf('MKV 만들기{0}', [count]),
@ -691,7 +678,10 @@ class _EncodeBar extends StatelessWidget {
class _VideoList extends StatelessWidget { class _VideoList extends StatelessWidget {
final AppController c; final AppController c;
const _VideoList({required this.c});
/// 좁은 화면 '자동': 줄이 적으면 내용 높이만 (빈 칸을 남기지 않게)
final bool shrink;
const _VideoList({required this.c, this.shrink = false});
/// 정렬 아이콘: 지금 적용된 정렬은 강조색 + 방향 화살표 /// 정렬 아이콘: 지금 적용된 정렬은 강조색 + 방향 화살표
Widget _sortButton(String by, IconData icon, String name) { Widget _sortButton(String by, IconData icon, String name) {
@ -715,6 +705,7 @@ class _VideoList extends StatelessWidget {
color: JjColors.panel, color: JjColors.panel,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: shrink ? MainAxisSize.min : MainAxisSize.max,
children: [ children: [
// 전체 선택 · 선택 개수 · 선택 제거 · 모두 지우기 // 전체 선택 · 선택 개수 · 선택 제거 · 모두 지우기
Padding( Padding(
@ -775,8 +766,10 @@ class _VideoList extends StatelessWidget {
), ),
]), ]),
), ),
Expanded( Flexible(
fit: shrink ? FlexFit.loose : FlexFit.tight,
child: ListView.builder( child: ListView.builder(
shrinkWrap: shrink,
itemCount: c.videos.length, itemCount: c.videos.length,
itemBuilder: (_, i) => _VideoTile(c: c, v: c.videos[i]), itemBuilder: (_, i) => _VideoTile(c: c, v: c.videos[i]),
), ),
@ -985,6 +978,61 @@ class _StatusIcon extends StatelessWidget {
// ───────── 오른쪽: 선택한 동영상 ───────── // ───────── 오른쪽: 선택한 동영상 ─────────
/// 좁은 화면 (폰 세로): 목록 위 · 자세히 아래.
/// 15 지적 · 관리자 판단 (10/17): 목록을 높이 32% 로 고정하면 줄이 적을 때 빈 칸이 남고, 많을 때 4~5줄만 보였다.
/// '자동' (처음 값) 은 목록이 내용만큼 차지하되 자세히 칸의 최소 높이를 남기고, 남는 공간은 목록에.
/// 환경 설정 "폰 세로 목록 크기" 로 작게 (25%) · 크게 (55%) 를 고를 수 있다.
class _CompactBody extends StatelessWidget {
final AppController c;
const _CompactBody({required this.c});
/// 줄 하나의 가장 작은 높이 (이보다 줄이 많으면 내용이 칸보다 길다고 보고 줄여 그리지 않는다)
static const _rowMin = 48.0;
@override
Widget build(BuildContext context) => LayoutBuilder(builder: (context, box) {
final h = box.maxHeight;
final picked = c.selected != null;
final cap = switch (c.settings.mkvCompactList) {
'small' => h * 0.25,
'large' => h * 0.55,
// 자동: 자세히 칸 최소 - 고른 동영상이 있으면 62% (세부 정보 · 자막 버튼이 칸 밖으로 밀리지 않게, 261 · 266),
// 없으면 안내 한 덩이만큼
_ => h - (picked ? h * 0.62 : math.min(120.0, h * 0.4)),
}.clamp(math.min(120.0, h), h).toDouble();
// 줄이 많아 내용이 칸보다 확실히 길면 칸 높이 그대로 (shrinkWrap 은 줄을 모두 그려 느리다)
final shrink = c.settings.mkvCompactList == 'auto' && c.videos.length * _rowMin < cap;
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
if (shrink)
ConstrainedBox(constraints: BoxConstraints(maxHeight: cap), child: _VideoList(c: c, shrink: true))
else
SizedBox(height: cap, child: _VideoList(c: c)),
const Divider(height: 1),
Expanded(child: picked ? _VideoDetail(c: c, v: c.selected!) : const _PickHint()),
]);
});
}
/// 목록에 동영상이 있는데 고른 것이 없을 때 (15 지적: 빈 목록 안내 "동영상 추가로 파일을 고르세요" 가 나와 헷갈렸다)
class _PickHint extends StatelessWidget {
const _PickHint();
@override
Widget build(BuildContext context) => Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.touch_app_outlined, size: 40, color: JjColors.textDim),
const SizedBox(height: 8),
Text(tr('고른 동영상이 없습니다'), textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim)),
const SizedBox(height: 4),
Text(tr('목록에서 동영상을 누르면 여기에 자세히 보입니다'),
textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
]),
),
);
}
class _EmptyHint extends StatelessWidget { class _EmptyHint extends StatelessWidget {
final VoidCallback? onAdd; final VoidCallback? onAdd;
const _EmptyHint({this.onAdd}); const _EmptyHint({this.onAdd});
@ -1366,6 +1414,15 @@ class _LogPanel extends StatelessWidget {
Widget build(BuildContext context) => Container( Widget build(BuildContext context) => Container(
color: JjColors.bg, color: JjColors.bg,
child: Stack(children: [ child: Stack(children: [
// 15 지적: 기록이 없으면 ✕ 하나만 있는 빈 칸으로 보였다 - 무엇이 들어오는 칸인지 적는다
if (logs.isEmpty)
Positioned.fill(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 44, 8),
child: Text(tr('작업 기록이 아직 없습니다. MKV 만들기 · 자막 · 다운로드를 하면 여기에 차례로 남습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
),
),
Positioned.fill( Positioned.fill(
child: ListView.builder( child: ListView.builder(
reverse: true, reverse: true,

100
lib/ui/scroll_more.dart Normal file
View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import '../l10n/tr.dart';
import 'theme.dart';
/// 옆으로 미는 줄 (폰 세로의 버튼 줄 등) 에 "더 있음" 표시.
/// 줄이 화면보다 길면 끝 쪽에 흐려지는 덮개와 [›] (왼쪽으로 밀었으면 [‹]) 를 보이고, 누르면 그쪽으로 한 화면쯤 민다.
/// (MKV 화면 폭 412: 세 번째 버튼이 화면 밖에서 잘렸는데 더 있다는 표시가 없었다 - 15 지적)
class ScrollMore extends StatefulWidget {
final Widget child;
/// 덮개 색 (줄 뒤 바탕과 같게)
final Color? background;
const ScrollMore({super.key, required this.child, this.background});
@override
State<ScrollMore> createState() => _ScrollMoreState();
}
class _ScrollMoreState extends State<ScrollMore> {
final _scroll = ScrollController();
bool _left = false, _right = false;
@override
void initState() {
super.initState();
_scroll.addListener(_update);
WidgetsBinding.instance.addPostFrameCallback((_) => _update());
}
@override
void dispose() {
_scroll.dispose();
super.dispose();
}
void _update() {
if (!mounted || !_scroll.hasClients) return;
final p = _scroll.position;
final left = p.pixels > 1;
final right = p.pixels < p.maxScrollExtent - 1;
if (left != _left || right != _right) {
setState(() {
_left = left;
_right = right;
});
}
}
void _jump(int dir) {
final p = _scroll.position;
final to = (p.pixels + dir * p.viewportDimension * 0.7).clamp(0.0, p.maxScrollExtent);
_scroll.animateTo(to, duration: const Duration(milliseconds: 220), curve: Curves.easeOut);
}
Widget _edge(bool right) {
final bg = widget.background ?? Theme.of(context).scaffoldBackgroundColor;
return Positioned(
top: 0,
bottom: 0,
left: right ? null : 0,
right: right ? 0 : null,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: right ? Alignment.centerLeft : Alignment.centerRight,
end: right ? Alignment.centerRight : Alignment.centerLeft,
colors: [bg.withValues(alpha: 0), bg.withValues(alpha: 0.95), bg],
stops: const [0, 0.45, 1],
),
),
child: Padding(
padding: EdgeInsets.only(left: right ? 18 : 0, right: right ? 0 : 18),
child: Center(
child: IconButton(
tooltip: right ? tr('버튼 더 보기') : tr('앞의 버튼 보기'),
visualDensity: VisualDensity.compact,
icon: Icon(right ? Icons.chevron_right : Icons.chevron_left, color: JjColors.accent),
onPressed: () => _jump(right ? 1 : -1),
),
),
),
),
);
}
@override
Widget build(BuildContext context) => NotificationListener<ScrollMetricsNotification>(
// 줄 안의 버튼 글이 바뀌어 길이가 달라져도 다시 본다
onNotification: (_) {
WidgetsBinding.instance.addPostFrameCallback((_) => _update());
return false;
},
child: Stack(children: [
SingleChildScrollView(controller: _scroll, scrollDirection: Axis.horizontal, child: widget.child),
if (_left) _edge(false),
if (_right) _edge(true),
]),
);
}

View File

@ -486,6 +486,20 @@ class _SettingsPageState extends State<SettingsPage> {
onChanged: (v) => c.updateSettings((x) => x.mkvExisting = v ?? 'ask'), onChanged: (v) => c.updateSettings((x) => x.mkvExisting = v ?? 'ask'),
), ),
), ),
// 15 지적 · 관리자 판단 (10/17): 폰 세로에서 목록 · 자세히 칸 나누기
SettingTile(
title: Text(tr('폰 세로 목록 크기')),
subtitle: Text(tr('좁은 화면에서 동영상 목록이 차지하는 높이 (자동: 목록은 줄 수만큼, 자세히 칸 자리는 남김)')),
trailing: DropdownButton<String>(
value: s.mkvCompactList,
items: [
DropdownMenuItem(value: 'auto', child: Text(tr('자동 (기본)'))),
DropdownMenuItem(value: 'small', child: Text(tr('작게'))),
DropdownMenuItem(value: 'large', child: Text(tr('크게'))),
],
onChanged: (v) => c.updateSettings((x) => x.mkvCompactList = v ?? 'auto'),
),
),
SettingTile( SettingTile(
title: Text(tr('동시 MKV 변환 수')), title: Text(tr('동시 MKV 변환 수')),
subtitle: Text(tr('여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)')), subtitle: Text(tr('여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)')),

View File

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/core/models.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/home_page.dart';
/// 15 지적 (10/17, 시험 캡처 412 · 960) · 관리자 판단:
/// ① 동영상이 있는데 고른 것이 없으면 빈 목록 안내가 아니라 "목록에서 누르세요"
/// ② 폭 412 위 버튼 줄이 잘리면 "더 있음" 표시 ([›])
/// ③ 폰 세로 목록은 32% 고정이 아니라 내용만큼 (자세히 칸 자리는 남김) · 설정 "폰 세로 목록 크기"
void main() {
AppController make(int n, {String size = 'auto', bool pick = false}) {
final c = AppController(PlatformServices(
mediaTool: ProcessMediaTool('x', 'y'),
storage: DesktopStorageService(),
createRecognizer: () => throw UnimplementedError(),
createTranslator: () => throw UnimplementedError(),
));
c.settings
..showLog = true
..mkvCompactList = size;
for (var i = 0; i < n; i++) {
c.videos.add(VideoItem('D:/v/v$i.mp4'));
}
if (pick) c.selected = c.videos.first;
return c;
}
Future<void> show(WidgetTester tester, AppController c, Size size) async {
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(MaterialApp(home: HomePage(c: c)));
await tester.pump();
expect(tester.takeException(), isNull);
}
/// 목록 (v0.mp4 가 있는 스크롤) 의 높이
double listHeight(WidgetTester tester) =>
tester.getRect(find.ancestor(of: find.text('v0.mp4'), matching: find.byType(Scrollable)).first).height;
for (final size in const [Size(412, 869), Size(1280, 800)]) {
testWidgets('① ${size.width.toInt()}: 동영상이 있고 고른 것이 없으면 "목록에서 누르세요" (빈 목록 안내 아님)', (tester) async {
await show(tester, make(50), size);
expect(find.text('고른 동영상이 없습니다'), findsOneWidget);
expect(find.text('"동영상 추가" 로 파일을 선택하세요'), findsNothing);
});
}
testWidgets('빈 목록이면 예전처럼 [동영상 추가] 안내', (tester) async {
await show(tester, make(0), const Size(412, 869));
expect(find.text('"동영상 추가" 로 파일을 선택하세요'), findsOneWidget);
expect(find.text('고른 동영상이 없습니다'), findsNothing);
});
testWidgets('③ 자동 · 50개 · 고른 것 없음: 목록이 화면 대부분 (예전 32% 는 4~5줄)', (tester) async {
await show(tester, make(50), const Size(412, 869));
// 예전: 목록 칸이 화면 높이의 32% (869 → 278, 머리 줄을 빼면 스크롤 약 234).
// 이제는 위 막대 · 인코딩 줄 · 작업 기록을 뺀 칸에서 안내 자리 (120) 만 남긴다
expect(listHeight(tester), greaterThan(869 * 0.32 - 44 + 60));
expect(find.text('v4.mp4'), findsOneWidget);
expect(find.text('고른 동영상이 없습니다'), findsOneWidget, reason: '안내 자리는 남는다');
});
testWidgets('③ 자동 · 3개: 목록은 줄 수만큼 - 안내가 목록 바로 아래 칸에 (빈 칸 없음)', (tester) async {
await show(tester, make(3), const Size(412, 869));
final list = listHeight(tester);
final rows = tester.getRect(find.text('v2.mp4')).bottom - tester.getRect(find.text('v0.mp4')).top;
expect(list, lessThan(rows + 120), reason: '목록 $list · 줄 $rows - 남는 칸을 목록이 차지하지 않는다');
});
testWidgets('③ 설정: 작게 < 크게 < 자동 (50개 · 고른 것 없음)', (tester) async {
final h = <String, double>{};
for (final s in const ['small', 'large', 'auto']) {
await show(tester, make(50, size: s), const Size(412, 869));
h[s] = listHeight(tester);
}
expect(h['small']!, lessThan(h['large']!));
expect(h['large']!, lessThan(h['auto']!));
});
testWidgets('③ 자동 · 고른 동영상: 자세히 칸이 화면 높이의 절반 넘게 (261 · 266 의 버튼이 밀리지 않게)', (tester) async {
await show(tester, make(50, pick: true), const Size(412, 869));
expect(listHeight(tester), lessThan(869 * 0.4));
});
testWidgets('② 폭 412: 위 버튼 줄이 넘치면 [›] - 누르면 밀리고 [‹] 가 생긴다', (tester) async {
await show(tester, make(5, pick: true), const Size(412, 869));
expect(find.byTooltip('버튼 더 보기'), findsOneWidget);
expect(find.byTooltip('앞의 버튼 보기'), findsNothing);
await tester.tap(find.byTooltip('버튼 더 보기'));
await tester.pumpAndSettle();
expect(find.byTooltip('앞의 버튼 보기'), findsOneWidget);
});
testWidgets('작업 기록이 비었으면 무엇이 들어오는 칸인지 적는다', (tester) async {
final c = make(3);
c.logs.clear();
await show(tester, c, const Size(412, 869));
expect(find.textContaining('작업 기록이 아직 없습니다'), findsOneWidget);
});
}