전체 점검에서 고침: 다시 켜면 전에 만든 자막 (jj_mkv) 다시 연결, Android 16 태블릿에서 화면 방향 고정 동작, 목록 머리글 한 줄, '이 기기에서' 띄어쓰기

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
RyuJongJin 2026-10-04 19:18:56 +09:00
parent 52deef4118
commit 98122539e7
10 changed files with 66 additions and 24 deletions

View File

@ -17,6 +17,10 @@
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"> android:requestLegacyExternalStorage="true">
<!-- Android 16: 큰 화면 (태블릿) 에서도 환경 설정 > 화면 방향 (가로 고정 · 세로 고정) 을 따르게 -->
<property
android:name="android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY"
android:value="true" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

View File

@ -16,6 +16,7 @@
" · 다시 시작해야 Chrome 으로 바뀝니다": " · Restart to switch to Chrome", " · 다시 시작해야 Chrome 으로 바뀝니다": " · Restart to switch to Chrome",
" · 대기 {0}": " · {0} waiting", " · 대기 {0}": " · {0} waiting",
" · 대기 {0}개": " · {0} waiting", " · 대기 {0}개": " · {0} waiting",
" · 전에 만든 자막 {0}개 (jj_mkv)": " · {0} subtitles made earlier (jj_mkv)",
" → MKV 만들기": " → Make MKV", " → MKV 만들기": " → Make MKV",
" ⚠ 사용할 수 없는 코덱: {0}": " ⚠ Unavailable codec: {0}", " ⚠ 사용할 수 없는 코덱: {0}": " ⚠ Unavailable codec: {0}",
" ⚠ 표현할 수 없는 글자 {0}개": " ⚠ {0} characters cannot be represented", " ⚠ 표현할 수 없는 글자 {0}개": " ⚠ {0} characters cannot be represented",
@ -261,7 +262,7 @@
"다른 브라우저의 즐겨찾기 가져오기": "Import bookmarks from another browser", "다른 브라우저의 즐겨찾기 가져오기": "Import bookmarks from another browser",
"다른 앱에서 동영상을 열 때": "When opening a video from another app", "다른 앱에서 동영상을 열 때": "When opening a video from another app",
"다른 앱에서 열기: {0}": "Opened from another app: {0}", "다른 앱에서 열기: {0}": "Opened from another app: {0}",
"다른 언어로 번역 (AI · 이 {0} 에서)": "Translate to another language (AI · on this {0})", "다른 언어로 번역 (AI · 이 {0}에서)": "Translate to another language (AI · on this {0})",
"다른 이름으로 저장": "Save as", "다른 이름으로 저장": "Save as",
"다른 이름으로 저장: {0} ({1})": "Save as: {0} ({1})", "다른 이름으로 저장: {0} ({1})": "Save as: {0} ({1})",
"다른 폴더로 이동": "Move to another folder", "다른 폴더로 이동": "Move to another folder",
@ -386,7 +387,7 @@
"번역 모델 불러오는 중": "Loading translation model", "번역 모델 불러오는 중": "Loading translation model",
"번역 모델 파일이 없습니다": "Translation model files are missing", "번역 모델 파일이 없습니다": "Translation model files are missing",
"번역 모델: NLLB-200 (로컬) ": "Translation model: NLLB-200 (local) ", "번역 모델: NLLB-200 (로컬) ": "Translation model: NLLB-200 (local) ",
"번역 모델: NLLB-200 (이 {0} 에서만) ": "Translation model: NLLB-200 (only on this {0}) ", "번역 모델: NLLB-200 (이 {0}에서만) ": "Translation model: NLLB-200 (only on this {0}) ",
"번역 모델을 먼저 불러와야 합니다.": "The translation model must be loaded first.", "번역 모델을 먼저 불러와야 합니다.": "The translation model must be loaded first.",
"번역 시작": "Start translation", "번역 시작": "Start translation",
"번역할 언어 (jj_mkv\\파일명_언어코드.srt 로 저장 후 MKV 에 추가)": "Languages to translate to (saved as jj_mkv\\filename_languagecode.srt and added to the MKV)", "번역할 언어 (jj_mkv\\파일명_언어코드.srt 로 저장 후 MKV 에 추가)": "Languages to translate to (saved as jj_mkv\\filename_languagecode.srt and added to the MKV)",
@ -502,7 +503,7 @@
"영상·음성을 그대로 복사합니다 (빠름, 화질 손실 없음)": "Copies video · audio as is (fast, no quality loss)", "영상·음성을 그대로 복사합니다 (빠름, 화질 손실 없음)": "Copies video · audio as is (fast, no quality loss)",
"영상마다 한 줄씩 보여 주고 jj_yt-dlp\\재생목록 이름\\ 에 저장 (믹스 목록 제외)": "Shows one line per video and saves to jj_yt-dlp\\playlist name\\ (mix lists excluded)", "영상마다 한 줄씩 보여 주고 jj_yt-dlp\\재생목록 이름\\ 에 저장 (믹스 목록 제외)": "Shows one line per video and saves to jj_yt-dlp\\playlist name\\ (mix lists excluded)",
"영상을 다시 인코딩합니다 (시간이 오래 걸림, 음성은 그대로){0}": "Re-encodes the video (takes a long time, audio unchanged){0}", "영상을 다시 인코딩합니다 (시간이 오래 걸림, 음성은 그대로){0}": "Re-encodes the video (takes a long time, audio unchanged){0}",
"영상의 음성을 인식하고 선택한 언어로 번역합니다. 모든 처리는 이 {0} 에서 이루어집니다.": "Recognizes the video's speech and translates it into the chosen languages. All processing happens on this {0}.", "영상의 음성을 인식하고 선택한 언어로 번역합니다. 모든 처리는 이 {0}에서 이루어집니다.": "Recognizes the video's speech and translates it into the chosen languages. All processing happens on this {0}.",
"영어": "English", "영어": "English",
"예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "e.g. Ctrl+Shift+X (Ctrl·Shift·Alt·Win + letter·digit·F1~F12)", "예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "e.g. Ctrl+Shift+X (Ctrl·Shift·Alt·Win + letter·digit·F1~F12)",
"예전 구조의 프로그램 파일 정리 ({0}개): {1}": "Cleaned up program files of the old layout ({0}): {1}", "예전 구조의 프로그램 파일 정리 ({0}개): {1}": "Cleaned up program files of the old layout ({0}): {1}",
@ -551,7 +552,7 @@
"음성인식 중": "Recognizing speech", "음성인식 중": "Recognizing speech",
"음성인식 중 {0}%": "Recognizing speech {0}%", "음성인식 중 {0}%": "Recognizing speech {0}%",
"음소거 (M)": "Mute (M)", "음소거 (M)": "Mute (M)",
"이 {0} 의 설정 파일에 저장됩니다": "Saved in this {0}'s settings file", "이 {0}의 설정 파일에 저장됩니다": "Saved in this {0}'s settings file",
"이 기기에서는 AI 번역을 쓸 수 없습니다.": "AI translation is not available on this device.", "이 기기에서는 AI 번역을 쓸 수 없습니다.": "AI translation is not available on this device.",
"이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)": "Cannot register this hotkey (another program may be using it)", "이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)": "Cannot register this hotkey (another program may be using it)",
"이 버전 건너뛰기": "Skip this version", "이 버전 건너뛰기": "Skip this version",

View File

@ -16,6 +16,7 @@
" · 다시 시작해야 Chrome 으로 바뀝니다": " · 再起動すると Chrome に切り替わります", " · 다시 시작해야 Chrome 으로 바뀝니다": " · 再起動すると Chrome に切り替わります",
" · 대기 {0}": " · 待機 {0}", " · 대기 {0}": " · 待機 {0}",
" · 대기 {0}개": " · 待機 {0}個", " · 대기 {0}개": " · 待機 {0}個",
" · 전에 만든 자막 {0}개 (jj_mkv)": " · 以前作成した字幕 {0}個(jj_mkv)",
" → MKV 만들기": " → MKV 作成", " → MKV 만들기": " → MKV 作成",
" ⚠ 사용할 수 없는 코덱: {0}": " ⚠ 使用できないコーデック: {0}", " ⚠ 사용할 수 없는 코덱: {0}": " ⚠ 使用できないコーデック: {0}",
" ⚠ 표현할 수 없는 글자 {0}개": " ⚠ 表現できない文字 {0}個", " ⚠ 표현할 수 없는 글자 {0}개": " ⚠ 表現できない文字 {0}個",
@ -261,7 +262,7 @@
"다른 브라우저의 즐겨찾기 가져오기": "他のブラウザーのブックマークを取り込む", "다른 브라우저의 즐겨찾기 가져오기": "他のブラウザーのブックマークを取り込む",
"다른 앱에서 동영상을 열 때": "他のアプリで動画を開いたとき", "다른 앱에서 동영상을 열 때": "他のアプリで動画を開いたとき",
"다른 앱에서 열기: {0}": "他のアプリから開く: {0}", "다른 앱에서 열기: {0}": "他のアプリから開く: {0}",
"다른 언어로 번역 (AI · 이 {0} 에서)": "他の言語に翻訳(AI・この{0}で)", "다른 언어로 번역 (AI · 이 {0}에서)": "他の言語に翻訳(AI・この{0}で)",
"다른 이름으로 저장": "名前を付けて保存", "다른 이름으로 저장": "名前を付けて保存",
"다른 이름으로 저장: {0} ({1})": "名前を付けて保存: {0}({1})", "다른 이름으로 저장: {0} ({1})": "名前を付けて保存: {0}({1})",
"다른 폴더로 이동": "他のフォルダーに移動", "다른 폴더로 이동": "他のフォルダーに移動",
@ -386,7 +387,7 @@
"번역 모델 불러오는 중": "翻訳モデルを読み込み中", "번역 모델 불러오는 중": "翻訳モデルを読み込み中",
"번역 모델 파일이 없습니다": "翻訳モデルファイルがありません", "번역 모델 파일이 없습니다": "翻訳モデルファイルがありません",
"번역 모델: NLLB-200 (로컬) ": "翻訳モデル: NLLB-200(ローカル) ", "번역 모델: NLLB-200 (로컬) ": "翻訳モデル: NLLB-200(ローカル) ",
"번역 모델: NLLB-200 (이 {0} 에서만) ": "翻訳モデル: NLLB-200(この{0}のみ) ", "번역 모델: NLLB-200 (이 {0}에서만) ": "翻訳モデル: NLLB-200(この{0}のみ) ",
"번역 모델을 먼저 불러와야 합니다.": "先に翻訳モデルを読み込む必要があります。", "번역 모델을 먼저 불러와야 합니다.": "先に翻訳モデルを読み込む必要があります。",
"번역 시작": "翻訳開始", "번역 시작": "翻訳開始",
"번역할 언어 (jj_mkv\\파일명_언어코드.srt 로 저장 후 MKV 에 추가)": "翻訳する言語(jj_mkv\\ファイル名_言語コード.srt に保存して MKV に追加)", "번역할 언어 (jj_mkv\\파일명_언어코드.srt 로 저장 후 MKV 에 추가)": "翻訳する言語(jj_mkv\\ファイル名_言語コード.srt に保存して MKV に追加)",
@ -502,7 +503,7 @@
"영상·음성을 그대로 복사합니다 (빠름, 화질 손실 없음)": "映像・音声をそのままコピーします(高速、画質劣化なし)", "영상·음성을 그대로 복사합니다 (빠름, 화질 손실 없음)": "映像・音声をそのままコピーします(高速、画質劣化なし)",
"영상마다 한 줄씩 보여 주고 jj_yt-dlp\\재생목록 이름\\ 에 저장 (믹스 목록 제외)": "動画ごとに 1行ずつ表示し、jj_yt-dlp\\再生リスト名\\ に保存(ミックスリストは除く)", "영상마다 한 줄씩 보여 주고 jj_yt-dlp\\재생목록 이름\\ 에 저장 (믹스 목록 제외)": "動画ごとに 1行ずつ表示し、jj_yt-dlp\\再生リスト名\\ に保存(ミックスリストは除く)",
"영상을 다시 인코딩합니다 (시간이 오래 걸림, 음성은 그대로){0}": "映像を再エンコードします(時間がかかります、音声はそのまま){0}", "영상을 다시 인코딩합니다 (시간이 오래 걸림, 음성은 그대로){0}": "映像を再エンコードします(時間がかかります、音声はそのまま){0}",
"영상의 음성을 인식하고 선택한 언어로 번역합니다. 모든 처리는 이 {0} 에서 이루어집니다.": "動画の音声を認識し、選んだ言語に翻訳します。すべての処理はこの{0}で行われます。", "영상의 음성을 인식하고 선택한 언어로 번역합니다. 모든 처리는 이 {0}에서 이루어집니다.": "動画の音声を認識し、選んだ言語に翻訳します。すべての処理はこの{0}で行われます。",
"영어": "英語", "영어": "英語",
"예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "例: Ctrl+Shift+X(Ctrl・Shift・Alt・Win + 英字・数字・F1〜F12)", "예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "例: Ctrl+Shift+X(Ctrl・Shift・Alt・Win + 英字・数字・F1〜F12)",
"예전 구조의 프로그램 파일 정리 ({0}개): {1}": "旧構成のプログラムファイルを整理({0}個): {1}", "예전 구조의 프로그램 파일 정리 ({0}개): {1}": "旧構成のプログラムファイルを整理({0}個): {1}",
@ -551,7 +552,7 @@
"음성인식 중": "音声認識中", "음성인식 중": "音声認識中",
"음성인식 중 {0}%": "音声認識中 {0}%", "음성인식 중 {0}%": "音声認識中 {0}%",
"음소거 (M)": "ミュート(M)", "음소거 (M)": "ミュート(M)",
"이 {0} 의 설정 파일에 저장됩니다": "この{0}の設定ファイルに保存されます", "이 {0}의 설정 파일에 저장됩니다": "この{0}の設定ファイルに保存されます",
"이 기기에서는 AI 번역을 쓸 수 없습니다.": "この端末では AI 翻訳を使えません。", "이 기기에서는 AI 번역을 쓸 수 없습니다.": "この端末では AI 翻訳を使えません。",
"이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)": "このショートカットを登録できません(他のプログラムが使用中の可能性があります)", "이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)": "このショートカットを登録できません(他のプログラムが使用中の可能性があります)",
"이 버전 건너뛰기": "このバージョンをスキップ", "이 버전 건너뛰기": "このバージョンをスキップ",

View File

@ -16,6 +16,7 @@
" · 다시 시작해야 Chrome 으로 바뀝니다": " · 重新启动后切换到 Chrome", " · 다시 시작해야 Chrome 으로 바뀝니다": " · 重新启动后切换到 Chrome",
" · 대기 {0}": " · 等待 {0}", " · 대기 {0}": " · 等待 {0}",
" · 대기 {0}개": " · 等待 {0} 个", " · 대기 {0}개": " · 等待 {0} 个",
" · 전에 만든 자막 {0}개 (jj_mkv)": " · 之前制作的字幕 {0} 个(jj_mkv)",
" → MKV 만들기": " → 制作 MKV", " → MKV 만들기": " → 制作 MKV",
" ⚠ 사용할 수 없는 코덱: {0}": " ⚠ 不可用的编解码器: {0}", " ⚠ 사용할 수 없는 코덱: {0}": " ⚠ 不可用的编解码器: {0}",
" ⚠ 표현할 수 없는 글자 {0}개": " ⚠ {0} 个字符无法表示", " ⚠ 표현할 수 없는 글자 {0}개": " ⚠ {0} 个字符无法表示",
@ -261,7 +262,7 @@
"다른 브라우저의 즐겨찾기 가져오기": "导入其他浏览器的书签", "다른 브라우저의 즐겨찾기 가져오기": "导入其他浏览器的书签",
"다른 앱에서 동영상을 열 때": "从其他应用打开视频时", "다른 앱에서 동영상을 열 때": "从其他应用打开视频时",
"다른 앱에서 열기: {0}": "从其他应用打开: {0}", "다른 앱에서 열기: {0}": "从其他应用打开: {0}",
"다른 언어로 번역 (AI · 이 {0} 에서)": "翻译为其他语言(AI · 在此{0}上)", "다른 언어로 번역 (AI · 이 {0}에서)": "翻译为其他语言(AI · 在此{0}上)",
"다른 이름으로 저장": "另存为", "다른 이름으로 저장": "另存为",
"다른 이름으로 저장: {0} ({1})": "另存为: {0}({1})", "다른 이름으로 저장: {0} ({1})": "另存为: {0}({1})",
"다른 폴더로 이동": "移动到其他文件夹", "다른 폴더로 이동": "移动到其他文件夹",
@ -386,7 +387,7 @@
"번역 모델 불러오는 중": "正在加载翻译模型", "번역 모델 불러오는 중": "正在加载翻译模型",
"번역 모델 파일이 없습니다": "缺少翻译模型文件", "번역 모델 파일이 없습니다": "缺少翻译模型文件",
"번역 모델: NLLB-200 (로컬) ": "翻译模型: NLLB-200(本地) ", "번역 모델: NLLB-200 (로컬) ": "翻译模型: NLLB-200(本地) ",
"번역 모델: NLLB-200 (이 {0} 에서만) ": "翻译模型: NLLB-200(仅在此{0}上) ", "번역 모델: NLLB-200 (이 {0}에서만) ": "翻译模型: NLLB-200(仅在此{0}上) ",
"번역 모델을 먼저 불러와야 합니다.": "必须先加载翻译模型。", "번역 모델을 먼저 불러와야 합니다.": "必须先加载翻译模型。",
"번역 시작": "开始翻译", "번역 시작": "开始翻译",
"번역할 언어 (jj_mkv\\파일명_언어코드.srt 로 저장 후 MKV 에 추가)": "要翻译的语言(保存为 jj_mkv\\文件名_语言代码.srt 后加入 MKV)", "번역할 언어 (jj_mkv\\파일명_언어코드.srt 로 저장 후 MKV 에 추가)": "要翻译的语言(保存为 jj_mkv\\文件名_语言代码.srt 后加入 MKV)",
@ -502,7 +503,7 @@
"영상·음성을 그대로 복사합니다 (빠름, 화질 손실 없음)": "直接复制视频 · 音频(快速,无画质损失)", "영상·음성을 그대로 복사합니다 (빠름, 화질 손실 없음)": "直接复制视频 · 音频(快速,无画质损失)",
"영상마다 한 줄씩 보여 주고 jj_yt-dlp\\재생목록 이름\\ 에 저장 (믹스 목록 제외)": "每个视频显示一行并保存到 jj_yt-dlp\\播放列表名\\(合辑列表除外)", "영상마다 한 줄씩 보여 주고 jj_yt-dlp\\재생목록 이름\\ 에 저장 (믹스 목록 제외)": "每个视频显示一行并保存到 jj_yt-dlp\\播放列表名\\(合辑列表除外)",
"영상을 다시 인코딩합니다 (시간이 오래 걸림, 음성은 그대로){0}": "重新编码视频(耗时较长,音频不变){0}", "영상을 다시 인코딩합니다 (시간이 오래 걸림, 음성은 그대로){0}": "重新编码视频(耗时较长,音频不变){0}",
"영상의 음성을 인식하고 선택한 언어로 번역합니다. 모든 처리는 이 {0} 에서 이루어집니다.": "识别视频中的语音并翻译成所选语言。所有处理均在此{0}上完成。", "영상의 음성을 인식하고 선택한 언어로 번역합니다. 모든 처리는 이 {0}에서 이루어집니다.": "识别视频中的语音并翻译成所选语言。所有处理均在此{0}上完成。",
"영어": "英语", "영어": "英语",
"예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "例: Ctrl+Shift+X(Ctrl·Shift·Alt·Win + 字母·数字·F1~F12)", "예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "例: Ctrl+Shift+X(Ctrl·Shift·Alt·Win + 字母·数字·F1~F12)",
"예전 구조의 프로그램 파일 정리 ({0}개): {1}": "已清理旧结构的程序文件({0} 个): {1}", "예전 구조의 프로그램 파일 정리 ({0}개): {1}": "已清理旧结构的程序文件({0} 个): {1}",
@ -551,7 +552,7 @@
"음성인식 중": "正在识别语音", "음성인식 중": "正在识别语音",
"음성인식 중 {0}%": "正在识别语音 {0}%", "음성인식 중 {0}%": "正在识别语音 {0}%",
"음소거 (M)": "静音(M)", "음소거 (M)": "静音(M)",
"이 {0} 의 설정 파일에 저장됩니다": "保存在此{0}的设置文件中", "이 {0}의 설정 파일에 저장됩니다": "保存在此{0}的设置文件中",
"이 기기에서는 AI 번역을 쓸 수 없습니다.": "此设备无法使用 AI 翻译。", "이 기기에서는 AI 번역을 쓸 수 없습니다.": "此设备无法使用 AI 翻译。",
"이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)": "无法注册此快捷键(可能已被其他程序使用)", "이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)": "无法注册此快捷键(可能已被其他程序使用)",
"이 버전 건너뛰기": "跳过此版本", "이 버전 건너뛰기": "跳过此版本",

View File

@ -340,9 +340,26 @@ class AppController extends ChangeNotifier {
for (final d in siblings) { for (final d in siblings) {
await _addExternal(v, d.path, d.language); await _addExternal(v, d.path, d.language);
} }
// 전에 만든 자막 (jj_mkv 의 파일명_ko.srt 등: AI 자막 · 번역 · 인터넷 자막) 도 다시 연결
// (앱을 다시 켜거나 목록에 다시 넣어도 MKV 에 빠지지 않게). 음성인식 원본 파일명_AI.srt 는 언어별 파일과 같아서 뺀다.
var made = 0;
final out = outputDirFor(v.path);
if (!p.equals(out, v.directory)) {
List<String> files = const [];
try {
files = await services.storage.listFiles(out);
} catch (_) {}
for (final d in findSiblingSubtitles(v.path, files)) {
if (d.isAi || d.language == undetermined) continue;
if (v.subtitles.any((s) => s.path != null && p.equals(s.path!, d.path))) continue;
await _addExternal(v, d.path, d.language);
made++;
}
}
final embeddedCount = final embeddedCount =
v.subtitles.where((s) => s.kind == SubtitleKind.embedded).length; v.subtitles.where((s) => s.kind == SubtitleKind.embedded).length;
_log(trf('추가: {0} (내장 자막 {1}개, 같은 폴더 자막 {2}개)', [v.fileName, embeddedCount, siblings.length])); _log(trf('추가: {0} (내장 자막 {1}개, 같은 폴더 자막 {2}개)', [v.fileName, embeddedCount, siblings.length]) +
(made > 0 ? trf(' · 전에 만든 자막 {0}개 (jj_mkv)', [made]) : ''));
notifyListeners(); notifyListeners();
} }

View File

@ -79,7 +79,7 @@ class _AiDialogState extends State<_AiDialog> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(trf('영상의 음성을 인식하고 선택한 언어로 번역합니다. 모든 처리는 이 {0} 에서 이루어집니다.', [Platform.isAndroid ? tr('기기') : 'PC']), Text(trf('영상의 음성을 인식하고 선택한 언어로 번역합니다. 모든 처리는 이 {0}에서 이루어집니다.', [Platform.isAndroid ? tr('기기') : 'PC']),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 16), const SizedBox(height: 16),
_label(tr('원어 (영상 속 언어)')), _label(tr('원어 (영상 속 언어)')),

View File

@ -454,13 +454,17 @@ class _VideoList extends StatelessWidget {
Expanded( Expanded(
child: InkWell( child: InkWell(
onTap: c.videos.isEmpty ? null : c.toggleAllChecked, onTap: c.videos.isEmpty ? null : c.toggleAllChecked,
child: Text( // 좁으면 글자를 줄여서 한 줄에 다 보이게
c.checked.isEmpty child: FittedBox(
? trf('동영상 {0}개 · 전체 선택', [c.videos.length]) fit: BoxFit.scaleDown,
: trf('선택 {0} / {1}개', [c.checked.length, c.videos.length]), alignment: Alignment.centerLeft,
maxLines: 1, child: Text(
overflow: TextOverflow.ellipsis, c.checked.isEmpty
style: const TextStyle(color: JjColors.textDim, fontSize: 12)), ? trf('동영상 {0}개 · 전체 선택', [c.videos.length])
: trf('선택 {0} / {1}개', [c.checked.length, c.videos.length]),
maxLines: 1,
style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
),
), ),
), ),
// 정렬: 같은 것을 다시 누르면 반대 순서 // 정렬: 같은 것을 다시 누르면 반대 순서
@ -907,7 +911,7 @@ class _SubtitleRow extends StatelessWidget {
), ),
if (c.services.createTranslator != null) if (c.services.createTranslator != null)
IconButton( IconButton(
tooltip: c.canEdit(s) ? trf('다른 언어로 번역 (AI · 이 {0} 에서)', [Platform.isAndroid ? tr('기기') : 'PC']) : tr('이미지 자막은 번역할 수 없습니다'), tooltip: c.canEdit(s) ? trf('다른 언어로 번역 (AI · 이 {0}에서)', [Platform.isAndroid ? tr('기기') : 'PC']) : tr('이미지 자막은 번역할 수 없습니다'),
iconSize: 18, iconSize: 18,
onPressed: locked || off || !c.canEdit(s) ? null : () => showTranslateDialog(context, c, v, s), onPressed: locked || off || !c.canEdit(s) ? null : () => showTranslateDialog(context, c, v, s),
icon: const Icon(Icons.translate, color: JjColors.accent), icon: const Icon(Icons.translate, color: JjColors.accent),

View File

@ -397,7 +397,7 @@ class _SettingsPageState extends State<SettingsPage> {
(v) => c.updateSettings((x) => x.openSubtitlesKey = v.trim())), (v) => c.updateSettings((x) => x.openSubtitlesKey = v.trim())),
_textTile(tr('아이디 (선택)'), s.openSubtitlesUser, tr('로그인하면 하루 받기 횟수가 늘어납니다'), _textTile(tr('아이디 (선택)'), s.openSubtitlesUser, tr('로그인하면 하루 받기 횟수가 늘어납니다'),
(v) => c.updateSettings((x) => x.openSubtitlesUser = v.trim())), (v) => c.updateSettings((x) => x.openSubtitlesUser = v.trim())),
_textTile(tr('비밀번호 (선택)'), s.openSubtitlesPassword, trf('이 {0} 의 설정 파일에 저장됩니다', [desk ? 'PC' : tr('기기')]), _textTile(tr('비밀번호 (선택)'), s.openSubtitlesPassword, trf('이 {0}의 설정 파일에 저장됩니다', [desk ? 'PC' : tr('기기')]),
(v) => c.updateSettings((x) => x.openSubtitlesPassword = v), obscure: true), (v) => c.updateSettings((x) => x.openSubtitlesPassword = v), obscure: true),
]), ]),
_group('play', Icons.play_circle_outline, tr('재생'), [ _group('play', Icons.play_circle_outline, tr('재생'), [

View File

@ -87,7 +87,7 @@ class _TranslateDialogState extends State<_TranslateDialog> {
]), ]),
const SizedBox(height: 16), const SizedBox(height: 16),
Row(children: [ Row(children: [
Text(trf('번역 모델: NLLB-200 (이 {0} 에서만) ', [Platform.isAndroid ? tr('기기') : 'PC']), style: const TextStyle(fontSize: 13)), Text(trf('번역 모델: NLLB-200 (이 {0}에서만) ', [Platform.isAndroid ? tr('기기') : 'PC']), style: const TextStyle(fontSize: 13)),
if (_installed != null) if (_installed != null)
Text(_installed! ? tr('설치됨') : trf('처음 사용 시 내려받기 {0}', [nllbModel.sizeLabel]), Text(_installed! ? tr('설치됨') : trf('처음 사용 시 내려받기 {0}', [nllbModel.sizeLabel]),
style: TextStyle(fontSize: 11, color: _installed! ? JjColors.success : JjColors.textDim)), style: TextStyle(fontSize: 11, color: _installed! ? JjColors.success : JjColors.textDim)),

View File

@ -62,6 +62,20 @@ void main() {
expect(c.selected, busy); expect(c.selected, busy);
}); });
test('목록에 다시 넣으면 (앱을 다시 켜도) 전에 만든 자막 (jj_mkv 의 파일명_ko.srt 등) 이 다시 붙는다', () async {
final c = _controller();
final video = make('강의.mp4').path;
make(p.join('jj_mkv', '강의_AI.srt'));
make(p.join('jj_mkv', '강의_ko.srt'));
make(p.join('jj_mkv', '강의_en.srt'));
make(p.join('jj_mkv', '다른영상_ko.srt'));
make('강의.ja.srt'); // 같은 폴더 자막
await c.addVideos([video]);
final v = c.videos.single;
expect([for (final s in v.subtitles) p.basename(s.path!)]..sort(), ['강의.ja.srt', '강의_en.srt', '강의_ko.srt']);
expect(v.subtitles.firstWhere((s) => s.path!.endsWith('_ko.srt')).language.code, 'ko');
});
test('이동 폴더를 안 정했으면 옮기지 않는다', () async { test('이동 폴더를 안 정했으면 옮기지 않는다', () async {
final c = _controller(); final c = _controller();
final a = VideoItem(make('a.mp4').path); final a = VideoItem(make('a.mp4').path);