JJ_MKVMaker/lib/ui/app_root.dart

174 lines
6.8 KiB
Dart

/// 약속: 앱의 뿌리 위젯 (MaterialApp - 화면 언어 · 밝기 · 시스템 막대 · 끌어다 놓기 · 화면 배율) 과 제목 글 [appTitle].
library;
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart' show CupertinoLocalizations, DefaultCupertinoLocalizations;
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import '../app/app_controller.dart';
import '../app/bookmarks_controller.dart';
import '../app/download_manager.dart';
import 'app_actions.dart';
import 'app_drop.dart';
import 'browser_page.dart';
import 'home_page.dart';
import 'theme.dart';
import '../l10n/tr.dart';
import '../app/i18n_controller.dart';
/// 제목 표시줄 글: "JJ_MKVMaker v1.2.3"
String appTitle = 'JJ_MKVMaker';
class JjMkvMakerApp extends StatelessWidget {
final AppController controller;
final DownloadManager? downloads;
final BookmarksController? bookmarks;
final GlobalKey<NavigatorState>? navigatorKey;
final GlobalKey<ScaffoldMessengerState>? messengerKey;
final VoidCallback? onExit;
const JjMkvMakerApp({
super.key,
required this.controller,
this.downloads,
this.bookmarks,
this.navigatorKey,
this.messengerKey,
this.onExit,
});
/// 140 · 158: Flutter 기본 글 (뒤로 · Licenses 등) 을 화면 언어로 - 앱 언어 코드 → Locale
/// 앱이 더한 언어 (기계 번역 사전) 도 Flutter 가 아는 언어면 그 언어로, 모르는 언어일 때만 영어로 (한국어가 섞이지 않게).
/// "zh-Hant" · "pt-BR" 같은 코드는 글자 체계 (4글자) · 나라 (2글자 · 숫자) 로 나눈다.
static Locale localeOf(String code) {
final parts = code.split(RegExp('[-_]'));
String? script;
String? country;
for (final s in parts.skip(1)) {
if (s.length == 4) {
script = s;
} else if (s.isNotEmpty) {
country = s;
}
}
final l = Locale.fromSubtags(languageCode: parts.first.toLowerCase(), scriptCode: script, countryCode: country?.toUpperCase());
return GlobalMaterialLocalizations.delegate.isSupported(l) ? l : const Locale('en');
}
/// Flutter 기본 글 사전들. Material 은 알고 Cupertino 는 모르는 언어 (예: ps) 에서도 Cupertino 글이 비지 않게 영어로 채운다.
static const localizationsDelegates = [...GlobalMaterialLocalizations.delegates, _CupertinoFallbackDelegate()];
/// Flutter 가 아는 모든 언어. 지금 언어를 맨 앞에 두어 글자 체계 · 나라까지 그대로 고르게 ("zh-Hant" 가 간체로 바뀌지 않게).
static List<Locale> supportedLocalesFor(Locale current) => [
current,
for (final c in kMaterialSupportedLanguages)
if (c != current.languageCode || current.scriptCode != null || current.countryCode != null) Locale(c),
];
@override
Widget build(BuildContext context) => _BrightnessHost(
c: controller,
// 화면 언어를 바꾸면 (i18n) Flutter 기본 글도 그 언어로
builder: (context) => ListenableBuilder(listenable: i18n, builder: (context, _) => _app(context)),
);
Widget _app(BuildContext context) {
final locale = localeOf(uiLanguage);
return MaterialApp(
locale: locale,
supportedLocales: supportedLocalesFor(locale),
localizationsDelegates: localizationsDelegates,
title: appTitle,
debugShowCheckedModeBanner: false,
theme: buildTheme(),
// 밝기 바꿈은 화면 전체를 다시 그려 바로 (전환 그림 없이 - JjColors 를 직접 읽는 곳과 같은 때 바뀌게)
themeAnimationDuration: Duration.zero,
navigatorKey: navigatorKey,
scaffoldMessengerKey: messengerKey,
navigatorObservers: [browserRouteObserver],
// 모든 화면에: 환경 설정 · 종료 버튼이 쓸 것 + 끌어다 놓은 동영상을 목록에 추가
builder: (context, child) => AppScope(
controller: controller,
onExit: onExit,
downloads: downloads,
bookmarks: bookmarks,
child: AppDropArea(c: controller, child: _SystemBarsArea(child: UiScaler(c: controller, child: child!)))),
home: HomePage(c: controller, downloads: downloads, onExit: onExit, bookmarks: bookmarks),
);
}
}
/// X-plore 비교 5-1 ⑦: 환경 설정 "화면 밝기" · 기기 밝기를 따라 [JjColors] 를 바꾸고 화면 전체를 다시 그린다 (상태는 그대로)
class _BrightnessHost extends StatefulWidget {
final AppController c;
final WidgetBuilder builder;
const _BrightnessHost({required this.c, required this.builder});
@override
State<_BrightnessHost> createState() => _BrightnessHostState();
}
class _BrightnessHostState extends State<_BrightnessHost> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
widget.c.addListener(_check);
JjColors.use(_want());
}
@override
void dispose() {
widget.c.removeListener(_check);
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
Brightness _want() =>
resolveBrightness(widget.c.settings.display.brightness, WidgetsBinding.instance.platformDispatcher.platformBrightness);
@override
void didChangePlatformBrightness() => _check();
void _check() {
if (!mounted || !JjColors.use(_want())) return;
setState(() {});
// JjColors 를 직접 읽는 화면 조각도 새 색으로 (그 조각들은 Theme 이 바뀌어도 다시 그려지지 않는다)
WidgetsBinding.instance.addPostFrameCallback((_) {
final root = WidgetsBinding.instance.rootElement;
if (root != null) rebuildAllElements(root);
});
}
@override
Widget build(BuildContext context) => widget.builder(context);
}
/// Cupertino 글이 없는 언어만 영어로 (GlobalCupertinoLocalizations 뒤에 두어 그쪽이 아는 언어는 그쪽이 먼저)
class _CupertinoFallbackDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
const _CupertinoFallbackDelegate();
@override
bool isSupported(Locale locale) => true;
@override
Future<CupertinoLocalizations> load(Locale locale) => DefaultCupertinoLocalizations.load(locale);
@override
bool shouldReload(_CupertinoFallbackDelegate old) => false;
}
/// Android: 앱 화면이 상태 표시줄 · 아래쪽 작업 표시줄 밑까지 그려지므로 (Android 15 부터 기본)
/// 위쪽 버튼 줄이 시계 · 배터리 표시와 겹치지 않게 그만큼 띄운다. 전체 화면 재생 중에는 표시줄이 숨어 여백도 없다.
class _SystemBarsArea extends StatelessWidget {
final Widget child;
const _SystemBarsArea({required this.child});
@override
Widget build(BuildContext context) => Platform.isAndroid
? ColoredBox(color: Theme.of(context).scaffoldBackgroundColor, child: SafeArea(child: child))
: child;
}