import 'dart:async'; import 'dart:math' as math; import 'dart:ui' as ui; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/cupertino.dart' show CupertinoPageTransitionsBuilder; import 'package:kolibri/kolibri.dart' show initKolibri; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:video_player_media_kit/video_player_media_kit.dart'; import 'package:komet/l10n/app_localizations.dart'; import 'package:m3e_collection/m3e_collection.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'backend/api.dart'; import 'core/cache/info_cache.dart'; import 'core/utils/logger.dart'; import 'core/cache/self_presence.dart'; import 'core/storage/app_instance.dart'; import 'core/storage/draft_store.dart'; import 'core/storage/archived_chats_store.dart'; import 'core/storage/chat_encryption_store.dart'; import 'core/config/app_accent.dart'; import 'core/config/app_amoled.dart'; import 'core/config/app_show_extra_info.dart'; import 'core/config/app_spectrum_background.dart'; import 'core/config/app_bubble_behavior.dart'; import 'core/config/komet_settings.dart'; import 'core/config/call_no_mute.dart'; import 'core/config/debug_test.dart'; import 'core/config/app_bubble_shape.dart'; import 'core/config/app_cache_extent.dart'; import 'core/config/app_fonts.dart'; import 'core/config/custom_font_service.dart'; import 'core/config/app_message_actions_style.dart'; import 'core/config/app_microphone.dart'; import 'core/config/app_swipe_back_desktop.dart'; import 'core/config/app_pranks.dart'; import 'core/config/app_stories.dart'; import 'core/config/app_commands.dart'; import 'core/config/app_phonebook_names.dart'; import 'core/contacts/device_contacts_service.dart'; import 'core/config/app_link_preview.dart'; import 'core/config/app_media_cache.dart'; import 'core/config/app_video_note_quality.dart'; import 'core/config/app_pill_gradient.dart'; import 'core/config/app_visual_style.dart'; import 'core/config/app_chat_chrome.dart'; import 'core/config/app_composer_background.dart'; import 'core/config/app_composer_style.dart'; import 'core/config/app_nav_pill_style.dart'; import 'core/config/app_wallpaper_tint.dart'; import 'core/storage/chat_wallpaper_store.dart'; import 'core/utils/wallpaper_seed.dart'; import 'core/config/app_theme_mode.dart'; import 'core/config/app_theme_schedule.dart'; import 'core/config/app_digital_id_mode.dart'; import 'backend/modules/account.dart'; import 'backend/modules/chats.dart'; import 'backend/modules/comments.dart'; import 'backend/modules/contacts.dart'; import 'backend/modules/file_uploader.dart'; import 'backend/modules/folders.dart'; import 'backend/modules/messages.dart'; import 'backend/modules/outbox.dart'; import 'backend/modules/polls.dart'; import 'backend/modules/stickers.dart'; import 'backend/modules/animoji.dart'; import 'backend/modules/stories.dart'; import 'backend/modules/self_check.dart'; import 'backend/modules/shared_content.dart'; import 'backend/modules/webapp.dart'; import 'backend/modules/digital_id.dart'; import 'core/calls/call_bridge.dart'; import 'core/calls/call_controller.dart'; import 'core/links/deep_link_service.dart'; import 'frontend/screens/calls/call_screen.dart'; import 'core/push/fkm_controller.dart'; import 'core/push/notification_bridge.dart'; import 'core/share/share_intent_bridge.dart'; import 'core/push/push_service.dart'; import 'core/storage/app_database.dart'; import 'core/transport/tls_config.dart'; import 'core/transport/traffic_monitor.dart'; import 'core/transport/vpn_bypass.dart'; import 'core/storage/token_storage.dart'; import 'core/utils/haptics.dart'; import 'core/utils/debug_session_log.dart'; import 'core/protocol/packet.dart'; import 'frontend/debug/fps_overlay_layer.dart'; import 'frontend/screens/auth/login_screen.dart'; import 'frontend/widgets/adaptive_shell.dart'; import 'frontend/widgets/custom_notification.dart'; import 'frontend/widgets/liquid_glass.dart'; import 'frontend/widgets/small_spinner.dart'; import 'frontend/widgets/theme_reveal.dart'; import 'frontend/widgets/floating_call_badge.dart'; import 'frontend/widgets/floating_video_note.dart'; final api = Api(); final accountModule = AccountModule(api); final messagesModule = MessagesModule(api); final commentsModule = CommentsModule(api); final sharedContentModule = SharedContentModule(api); final pollsModule = PollsModule(api); final stickersModule = StickersModule(api); final animojiModule = AnimojiModule(api); final webAppModule = WebAppModule(api); final digitalIdModule = DigitalIdModule(webAppModule); final fileUploader = FileUploader(api: api, messages: messagesModule); final storiesModule = StoriesModule(api); final bannersModule = accountModule.banners; final RouteObserver> appRouteObserver = RouteObserver>(); bool isOnemeFlavor = false; const ProgressIndicatorThemeData _expressiveProgressTheme = ProgressIndicatorThemeData(year2023: false); const PageTransitionsTheme _appPageTransitions = PageTransitionsTheme( builders: { TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), TargetPlatform.windows: ZoomPageTransitionsBuilder(), TargetPlatform.linux: ZoomPageTransitionsBuilder(), }, ); Future _loadInitialLocale() async { final prefs = await SharedPreferences.getInstance(); final code = prefs.getString('app_locale'); if (code != null && (code == 'en' || code == 'ru')) { return Locale(code); } final platform = WidgetsBinding.instance.platformDispatcher.locale; if (platform.languageCode == 'en' || platform.languageCode == 'ru') { return Locale(platform.languageCode); } return const Locale('ru'); } void _installLogCapture() { final previousDebugPrint = debugPrint; debugPrint = (String? message, {int? wrapWidth}) { if (message != null) { final t = DateTime.now(); final stamp = '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}:${t.second.toString().padLeft(2, '0')}.${t.millisecond.toString().padLeft(3, '0')}'; DebugSessionLog.instance.recordLogLine(' |$stamp P $message'); } previousDebugPrint(message, wrapWidth: wrapWidth); }; final previousFlutterOnError = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails details) { DebugSessionLog.instance.recordLogLine( ' | FlutterError: ${details.exceptionAsString()}', ); if (details.stack != null) { DebugSessionLog.instance.recordLogLine(details.stack.toString()); } if (previousFlutterOnError != null) { previousFlutterOnError(details); } else { FlutterError.presentError(details); } }; final previousPlatformOnError = ui.PlatformDispatcher.instance.onError; ui.PlatformDispatcher.instance.onError = (Object error, StackTrace stack) { DebugSessionLog.instance.recordLogLine(' | Uncaught: $error'); DebugSessionLog.instance.recordLogLine(stack.toString()); if (previousPlatformOnError != null) { return previousPlatformOnError(error, stack); } return false; }; } void main(List args) async { WidgetsFlutterBinding.ensureInitialized(); await initKolibri(); DebugTest.parse(args); CallNoMute.parse(args); _installLogCapture(); VideoPlayerMediaKit.ensureInitialized( windows: true, linux: true, macOS: true, ); if (AppInstance.isNamed) { SharedPreferences.setPrefix('flutter.${AppInstance.id}.'); } await TlsConfig.applyMincifryTrust(); await AppDatabase.init(); final activeAccountId = await TokenStorage.getActiveAccountId(); if (activeAccountId != null) { await ContactsModule.primeCacheFromDb(activeAccountId); } attachInfoCacheApi(api); chats.attachGlobalPushHandlers(api); unawaited(FkmController.instance.init(api)); FoldersModule.attachGlobalPushHandlers(api); TranscriptionPushHandler.attach(api); commentsModule.attachPushHandlers(api); storiesModule.attach(); unawaited(storiesModule.loadCache()); unawaited(DeepLinkService.instance.init()); final packageInfoFuture = PackageInfo.fromPlatform(); final localeFuture = _loadInitialLocale(); final hapticsFuture = Haptics.load(); final prefsFuture = SharedPreferences.getInstance(); final accentFuture = AppAccent.load(); final bubbleShapeFuture = AppBubbleShape.load(); final bubbleBehaviorFuture = AppBubbleBehavior.load(); final cacheExtentFuture = AppCacheExtent.load(); final themeModeFuture = AppThemeModeConfig.load(); final amoledFuture = AppAmoled.load(); final pillGradientFuture = AppPillGradient.load(); final visualStyleFuture = AppVisualStyle.load(); final liquidGlassFuture = LiquidGlass.load(); final chatChromeFuture = AppChatChrome.load(); final composerStyleFuture = AppComposerStyle.load(); final composerBackgroundFuture = AppComposerBackground.load(); final navPillStyleFuture = AppNavPillStyle.load(); final wallpaperTintFuture = AppWallpaperTint.load(); final themeScheduleFuture = AppThemeSchedule.load(); final messageActionsFuture = AppMessageActionsStyle.load(); final swipeBackFuture = AppSwipeBackDesktop.load(); final microphoneFuture = AppMicrophone.load(); final pranksFuture = AppPranks.load(); final storiesFuture = AppStories.load(); final commandsFuture = AppCommands.load(); final phonebookNamesFuture = AppPhonebookNames.load(); final linkPreviewFuture = AppLinkPreview.load(); final cacheLimitFuture = AppMediaCacheLimit.load(); final videoNoteResolutionFuture = AppVideoNoteResolution.load(); final videoNoteFpsFuture = AppVideoNoteFps.load(); final videoNoteRearCameraFuture = AppVideoNoteRearCamera.load(); final digitalIdNativeFuture = AppDigitalIdNative.load(); final showExtraInfoFuture = AppShowExtraInfo.load(); final spectrumBackgroundFuture = AppSpectrumBackground.load(); final trafficCaptureFuture = TrafficMonitor.instance.load(); final debugLogFuture = DebugSessionLog.instance.init(); final packageInfo = await packageInfoFuture; isOnemeFlavor = packageInfo.packageName == 'ru.oneme.app'; final initialLocale = await localeFuture; await hapticsFuture; final prefs = await prefsFuture; await FileHistoryCache.load(prefs); await DraftStore.instance.load(); await ArchivedChatsStore.instance.load(); await ChatEncryptionStore.instance.load(); await KometSettings.load(); if (KometSettings.ghostMode.value) SelfPresence.markOffline(); await ContactCache.load(); final initialFpsOverlay = prefs.getBool('dev_fps_overlay') ?? false; final initialVpnBypass = prefs.getBool(VpnBypassService.prefKey) ?? false; final initialTlsInsecure = prefs.getBool(TlsConfig.prefKey) ?? false; final initialFontId = prefs.getString(AppFonts.prefKey) ?? AppFonts.fallback.id; final initialFontScale = AppFonts.clampScale( prefs.getDouble(AppFonts.scalePrefKey) ?? AppFonts.defaultScale, ); if (AppFonts.resolve(initialFontId).isCustom) { await CustomFontService.preloadCached(); } else { unawaited(CustomFontService.preloadCached()); } final initialAccentSeed = await accentFuture; await Future.wait([ bubbleShapeFuture, bubbleBehaviorFuture, cacheExtentFuture, themeModeFuture, amoledFuture, pillGradientFuture, visualStyleFuture, liquidGlassFuture, chatChromeFuture, composerStyleFuture, composerBackgroundFuture, navPillStyleFuture, wallpaperTintFuture, themeScheduleFuture, messageActionsFuture, swipeBackFuture, microphoneFuture, pranksFuture, storiesFuture, commandsFuture, phonebookNamesFuture, linkPreviewFuture, cacheLimitFuture, videoNoteResolutionFuture, videoNoteFpsFuture, videoNoteRearCameraFuture, digitalIdNativeFuture, showExtraInfoFuture, spectrumBackgroundFuture, ]); await DeviceContactsService.loadFromStartup(); await trafficCaptureFuture; await debugLogFuture; runApp( KometApp( initialLocale: initialLocale, initialFpsOverlay: initialFpsOverlay, initialVpnBypass: initialVpnBypass, initialTlsInsecure: initialTlsInsecure, initialFontId: initialFontId, initialFontScale: initialFontScale, initialAccentSeed: initialAccentSeed, ), ); } class KometApp extends StatefulWidget { const KometApp({ super.key, required this.initialLocale, this.initialFpsOverlay = false, this.initialVpnBypass = false, this.initialTlsInsecure = false, required this.initialFontId, required this.initialFontScale, this.initialAccentSeed, }); final Locale initialLocale; final bool initialFpsOverlay; final bool initialVpnBypass; final bool initialTlsInsecure; final String initialFontId; final double initialFontScale; final Color? initialAccentSeed; static final navigatorKey = GlobalKey(); static KometAppState? stateOf(BuildContext context) { return context.findAncestorStateOfType(); } @override State createState() => KometAppState(); } class KometAppState extends State with WidgetsBindingObserver, TickerProviderStateMixin { static const _fallbackSeed = Color(0xFFC1C4FF); final GlobalKey _captureBoundaryKey = GlobalKey(); OverlayEntry? _revealEntry; AnimationController? _revealController; ui.Image? _revealImage; late Locale _locale; late String _fontId; bool _isLoggingOut = false; bool _shellReady = false; bool _incomingRouteActive = false; IncomingCall? _pendingIncoming; late final ValueNotifier accentSeed = ValueNotifier( widget.initialAccentSeed, ); final ValueNotifier wallpaperSeed = ValueNotifier(null); StreamSubscription? _sessionExpiredSub; StreamSubscription? _loginStatusSub; StreamSubscription? _vpnBypassSub; StreamSubscription? _callIncomingSub; StreamSubscription? _serverErrorSub; StreamSubscription? _accountNoticeSub; Timer? _scheduleTimer; String? _lastVpnNotice; DateTime _lastVpnNoticeAt = DateTime.fromMillisecondsSinceEpoch(0); String? _lastServerError; DateTime _lastServerErrorAt = DateTime.fromMillisecondsSinceEpoch(0); late final ValueNotifier fpsOverlayEnabled = ValueNotifier( widget.initialFpsOverlay, ); late final ValueNotifier vpnBypassEnabled = ValueNotifier( widget.initialVpnBypass, ); late final ValueNotifier tlsInsecureEnabled = ValueNotifier( widget.initialTlsInsecure, ); late final ValueNotifier fontScale = ValueNotifier( widget.initialFontScale, ); final _profileUpdateController = StreamController.broadcast(); Stream get profileUpdateStream => _profileUpdateController.stream; @override void initState() { super.initState(); _locale = widget.initialLocale; _fontId = widget.initialFontId; WidgetsBinding.instance.addObserver(this); AppThemeModeConfig.current.addListener(_onThemeModeChanged); AppAmoled.current.addListener(_onAmoledChanged); AppThemeSchedule.current.addListener(_onScheduleChanged); AppWallpaperTint.current.addListener(_onWallpaperTintChanged); ChatWallpaperStore.instance.revision.addListener(_onWallpaperTintChanged); _lastAppliedThemeMode = _effectiveThemeMode; _rescheduleSwitch(); unawaited(_refreshWallpaperSeed()); api.setReconnectCallback(() async { try { final accountId = await TokenStorage.getActiveAccountId(); if (accountId != null) { final token = await TokenStorage.readToken(accountId); if (token != null) { await accountModule.login(accountId: accountId, token: token); } } } catch (e) { logger.w('reconnect login failed: $e'); } }); _loginStatusSub = accountModule.loginStatusStream.listen((status) async { if (status == LoginStatus.success) { DeepLinkService.instance.markReady(); NotificationBridge.instance.markReady(); ShareIntentBridge.instance.markReady(); unawaited(_refreshWallpaperSeed()); CallController.instance.init(api); OutboxService.instance.init(api, messagesModule); SelfCheckService.instance.init(api); SelfCheckService.instance.checkNow(); if (isOnemeFlavor) { await PushService.instance.init(api: api, account: accountModule); await PushService.instance.onLoginSuccess(); await _ensureFullScreenIntentPermission(); } } }); _callIncomingSub = CallController.instance.incomingCalls.listen( _onIncomingCall, ); CallController.instance.appResumed = true; CallBridge.instance.init(); NotificationBridge.instance.init(); ShareIntentBridge.instance.init(); WidgetsBinding.instance.addPostFrameCallback((_) { CallBridge.instance.checkInitialCall(); unawaited(NotificationBridge.instance.checkInitialChat()); unawaited(ShareIntentBridge.instance.checkInitialShare()); }); _sessionExpiredSub = api.sessionExpiredStream.listen(( SessionExpiredException e, ) async { if (_isLoggingOut) return; _isLoggingOut = true; await PushService.instance.unregister(); final accountId = await TokenStorage.getActiveAccountId(); if (accountId != null) { await accountModule.removeAccount(accountId); } final navState = KometApp.navigatorKey.currentState; if (navState != null) { final overlay = navState.overlay; if (overlay != null) { showCustomNotificationOnOverlay(overlay, e.message); } await navState.pushAndRemoveUntil( MaterialPageRoute(builder: (_) => const LoginScreen()), (route) => false, ); } _isLoggingOut = false; }); _vpnBypassSub = VpnBypassService.instance.events.listen((r) { final msg = r.bound ? 'Обход VPN включён — прямое подключение через ' '${r.boundInterface ?? r.transport ?? 'сеть без VPN'}' : 'Обход VPN не удался, подключение через туннель' '${r.reason != null ? ' (${r.reason})' : ''}'; final now = DateTime.now(); if (msg == _lastVpnNotice && now.difference(_lastVpnNoticeAt).inSeconds < 10) { return; } _lastVpnNotice = msg; _lastVpnNoticeAt = now; final overlay = KometApp.navigatorKey.currentState?.overlay; if (overlay != null) { showCustomNotificationOnOverlay(overlay, msg); } }); _serverErrorSub = api.errorStream.listen((msg) { final now = DateTime.now(); if (msg == _lastServerError && now.difference(_lastServerErrorAt).inSeconds < 3) { return; } _lastServerError = msg; _lastServerErrorAt = now; final overlay = KometApp.navigatorKey.currentState?.overlay; if (overlay != null) { showCustomNotificationOnOverlay(overlay, msg); } }); _accountNoticeSub = accountModule.noticeStream.listen((notice) { final overlay = KometApp.navigatorKey.currentState?.overlay; final ctx = KometApp.navigatorKey.currentContext; if (overlay == null || ctx == null || !ctx.mounted) return; final l10n = AppLocalizations.of(ctx); if (l10n == null) return; final message = switch (notice) { AccountNotice.resurrectingProfile => l10n.profileResurrecting, }; showCustomNotificationOnOverlay(overlay, message); }); } Future _ensureFullScreenIntentPermission() async { final prefs = await SharedPreferences.getInstance(); if (prefs.getBool('fsi_prompted') ?? false) return; if (await CallBridge.instance.canUseFullScreenIntent()) return; await prefs.setBool('fsi_prompted', true); await CallBridge.instance.openFullScreenIntentSettings(); } void _onIncomingCall(IncomingCall call) { _pendingIncoming = call; _presentIncomingCall(); } void markShellReady() { if (_shellReady) return; _shellReady = true; _presentIncomingCall(); } void _presentIncomingCall() { final call = _pendingIncoming; if (call == null || _incomingRouteActive || !_shellReady) return; final navState = KometApp.navigatorKey.currentState; if (navState == null) { WidgetsBinding.instance.addPostFrameCallback( (_) => _presentIncomingCall(), ); return; } _incomingRouteActive = true; navState .push( MaterialPageRoute( builder: (_) => CallScreen( name: ContactCache.get(call.callerId) ?? call.callerName ?? '', avatarUrl: ContactCache.getAvatar(call.callerId), incoming: call, autoAccept: call.autoAccept, ), ), ) .whenComplete(() { _incomingRouteActive = false; if (identical(_pendingIncoming, call)) _pendingIncoming = null; }); } @override void dispose() { _finishReveal(); _sessionExpiredSub?.cancel(); _loginStatusSub?.cancel(); _vpnBypassSub?.cancel(); _callIncomingSub?.cancel(); _serverErrorSub?.cancel(); _accountNoticeSub?.cancel(); _scheduleTimer?.cancel(); AppThemeModeConfig.current.removeListener(_onThemeModeChanged); AppAmoled.current.removeListener(_onAmoledChanged); AppThemeSchedule.current.removeListener(_onScheduleChanged); AppWallpaperTint.current.removeListener(_onWallpaperTintChanged); ChatWallpaperStore.instance.revision.removeListener( _onWallpaperTintChanged, ); WidgetsBinding.instance.removeObserver(this); _profileUpdateController.close(); fpsOverlayEnabled.dispose(); vpnBypassEnabled.dispose(); tlsInsecureEnabled.dispose(); fontScale.dispose(); accentSeed.dispose(); wallpaperSeed.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { CallController.instance.appResumed = state == AppLifecycleState.resumed; if (state == AppLifecycleState.inactive && CallController.instance.isBusy) { unawaited(CallBridge.instance.ensureOngoing()); } if (state == AppLifecycleState.paused || state == AppLifecycleState.hidden || state == AppLifecycleState.detached) { DebugSessionLog.instance.flushNow(); SelfCheckService.instance.pause(); } if (state != AppLifecycleState.resumed) return; api.wakeUp(); SelfCheckService.instance.resume(); if (!CallController.instance.isBusy) { unawaited(CallBridge.instance.dropOngoing()); } CallBridge.instance.checkInitialCall(); unawaited(NotificationBridge.instance.checkInitialChat()); unawaited(ShareIntentBridge.instance.checkInitialShare()); if (AppThemeModeConfig.current.value != AppThemeMode.schedule) return; _rescheduleSwitch(); final next = _effectiveThemeMode; if (next == _lastAppliedThemeMode) return; _lastAppliedThemeMode = next; if (mounted) setState(() {}); } void _onThemeModeChanged() { _rescheduleSwitch(); _lastAppliedThemeMode = _effectiveThemeMode; if (mounted) setState(() {}); } void _onAmoledChanged() { if (mounted) setState(() {}); } void _onScheduleChanged() { if (AppThemeModeConfig.current.value != AppThemeMode.schedule) return; _rescheduleSwitch(); final next = _effectiveThemeMode; if (next == _lastAppliedThemeMode) return; _lastAppliedThemeMode = next; if (mounted) setState(() {}); } ThemeMode _lastAppliedThemeMode = ThemeMode.system; void _rescheduleSwitch() { _scheduleTimer?.cancel(); _scheduleTimer = null; if (AppThemeModeConfig.current.value != AppThemeMode.schedule) return; final until = AppThemeSchedule.current.value.durationUntilNextSwitch( DateTime.now(), ); _scheduleTimer = Timer(until, () { if (!mounted) return; _lastAppliedThemeMode = _effectiveThemeMode; setState(() {}); _rescheduleSwitch(); }); } ThemeMode get _effectiveThemeMode { switch (AppThemeModeConfig.current.value) { case AppThemeMode.system: return ThemeMode.system; case AppThemeMode.light: return ThemeMode.light; case AppThemeMode.dark: return ThemeMode.dark; case AppThemeMode.schedule: final isDark = AppThemeSchedule.current.value.isDarkAt(DateTime.now()); return isDark ? ThemeMode.dark : ThemeMode.light; } } Future applyThemeMode(AppThemeMode mode) async { await AppThemeModeConfig.save(mode); } void applyThemeModeWithReveal(AppThemeMode mode, Offset center) { if (AppThemeModeConfig.current.value == mode) return; _runThemeReveal(center, () => AppThemeModeConfig.save(mode)); } Future applyAmoled(bool value) async { await AppAmoled.save(value); } void applyAmoledWithReveal(bool value, Offset center) { if (AppAmoled.current.value == value) return; _runThemeReveal(center, () => AppAmoled.save(value)); } void _runThemeReveal(Offset center, Future Function() apply) { final overlay = KometApp.navigatorKey.currentState?.overlay; final ctx = _captureBoundaryKey.currentContext; if (overlay == null || ctx == null) { apply(); return; } if (MediaQuery.disableAnimationsOf(ctx)) { apply(); return; } final renderObject = ctx.findRenderObject(); if (renderObject is! RenderRepaintBoundary) { apply(); return; } final ui.Image snapshot; try { final dpr = math.min(MediaQuery.of(ctx).devicePixelRatio, 2.0); snapshot = renderObject.toImageSync(pixelRatio: dpr); } catch (_) { apply(); return; } _finishReveal(); final controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 650), ); final entry = ThemeRevealOverlay.build( snapshot: snapshot, center: center, animation: controller, ); _revealController = controller; _revealEntry = entry; _revealImage = snapshot; overlay.insert(entry); apply(); WidgetsBinding.instance.endOfFrame.then((_) { if (_revealController != controller) return; controller.forward().then((_) { if (_revealController != controller) return; _finishReveal(); }, onError: (_) {}); }); } void _finishReveal() { _revealEntry?.remove(); _revealEntry = null; _revealController?.dispose(); _revealController = null; final img = _revealImage; _revealImage = null; if (img != null) { WidgetsBinding.instance.addPostFrameCallback((_) => img.dispose()); } } Future applyThemeSchedule(ThemeSchedule schedule) async { await AppThemeSchedule.save(schedule); } Future setFpsOverlayEnabled(bool value) async { if (fpsOverlayEnabled.value == value) return; fpsOverlayEnabled.value = value; final prefs = await SharedPreferences.getInstance(); await prefs.setBool('dev_fps_overlay', value); } Future setVpnBypassEnabled(bool value) async { if (vpnBypassEnabled.value == value) return; vpnBypassEnabled.value = value; final prefs = await SharedPreferences.getInstance(); await prefs.setBool(VpnBypassService.prefKey, value); } Future setTlsInsecureEnabled(bool value) async { if (tlsInsecureEnabled.value == value) return; tlsInsecureEnabled.value = value; await TlsConfig.setInsecureAllowed(value); } Future applyLocale(Locale locale) async { if (!AppLocalizations.supportedLocales.any( (l) => l.languageCode == locale.languageCode, )) { return; } final prefs = await SharedPreferences.getInstance(); await prefs.setString('app_locale', locale.languageCode); if (mounted) { setState(() => _locale = locale); } } String get fontId => _fontId; Future applyAccentColor(Color? seed) async { await AppAccent.save(seed); accentSeed.value = seed; } void _onWallpaperTintChanged() => unawaited(_refreshWallpaperSeed()); Future _refreshWallpaperSeed() async { if (!AppWallpaperTint.current.value) { wallpaperSeed.value = null; return; } final profile = await AppDatabase.loadActiveProfile(); final accountId = profile?.id ?? 0; if (accountId == 0) { wallpaperSeed.value = null; return; } await ChatWallpaperStore.instance.load(); final wallpaper = ChatWallpaperStore.instance.get( accountId, kGlobalWallpaperChatId, ); final seed = await computeWallpaperSeed(wallpaper); if (!mounted) return; wallpaperSeed.value = seed; } Future applyAppFont(String fontId) async { if (_fontId == fontId) return; final prefs = await SharedPreferences.getInstance(); await prefs.setString(AppFonts.prefKey, fontId); if (mounted) { setState(() => _fontId = fontId); } } Future applyFontScale(double scale, {bool persist = true}) async { final next = AppFonts.clampScale(scale); fontScale.value = next; if (persist) { final prefs = await SharedPreferences.getInstance(); await prefs.setDouble(AppFonts.scalePrefKey, next); } } void notifyProfileUpdate() { _profileUpdateController.add(null); } String? _themeCacheFontId; ColorScheme? _themeCacheLight; ColorScheme? _themeCacheDark; ThemeData? _lightTheme; ThemeData? _darkTheme; Color? _seedCacheKey; ColorScheme? _seedCacheLight; ColorScheme? _seedCacheDark; ({ColorScheme light, ColorScheme dark}) _schemesForSeed(Color seed) { if (_seedCacheKey == seed && _seedCacheLight != null && _seedCacheDark != null) { return (light: _seedCacheLight!, dark: _seedCacheDark!); } _seedCacheKey = seed; _seedCacheLight = ColorScheme.fromSeed( seedColor: seed, brightness: Brightness.light, ); _seedCacheDark = ColorScheme.fromSeed( seedColor: seed, brightness: Brightness.dark, ); return (light: _seedCacheLight!, dark: _seedCacheDark!); } void _rebuildThemesIfNeeded(ColorScheme light, ColorScheme dark) { if (_themeCacheFontId == _fontId && _themeCacheLight == light && _themeCacheDark == dark) { return; } _themeCacheFontId = _fontId; _themeCacheLight = light; _themeCacheDark = dark; final displayFont = AppDisplayFont(AppFonts.displayFamily(_fontId)); _lightTheme = withM3ETheme( ThemeData( useMaterial3: true, colorScheme: light, pageTransitionsTheme: _appPageTransitions, progressIndicatorTheme: _expressiveProgressTheme, extensions: [displayFont], textTheme: AppFonts.textTheme( _fontId, ThemeData(brightness: Brightness.light).textTheme, ), ), ); _darkTheme = withM3ETheme( ThemeData( useMaterial3: true, colorScheme: dark, pageTransitionsTheme: _appPageTransitions, progressIndicatorTheme: _expressiveProgressTheme, extensions: [displayFont], textTheme: AppFonts.textTheme( _fontId, ThemeData(brightness: Brightness.dark).textTheme, ), ), ); } ColorScheme _adjustDarkScheme(ColorScheme base) { if (AppAmoled.current.value) { return base.copyWith( surface: Colors.black, surfaceContainerLowest: Colors.black, surfaceContainerLow: const Color(0xFF080808), surfaceContainer: const Color(0xFF101010), surfaceContainerHigh: const Color(0xFF161616), surfaceContainerHighest: const Color(0xFF1C1C1C), ); } return base.copyWith( surface: Color.alphaBlend( base.primary.withValues(alpha: 0.05), const Color(0xFF0D0D14), ), surfaceContainerHigh: Color.alphaBlend( base.primary.withValues(alpha: 0.08), const Color(0xFF1A1A26), ), surfaceContainerHighest: Color.alphaBlend( base.primary.withValues(alpha: 0.12), const Color(0xFF262636), ), ); } ColorScheme _adjustLightScheme(ColorScheme base) { return base.copyWith( surface: Color.alphaBlend( base.primary.withValues(alpha: 0.06), const Color(0xFFF5F5FA), ), surfaceContainerHigh: Color.alphaBlend( base.primary.withValues(alpha: 0.08), const Color(0xFFEAEAF2), ), surfaceContainerHighest: Color.alphaBlend( base.primary.withValues(alpha: 0.11), const Color(0xFFDEDEE8), ), ); } @override Widget build(BuildContext context) { return DynamicColorBuilder( builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { return ListenableBuilder( listenable: Listenable.merge([ accentSeed, wallpaperSeed, AppWallpaperTint.current, ]), builder: (context, _) { final seed = AppWallpaperTint.current.value && wallpaperSeed.value != null ? wallpaperSeed.value : accentSeed.value; final ColorScheme lightBase; final ColorScheme darkBase; if (seed != null) { final s = _schemesForSeed(seed); lightBase = s.light; darkBase = s.dark; } else if (lightDynamic != null && darkDynamic != null) { lightBase = lightDynamic; darkBase = darkDynamic; } else { final s = _schemesForSeed(_fallbackSeed); lightBase = lightDynamic ?? s.light; darkBase = darkDynamic ?? s.dark; } final lightScheme = _adjustLightScheme(lightBase); final darkScheme = _adjustDarkScheme(darkBase); _rebuildThemesIfNeeded(lightScheme, darkScheme); return MaterialApp( title: 'Komet', debugShowCheckedModeBanner: false, locale: _locale, themeMode: _effectiveThemeMode, themeAnimationDuration: Duration.zero, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, theme: _lightTheme, darkTheme: _darkTheme, navigatorKey: KometApp.navigatorKey, navigatorObservers: [appRouteObserver], builder: (context, child) { return ValueListenableBuilder( valueListenable: fontScale, child: child ?? const SizedBox.shrink(), builder: (context, scale, appChild) { Widget scaledChild = appChild!; final effective = AppFonts.effectiveScale(_fontId, scale); if ((effective - 1.0).abs() > 0.001) { scaledChild = MediaQuery.withClampedTextScaling( minScaleFactor: effective, maxScaleFactor: effective, child: scaledChild, ); } return ValueListenableBuilder( valueListenable: fpsOverlayEnabled, child: scaledChild, builder: (context, fpsOn, sChild) { return Stack( fit: StackFit.expand, clipBehavior: Clip.none, children: [ RepaintBoundary( key: _captureBoundaryKey, child: sChild!, ), const Positioned.fill( child: FloatingVideoNoteLayer(), ), const Positioned.fill( child: FloatingCallBadgeLayer(), ), if (fpsOn) const FpsOverlayLayer(), ], ); }, ); }, ); }, home: const _StartupScreen(), ); }, ); }, ); } } class _StartupScreen extends StatefulWidget { const _StartupScreen(); @override State<_StartupScreen> createState() => _StartupScreenState(); } class _StartupScreenState extends State<_StartupScreen> { @override void initState() { super.initState(); _tryAutoLogin(); } Future _tryAutoLogin() async { if (DebugTest.enabled) { await Future.delayed(Duration.zero); if (!mounted) return; Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const AdaptiveShell()), ); KometApp.stateOf(context)?.markShellReady(); return; } unawaited(api.connect()); int? accountId = await TokenStorage.getActiveAccountId(); if (accountId == null || await TokenStorage.readToken(accountId) == null) { accountId = await _recoverActiveAccount(); } if (!mounted) return; if (accountId == null) { _goToLogin(); return; } Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const AdaptiveShell()), ); KometApp.stateOf(context)?.markShellReady(); } Future _recoverActiveAccount() async { final profiles = await AppDatabase.loadAllProfiles(); for (final profile in profiles) { if (await TokenStorage.readToken(profile.id) != null) { await TokenStorage.setActiveAccount(profile.id); await AppDatabase.setActiveAccount(profile.id); await ContactsModule.primeCacheFromDb(profile.id); return profile.id; } } return null; } void _goToLogin() { if (mounted) { Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const LoginScreen()), ); KometApp.stateOf(context)?.markShellReady(); } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Scaffold( backgroundColor: cs.surface, body: Center(child: SmallSpinner(size: 36, color: cs.primary)), ); } }