휴대폰 (Galaxy Z Fold6 커버 화면) 점검에서 찾은 문제 수정
- 자막 편집기: 폭이 좁으면 위 도구 줄 · 아래 버튼 줄이 화면 밖으로 밀려 설정 · 종료 · 저장 버튼이 안 보이던 것 → 가로로 밀어서 보기 - 플레이어: 같은 폴더 자막이 있어도 mpv 가 켜지 않아 자막 없이 재생되던 것 → 한국어 먼저 자동으로 켬 - 다운로드 목록 안내: Android 는 클립보드를 감시하지 않으므로 Ctrl+C 안내 대신 붙여 넣기 · 브라우저 [다운로드] 안내 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
parent
acf82dc323
commit
4cf2ac29ee
|
|
@ -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]),
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue