MKV 화면 · 맨 위 줄 · 브라우저 주소 · 재생 버튼 · 브라우저 소리 설정 · 정리 창 체크

근거:
- 감독 세션 지시 (폰 · S10 Ultra 를 사용자처럼 써 본 결과) 2 · 3 · 4 · 7 · 8 · 9 · 13 · 15번
- 사용자 결정 (10/8): 브라우저 소리 멈춤을 설정으로 (기본 켜짐), 정리 창은 임시 · 받다 만 다운로드만 미리 체크

- 2: 좁은 화면 MKV 화면 둘째 줄은 글자를 숨기지 않고 옆으로 밀어 봄, [MKV 만들기] 맨 앞.
     변환 설정 줄은 줄바꿈 ([화면 · 색 보정] 이 세로에서 안 보이던 것).
- 3: 좁은 화면 맨 위 줄: 이동 아이콘 아래 짧은 이름 (홈 · MKV · 브라우저 · 탐색기 · Rsync · 다운로드 · 뒤로),
     [환경 설정] 옆에는 ⋮ 메뉴 (화면 크게 · 작게 · 기본 크기 · 종료) - 종료가 설정 바로 옆이라 잘못 누르던 것.
- 4 · 15: 브라우저 주소 칸이 좁으면 (폰 세로 · 태블릿 세로 · 폰 가로) 홈 · 번역 · 즐겨찾기 · 작업 현황 · 외부 브라우저를
     ⋮ 메뉴로 (없애지 않음). 주소가 "http" 만 보이거나 사라지던 것.
- 7: Android 에서 "탐색기에서 끌어다 놓아도" 안내 뺌. 켤 때 "⚠ 사용할 수 없는 코덱" 기록 뺌 (코덱 목록에 "(사용 불가)" 로 보임).
- 8: 좁은 화면에 동영상이 없으면 한 화면에 안내 + [동영상 추가].
- 9: Android 재생 화면 버튼 크게, 재생 버튼 줄과 나머지 줄을 나눔.
- 13: 넓은 화면의 동영상 목록 칸을 화면 너비의 40% (320~640) 로 - 태블릿 세로에서 제목이 앞부분만 보이던 것.
- 사용자 결정: 환경 설정 > 웹 브라우저 "다른 화면으로 가면 페이지 소리 멈춤" (기본 켜짐).
- 사용자 결정: 저장 공간 정리 창은 작업 임시 파일 · 받다 만 다운로드만 미리 체크.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음3
RyuJongJin 2026-10-08 23:36:21 +09:00
parent fead23ce2f
commit 951a1c927c
12 changed files with 320 additions and 54 deletions

View File

@ -276,6 +276,7 @@
"검색 지우기": "Clear search",
"검정": "Black",
"결과 폴더": "Output folder",
"결과 폴더 열기": "Open output folder",
"결과 폴더 열기 (동영상을 고르세요)": "Open output folder (choose a video)",
"결과 폴더 열기: {0}": "Open output folder: {0}",
"결과물 이동 실패: {0} ({1})": "Failed to move result: {0} ({1})",
@ -313,6 +314,7 @@
"기본": "Default",
"기본 앱으로 열기": "Open with default app",
"기본 연결 프로그램": "Default app",
"기본 크기 ({0}%)": "Default size ({0}%)",
"기본 크기 ({0}%) 로 · 기본 크기는 환경 설정 > 화면 에서": "Back to the default size ({0}%) · set the default size in Settings > Display",
"기본 프로그램": "Default program",
"기본 프로그램으로 열기": "Open with the default program",
@ -328,6 +330,7 @@
"끄면 마지막 설정으로 바로 시작: 원어 {0}, 언어 {1}, {2}": "When off, starts right away with the last settings: source {0}, languages {1}, {2}",
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). Chrome 엔진은 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "When off, page scripts do not run (fewer ads · trackers, but script-built sites such as YouTube will not show). With the Chrome engine, page translation · video detection also stop.",
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "When off, page scripts do not run (fewer ads · trackers, but script-built sites such as YouTube will not show). Page translation · video detection also stop.",
"끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다": "When off, the web page keeps playing sound while you look at settings, the download list or other screens",
"끌어다 놓은 항목에 동영상이 없습니다.": "There are no videos in the dropped items.",
"끝": "End",
"끝 (오른쪽 · 아래)": "Edge (right · bottom)",
@ -396,6 +399,7 @@
"다른 창으로 복사": "Copy to other pane",
"다른 창으로 이동": "Move to other pane",
"다른 폴더로 이동": "Move to another folder",
"다른 화면으로 가면 페이지 소리 멈춤": "Pause page sound when leaving the browser",
"다시 만들기 대기로": "Back to waiting for remake",
"다시 설치": "Reinstall",
"다시 세기": "Recount",
@ -609,6 +613,7 @@
"분석 실패: {0} - {1}": "Analysis failed: {0} - {1}",
"붙여넣기": "Paste",
"붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "Nothing to paste. First hold items with [Copy] · [Move].",
"브라우저": "Browser",
"브라우저 엔진 (앱 안)": "Browser engine (in-app)",
"브라우저 쿠키를 읽지 못했습니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 \"앱 안 브라우저\" 를 고르거나, 고른 브라우저가 설치되어 있는지 확인하세요 (Chrome · Edge 는 브라우저를 닫아야 읽힙니다).": "Could not read the browser cookies. In Settings > Downloads > YouTube cookies, choose \"In-app browser\", or check that the chosen browser is installed (Chrome · Edge must be closed to be read).",
"비공개 · 회원 전용 영상입니다.": "This is a private · members-only video.",
@ -896,6 +901,7 @@
"이 파일에 맞는 자막 우선 (영상 해시)": "Prefer subtitles that match this file (video hash)",
"이 파일용": "For this file",
"이 파일을 열 수 있는 앱이 없습니다.": "No app can open this file.",
"이 페이지 번역": "Translate this page",
"이 페이지 지우기": "Delete this page",
"이 페이지를 {0}(으)로 번역": "Translate this page to {0}",
"이 폴더 선택": "Choose this folder",
@ -1085,6 +1091,7 @@
"즐겨찾기": "Bookmarks",
"즐겨찾기 HTML 가져오기": "Import bookmarks HTML",
"즐겨찾기 검색 (이름 · 주소)": "Search bookmarks (name · address)",
"즐겨찾기 관리": "Manage bookmarks",
"즐겨찾기 관리 (Ctrl+Shift+B)": "Manage bookmarks (Ctrl+Shift+B)",
"즐겨찾기 관리자": "Bookmark manager",
"즐겨찾기 관리자 (Ctrl+Shift+O)": "Bookmark manager (Ctrl+Shift+O)",
@ -1098,6 +1105,7 @@
"즐겨찾기 추가됨": "Bookmark added",
"즐겨찾기 표시줄": "Bookmarks bar",
"지금": "Current",
"지금 {0}%": "Now {0}%",
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "The running copy uses the previous options. Stop it and run again with the new options?",
"지금 맞추기": "Sync now",
"지금 맞추는 중입니다. 끝난 뒤에 하세요.": "Syncing right now. Try again when it finishes.",
@ -1176,6 +1184,7 @@
"크메르어": "Khmer",
"타갈로그어": "Tagalog",
"타밀어": "Tamil",
"탐색기": "Explorer",
"탐색기 오른쪽 클릭 메뉴": "Explorer right-click menu",
"탐색기에서 동영상을 열 때": "When opening a video from Explorer",
"탐색기에서 연 동영상을 재생할 창": "Window to play videos opened from Explorer",
@ -1302,6 +1311,7 @@
"화면 작게": "Smaller",
"화면 크게": "Larger",
"화면 크기": "Size",
"화면 크기 · 종료": "Screen size · Exit",
"화면 크기 따라 (기본)": "By screen size (default)",
"화면이 있는 컴포넌트는 하나 이상 남아 있어야 합니다.": "At least one component with a screen must stay installed.",
"화질": "Quality",

View File

@ -276,6 +276,7 @@
"검색 지우기": "検索をクリア",
"검정": "黒",
"결과 폴더": "出力フォルダー",
"결과 폴더 열기": "出力フォルダーを開く",
"결과 폴더 열기 (동영상을 고르세요)": "出力フォルダーを開く(動画を選んでください)",
"결과 폴더 열기: {0}": "出力フォルダーを開く: {0}",
"결과물 이동 실패: {0} ({1})": "成果物の移動失敗: {0}({1})",
@ -313,6 +314,7 @@
"기본": "既定",
"기본 앱으로 열기": "既定のアプリで開く",
"기본 연결 프로그램": "既定のアプリ",
"기본 크기 ({0}%)": "既定のサイズ ({0}%)",
"기본 크기 ({0}%) 로 · 기본 크기는 환경 설정 > 화면 에서": "既定の大きさ({0}%)に戻す・既定の大きさは 環境設定 > 画面 で",
"기본 프로그램": "既定のプログラム",
"기본 프로그램으로 열기": "既定のプログラムで開く",
@ -328,6 +330,7 @@
"끄면 마지막 설정으로 바로 시작: 원어 {0}, 언어 {1}, {2}": "オフにすると前回の設定ですぐ開始: 原語 {0}、言語 {1}、{2}",
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). Chrome 엔진은 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "オフにするとページのスクリプトを実行しません (広告 · 追跡は減りますが、YouTube のようにスクリプトで作られたサイトは表示されません)。Chrome エンジンではページ翻訳 · 動画の検出も止まります。",
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "オフにするとページのスクリプトを実行しません (広告 · 追跡は減りますが、YouTube のようにスクリプトで作られたサイトは表示されません)。ページ翻訳 · 動画の検出も止まります。",
"끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다": "オフにすると、環境設定・ダウンロード一覧・他の画面を見ている間も Web ページの音が流れ続けます",
"끌어다 놓은 항목에 동영상이 없습니다.": "ドロップした項目に動画がありません。",
"끝": "終了",
"끝 (오른쪽 · 아래)": "端 (右 · 下)",
@ -396,6 +399,7 @@
"다른 창으로 복사": "もう一方のペインへコピー",
"다른 창으로 이동": "もう一方のペインへ移動",
"다른 폴더로 이동": "他のフォルダーに移動",
"다른 화면으로 가면 페이지 소리 멈춤": "他の画面に移るとページの音を止める",
"다시 만들기 대기로": "作り直し待ちに戻す",
"다시 설치": "再インストール",
"다시 세기": "再カウント",
@ -609,6 +613,7 @@
"분석 실패: {0} - {1}": "分析失敗: {0} - {1}",
"붙여넣기": "貼り付け",
"붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "貼り付ける項目がありません。先に [コピー] · [移動] で保持してください。",
"브라우저": "ブラウザー",
"브라우저 엔진 (앱 안)": "ブラウザーエンジン(アプリ内)",
"브라우저 쿠키를 읽지 못했습니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 \"앱 안 브라우저\" 를 고르거나, 고른 브라우저가 설치되어 있는지 확인하세요 (Chrome · Edge 는 브라우저를 닫아야 읽힙니다).": "ブラウザーのクッキーを読めませんでした。環境設定 > ダウンロード > YouTube クッキー で「アプリ内ブラウザー」を選ぶか、選んだブラウザーがインストールされているか確認してください(Chrome・Edge はブラウザーを閉じないと読めません)。",
"비공개 · 회원 전용 영상입니다.": "非公開・メンバー限定の動画です。",
@ -896,6 +901,7 @@
"이 파일에 맞는 자막 우선 (영상 해시)": "このファイルに合う字幕を優先(動画ハッシュ)",
"이 파일용": "このファイル用",
"이 파일을 열 수 있는 앱이 없습니다.": "このファイルを開けるアプリがありません。",
"이 페이지 번역": "このページを翻訳",
"이 페이지 지우기": "このページを削除",
"이 페이지를 {0}(으)로 번역": "このページを{0}に翻訳",
"이 폴더 선택": "このフォルダーを選択",
@ -1085,6 +1091,7 @@
"즐겨찾기": "ブックマーク",
"즐겨찾기 HTML 가져오기": "ブックマーク HTML を取り込む",
"즐겨찾기 검색 (이름 · 주소)": "ブックマークを検索(名前・アドレス)",
"즐겨찾기 관리": "ブックマーク管理",
"즐겨찾기 관리 (Ctrl+Shift+B)": "ブックマーク管理(Ctrl+Shift+B)",
"즐겨찾기 관리자": "ブックマークマネージャー",
"즐겨찾기 관리자 (Ctrl+Shift+O)": "ブックマークマネージャー(Ctrl+Shift+O)",
@ -1098,6 +1105,7 @@
"즐겨찾기 추가됨": "ブックマークを追加しました",
"즐겨찾기 표시줄": "ブックマークバー",
"지금": "現在",
"지금 {0}%": "現在 {0}%",
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "実行中のコピーは前のオプションで動いています。止めて新しいオプションで再実行しますか?",
"지금 맞추기": "今すぐ同期",
"지금 맞추는 중입니다. 끝난 뒤에 하세요.": "同期中です。終わってから行ってください。",
@ -1176,6 +1184,7 @@
"크메르어": "クメール語",
"타갈로그어": "タガログ語",
"타밀어": "タミル語",
"탐색기": "エクスプローラー",
"탐색기 오른쪽 클릭 메뉴": "エクスプローラーの右クリックメニュー",
"탐색기에서 동영상을 열 때": "エクスプローラーで動画を開いたとき",
"탐색기에서 연 동영상을 재생할 창": "エクスプローラーで開いた動画を再生するウィンドウ",
@ -1302,6 +1311,7 @@
"화면 작게": "画面を小さく",
"화면 크게": "画面を大きく",
"화면 크기": "画面サイズ",
"화면 크기 · 종료": "画面サイズ・終了",
"화면 크기 따라 (기본)": "画面サイズに合わせる (既定)",
"화면이 있는 컴포넌트는 하나 이상 남아 있어야 합니다.": "画面のあるコンポーネントを少なくとも1つ残す必要があります。",
"화질": "画質",

View File

@ -276,6 +276,7 @@
"검색 지우기": "清除搜索",
"검정": "黑色",
"결과 폴더": "输出文件夹",
"결과 폴더 열기": "打开输出文件夹",
"결과 폴더 열기 (동영상을 고르세요)": "打开输出文件夹(请选择视频)",
"결과 폴더 열기: {0}": "打开输出文件夹: {0}",
"결과물 이동 실패: {0} ({1})": "成品移动失败: {0}({1})",
@ -313,6 +314,7 @@
"기본": "默认",
"기본 앱으로 열기": "用默认应用打开",
"기본 연결 프로그램": "默认打开方式",
"기본 크기 ({0}%)": "默认大小 ({0}%)",
"기본 크기 ({0}%) 로 · 기본 크기는 환경 설정 > 화면 에서": "恢复默认大小({0}%)· 默认大小在 设置 > 显示 中设置",
"기본 프로그램": "默认程序",
"기본 프로그램으로 열기": "用默认程序打开",
@ -328,6 +330,7 @@
"끄면 마지막 설정으로 바로 시작: 원어 {0}, 언어 {1}, {2}": "关闭后将直接使用上次的设置开始: 原语言 {0},语言 {1},{2}",
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). Chrome 엔진은 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "关闭后不运行页面脚本 (广告 · 跟踪会减少,但 YouTube 等由脚本构建的网站将无法显示)。使用 Chrome 引擎时,页面翻译 · 视频检测也会停止。",
"끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "关闭后不运行页面脚本 (广告 · 跟踪会减少,但 YouTube 等由脚本构建的网站将无法显示)。页面翻译 · 视频检测也会停止。",
"끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다": "关闭后,在查看设置、下载列表或其他界面时网页声音会继续播放",
"끌어다 놓은 항목에 동영상이 없습니다.": "拖入的项目中没有视频。",
"끝": "结束",
"끝 (오른쪽 · 아래)": "边缘 (右 · 下)",
@ -396,6 +399,7 @@
"다른 창으로 복사": "复制到另一窗格",
"다른 창으로 이동": "移动到另一窗格",
"다른 폴더로 이동": "移动到其他文件夹",
"다른 화면으로 가면 페이지 소리 멈춤": "离开浏览器时暂停网页声音",
"다시 만들기 대기로": "改回等待重新制作",
"다시 설치": "重新安装",
"다시 세기": "重新统计",
@ -609,6 +613,7 @@
"분석 실패: {0} - {1}": "分析失败: {0} - {1}",
"붙여넣기": "粘贴",
"붙여넣을 항목이 없습니다. 먼저 [복사] · [이동] 으로 담으세요.": "没有可粘贴的项目。请先用 [复制] · [移动] 暂存。",
"브라우저": "浏览器",
"브라우저 엔진 (앱 안)": "浏览器引擎(应用内)",
"브라우저 쿠키를 읽지 못했습니다. 환경 설정 > 다운로드 > YouTube 쿠키 에서 \"앱 안 브라우저\" 를 고르거나, 고른 브라우저가 설치되어 있는지 확인하세요 (Chrome · Edge 는 브라우저를 닫아야 읽힙니다).": "未能读取浏览器 Cookie。请在 设置 > 下载 > YouTube Cookie 中选择“应用内浏览器”,或确认所选浏览器已安装(Chrome · Edge 需关闭浏览器才能读取)。",
"비공개 · 회원 전용 영상입니다.": "这是私享 · 会员专享视频。",
@ -896,6 +901,7 @@
"이 파일에 맞는 자막 우선 (영상 해시)": "优先匹配此文件的字幕(视频哈希)",
"이 파일용": "适用于此文件",
"이 파일을 열 수 있는 앱이 없습니다.": "没有可以打开此文件的应用。",
"이 페이지 번역": "翻译此页",
"이 페이지 지우기": "删除此页",
"이 페이지를 {0}(으)로 번역": "将此页面翻译为{0}",
"이 폴더 선택": "选择此文件夹",
@ -1085,6 +1091,7 @@
"즐겨찾기": "书签",
"즐겨찾기 HTML 가져오기": "导入书签 HTML",
"즐겨찾기 검색 (이름 · 주소)": "搜索书签(名称 · 地址)",
"즐겨찾기 관리": "管理书签",
"즐겨찾기 관리 (Ctrl+Shift+B)": "管理书签(Ctrl+Shift+B)",
"즐겨찾기 관리자": "书签管理器",
"즐겨찾기 관리자 (Ctrl+Shift+O)": "书签管理器(Ctrl+Shift+O)",
@ -1098,6 +1105,7 @@
"즐겨찾기 추가됨": "已添加书签",
"즐겨찾기 표시줄": "书签栏",
"지금": "当前",
"지금 {0}%": "当前 {0}%",
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "正在运行的复制使用的是之前的选项。停止并用新选项重新运行吗?",
"지금 맞추기": "立即同步",
"지금 맞추는 중입니다. 끝난 뒤에 하세요.": "正在同步。请等结束后再进行。",
@ -1176,6 +1184,7 @@
"크메르어": "高棉语",
"타갈로그어": "他加禄语",
"타밀어": "泰米尔语",
"탐색기": "文件管理",
"탐색기 오른쪽 클릭 메뉴": "资源管理器右键菜单",
"탐색기에서 동영상을 열 때": "从资源管理器打开视频时",
"탐색기에서 연 동영상을 재생할 창": "播放从资源管理器打开的视频的窗口",
@ -1302,6 +1311,7 @@
"화면 작게": "缩小界面",
"화면 크게": "放大界面",
"화면 크기": "画面大小",
"화면 크기 · 종료": "界面大小 · 退出",
"화면 크기 따라 (기본)": "按屏幕大小 (默认)",
"화면이 있는 컴포넌트는 하나 이상 남아 있어야 합니다.": "至少要保留一个带界面的组件。",
"화질": "画质",

View File

@ -177,11 +177,8 @@ class AppController extends ChangeNotifier {
return;
}
encoders = await _tool.encoders();
final missing = [
for (final c in VideoCodecChoice.values)
if (c != VideoCodecChoice.copy && !isCodecAvailable(c)) c.label,
];
_log(trf('FFmpeg 준비됨: {0}' '{1}', [ffmpegVersion, missing.isEmpty ? '' : trf(' ⚠ 사용할 수 없는 코덱: {0}', [missing.join(', ')])]));
// 이 기기에 없는 코덱은 코덱 고르기 목록에 "(사용 불가)" 로 보인다. 켤 때마다 경고처럼 남기지 않는다 (고장으로 보였음)
_log(trf('FFmpeg 준비됨: {0}' '{1}', [ffmpegVersion, '']));
}
bool isCodecAvailable(VideoCodecChoice c) =>

View File

@ -193,6 +193,9 @@ class AppSettings {
/// YouTube 페이지의 광고 배너 · 광고 영역 숨기기
bool youtubeAdHide = true;
/// 웹 브라우저 위에 다른 화면이 올라오거나 다른 화면으로 가면 페이지의 동영상 · 소리를 멈춤 (끄면 계속 들림)
bool webPauseOnLeave = true;
/// 다운로드 위치 (아래에 jj_yt-dlp, jj_aria2 생성). null 이면 프로그램 폴더
String? downloadRoot;
@ -426,6 +429,7 @@ class AppSettings {
'moveTargets': [for (final t in moveTargets) t.toJson()],
'youtubeAdSkip': youtubeAdSkip,
'youtubeAdHide': youtubeAdHide,
'webPauseOnLeave': webPauseOnLeave,
'downloadRoot': downloadRoot,
'clipboardWatch': clipboardWatch,
'minimizeToTray': minimizeToTray,
@ -542,6 +546,7 @@ class AppSettings {
]
..youtubeAdSkip = j['youtubeAdSkip'] as bool? ?? true
..youtubeAdHide = j['youtubeAdHide'] as bool? ?? true
..webPauseOnLeave = j['webPauseOnLeave'] as bool? ?? true
..downloadRoot = j['downloadRoot'] as String?
..clipboardWatch = j['clipboardWatch'] as bool? ?? true
..minimizeToTray = j['minimizeToTray'] as bool? ?? true

View File

@ -203,7 +203,33 @@ class AppNavButtons extends StatelessWidget {
'downloads' => here ? tr('다운로드 목록 (지금 여기)') : tr('다운로드 목록'),
_ => id,
};
Widget btn(Widget icon, String tip, VoidCallback? f, {bool here = false}) => SizedBox(
// 좁은 화면 (폰): 아이콘 아래에 짧은 이름 - 글자 없는 작은 아이콘만 늘어서 무엇인지 몰랐던 것
final labels = isCompact(context);
Widget btn(Widget icon, String tip, VoidCallback? f, {bool here = false, String label = ''}) => labels
? Tooltip(
message: tip,
child: InkWell(
onTap: f,
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 54,
height: 46,
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
IconTheme(
data: IconThemeData(
size: 20, color: here ? JjColors.accent : (f == null ? JjColors.textDim : JjColors.text)),
child: icon,
),
const SizedBox(height: 2),
Text(label,
maxLines: 1,
overflow: TextOverflow.clip,
style: TextStyle(fontSize: 10, color: here ? JjColors.accent : JjColors.textDim)),
]),
),
),
)
: SizedBox(
width: 40,
height: 40,
child: IconButton(
@ -214,12 +240,21 @@ class AppNavButtons extends StatelessWidget {
onPressed: f,
),
);
String short(String id) => switch (id) {
'mkv' => 'MKV',
'browser' => tr('브라우저'),
'explorer' => tr('탐색기'),
'rsync' => 'Rsync',
'downloads' => tr('다운로드'),
_ => id,
};
return Row(mainAxisSize: MainAxisSize.min, children: [
btn(
Image.asset(appIconButtonAsset(scope?.controller.settings.appIcon),
width: 26, height: 26, filterQuality: FilterQuality.medium),
width: labels ? 22 : 26, height: labels ? 22 : 26, filterQuality: FilterQuality.medium),
trf('홈 화면 ({0}) · 환경 설정에서 바꿈', [homeName]),
scope == null ? null : () => toHome(context),
label: tr('홈'),
),
// 설치한 화면들 (환경 설정 > 컴포넌트 의 순서). 하나뿐이면 버튼 줄은 그것만.
for (final p in pages)
@ -228,10 +263,12 @@ class AppNavButtons extends StatelessWidget {
tip(p.id, p.id == current),
p.id == current ? null : () => open(context, p.id),
here: p.id == current,
label: short(p.id),
),
btn(const Icon(Icons.arrow_back), tr('뒤로'),
// 브라우저 화면: 뒤로 키는 웹 페이지 뒤로 (PopScope) 이지만 이 버튼은 화면 이동이라 바로 닫는다
atRoot ? null : () => onBrowserPage ? Navigator.pop(context) : Navigator.maybePop(context)),
atRoot ? null : () => onBrowserPage ? Navigator.pop(context) : Navigator.maybePop(context),
label: tr('뒤로')),
]);
}
}
@ -440,6 +477,62 @@ class AppActions extends StatelessWidget {
if (controller == null && exit == null) return const SizedBox();
final usage = controller?.usage;
final compact = isCompact(context);
if (compact) {
return Row(mainAxisSize: MainAxisSize.min, children: [
if (controller != null)
IconButton(
tooltip: tr('환경 설정'),
icon: Icon(onSettingsPage ? Icons.settings : Icons.settings_outlined,
color: onSettingsPage ? JjColors.accent : null),
onPressed: onSettingsPage
? null
: () => Navigator.push(context, MaterialPageRoute<void>(builder: (_) => SettingsPage(c: controller))),
),
PopupMenuButton<String>(
tooltip: tr('화면 크기 · 종료'),
icon: const Icon(Icons.more_vert),
onSelected: (v) {
final s = controller?.settings;
switch (v) {
case 'bigger' when s != null:
controller!.updateSettings((x) => x.uiScale = AppSettings.clampUiScale(s.uiScale + 0.1));
case 'smaller' when s != null:
controller!.updateSettings((x) => x.uiScale = AppSettings.clampUiScale(s.uiScale - 0.1));
case 'default' when s != null:
controller!.updateSettings((x) => x.uiScale = s.uiScaleDefault);
case 'exit':
exit?.call();
}
},
itemBuilder: (_) => [
if (controller != null) ...[
PopupMenuItem(
value: 'bigger',
child: ListTile(leading: const Icon(Icons.zoom_in), title: Text(tr('화면 크게')), dense: true)),
PopupMenuItem(
value: 'smaller',
child: ListTile(leading: const Icon(Icons.zoom_out), title: Text(tr('화면 작게')), dense: true)),
PopupMenuItem(
value: 'default',
child: ListTile(
leading: const Icon(Icons.fit_screen_outlined),
title: Text(trf('기본 크기 ({0}%)', [(controller.settings.uiScaleDefault * 100).round()])),
subtitle: Text(trf('지금 {0}%', [(controller.settings.uiScale * 100).round()])),
dense: true)),
],
if (exit != null) ...[
const PopupMenuDivider(),
PopupMenuItem(
value: 'exit',
child: ListTile(
leading: const Icon(Icons.power_settings_new, color: JjColors.danger),
title: Text(tr('종료')),
dense: true)),
],
],
),
]);
}
return Row(mainAxisSize: MainAxisSize.min, children: [
// CPU · MEM: 변환 · AI 작업이 PC 를 얼마나 쓰는지 (PC 전체 기준)
if (usage != null && showUsage && !compact) UsageView(usage: usage),

View File

@ -291,7 +291,8 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
@override
void didPushNext() {
_visible = false;
_nav?.pauseMedia();
// 환경 설정 > 웹 브라우저 > "다른 화면으로 가면 페이지 소리 멈춤" (끄면 다른 화면에서도 계속 들림)
if (widget.c.settings.webPauseOnLeave) _nav?.pauseMedia();
}
/// 설정 화면 등에서 돌아왔다: 바뀐 광고 설정을 지금 페이지에 적용
@ -312,7 +313,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
/// MKV 화면 등으로 가며 브라우저 화면이 닫힌다: 살려 둔 웹뷰에서 소리만 나지 않게 멈춘다
@override
void didPop() {
_nav?.pauseMedia();
if (widget.c.settings.webPauseOnLeave) _nav?.pauseMedia();
}
@override
@ -519,12 +520,14 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
icon: Icon(i, size: 20, color: color),
onPressed: f,
);
// 좁은 화면: 둘째 줄에 페이지 이동 · 주소 · 다운로드 (외부 브라우저 · 작업 현황은 빼고, 다운로드는 아이콘만)
final compact = isCompact(context);
// 주소 칸이 좁아지면 (폰 세로 · 태블릿 세로 · 폰 가로 100%) 자주 안 쓰는 버튼은 ⋮ 메뉴로 - 주소가 "http" 만 보이거나
// 아예 사라지던 것. 버튼을 없애지는 않는다.
return AppTopBar(
nav: const AppNavButtons(onBrowserPage: true),
actions: AppActions(c: widget.c),
middle: Row(children: [
middle: LayoutBuilder(builder: (context, box) {
final compact = box.maxWidth < 640;
return Row(children: [
if (!compact) ...[
Container(width: 1, height: 24, color: JjColors.border),
const SizedBox(width: 4),
@ -535,7 +538,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
_progress < 1
? btn(Icons.close, tr('중지'), () => _nav?.stop())
: btn(Icons.refresh, tr('새로고침 (F5)'), () => _nav?.reload()),
btn(Icons.home_outlined, tr('홈'), () => _go(widget.c.settings.homeUrl)),
if (!compact) btn(Icons.home_outlined, tr('홈'), () => _go(widget.c.settings.homeUrl)),
const SizedBox(width: 6),
Expanded(
child: TextField(
@ -561,6 +564,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
),
),
const SizedBox(width: 4),
if (!compact)
btn(
Icons.translate,
_trOn
@ -596,9 +600,41 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
if (!compact)
btn(_work ? Icons.view_sidebar : Icons.view_sidebar_outlined, tr('작업 현황 보기 · 화면 분할 (Ctrl+Shift+J)'),
_toggleWork, color: widget.c.busy ? JjColors.accent : null),
if (!compact)
btn(_panel ? Icons.bookmarks : Icons.bookmarks_outlined, tr('즐겨찾기 관리 (Ctrl+Shift+B)'),
() => setState(() => _panel = !_panel)),
]),
() => setState(() => _panel = !_panel))
else
PopupMenuButton<String>(
tooltip: tr('더 보기'),
icon: const Icon(Icons.more_vert, size: 20),
onSelected: (v) => switch (v) {
'home' => _go(widget.c.settings.homeUrl),
'translate' => _setTranslate(!_trOn, force: true),
'external' => _openExternal(_url),
'work' => _toggleWork(),
_ => setState(() => _panel = !_panel),
},
itemBuilder: (_) => [
PopupMenuItem(value: 'home', child: ListTile(dense: true, leading: const Icon(Icons.home_outlined), title: Text(tr('홈')))),
PopupMenuItem(
value: 'translate',
child: ListTile(
dense: true,
leading: Icon(Icons.translate, color: _trOn ? JjColors.accent : null),
title: Text(_trOn ? tr('원문 보기 (번역 끄기)') : tr('이 페이지 번역')))),
PopupMenuItem(
value: 'bookmarks',
child: ListTile(dense: true, leading: const Icon(Icons.bookmarks_outlined), title: Text(tr('즐겨찾기 관리')))),
PopupMenuItem(
value: 'work',
child: ListTile(dense: true, leading: const Icon(Icons.view_sidebar_outlined), title: Text(tr('작업 현황')))),
PopupMenuItem(
value: 'external',
child: ListTile(dense: true, leading: const Icon(Icons.open_in_new), title: Text(tr('외부 브라우저로 열기')))),
],
),
]);
}),
);
}
}

View File

@ -41,8 +41,10 @@ class _CleanupDialogState extends State<_CleanupDialog> {
if (!mounted) return;
setState(() {
_groups = g;
// 처음에는 지울 수 있고 무언가 있는 묶음을 모두 고른 상태로
_picked.addAll([for (final x in g) if (x.skipped == null && x.items.isNotEmpty) x.id]);
// 처음에는 작업 임시 파일 · 받다 만 다운로드만 고른 상태로 (사용자 결정 10/8).
// 받다 만 AI 모델 · 지난 작업 기록 · 업데이트 남은 파일은 직접 체크해야 지운다
const pre = {'work', 'download'};
_picked.addAll([for (final x in g) if (x.skipped == null && x.items.isNotEmpty && pre.contains(x.id)) x.id]);
});
});
}

View File

@ -57,7 +57,10 @@ class HomePage extends StatelessWidget {
const Divider(height: 1),
Expanded(
// 좁은 화면 (접은 폴드 · 휴대폰 세로): 목록을 위에, 자세히 보기를 아래에
child: isCompact(context)
child: isCompact(context) && c.videos.isEmpty
// 8: 좁은 화면에 동영상이 없으면 빈 목록 · 안내로 나누지 않고 한 화면에 안내 + [동영상 추가]
? _EmptyHint(onAdd: c.pickVideos)
: isCompact(context)
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -67,7 +70,7 @@ class HomePage extends StatelessWidget {
const Divider(height: 1),
Expanded(
child: c.selected == null
? const _EmptyHint()
? _EmptyHint(onAdd: c.pickVideos)
: _VideoDetail(c: c, v: c.selected!),
),
],
@ -75,11 +78,14 @@ class HomePage extends StatelessWidget {
: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(width: 320, child: _VideoList(c: c)),
// 13: 큰 화면 (태블릿 세로 등) 은 목록도 넓게 - 제목이 앞부분만 보여 구분이 안 되던 것
SizedBox(
width: (MediaQuery.sizeOf(context).width * 0.4).clamp(320.0, 640.0),
child: _VideoList(c: c)),
const VerticalDivider(width: 1),
Expanded(
child: c.selected == null
? const _EmptyHint()
? _EmptyHint(onAdd: c.pickVideos)
: _VideoDetail(c: c, v: c.selected!),
),
],
@ -193,6 +199,9 @@ class _TopBar extends StatelessWidget {
if (need() > w) dlBox = false;
if (need() > w) mkvText = false;
// 7: 폰에는 "탐색기에서 끌어다 놓기" 가 없다
final addTip = Platform.isAndroid ? tr('동영상 추가') : tr('동영상 추가 (탐색기에서 끌어다 놓아도 됩니다)');
// 글이 있는 버튼, 또는 (좁을 때) 아이콘만 있는 버튼
Widget action(IconData icon, String label, VoidCallback? onPressed,
{required bool text, required String tip, Color? color}) {
@ -210,6 +219,60 @@ class _TopBar extends StatelessWidget {
);
}
// 2: 좁은 화면 (폰 세로) 에서는 버튼 글자를 숨기지 않고 옆으로 밀어 본다. 가장 많이 쓰는 [MKV 만들기] 를 맨 앞에.
if (compact) {
VoidCallback? build0() =>
!canBuild ? null : (batch.isEmpty ? () => _confirmBuildAll(context) : () => c.buildVideos(batch));
return AppTopBar(
nav: const AppNavButtons(),
actions: AppActions(c: c, onExit: onExit, showUsage: false),
middle: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: [
Tooltip(
message: trf('MKV 만들기{0}', [count]),
child: FilledButton.icon(
onPressed: build0(),
icon: Icon(c.busy ? Icons.playlist_add : Icons.play_arrow, size: 20),
label: Text(trf('MKV 만들기{0}{1}', [count, c.busy ? tr(' (대기열)') : ''])),
),
),
const SizedBox(width: 8),
action(Icons.add, tr('동영상 추가'), c.pickVideos, text: true, tip: addTip),
if (hasPlayer) ...[
const SizedBox(width: 8),
action(
Icons.playlist_play,
trf('선택한 파일 재생{0}', [count]),
batch.isEmpty ? null : () => playFiles(context, c, [for (final v in batch) v.path], keepOrder: true),
text: true,
tip: tr('선택한 파일 재생: 지금 보고 있는 동영상 (여러 개는 목록에서 체크)')),
],
if (hasAi) ...[
const SizedBox(width: 8),
action(Icons.auto_awesome, trf('자막 만들기{0}', [count]),
canBatch ? () => showAiDialog(context, c, batch.first, targets: batch) : null,
text: true, tip: trf('자막 만들기: {0}', [_batchHint(c, tr('AI 자막을 만듭니다'))])),
const SizedBox(width: 8),
action(Icons.auto_mode, trf('자막 만들기 & MKV 만들기{0}', [count]),
canBatch ? () => showAiDialog(context, c, batch.first, targets: batch, thenBuild: true) : null,
text: true,
tip: trf('자막 만들기 & MKV 만들기: {0}', [_batchHint(c, tr('AI 자막을 만들고 이어서 MKV 로 만듭니다'))])),
],
const SizedBox(width: 8),
action(Icons.folder_special_outlined, tr('결과 폴더'), c.selected == null ? null : () => _openOutput(context),
text: true, tip: tr('결과 폴더 열기')),
if (c.busy) ...[
const SizedBox(width: 8),
JobIndicator(c: c, compact: true, iconOnly: false),
const SizedBox(width: 8),
action(Icons.stop, tr('취소'), c.cancel, text: true, tip: tr('작업 취소'), color: JjColors.danger),
],
]),
),
);
}
return AppTopBar(
// 모든 화면 공통: [JJ 홈] [MKV 화면] [뒤로] [다운로드 목록]
nav: const AppNavButtons(),
@ -225,7 +288,7 @@ class _TopBar extends StatelessWidget {
scrollDirection: Axis.horizontal,
child: Row(children: [
action(Icons.add, tr('동영상 추가'), c.pickVideos,
text: leftText, tip: tr('동영상 추가 (탐색기에서 끌어다 놓아도 됩니다)')),
text: leftText, tip: addTip),
if (hasAi) ...[
const SizedBox(width: 8),
action(Icons.auto_awesome, trf('자막 만들기{0}', [count]),
@ -423,11 +486,23 @@ class _EncodeBar extends StatelessWidget {
),
),
]);
if (compact) {
return Container(
width: double.infinity,
color: JjColors.panel,
padding: const EdgeInsets.fromLTRB(8, 4, 8, 6),
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
runSpacing: 6,
children: [for (final w in row.children) if (w is! Expanded) w],
),
);
}
return Container(
height: 44,
color: JjColors.panel,
padding: EdgeInsets.symmetric(horizontal: compact ? 8 : 16),
child: compact ? SingleChildScrollView(scrollDirection: Axis.horizontal, child: row) : row,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: row,
);
}
}
@ -697,21 +772,28 @@ class _StatusIcon extends StatelessWidget {
// ───────── 오른쪽: 선택한 동영상 ─────────
class _EmptyHint extends StatelessWidget {
const _EmptyHint();
final VoidCallback? onAdd;
const _EmptyHint({this.onAdd});
@override
Widget build(BuildContext context) => Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.video_library_outlined, size: 56, color: JjColors.textDim),
SizedBox(height: 12),
Text(tr('"동영상 추가" 로 파일을 선택하세요'),
style: TextStyle(color: JjColors.textDim)),
SizedBox(height: 4),
const SizedBox(height: 12),
Text(tr('"동영상 추가" 로 파일을 선택하세요'), textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim)),
const SizedBox(height: 4),
Text(tr('같은 폴더의 자막은 자동으로 추가됩니다'),
style: TextStyle(color: JjColors.textDim, fontSize: 12)),
textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
if (onAdd != null) ...[
const SizedBox(height: 16),
FilledButton.icon(onPressed: onAdd, icon: const Icon(Icons.add), label: Text(tr('동영상 추가'))),
],
],
),
),
);
}

View File

@ -384,10 +384,12 @@ class _PlayerPageState extends State<PlayerPage> {
builder: (_, s, _) {
final total = s.duration.inMilliseconds.toDouble();
final pos = (_seekDrag ?? s.position.inMilliseconds.toDouble()).clamp(0.0, total <= 0 ? 1.0 : total);
// 9: 손가락 (Android) 은 버튼을 크게 · 넉넉하게 (작고 촘촘해 누르기 어려웠던 것)
final touch = Platform.isAndroid;
IconButton btn(IconData i, String tip, VoidCallback onTap, {double size = 22}) => IconButton(
tooltip: tip,
iconSize: size,
visualDensity: VisualDensity.compact,
iconSize: touch ? size + 6 : size,
visualDensity: touch ? VisualDensity.standard : VisualDensity.compact,
color: Colors.white,
onPressed: onTap,
icon: Icon(i),
@ -416,7 +418,19 @@ class _PlayerPageState extends State<PlayerPage> {
Text(formatSrtTime(s.duration).substring(0, 8),
style: const TextStyle(fontSize: 12, color: Colors.white70, fontFamily: 'Consolas')),
]),
// 손가락: 재생 버튼 줄과 나머지 (음량 · 자막 · 목록 · 전체 화면) 줄을 나눠 넉넉하게
if (touch)
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
btn(Icons.skip_previous, tr('이전 (P)'), () => pl.previous()),
btn(Icons.replay_10, tr('10초 뒤로 (←)'), () => _seekBy(-10)),
btn(s.playing ? Icons.pause_circle_filled : Icons.play_circle_fill, tr('재생/일시정지 (Space)'),
() => pl.playOrPause(), size: 40),
btn(Icons.stop, tr('정지'), () => pl.stop()),
btn(Icons.forward_10, tr('10초 앞으로 (→)'), () => _seekBy(10)),
btn(Icons.skip_next, tr('다음 (N)'), () => pl.next()),
]),
Row(children: [
if (!touch) ...[
btn(Icons.skip_previous, tr('이전 (P)'), () => pl.previous()),
if (!compact) btn(Icons.replay_10, tr('10초 뒤로 (←)'), () => _seekBy(-10)),
btn(s.playing ? Icons.pause_circle_filled : Icons.play_circle_fill, tr('재생/일시정지 (Space)'),
@ -425,6 +439,7 @@ class _PlayerPageState extends State<PlayerPage> {
if (!compact) btn(Icons.forward_10, tr('10초 앞으로 (→)'), () => _seekBy(10)),
btn(Icons.skip_next, tr('다음 (N)'), () => pl.next()),
const SizedBox(width: 8),
],
btn(s.volume == 0 ? Icons.volume_off : Icons.volume_up, tr('음소거 (M)'), _toggleMute),
if (!compact)
SizedBox(
@ -463,7 +478,7 @@ class _PlayerPageState extends State<PlayerPage> {
List<({String label, bool checked, VoidCallback onTap})> items) =>
PopupMenuButton<int>(
tooltip: tip,
icon: Icon(icon, color: Colors.white, size: 22),
icon: Icon(icon, color: Colors.white, size: Platform.isAndroid ? 28 : 22),
itemBuilder: (_) => [
for (var i = 0; i < items.length; i++)
CheckedPopupMenuItem(value: i, checked: items[i].checked, child: Text(items[i].label)),

View File

@ -492,6 +492,12 @@ class _SettingsPageState extends State<SettingsPage> {
title: Text(tr('YouTube 광고 배너 숨기기')),
subtitle: Text(tr('목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다')),
),
SwitchListTile(
value: s.webPauseOnLeave,
onChanged: (v) => c.updateSettings((x) => x.webPauseOnLeave = v),
title: Text(tr('다른 화면으로 가면 페이지 소리 멈춤')),
subtitle: Text(tr('끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다')),
),
SwitchListTile(
value: s.webTranslate,
onChanged: (v) => c.updateSettings((x) => x.webTranslate = v),

View File

@ -120,13 +120,13 @@ void main() {
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(MaterialApp(home: HomePage(c: c, onExit: () {})));
expect(find.text('동영상 추가'), findsOneWidget);
expect(find.text('동영상 추가'), findsWidgets);
await tester.runAsync(() => i18n.apply('en'));
await tester.pump();
expect(find.text('Add videos'), findsOneWidget);
expect(find.text('Add videos'), findsWidgets);
expect(find.text('동영상 추가'), findsNothing);
await tester.runAsync(() => i18n.apply('zh-Hans'));
await tester.pump();
expect(find.text('添加视频'), findsOneWidget);
expect(find.text('添加视频'), findsWidgets);
});
}