From 69b8ab52ad82560b01f1804a5106e47d6dff1a30 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:59:08 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=ED=95=B4=EC=83=81=EB=8F=84=20=EC=98=AC?= =?UTF-8?q?=EB=A6=AC=EA=B8=B0=20=EB=A9=94=EB=AA=A8=EB=A6=AC:=20=EC=9D=B4?= =?UTF-8?q?=20=EA=B8=B0=EA=B8=B0=EC=97=90=EC=84=9C=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=EA=B0=92=EC=9D=B4=EB=A9=B4=20=EA=B0=99=EB=8B=A4=EA=B3=A0=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20(192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 편의 (기기 확인, P11): 메모리가 작은 기기는 적게 · 보통 · 많이가 모두 가장 작은 타일이라 같은 값이 세 번 보였다 Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 2 ++ assets/l10n/ja.json | 2 ++ assets/l10n/zh-Hans.json | 2 ++ lib/app/ai_upscale.dart | 20 ++++++++++++++++++++ lib/ui/ai_settings.dart | 15 ++------------- test/upscale_job_test.dart | 8 ++++++++ 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 1f697b2..3bea859 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -9,6 +9,7 @@ " (바뀐 날 같음)": " (same modified time)", " (보낼 것이 더 새것)": " (the one being sent is newer)", " (보통)": " (normal)", + " (보통과 같음)": " (same as Normal)", " (사용 불가)": " (unavailable)", " (있는 것이 더 새것)": " (the existing one is newer)", " (자막 {0}개 함께)": " ({0} subtitles moved too)", @@ -1458,6 +1459,7 @@ "이 기기에서는 AI 번역을 쓸 수 없습니다.": "AI translation is not available on this device.", "이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요": "The AI engine was not found on this device. Try reinstalling the app", "이 기기에서는 쓸 수 없는 컴포넌트입니다": "This component is not available on this device", + "이 기기에서는 적게 · 보통 · 많이 가 모두 같습니다: {0} (메모리가 작아 가장 작은 타일)": "On this device Low · Normal · High are all the same: {0} (smallest tile because memory is small)", "이 기기용 파일 없음": "No file for this device", "이 기기의 CPU 가 이 AI 엔진을 지원하지 않습니다": "This device's CPU does not support this AI engine", "이 기기의 안전 저장소를 쓸 수 없어 비밀번호를 저장하지 못했습니다": "Couldn't save the password because this device's secure storage is unavailable", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 8fccf17..22b6828 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -9,6 +9,7 @@ " (바뀐 날 같음)": " (更新日時が同じ)", " (보낼 것이 더 새것)": " (送る方が新しい)", " (보통)": "(標準)", + " (보통과 같음)": " (普通と同じ)", " (사용 불가)": "(使用不可)", " (있는 것이 더 새것)": " (既存の方が新しい)", " (자막 {0}개 함께)": "(字幕 {0}個も一緒に)", @@ -1458,6 +1459,7 @@ "이 기기에서는 AI 번역을 쓸 수 없습니다.": "この端末では AI 翻訳を使えません。", "이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요": "この端末で AI エンジンが見つかりませんでした。アプリを再インストールしてください", "이 기기에서는 쓸 수 없는 컴포넌트입니다": "この端末では使えないコンポーネントです", + "이 기기에서는 적게 · 보통 · 많이 가 모두 같습니다: {0} (메모리가 작아 가장 작은 타일)": "この機器では少なめ · 普通 · 多めはすべて同じです: {0} (メモリが少ないため最小のタイル)", "이 기기용 파일 없음": "このデバイス用のファイルなし", "이 기기의 CPU 가 이 AI 엔진을 지원하지 않습니다": "この端末の CPU はこの AI エンジンに対応していません", "이 기기의 안전 저장소를 쓸 수 없어 비밀번호를 저장하지 못했습니다": "この端末の安全な保存領域を使えないため、パスワードを保存できませんでした", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 57960e7..1f464b8 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -9,6 +9,7 @@ " (바뀐 날 같음)": "(修改时间相同)", " (보낼 것이 더 새것)": "(要发送的较新)", " (보통)": "(标准)", + " (보통과 같음)": "(与普通相同)", " (사용 불가)": "(不可用)", " (있는 것이 더 새것)": "(已有的较新)", " (자막 {0}개 함께)": "(字幕 {0} 个一并移动)", @@ -1458,6 +1459,7 @@ "이 기기에서는 AI 번역을 쓸 수 없습니다.": "此设备无法使用 AI 翻译。", "이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요": "在此设备上找不到 AI 引擎。请尝试重新安装应用", "이 기기에서는 쓸 수 없는 컴포넌트입니다": "此组件在本设备上不可用", + "이 기기에서는 적게 · 보통 · 많이 가 모두 같습니다: {0} (메모리가 작아 가장 작은 타일)": "在此设备上,少 · 普通 · 多 都相同:{0}(内存较小,使用最小的分块)", "이 기기용 파일 없음": "没有此设备的文件", "이 기기의 CPU 가 이 AI 엔진을 지원하지 않습니다": "此设备的 CPU 不支持该 AI 引擎", "이 기기의 안전 저장소를 쓸 수 없어 비밀번호를 저장하지 못했습니다": "本设备的安全存储不可用,无法保存密码", diff --git a/lib/app/ai_upscale.dart b/lib/app/ai_upscale.dart index 5a229e3..0baaf9b 100644 --- a/lib/app/ai_upscale.dart +++ b/lib/app/ai_upscale.dart @@ -92,6 +92,26 @@ int androidUpscaleTile({int? memTotalMb, int? longSide, double share = 0.30}) { return 192; } +/// 176 · 192: 환경 설정 "해상도 올리기 메모리" 의 설명 줄 - 이 기기에서 적게 · 보통 · 많이 가 고르는 타일 · 메모리. +/// 메모리가 작은 기기 (P11 4GB 등) 는 셋 다 가장 작은 타일이라 같은 값이 세 번 보였다 - 같으면 그렇다고 알린다 +List upscaleMemoryLines(int? memTotalMb) { + int tile(String id) => androidUpscaleTile(memTotalMb: memTotalMb, share: upscaleMemoryShare(id)); + String text(int t) => trf('타일 {0} · 약 {1}GB', [t, (upscaleTileMemoryMb(t) / 1024).toStringAsFixed(1)]); + final low = tile('low'), normal = tile('normal'), high = tile('high'); + final same = tr(' (보통과 같음)'); + return [ + if (memTotalMb != null) trf('이 기기 메모리 {0}GB 기준:', [(memTotalMb / 1024).toStringAsFixed(0)]), + if (low == normal && normal == high) + trf('이 기기에서는 적게 · 보통 · 많이 가 모두 같습니다: {0} (메모리가 작아 가장 작은 타일)', [text(normal)]) + else ...[ + trf('적게 (느림): {0}', [text(low)]) + (low == normal ? same : ''), + trf('보통: {0}', [text(normal)]), + trf('많이 (빠름): {0} - 다른 앱이 많이 켜져 있으면 Android 가 이 앱을 끌 수 있습니다', [text(high)]) + (high == normal ? same : ''), + ], + tr('타일이 작을수록 느립니다 (예: 16GB 폰에서 1024×768 을 2배로 - 512 약 4분 · 320 약 9분 30초).'), + ]; +} + /// 타일 크기 → 엔진이 쓸 메모리 어림 (MB, S10 실측 512 → 6.6GB 에서 넓이 비례) int upscaleTileMemoryMb(int tile) => (6600 * (tile / 512) * (tile / 512)).round(); diff --git a/lib/ui/ai_settings.dart b/lib/ui/ai_settings.dart index 695ac0a..ff653e1 100644 --- a/lib/ui/ai_settings.dart +++ b/lib/ui/ai_settings.dart @@ -15,7 +15,7 @@ import 'folder_picker.dart'; import 'license_texts.dart' show showLicenseDocument; import 'setting_tile.dart'; import 'theme.dart'; -import '../app/ai_upscale.dart' show androidUpscaleTile, upscaleMemoryShare, upscaleTileMemoryMb, deviceMemTotalMb; +import '../app/ai_upscale.dart' show deviceMemTotalMb, upscaleMemoryLines; /// 기기 안에서 그리는 데 아직 받지 않은 파일 (Windows 는 실행 파일까지, TAESD 포함) - AI 그림 화면 · 받기 화면이 같은 값을 보인다 (139) List aiMissingFiles(AiStore store) => [ @@ -75,21 +75,10 @@ class _AiSettingsState extends State { /// 176: 해상도 올리기 메모리 (적게 · 보통 · 많이) - 이 기기에서의 타일 · 예상 메모리 Widget _upMemoryTile() { final mem = deviceMemTotalMb(); - String level(String id) { - final t = androidUpscaleTile(memTotalMb: mem, share: upscaleMemoryShare(id)); - return trf('타일 {0} · 약 {1}GB', [t, (upscaleTileMemoryMb(t) / 1024).toStringAsFixed(1)]); - } - return SettingTile( leading: const Icon(Icons.memory), title: Text(tr('해상도 올리기 메모리')), - subtitle: Text([ - if (mem != null) trf('이 기기 메모리 {0}GB 기준:', [(mem / 1024).toStringAsFixed(0)]), - trf('적게 (느림): {0}', [level('low')]), - trf('보통: {0}', [level('normal')]), - trf('많이 (빠름): {0} - 다른 앱이 많이 켜져 있으면 Android 가 이 앱을 끌 수 있습니다', [level('high')]), - tr('타일이 작을수록 느립니다 (예: 16GB 폰에서 1024×768 을 2배로 - 512 약 4분 · 320 약 9분 30초).'), - ].join('\n')), + subtitle: Text(upscaleMemoryLines(mem).join('\n')), isThreeLine: true, trailing: DropdownButton( value: c.settings.aiUpMemory, diff --git a/test/upscale_job_test.dart b/test/upscale_job_test.dart index 08c0948..36b3818 100644 --- a/test/upscale_job_test.dart +++ b/test/upscale_job_test.dart @@ -58,6 +58,14 @@ void main() { expect(androidUpscaleTile(memTotalMb: 40000, share: 0.20), 512); expect(androidUpscaleTile(memTotalMb: 6000, share: 0.20), 192); expect(androidUpscaleTile(longSide: 1024), 256); + // 192: 메모리가 작은 기기 (P11 4GB, MemTotal 약 3.5GB) 는 셋 다 가장 작은 타일 - 같은 값을 세 번 보이지 않고 같다고 알린다 + final small = upscaleMemoryLines(3500); + expect(small.where((l) => l.contains('모두 같습니다')), hasLength(1)); + expect(small.where((l) => l.startsWith('적게') || l.startsWith('많이')), isEmpty); + // 16GB 는 셋이 다르다 + expect(upscaleMemoryLines(15000).where((l) => l.contains('같')), isEmpty); + // 일부만 같으면 그 줄에 (보통과 같음) + expect(upscaleMemoryLines(5000).where((l) => l.contains('(보통과 같음)')), isNotEmpty); expect(androidUpscaleTile(longSide: 800), 512); }); From e32cd7d648c6429ebfc9f696bd6f063d87c3cef8 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:03:54 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=EB=8B=A8=EC=B6=95=ED=82=A4=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20'(Alt+=E2=86=90)'=20=EB=93=B1=EC=9D=80=20PC=20?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=A7=8C=20-=20=ED=83=9C=EB=B8=94=EB=A6=BF?= =?UTF-8?q?=20=C2=B7=20=ED=8F=B0=EC=9D=80=20=EC=9D=B4=EB=A6=84=EB=A7=8C=20?= =?UTF-8?q?(194)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 편의 · 접근성 (기기 확인): 키보드가 없는 태블릿에 Alt 안내가 보이고 TalkBack 도 읽었다 - keyHint Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 19 +++++++------------ assets/l10n/ja.json | 19 +++++++------------ assets/l10n/zh-Hans.json | 19 +++++++------------ lib/ui/browser_page.dart | 17 +++++++++-------- lib/ui/explorer_page.dart | 5 +++-- lib/ui/key_hint.dart | 5 +++++ lib/ui/subtitle_editor_page.dart | 3 ++- test/key_hint_test.dart | 11 +++++++++++ 8 files changed, 51 insertions(+), 47 deletions(-) create mode 100644 lib/ui/key_hint.dart create mode 100644 test/key_hint_test.dart diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 3bea859..8dcb56b 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -689,7 +689,7 @@ "다음": "Next", "다음 (N)": "Next (N)", "다음 {0}개 기다림": "{0} more waiting", - "다음 페이지 (Alt+→)": "Next page (Alt+→)", + "다음 페이지": "Next page", "다음부터 묻지 않기 (환경 설정 > 실행 에서 바꿀 수 있음)": "Don't ask again (can be changed in Settings > Run)", "단계": "Steps", "단축키 해제": "Unregister hotkey", @@ -777,7 +777,6 @@ "두 창으로": "Two panes", "두 창으로 보기 (좌우)": "Show two panes (side by side)", "뒤로": "Back", - "뒤로 (Alt+←)": "Back (Alt+←)", "뒤로 보내고 계속": "Keep running in background", "뒤에서 계속": "Continue in background", "드라이브의 맨 위 폴더는 지울 수 없습니다": "The top folder of a drive can't be deleted", @@ -1078,7 +1077,7 @@ "새 화면 \"AI 그림\" 을 위쪽 이동 버튼에 넣었습니다 (글 → 그림 · 그림 → 그림, 모델은 환경 설정에서 받기). 끄기: 환경 설정 > 컴포넌트": "Added a new \"AI images\" screen to the top navigation buttons (text → image · image → image; download models in Settings). To turn off: Settings > Components", "새로 고침": "Refresh", "새로 넣은 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 사라집니다.": "The password you entered has not been saved yet. It will be lost when you close the app.", - "새로고침 (F5)": "Reload (F5)", + "새로고침": "Reload", "색 보정": "Color correction", "색온도": "Temperature", "서버 · 서비스 (기기 밖)": "Servers · services (off device)", @@ -1236,7 +1235,7 @@ "압축 파일 안에 없습니다": "Not in the archive", "압축 파일 안에 프로그램이 없습니다.": "The program is not in the archive.", "압축을 풀 수 없습니다: {0}": "Cannot extract the archive: {0}", - "앞으로 (Alt+→)": "Forward (Alt+→)", + "앞으로": "Forward", "앞의 rsync 가 끝난 뒤에 하세요.": "Wait until the previous rsync 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", @@ -1551,7 +1550,7 @@ "이전": "Previous", "이전 (P)": "Previous (P)", "이전 설정을 되살렸습니다": "Previous settings restored", - "이전 페이지 (Alt+←)": "Previous page (Alt+←)", + "이전 페이지": "Previous page", "이탈리아어": "Italian", "인도네시아어": "Indonesian", "인식된 말이 없습니다.": "No speech was recognized.", @@ -1631,7 +1630,7 @@ "작업 취소": "Cancel job", "작업 현황": "Job status", "작업 현황 닫기": "Close job status", - "작업 현황 보기 · 화면 분할 (Ctrl+Shift+J)": "Show job status · split screen (Ctrl+Shift+J)", + "작업 현황 보기 · 화면 분할": "Show job status · split screen", "작업 확인 (진행 중인 작업 없음)": "Job check (no jobs in progress)", "작업이 진행 중입니다": "Tasks are running", "작업하다 남은 임시 파일 · 받다 만 다운로드 (.part 등) · 받다 만 AI 모델 · 업데이트하고 남은 파일 · 지난 작업 기록을 찾아 지웁니다. 받은 동영상 · 만든 MKV · 자막은 지우지 않고, 진행 중인 작업 · 다운로드가 쓰는 것은 건너뜁니다": "Finds and deletes temporary files left from jobs · unfinished downloads (.part etc.) · unfinished AI models · update leftovers · old job logs. Downloaded videos · made MKVs · subtitles are not deleted, and anything used by running jobs · downloads is skipped", @@ -1648,7 +1647,7 @@ "재생 속도": "Playback speed", "재생 오류: {0}": "Playback error: {0}", "재생/일시정지 (Space)": "Play/pause (Space)", - "재생/정지 (Ctrl+Space)": "Play/stop (Ctrl+Space)", + "재생/정지": "Play/stop", "재생목록 불러오는 중… {0}": "Loading playlist… {0}", "재생목록 주소면 목록 전체 받기": "Download the whole list for playlist URLs", "재생목록을 읽을 수 없습니다: {0}": "Cannot read the playlist: {0}", @@ -1738,7 +1737,7 @@ "주말 종일": "Weekends, all day", "주소": "Address", "주소 (http · https · 마그넷) 를 찾지 못했거나 이미 받는 중입니다.": "No address (http · https · magnet) found, or it's already downloading.", - "주소 입력 또는 검색 (Ctrl+L)": "Enter an address or search (Ctrl+L)", + "주소 입력 또는 검색": "Enter an address or search", "주소로 열기": "Open by address", "주소를 https 로 바꾸고 다시 확인": "Switch the address to https and check again", "주소에 영상이 없습니다 (404)": "No video at this address (404)", @@ -1754,15 +1753,11 @@ "즐겨찾기 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)", "즐겨찾기 관리자에서 보기": "Show in bookmark manager", "즐겨찾기 내보내기 (HTML)": "Export bookmarks (HTML)", "즐겨찾기 수정": "Edit bookmark", - "즐겨찾기 수정 (Ctrl+D)": "Edit bookmark (Ctrl+D)", "즐겨찾기 추가": "Add bookmark", - "즐겨찾기 추가 (Ctrl+D)": "Add bookmark (Ctrl+D)", "즐겨찾기 추가…": "Add bookmark…", "즐겨찾기 추가됨": "Bookmark added", "즐겨찾기 표시줄": "Bookmarks bar", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 22b6828..641f3d6 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -689,7 +689,7 @@ "다음": "次へ", "다음 (N)": "次へ(N)", "다음 {0}개 기다림": "次の {0} 件が待機中", - "다음 페이지 (Alt+→)": "次のページ(Alt+→)", + "다음 페이지": "次のページ", "다음부터 묻지 않기 (환경 설정 > 실행 에서 바꿀 수 있음)": "次回から確認しない(環境設定 > 実行 で変更できます)", "단계": "ステップ", "단축키 해제": "ショートカット解除", @@ -777,7 +777,6 @@ "두 창으로": "2 画面に", "두 창으로 보기 (좌우)": "2 画面で表示 (左右)", "뒤로": "戻る", - "뒤로 (Alt+←)": "戻る (Alt+←)", "뒤로 보내고 계속": "裏で続ける", "뒤에서 계속": "バックグラウンドで続ける", "드라이브의 맨 위 폴더는 지울 수 없습니다": "ドライブの最上位フォルダーは削除できません", @@ -1078,7 +1077,7 @@ "새 화면 \"AI 그림\" 을 위쪽 이동 버튼에 넣었습니다 (글 → 그림 · 그림 → 그림, 모델은 환경 설정에서 받기). 끄기: 환경 설정 > 컴포넌트": "新しい画面「AI 画像」を上部の移動ボタンに追加しました (テキスト → 画像・画像 → 画像、モデルは環境設定でダウンロード)。オフにする: 環境設定 > コンポーネント", "새로 고침": "更新", "새로 넣은 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 사라집니다.": "入力したパスワードはまだ保存されていません。アプリを終了すると消えます。", - "새로고침 (F5)": "再読み込み(F5)", + "새로고침": "再読み込み", "색 보정": "カラー補正", "색온도": "色温度", "서버 · 서비스 (기기 밖)": "サーバー・サービス (端末外)", @@ -1236,7 +1235,7 @@ "압축 파일 안에 없습니다": "圧縮ファイル内にありません", "압축 파일 안에 프로그램이 없습니다.": "圧縮ファイル内にプログラムがありません。", "압축을 풀 수 없습니다: {0}": "圧縮ファイルを展開できません: {0}", - "앞으로 (Alt+→)": "進む (Alt+→)", + "앞으로": "進む", "앞의 rsync 가 끝난 뒤에 하세요.": "前の rsync が終わってから行ってください。", "앞의 복사 · 이동이 끝난 뒤에 하세요.": "前のコピー · 移動が終わってから行ってください。", "앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "アプリ一覧・ホーム画面とアプリ内左上のボタンのアイコンです。変更するとホーム画面のアイコンが消えることがあり、アプリ一覧から再度ドラッグする必要がある場合があります", @@ -1551,7 +1550,7 @@ "이전": "前へ", "이전 (P)": "前へ(P)", "이전 설정을 되살렸습니다": "以前の設定を復元しました", - "이전 페이지 (Alt+←)": "前のページ(Alt+←)", + "이전 페이지": "前のページ", "이탈리아어": "イタリア語", "인도네시아어": "インドネシア語", "인식된 말이 없습니다.": "認識された音声がありません。", @@ -1631,7 +1630,7 @@ "작업 취소": "作業をキャンセル", "작업 현황": "作業状況", "작업 현황 닫기": "作業状況を閉じる", - "작업 현황 보기 · 화면 분할 (Ctrl+Shift+J)": "作業状況を表示・画面分割(Ctrl+Shift+J)", + "작업 현황 보기 · 화면 분할": "作業状況を表示・画面分割", "작업 확인 (진행 중인 작업 없음)": "作業確認(進行中の作業なし)", "작업이 진행 중입니다": "作業中です", "작업하다 남은 임시 파일 · 받다 만 다운로드 (.part 등) · 받다 만 AI 모델 · 업데이트하고 남은 파일 · 지난 작업 기록을 찾아 지웁니다. 받은 동영상 · 만든 MKV · 자막은 지우지 않고, 진행 중인 작업 · 다운로드가 쓰는 것은 건너뜁니다": "作業で残った一時ファイル・ダウンロード途中のファイル(.part など)・途中の AI モデル・更新の残り・古い作業記録を探して削除します。ダウンロードした動画・作成した MKV・字幕は削除せず、進行中の作業・ダウンロードが使っているものはスキップします", @@ -1648,7 +1647,7 @@ "재생 속도": "再生速度", "재생 오류: {0}": "再生エラー: {0}", "재생/일시정지 (Space)": "再生/一時停止(Space)", - "재생/정지 (Ctrl+Space)": "再生/停止(Ctrl+Space)", + "재생/정지": "再生/停止", "재생목록 불러오는 중… {0}": "再生リストを読み込み中… {0}", "재생목록 주소면 목록 전체 받기": "再生リストの URL ならリスト全体をダウンロード", "재생목록을 읽을 수 없습니다: {0}": "再生リストを読めません: {0}", @@ -1738,7 +1737,7 @@ "주말 종일": "週末 終日", "주소": "アドレス", "주소 (http · https · 마그넷) 를 찾지 못했거나 이미 받는 중입니다.": "アドレス (http・https・マグネット) が見つからないか、すでにダウンロード中です。", - "주소 입력 또는 검색 (Ctrl+L)": "アドレスを入力または検索(Ctrl+L)", + "주소 입력 또는 검색": "アドレスを入力または検索", "주소로 열기": "アドレスで開く", "주소를 https 로 바꾸고 다시 확인": "アドレスを https に変えてもう一度確認", "주소에 영상이 없습니다 (404)": "このアドレスに動画がありません (404)", @@ -1754,15 +1753,11 @@ "즐겨찾기 HTML 가져오기": "ブックマーク HTML を取り込む", "즐겨찾기 검색 (이름 · 주소)": "ブックマークを検索(名前・アドレス)", "즐겨찾기 관리": "ブックマーク管理", - "즐겨찾기 관리 (Ctrl+Shift+B)": "ブックマーク管理(Ctrl+Shift+B)", "즐겨찾기 관리자": "ブックマークマネージャー", - "즐겨찾기 관리자 (Ctrl+Shift+O)": "ブックマークマネージャー(Ctrl+Shift+O)", "즐겨찾기 관리자에서 보기": "ブックマークマネージャーで表示", "즐겨찾기 내보내기 (HTML)": "ブックマークを書き出す(HTML)", "즐겨찾기 수정": "ブックマークを編集", - "즐겨찾기 수정 (Ctrl+D)": "ブックマークを編集(Ctrl+D)", "즐겨찾기 추가": "ブックマークを追加", - "즐겨찾기 추가 (Ctrl+D)": "ブックマークを追加(Ctrl+D)", "즐겨찾기 추가…": "ブックマークを追加…", "즐겨찾기 추가됨": "ブックマークを追加しました", "즐겨찾기 표시줄": "ブックマークバー", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 1f464b8..36a8462 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -689,7 +689,7 @@ "다음": "下一个", "다음 (N)": "下一个(N)", "다음 {0}개 기다림": "另有 {0} 个等待中", - "다음 페이지 (Alt+→)": "下一页(Alt+→)", + "다음 페이지": "下一页", "다음부터 묻지 않기 (환경 설정 > 실행 에서 바꿀 수 있음)": "下次不再询问(可在 设置 > 运行 中更改)", "단계": "步数", "단축키 해제": "注销快捷键", @@ -777,7 +777,6 @@ "두 창으로": "双窗格", "두 창으로 보기 (좌우)": "以双窗格显示 (左右)", "뒤로": "后退", - "뒤로 (Alt+←)": "后退 (Alt+←)", "뒤로 보내고 계속": "转到后台继续", "뒤에서 계속": "在后台继续", "드라이브의 맨 위 폴더는 지울 수 없습니다": "无法删除驱动器的根文件夹", @@ -1078,7 +1077,7 @@ "새 화면 \"AI 그림\" 을 위쪽 이동 버튼에 넣었습니다 (글 → 그림 · 그림 → 그림, 모델은 환경 설정에서 받기). 끄기: 환경 설정 > 컴포넌트": "已将新画面“AI 图片”加入顶部导航按钮 (文字 → 图片 · 图片 → 图片,模型在设置中下载)。关闭: 设置 > 组件", "새로 고침": "刷新", "새로 넣은 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 사라집니다.": "输入的密码尚未保存。关闭应用后会丢失。", - "새로고침 (F5)": "刷新(F5)", + "새로고침": "刷新", "색 보정": "色彩校正", "색온도": "色温", "서버 · 서비스 (기기 밖)": "服务器 · 服务 (设备外)", @@ -1236,7 +1235,7 @@ "압축 파일 안에 없습니다": "压缩文件中没有", "압축 파일 안에 프로그램이 없습니다.": "压缩文件中没有程序。", "압축을 풀 수 없습니다: {0}": "无法解压: {0}", - "앞으로 (Alt+→)": "前进 (Alt+→)", + "앞으로": "前进", "앞의 rsync 가 끝난 뒤에 하세요.": "请等上一个 rsync 结束后再运行。", "앞의 복사 · 이동이 끝난 뒤에 하세요.": "请等待前一个复制 · 移动完成。", "앱 목록 · 홈 화면과 앱 안 왼쪽 위 버튼의 아이콘입니다. 바꾸면 홈 화면에 둔 아이콘이 없어질 수 있어 앱 목록에서 다시 끌어다 놓아야 할 수 있습니다": "应用列表、主屏幕以及应用内左上角按钮的图标。更改后主屏幕上的图标可能会消失,可能需要从应用列表重新拖放", @@ -1551,7 +1550,7 @@ "이전": "上一个", "이전 (P)": "上一个(P)", "이전 설정을 되살렸습니다": "已恢复之前的设置", - "이전 페이지 (Alt+←)": "上一页(Alt+←)", + "이전 페이지": "上一页", "이탈리아어": "意大利语", "인도네시아어": "印尼语", "인식된 말이 없습니다.": "未识别到语音。", @@ -1631,7 +1630,7 @@ "작업 취소": "取消任务", "작업 현황": "任务状态", "작업 현황 닫기": "关闭任务状态", - "작업 현황 보기 · 화면 분할 (Ctrl+Shift+J)": "显示任务状态 · 分屏(Ctrl+Shift+J)", + "작업 현황 보기 · 화면 분할": "显示任务状态 · 分屏", "작업 확인 (진행 중인 작업 없음)": "任务检查(没有进行中的任务)", "작업이 진행 중입니다": "任务正在进行", "작업하다 남은 임시 파일 · 받다 만 다운로드 (.part 등) · 받다 만 AI 모델 · 업데이트하고 남은 파일 · 지난 작업 기록을 찾아 지웁니다. 받은 동영상 · 만든 MKV · 자막은 지우지 않고, 진행 중인 작업 · 다운로드가 쓰는 것은 건너뜁니다": "查找并删除任务残留的临时文件 · 未完成的下载(.part 等)· 未下载完的 AI 模型 · 更新残留 · 旧任务记录。不会删除已下载的视频 · 制作的 MKV · 字幕,正在进行的任务 · 下载所用的文件会跳过", @@ -1648,7 +1647,7 @@ "재생 속도": "播放速度", "재생 오류: {0}": "播放错误: {0}", "재생/일시정지 (Space)": "播放/暂停(Space)", - "재생/정지 (Ctrl+Space)": "播放/停止(Ctrl+Space)", + "재생/정지": "播放/停止", "재생목록 불러오는 중… {0}": "正在加载播放列表… {0}", "재생목록 주소면 목록 전체 받기": "如果是播放列表地址则下载整个列表", "재생목록을 읽을 수 없습니다: {0}": "无法读取播放列表: {0}", @@ -1738,7 +1737,7 @@ "주말 종일": "周末 全天", "주소": "地址", "주소 (http · https · 마그넷) 를 찾지 못했거나 이미 받는 중입니다.": "未找到地址 (http · https · 磁力),或已在下载。", - "주소 입력 또는 검색 (Ctrl+L)": "输入地址或搜索(Ctrl+L)", + "주소 입력 또는 검색": "输入地址或搜索", "주소로 열기": "按地址打开", "주소를 https 로 바꾸고 다시 확인": "将地址改为 https 并重新检查", "주소에 영상이 없습니다 (404)": "此地址没有视频 (404)", @@ -1754,15 +1753,11 @@ "즐겨찾기 HTML 가져오기": "导入书签 HTML", "즐겨찾기 검색 (이름 · 주소)": "搜索书签(名称 · 地址)", "즐겨찾기 관리": "管理书签", - "즐겨찾기 관리 (Ctrl+Shift+B)": "管理书签(Ctrl+Shift+B)", "즐겨찾기 관리자": "书签管理器", - "즐겨찾기 관리자 (Ctrl+Shift+O)": "书签管理器(Ctrl+Shift+O)", "즐겨찾기 관리자에서 보기": "在书签管理器中显示", "즐겨찾기 내보내기 (HTML)": "导出书签(HTML)", "즐겨찾기 수정": "编辑书签", - "즐겨찾기 수정 (Ctrl+D)": "编辑书签(Ctrl+D)", "즐겨찾기 추가": "添加书签", - "즐겨찾기 추가 (Ctrl+D)": "添加书签(Ctrl+D)", "즐겨찾기 추가…": "添加书签…", "즐겨찾기 추가됨": "已添加书签", "즐겨찾기 표시줄": "书签栏", diff --git a/lib/ui/browser_page.dart b/lib/ui/browser_page.dart index f3c5232..e4040e5 100644 --- a/lib/ui/browser_page.dart +++ b/lib/ui/browser_page.dart @@ -24,6 +24,7 @@ import 'app_actions.dart'; import 'bookmark_ui.dart'; import 'confirm.dart'; import 'downloads_page.dart'; +import 'key_hint.dart'; import 'theme.dart'; import 'work_panel.dart'; import '../l10n/tr.dart'; @@ -604,11 +605,11 @@ class _BrowserPageState extends State with RouteAware { const SizedBox(width: 4), ], // 웹 페이지 이동 (앱 화면 이동인 왼쪽 버튼과 구분되는 모양) - btn(Icons.arrow_back_ios_new, tr('이전 페이지 (Alt+←)'), _canBack ? () => _nav?.back() : null), - btn(Icons.arrow_forward_ios, tr('다음 페이지 (Alt+→)'), _canFwd ? () => _nav?.forward() : null), + btn(Icons.arrow_back_ios_new, keyHint(tr('이전 페이지'), 'Alt+←'), _canBack ? () => _nav?.back() : null), + btn(Icons.arrow_forward_ios, keyHint(tr('다음 페이지'), 'Alt+→'), _canFwd ? () => _nav?.forward() : null), _progress < 1 ? btn(Icons.close, tr('중지'), () => _nav?.stop()) - : btn(Icons.refresh, tr('새로고침 (F5)'), () => _nav?.reload()), + : btn(Icons.refresh, keyHint(tr('새로고침'), 'F5'), () => _nav?.reload()), if (!compact) btn(Icons.home_outlined, tr('홈'), () => _go(widget.c.settings.homeUrl)), const SizedBox(width: 6), Expanded( @@ -620,12 +621,12 @@ class _BrowserPageState extends State with RouteAware { isDense: true, filled: true, fillColor: JjColors.bg, - hintText: tr('주소 입력 또는 검색 (Ctrl+L)'), + hintText: keyHint(tr('주소 입력 또는 검색'), 'Ctrl+L'), prefixIcon: Icon(_url.startsWith('https') ? Icons.lock_outline : Icons.public, size: 16), prefixIconConstraints: const BoxConstraints(minWidth: 32), border: OutlineInputBorder(borderRadius: BorderRadius.circular(18), borderSide: BorderSide.none), suffixIcon: IconButton( - tooltip: starred ? tr('즐겨찾기 수정 (Ctrl+D)') : tr('즐겨찾기 추가 (Ctrl+D)'), + tooltip: starred ? keyHint(tr('즐겨찾기 수정'), 'Ctrl+D') : keyHint(tr('즐겨찾기 추가'), 'Ctrl+D'), icon: Icon(starred ? Icons.star : Icons.star_border, size: 18, color: starred ? Colors.amber : JjColors.textDim), onPressed: _star, @@ -686,10 +687,10 @@ class _BrowserPageState extends State with RouteAware { ), ), if (!compact) - btn(_work ? Icons.view_sidebar : Icons.view_sidebar_outlined, tr('작업 현황 보기 · 화면 분할 (Ctrl+Shift+J)'), + btn(_work ? Icons.view_sidebar : Icons.view_sidebar_outlined, keyHint(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)'), + btn(_panel ? Icons.bookmarks : Icons.bookmarks_outlined, keyHint(tr('즐겨찾기 관리'), 'Ctrl+Shift+B'), () => setState(() => _panel = !_panel)) else PopupMenuButton( @@ -1404,7 +1405,7 @@ class _BookmarkPanelState extends State { const Spacer(), if (widget.onOpenManager != null) IconButton( - tooltip: tr('즐겨찾기 관리자 (Ctrl+Shift+O)'), + tooltip: keyHint(tr('즐겨찾기 관리자'), 'Ctrl+Shift+O'), iconSize: 18, icon: const Icon(Icons.open_in_full), onPressed: () => widget.onOpenManager!(folderId: _folderId), diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index dc157ae..54d52f9 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -24,6 +24,7 @@ import '../core/webdav.dart' show DavRegistry; import '../platform/android/android_storage.dart'; import 'app_actions.dart'; import 'explorer_look.dart'; +import 'key_hint.dart'; import 'monitor_page.dart'; import 'player_page.dart'; import 'rsync_setup.dart'; @@ -3164,13 +3165,13 @@ class _ExplorerPageState extends State { List _headerStart(_Pane pane) => [ // 75: 이 창의 뒤로 · 앞으로 (가 본 폴더) IconButton( - tooltip: tr('뒤로 (Alt+←)'), + tooltip: keyHint(tr('뒤로'), 'Alt+←'), visualDensity: VisualDensity.compact, icon: const Icon(Icons.arrow_back, size: 16), onPressed: pane.back.isEmpty ? null : () => _travel(pane, forward: false), ), IconButton( - tooltip: tr('앞으로 (Alt+→)'), + tooltip: keyHint(tr('앞으로'), 'Alt+→'), visualDensity: VisualDensity.compact, icon: const Icon(Icons.arrow_forward, size: 16), onPressed: pane.forward.isEmpty ? null : () => _travel(pane, forward: true), diff --git a/lib/ui/key_hint.dart b/lib/ui/key_hint.dart new file mode 100644 index 0000000..a73ae34 --- /dev/null +++ b/lib/ui/key_hint.dart @@ -0,0 +1,5 @@ +import 'dart:io'; + +/// 194: 단축키 안내 "(Alt+←)" 는 키보드를 쓰는 PC 에서만 붙인다. +/// 태블릿 · 폰에서는 이름만 (누를 키가 없고, TalkBack 도 "Alt 왼쪽 화살표" 까지 읽었다) +String keyHint(String text, String keys) => Platform.isAndroid ? text : '$text ($keys)'; diff --git a/lib/ui/subtitle_editor_page.dart b/lib/ui/subtitle_editor_page.dart index d6d8f52..c3caa3b 100644 --- a/lib/ui/subtitle_editor_page.dart +++ b/lib/ui/subtitle_editor_page.dart @@ -11,6 +11,7 @@ import '../core/srt.dart'; import '../core/text_codec.dart'; import '../services/preview_player.dart'; import 'app_actions.dart'; +import 'key_hint.dart'; import 'theme.dart'; import 'timeline.dart'; import '../l10n/tr.dart'; @@ -428,7 +429,7 @@ class _SubtitleEditorPageState extends State { onPressed: () => _seekBy(-5000), ), IconButton( - tooltip: tr('재생/정지 (Ctrl+Space)'), + tooltip: keyHint(tr('재생/정지'), 'Ctrl+Space'), iconSize: 30, icon: Icon(_playing ? Icons.pause_circle : Icons.play_circle, color: JjColors.accent), diff --git a/test/key_hint_test.dart b/test/key_hint_test.dart new file mode 100644 index 0000000..8c716e5 --- /dev/null +++ b/test/key_hint_test.dart @@ -0,0 +1,11 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/ui/key_hint.dart'; + +/// 194: 단축키 안내는 PC 에서만 (태블릿 · 폰은 이름만 - TalkBack 도 이름만 읽게) +void main() { + test('194: keyHint - PC 는 "이름 (키)", Android 는 이름만', () { + expect(keyHint('뒤로', 'Alt+←'), Platform.isAndroid ? '뒤로' : '뒤로 (Alt+←)'); + }); +} From 624076f787001d0fc00288b50cda9a33f3bcfe06 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:06:16 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=EC=98=88=EC=A0=84=20=ED=8C=90=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=98=AC=EB=9D=BC=EC=98=A4=EB=A9=B4=20=EB=B0=94?= =?UTF-8?q?=EB=80=90=20=EB=8F=99=EC=9E=91=EC=9D=84=20=ED=95=9C=20=EB=B2=88?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC:=20=EB=B0=9B=EB=8A=94=20=EC=AA=BD?= =?UTF-8?q?=EC=9D=B4=20=EB=8D=94=20=EC=83=88=EA=B2=83=EC=9D=B4=EB=A9=B4=20?= =?UTF-8?q?=EA=B1=B4=EB=84=88=EB=9C=80=20=C2=B7=20robocopy=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20(193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 편의 (관리자 지시): 기존 사용자에게 동작이 바뀐 것을 '이번 업데이트로 바뀐 기본 설정' 창에 Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 2 ++ assets/l10n/ja.json | 2 ++ assets/l10n/zh-Hans.json | 2 ++ lib/app/settings.dart | 14 ++++++++++++++ lib/ui/migration_notice.dart | 4 ++++ test/settings_store_test.dart | 12 ++++++++++++ 6 files changed, 36 insertions(+) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 8dcb56b..bc34fb3 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -281,6 +281,7 @@ "lsync (실시간 동기화)": "lsync (live sync)", "lsync 로 이동": "Move to lsync", "quit 시작": "quit started", + "robocopy 복사 · 이동: 원본 안의 링크 · junction 은 따라가지 않고 건너뜁니다 (/XJ - 예전에는 링크가 가리키는 바깥 폴더의 파일까지 복사 · 이동했습니다). 링크째 복사하려면 robocopy 옵션 칸에 /SJ 를 넣으세요": "robocopy copy · move: links and junctions inside the source are no longer followed and are skipped (/XJ - before, files in the outside folder a link pointed to were copied or moved too). To copy links as links, add /SJ to the robocopy options", "robocopy 옵션": "robocopy options", "robocopy 종료 코드 {0}": "robocopy exit code {0}", "rsync --delete · robocopy /PURGE. 끄면 대상에 더하기 · 바꾸기만": "rsync --delete · robocopy /PURGE. When off, only adds · updates in the target", @@ -994,6 +995,7 @@ "복사 · 이동 {0}개": "{0} copy · move", "복사 · 이동 {0}개가 아직 끝나지 않았습니다. 끝내면 멈추고, 복사하던 파일은 완성되지 않은 채로 남지 않게 지웁니다.": "{0} copy/move tasks have not finished. Exiting stops them, and the file being copied is removed so no incomplete file is left.", "복사 · 이동 중입니다": "Copying · moving in progress", + "복사 · 이동: 받는 쪽 파일이 더 새것이면 건너뜁니다 (예전에는 예전 원본으로 덮어쓰고, 이동이면 원본도 지웠습니다). 건너뛴 파일은 원본에 남고 끝 알림에 그 수가 나옵니다. 되돌리기: 환경 설정 > 파일 탐색기 (또는 Rsync) > 받는 쪽이 더 새것이면 건너뛰기": "Copy · move: files are skipped when the receiving side is newer (before, the older source overwrote them, and a move also deleted the source). Skipped files stay in the source and their number appears in the finish notice. To change back: Settings > File explorer (or Rsync) > Skip when the receiving side is newer", "복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).": "Copy · move works with two panes (Pane layout > Two panes).", "복사 · 이동할 폴더에 같은 이름이 있으면 (현재 방식 · WebDAV)": "If the destination folder already has the same name (built-in method · WebDAV)", "복사 · 이동할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "Choose items to copy · move (tap, or mark with the circle on the right).", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 641f3d6..51465bf 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -281,6 +281,7 @@ "lsync (실시간 동기화)": "lsync (リアルタイム同期)", "lsync 로 이동": "lsync へ移動", "quit 시작": "終了開始", + "robocopy 복사 · 이동: 원본 안의 링크 · junction 은 따라가지 않고 건너뜁니다 (/XJ - 예전에는 링크가 가리키는 바깥 폴더의 파일까지 복사 · 이동했습니다). 링크째 복사하려면 robocopy 옵션 칸에 /SJ 를 넣으세요": "robocopy のコピー · 移動: 元の中のリンク · ジャンクションはたどらずにスキップします (/XJ - 以前はリンク先の外のフォルダーのファイルまでコピー · 移動していました)。リンクのままコピーするには robocopy オプション欄に /SJ を入れてください", "robocopy 옵션": "robocopy オプション", "robocopy 종료 코드 {0}": "robocopy 終了コード {0}", "rsync --delete · robocopy /PURGE. 끄면 대상에 더하기 · 바꾸기만": "rsync --delete · robocopy /PURGE。オフなら対象に追加 · 更新のみ", @@ -994,6 +995,7 @@ "복사 · 이동 {0}개": "コピー・移動 {0} 件", "복사 · 이동 {0}개가 아직 끝나지 않았습니다. 끝내면 멈추고, 복사하던 파일은 완성되지 않은 채로 남지 않게 지웁니다.": "コピー・移動 {0} 件がまだ終わっていません。終了すると止まり、コピー中のファイルは未完成のまま残らないよう削除します。", "복사 · 이동 중입니다": "コピー・移動中です", + "복사 · 이동: 받는 쪽 파일이 더 새것이면 건너뜁니다 (예전에는 예전 원본으로 덮어쓰고, 이동이면 원본도 지웠습니다). 건너뛴 파일은 원본에 남고 끝 알림에 그 수가 나옵니다. 되돌리기: 환경 설정 > 파일 탐색기 (또는 Rsync) > 받는 쪽이 더 새것이면 건너뛰기": "コピー · 移動: 受け取る側のファイルが新しければスキップします (以前は古い元で上書きし、移動なら元も削除していました)。スキップしたファイルは元に残り、終了の通知にその数が出ます。戻すには: 環境設定 > ファイルエクスプローラー (または Rsync) > 受け取る側が新しければスキップ", "복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).": "コピー · 移動は 2 ペインで使います (ペインの配置 > 2 ペイン)。", "복사 · 이동할 폴더에 같은 이름이 있으면 (현재 방식 · WebDAV)": "コピー・移動先のフォルダーに同じ名前がある場合 (現在の方式・WebDAV)", "복사 · 이동할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "コピー · 移動する項目を選んでください (タップ、または右の丸でマーク)。", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 36a8462..452b72b 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -281,6 +281,7 @@ "lsync (실시간 동기화)": "lsync (实时同步)", "lsync 로 이동": "移到 lsync", "quit 시작": "开始退出", + "robocopy 복사 · 이동: 원본 안의 링크 · junction 은 따라가지 않고 건너뜁니다 (/XJ - 예전에는 링크가 가리키는 바깥 폴더의 파일까지 복사 · 이동했습니다). 링크째 복사하려면 robocopy 옵션 칸에 /SJ 를 넣으세요": "robocopy 复制 · 移动:源中的链接 · 联接不再跟随,而是跳过(/XJ - 以前会连链接指向的外部文件夹中的文件一起复制 · 移动)。要按链接本身复制,请在 robocopy 选项栏中加入 /SJ", "robocopy 옵션": "robocopy 选项", "robocopy 종료 코드 {0}": "robocopy 退出代码 {0}", "rsync --delete · robocopy /PURGE. 끄면 대상에 더하기 · 바꾸기만": "rsync --delete · robocopy /PURGE。关闭时只在目标中添加 · 更新", @@ -994,6 +995,7 @@ "복사 · 이동 {0}개": "复制 · 移动 {0} 个", "복사 · 이동 {0}개가 아직 끝나지 않았습니다. 끝내면 멈추고, 복사하던 파일은 완성되지 않은 채로 남지 않게 지웁니다.": "{0} 个复制 · 移动尚未完成。退出会停止它们,并删除正在复制的文件,以免留下不完整的文件。", "복사 · 이동 중입니다": "正在复制 · 移动", + "복사 · 이동: 받는 쪽 파일이 더 새것이면 건너뜁니다 (예전에는 예전 원본으로 덮어쓰고, 이동이면 원본도 지웠습니다). 건너뛴 파일은 원본에 남고 끝 알림에 그 수가 나옵니다. 되돌리기: 환경 설정 > 파일 탐색기 (또는 Rsync) > 받는 쪽이 더 새것이면 건너뛰기": "复制 · 移动:接收端文件较新时跳过(以前会用旧的源文件覆盖,移动时还会删除源)。跳过的文件留在源中,完成通知中会显示数量。恢复:设置 > 文件浏览器(或 Rsync)> 接收端较新时跳过", "복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).": "复制 · 移动需在双窗格中使用 (窗格布局 > 双窗格)。", "복사 · 이동할 폴더에 같은 이름이 있으면 (현재 방식 · WebDAV)": "目标文件夹中已有同名项目时 (当前方式 · WebDAV)", "복사 · 이동할 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "请选择要复制 · 移动的项目 (点按,或用右侧圆圈标记)。", diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 079b064..f6c48a3 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -692,6 +692,7 @@ class AppSettings { 'rsyncPaths': rsyncPaths, 'components': components, 'aiImageV1': true, + 'robocopyLinksV1': true, 'navOrder': navOrder, 'swipeNav': swipeNav, 'imageExts': imageExts, @@ -1043,8 +1044,21 @@ class AppSettings { if (j['zipComicV2'] != true && j['zipComic'] != false && j.containsKey('imageExts') && !s.zipComic) 'zipComic', if (j['backgroundV2'] != true && j['runInBackground'] == false && s.runInBackground && Platform.isAndroid) 'background', + // 193 · 229: 예전 판을 쓰던 사람 (설정 파일이 있는데 이 항목이 없음) 에게 복사 · 이동이 받는 쪽의 더 새 파일을 건너뛰게 바뀐 것을 한 번 + if (j.isNotEmpty && !j.containsKey('copySkipNewer')) 'copySkipNewer', + // 193: robocopy 를 쓰던 사람 (Windows) 에게 링크 · junction 을 따라가지 않게 (/XJ) 바뀐 것을 한 번 + if (j['robocopyLinksV1'] != true && Platform.isWindows && _usedRobocopy(j)) 'robocopyLinks', ]; + /// 193: 예전 설정에서 robocopy 를 쓰고 있었는지 (복사 방법 · 기억한 복사 · 실시간 동기화 쌍) + static bool _usedRobocopy(Map j) { + bool robo(Object? x) => x is Map && x['method'] == 'robocopy'; + return j['copyMethodFile'] == 'robocopy' || + j['copyMethodFolder'] == 'robocopy' || + ((j['copyTasks'] as List?) ?? const []).any(robo) || + ((j['liveSyncPairs'] as List?) ?? const []).any(robo); + } + /// 다운로드 기본 위치: 프로그램 폴더 (쓸 수 없으면 사용자 다운로드 폴더) String resolvedDownloadRoot() { if (downloadRoot != null && downloadRoot!.isNotEmpty) return downloadRoot!; diff --git a/lib/ui/migration_notice.dart b/lib/ui/migration_notice.dart index ea73dd8..a5d950f 100644 --- a/lib/ui/migration_notice.dart +++ b/lib/ui/migration_notice.dart @@ -24,6 +24,10 @@ Future showMigrationNotice(BuildContext context, AppController c) async { 'zipComic' => tr('ZIP · CBZ 두 번 누르기: 만화 보기 → 목록. 되돌리기: 환경 설정 > 파일 탐색기 > ZIP · CBZ 를 만화로 보기'), 'background' => tr('백그라운드로 실행: 꺼짐 → 켜짐 (← 나 최근 앱에서 밀어도 작업이 계속됨). 되돌리기: 환경 설정 > Rsync > 백그라운드로 실행'), + 'copySkipNewer' => tr('복사 · 이동: 받는 쪽 파일이 더 새것이면 건너뜁니다 (예전에는 예전 원본으로 덮어쓰고, 이동이면 원본도 지웠습니다). ' + '건너뛴 파일은 원본에 남고 끝 알림에 그 수가 나옵니다. 되돌리기: 환경 설정 > 파일 탐색기 (또는 Rsync) > 받는 쪽이 더 새것이면 건너뛰기'), + 'robocopyLinks' => tr('robocopy 복사 · 이동: 원본 안의 링크 · junction 은 따라가지 않고 건너뜁니다 (/XJ - 예전에는 링크가 가리키는 바깥 폴더의 파일까지 ' + '복사 · 이동했습니다). 링크째 복사하려면 robocopy 옵션 칸에 /SJ 를 넣으세요'), _ => id, }; final open = await showDialog( diff --git a/test/settings_store_test.dart b/test/settings_store_test.dart index 4713f26..9e95a75 100644 --- a/test/settings_store_test.dart +++ b/test/settings_store_test.dart @@ -84,4 +84,16 @@ void main() { // 처음 설치 (빈 설정) 는 알릴 것 없음 expect(AppSettings.fromJson({}).migrated, isEmpty); }); + + test('193: 예전 판에서 올라오면 바뀐 동작 (229 받는 쪽이 더 새것이면 건너뜀 · robocopy 링크) 을 한 번 알린다', () { + final old = AppSettings.fromJson({'copyMethodFolder': 'robocopy', 'explorerV2': true}); + expect(old.migrated, contains('copySkipNewer')); + expect(old.migrated.contains('robocopyLinks'), Platform.isWindows, reason: 'robocopy 는 Windows 만'); + // 저장 뒤 다시 읽으면 알리지 않음 + final again = AppSettings.fromJson(old.toJson()); + expect(again.migrated, isNot(contains('copySkipNewer'))); + expect(again.migrated, isNot(contains('robocopyLinks'))); + // robocopy 를 쓰지 않던 사람에게는 링크 알림 없음 + expect(AppSettings.fromJson({'explorerV2': true}).migrated, isNot(contains('robocopyLinks'))); + }); } From 6cb2dde0891f1ee9ac3a7bc057d82ca579d30480 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:10:19 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=EA=B7=B8=20=EC=84=A4=EC=A0=95=EC=9D=B4=20?= =?UTF-8?q?=EC=97=86=EB=8D=98=20=EC=98=88=EC=A0=84=20=ED=8C=90=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=98=AC=EB=9D=BC=EC=98=A4=EB=A9=B4=20=ED=83=90?= =?UTF-8?q?=EC=83=89=EA=B8=B0=20=ED=95=9C=20=EB=B2=88=20=EB=88=84=EB=A5=B4?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20=ED=99=94=EB=A9=B4=20=EB=B0=A9=ED=96=A5=20?= =?UTF-8?q?=C2=B7=20=EC=A2=8C=EC=9A=B0=20=EB=B0=80=EA=B8=B0=20=C2=B7=20?= =?UTF-8?q?=EB=B0=B1=EA=B7=B8=EB=9D=BC=EC=9A=B4=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=ED=95=9C=20=EB=B2=88=20=EC=95=8C=EB=A6=BC=20(193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 편의 (기기 확인, P11 10.03 → 10.10): 한 번 눌렀는데 바로 열려 놀람 - 예전 판에는 키가 없어 알림 조건에 걸리지 않았다. 처음 설치는 알리지 않음 Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 2 ++ assets/l10n/ja.json | 2 ++ assets/l10n/zh-Hans.json | 2 ++ lib/app/settings.dart | 6 ++++++ lib/ui/migration_notice.dart | 4 ++++ test/settings_store_test.dart | 15 +++++++++++++++ 6 files changed, 31 insertions(+) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index bc34fb3..5827bf9 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -1937,6 +1937,7 @@ "파일 탐색기 두 창 배치: 화면 모양 따라 → 좌우. 되돌리기: 파일 탐색기의 [좌우 ⇆ / 위아래 ⇅] 버튼 또는 창 배치": "File explorer two-pane layout: follow screen shape → side by side. To revert: the [Side by side ⇆ / Top and bottom ⇅] button in the file explorer, or Pane layout", "파일 탐색기 위쪽 ⋮ 메뉴에서도 바꿀 수 있습니다 (좌우 · 위아래 · 버튼 줄 위치 · 버튼 구성)": "You can also change it from the ⋮ menu at the top of the file explorer (side by side · top and bottom · button bar position · buttons)", "파일 탐색기 창: 두 창 → 화면 크기 따라 (폰 세로는 한 창). 되돌리기: 환경 설정 > 파일 탐색기 > 창 배치": "File explorer panes: two panes → by screen size (one pane on a portrait phone). To revert: Settings > File explorer > Pane layout", + "파일 탐색기: 파일을 한 번 누르면 바로 엽니다 (고르기 · 메뉴는 길게 누르기). 바꾸기: 환경 설정 > 파일 탐색기 > 누르기": "File explorer: a single tap now opens a file right away (long-press to select or for the menu). To change: Settings > File explorer > Tap", "파일 폴더": "File folder", "파일 확인 중…": "Checking files…", "파일만 골랐을 때": "When only files are selected", @@ -2066,6 +2067,7 @@ "화면 · 색 보정": "Frame · color", "화면 가득 (시스템 막대 숨기기)": "Full screen (hide system bars)", "화면 가운데를 왼쪽으로 밀면 다음 화면, 오른쪽으로 밀면 이전 화면 (마지막 다음은 처음으로).": "Swipe left in the middle of the screen for the next screen, right for the previous one (after the last comes the first).", + "화면 가운데를 좌우로 밀면 다른 화면 (파일 탐색기 · 다운로드 등) 으로 넘어갑니다. 끄기: 환경 설정 > 컴포넌트 > 좌우로 밀어 화면 이동": "Swiping left or right in the middle of the screen now switches to other screens (file explorer, downloads, etc.). To turn off: Settings > Components > Swipe left/right to switch screens", "화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "Translates all of the program's text on this device. It may take a few minutes.", "화면 모양 따라": "Follow screen shape", "화면 방향": "Screen orientation", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 51465bf..92e2e67 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -1937,6 +1937,7 @@ "파일 탐색기 두 창 배치: 화면 모양 따라 → 좌우. 되돌리기: 파일 탐색기의 [좌우 ⇆ / 위아래 ⇅] 버튼 또는 창 배치": "ファイルエクスプローラーの 2 画面配置: 画面の形に合わせる → 左右。元に戻す: ファイルエクスプローラーの [左右 ⇆ / 上下 ⇅] ボタンまたは画面配置", "파일 탐색기 위쪽 ⋮ 메뉴에서도 바꿀 수 있습니다 (좌우 · 위아래 · 버튼 줄 위치 · 버튼 구성)": "ファイルエクスプローラー上部の ⋮ メニューからも変えられます (左右 · 上下 · ボタン列の位置 · ボタン構成)", "파일 탐색기 창: 두 창 → 화면 크기 따라 (폰 세로는 한 창). 되돌리기: 환경 설정 > 파일 탐색기 > 창 배치": "ファイルエクスプローラーの画面: 2 画面 → 画面サイズに合わせる (縦向きのスマホは 1 画面)。元に戻す: 環境設定 > ファイルエクスプローラー > 画面配置", + "파일 탐색기: 파일을 한 번 누르면 바로 엽니다 (고르기 · 메뉴는 길게 누르기). 바꾸기: 환경 설정 > 파일 탐색기 > 누르기": "ファイルエクスプローラー: ファイルを 1 回タップするとすぐ開きます (選択 · メニューは長押し)。変更: 環境設定 > ファイルエクスプローラー > タップ", "파일 폴더": "ファイル フォルダー", "파일 확인 중…": "ファイルを確認中…", "파일만 골랐을 때": "ファイルだけを選んだとき", @@ -2066,6 +2067,7 @@ "화면 · 색 보정": "画面・カラー補正", "화면 가득 (시스템 막대 숨기기)": "全画面 (システムバーを隠す)", "화면 가운데를 왼쪽으로 밀면 다음 화면, 오른쪽으로 밀면 이전 화면 (마지막 다음은 처음으로).": "画面中央を左へスワイプで次の画面、右へで前の画面 (最後の次は最初へ)。", + "화면 가운데를 좌우로 밀면 다른 화면 (파일 탐색기 · 다운로드 등) 으로 넘어갑니다. 끄기: 환경 설정 > 컴포넌트 > 좌우로 밀어 화면 이동": "画面の中央を左右にスワイプすると別の画面 (ファイルエクスプローラー · ダウンロードなど) に切り替わります。オフにする: 環境設定 > コンポーネント > 左右スワイプで画面を移動", "화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "画面の文字をすべてこの端末で翻訳します。数分かかることがあります。", "화면 모양 따라": "画面の形に合わせる", "화면 방향": "画面の向き", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 452b72b..6c7cac3 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -1937,6 +1937,7 @@ "파일 탐색기 두 창 배치: 화면 모양 따라 → 좌우. 되돌리기: 파일 탐색기의 [좌우 ⇆ / 위아래 ⇅] 버튼 또는 창 배치": "文件浏览器双窗格布局: 跟随屏幕形状 → 左右。恢复: 文件浏览器中的 [左右 ⇆ / 上下 ⇅] 按钮或窗格布局", "파일 탐색기 위쪽 ⋮ 메뉴에서도 바꿀 수 있습니다 (좌우 · 위아래 · 버튼 줄 위치 · 버튼 구성)": "也可以在文件浏览器顶部的 ⋮ 菜单中更改 (左右 · 上下 · 按钮栏位置 · 按钮配置)", "파일 탐색기 창: 두 창 → 화면 크기 따라 (폰 세로는 한 창). 되돌리기: 환경 설정 > 파일 탐색기 > 창 배치": "文件浏览器窗格: 双窗格 → 按屏幕大小 (竖屏手机为单窗格)。恢复: 设置 > 文件浏览器 > 窗格布局", + "파일 탐색기: 파일을 한 번 누르면 바로 엽니다 (고르기 · 메뉴는 길게 누르기). 바꾸기: 환경 설정 > 파일 탐색기 > 누르기": "文件浏览器:单击文件会立即打开(选择 · 菜单请长按)。更改:设置 > 文件浏览器 > 点按", "파일 폴더": "文件夹", "파일 확인 중…": "正在检查文件…", "파일만 골랐을 때": "只选择文件时", @@ -2066,6 +2067,7 @@ "화면 · 색 보정": "画面 · 色彩", "화면 가득 (시스템 막대 숨기기)": "全屏 (隐藏系统栏)", "화면 가운데를 왼쪽으로 밀면 다음 화면, 오른쪽으로 밀면 이전 화면 (마지막 다음은 처음으로).": "在屏幕中间向左滑动进入下一个界面,向右滑动返回上一个界面 (最后一个之后回到第一个)。", + "화면 가운데를 좌우로 밀면 다른 화면 (파일 탐색기 · 다운로드 등) 으로 넘어갑니다. 끄기: 환경 설정 > 컴포넌트 > 좌우로 밀어 화면 이동": "在屏幕中间左右滑动会切换到其他屏幕(文件浏览器 · 下载等)。关闭:设置 > 组件 > 左右滑动切换界面", "화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "将在此设备上翻译所有界面文字。可能需要几分钟。", "화면 모양 따라": "跟随屏幕形状", "화면 방향": "屏幕方向", diff --git a/lib/app/settings.dart b/lib/app/settings.dart index f6c48a3..ad875a8 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -1044,6 +1044,12 @@ class AppSettings { if (j['zipComicV2'] != true && j['zipComic'] != false && j.containsKey('imageExts') && !s.zipComic) 'zipComic', if (j['backgroundV2'] != true && j['runInBackground'] == false && s.runInBackground && Platform.isAndroid) 'background', + // 193: 그 설정이 생기기 전 판 (설정 파일은 있는데 그 항목이 없음 - 예: 10.03) 에서 올라온 사람이 바로 겪는 동작 변화. + // 예전 판에는 키가 없어 위의 "예전 값 → 새 값" 조건에 걸리지 않았다 (P11: 탐색기에서 한 번 눌렀는데 바로 열려 놀람) + if (j.isNotEmpty && !j.containsKey('explorerClick') && s.explorerClick == 'open') 'explorerClickNew', + if (j.isNotEmpty && !j.containsKey('screenOrientation') && s.screenOrientation == 'auto' && Platform.isAndroid) 'orientation', + if (j.isNotEmpty && !j.containsKey('swipeNav') && s.swipeNav) 'swipeNav', + if (j.isNotEmpty && !j.containsKey('runInBackground') && s.runInBackground && Platform.isAndroid) 'background', // 193 · 229: 예전 판을 쓰던 사람 (설정 파일이 있는데 이 항목이 없음) 에게 복사 · 이동이 받는 쪽의 더 새 파일을 건너뛰게 바뀐 것을 한 번 if (j.isNotEmpty && !j.containsKey('copySkipNewer')) 'copySkipNewer', // 193: robocopy 를 쓰던 사람 (Windows) 에게 링크 · junction 을 따라가지 않게 (/XJ) 바뀐 것을 한 번 diff --git a/lib/ui/migration_notice.dart b/lib/ui/migration_notice.dart index a5d950f..4e9352e 100644 --- a/lib/ui/migration_notice.dart +++ b/lib/ui/migration_notice.dart @@ -24,6 +24,10 @@ Future showMigrationNotice(BuildContext context, AppController c) async { 'zipComic' => tr('ZIP · CBZ 두 번 누르기: 만화 보기 → 목록. 되돌리기: 환경 설정 > 파일 탐색기 > ZIP · CBZ 를 만화로 보기'), 'background' => tr('백그라운드로 실행: 꺼짐 → 켜짐 (← 나 최근 앱에서 밀어도 작업이 계속됨). 되돌리기: 환경 설정 > Rsync > 백그라운드로 실행'), + 'explorerClickNew' => tr('파일 탐색기: 파일을 한 번 누르면 바로 엽니다 (고르기 · 메뉴는 길게 누르기). ' + '바꾸기: 환경 설정 > 파일 탐색기 > 누르기'), + 'swipeNav' => tr('화면 가운데를 좌우로 밀면 다른 화면 (파일 탐색기 · 다운로드 등) 으로 넘어갑니다. ' + '끄기: 환경 설정 > 컴포넌트 > 좌우로 밀어 화면 이동'), 'copySkipNewer' => tr('복사 · 이동: 받는 쪽 파일이 더 새것이면 건너뜁니다 (예전에는 예전 원본으로 덮어쓰고, 이동이면 원본도 지웠습니다). ' '건너뛴 파일은 원본에 남고 끝 알림에 그 수가 나옵니다. 되돌리기: 환경 설정 > 파일 탐색기 (또는 Rsync) > 받는 쪽이 더 새것이면 건너뛰기'), 'robocopyLinks' => tr('robocopy 복사 · 이동: 원본 안의 링크 · junction 은 따라가지 않고 건너뜁니다 (/XJ - 예전에는 링크가 가리키는 바깥 폴더의 파일까지 ' diff --git a/test/settings_store_test.dart b/test/settings_store_test.dart index 9e95a75..5b57b8b 100644 --- a/test/settings_store_test.dart +++ b/test/settings_store_test.dart @@ -96,4 +96,19 @@ void main() { // robocopy 를 쓰지 않던 사람에게는 링크 알림 없음 expect(AppSettings.fromJson({'explorerV2': true}).migrated, isNot(contains('robocopyLinks'))); }); + + test('193: 그 설정이 없던 예전 판 (10.03 모양 - 키 없음) 에서 올라오면 바로 겪는 동작 변화를 알린다 · 처음 설치는 없음', () { + // 10.03 의 설정 파일: 탐색기 · 화면 이동 · 백그라운드 설정이 아직 없던 때 (있는 것은 그때의 항목들) + final old = AppSettings.fromJson({'theme': 'dark', 'outputFolderName': 'jj_mkv'}); + expect(old.migrated, contains('swipeNav')); + if (Platform.isAndroid) { + expect(old.migrated, containsAll(['explorerClickNew', 'orientation', 'background'])); + } else { + expect(old.migrated, isNot(contains('explorerClickNew')), reason: 'PC 는 처음 값이 한 번 누르면 고르기 (바뀐 것 없음)'); + } + // 저장 뒤 다시 읽으면 알리지 않음 + expect(AppSettings.fromJson(old.toJson()).migrated, isEmpty); + // 처음 설치 (설정 파일 없음) 는 알릴 것 없음 + expect(AppSettings.fromJson({}).migrated, isEmpty); + }); } From 65c049d0437caa76aeb3874355a6e00e59ba9848 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:14:57 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=EC=9E=91=EC=97=85=20=EA=B8=B0=EB=A1=9D?= =?UTF-8?q?=EC=9D=98=20=EA=B2=BD=EB=A1=9C=EB=8A=94=20=EA=B8=B0=EA=B8=B0=20?= =?UTF-8?q?=EA=B5=AC=EB=B6=84=EC=9E=90=EB=A1=9C=20=C2=B7=20AI=20=EC=98=88?= =?UTF-8?q?=EC=83=81=20=EC=8B=9C=EA=B0=84=20'=EC=95=BD=201~1=EB=B6=84'=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8=20(196=20=C2=B7=20197)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 196 (버그): Android 에서도 'jj_mkv\이름' - p.join - 197 (버그): 짧은 동영상의 '약 1~1분' → '1분 안', 같은 수면 하나 Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 8 +++++--- assets/l10n/ja.json | 8 +++++--- assets/l10n/zh-Hans.json | 8 +++++--- lib/app/app_controller.dart | 5 +++-- lib/ui/ai_dialog.dart | 12 ++++++++++-- test/ai_confirm_test.dart | 4 ++++ 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 5827bf9..35bd94b 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -65,6 +65,7 @@ "0 으로": "Reset to 0", "10초 뒤로 (←)": "Back 10 seconds (←)", "10초 앞으로 (→)": "Forward 10 seconds (→)", + "1분 안": "under 1 min", "5초 뒤로": "Back 5 seconds", "5초 앞으로": "Forward 5 seconds", "90° 회전 (시계 방향)": "Rotate 90° (clockwise)", @@ -102,7 +103,7 @@ "AI 자막 만들기{0}{1}": "Make AI subtitles{0}{1}", "AI 자막 시작: {0}": "AI subtitles started: {0}", "AI 자막 실패: {0}\n{1}": "AI subtitles failed: {0}\n{1}", - "AI 자막 완료: {0} → {1}\\{2}_AI.srt 외 {3}개": "AI subtitles done: {0} → {1}\\{2}_AI.srt and {3} more", + "AI 자막 완료: {0} → {1} 외 {2}개": "AI subtitles done: {0} → {1} and {2} more", "AI 자막 중단: {0}": "AI subtitles stopped: {0}", "AI 자막 취소: {0}": "AI subtitles cancelled: {0}", "AI 자막: {0}": "AI subtitles: {0}", @@ -320,6 +321,7 @@ "yt-dlp 의 YouTube 화질 목록 추출": "Extracts the YouTube quality list for yt-dlp", "{0} (AI 번역 ← {1})": "{0} (AI translation ← {1})", "{0} (기본): {1}": "{0} (default): {1}", + "{0} (동영상 {1})": "{0} (videos: {1})", "{0} - {1} (약 {2}MB)": "{0} - {1} (about {2}MB)", "{0} / {1}장": "{0} / {1} images", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2} (change at the top of the main screen)", @@ -1176,7 +1178,7 @@ "시작 · 웹 브라우저": "Startup · web browser", "시작 메뉴에 등록": "Add to Start menu", "시작 안 함 (모니터링에서 시작)": "Don't start (start from Monitor)", - "시작: {0} → {1}\\{2}{3}": "Started: {0} → {1}\\{2}{3}", + "시작: {0} → {1}{2}": "Started: {0} → {1}{2}", "시작하는 중…": "Starting…", "시작하지 못했습니다: {0}": "Could not start: {0}", "시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).": "Choose which syncs to start. The others stay paused for this session (start them in Monitor > lsync).", @@ -1262,7 +1264,7 @@ "앱이 켜져 있는 동안 늘 동기화합니다.": "Always syncs while the app is running.", "약 {0} 남음": "about {0} left", "약 {0}~{1}분": "about {0}–{1} min", - "약 {0}~{1}분 (동영상 {2})": "about {0}–{1} min (videos: {2})", + "약 {0}분": "about {0} min", "양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).": "Both ways (⇄) never deletes source files (--remove-source-files in the options is not used).", "양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "Both ways: newer files on the receiving side are not overwritten (-u).", "어둡게": "Darker", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 92e2e67..2dea2f0 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -65,6 +65,7 @@ "0 으로": "0 に戻す", "10초 뒤로 (←)": "10秒戻る(←)", "10초 앞으로 (→)": "10秒進む(→)", + "1분 안": "1 分以内", "5초 뒤로": "5秒戻る", "5초 앞으로": "5秒進む", "90° 회전 (시계 방향)": "90° 回転 (時計回り)", @@ -102,7 +103,7 @@ "AI 자막 만들기{0}{1}": "AI 字幕を作成{0}{1}", "AI 자막 시작: {0}": "AI 字幕開始: {0}", "AI 자막 실패: {0}\n{1}": "AI 字幕失敗: {0}\n{1}", - "AI 자막 완료: {0} → {1}\\{2}_AI.srt 외 {3}개": "AI 字幕完了: {0} → {1}\\{2}_AI.srt ほか {3}個", + "AI 자막 완료: {0} → {1} 외 {2}개": "AI 字幕完了: {0} → {1} ほか {2}個", "AI 자막 중단: {0}": "AI 字幕中断: {0}", "AI 자막 취소: {0}": "AI 字幕キャンセル: {0}", "AI 자막: {0}": "AI 字幕: {0}", @@ -320,6 +321,7 @@ "yt-dlp 의 YouTube 화질 목록 추출": "yt-dlp 用の YouTube 画質リストを取得", "{0} (AI 번역 ← {1})": "{0}(AI 翻訳 ← {1})", "{0} (기본): {1}": "{0}(既定): {1}", + "{0} (동영상 {1})": "{0} (動画 {1})", "{0} - {1} (약 {2}MB)": "{0} - {1}(約 {2}MB)", "{0} / {1}장": "{0} / {1} 枚", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)", @@ -1176,7 +1178,7 @@ "시작 · 웹 브라우저": "起動・ウェブブラウザー", "시작 메뉴에 등록": "スタートメニューに登録", "시작 안 함 (모니터링에서 시작)": "開始しない (モニタリングで開始)", - "시작: {0} → {1}\\{2}{3}": "開始: {0} → {1}\\{2}{3}", + "시작: {0} → {1}{2}": "開始: {0} → {1}{2}", "시작하는 중…": "開始中…", "시작하지 못했습니다: {0}": "開始できませんでした: {0}", "시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).": "開始する同期を選んでください。選ばなかったものは今回の実行中は停止します (モニタリング > lsync で開始)。", @@ -1262,7 +1264,7 @@ "앱이 켜져 있는 동안 늘 동기화합니다.": "アプリの起動中は常に同期します。", "약 {0} 남음": "残り約 {0}", "약 {0}~{1}분": "約 {0}~{1} 分", - "약 {0}~{1}분 (동영상 {2})": "約 {0}~{1} 分 (動画 {2})", + "약 {0}분": "約 {0} 分", "양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).": "双方向 (⇄) では元のファイルを削除しません (オプションの --remove-source-files は使いません)。", "양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "双方向: 受け取る側の方が新しいファイルは上書きしません (-u)。", "어둡게": "暗く", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 6c7cac3..6cc35a9 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -65,6 +65,7 @@ "0 으로": "归零", "10초 뒤로 (←)": "后退 10 秒(←)", "10초 앞으로 (→)": "前进 10 秒(→)", + "1분 안": "1 分钟以内", "5초 뒤로": "后退 5 秒", "5초 앞으로": "前进 5 秒", "90° 회전 (시계 방향)": "旋转 90° (顺时针)", @@ -102,7 +103,7 @@ "AI 자막 만들기{0}{1}": "生成 AI 字幕{0}{1}", "AI 자막 시작: {0}": "AI 字幕开始: {0}", "AI 자막 실패: {0}\n{1}": "AI 字幕失败: {0}\n{1}", - "AI 자막 완료: {0} → {1}\\{2}_AI.srt 외 {3}개": "AI 字幕完成: {0} → {1}\\{2}_AI.srt 等 {3} 个", + "AI 자막 완료: {0} → {1} 외 {2}개": "AI 字幕完成: {0} → {1} 等 {2} 个", "AI 자막 중단: {0}": "AI 字幕中断: {0}", "AI 자막 취소: {0}": "AI 字幕已取消: {0}", "AI 자막: {0}": "AI 字幕: {0}", @@ -320,6 +321,7 @@ "yt-dlp 의 YouTube 화질 목록 추출": "提取 yt-dlp 的 YouTube 画质列表", "{0} (AI 번역 ← {1})": "{0}(AI 翻译 ← {1})", "{0} (기본): {1}": "{0}(默认): {1}", + "{0} (동영상 {1})": "{0}(视频 {1})", "{0} - {1} (약 {2}MB)": "{0} - {1}(约 {2}MB)", "{0} / {1}장": "{0} / {1} 张", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)", @@ -1176,7 +1178,7 @@ "시작 · 웹 브라우저": "启动 · 网页浏览器", "시작 메뉴에 등록": "添加到开始菜单", "시작 안 함 (모니터링에서 시작)": "不开始 (在监控中开始)", - "시작: {0} → {1}\\{2}{3}": "开始: {0} → {1}\\{2}{3}", + "시작: {0} → {1}{2}": "开始: {0} → {1}{2}", "시작하는 중…": "正在开始…", "시작하지 못했습니다: {0}": "无法开始:{0}", "시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).": "请选择要开始的同步。未选的在本次运行期间暂停 (可在 监控 > lsync 中开始)。", @@ -1262,7 +1264,7 @@ "앱이 켜져 있는 동안 늘 동기화합니다.": "应用运行期间始终同步。", "약 {0} 남음": "约剩 {0}", "약 {0}~{1}분": "约 {0}~{1} 分钟", - "약 {0}~{1}분 (동영상 {2})": "约 {0}~{1} 分钟(视频 {2})", + "약 {0}분": "约 {0} 分钟", "양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).": "双向 (⇄) 不会删除源文件(不使用选项中的 --remove-source-files)。", "양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "双向: 接收端较新的文件不会被覆盖 (-u)。", "어둡게": "调暗", diff --git a/lib/app/app_controller.dart b/lib/app/app_controller.dart index 19f991c..5b820cd 100644 --- a/lib/app/app_controller.dart +++ b/lib/app/app_controller.dart @@ -990,7 +990,8 @@ class AppController extends ChangeNotifier { ..status = JobStatus.ready ..phase = null ..progress = 0; - _log(trf('AI 자막 완료: {0} → {1}\\{2}_AI.srt 외 {3}개', [v.fileName, outputFolderName, v.baseName, langs.length])); + // 196: 경로는 기기의 구분자로 (Android 에서도 "jj_mkv\이름" 으로 보였다) + _log(trf('AI 자막 완료: {0} → {1} 외 {2}개', [v.fileName, p.join(outputFolderName, '${v.baseName}_AI.srt'), langs.length])); } on AiCancelled { v ..status = JobStatus.ready @@ -1388,7 +1389,7 @@ class AppController extends ChangeNotifier { ..progress = 0 ..message = null; notifyListeners(); - _log(trf('시작: {0} → {1}\\{2}' '{3}', [v.fileName, outputFolderName, p.basename(out), encode.reencode ? ' [${encode.codec.label} · ${encode.resolution.label} · ${encode.quality.label}${encode.adjusts ? ' · ${encode.adjustSummary}' : ''}]' : ''])); + _log(trf('시작: {0} → {1}{2}', [v.fileName, p.join(outputFolderName, p.basename(out)), encode.reencode ? ' [${encode.codec.label} · ${encode.resolution.label} · ${encode.quality.label}${encode.adjusts ? ' · ${encode.adjustSummary}' : ''}]' : ''])); final copies = {}; var started = false; // FFmpeg 가 출력 파일을 쓰기 시작했는지 try { diff --git a/lib/ui/ai_dialog.dart b/lib/ui/ai_dialog.dart index 5d4bada..b629f8b 100644 --- a/lib/ui/ai_dialog.dart +++ b/lib/ui/ai_dialog.dart @@ -50,7 +50,7 @@ const aiStartDelay = Duration(seconds: 3); String aiStartNotice(List videos, AiOptions o, {required bool thenBuild}) { final known = videos.map((v) => v.info?.duration).whereType().toList(); final est = known.length == videos.length ? aiEstimateMinutes(known.fold(Duration.zero, (a, b) => a + b), aiTranslateCount(o)) : null; - final what = [trf('동영상 {0}개', [videos.length]), if (est != null) trf('약 {0}~{1}분', [est.$1, est.$2])].join(' · '); + final what = [trf('동영상 {0}개', [videos.length]), if (est != null) aiEstimateText(est)].join(' · '); return thenBuild ? trf('AI 자막 → MKV ({0}) 을 시작합니다', [what]) : trf('AI 자막 ({0}) 을 시작합니다', [what]); } @@ -64,6 +64,14 @@ const aiColor = Color(0xFFB388FF); return ((h * (15 + 10 * translateLanguages)).ceil(), (h * (30 + 20 * translateLanguages)).ceil()); } +/// 197: 예상 시간 글 - 짧은 동영상은 "약 1~1분" 처럼 같은 수가 두 번 보였다 +String aiEstimateText((int, int) est) { + final (lo, hi) = est; + if (hi <= 1) return tr('1분 안'); + if (lo == hi) return trf('약 {0}분', [lo]); + return trf('약 {0}~{1}분', [lo, hi]); +} + /// 번역할 언어 수 (원어와 같은 언어는 번역하지 않음, 원어 자동 감지면 하나는 원어로 본다) int aiTranslateCount(AiOptions o) => o.source == undetermined ? (o.targets.length - 1).clamp(0, 99) @@ -90,7 +98,7 @@ class AiPlanSummary extends StatelessWidget { tr('예상 시간'), est == null ? tr('동영상 길이를 몰라 셀 수 없습니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)') - : trf('약 {0}~{1}분 (동영상 {2})', [est.$1, est.$2, hm(total)]), + : trf('{0} (동영상 {1})', [aiEstimateText(est), hm(total)]), ), ]; return Container( diff --git a/test/ai_confirm_test.dart b/test/ai_confirm_test.dart index e09bc61..ae0a642 100644 --- a/test/ai_confirm_test.dart +++ b/test/ai_confirm_test.dart @@ -15,6 +15,10 @@ void main() { expect(aiEstimateMinutes(const Duration(hours: 1), 0), (15, 30)); expect(aiEstimateMinutes(const Duration(hours: 2), 2), (70, 140)); expect(aiEstimateMinutes(Duration.zero, 2), isNull); + // 197: 짧은 동영상은 '약 1~1분' 이 아니라 '1분 안', 같은 수면 하나만 + expect(aiEstimateText(aiEstimateMinutes(const Duration(seconds: 30), 0)!), '1분 안'); + expect(aiEstimateText((3, 3)), '약 3분'); + expect(aiEstimateText((15, 30)), '약 15~30분'); final ko = languageOf('ko'), en = languageOf('en'), ja = languageOf('ja'); expect(aiTranslateCount(AiOptions(source: ko, targets: {ko, en, ja}, whisper: whisperModels.first)), 2); expect(aiTranslateCount(AiOptions(targets: {ko, en, ja}, whisper: whisperModels.first)), 2, reason: '자동 감지면 하나는 원어로'); From 36bbee615d2e9a713d9eea05d2c4bdc33470ad18 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 12 Oct 2026 05:51:56 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=EC=9B=90=EC=96=B4=EB=A5=BC=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EA=B0=90=EC=A7=80=ED=96=88=EC=9C=BC=EB=A9=B4=20?= =?UTF-8?q?=EB=81=9D=EB=82=9C=20=EB=92=A4=20=EA=B0=90=EC=A7=80=ED=95=9C=20?= =?UTF-8?q?=EC=96=B8=EC=96=B4=20=C2=B7=20[=EB=8B=A4=EB=A5=B8=20=EC=96=B8?= =?UTF-8?q?=EC=96=B4=EB=A1=9C=20=EB=8B=A4=EC=8B=9C]=20(198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 편의 (기기 확인): 영어 음성을 한국어로 감지해 영어 발음을 한글로 적은 자막 - 막지는 못하므로 알리고 고른 원어로 다시 만들 수 있게 Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 5 ++++ assets/l10n/ja.json | 5 ++++ assets/l10n/zh-Hans.json | 5 ++++ lib/app/app_controller.dart | 2 ++ lib/core/models.dart | 3 ++ lib/ui/ai_dialog.dart | 56 +++++++++++++++++++++++++++++++++++-- test/ai_confirm_test.dart | 24 ++++++++++++++++ 7 files changed, 98 insertions(+), 2 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 35bd94b..e390eae 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -59,6 +59,7 @@ "(현재 방식은 옵션 없음)": "(no options for the built-in method)", "* 는 아무 글자, ? 는 한 글자. 여러 개는 ; 로 (예: *.srt; *.ass)": "* matches any text, ? one character. Separate several with ; (e.g. *.srt; *.ass)", "+ 여기에 새 줄": "+ New line here", + "- 감지한 언어": "- detected", "-1 은 아무렇게나": "-1 = random", ".{0} 재생 프로그램 (exe)": ".{0} player (exe)", "0 = 제한 없음. 모든 방법에 적용: rsync --bwlimit, 현재 방식은 앱이 조절, robocopy 는 /IPG 로 비슷하게. 옵션 칸에 직접 넣어도 됩니다 (그쪽이 우선)": "0 = unlimited. Applies to every method: rsync --bwlimit, the built-in method throttles itself, robocopy approximates with /IPG. You can also put it in the options field (that takes priority)", @@ -442,6 +443,7 @@ "가져온 즐겨찾기": "Imported bookmarks", "가져온 즐겨찾기 ({0})": "Imported bookmarks ({0})", "가져올 즐겨찾기가 없습니다.": "No bookmarks to import.", + "감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "Detected language: {0} - if it is wrong, make them again with another language", "같은 시리즈 (번호만 다른 파일)": "Same series (files that differ only by number)", "같은 이름이 이미 있습니다": "An item with the same name already exists", "같은 이름이 이미 있습니다: {0}": "Already exists with the same name: {0}", @@ -641,6 +643,7 @@ "다른 앱에서 동영상을 열 때": "When opening a video from another app", "다른 앱에서 열기: {0}": "Opened from another app: {0}", "다른 앱으로 열기": "Open with another app", + "다른 언어로 다시": "Redo with another language", "다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. 주소창 옆 번역 버튼으로 원문 · 번역을 바꿀 수 있습니다.": "Translates pages in other languages into the display language ({0}). The page text is sent to Google Translate. Use the translate button next to the address bar to switch between original · translation.", "다른 언어로 번역 (AI · 이 {0}에서)": "Translate to another language (AI · on this {0})", "다른 이름으로 저장": "Save as", @@ -1415,6 +1418,7 @@ "원어({0})와 같은 언어로는 번역하지 않습니다.": "Will not translate into the same language as the source ({0}).", "원어: {0} ({1})": "Source: {0} ({1})", "원어: 자동 감지 (글자로 추정)": "Source: auto-detect (guessed from the text)", + "원어를 골라 다시 만들기": "Choose the source language and redo", "원인: {0}": "Cause: {0}", "월": "Mon", "웹 브라우저": "Web browser", @@ -1849,6 +1853,7 @@ "처음 쓸 때 내려받기 (기본)": "Download on first use (default)", "처음 아이콘 (파랑)": "Original icon (blue)", "처음대로": "Reset to default", + "처음부터 정하려면 AI 자막 창의 \"원어 (영상 속 언어)\" 에서 고르세요.": "To set it from the start, choose it in \"Source language (spoken in the video)\" in the AI subtitles window.", "처음으로: {0}": "Back to the first: {0}", "첫 장입니다.": "This is the first page.", "첫 줄 추가": "Add first line", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 2dea2f0..1da1d0a 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -59,6 +59,7 @@ "(현재 방식은 옵션 없음)": "(現在の方式にはオプションなし)", "* 는 아무 글자, ? 는 한 글자. 여러 개는 ; 로 (예: *.srt; *.ass)": "* は任意の文字列、? は 1 文字。複数は ; で区切る (例: *.srt; *.ass)", "+ 여기에 새 줄": "+ ここに新しい行", + "- 감지한 언어": "- 検出した言語", "-1 은 아무렇게나": "-1 はランダム", ".{0} 재생 프로그램 (exe)": ".{0} 再生プログラム(exe)", "0 = 제한 없음. 모든 방법에 적용: rsync --bwlimit, 현재 방식은 앱이 조절, robocopy 는 /IPG 로 비슷하게. 옵션 칸에 직접 넣어도 됩니다 (그쪽이 우선)": "0 = 制限なし。すべての方法に適用: rsync は --bwlimit、現在の方式はアプリが調整、robocopy は /IPG で近似。オプション欄に直接入れても構いません (そちらが優先)", @@ -442,6 +443,7 @@ "가져온 즐겨찾기": "取り込んだブックマーク", "가져온 즐겨찾기 ({0})": "取り込んだブックマーク({0})", "가져올 즐겨찾기가 없습니다.": "取り込むブックマークがありません。", + "감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "検出した言語: {0} - 違っていれば別の言語で作り直してください", "같은 시리즈 (번호만 다른 파일)": "同じシリーズ(番号だけ違うファイル)", "같은 이름이 이미 있습니다": "同じ名前のものが既にあります", "같은 이름이 이미 있습니다: {0}": "同じ名前が既にあります: {0}", @@ -641,6 +643,7 @@ "다른 앱에서 동영상을 열 때": "他のアプリで動画を開いたとき", "다른 앱에서 열기: {0}": "他のアプリから開く: {0}", "다른 앱으로 열기": "別のアプリで開く", + "다른 언어로 다시": "別の言語でやり直す", "다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. 주소창 옆 번역 버튼으로 원문 · 번역을 바꿀 수 있습니다.": "ほかの言語のページを表示言語 ({0}) に翻訳します。ページの文章は Google 翻訳に送られます。アドレスバー横の翻訳ボタンで原文 · 翻訳を切り替えられます。", "다른 언어로 번역 (AI · 이 {0}에서)": "他の言語に翻訳(AI・この{0}で)", "다른 이름으로 저장": "名前を付けて保存", @@ -1415,6 +1418,7 @@ "원어({0})와 같은 언어로는 번역하지 않습니다.": "原語({0})と同じ言語には翻訳しません。", "원어: {0} ({1})": "原語: {0}({1})", "원어: 자동 감지 (글자로 추정)": "原語: 自動検出(文字から推定)", + "원어를 골라 다시 만들기": "原語を選んで作り直す", "원인: {0}": "原因: {0}", "월": "月", "웹 브라우저": "ウェブブラウザー", @@ -1849,6 +1853,7 @@ "처음 쓸 때 내려받기 (기본)": "初めて使うときにダウンロード (既定)", "처음 아이콘 (파랑)": "最初のアイコン(青)", "처음대로": "既定に戻す", + "처음부터 정하려면 AI 자막 창의 \"원어 (영상 속 언어)\" 에서 고르세요.": "最初から決めるには、AI 字幕ウィンドウの「原語(動画内の言語)」で選んでください。", "처음으로: {0}": "最初へ: {0}", "첫 장입니다.": "最初のページです。", "첫 줄 추가": "最初の行を追加", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 6cc35a9..95429b2 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -59,6 +59,7 @@ "(현재 방식은 옵션 없음)": "(当前方式没有选项)", "* 는 아무 글자, ? 는 한 글자. 여러 개는 ; 로 (예: *.srt; *.ass)": "* 表示任意字符,? 表示一个字符。多个用 ; 分隔(例:*.srt; *.ass)", "+ 여기에 새 줄": "+ 在此新增一行", + "- 감지한 언어": "- 检测到的语言", "-1 은 아무렇게나": "-1 为随机", ".{0} 재생 프로그램 (exe)": ".{0} 播放程序(exe)", "0 = 제한 없음. 모든 방법에 적용: rsync --bwlimit, 현재 방식은 앱이 조절, robocopy 는 /IPG 로 비슷하게. 옵션 칸에 직접 넣어도 됩니다 (그쪽이 우선)": "0 = 不限制。适用于所有方式: rsync 用 --bwlimit,当前方式由应用调节,robocopy 用 /IPG 近似。也可直接写在选项栏中 (优先)", @@ -442,6 +443,7 @@ "가져온 즐겨찾기": "导入的书签", "가져온 즐겨찾기 ({0})": "导入的书签({0})", "가져올 즐겨찾기가 없습니다.": "没有可导入的书签。", + "감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "检测到的语言:{0} - 如有误,请用其他语言重新生成", "같은 시리즈 (번호만 다른 파일)": "同一系列(仅编号不同的文件)", "같은 이름이 이미 있습니다": "已存在同名项目", "같은 이름이 이미 있습니다: {0}": "已存在同名项目: {0}", @@ -641,6 +643,7 @@ "다른 앱에서 동영상을 열 때": "从其他应用打开视频时", "다른 앱에서 열기: {0}": "从其他应用打开: {0}", "다른 앱으로 열기": "用其他应用打开", + "다른 언어로 다시": "用其他语言重做", "다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. 주소창 옆 번역 버튼으로 원문 · 번역을 바꿀 수 있습니다.": "将其他语言的页面翻译为界面语言 ({0})。页面文字会发送到 Google 翻译。可用地址栏旁的翻译按钮切换原文 · 译文。", "다른 언어로 번역 (AI · 이 {0}에서)": "翻译为其他语言(AI · 在此{0}上)", "다른 이름으로 저장": "另存为", @@ -1415,6 +1418,7 @@ "원어({0})와 같은 언어로는 번역하지 않습니다.": "不会翻译成与原语言({0})相同的语言。", "원어: {0} ({1})": "原语言: {0}({1})", "원어: 자동 감지 (글자로 추정)": "原语言: 自动检测(根据文字推测)", + "원어를 골라 다시 만들기": "选择原语言并重新生成", "원인: {0}": "原因: {0}", "월": "一", "웹 브라우저": "网页浏览器", @@ -1849,6 +1853,7 @@ "처음 쓸 때 내려받기 (기본)": "首次使用时下载 (默认)", "처음 아이콘 (파랑)": "最初的图标(蓝色)", "처음대로": "恢复默认", + "처음부터 정하려면 AI 자막 창의 \"원어 (영상 속 언어)\" 에서 고르세요.": "如需一开始就指定,请在 AI 字幕窗口的“原语言(视频中的语言)”中选择。", "처음으로: {0}": "回到第一个: {0}", "첫 장입니다.": "已是第一页。", "첫 줄 추가": "添加第一行", diff --git a/lib/app/app_controller.dart b/lib/app/app_controller.dart index 5b820cd..07ad65d 100644 --- a/lib/app/app_controller.dart +++ b/lib/app/app_controller.dart @@ -943,6 +943,8 @@ class AppController extends ChangeNotifier { ? opts.source : detectLanguage(cues.map((c) => c.text).join(' ')); _log(trf('음성인식 완료: {0}줄, 원어 {1}({2})', [cues.length, src.name, src.code])); + // 198: 자동 감지가 틀릴 수 있다 (영어를 한국어로 잡아 영어 발음을 한글로 적음) - 끝난 뒤 알리고 다른 언어로 다시 할 수 있게 + v.aiDetected = opts.source == undetermined ? src.code : null; // 5. 파일명_AI.srt (원본) await storage.ensureDirectory(outputDirFor(v.path)); diff --git a/lib/core/models.dart b/lib/core/models.dart index 607f603..22a0c92 100644 --- a/lib/core/models.dart +++ b/lib/core/models.dart @@ -99,6 +99,9 @@ class VideoItem { String? message; String? outputPath; + /// 198: 마지막 AI 자막에서 원어를 자동 감지했으면 그 언어 코드 (원어를 직접 골랐으면 null) + String? aiDetected; + VideoItem(this.path); String get fileName => p.basename(path); diff --git a/lib/ui/ai_dialog.dart b/lib/ui/ai_dialog.dart index b629f8b..db11565 100644 --- a/lib/ui/ai_dialog.dart +++ b/lib/ui/ai_dialog.dart @@ -16,8 +16,16 @@ import '../l10n/tr.dart'; Future showAiDialog(BuildContext context, AppController c, VideoItem current, {List? targets, bool thenBuild = false}) async { final list = targets ?? [current]; - Future run(List v, AiOptions o) => - thenBuild ? c.aiThenBuild(v, o) : c.generateAiSubtitles(v, o); + // 198: 끝난 뒤 알림 · 다시 하기 창을 띄울 곳 (이 화면이 닫혀도 남는 것) + final messenger = ScaffoldMessenger.maybeOf(context); + final overlay = Navigator.maybeOf(context)?.overlay; + Future run(List v, AiOptions o) async { + await (thenBuild ? c.aiThenBuild(v, o) : c.generateAiSubtitles(v, o)); + if (o.source == undetermined && messenger != null) { + showDetectedLanguage(messenger, overlay, v, onRetry: (lang) => run(v, o.copyWith(source: lang))); + } + } + Future<(AiOptions, bool)?> full() => showDialog<(AiOptions, bool)>( context: context, builder: (_) => _AiDialog(c: c, videos: list, videoCount: targets == null ? c.videos.length : 1, thenBuild: thenBuild), @@ -43,6 +51,50 @@ Future showAiDialog(BuildContext context, AppController c, VideoItem curre await run(all ? List.of(c.videos) : list, opts); } +/// 198: 원어를 자동 감지했으면 끝난 뒤 "감지한 언어: …" 와 [다른 언어로 다시] (언어 고르기 → 그 원어로 다시 만듦) +void showDetectedLanguage(ScaffoldMessengerState messenger, OverlayState? overlay, List videos, + {required Future Function(Language) onRetry}) { + final found = [for (final v in videos) if (v.aiDetected != null) (v, languageOf(v.aiDetected))]; + if (found.isEmpty) return; + final names = found.length == 1 + ? found.single.$2.name + : found.map((x) => '${x.$2.name} (${x.$1.fileName})').join(' · '); + messenger.showSnackBar(SnackBar( + duration: const Duration(seconds: 12), + content: Text(trf('감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요', [names])), + action: overlay == null + ? null + : SnackBarAction( + label: tr('다른 언어로 다시'), + onPressed: () async { + if (!overlay.mounted) return; + final lang = await pickSourceLanguage(overlay.context, found.first.$2); + if (lang != null) await onRetry(lang); + }, + ), + )); +} + +/// 198: 원어를 골라 다시 만들 때의 언어 고르기 (원어를 처음부터 정하는 곳도 알린다) +Future pickSourceLanguage(BuildContext context, Language detected) => showDialog( + context: context, + builder: (ctx) => SimpleDialog( + title: Text(tr('원어를 골라 다시 만들기')), + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 8), + child: Text(tr('처음부터 정하려면 AI 자막 창의 "원어 (영상 속 언어)" 에서 고르세요.'), + style: const TextStyle(fontSize: 12, color: JjColors.textDim)), + ), + for (final l in languages) + SimpleDialogOption( + onPressed: () => Navigator.pop(ctx, l), + child: Text('${l.name} (${l.code})${l == detected ? ' ${tr('- 감지한 언어')}' : ''}'), + ), + ], + ), + ); + /// 138: "매번 묻기" 를 껐을 때 시작 전에 [취소] 할 수 있는 시간 const aiStartDelay = Duration(seconds: 3); diff --git a/test/ai_confirm_test.dart b/test/ai_confirm_test.dart index ae0a642..9c47598 100644 --- a/test/ai_confirm_test.dart +++ b/test/ai_confirm_test.dart @@ -79,4 +79,28 @@ void main() { await done; expect(c.busy, isFalse); }); + testWidgets('198: 원어를 자동 감지했으면 끝난 뒤 "감지한 언어" 와 [다른 언어로 다시] (고른 원어로 다시)', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(MaterialApp(navigatorKey: key, home: const Scaffold(body: SizedBox()))); + final v = VideoItem(r'D:\v\a.mp4')..aiDetected = 'ko'; + Language? again; + showDetectedLanguage(tester.state(find.byType(ScaffoldMessenger)), key.currentState!.overlay, [v], + onRetry: (l) async => again = l); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 800)); // 알림이 다 올라올 때까지 + expect(find.text('감지한 언어: 한국어 - 틀렸으면 다른 언어로 다시 만드세요'), findsOneWidget); + await tester.tap(find.text('다른 언어로 다시')); + await tester.pumpAndSettle(); + expect(find.text('원어를 골라 다시 만들기'), findsOneWidget); + expect(find.textContaining('- 감지한 언어'), findsOneWidget); + await tester.tap(find.textContaining('영어 (en)')); + await tester.pumpAndSettle(); + expect(again?.code, 'en'); + // 원어를 직접 고른 동영상 (감지 안 함) 만 있으면 알리지 않는다 + final w = VideoItem(r'D:\v\b.mp4'); + showDetectedLanguage(tester.state(find.byType(ScaffoldMessenger)), key.currentState!.overlay, [w], + onRetry: (_) async {}); + await tester.pump(const Duration(seconds: 13)); + expect(find.textContaining('감지한 언어:'), findsNothing); + }); } From 433f5b187a3f3fc6ef8ad08f7132edb6590bef57 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 12 Oct 2026 06:01:35 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=EA=B0=90=EC=A7=80=ED=95=9C=20=EC=96=B8?= =?UTF-8?q?=EC=96=B4=20=EC=95=8C=EB=A6=BC=EC=9D=80=20=EB=8B=AB=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C=EA=B9=8C=EC=A7=80=20=C2=B7=20MKV=20=EC=84=B8=EB=B6=80?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=EC=97=90=EB=8F=84=20[=EB=8B=A4=EB=A5=B8?= =?UTF-8?q?=20=EC=96=B8=EC=96=B4=EB=A1=9C=20=EB=8B=A4=EC=8B=9C]=20(198=20?= =?UTF-8?q?=EB=B3=B4=EC=B6=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관리자 지시: 248 과 같게 버튼이 있는 안내는 닫을 때까지, 알림을 놓쳐도 세부 정보에서 다시 할 수 있게 Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 1 + assets/l10n/ja.json | 1 + assets/l10n/zh-Hans.json | 1 + lib/ui/ai_dialog.dart | 4 +++- lib/ui/home_page.dart | 17 +++++++++++++++++ test/ai_confirm_test.dart | 4 ++++ test/batch_test.dart | 17 +++++++++++++++++ 7 files changed, 44 insertions(+), 1 deletion(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index e390eae..e406f1d 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -443,6 +443,7 @@ "가져온 즐겨찾기": "Imported bookmarks", "가져온 즐겨찾기 ({0})": "Imported bookmarks ({0})", "가져올 즐겨찾기가 없습니다.": "No bookmarks to import.", + "감지한 언어: {0} (자동 감지)": "Detected language: {0} (auto-detect)", "감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "Detected language: {0} - if it is wrong, make them again with another language", "같은 시리즈 (번호만 다른 파일)": "Same series (files that differ only by number)", "같은 이름이 이미 있습니다": "An item with the same name already exists", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 1da1d0a..978d084 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -443,6 +443,7 @@ "가져온 즐겨찾기": "取り込んだブックマーク", "가져온 즐겨찾기 ({0})": "取り込んだブックマーク({0})", "가져올 즐겨찾기가 없습니다.": "取り込むブックマークがありません。", + "감지한 언어: {0} (자동 감지)": "検出した言語: {0} (自動検出)", "감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "検出した言語: {0} - 違っていれば別の言語で作り直してください", "같은 시리즈 (번호만 다른 파일)": "同じシリーズ(番号だけ違うファイル)", "같은 이름이 이미 있습니다": "同じ名前のものが既にあります", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 95429b2..fe3dbc3 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -443,6 +443,7 @@ "가져온 즐겨찾기": "导入的书签", "가져온 즐겨찾기 ({0})": "导入的书签({0})", "가져올 즐겨찾기가 없습니다.": "没有可导入的书签。", + "감지한 언어: {0} (자동 감지)": "检测到的语言:{0}(自动检测)", "감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "检测到的语言:{0} - 如有误,请用其他语言重新生成", "같은 시리즈 (번호만 다른 파일)": "同一系列(仅编号不同的文件)", "같은 이름이 이미 있습니다": "已存在同名项目", diff --git a/lib/ui/ai_dialog.dart b/lib/ui/ai_dialog.dart index db11565..3b7ebaa 100644 --- a/lib/ui/ai_dialog.dart +++ b/lib/ui/ai_dialog.dart @@ -60,7 +60,9 @@ void showDetectedLanguage(ScaffoldMessengerState messenger, OverlayState? overla ? found.single.$2.name : found.map((x) => '${x.$2.name} (${x.$1.fileName})').join(' · '); messenger.showSnackBar(SnackBar( - duration: const Duration(seconds: 12), + // 248 과 같게: 버튼이 있는 안내는 사용자가 닫을 때까지 ([✕]). 나중에는 MKV 목록의 그 동영상 세부 정보에서도 + persist: true, + showCloseIcon: true, content: Text(trf('감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요', [names])), action: overlay == null ? null diff --git a/lib/ui/home_page.dart b/lib/ui/home_page.dart index 03d5989..3379df5 100644 --- a/lib/ui/home_page.dart +++ b/lib/ui/home_page.dart @@ -1063,6 +1063,23 @@ class _VideoDetail extends StatelessWidget { ], _Chip(trf('음성 {0}개', [audioCount])), ]), + // 198: 마지막 AI 자막의 원어를 자동 감지했으면 - 알림을 놓쳐도 여기서 다른 언어로 다시 만들 수 있게 + if (v.aiDetected != null) ...[ + const SizedBox(height: 8), + Wrap(crossAxisAlignment: WrapCrossAlignment.center, spacing: 8, children: [ + Text(trf('감지한 언어: {0} (자동 감지)', [languageOf(v.aiDetected).name]), + style: const TextStyle(fontSize: 12, color: JjColors.textDim)), + TextButton( + onPressed: c.busy + ? null + : () async { + final lang = await pickSourceLanguage(context, languageOf(v.aiDetected)); + if (lang != null) await c.generateAiSubtitles([v], c.aiOptions.copyWith(source: lang)); + }, + child: Text(tr('다른 언어로 다시')), + ), + ]), + ], if (v.message != null) ...[ const SizedBox(height: 12), Container( diff --git a/test/ai_confirm_test.dart b/test/ai_confirm_test.dart index 9c47598..e742ba5 100644 --- a/test/ai_confirm_test.dart +++ b/test/ai_confirm_test.dart @@ -89,6 +89,10 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 800)); // 알림이 다 올라올 때까지 expect(find.text('감지한 언어: 한국어 - 틀렸으면 다른 언어로 다시 만드세요'), findsOneWidget); + // 248 과 같게: 닫을 때까지 남는다 ([✕]) + await tester.pump(const Duration(minutes: 1)); + expect(find.text('감지한 언어: 한국어 - 틀렸으면 다른 언어로 다시 만드세요'), findsOneWidget); + expect(find.byIcon(Icons.close), findsOneWidget); await tester.tap(find.text('다른 언어로 다시')); await tester.pumpAndSettle(); expect(find.text('원어를 골라 다시 만들기'), findsOneWidget); diff --git a/test/batch_test.dart b/test/batch_test.dart index 97a29e5..cad2fa9 100644 --- a/test/batch_test.dart +++ b/test/batch_test.dart @@ -293,6 +293,23 @@ void main() { reason: '${v1.message} ${v2.message} ${v3.message}'); }); + testWidgets('198: 원어를 자동 감지한 동영상은 세부 정보에 감지한 언어 · [다른 언어로 다시] (알림을 놓쳐도)', (tester) async { + tester.view.physicalSize = const Size(1600, 900); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + c.videos.add(VideoItem(r'D:\v\a.mp4')..aiDetected = 'ko'); + c.selected = c.videos.first; + await tester.pumpWidget(MaterialApp(home: HomePage(c: c))); + expect(find.text('감지한 언어: 한국어 (자동 감지)'), findsOneWidget); + expect(find.widgetWithText(TextButton, '다른 언어로 다시'), findsOneWidget); + // 원어를 직접 고르면 (null) 보이지 않는다 + c.videos.first.aiDetected = null; + c.selected = c.videos.first; + await tester.pumpWidget(MaterialApp(key: UniqueKey(), home: HomePage(c: c))); + expect(find.textContaining('감지한 언어'), findsNothing); + }); + testWidgets('MKV 화면: 체크 → "자막 만들기 (2)" · "MKV 만들기 (2)" 글, 전체 선택', (tester) async { tester.view.physicalSize = const Size(1600, 900); // 위쪽 버튼 글이 다 보이는 너비 tester.view.devicePixelRatio = 1;