JJ_MKVMaker/lib/ui/app_bar/app_nav_buttons.dart

315 lines
14 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/// 약속: 화면 이동 - 모든 화면이 쓰는 것 ([AppScope]: 설정 · 종료 · 다운로드 · 즐겨찾기) 과 왼쪽 공통 버튼 ([AppNavButtons]: 홈 · MKV · 뒤로 ·
/// 다운로드 목록, 켠 화면 차례로 옮기기 · 홈 화면 열기).
library;
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../app/bookmarks_controller.dart';
import '../../app/components.dart';
import '../../app/download_manager.dart';
import '../../app/settings.dart';
import '../../services/app_shell.dart' show appIconButtonAsset;
import '../browser_page.dart';
import '../downloads_page.dart';
import '../ai_image_page.dart';
import '../explorer_page.dart';
import '../tor_page.dart';
import '../theme.dart';
import '../../l10n/tr.dart';
import 'app_top_bar.dart';
import '../app_actions.dart';
/// 앱 전체에서 쓰는 것 (환경 설정을 열 컨트롤러 · 종료 동작) 을 화면들에 내려 준다
class AppScope extends InheritedWidget {
final AppController controller;
/// 종료 버튼을 눌렀을 때 (없으면 버튼을 숨김)
final VoidCallback? onExit;
/// 다운로드 목록 · 웹 브라우저 (홈 화면이 브라우저일 때). 새 창 (재생 창) 에서는 없음.
final DownloadManager? downloads;
final BookmarksController? bookmarks;
const AppScope(
{super.key, required this.controller, this.onExit, this.downloads, this.bookmarks, required super.child});
static AppScope? maybeOf(BuildContext context) => context.dependOnInheritedWidgetOfExactType<AppScope>();
@override
bool updateShouldNotify(AppScope old) =>
controller != old.controller || onExit != old.onExit || downloads != old.downloads || bookmarks != old.bookmarks;
}
/// 모든 화면의 위쪽 막대 맨 왼쪽 (같은 자리 · 같은 순서):
/// [JJ] 홈 화면 (환경 설정의 "홈 화면": MKV 화면 · 웹 브라우저 · 파일 탐색기)
/// [▤] MKV 화면으로 [🌐] 웹 브라우저 [📁] 파일 탐색기 [←] 뒤로 [⇩] 다운로드 목록
class AppNavButtons extends StatelessWidget {
/// 다운로드 목록 화면 자신 (그 버튼을 "지금 여기" 로 표시)
final bool onDownloadsPage;
/// 웹 브라우저 화면 자신
final bool onBrowserPage;
/// 파일 탐색기 화면 자신
final bool onExplorerPage;
/// Rsync 화면 자신
final bool onRsyncPage;
/// AI 그림 화면 자신 (123)
final bool onAiPage;
/// Tor 브라우저 화면 자신
final bool onTorPage;
/// 174: MKV 화면 자신 - 홈 화면이 탐색기 등이면 MKV 화면이 맨 처음 화면이 아닐 수 있어 따로 알린다
final bool onMkvPage;
const AppNavButtons(
{super.key,
this.onDownloadsPage = false,
this.onBrowserPage = false,
this.onExplorerPage = false,
this.onRsyncPage = false,
this.onAiPage = false,
this.onTorPage = false,
this.onMkvPage = false});
/// 버튼 줄의 가장 넓은 폭 (JJ · 화면 6개 · 뒤로)
static const double width = 8 * 40;
/// MKV 화면 (맨 처음 화면) 까지 돌아가기
static void toMkv(BuildContext context) => Navigator.of(context).popUntil((r) => r.isFirst);
/// 홈 화면 (환경 설정의 "홈 화면") 의 컴포넌트 id. 그 컴포넌트를 제거했으면 설치된 첫 화면.
static String homeId(AppSettings s) {
final want = switch (s.display.startScreen) { 'browser' => 'browser', 'files' => 'explorer', 'tor' => 'tor', _ => 'mkv' };
final pages = AppComponent.pages(s.display.components, s.display.navOrder);
if (pages.any((p) => p.id == want)) return want;
return pages.isEmpty ? 'mkv' : pages.first.id;
}
/// 그 화면으로 (이미 열려 있으면 그 화면으로 돌아감). 'mkv' 는 맨 처음 화면.
static Future<void> openPage(NavigatorState nav, String id,
{required AppController c, DownloadManager? downloads, BookmarksController? bookmarks}) async {
switch (id) {
case 'mkv':
nav.popUntil((r) => r.isFirst);
case 'browser':
if (bookmarks != null) await BrowserPage.open(nav, c: c, downloads: downloads, bookmarks: bookmarks);
case 'explorer':
await ExplorerPage.open(nav, c: c);
case 'rsync':
await ExplorerPage.openRsync(nav, c: c);
case 'downloads':
if (downloads != null) await DownloadsPage.open(nav, downloads);
case 'aiimage':
await AiImagePage.open(nav, c: c);
case 'tor':
await TorPage.open(nav, c: c);
}
}
/// 이 창에서 열 수 있는 화면 (브라우저 · 다운로드가 없는 새 창에서는 그 둘을 뺀다)
static List<AppComponent> pagesOf(AppScope scope) => [
for (final p in AppComponent.pages(scope.controller.settings.display.components, scope.controller.settings.display.navOrder))
if ((p.id != 'browser' || scope.bookmarks != null) && (p.id != 'downloads' || scope.downloads != null)) p,
];
static void open(BuildContext context, String id) {
final scope = AppScope.maybeOf(context);
if (scope == null) return;
openPage(Navigator.of(context), id, c: scope.controller, downloads: scope.downloads, bookmarks: scope.bookmarks);
}
/// 홈 화면으로: MKV 화면까지 돌아간 뒤, 홈 화면이 다른 화면이면 그 화면을 연다
static void toHome(BuildContext context) {
final scope = AppScope.maybeOf(context);
final nav = Navigator.of(context);
nav.popUntil((r) => r.isFirst);
if (scope == null) return;
final id = homeId(scope.controller.settings);
if (id != 'mkv') open(context, id);
}
/// 지금 화면에서 [step] 만큼 (−1 이전 · +1 다음) 옮긴 화면으로. 끝 다음은 처음으로.
static void step(BuildContext context, String current, int step) {
final scope = AppScope.maybeOf(context);
if (scope == null) return;
final pages = pagesOf(scope);
if (pages.length < 2) return;
var i = pages.indexWhere((p) => p.id == current);
if (i < 0) i = 0;
final j = i + step;
// 102 · 66: 끝에서 처음으로 돌기를 끄면 끝에서 멈춘다
final wraps = j < 0 || j >= pages.length;
if (!scope.controller.settings.display.swipeWrap && wraps) {
_hint(context, step > 0 ? tr('마지막 화면입니다') : tr('첫 화면입니다'));
return;
}
final target = pages[j % pages.length];
// 66-2: 민 방향으로 미끄러져 들어오게 (다음 = 오른쪽에서, 이전 = 왼쪽에서). 이미 열린 화면으로 돌아가면 그 화면의 전환대로
NavSlide.pending = step > 0 ? 1 : -1;
Future<void>.delayed(const Duration(milliseconds: 600), () => NavSlide.pending = 0);
// 66-2: 끝에서 처음으로 (처음에서 끝으로) 넘어갈 때 알린다 - 어디로 갔는지 몰라 헤매지 않게
if (wraps) {
_hint(context, step > 0 ? trf('처음으로: {0}', [tr(target.name)]) : trf('마지막으로: {0}', [tr(target.name)]));
}
open(context, target.id);
}
static void _hint(BuildContext context, String text) {
final m = ScaffoldMessenger.maybeOf(context);
m?.hideCurrentSnackBar();
m?.showSnackBar(SnackBar(content: Text(text), duration: const Duration(milliseconds: 1500)));
}
@override
Widget build(BuildContext context) {
final scope = AppScope.maybeOf(context);
// 컴포넌트 설치 · 순서를 바꾸면 바로 다시 그린다 (이 위젯은 const 로 쓰여 부모가 다시 그려도 그대로이므로)
if (scope == null) return _build(context, null);
return ListenableBuilder(listenable: scope.controller, builder: (context, _) => _build(context, scope));
}
Widget _build(BuildContext context, AppScope? scope) {
final nav = Navigator.maybeOf(context);
final atRoot = !(nav?.canPop() ?? false);
final current = onDownloadsPage
? 'downloads'
: onBrowserPage
? 'browser'
: onExplorerPage
? 'explorer'
: onRsyncPage
? 'rsync'
: onAiPage
? 'aiimage'
: onTorPage
? 'tor'
: onMkvPage || atRoot
? 'mkv'
: '';
final pages = scope == null ? const <AppComponent>[] : pagesOf(scope);
final homeName = switch (scope == null ? 'mkv' : homeId(scope.controller.settings)) {
'browser' => tr('웹 브라우저'),
'explorer' => tr('파일 탐색기'),
'rsync' => 'Rsync',
'downloads' => tr('다운로드 목록'),
'tor' => tr('Tor 브라우저'),
_ => tr('MKV 화면'),
};
String tip(String id, bool here) => switch (id) {
'mkv' => here ? tr('MKV 화면 (지금 여기)') : tr('MKV 화면으로'),
'browser' => here ? tr('웹 브라우저 (지금 여기)') : tr('웹 브라우저'),
'explorer' => here ? tr('파일 탐색기 (지금 여기)') : tr('파일 탐색기'),
'rsync' => here ? tr('Rsync (지금 여기)') : 'Rsync',
'downloads' => here ? tr('다운로드 목록 (지금 여기)') : tr('다운로드 목록'),
'aiimage' => here ? tr('AI 그림 (지금 여기)') : tr('AI 그림'),
'tor' => here ? tr('Tor 브라우저 (지금 여기)') : tr('Tor 브라우저'),
_ => id,
};
// 좁은 화면 (폰): 아이콘 아래에 짧은 이름 - 글자 없는 작은 아이콘만 늘어서 무엇인지 몰랐던 것
final labels = isCompact(context);
// 지금 화면 버튼 아래 밑줄 (색만으로는 알아보기 어려운 사람도 어디인지 알게)
Widget underline(Widget child, bool here) => !here
? child
: Stack(clipBehavior: Clip.none, children: [
child,
Positioned(
left: 10,
right: 10,
bottom: 0,
child: Container(
key: const ValueKey('nav-here-underline'),
height: 3,
decoration: BoxDecoration(color: JjColors.accent, borderRadius: BorderRadius.circular(2))),
),
]);
Widget btn0(Widget icon, String tip, VoidCallback? f, {bool here = false, String label = ''}) => labels
? Tooltip(
message: tip,
child: InkWell(
onTap: f,
borderRadius: BorderRadius.circular(8),
// 66-2: 지금 화면 아이콘은 파랗게 · 뒤에 옅은 바탕
child: Container(
decoration: here
? BoxDecoration(color: JjColors.accent.withValues(alpha: 0.16), borderRadius: BorderRadius.circular(8))
: null,
width: 54,
height: 46,
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
IconTheme(
data: IconThemeData(
size: 20, color: here ? JjColors.accent : (f == null ? JjColors.textDim : JjColors.text)),
child: icon,
),
const SizedBox(height: 2),
// 253: 글자가 크면 칸 (높이 46) 아래로 넘쳤다 - 남는 높이에 맞게 줄인다
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(label,
maxLines: 1,
overflow: TextOverflow.clip,
style: TextStyle(fontSize: 10, color: here ? JjColors.accent : JjColors.textDim)),
),
),
]),
),
),
)
: SizedBox(
width: 40,
height: 40,
child: IconButton(
tooltip: tip,
padding: EdgeInsets.zero,
isSelected: here,
// 66-2: 지금 화면 아이콘은 파랗게 · 뒤에 옅은 바탕 (넓은 화면에서도 어디인지 보이게)
style: here
? IconButton.styleFrom(
foregroundColor: JjColors.accent, backgroundColor: JjColors.accent.withValues(alpha: 0.16))
: null,
icon: icon,
onPressed: f,
),
);
Widget btn(Widget icon, String tip, VoidCallback? f, {bool here = false, String label = ''}) => underline(btn0(icon, tip, f, here: here, label: label), here);
String short(String id) => switch (id) {
'mkv' => 'MKV',
'browser' => tr('브라우저'),
'explorer' => tr('탐색기'),
'rsync' => 'Rsync',
'downloads' => tr('다운로드'),
'aiimage' => tr('AI 그림'),
'tor' => 'Tor',
_ => id,
};
return Row(mainAxisSize: MainAxisSize.min, children: [
btn(
Image.asset(appIconButtonAsset(scope?.controller.settings.display.appIcon),
width: labels ? 22 : 26, height: labels ? 22 : 26, filterQuality: FilterQuality.medium),
trf('홈 화면 ({0}) · 환경 설정에서 바꿈', [homeName]),
scope == null ? null : () => toHome(context),
label: tr('홈'),
),
// 설치한 화면들 (환경 설정 > 컴포넌트 의 순서). 하나뿐이면 버튼 줄은 그것만.
for (final p in pages)
btn(
Icon(p.icon, color: p.id == current ? JjColors.accent : null),
tip(p.id, p.id == current),
p.id == current ? null : () => open(context, p.id),
here: p.id == current,
label: short(p.id),
),
btn(const Icon(Icons.arrow_back), tr('뒤로'),
// 브라우저 화면: 뒤로 키는 웹 페이지 뒤로 (PopScope) 이지만 이 버튼은 화면 이동이라 바로 닫는다
// 위쪽 ← 는 늘 화면 닫기 (브라우저 · 탐색기는 뒤로 키가 웹 뒤로 · 선택 풀기 · 상위 폴더를 먼저 하므로 그것을 거치지 않게 - 49)
atRoot ? null : () => onBrowserPage || onTorPage || onExplorerPage || onRsyncPage ? Navigator.pop(context) : Navigator.maybePop(context),
label: tr('뒤로')),
]);
}
}