/// 약속: 앱의 뿌리 위젯 (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? navigatorKey; final GlobalKey? 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 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 { const _CupertinoFallbackDelegate(); @override bool isSupported(Locale locale) => true; @override Future 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; }