휴대폰 (Galaxy Z Fold6 커버 화면) 점검에서 찾은 문제 수정

- 자막 편집기: 폭이 좁으면 위 도구 줄 · 아래 버튼 줄이 화면 밖으로 밀려 설정 · 종료 · 저장 버튼이 안 보이던 것 → 가로로 밀어서 보기
- 플레이어: 같은 폴더 자막이 있어도 mpv 가 켜지 않아 자막 없이 재생되던 것 → 한국어 먼저 자동으로 켬
- 다운로드 목록 안내: Android 는 클립보드를 감시하지 않으므로 Ctrl+C 안내 대신 붙여 넣기 · 브라우저 [다운로드] 안내

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
RyuJongJin 2026-10-03 01:46:53 +09:00
parent acf82dc323
commit 4cf2ac29ee
3 changed files with 88 additions and 62 deletions

View File

@ -59,9 +59,12 @@ class _DownloadsPageState extends State<DownloadsPage> {
child: TextField( child: TextField(
controller: _url, controller: _url,
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),
decoration: const InputDecoration( decoration: InputDecoration(
isDense: true, isDense: true,
hintText: 'YouTube 주소 · 마그넷 링크 · .torrent 주소 붙여넣기 (Ctrl+C 만 해도 자동 추가)', // Android 는 클립보드를 감시하지 않는다
hintText: Platform.isAndroid
? 'YouTube 주소 · 마그넷 링크 · .torrent 주소 붙여넣기'
: 'YouTube 주소 · 마그넷 링크 · .torrent 주소 붙여넣기 (Ctrl+C 만 해도 자동 추가)',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
onSubmitted: (_) => _add(), onSubmitted: (_) => _add(),
@ -129,9 +132,13 @@ class _DownloadsPageState extends State<DownloadsPage> {
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: d.tasks.isEmpty child: d.tasks.isEmpty
? const Center( ? Center(
child: Text('다운로드가 없습니다.\nYouTube 주소나 마그넷 링크를 복사(Ctrl+C)하면 자동으로 받습니다.', child: Text(
textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim))) Platform.isAndroid
? '다운로드가 없습니다.\nYouTube 주소 · 마그넷 링크를 위 칸에 붙여 넣거나, 웹 브라우저에서 [다운로드] 를 누르세요.'
: '다운로드가 없습니다.\nYouTube 주소나 마그넷 링크를 복사(Ctrl+C)하면 자동으로 받습니다.',
textAlign: TextAlign.center,
style: const TextStyle(color: JjColors.textDim)))
: ListView.builder( : ListView.builder(
itemCount: d.tasks.length, itemCount: d.tasks.length,
itemBuilder: (_, i) => _DownloadRow(d: d, t: d.tasks[i]), itemBuilder: (_, i) => _DownloadRow(d: d, t: d.tasks[i]),

View File

@ -95,14 +95,16 @@ class _PlayerPageState extends State<PlayerPage> {
c.externalSubtitlesFor(pl.playlist[i]).then((s) async { c.externalSubtitlesFor(pl.playlist[i]).then((s) async {
if (!mounted) return; if (!mounted) return;
setState(() => _externalSubs = s); setState(() => _externalSubs = s);
// 영상 안 · 같은 폴더 자막이 없고 jj_mkv 에 만든 자막 (번역 · 받은 자막) 만 있으면 하나를 켠다 (한국어 먼저). // 켜진 자막이 없으면 하나를 켠다 (한국어 먼저, 없으면 첫 번째). mpv 가 같은 폴더 자막을 불러와도
// 같은 폴더 자막은 mpv 가 스스로 켜므로 그 뒤에 (1초) 확인한다. // 켜지 않는 일이 있어 (Android) 그 뒤에 (1초) 확인한다.
await Future<void>.delayed(const Duration(seconds: 1)); await Future<void>.delayed(const Duration(seconds: 1));
final st = pl.state.value; final st = pl.state.value;
if (!mounted || s.isEmpty || st.index != i || st.subtitleId != null || st.subtitleTracks.isNotEmpty) return; if (!mounted || st.index != i || st.subtitleId != null) return;
final pick = s.firstWhere((f) => RegExp(r'[._-](ko|kor)[._]', caseSensitive: false).hasMatch(p.basename(f)), final all = _subtitleChoices();
orElse: () => s.first); if (all.isEmpty) return;
await _setSub(TrackInfo('file:$pick', p.basename(pick), file: pick)); 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));
}); });
} }
} }

View File

@ -460,12 +460,20 @@ class _SubtitleEditorPageState extends State<SubtitleEditorPage> {
_pl?.setRate(r!); _pl?.setRate(r!);
}, },
), ),
const Spacer(), // 폭이 좁으면 (휴대폰) 밀어서 본다. 넓으면 지금처럼 오른쪽에 붙는다 (reverse)
btn('+ 여기에 새 줄', 'F7', _insertHere), Expanded(
btn('선택 줄 재생', 'F8', hasSel ? _playSelected : null), child: SingleChildScrollView(
btn('시작 = 현재', 'F9', hasSel ? _setStartHere : null), scrollDirection: Axis.horizontal,
btn('끝 = 현재', 'F10', hasSel ? _setEndHere : null), reverse: true,
btn('선택 줄부터 여기로 맞추기', '선택한 줄과 그 뒤 모든 줄을 함께 이동', hasSel ? _alignHere : null), 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<SubtitleEditorPage> {
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
), ),
), ),
// 전체 싱크 이동 // 폭이 좁으면 (휴대폰) 도구 줄을 밀어서 본다 - 설정 · 종료 버튼은 늘 오른쪽 끝에
const Text('싱크', style: TextStyle(fontSize: 12, color: JjColors.textDim)), Flexible(
const SizedBox(width: 6), flex: 4,
SizedBox( child: SingleChildScrollView(
width: 80, scrollDirection: Axis.horizontal,
child: TextField( child: Row(mainAxisSize: MainAxisSize.min, children: [
controller: _shift, // 전체 싱크 이동
textAlign: TextAlign.right, const Text('싱크', style: TextStyle(fontSize: 12, color: JjColors.textDim)),
style: const TextStyle(fontSize: 13), const SizedBox(width: 6),
decoration: const InputDecoration( SizedBox(
isDense: true, suffixText: 'ms', border: OutlineInputBorder()), 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<String>(
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<String>(
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(), const AppActions(),
], ],
), ),