impelement geckoview intent handling via viewport api; fix lints and ui issues
This commit is contained in:
@@ -80,6 +80,10 @@ class AppColors extends ThemeExtension<AppColors> {
|
||||
|
||||
/// Get AppColors from the current theme
|
||||
static AppColors of(BuildContext context) {
|
||||
return Theme.of(context).extension<AppColors>() ?? light;
|
||||
return Theme.of(context).extension<AppColors>() ??
|
||||
switch (Theme.of(context).brightness) {
|
||||
Brightness.dark => dark,
|
||||
Brightness.light => light,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<GeckoViewportService> {
|
||||
ViewportServiceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'viewportServiceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$viewportServiceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<GeckoViewportService> $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<GeckoViewportService>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$viewportServiceHash() => r'bab39db3180bb6a1cf8c055966b7f5910b41b424';
|
||||
|
||||
@ProviderFor(EngineReadyState)
|
||||
final engineReadyStateProvider = EngineReadyStateProvider._();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+18
@@ -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<WebExtensionPopupObserver>()
|
||||
private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
|
||||
|
||||
// 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
|
||||
|
||||
+8
@@ -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) {
|
||||
|
||||
+13
@@ -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
|
||||
|
||||
+93
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+223
@@ -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<WindowInsetsAnimationCompat>
|
||||
): 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+131
@@ -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<Any?> 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<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setDynamicToolbarMaxHeight$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val heightPxArg = args[0] as Long
|
||||
val wrapped: List<Any?> = try {
|
||||
api.setDynamicToolbarMaxHeight(heightPxArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
GeckoPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setVerticalClipping$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val clippingPxArg = args[0] as Long
|
||||
val wrapped: List<Any?> = 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<Any?> 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>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onKeyboardVisibilityChanged$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(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 {
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<KeyboardEvent>.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<KeyboardEvent> 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<void> 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<void> 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<void> dispose() async {
|
||||
await _keyboardSubject.close();
|
||||
}
|
||||
}
|
||||
@@ -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<Object?> 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<void> setDynamicToolbarMaxHeight(int heightPx) async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setDynamicToolbarMaxHeight$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[heightPx]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
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<void> setVerticalClipping(int clippingPx) async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportApi.setVerticalClipping$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[clippingPx]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
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<Object?> 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<Object?>(
|
||||
'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<Object?> args = (message as List<Object?>?)!;
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<TorDemoPage> {
|
||||
);
|
||||
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<TorDemoPage> {
|
||||
);
|
||||
|
||||
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<TorDemoPage> {
|
||||
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<TorDemoPage> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
library flutter_tor;
|
||||
|
||||
export 'src/tor_api.g.dart'
|
||||
show
|
||||
TransportType,
|
||||
|
||||
Reference in New Issue
Block a user