diff --git a/app/lib/core/providers/device_info.dart b/app/lib/core/providers/device_info.dart new file mode 100644 index 00000000..ade3a7ba --- /dev/null +++ b/app/lib/core/providers/device_info.dart @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024-2025 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:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'device_info.g.dart'; + +@Riverpod(keepAlive: true) +class AndroidDeviceInfo extends _$AndroidDeviceInfo { + @override + Future build() async { + if (!Platform.isAndroid) return null; + + final deviceInfo = DeviceInfoPlugin(); + final androidInfo = await deviceInfo.androidInfo; + + return AndroidDeviceInfoData(sdkInt: androidInfo.version.sdkInt); + } +} + +class AndroidDeviceInfoData { + final int sdkInt; + + const AndroidDeviceInfoData({required this.sdkInt}); +} diff --git a/app/lib/core/providers/device_info.g.dart b/app/lib/core/providers/device_info.g.dart new file mode 100644 index 00000000..74532d06 --- /dev/null +++ b/app/lib/core/providers/device_info.g.dart @@ -0,0 +1,60 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device_info.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(AndroidDeviceInfo) +final androidDeviceInfoProvider = AndroidDeviceInfoProvider._(); + +final class AndroidDeviceInfoProvider + extends $AsyncNotifierProvider { + AndroidDeviceInfoProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'androidDeviceInfoProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$androidDeviceInfoHash(); + + @$internal + @override + AndroidDeviceInfo create() => AndroidDeviceInfo(); +} + +String _$androidDeviceInfoHash() => r'05c6cc63a6ee34f137aef538d65e8ab94b9cca89'; + +abstract class _$AndroidDeviceInfo + extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref, AndroidDeviceInfoData?>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + AsyncValue, + AndroidDeviceInfoData? + >, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/app/lib/core/providers/router.dart b/app/lib/core/providers/router.dart index 2aa69b9b..2d7fbc3c 100644 --- a/app/lib/core/providers/router.dart +++ b/app/lib/core/providers/router.dart @@ -52,3 +52,26 @@ Future router(Ref ref) async { initialLocation: initialLocation ?? const BrowserRoute().location, ); } + +@Riverpod(keepAlive: true) +class CurrentTopRoute extends _$CurrentTopRoute { + @override + RouteBase? build() { + final router = ref.watch(routerProvider).value; + if (router == null) return null; + + void update() { + final config = router.routerDelegate.currentConfiguration; + final match = config.last; + + state = match.route; + } + + router.routerDelegate.addListener(update); + ref.onDispose(() => router.routerDelegate.removeListener(update)); + + update(); + + return state; + } +} diff --git a/app/lib/core/providers/router.g.dart b/app/lib/core/providers/router.g.dart index fb9e2a43..009adf96 100644 --- a/app/lib/core/providers/router.g.dart +++ b/app/lib/core/providers/router.g.dart @@ -42,3 +42,55 @@ final class RouterProvider } String _$routerHash() => r'cbaa7e982114942303574f9573f4a75b79955583'; + +@ProviderFor(CurrentTopRoute) +final currentTopRouteProvider = CurrentTopRouteProvider._(); + +final class CurrentTopRouteProvider + extends $NotifierProvider { + CurrentTopRouteProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'currentTopRouteProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$currentTopRouteHash(); + + @$internal + @override + CurrentTopRoute create() => CurrentTopRoute(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(RouteBase? value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$currentTopRouteHash() => r'71a66713adafb5f64359eec88a69344167aae64f'; + +abstract class _$CurrentTopRoute extends $Notifier { + RouteBase? build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + RouteBase?, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart index 68a79937..57974a2a 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart @@ -28,6 +28,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:nullability/nullability.dart'; import 'package:quick_actions/quick_actions.dart'; import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/core/providers/device_info.dart'; import 'package:weblibre/core/providers/router.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; @@ -248,6 +249,26 @@ class _BrowserViewState extends ConsumerState }, ); + final topRoute = ref.watch(currentTopRouteProvider); + final androidInfoAsync = ref.watch(androidDeviceInfoProvider); + + final isGeckoViewVisible = androidInfoAsync.when( + data: (androidInfo) { + if (androidInfo == null) { + // Not Android, always show GeckoView based on route + return topRoute is GoRoute && topRoute.name == BrowserRoute.name; + } + // Android: only apply visibility fix on Android 12 and lower (API <= 31) + if (androidInfo.sdkInt <= 31) { + return topRoute is GoRoute && topRoute.name == BrowserRoute.name; + } + // Android 13+: always show GeckoView + return true; + }, + loading: () => true, // Show by default while loading + error: (_, _) => true, // Show by default on error + ); + return Listener( behavior: HitTestBehavior.translucent, onPointerMove: (widget.pointerMoveEventSink != null) @@ -259,71 +280,74 @@ class _BrowserViewState extends ConsumerState : null, child: Stack( children: [ - GeckoView( - preInitializationStep: () async { - await ref - .read(eventServiceProvider) - .viewReadyStateEvents - .firstWhere((state) => state == true) - .timeout( - const Duration(seconds: 3), - onTimeout: () { - logger.e( - 'Browser fragement not reported ready, trying to intitialize anyways', - ); - return true; - }, - ); - }, - postInitializationStep: () async { - await widget.postInitializationStep?.call(); + Visibility( + visible: isGeckoViewVisible, + child: GeckoView( + preInitializationStep: () async { + await ref + .read(eventServiceProvider) + .viewReadyStateEvents + .firstWhere((state) => state == true) + .timeout( + const Duration(seconds: 3), + onTimeout: () { + logger.e( + 'Browser fragement not reported ready, trying to intitialize anyways', + ); + return true; + }, + ); + }, + postInitializationStep: () async { + await widget.postInitializationStep?.call(); - if (!initializationCompleter.isCompleted) { - const quickActions = QuickActions(); + if (!initializationCompleter.isCompleted) { + const quickActions = QuickActions(); - //Debounce: https://github.com/flutter/flutter/issues/131121 - DateTime? lastAction; - await quickActions.initialize((type) async { - if (lastAction == null || - DateTime.now().difference(lastAction!) > - const Duration(seconds: 5)) { - if (type == 'new_tab') { - lastAction = DateTime.now(); + //Debounce: https://github.com/flutter/flutter/issues/131121 + DateTime? lastAction; + await quickActions.initialize((type) async { + if (lastAction == null || + DateTime.now().difference(lastAction!) > + const Duration(seconds: 5)) { + if (type == 'new_tab') { + lastAction = DateTime.now(); - final router = await ref.read(routerProvider.future); - const route = SearchRoute(tabType: TabType.regular); + final router = await ref.read(routerProvider.future); + const route = SearchRoute(tabType: TabType.regular); - await router.push(route.location); - } else if (type == 'new_private_tab') { - lastAction = DateTime.now(); + await router.push(route.location); + } else if (type == 'new_private_tab') { + lastAction = DateTime.now(); - final router = await ref.read(routerProvider.future); - const route = SearchRoute(tabType: TabType.private); + final router = await ref.read(routerProvider.future); + const route = SearchRoute(tabType: TabType.private); - await router.push(route.location); - } else { - throw UnimplementedError( - 'Unknown quick action shortcut type', - ); + await router.push(route.location); + } else { + throw UnimplementedError( + 'Unknown quick action shortcut type', + ); + } } - } - }); + }); - await quickActions.setShortcutItems([ - //TODO: add icons - const ShortcutItem( - type: 'new_tab', - localizedTitle: 'New Tab', - ), - const ShortcutItem( - type: 'new_private_tab', - localizedTitle: 'New Private Tab', - ), - ]); + await quickActions.setShortcutItems([ + //TODO: add icons + const ShortcutItem( + type: 'new_tab', + localizedTitle: 'New Tab', + ), + const ShortcutItem( + type: 'new_private_tab', + localizedTitle: 'New Private Tab', + ), + ]); - initializationCompleter.complete(); - } - }, + initializationCompleter.complete(); + } + }, + ), ), if (!hasTab) Positioned.fill( diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 2d4e47b4..be8949bb 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: country_codes: ^3.3.0 country_flags: ^4.1.1 cryptography_flutter: ^2.3.4 + device_info_plus: ^12.3.0 drift: ^2.30.1 drift_dev: ^2.30.1 dynamic_color: ^1.8.1