191 lines
8.6 KiB
Dart
191 lines
8.6 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../app/app_controller.dart';
|
|
import '../app/bookmarks_controller.dart';
|
|
import '../app/download_manager.dart';
|
|
import 'app_actions.dart';
|
|
import 'exit_guard.dart';
|
|
import 'network_settings.dart' show DnsFailureLine;
|
|
import 'theme.dart';
|
|
import '../l10n/tr.dart';
|
|
import 'home/log_panel.dart';
|
|
import 'home/video_detail.dart';
|
|
import 'home/video_list_view.dart';
|
|
import 'home/encode_bar.dart';
|
|
import 'home/home_top_bar.dart';
|
|
|
|
class HomePage extends StatelessWidget {
|
|
final AppController c;
|
|
|
|
/// 다운로드 (없으면 "다운로딩" 상자 숨김)
|
|
final DownloadManager? downloads;
|
|
|
|
/// 종료 버튼 (없으면 숨김)
|
|
final VoidCallback? onExit;
|
|
|
|
/// 즐겨찾기 (없으면 브라우저 버튼 숨김)
|
|
final BookmarksController? bookmarks;
|
|
|
|
const HomePage({super.key, required this.c, this.downloads, this.onExit, this.bookmarks});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListenableBuilder(
|
|
listenable: c,
|
|
builder: (context, _) => ExitGuard(
|
|
c: c,
|
|
downloads: downloads,
|
|
child: Scaffold(
|
|
// 끌어다 놓기는 앱 전체에서 받는다 (ui/app_drop.dart 의 AppDropArea - 어느 화면에서 놓아도 이 목록에 추가)
|
|
body: SwipeNav(
|
|
current: 'mkv',
|
|
child: Column(
|
|
children: [
|
|
HomeTopBar(c: c, downloads: downloads, onExit: onExit, bookmarks: bookmarks),
|
|
const Divider(height: 1),
|
|
EncodeBar(c: c),
|
|
const Divider(height: 1),
|
|
Expanded(
|
|
// 좁은 화면 (접은 폴드 · 휴대폰 세로): 목록을 위에, 자세히 보기를 아래에
|
|
child: isCompact(context) && c.videos.isEmpty
|
|
// 8: 좁은 화면에 동영상이 없으면 빈 목록 · 안내로 나누지 않고 한 화면에 안내 + [동영상 추가]
|
|
? _EmptyHint(onAdd: c.pickVideos)
|
|
: isCompact(context)
|
|
? _CompactBody(c: c)
|
|
: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// 13: 큰 화면 (태블릿 세로 등) 은 목록도 넓게 - 제목이 앞부분만 보여 구분이 안 되던 것
|
|
SizedBox(
|
|
width: (MediaQuery.sizeOf(context).width * 0.4).clamp(320.0, 640.0),
|
|
child: HomeVideoList(c: c)),
|
|
const VerticalDivider(width: 1),
|
|
Expanded(
|
|
// 목록이 비었으면 [동영상 추가] 안내, 동영상은 있는데 고른 것이 없으면 "목록에서 누르세요"
|
|
child: c.selected != null
|
|
? VideoDetailPane(c: c, v: c.selected!)
|
|
: c.videos.isEmpty
|
|
? _EmptyHint(onAdd: c.pickVideos)
|
|
: const _PickHint(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Divider(height: 1),
|
|
// 249: DNS 서버에 닿지 않는 동안 작업 기록 위에 (아래 안내를 놓쳐도 MKV 화면에서 보고 바꿀 수 있게)
|
|
Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: DnsFailureLine(c: c, compact: isCompact(context))),
|
|
if (c.settings.display.showLog)
|
|
SizedBox(
|
|
// 화면이 낮은 기기 (휴대폰 가로) 에서 목록 · 자세히 보기를 가리지 않도록 화면 높이에 맞춘다.
|
|
// 256: 폰 가로 (높이 500 미만) 는 더 작게 - 목록이 한 줄만 보였다
|
|
// 관리자 판단 (10/17, 15 지적): 폰 세로에서 기록이 비었으면 안내 한 줄 높이만 (빈 자리만 없앰), 기록이 생기면 예전 높이
|
|
height: isCompact(context) && c.logs.isEmpty
|
|
? LogPanel.emptyHeight
|
|
: MediaQuery.sizeOf(context).height < 500
|
|
? 52
|
|
: (MediaQuery.sizeOf(context).height * 0.22).clamp(80.0, 140.0),
|
|
child: LogPanel(
|
|
logs: c.logs,
|
|
onClose: () => c.updateSettings((s) => s.display.showLog = false),
|
|
),
|
|
)
|
|
else
|
|
LogStrip(
|
|
last: c.logs.isEmpty ? '' : c.logs.last,
|
|
onOpen: () => c.updateSettings((s) => s.display.showLog = true),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ───────── 상단 바 ─────────
|
|
|
|
/// 좁은 화면 (폰 세로): 목록 위 · 자세히 아래.
|
|
/// 15 지적 · 관리자 판단 (10/17): 목록을 높이 32% 로 고정하면 줄이 적을 때 빈 칸이 남고, 많을 때 4~5줄만 보였다.
|
|
/// '자동' (처음 값) 은 목록이 내용만큼 차지하되 자세히 칸의 최소 높이를 남기고, 남는 공간은 목록에.
|
|
/// 환경 설정 "폰 세로 목록 크기" 로 작게 (25%) · 크게 (55%) 를 고를 수 있다.
|
|
class _CompactBody extends StatelessWidget {
|
|
final AppController c;
|
|
const _CompactBody({required this.c});
|
|
|
|
/// 줄 하나의 가장 작은 높이 (이보다 줄이 많으면 내용이 칸보다 길다고 보고 줄여 그리지 않는다)
|
|
static const _rowMin = 48.0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => LayoutBuilder(builder: (context, box) {
|
|
final h = box.maxHeight;
|
|
final picked = c.selected != null;
|
|
final cap = switch (c.settings.mkv.compactList) {
|
|
'small' => h * 0.25,
|
|
'large' => h * 0.55,
|
|
// 자동: 자세히 칸 최소 - 고른 동영상이 있으면 62% (세부 정보 · 자막 버튼이 칸 밖으로 밀리지 않게, 261 · 266),
|
|
// 없으면 안내 한 덩이만큼
|
|
_ => h - (picked ? h * 0.62 : math.min(120.0, h * 0.4)),
|
|
}.clamp(math.min(120.0, h), h).toDouble();
|
|
// 줄이 많아 내용이 칸보다 확실히 길면 칸 높이 그대로 (shrinkWrap 은 줄을 모두 그려 느리다)
|
|
final shrink = c.settings.mkv.compactList == 'auto' && c.videos.length * _rowMin < cap;
|
|
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
|
if (shrink)
|
|
ConstrainedBox(constraints: BoxConstraints(maxHeight: cap), child: HomeVideoList(c: c, shrink: true))
|
|
else
|
|
SizedBox(height: cap, child: HomeVideoList(c: c)),
|
|
const Divider(height: 1),
|
|
Expanded(child: picked ? VideoDetailPane(c: c, v: c.selected!) : const _PickHint()),
|
|
]);
|
|
});
|
|
}
|
|
|
|
/// 목록에 동영상이 있는데 고른 것이 없을 때 (15 지적: 빈 목록 안내 "동영상 추가로 파일을 고르세요" 가 나와 헷갈렸다)
|
|
class _PickHint extends StatelessWidget {
|
|
const _PickHint();
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Center(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
Icon(Icons.touch_app_outlined, size: 40, color: JjColors.textDim),
|
|
const SizedBox(height: 8),
|
|
Text(tr('고른 동영상이 없습니다'), textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim)),
|
|
const SizedBox(height: 4),
|
|
Text(tr('목록에서 동영상을 누르면 여기에 자세히 보입니다'),
|
|
textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim, fontSize: 12)),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
|
|
class _EmptyHint extends StatelessWidget {
|
|
final VoidCallback? onAdd;
|
|
const _EmptyHint({this.onAdd});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.video_library_outlined, size: 56, color: JjColors.textDim),
|
|
const SizedBox(height: 12),
|
|
Text(tr('"동영상 추가" 로 파일을 선택하세요'), textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim)),
|
|
const SizedBox(height: 4),
|
|
Text(tr('같은 폴더의 자막은 자동으로 추가됩니다'),
|
|
textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim, fontSize: 12)),
|
|
if (onAdd != null) ...[
|
|
const SizedBox(height: 16),
|
|
FilledButton.icon(onPressed: onAdd, icon: const Icon(Icons.add), label: Text(tr('동영상 추가'))),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|