/* * Copyright (c) 2024-2026 Fabian Freund. * * This file is part of WebLibre * (see https://weblibre.eu). * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'package:background_fetch/background_fetch.dart'; import 'package:country_codes/country_codes.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart' show GeckoBrowserService, GeckoLoggingService, LogLevel; import 'package:home_widget/home_widget.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logger/logger.dart'; import 'package:material_color_utilities/material_color_utilities.dart'; import 'package:nullability/nullability.dart'; import 'package:privacypass_client/privacypass_client.dart'; import 'package:weblibre/core/design/app_colors.dart'; import 'package:weblibre/core/error_observer.dart'; import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/providers/app_state.dart'; import 'package:weblibre/core/providers/defaults.dart'; import 'package:weblibre/core/providers/router.dart'; import 'package:weblibre/domain/services/app_initialization.dart'; import 'package:weblibre/features/account/domain/services/account_callback_handler.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/services/engine_settings_replication.dart'; import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_catalog_service.dart'; import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/services/local_index_pruner.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/services/local_index_settings_sync.dart'; import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_logs.dart'; import 'package:weblibre/features/proxy/domain/services/browser_dns_leak_guard.dart'; import 'package:weblibre/features/proxy/domain/services/singbox_proxy_endpoint_sync.dart'; import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/features/web_feed/presentation/controllers/fetch_articles.dart'; import 'package:weblibre/features/web_feed/utils/fetch_entrypoint.dart'; import 'package:weblibre/features/web_search/domain/controllers/sandbox_capture_controller.dart'; import 'package:weblibre/presentation/hooks/on_initialization.dart'; import 'package:weblibre/presentation/main_app.dart'; ColorScheme _fixSurfaceContainerColors( ColorScheme scheme, TonalPalette neutralPalette, Brightness brightness, ) { if (brightness == Brightness.light) { return scheme.copyWith( surfaceContainerLowest: Color(neutralPalette.get(100)), surfaceContainerLow: Color(neutralPalette.get(96)), surfaceContainer: Color(neutralPalette.get(94)), surfaceContainerHigh: Color(neutralPalette.get(92)), surfaceContainerHighest: Color(neutralPalette.get(90)), ); } else { return scheme.copyWith( surfaceContainerLowest: Color(neutralPalette.get(4)), surfaceContainerLow: Color(neutralPalette.get(10)), surfaceContainer: Color(neutralPalette.get(12)), surfaceContainerHigh: Color(neutralPalette.get(17)), surfaceContainerHighest: Color(neutralPalette.get(22)), ); } } bool _hasBrokenSurfaceContainerColors(ColorScheme scheme) { return scheme.surfaceContainerLowest == scheme.surface && scheme.surfaceContainerLow == scheme.surface && scheme.surfaceContainer == scheme.surface && scheme.surfaceContainerHigh == scheme.surface && scheme.surfaceContainerHighest == scheme.surface; } class _NoAnimationPageTransitionsBuilder extends PageTransitionsBuilder { const _NoAnimationPageTransitionsBuilder(); @override Widget buildTransitions( PageRoute route, BuildContext context, Animation animation, Animation secondaryAnimation, Widget child, ) { return child; } } const _noAnimationPageTransitionsTheme = PageTransitionsTheme( builders: { TargetPlatform.android: _NoAnimationPageTransitionsBuilder(), TargetPlatform.iOS: _NoAnimationPageTransitionsBuilder(), TargetPlatform.linux: _NoAnimationPageTransitionsBuilder(), TargetPlatform.macOS: _NoAnimationPageTransitionsBuilder(), TargetPlatform.windows: _NoAnimationPageTransitionsBuilder(), }, ); class _MainWidget extends HookConsumerWidget { const _MainWidget(); @override Widget build(BuildContext context, WidgetRef ref) { // Keep the sandbox capture controller alive for the lifetime of the app // so it can react to pigeon events even when no UI subscribes to it. ref.watch(sandboxCaptureControllerProvider); // Keep proxy/Tor log subscriptions active from app start so startup // messages reach the ring buffer before the browser view (or logs // screen) mounts and would otherwise drop them. ref.watch(singboxProxyLogsProvider.select((_) => null)); final rootKey = ref.watch(appStateKeyProvider); final pauseTime = useRef(null); useOnAppLifecycleStateChange((previous, current) async { switch (current) { case AppLifecycleState.resumed: if (pauseTime.value != null && DateTime.now().difference(pauseTime.value!) > const Duration(minutes: 30)) { final routerConfig = ref .read(routerProvider) .value ?.routerDelegate .currentConfiguration; //Rebuild widget tree after long time of inactivity ref.read(appStateKeyProvider.notifier).reset(); //Wait for the new router to start await Future.delayed( const Duration(milliseconds: 250), ).whenComplete(() { routerConfig.mapNotNull( (routerConfig) => ref.read(routerProvider).value?.restore(routerConfig), ); }); logger.i('UI reset'); } pauseTime.value = null; case AppLifecycleState.detached: case AppLifecycleState.inactive: case AppLifecycleState.hidden: case AppLifecycleState.paused: pauseTime.value ??= DateTime.now(); } }); final themeMode = ref.watch( generalSettingsWithDefaultsProvider.select((value) => value.themeMode), ); final uiScaleFactor = ref.watch( generalSettingsWithDefaultsProvider.select( (value) => value.uiScaleFactor, ), ); final disableAnimations = ref.watch( generalSettingsWithDefaultsProvider.select( (value) => value.disableAnimations, ), ); final showModalBarrier = ref.watch( generalSettingsWithDefaultsProvider.select( (value) => value.showModalBarrier, ), ); useOnInitialization(() async { await CountryCodes.init(); final engineSettings = await ref .read(engineSettingsRepositoryProvider.notifier) .fetchSettings(); final generalSettings = await ref .read(generalSettingsRepositoryProvider.notifier) .fetchSettings(); final startupUBlockFilterListsPref = engineSettings.ublockFilterListSettings.enabled ? jsonEncode( engineSettings.ublockFilterListSettings.resolveFinalList(), ) : null; final clearStartupUBlockFilterListsPref = !engineSettings.ublockFilterListSettings.enabled; try { await GeckoBrowserService().initialize( filesystem.relativeProfilePath, kDebugMode ? LogLevel.debug : LogLevel.warn, engineSettings.contentBlocking, engineSettings.addonCollection, generalSettings.syncServerOverride, generalSettings.syncTokenServerOverride, engineSettings, startupUBlockFilterListsPref, clearStartupUBlockFilterListsPref, ); } on PlatformException catch (e, s) { logger.e( 'Platform exception during Gecko initialization', error: e, stackTrace: s, ); rethrow; } catch (e, s) { logger.e( 'Failed to initialize Gecko browser service', error: e, stackTrace: s, ); rethrow; } // Mirror the startup uBO pref into the fixator so later pref changes are // still observed and enforced after native startup initialization. try { await syncUBlockFilterLists( ref.read(preferenceFixatorProvider.notifier), engineSettings, ); } catch (e, s) { logger.w( 'Failed to sync uBlock filter list pref at startup', error: e, stackTrace: s, ); } await ref.read(appInitializationServiceProvider.notifier).initialize(); Future preloadUrlCleanerCatalog() async { if (!generalSettings.urlCleanerEnabled) { return; } try { await ref.read(urlCleanerCatalogServiceProvider.future); } catch (e, s) { logger.w( 'Failed preloading URL cleaner catalog', error: e, stackTrace: s, ); } } unawaited(preloadUrlCleanerCatalog()); // Wire settings → local_index_setting (tab.db) so the trigger gate // is in sync from the moment tabs start writing. ref.read(localIndexSettingsSyncProvider); // Cold-start prune of the local search index — drops rows the engine // has forgotten (Places retention, user-initiated clears). Cheap and // background; failures are logged and ignored. unawaited(ref.read(localIndexPrunerProvider.notifier).prune()); // Activate account callback deep link handler ref.read(accountCallbackHandlerProvider); // Activate the proxy DNS leak guard: when any sing-box profile is // running AND the user has opted in, GeckoView TRR is forced to // off so browser DNS follows the SOCKS proxyDNS path instead of // resolving outside the proxy. ref.read(browserDnsLeakGuardProvider); // Mirror the sing-box runtime's SOCKS endpoints into Gecko's // container-proxy registry. Side-effect-only notifier. ref.read(singboxProxyEndpointSyncProvider); if (!kDebugMode) { await BackgroundFetch.configure( BackgroundFetchConfig( minimumFetchInterval: 15, enableHeadless: true, stopOnTerminate: false, requiredNetworkType: NetworkType.ANY, startOnBoot: true, ), (String taskId) async { try { await ref .read(fetchArticlesControllerProvider.notifier) .fetchAllArticles(); logger.i('Fetched articles in foreground'); } catch (e, s) { logger.e('Failed fetching articles', error: e, stackTrace: s); } finally { await BackgroundFetch.finish(taskId); } }, ); } }); final corePaletteSnapshot = useFuture( useMemoized(() => DynamicColorPlugin.getCorePalette()), ); return DynamicColorBuilder( builder: (lightDynamic, darkDynamic) { ColorScheme lightColorScheme; ColorScheme darkColorScheme; if (lightDynamic != null && darkDynamic != null) { final corePalette = corePaletteSnapshot.data; // On Android S+ devices, use the provided dynamic color scheme. // (Recommended) Harmonize the dynamic color scheme' built-in semantic colors. final harmonizedLight = lightDynamic.harmonized(); final harmonizedDark = darkDynamic.harmonized(); // Workaround for https://github.com/material-foundation/flutter-packages/issues/649 // dynamic_color package returns broken surfaceContainer* colors. // Fix them using the neutral tonal palette from CorePalette. if (corePalette != null) { lightColorScheme = _hasBrokenSurfaceContainerColors(harmonizedLight) ? _fixSurfaceContainerColors( harmonizedLight, corePalette.neutral, Brightness.light, ) : harmonizedLight; darkColorScheme = _hasBrokenSurfaceContainerColors(harmonizedDark) ? _fixSurfaceContainerColors( harmonizedDark, corePalette.neutral, Brightness.dark, ) : harmonizedDark; } else { lightColorScheme = harmonizedLight; darkColorScheme = harmonizedDark; } } else { // Otherwise, use fallback schemes. lightColorScheme = ColorScheme.fromSeed( seedColor: ref.read(lightSeedColorFallbackProvider), ); darkColorScheme = ColorScheme.fromSeed( seedColor: ref.read(darkSeedColorFallbackProvider), brightness: Brightness.dark, ); } return MainApp( key: rootKey, theme: ThemeData( useMaterial3: true, colorScheme: lightColorScheme, pageTransitionsTheme: disableAnimations ? _noAnimationPageTransitionsTheme : null, dialogTheme: DialogThemeData( barrierColor: showModalBarrier ? null : Colors.transparent, ), bottomSheetTheme: BottomSheetThemeData( modalBarrierColor: showModalBarrier ? null : Colors.transparent, ), extensions: const >[AppColors.light], ), darkTheme: ThemeData( useMaterial3: true, colorScheme: darkColorScheme, pageTransitionsTheme: disableAnimations ? _noAnimationPageTransitionsTheme : null, dialogTheme: DialogThemeData( barrierColor: showModalBarrier ? null : Colors.transparent, ), bottomSheetTheme: BottomSheetThemeData( modalBarrierColor: showModalBarrier ? null : Colors.transparent, ), extensions: const >[AppColors.dark], ), themeMode: themeMode, uiScaleFactor: uiScaleFactor, disableAnimations: disableAnimations, ); }, ); } } void main() async { WidgetsFlutterBinding.ensureInitialized(); FlutterError.onError = (e) { logger.e(e.toString(), error: e.exception, stackTrace: e.stack); }; PlatformDispatcher.instance.onError = (error, stack) { logger.e('Unhandled Error', error: error, stackTrace: stack); return true; }; await filesystem.init(); await RustLib.init(); if (!kDebugMode) { await BackgroundFetch.registerHeadlessTask(backgroundFetch); } if (kDebugMode) { final serviceProtocolInfo = await Service.getInfo(); logger.d('VM: ${serviceProtocolInfo.serverUri}'); } //Ensure everything is ready await Future.delayed(Duration.zero); GeckoLoggingService.setUp((level, message) { logger.log(switch (level) { LogLevel.debug => Level.debug, LogLevel.info => Level.info, LogLevel.warn => Level.warning, LogLevel.error => Level.error, }, message); }); await HomeWidget.setAppGroupId('weblibre'); runApp( const ProviderScope(observers: [ErrorObserver()], child: _MainWidget()), ); }