From 951a1c927c04255b19f684a36ac72a6e7e782578 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Thu, 8 Oct 2026 23:36:21 +0900 Subject: [PATCH] =?UTF-8?q?MKV=20=ED=99=94=EB=A9=B4=20=C2=B7=20=EB=A7=A8?= =?UTF-8?q?=20=EC=9C=84=20=EC=A4=84=20=C2=B7=20=EB=B8=8C=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=EC=A0=80=20=EC=A3=BC=EC=86=8C=20=C2=B7=20=EC=9E=AC=EC=83=9D=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=C2=B7=20=EB=B8=8C=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=EC=A0=80=20=EC=86=8C=EB=A6=AC=20=EC=84=A4=EC=A0=95=20=C2=B7=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20=EC=B0=BD=20=EC=B2=B4=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 근거: - 감독 세션 지시 (폰 · 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 --- assets/l10n/en.json | 10 +++ assets/l10n/ja.json | 10 +++ assets/l10n/zh-Hans.json | 10 +++ lib/app/app_controller.dart | 7 +-- lib/app/settings.dart | 5 ++ lib/ui/app_actions.dart | 119 +++++++++++++++++++++++++++++++---- lib/ui/browser_page.dart | 54 +++++++++++++--- lib/ui/cleanup_dialog.dart | 6 +- lib/ui/home_page.dart | 120 ++++++++++++++++++++++++++++++------ lib/ui/player_page.dart | 21 ++++++- lib/ui/settings_page.dart | 6 ++ test/i18n_test.dart | 6 +- 12 files changed, 320 insertions(+), 54 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index efd3061..5d4d1fc 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -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", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 6b8eb13..400c343 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -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つ残す必要があります。", "화질": "画質", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 035cf5c..788f173 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -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 @@ "화면 작게": "缩小界面", "화면 크게": "放大界面", "화면 크기": "画面大小", + "화면 크기 · 종료": "界面大小 · 退出", "화면 크기 따라 (기본)": "按屏幕大小 (默认)", "화면이 있는 컴포넌트는 하나 이상 남아 있어야 합니다.": "至少要保留一个带界面的组件。", "화질": "画质", diff --git a/lib/app/app_controller.dart b/lib/app/app_controller.dart index b81745b..6f2507a 100644 --- a/lib/app/app_controller.dart +++ b/lib/app/app_controller.dart @@ -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) => diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 6dfd565..39accc9 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -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 diff --git a/lib/ui/app_actions.dart b/lib/ui/app_actions.dart index 72c4380..fbfa87d 100644 --- a/lib/ui/app_actions.dart +++ b/lib/ui/app_actions.dart @@ -203,23 +203,58 @@ class AppNavButtons extends StatelessWidget { 'downloads' => here ? tr('다운로드 목록 (지금 여기)') : tr('다운로드 목록'), _ => id, }; - Widget btn(Widget icon, String tip, VoidCallback? f, {bool here = false}) => SizedBox( - width: 40, - height: 40, - child: IconButton( - tooltip: tip, - padding: EdgeInsets.zero, - isSelected: here, - icon: icon, - onPressed: f, - ), - ); + // 좁은 화면 (폰): 아이콘 아래에 짧은 이름 - 글자 없는 작은 아이콘만 늘어서 무엇인지 몰랐던 것 + 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( + tooltip: tip, + padding: EdgeInsets.zero, + isSelected: here, + icon: icon, + 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(builder: (_) => SettingsPage(c: controller))), + ), + PopupMenuButton( + 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), diff --git a/lib/ui/browser_page.dart b/lib/ui/browser_page.dart index 2ebbde5..1bc7c01 100644 --- a/lib/ui/browser_page.dart +++ b/lib/ui/browser_page.dart @@ -291,7 +291,8 @@ class _BrowserPageState extends State with RouteAware { @override void didPushNext() { _visible = false; - _nav?.pauseMedia(); + // 환경 설정 > 웹 브라우저 > "다른 화면으로 가면 페이지 소리 멈춤" (끄면 다른 화면에서도 계속 들림) + if (widget.c.settings.webPauseOnLeave) _nav?.pauseMedia(); } /// 설정 화면 등에서 돌아왔다: 바뀐 광고 설정을 지금 페이지에 적용 @@ -312,7 +313,7 @@ class _BrowserPageState extends State with RouteAware { /// MKV 화면 등으로 가며 브라우저 화면이 닫힌다: 살려 둔 웹뷰에서 소리만 나지 않게 멈춘다 @override void didPop() { - _nav?.pauseMedia(); + if (widget.c.settings.webPauseOnLeave) _nav?.pauseMedia(); } @override @@ -519,12 +520,14 @@ class _BrowserPageState extends State 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 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 with RouteAware { ), ), const SizedBox(width: 4), + if (!compact) btn( Icons.translate, _trOn @@ -596,9 +600,41 @@ class _BrowserPageState extends State 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), - btn(_panel ? Icons.bookmarks : Icons.bookmarks_outlined, tr('즐겨찾기 관리 (Ctrl+Shift+B)'), - () => setState(() => _panel = !_panel)), - ]), + if (!compact) + btn(_panel ? Icons.bookmarks : Icons.bookmarks_outlined, tr('즐겨찾기 관리 (Ctrl+Shift+B)'), + () => setState(() => _panel = !_panel)) + else + PopupMenuButton( + 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('외부 브라우저로 열기')))), + ], + ), + ]); + }), ); } } diff --git a/lib/ui/cleanup_dialog.dart b/lib/ui/cleanup_dialog.dart index 0863b12..cc78c80 100644 --- a/lib/ui/cleanup_dialog.dart +++ b/lib/ui/cleanup_dialog.dart @@ -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]); }); }); } diff --git a/lib/ui/home_page.dart b/lib/ui/home_page.dart index 5eed681..860ee58 100644 --- a/lib/ui/home_page.dart +++ b/lib/ui/home_page.dart @@ -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: 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), - Text(tr('같은 폴더의 자막은 자동으로 추가됩니다'), - style: TextStyle(color: JjColors.textDim, fontSize: 12)), - ], + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.video_library_outlined, size: 56, color: JjColors.textDim), + const SizedBox(height: 12), + 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)), + if (onAdd != null) ...[ + const SizedBox(height: 16), + FilledButton.icon(onPressed: onAdd, icon: const Icon(Icons.add), label: Text(tr('동영상 추가'))), + ], + ], + ), ), ); } diff --git a/lib/ui/player_page.dart b/lib/ui/player_page.dart index 92d5f1c..54077ef 100644 --- a/lib/ui/player_page.dart +++ b/lib/ui/player_page.dart @@ -384,10 +384,12 @@ class _PlayerPageState extends State { 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 { 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 { 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 { List<({String label, bool checked, VoidCallback onTap})> items) => PopupMenuButton( 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)), diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index b630a8f..01d05c4 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -492,6 +492,12 @@ class _SettingsPageState extends State { 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), diff --git a/test/i18n_test.dart b/test/i18n_test.dart index 6c604a0..89b68f9 100644 --- a/test/i18n_test.dart +++ b/test/i18n_test.dart @@ -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); }); }