휴대폰 (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(
|
||||
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<DownloadsPage> {
|
|||
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]),
|
||||
|
|
|
|||
|
|
@ -95,14 +95,16 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||
c.externalSubtitlesFor(pl.playlist[i]).then((s) async {
|
||||
if (!mounted) return;
|
||||
setState(() => _externalSubs = s);
|
||||
// 영상 안 · 같은 폴더 자막이 없고 jj_mkv 에 만든 자막 (번역 · 받은 자막) 만 있으면 하나를 켠다 (한국어 먼저).
|
||||
// 같은 폴더 자막은 mpv 가 스스로 켜므로 그 뒤에 (1초) 확인한다.
|
||||
// 켜진 자막이 없으면 하나를 켠다 (한국어 먼저, 없으면 첫 번째). mpv 가 같은 폴더 자막을 불러와도
|
||||
// 켜지 않는 일이 있어 (Android) 그 뒤에 (1초) 확인한다.
|
||||
await Future<void>.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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -460,12 +460,20 @@ class _SubtitleEditorPageState extends State<SubtitleEditorPage> {
|
|||
_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<SubtitleEditorPage> {
|
|||
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<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(),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in New Issue