From 849823f5df6f831571cfe9753d619723c8d6b737 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Tue, 13 Jan 2026 11:03:26 +0100 Subject: [PATCH] impelement geckoview intent handling via viewport api; fix lints and ui issues --- app/lib/core/design/app_colors.dart | 6 +- app/lib/domain/entities/equatable_image.dart | 7 +- .../features/geckoview/domain/providers.dart | 12 + .../geckoview/domain/providers.g.dart | 47 ++++ .../browser/presentation/screens/browser.dart | 58 +++++ .../BaseBrowserFragment.kt | 18 ++ .../GlobalComponents.kt | 8 + .../api/GeckoBrowserApiImpl.kt | 13 + .../api/GeckoViewportApiImpl.kt | 93 ++++++++ .../feature/KeyboardVisibilityFeature.kt | 223 ++++++++++++++++++ .../pigeons/Gecko.g.kt | 131 ++++++++++ .../lib/flutter_mozilla_components.dart | 1 + .../src/domain/services/gecko_viewport.dart | 128 ++++++++++ .../lib/src/pigeons/gecko.g.dart | 142 +++++++++++ .../pigeons/gecko.dart | 65 +++++ packages/flutter_tor/example/lib/main.dart | 17 +- packages/flutter_tor/example/pubspec.yaml | 22 +- packages/flutter_tor/lib/flutter_tor.dart | 2 - 18 files changed, 960 insertions(+), 33 deletions(-) create mode 100644 packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoViewportApiImpl.kt create mode 100644 packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/feature/KeyboardVisibilityFeature.kt create mode 100644 packages/flutter_mozilla_components/lib/src/domain/services/gecko_viewport.dart diff --git a/app/lib/core/design/app_colors.dart b/app/lib/core/design/app_colors.dart index 5fc73b63..4d03cf3d 100644 --- a/app/lib/core/design/app_colors.dart +++ b/app/lib/core/design/app_colors.dart @@ -80,6 +80,10 @@ class AppColors extends ThemeExtension { /// Get AppColors from the current theme static AppColors of(BuildContext context) { - return Theme.of(context).extension() ?? light; + return Theme.of(context).extension() ?? + switch (Theme.of(context).brightness) { + Brightness.dark => dark, + Brightness.light => light, + }; } } diff --git a/app/lib/domain/entities/equatable_image.dart b/app/lib/domain/entities/equatable_image.dart index 0cfe0564..1ff8ad38 100644 --- a/app/lib/domain/entities/equatable_image.dart +++ b/app/lib/domain/entities/equatable_image.dart @@ -39,8 +39,11 @@ class EquatableImage { void dispose() { if (_isDisposed) return; _isDisposed = true; - _value?.dispose(); - _value = null; + // Delay disposal to allow widgets to finish rendering + Future.delayed(const Duration(milliseconds: 500), () { + _value?.dispose(); + _value = null; + }); } @override diff --git a/app/lib/features/geckoview/domain/providers.dart b/app/lib/features/geckoview/domain/providers.dart index 083d92d7..da261969 100644 --- a/app/lib/features/geckoview/domain/providers.dart +++ b/app/lib/features/geckoview/domain/providers.dart @@ -171,6 +171,18 @@ GeckoSuggestionsService engineSuggestionsService(Ref ref) { return service; } +@Riverpod(keepAlive: true) +GeckoViewportService viewportService(Ref ref) { + final service = GeckoViewportService(); + service.setUp(); + + ref.onDispose(() async { + await service.dispose(); + }); + + return service; +} + @Riverpod(keepAlive: true) class EngineReadyState extends _$EngineReadyState { @override diff --git a/app/lib/features/geckoview/domain/providers.g.dart b/app/lib/features/geckoview/domain/providers.g.dart index 4812cf04..213a9021 100644 --- a/app/lib/features/geckoview/domain/providers.g.dart +++ b/app/lib/features/geckoview/domain/providers.g.dart @@ -246,6 +246,53 @@ final class EngineSuggestionsServiceProvider String _$engineSuggestionsServiceHash() => r'1ec1192f0c5c86cecc7ad448ee2b039f7a48e32b'; +@ProviderFor(viewportService) +final viewportServiceProvider = ViewportServiceProvider._(); + +final class ViewportServiceProvider + extends + $FunctionalProvider< + GeckoViewportService, + GeckoViewportService, + GeckoViewportService + > + with $Provider { + ViewportServiceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'viewportServiceProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$viewportServiceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + GeckoViewportService create(Ref ref) { + return viewportService(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(GeckoViewportService value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$viewportServiceHash() => r'bab39db3180bb6a1cf8c055966b7f5910b41b424'; + @ProviderFor(EngineReadyState) final engineReadyStateProvider = EngineReadyStateProvider._(); 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 acbe3863..3b726b91 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -57,6 +57,12 @@ import 'package:weblibre/features/user/data/models/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/utils/ui_helper.dart' as ui_helper; +/// Callback for toolbar animation progress updates. +/// [progress] is 0.0 when hidden, 1.0 when fully visible. +/// [heightPx] is the toolbar height in pixels. +typedef ToolbarAnimationCallback = + void Function(double progress, double heightPx); + /// Animated toolbar that slides in/out without changing layout constraints. /// Uses SlideTransition to animate visual transform while maintaining /// constant intrinsic size for layout purposes. @@ -64,6 +70,8 @@ class _AnimatedToolbar extends HookWidget { final bool visible; final TabBarPosition position; final Widget child; + final double toolbarHeight; + final ToolbarAnimationCallback? onAnimationProgress; static const _kAnimationDuration = Duration(milliseconds: 250); @@ -71,6 +79,9 @@ class _AnimatedToolbar extends HookWidget { required this.visible, required this.position, required this.child, + required this.toolbarHeight, + // ignore: unused_element_parameter + this.onAnimationProgress, }); @override @@ -80,6 +91,18 @@ class _AnimatedToolbar extends HookWidget { initialValue: visible ? 1.0 : 0.0, ); + // Listen to animation changes and report progress + useEffect(() { + if (onAnimationProgress == null) return null; + + void listener() { + onAnimationProgress?.call(controller.value, toolbarHeight); + } + + controller.addListener(listener); + return () => controller.removeListener(listener); + }, [onAnimationProgress, toolbarHeight]); + useEffect(() { if (visible) { unawaited(controller.forward()); @@ -230,6 +253,7 @@ class BrowserScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final eventService = ref.watch(eventServiceProvider); + final viewportService = ref.watch(viewportServiceProvider); final tabInFullScreen = ref.watch( selectedTabStateProvider.select((value) => value?.isFullScreen ?? false), @@ -339,6 +363,38 @@ class BrowserScreen extends HookConsumerWidget { ? topAppBarTotalHeight : 0.0; + // Get pixel ratio for converting logical pixels to physical pixels + final pixelRatio = MediaQuery.of(context).devicePixelRatio; + + // Set up GeckoView dynamic toolbar height + // This tells GeckoView the maximum toolbar space so it can adjust viewport + final bottomToolbarHeightPx = bottomToolbarVisible + ? (bottomAppBarTotalHeight * pixelRatio).round() + : 0; + useEffect(() { + final lastKeyboardEvent = viewportService.keyboardEvents.valueOrNull; + + if (lastKeyboardEvent == null || !lastKeyboardEvent.isVisible) { + unawaited( + viewportService.setDynamicToolbarMaxHeight(bottomToolbarHeightPx), + ); + } + + return null; + }, [bottomToolbarHeightPx]); + + // Listen to keyboard visibility changes from native + useOnStreamChange( + viewportService.keyboardEvents, + onData: (event) { + if (event.isVisible && event.heightPx > 0) { + // When keyboard is visible, notify GeckoView to adjust viewport + // This uses the native API to handle keyboard without Flutter resize + unawaited(viewportService.setDynamicToolbarMaxHeight(event.heightPx)); + } + }, + ); + // Theme with dynamic snackbar margin to position above bottom toolbar final themeData = Theme.of(context).copyWith( bottomSheetTheme: BottomSheetThemeData( @@ -412,6 +468,7 @@ class BrowserScreen extends HookConsumerWidget { child: _AnimatedToolbar( position: TabBarPosition.bottom, visible: bottomToolbarVisible, + toolbarHeight: bottomAppBarTotalHeight, child: _TabBar( tabBarPosition: TabBarPosition.bottom, displayAppBar: displayAppBar, @@ -435,6 +492,7 @@ class BrowserScreen extends HookConsumerWidget { child: _AnimatedToolbar( position: TabBarPosition.top, visible: topToolbarVisible, + toolbarHeight: topAppBarTotalHeight, child: _TabBar( tabBarPosition: TabBarPosition.top, showMainToolbar: true, diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt index b3d92086..1429aa9e 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt @@ -24,6 +24,7 @@ import eu.weblibre.flutter_mozilla_components.addons.WebExtensionActionPopupActi import eu.weblibre.flutter_mozilla_components.addons.WebExtensionPromptFeature import eu.weblibre.flutter_mozilla_components.databinding.FragmentBrowserBinding import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey +import eu.weblibre.flutter_mozilla_components.feature.KeyboardVisibilityFeature import eu.weblibre.flutter_mozilla_components.feature.ReadabilityExtractFeature import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature import eu.weblibre.flutter_mozilla_components.integration.ReaderViewIntegration @@ -90,6 +91,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit private val webExtensionPopupObserver = ViewBoundFeatureWrapper() private val webExtToolbarFeature = ViewBoundFeatureWrapper() + // Keyboard visibility detection feature + private var keyboardVisibilityFeature: KeyboardVisibilityFeature? = null + // Registers a photo picker activity launcher in single-select mode. private val singleMediaPicker = AndroidPhotoPicker.singleMediaPicker( @@ -220,6 +224,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit components.engineView = engineView + // Apply any pending viewport settings that were set before engineView was ready + GlobalComponents.viewportApi?.applyPendingSettings() + sessionFeature.set( feature = SessionFeature( components.core.store, @@ -468,6 +475,13 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit components.core.historyStorage.registerStorageMaintenanceWorker() + // Start keyboard visibility detection if viewport events are available + GlobalComponents.viewportEvents?.let { viewportEvents -> + keyboardVisibilityFeature = KeyboardVisibilityFeature(viewportEvents).also { + it.start(binding.root) + } + } + } catch (e: Exception) { Log.e("EngineCreation", "Failed to create engine: ${e.message}", e) context?.let { restartApp(it) } @@ -560,6 +574,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit override fun onDestroyView() { super.onDestroyView() + // Stop keyboard visibility detection + keyboardVisibilityFeature?.stop() + keyboardVisibilityFeature = null + GlobalComponents.onPullToRefreshEnabledChanged = null components.engineView?.setActivityContext(null) _binding = null diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt index a7616d4d..a2c9975a 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt @@ -14,7 +14,9 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents +import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController +import eu.weblibre.flutter_mozilla_components.api.GeckoViewportApiImpl import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -45,6 +47,12 @@ object GlobalComponents { var onPullToRefreshEnabledChanged: ((Boolean) -> Unit)? = null + // Viewport events for keyboard visibility notifications + var viewportEvents: GeckoViewportEvents? = null + + // Viewport API for applying pending settings when engineView becomes available + var viewportApi: GeckoViewportApiImpl? = null + @DelicateCoroutinesApi private fun restoreBrowserState(newComponents: Components) = GlobalScope.launch(Dispatchers.Main) { diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt index 44e0f803..bc7613dc 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt @@ -44,6 +44,8 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionApi import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabsApi +import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportApi +import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents import eu.weblibre.flutter_mozilla_components.pigeons.LogLevel import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewEvents @@ -263,6 +265,17 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { GeckoFetchApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFetchApiImpl()) GeckoBookmarksApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBookmarksApiImpl()) + // Viewport API for dynamic toolbar and keyboard handling + val viewportEvents = GeckoViewportEvents(_flutterPluginBinding.binaryMessenger) + val viewportApi = GeckoViewportApiImpl() + GeckoViewportApi.setUp( + _flutterPluginBinding.binaryMessenger, + viewportApi + ) + // Store viewport events and API for keyboard feature and pending settings + GlobalComponents.viewportEvents = viewportEvents + GlobalComponents.viewportApi = viewportApi + ReaderViewEvents.setUp( _flutterPluginBinding.binaryMessenger, components.events.readerViewEvents diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoViewportApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoViewportApiImpl.kt new file mode 100644 index 00000000..1ee8db97 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoViewportApiImpl.kt @@ -0,0 +1,93 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package eu.weblibre.flutter_mozilla_components.api + +import eu.weblibre.flutter_mozilla_components.GlobalComponents +import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportApi +import mozilla.components.support.base.log.logger.Logger + +/** + * Implementation of GeckoViewportApi that controls GeckoView's viewport behavior + * for dynamic toolbar and keyboard handling. + * + * This allows Flutter to control how GeckoView adjusts its internal viewport + * without resizing the platform view itself, avoiding visual flickering. + */ +class GeckoViewportApiImpl : GeckoViewportApi { + companion object { + private const val TAG = "GeckoViewportApi" + } + + private val logger = Logger(TAG) + + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + + // Store the current dynamic toolbar max height + private var currentDynamicToolbarMaxHeight: Int = 0 + + /** + * Sets the maximum height that dynamic toolbars (top + bottom) can occupy. + * + * GeckoView will adjust its internal viewport calculations to account for + * this space. The website will receive proper viewport dimensions through + * standard web APIs (CSS viewport units, window.innerHeight). + */ + override fun setDynamicToolbarMaxHeight(heightPx: Long) { + val height = heightPx.toInt() + currentDynamicToolbarMaxHeight = height + + val engineView = components.engineView + if (engineView == null) { + logger.warn("$TAG: setDynamicToolbarMaxHeight called but engineView is null") + return + } + + logger.debug("$TAG: setDynamicToolbarMaxHeight($height)") + engineView.setDynamicToolbarMaxHeight(height) + } + + /** + * Sets the vertical clipping offset for the GeckoView content. + * + * Use this as the toolbar animates to clip content at the bottom. + * Negative values clip from the bottom (for bottom toolbar sliding up). + * Positive values clip from the top (for top toolbar sliding down). + */ + override fun setVerticalClipping(clippingPx: Long) { + val clipping = clippingPx.toInt() + + val engineView = components.engineView + if (engineView == null) { + logger.warn("$TAG: setVerticalClipping called but engineView is null") + return + } + + logger.debug("$TAG: setVerticalClipping($clipping)") + engineView.setVerticalClipping(clipping) + } + + /** + * Applies any pending viewport settings that were set before engineView was available. + * + * Call this method after setting components.engineView to ensure that any + * setDynamicToolbarMaxHeight calls made during startup are properly applied. + */ + fun applyPendingSettings() { + val engineView = components.engineView + if (engineView == null) { + logger.warn("$TAG: applyPendingSettings called but engineView is still null") + return + } + + if (currentDynamicToolbarMaxHeight > 0) { + logger.debug("$TAG: Applying pending dynamicToolbarMaxHeight: $currentDynamicToolbarMaxHeight") + engineView.setDynamicToolbarMaxHeight(currentDynamicToolbarMaxHeight) + } + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/feature/KeyboardVisibilityFeature.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/feature/KeyboardVisibilityFeature.kt new file mode 100644 index 00000000..63634ad5 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/feature/KeyboardVisibilityFeature.kt @@ -0,0 +1,223 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package eu.weblibre.flutter_mozilla_components.feature + +import android.os.Build +import android.view.View +import android.view.ViewTreeObserver +import android.view.WindowInsets +import androidx.core.graphics.Insets +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsAnimationCompat +import androidx.core.view.WindowInsetsCompat +import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.gecko.util.ThreadUtils.runOnUiThread + +/** + * Feature that detects keyboard visibility changes and reports them to Flutter. + * + * Uses WindowInsets API for accurate keyboard detection on Android 11+ (API 30+), + * with fallback to ViewTreeObserver.OnGlobalLayoutListener for older versions. + */ +class KeyboardVisibilityFeature( + private val flutterEvents: GeckoViewportEvents +) { + companion object { + private const val TAG = "KeyboardVisibilityFeature" + } + + private val logger = Logger(TAG) + + private var rootView: View? = null + private var lastKeyboardHeight: Int = 0 + private var lastKeyboardVisible: Boolean = false + private var isAnimating: Boolean = false + + // For legacy keyboard detection (pre-API 30) + private var globalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null + + // For modern keyboard animation tracking (API 30+) + private var insetsAnimationCallback: WindowInsetsAnimationCompat.Callback? = null + + /** + * Start observing keyboard visibility changes on the given root view. + */ + fun start(view: View) { + rootView = view + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + setupModernKeyboardDetection(view) + } else { + setupLegacyKeyboardDetection(view) + } + + // Also set up WindowInsets listener for immediate state + setupInsetsListener(view) + + logger.debug("$TAG: Started keyboard visibility detection") + } + + /** + * Stop observing keyboard visibility changes. + */ + fun stop() { + rootView?.let { view -> + globalLayoutListener?.let { + view.viewTreeObserver.removeOnGlobalLayoutListener(it) + } + insetsAnimationCallback?.let { + ViewCompat.setWindowInsetsAnimationCallback(view, null) + } + } + + globalLayoutListener = null + insetsAnimationCallback = null + rootView = null + + logger.debug("$TAG: Stopped keyboard visibility detection") + } + + /** + * Modern keyboard detection using WindowInsetsAnimation (API 30+). + * This provides smooth animation callbacks during keyboard show/hide. + */ + private fun setupModernKeyboardDetection(view: View) { + insetsAnimationCallback = object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + super.onPrepare(animation) + if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) { + isAnimating = true + } + } + + override fun onProgress( + insets: WindowInsetsCompat, + runningAnimations: MutableList + ): WindowInsetsCompat { + // Find IME animation + val imeAnimation = runningAnimations.find { + it.typeMask and WindowInsetsCompat.Type.ime() != 0 + } + + if (imeAnimation != null) { + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) + val navInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + + // Keyboard height is IME height minus navigation bar (which is included) + val keyboardHeight = (imeInsets.bottom - navInsets.bottom).coerceAtLeast(0) + + // During animation, report height changes + if (keyboardHeight != lastKeyboardHeight) { + notifyKeyboardChange(keyboardHeight, keyboardHeight > 0, isAnimating = true) + } + } + + return insets + } + + override fun onEnd(animation: WindowInsetsAnimationCompat) { + super.onEnd(animation) + if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) { + isAnimating = false + // Send final state + notifyKeyboardChange(lastKeyboardHeight, lastKeyboardVisible, isAnimating = false) + } + } + } + + ViewCompat.setWindowInsetsAnimationCallback(view, insetsAnimationCallback) + } + + /** + * Legacy keyboard detection using ViewTreeObserver (pre-API 30). + */ + private fun setupLegacyKeyboardDetection(view: View) { + globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener { + val insets = ViewCompat.getRootWindowInsets(view) ?: return@OnGlobalLayoutListener + + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) + val navInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + + val keyboardHeight = (imeInsets.bottom - navInsets.bottom).coerceAtLeast(0) + val isVisible = keyboardHeight > 0 + + if (keyboardHeight != lastKeyboardHeight || isVisible != lastKeyboardVisible) { + notifyKeyboardChange(keyboardHeight, isVisible, isAnimating = false) + } + } + + view.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener) + } + + /** + * Set up WindowInsets listener for immediate keyboard state. + */ + private fun setupInsetsListener(view: View) { + ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets -> + val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) + val navInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + + val keyboardHeight = if (imeVisible) { + (imeInsets.bottom - navInsets.bottom).coerceAtLeast(0) + } else { + 0 + } + + // Only notify if not currently animating (animation callbacks handle that) + if (!isAnimating && (keyboardHeight != lastKeyboardHeight || imeVisible != lastKeyboardVisible)) { + notifyKeyboardChange(keyboardHeight, imeVisible, isAnimating = false) + } + + insets + } + } + + /** + * Notify Flutter about keyboard visibility change. + */ + private fun notifyKeyboardChange(heightPx: Int, isVisible: Boolean, isAnimating: Boolean) { + lastKeyboardHeight = heightPx + lastKeyboardVisible = isVisible + + val timestamp = System.currentTimeMillis() + + logger.debug("$TAG: Keyboard change - height=$heightPx, visible=$isVisible, animating=$isAnimating") + + runOnUiThread { + flutterEvents.onKeyboardVisibilityChanged( + timestamp, + heightPx.toLong(), + isVisible, + isAnimating + ) { /* callback - ignore result */ } + } + } + + /** + * Manually trigger a keyboard state check. + * Useful after configuration changes or when resuming. + */ + fun checkKeyboardState() { + rootView?.let { view -> + val insets = ViewCompat.getRootWindowInsets(view) ?: return + + val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) + val navInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + + val keyboardHeight = if (imeVisible) { + (imeInsets.bottom - navInsets.bottom).coerceAtLeast(0) + } else { + 0 + } + + notifyKeyboardChange(keyboardHeight, imeVisible, isAnimating = false) + } + } +} 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 c572eb91..01e47ff0 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 @@ -5995,6 +5995,137 @@ interface GeckoFetchApi { } } } +/** + * Controls GeckoView's viewport behavior for dynamic toolbar and keyboard handling. + * + * This API allows Flutter to control how GeckoView adjusts its internal viewport + * without resizing the platform view itself, avoiding visual flickering. + * + * The dynamic toolbar system works by: + * 1. Setting the maximum toolbar height via [setDynamicToolbarMaxHeight] + * 2. Updating the vertical clipping as toolbar animates via [setVerticalClipping] + * 3. GeckoView internally adjusts viewport and notifies the website + * + * Generated interface from Pigeon that represents a handler of messages from Flutter. + */ +interface GeckoViewportApi { + /** + * Sets the maximum height that dynamic toolbars (top + bottom) can occupy. + * + * GeckoView will adjust its internal viewport calculations to account for + * this space. The website will receive proper viewport dimensions through + * standard web APIs (CSS viewport units, window.innerHeight). + * + * Call this once when toolbar dimensions are known, and again if they change. + * + * [heightPx] Combined height of top and bottom toolbars in pixels. + */ + fun setDynamicToolbarMaxHeight(heightPx: Long) + /** + * Sets the vertical clipping offset for the GeckoView content. + * + * Use this as the toolbar animates to clip content at the bottom. + * Negative values clip from the bottom (for bottom toolbar sliding up). + * Positive values clip from the top (for top toolbar sliding down). + * + * Call this during toolbar animation frames to smoothly adjust the visible area. + * + * [clippingPx] The clipping offset in pixels. Negative = bottom clip. + */ + fun setVerticalClipping(clippingPx: Long) + + companion object { + /** The codec used by GeckoViewportApi. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + /** Sets up an instance of `GeckoViewportApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: GeckoViewportApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setDynamicToolbarMaxHeight$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val heightPxArg = args[0] as Long + val wrapped: List = try { + api.setDynamicToolbarMaxHeight(heightPxArg) + listOf(null) + } catch (exception: Throwable) { + GeckoPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setVerticalClipping$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val clippingPxArg = args[0] as Long + val wrapped: List = try { + api.setVerticalClipping(clippingPxArg) + listOf(null) + } catch (exception: Throwable) { + GeckoPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** + * Events from native side about viewport and input-related changes. + * + * These events allow Flutter to react to native viewport changes, + * particularly keyboard visibility which is detected natively. + * + * Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. + */ +class GeckoViewportEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by GeckoViewportEvents. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + } + /** + * Called when keyboard visibility changes. + * + * This is detected natively using WindowInsets API and provides + * accurate keyboard height information. + * + * [timestamp] Event timestamp for ordering. + * [heightPx] Keyboard height in pixels (0 when hidden). + * [isVisible] Whether the keyboard is currently visible. + * [isAnimating] Whether the keyboard is currently animating. + */ + fun onKeyboardVisibilityChanged(timestampArg: Long, heightPxArg: Long, isVisibleArg: Boolean, isAnimatingArg: Boolean, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(timestampArg, heightPxArg, isVisibleArg, isAnimatingArg)) { + 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 interface from Pigeon that represents a handler of messages from Flutter. */ interface GeckoBookmarksApi { /** diff --git a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart index 240c6ef8..2698a849 100644 --- a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart +++ b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart @@ -30,6 +30,7 @@ export 'src/domain/services/gecko_session.dart'; export 'src/domain/services/gecko_suggestions.dart'; export 'src/domain/services/gecko_tab.dart'; export 'src/domain/services/gecko_tab_content.dart'; +export 'src/domain/services/gecko_viewport.dart'; export 'src/geckoview_widget.dart'; export 'src/pigeons/gecko.g.dart' show diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_viewport.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_viewport.dart new file mode 100644 index 00000000..53ab2307 --- /dev/null +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_viewport.dart @@ -0,0 +1,128 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import 'package:flutter/services.dart'; +import 'package:flutter_mozilla_components/src/extensions/subject.dart'; +import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; +import 'package:rxdart/rxdart.dart'; + +/// Keyboard visibility event data +typedef KeyboardEvent = ({ + int heightPx, + bool isVisible, + bool isAnimating, +}); + +/// Service for controlling GeckoView's viewport behavior. +/// +/// This service provides: +/// - Dynamic toolbar height management (setDynamicToolbarMaxHeight) +/// - Vertical clipping control (setVerticalClipping) +/// - Keyboard visibility events from native +/// +/// Use this service to implement Firefox-style dynamic toolbars that adjust +/// the GeckoView viewport without resizing the Flutter platform view. +class GeckoViewportService extends GeckoViewportEvents { + final GeckoViewportApi _api; + + // Stream controllers for events from native + final _keyboardSubject = BehaviorSubject.seeded(( + heightPx: 0, + isVisible: false, + isAnimating: false, + )); + + /// Stream of keyboard visibility changes. + /// + /// Emits events whenever the soft keyboard shows/hides. + /// The [KeyboardEvent] includes: + /// - [heightPx]: Keyboard height in pixels (0 when hidden) + /// - [isVisible]: Whether keyboard is currently visible + /// - [isAnimating]: Whether keyboard is currently animating + ValueStream get keyboardEvents => _keyboardSubject.stream; + + /// Current keyboard height in pixels. + int get currentKeyboardHeight => _keyboardSubject.value.heightPx; + + /// Whether the keyboard is currently visible. + bool get isKeyboardVisible => _keyboardSubject.value.isVisible; + + /// Creates a new viewport service. + /// + /// Call [setUp] to register the event handlers after construction. + GeckoViewportService({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : _api = GeckoViewportApi( + binaryMessenger: binaryMessenger, + messageChannelSuffix: messageChannelSuffix, + ); + + /// Sets up the service to receive events from native. + /// + /// Must be called before events will be received. + void setUp({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + GeckoViewportEvents.setUp( + this, + binaryMessenger: binaryMessenger, + messageChannelSuffix: messageChannelSuffix, + ); + } + + /// Sets the maximum height that dynamic toolbars can occupy. + /// + /// GeckoView will adjust its internal viewport calculations to account for + /// this space. The website will receive proper viewport dimensions through + /// standard web APIs (CSS viewport units, window.innerHeight). + /// + /// Call this once when toolbar dimensions are known, and again if they change. + /// + /// [heightPx] Combined height of top and bottom toolbars in pixels. + Future setDynamicToolbarMaxHeight(int heightPx) async { + await _api.setDynamicToolbarMaxHeight(heightPx); + } + + /// Sets the vertical clipping offset for GeckoView content. + /// + /// Use this as the toolbar animates to clip content at the bottom. + /// Negative values clip from the bottom (for bottom toolbar sliding up). + /// Positive values clip from the top (for top toolbar sliding down). + /// + /// Call this during toolbar animation frames to smoothly adjust the visible area. + /// + /// [clippingPx] The clipping offset in pixels. Negative = bottom clip. + Future setVerticalClipping(int clippingPx) async { + await _api.setVerticalClipping(clippingPx); + } + + // GeckoViewportEvents implementation + + @override + void onKeyboardVisibilityChanged( + int timestamp, + int heightPx, + bool isVisible, + bool isAnimating, + ) { + _keyboardSubject.addWhenMoreRecent( + timestamp, + null, + ( + heightPx: heightPx, + isVisible: isVisible, + isAnimating: isAnimating, + ), + ); + } + + /// Disposes the service and closes all streams. + Future dispose() async { + await _keyboardSubject.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 3dd56869..9d0d6e3a 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -7121,6 +7121,148 @@ class GeckoFetchApi { } } +/// Controls GeckoView's viewport behavior for dynamic toolbar and keyboard handling. +/// +/// This API allows Flutter to control how GeckoView adjusts its internal viewport +/// without resizing the platform view itself, avoiding visual flickering. +/// +/// The dynamic toolbar system works by: +/// 1. Setting the maximum toolbar height via [setDynamicToolbarMaxHeight] +/// 2. Updating the vertical clipping as toolbar animates via [setVerticalClipping] +/// 3. GeckoView internally adjusts viewport and notifies the website +class GeckoViewportApi { + /// Constructor for [GeckoViewportApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GeckoViewportApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Sets the maximum height that dynamic toolbars (top + bottom) can occupy. + /// + /// GeckoView will adjust its internal viewport calculations to account for + /// this space. The website will receive proper viewport dimensions through + /// standard web APIs (CSS viewport units, window.innerHeight). + /// + /// Call this once when toolbar dimensions are known, and again if they change. + /// + /// [heightPx] Combined height of top and bottom toolbars in pixels. + Future setDynamicToolbarMaxHeight(int heightPx) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setDynamicToolbarMaxHeight$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([heightPx]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + /// Sets the vertical clipping offset for the GeckoView content. + /// + /// Use this as the toolbar animates to clip content at the bottom. + /// Negative values clip from the bottom (for bottom toolbar sliding up). + /// Positive values clip from the top (for top toolbar sliding down). + /// + /// Call this during toolbar animation frames to smoothly adjust the visible area. + /// + /// [clippingPx] The clipping offset in pixels. Negative = bottom clip. + Future setVerticalClipping(int clippingPx) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setVerticalClipping$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([clippingPx]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } +} + +/// Events from native side about viewport and input-related changes. +/// +/// These events allow Flutter to react to native viewport changes, +/// particularly keyboard visibility which is detected natively. +abstract class GeckoViewportEvents { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + /// Called when keyboard visibility changes. + /// + /// This is detected natively using WindowInsets API and provides + /// accurate keyboard height information. + /// + /// [timestamp] Event timestamp for ordering. + /// [heightPx] Keyboard height in pixels (0 when hidden). + /// [isVisible] Whether the keyboard is currently visible. + /// [isAnimating] Whether the keyboard is currently animating. + void onKeyboardVisibilityChanged(int timestamp, int heightPx, bool isVisible, bool isAnimating); + + static void setUp(GeckoViewportEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged$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.GeckoViewportEvents.onKeyboardVisibilityChanged 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.GeckoViewportEvents.onKeyboardVisibilityChanged was null, expected non-null int.'); + final int? arg_heightPx = (args[1] as int?); + assert(arg_heightPx != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged was null, expected non-null int.'); + final bool? arg_isVisible = (args[2] as bool?); + assert(arg_isVisible != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged was null, expected non-null bool.'); + final bool? arg_isAnimating = (args[3] as bool?); + assert(arg_isAnimating != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged was null, expected non-null bool.'); + try { + api.onKeyboardVisibilityChanged(arg_timestamp!, arg_heightPx!, arg_isVisible!, arg_isAnimating!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} + class GeckoBookmarksApi { /// Constructor for [GeckoBookmarksApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index df58a7c4..4f1e44bf 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -1563,6 +1563,71 @@ class BookmarkInfo { }); } +// ============================================================================= +// Viewport & Dynamic Toolbar APIs +// ============================================================================= + +/// Controls GeckoView's viewport behavior for dynamic toolbar and keyboard handling. +/// +/// This API allows Flutter to control how GeckoView adjusts its internal viewport +/// without resizing the platform view itself, avoiding visual flickering. +/// +/// The dynamic toolbar system works by: +/// 1. Setting the maximum toolbar height via [setDynamicToolbarMaxHeight] +/// 2. Updating the vertical clipping as toolbar animates via [setVerticalClipping] +/// 3. GeckoView internally adjusts viewport and notifies the website +@HostApi() +abstract class GeckoViewportApi { + /// Sets the maximum height that dynamic toolbars (top + bottom) can occupy. + /// + /// GeckoView will adjust its internal viewport calculations to account for + /// this space. The website will receive proper viewport dimensions through + /// standard web APIs (CSS viewport units, window.innerHeight). + /// + /// Call this once when toolbar dimensions are known, and again if they change. + /// + /// [heightPx] Combined height of top and bottom toolbars in pixels. + void setDynamicToolbarMaxHeight(int heightPx); + + /// Sets the vertical clipping offset for the GeckoView content. + /// + /// Use this as the toolbar animates to clip content at the bottom. + /// Negative values clip from the bottom (for bottom toolbar sliding up). + /// Positive values clip from the top (for top toolbar sliding down). + /// + /// Call this during toolbar animation frames to smoothly adjust the visible area. + /// + /// [clippingPx] The clipping offset in pixels. Negative = bottom clip. + void setVerticalClipping(int clippingPx); +} + +/// Events from native side about viewport and input-related changes. +/// +/// These events allow Flutter to react to native viewport changes, +/// particularly keyboard visibility which is detected natively. +@FlutterApi() +abstract class GeckoViewportEvents { + /// Called when keyboard visibility changes. + /// + /// This is detected natively using WindowInsets API and provides + /// accurate keyboard height information. + /// + /// [timestamp] Event timestamp for ordering. + /// [heightPx] Keyboard height in pixels (0 when hidden). + /// [isVisible] Whether the keyboard is currently visible. + /// [isAnimating] Whether the keyboard is currently animating. + void onKeyboardVisibilityChanged( + int timestamp, + int heightPx, + bool isVisible, + bool isAnimating, + ); +} + +// ============================================================================= +// Bookmarks API +// ============================================================================= + @HostApi() abstract class GeckoBookmarksApi { /// Produces a bookmarks tree for the given guid string. diff --git a/packages/flutter_tor/example/lib/main.dart b/packages/flutter_tor/example/lib/main.dart index ac68afd7..cfdfa4b9 100644 --- a/packages/flutter_tor/example/lib/main.dart +++ b/packages/flutter_tor/example/lib/main.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_print, deprecated_member_use + import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -71,12 +73,12 @@ class _TorDemoPageState extends State { ); setState(() { _isRunning = status.isRunning; - final newPort = status.socksPort?.toInt(); + final newPort = status.socksPort; if (newPort != _socksPort) { print('DEBUG: Port changed from $_socksPort to $newPort'); } _socksPort = newPort; - _bootstrapProgress = status.bootstrapProgress.toInt(); + _bootstrapProgress = status.bootstrapProgress; }); }); @@ -104,15 +106,15 @@ class _TorDemoPageState extends State { ); final socksPort = await _tor.start(config); - print('DEBUG startTor result: socksPort=${socksPort}'); + print('DEBUG startTor result: socksPort=$socksPort'); setState(() { - _socksPort = socksPort.toInt(); + _socksPort = socksPort; print('DEBUG: Set _socksPort to $_socksPort from start result'); }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Tor started on port ${socksPort}')), + SnackBar(content: Text('Tor started on port $socksPort')), ); } } catch (e) { @@ -218,7 +220,7 @@ class _TorDemoPageState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Your Tor IP: ${_currentIp}'), + content: Text('Your Tor IP: $_currentIp'), backgroundColor: Colors.green, duration: const Duration(seconds: 5), ), @@ -438,13 +440,10 @@ class _TorDemoPageState extends State { switch (log.severity) { case 'ERR': color = Colors.red; - break; case 'WARN': color = Colors.orange; - break; case 'NOTICE': color = Colors.blue; - break; default: color = Colors.black; } diff --git a/packages/flutter_tor/example/pubspec.yaml b/packages/flutter_tor/example/pubspec.yaml index fe2e1879..809b1266 100644 --- a/packages/flutter_tor/example/pubspec.yaml +++ b/packages/flutter_tor/example/pubspec.yaml @@ -17,19 +17,9 @@ environment: dependencies: flutter: sdk: flutter - flutter_tor: - # When depending on this package from a real application you should use: - # flutter_tor: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. path: ../ - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 - # HTTP client for testing Tor connectivity http: any @@ -37,17 +27,11 @@ dependencies: socks5_proxy: any dev_dependencies: - integration_test: - sdk: flutter + flutter_lints: ^6.0.0 flutter_test: sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^6.0.0 + integration_test: + sdk: flutter # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/flutter_tor/lib/flutter_tor.dart b/packages/flutter_tor/lib/flutter_tor.dart index e94976d4..e79a9219 100644 --- a/packages/flutter_tor/lib/flutter_tor.dart +++ b/packages/flutter_tor/lib/flutter_tor.dart @@ -1,5 +1,3 @@ -library flutter_tor; - export 'src/tor_api.g.dart' show TransportType,