From 4cf2ac29eeeb8276588b2f4e532754cfe90b4299 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:46:53 +0900 Subject: [PATCH] =?UTF-8?q?=ED=9C=B4=EB=8C=80=ED=8F=B0=20(Galaxy=20Z=20Fol?= =?UTF-8?q?d6=20=EC=BB=A4=EB=B2=84=20=ED=99=94=EB=A9=B4)=20=EC=A0=90?= =?UTF-8?q?=EA=B2=80=EC=97=90=EC=84=9C=20=EC=B0=BE=EC=9D=80=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자막 편집기: 폭이 좁으면 위 도구 줄 · 아래 버튼 줄이 화면 밖으로 밀려 설정 · 종료 · 저장 버튼이 안 보이던 것 → 가로로 밀어서 보기 - 플레이어: 같은 폴더 자막이 있어도 mpv 가 켜지 않아 자막 없이 재생되던 것 → 한국어 먼저 자동으로 켬 - 다운로드 목록 안내: Android 는 클립보드를 감시하지 않으므로 Ctrl+C 안내 대신 붙여 넣기 · 브라우저 [다운로드] 안내 Co-Authored-By: Claude Opus 5.5 --- lib/ui/downloads_page.dart | 17 +++-- lib/ui/player_page.dart | 14 ++-- lib/ui/subtitle_editor_page.dart | 119 ++++++++++++++++++------------- 3 files changed, 88 insertions(+), 62 deletions(-) diff --git a/lib/ui/downloads_page.dart b/lib/ui/downloads_page.dart index 9885b34..dc8d111 100644 --- a/lib/ui/downloads_page.dart +++ b/lib/ui/downloads_page.dart @@ -59,9 +59,12 @@ class _DownloadsPageState extends State { child: TextField( controller: _url, style: const TextStyle(fontSize: 13), - decoration: const InputDecoration( + decoration: InputDecoration( isDense: true, - hintText: 'YouTube 주소 · 마그넷 링크 · .torrent 주소 붙여넣기 (Ctrl+C 만 해도 자동 추가)', + // Android 는 클립보드를 감시하지 않는다 + hintText: Platform.isAndroid + ? 'YouTube 주소 · 마그넷 링크 · .torrent 주소 붙여넣기' + : 'YouTube 주소 · 마그넷 링크 · .torrent 주소 붙여넣기 (Ctrl+C 만 해도 자동 추가)', border: OutlineInputBorder(), ), onSubmitted: (_) => _add(), @@ -129,9 +132,13 @@ class _DownloadsPageState extends State { const Divider(height: 1), Expanded( child: d.tasks.isEmpty - ? const Center( - child: Text('다운로드가 없습니다.\nYouTube 주소나 마그넷 링크를 복사(Ctrl+C)하면 자동으로 받습니다.', - textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim))) + ? Center( + child: Text( + Platform.isAndroid + ? '다운로드가 없습니다.\nYouTube 주소 · 마그넷 링크를 위 칸에 붙여 넣거나, 웹 브라우저에서 [다운로드] 를 누르세요.' + : '다운로드가 없습니다.\nYouTube 주소나 마그넷 링크를 복사(Ctrl+C)하면 자동으로 받습니다.', + textAlign: TextAlign.center, + style: const TextStyle(color: JjColors.textDim))) : ListView.builder( itemCount: d.tasks.length, itemBuilder: (_, i) => _DownloadRow(d: d, t: d.tasks[i]), diff --git a/lib/ui/player_page.dart b/lib/ui/player_page.dart index aeec025..32620c0 100644 --- a/lib/ui/player_page.dart +++ b/lib/ui/player_page.dart @@ -95,14 +95,16 @@ class _PlayerPageState extends State { c.externalSubtitlesFor(pl.playlist[i]).then((s) async { if (!mounted) return; setState(() => _externalSubs = s); - // 영상 안 · 같은 폴더 자막이 없고 jj_mkv 에 만든 자막 (번역 · 받은 자막) 만 있으면 하나를 켠다 (한국어 먼저). - // 같은 폴더 자막은 mpv 가 스스로 켜므로 그 뒤에 (1초) 확인한다. + // 켜진 자막이 없으면 하나를 켠다 (한국어 먼저, 없으면 첫 번째). mpv 가 같은 폴더 자막을 불러와도 + // 켜지 않는 일이 있어 (Android) 그 뒤에 (1초) 확인한다. await Future.delayed(const Duration(seconds: 1)); final st = pl.state.value; - if (!mounted || s.isEmpty || st.index != i || st.subtitleId != null || st.subtitleTracks.isNotEmpty) return; - final pick = s.firstWhere((f) => RegExp(r'[._-](ko|kor)[._]', caseSensitive: false).hasMatch(p.basename(f)), - orElse: () => s.first); - await _setSub(TrackInfo('file:$pick', p.basename(pick), file: pick)); + if (!mounted || st.index != i || st.subtitleId != null) return; + final all = _subtitleChoices(); + if (all.isEmpty) return; + final ko = RegExp(r'[._-](ko|kor)[._]|한국어|\(ko\)|\bkor\b', caseSensitive: false); + await _setSub(all.firstWhere((t) => ko.hasMatch(t.label) || ko.hasMatch(p.basename(t.file ?? '')), + orElse: () => all.first)); }); } } diff --git a/lib/ui/subtitle_editor_page.dart b/lib/ui/subtitle_editor_page.dart index 2c91d70..a493645 100644 --- a/lib/ui/subtitle_editor_page.dart +++ b/lib/ui/subtitle_editor_page.dart @@ -460,12 +460,20 @@ class _SubtitleEditorPageState extends State { _pl?.setRate(r!); }, ), - const Spacer(), - btn('+ 여기에 새 줄', 'F7', _insertHere), - btn('선택 줄 재생', 'F8', hasSel ? _playSelected : null), - btn('시작 = 현재', 'F9', hasSel ? _setStartHere : null), - btn('끝 = 현재', 'F10', hasSel ? _setEndHere : null), - btn('선택 줄부터 여기로 맞추기', '선택한 줄과 그 뒤 모든 줄을 함께 이동', hasSel ? _alignHere : null), + // 폭이 좁으면 (휴대폰) 밀어서 본다. 넓으면 지금처럼 오른쪽에 붙는다 (reverse) + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + reverse: true, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + btn('+ 여기에 새 줄', 'F7', _insertHere), + btn('선택 줄 재생', 'F8', hasSel ? _playSelected : null), + btn('시작 = 현재', 'F9', hasSel ? _setStartHere : null), + btn('끝 = 현재', 'F10', hasSel ? _setEndHere : null), + btn('선택 줄부터 여기로 맞추기', '선택한 줄과 그 뒤 모든 줄을 함께 이동', hasSel ? _alignHere : null), + ]), + ), + ), ], ), ); @@ -486,53 +494,62 @@ class _SubtitleEditorPageState extends State { style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), ), ), - // 전체 싱크 이동 - const Text('싱크', style: TextStyle(fontSize: 12, color: JjColors.textDim)), - const SizedBox(width: 6), - SizedBox( - width: 80, - child: TextField( - controller: _shift, - textAlign: TextAlign.right, - style: const TextStyle(fontSize: 13), - decoration: const InputDecoration( - isDense: true, suffixText: 'ms', border: OutlineInputBorder()), + // 폭이 좁으면 (휴대폰) 도구 줄을 밀어서 본다 - 설정 · 종료 버튼은 늘 오른쪽 끝에 + Flexible( + flex: 4, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + // 전체 싱크 이동 + const Text('싱크', style: TextStyle(fontSize: 12, color: JjColors.textDim)), + const SizedBox(width: 6), + SizedBox( + width: 80, + child: TextField( + controller: _shift, + textAlign: TextAlign.right, + style: const TextStyle(fontSize: 13), + decoration: const InputDecoration( + isDense: true, suffixText: 'ms', border: OutlineInputBorder()), + ), + ), + const SizedBox(width: 4), + OutlinedButton( + onPressed: () { + final ms = int.tryParse(_shift.text.trim()); + if (ms == null) return; + e.shiftAll(ms); + }, + child: const Text('전체 이동'), + ), + const SizedBox(width: 8), + OutlinedButton(onPressed: e.sortByTime, child: const Text('시간순 정렬')), + const SizedBox(width: 12), + DropdownButton( + value: e.saveCharset, + isDense: true, + underline: const SizedBox(), + style: const TextStyle(fontSize: 13, color: JjColors.text), + items: [ + for (final c in saveCharsets.entries) + DropdownMenuItem(value: c.key, child: Text(c.value)), + ], + onChanged: (v) => e.setCharset(v!), + ), + const SizedBox(width: 8), + OutlinedButton( + onPressed: e.canSave ? _saveAs : null, + child: const Text('다른 이름으로 저장'), + ), + const SizedBox(width: 8), + FilledButton.icon( + onPressed: e.canSave && !_saving ? _save : null, + icon: const Icon(Icons.save, size: 18), + label: Text(e.dirty ? '저장 (MKV 반영) *' : '저장 (MKV 반영)'), + ), + ]), ), ), - const SizedBox(width: 4), - OutlinedButton( - onPressed: () { - final ms = int.tryParse(_shift.text.trim()); - if (ms == null) return; - e.shiftAll(ms); - }, - child: const Text('전체 이동'), - ), - const SizedBox(width: 8), - OutlinedButton(onPressed: e.sortByTime, child: const Text('시간순 정렬')), - const SizedBox(width: 12), - DropdownButton( - value: e.saveCharset, - isDense: true, - underline: const SizedBox(), - style: const TextStyle(fontSize: 13, color: JjColors.text), - items: [ - for (final c in saveCharsets.entries) - DropdownMenuItem(value: c.key, child: Text(c.value)), - ], - onChanged: (v) => e.setCharset(v!), - ), - const SizedBox(width: 8), - OutlinedButton( - onPressed: e.canSave ? _saveAs : null, - child: const Text('다른 이름으로 저장'), - ), - const SizedBox(width: 8), - FilledButton.icon( - onPressed: e.canSave && !_saving ? _save : null, - icon: const Icon(Icons.save, size: 18), - label: Text(e.dirty ? '저장 (MKV 반영) *' : '저장 (MKV 반영)'), - ), const AppActions(), ], ),