diff --git a/app/lib/features/geckoview/domain/providers/tab_state.dart b/app/lib/features/geckoview/domain/providers/tab_state.dart index 6c5a6ddb..c978d024 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.dart @@ -254,3 +254,15 @@ AsyncValue selectedTabContainerId(Ref ref) { return const AsyncData(null); } + +@Riverpod() +Stream selectedTabScrollY(Ref ref, Duration sampleTime) { + final tabId = ref.watch(selectedTabProvider); + final eventService = ref.watch(eventServiceProvider); + + return eventService.scrollEvent + .where((event) => event.tabId == tabId) + .sampleTime(sampleTime) + .map((event) => event.scrollY) + .asBroadcastStream(); +} diff --git a/app/lib/features/geckoview/domain/providers/tab_state.g.dart b/app/lib/features/geckoview/domain/providers/tab_state.g.dart index 0c71e5d0..a8023ab1 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.g.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.g.dart @@ -193,6 +193,128 @@ final selectedTabContainerIdProvider = Provider>.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef SelectedTabContainerIdRef = ProviderRef>; +String _$selectedTabScrollYHash() => + r'7ca7cccfa7ebb7f13e0d7c096ce6c93f9e220cb5'; + +/// See also [selectedTabScrollY]. +@ProviderFor(selectedTabScrollY) +const selectedTabScrollYProvider = SelectedTabScrollYFamily(); + +/// See also [selectedTabScrollY]. +class SelectedTabScrollYFamily extends Family> { + /// See also [selectedTabScrollY]. + const SelectedTabScrollYFamily(); + + /// See also [selectedTabScrollY]. + SelectedTabScrollYProvider call(Duration sampleTime) { + return SelectedTabScrollYProvider(sampleTime); + } + + @override + SelectedTabScrollYProvider getProviderOverride( + covariant SelectedTabScrollYProvider provider, + ) { + return call(provider.sampleTime); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'selectedTabScrollYProvider'; +} + +/// See also [selectedTabScrollY]. +class SelectedTabScrollYProvider extends AutoDisposeStreamProvider { + /// See also [selectedTabScrollY]. + SelectedTabScrollYProvider(Duration sampleTime) + : this._internal( + (ref) => selectedTabScrollY(ref as SelectedTabScrollYRef, sampleTime), + from: selectedTabScrollYProvider, + name: r'selectedTabScrollYProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$selectedTabScrollYHash, + dependencies: SelectedTabScrollYFamily._dependencies, + allTransitiveDependencies: + SelectedTabScrollYFamily._allTransitiveDependencies, + sampleTime: sampleTime, + ); + + SelectedTabScrollYProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.sampleTime, + }) : super.internal(); + + final Duration sampleTime; + + @override + Override overrideWith( + Stream Function(SelectedTabScrollYRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: SelectedTabScrollYProvider._internal( + (ref) => create(ref as SelectedTabScrollYRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + sampleTime: sampleTime, + ), + ); + } + + @override + AutoDisposeStreamProviderElement createElement() { + return _SelectedTabScrollYProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is SelectedTabScrollYProvider && + other.sampleTime == sampleTime; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, sampleTime.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin SelectedTabScrollYRef on AutoDisposeStreamProviderRef { + /// The parameter `sampleTime` of this provider. + Duration get sampleTime; +} + +class _SelectedTabScrollYProviderElement + extends AutoDisposeStreamProviderElement + with SelectedTabScrollYRef { + _SelectedTabScrollYProviderElement(super.provider); + + @override + Duration get sampleTime => (origin as SelectedTabScrollYProvider).sampleTime; +} + String _$tabStatesHash() => r'66cff6a7b36328ee23b89fdd53046987b346bfcd'; /// See also [TabStates]. diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index 6080f20d..d4bf5590 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -99,7 +99,7 @@ class BrowserScreen extends HookConsumerWidget { ), ), child: Scaffold( - bottomNavigationBar: Consumer( + bottomNavigationBar: HookConsumer( builder: (context, ref, child) { final tabInFullScreen = ref.watch( selectedTabStateProvider.select( @@ -107,8 +107,30 @@ class BrowserScreen extends HookConsumerWidget { ), ); + final hidden = useState(false); + final diffAcc = useRef(0.0); + + ref.listen( + selectedTabScrollYProvider(const Duration(milliseconds: 250)), + (previous, next) { + if (previous?.valueOrNull != null && + next.valueOrNull != null) { + final diff = previous!.value! - next.value!; + if (diff < 0) { + diffAcc.value += diff; + if (diffAcc.value.abs() > kToolbarHeight * 2) { + hidden.value = true; + } + } else if (diff > 0) { + diffAcc.value = 0.0; + hidden.value = false; + } + } + }, + ); + return Visibility( - visible: !tabInFullScreen, + visible: !tabInFullScreen && !hidden.value, child: BrowserBottomAppBar(displayedSheet: displayedSheet), ); }, diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/middleware/FlutterEventMiddleware.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/middleware/FlutterEventMiddleware.kt index e3396be8..4878ce44 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/middleware/FlutterEventMiddleware.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/middleware/FlutterEventMiddleware.kt @@ -133,6 +133,15 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd ) { _ -> } } } + is ReaderAction.UpdateReaderScrollYAction -> { + runOnUiThread { + flutterEvents.onScrollChange( + System.currentTimeMillis(), + action.tabId, + action.scrollY.toLong() + ) { _ -> } + } + } else -> { //logger.debug("Event fired: " + action.javaClass.name) } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index 2e9ba203..22f4c780 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -3998,6 +3998,23 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val } } } + fun onScrollChange(timestampArg: Long, tabIdArg: String, scrollYArg: Long, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onScrollChange$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(timestampArg, tabIdArg, scrollYArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(GeckoPigeonUtils.createConnectionError(channelName))) + } + } + } } /** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ class GeckoLogging(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart index 6ee1c54d..8ee71b73 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart @@ -19,6 +19,7 @@ typedef IconUpdateEvent = ({String url, Uint8List bytes}); typedef ThumbnailEvent = ({String tabId, Uint8List? bytes}); typedef FindResultsEvent = ({String tabId, List results}); typedef LongPressEvent = ({String tabId, HitResult hitResult}); +typedef ScrollEvent = ({String tabId, int scrollY}); class GeckoEventService extends GeckoStateEvents { // Stream controllers @@ -37,6 +38,7 @@ class GeckoEventService extends GeckoStateEvents { final _thumbnailSubject = PublishSubject(); final _findResultsSubject = PublishSubject(); final _longPressSubject = PublishSubject(); + final _scrollEventSubject = PublishSubject(); final _tabAddedSubject = PublishSubject(); @@ -56,6 +58,7 @@ class GeckoEventService extends GeckoStateEvents { Stream get thumbnailEvents => _thumbnailSubject.stream; Stream get findResultsEvent => _findResultsSubject.stream; Stream get longPressEvent => _longPressSubject.stream; + Stream get scrollEvent => _scrollEventSubject.stream; Stream get tabAddedStream => _tabAddedSubject.stream; @@ -166,6 +169,14 @@ class GeckoEventService extends GeckoStateEvents { _tabAddedSubject.addWhenMoreRecent(timestamp, null, tabId); } + @override + void onScrollChange(int timestamp, String tabId, int scrollY) { + _scrollEventSubject.addWhenMoreRecent(timestamp, tabId, ( + tabId: tabId, + scrollY: scrollY, + )); + } + GeckoEventService.setUp({ BinaryMessenger? binaryMessenger, String messageChannelSuffix = '', @@ -188,6 +199,7 @@ class GeckoEventService extends GeckoStateEvents { unawaited(_thumbnailSubject.close()); unawaited(_findResultsSubject.close()); unawaited(_longPressSubject.close()); + unawaited(_scrollEventSubject.close()); unawaited(_tabAddedSubject.close()); } } diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index 8b5339fa..026bd7bb 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -4338,6 +4338,8 @@ abstract class GeckoStateEvents { void onLongPress(int timestamp, String id, HitResult hitResult); + void onScrollChange(int timestamp, String tabId, int scrollY); + static void setUp(GeckoStateEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; { @@ -4750,6 +4752,37 @@ abstract class GeckoStateEvents { }); } } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onScrollChange$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onScrollChange was null.'); + final List args = (message as List?)!; + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onScrollChange was null, expected non-null int.'); + final String? arg_tabId = (args[1] as String?); + assert(arg_tabId != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onScrollChange was null, expected non-null String.'); + final int? arg_scrollY = (args[2] as int?); + assert(arg_scrollY != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onScrollChange was null, expected non-null int.'); + try { + api.onScrollChange(arg_timestamp!, arg_tabId!, arg_scrollY!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } } } diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index ece7f7f0..556c08c3 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -1050,6 +1050,8 @@ abstract class GeckoStateEvents { void onFindResults(int timestamp, String id, List results); void onLongPress(int timestamp, String id, HitResult hitResult); + + void onScrollChange(int timestamp, String tabId, int scrollY); } @FlutterApi()