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
|
/// Get AppColors from the current theme
|
||||||
static AppColors of(BuildContext context) {
|
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() {
|
void dispose() {
|
||||||
if (_isDisposed) return;
|
if (_isDisposed) return;
|
||||||
_isDisposed = true;
|
_isDisposed = true;
|
||||||
_value?.dispose();
|
// Delay disposal to allow widgets to finish rendering
|
||||||
_value = null;
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
_value?.dispose();
|
||||||
|
_value = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -171,6 +171,18 @@ GeckoSuggestionsService engineSuggestionsService(Ref ref) {
|
|||||||
return service;
|
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)
|
@Riverpod(keepAlive: true)
|
||||||
class EngineReadyState extends _$EngineReadyState {
|
class EngineReadyState extends _$EngineReadyState {
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -246,6 +246,53 @@ final class EngineSuggestionsServiceProvider
|
|||||||
String _$engineSuggestionsServiceHash() =>
|
String _$engineSuggestionsServiceHash() =>
|
||||||
r'1ec1192f0c5c86cecc7ad448ee2b039f7a48e32b';
|
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)
|
@ProviderFor(EngineReadyState)
|
||||||
final engineReadyStateProvider = EngineReadyStateProvider._();
|
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/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
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.
|
/// Animated toolbar that slides in/out without changing layout constraints.
|
||||||
/// Uses SlideTransition to animate visual transform while maintaining
|
/// Uses SlideTransition to animate visual transform while maintaining
|
||||||
/// constant intrinsic size for layout purposes.
|
/// constant intrinsic size for layout purposes.
|
||||||
@@ -64,6 +70,8 @@ class _AnimatedToolbar extends HookWidget {
|
|||||||
final bool visible;
|
final bool visible;
|
||||||
final TabBarPosition position;
|
final TabBarPosition position;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final double toolbarHeight;
|
||||||
|
final ToolbarAnimationCallback? onAnimationProgress;
|
||||||
|
|
||||||
static const _kAnimationDuration = Duration(milliseconds: 250);
|
static const _kAnimationDuration = Duration(milliseconds: 250);
|
||||||
|
|
||||||
@@ -71,6 +79,9 @@ class _AnimatedToolbar extends HookWidget {
|
|||||||
required this.visible,
|
required this.visible,
|
||||||
required this.position,
|
required this.position,
|
||||||
required this.child,
|
required this.child,
|
||||||
|
required this.toolbarHeight,
|
||||||
|
// ignore: unused_element_parameter
|
||||||
|
this.onAnimationProgress,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -80,6 +91,18 @@ class _AnimatedToolbar extends HookWidget {
|
|||||||
initialValue: visible ? 1.0 : 0.0,
|
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(() {
|
useEffect(() {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
unawaited(controller.forward());
|
unawaited(controller.forward());
|
||||||
@@ -230,6 +253,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final eventService = ref.watch(eventServiceProvider);
|
final eventService = ref.watch(eventServiceProvider);
|
||||||
|
final viewportService = ref.watch(viewportServiceProvider);
|
||||||
|
|
||||||
final tabInFullScreen = ref.watch(
|
final tabInFullScreen = ref.watch(
|
||||||
selectedTabStateProvider.select((value) => value?.isFullScreen ?? false),
|
selectedTabStateProvider.select((value) => value?.isFullScreen ?? false),
|
||||||
@@ -339,6 +363,38 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
? topAppBarTotalHeight
|
? topAppBarTotalHeight
|
||||||
: 0.0;
|
: 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
|
// Theme with dynamic snackbar margin to position above bottom toolbar
|
||||||
final themeData = Theme.of(context).copyWith(
|
final themeData = Theme.of(context).copyWith(
|
||||||
bottomSheetTheme: BottomSheetThemeData(
|
bottomSheetTheme: BottomSheetThemeData(
|
||||||
@@ -412,6 +468,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
child: _AnimatedToolbar(
|
child: _AnimatedToolbar(
|
||||||
position: TabBarPosition.bottom,
|
position: TabBarPosition.bottom,
|
||||||
visible: bottomToolbarVisible,
|
visible: bottomToolbarVisible,
|
||||||
|
toolbarHeight: bottomAppBarTotalHeight,
|
||||||
child: _TabBar(
|
child: _TabBar(
|
||||||
tabBarPosition: TabBarPosition.bottom,
|
tabBarPosition: TabBarPosition.bottom,
|
||||||
displayAppBar: displayAppBar,
|
displayAppBar: displayAppBar,
|
||||||
@@ -435,6 +492,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
child: _AnimatedToolbar(
|
child: _AnimatedToolbar(
|
||||||
position: TabBarPosition.top,
|
position: TabBarPosition.top,
|
||||||
visible: topToolbarVisible,
|
visible: topToolbarVisible,
|
||||||
|
toolbarHeight: topAppBarTotalHeight,
|
||||||
child: _TabBar(
|
child: _TabBar(
|
||||||
tabBarPosition: TabBarPosition.top,
|
tabBarPosition: TabBarPosition.top,
|
||||||
showMainToolbar: true,
|
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.addons.WebExtensionPromptFeature
|
||||||
import eu.weblibre.flutter_mozilla_components.databinding.FragmentBrowserBinding
|
import eu.weblibre.flutter_mozilla_components.databinding.FragmentBrowserBinding
|
||||||
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
|
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.ReadabilityExtractFeature
|
||||||
import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature
|
import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature
|
||||||
import eu.weblibre.flutter_mozilla_components.integration.ReaderViewIntegration
|
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 webExtensionPopupObserver = ViewBoundFeatureWrapper<WebExtensionPopupObserver>()
|
||||||
private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
|
private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
|
||||||
|
|
||||||
|
// Keyboard visibility detection feature
|
||||||
|
private var keyboardVisibilityFeature: KeyboardVisibilityFeature? = null
|
||||||
|
|
||||||
// Registers a photo picker activity launcher in single-select mode.
|
// Registers a photo picker activity launcher in single-select mode.
|
||||||
private val singleMediaPicker =
|
private val singleMediaPicker =
|
||||||
AndroidPhotoPicker.singleMediaPicker(
|
AndroidPhotoPicker.singleMediaPicker(
|
||||||
@@ -220,6 +224,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
|||||||
|
|
||||||
components.engineView = engineView
|
components.engineView = engineView
|
||||||
|
|
||||||
|
// Apply any pending viewport settings that were set before engineView was ready
|
||||||
|
GlobalComponents.viewportApi?.applyPendingSettings()
|
||||||
|
|
||||||
sessionFeature.set(
|
sessionFeature.set(
|
||||||
feature = SessionFeature(
|
feature = SessionFeature(
|
||||||
components.core.store,
|
components.core.store,
|
||||||
@@ -468,6 +475,13 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
|||||||
|
|
||||||
components.core.historyStorage.registerStorageMaintenanceWorker()
|
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) {
|
} catch (e: Exception) {
|
||||||
Log.e("EngineCreation", "Failed to create engine: ${e.message}", e)
|
Log.e("EngineCreation", "Failed to create engine: ${e.message}", e)
|
||||||
context?.let { restartApp(it) }
|
context?.let { restartApp(it) }
|
||||||
@@ -560,6 +574,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
|||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
|
||||||
|
// Stop keyboard visibility detection
|
||||||
|
keyboardVisibilityFeature?.stop()
|
||||||
|
keyboardVisibilityFeature = null
|
||||||
|
|
||||||
GlobalComponents.onPullToRefreshEnabledChanged = null
|
GlobalComponents.onPullToRefreshEnabledChanged = null
|
||||||
components.engineView?.setActivityContext(null)
|
components.engineView?.setActivityContext(null)
|
||||||
_binding = 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.GeckoStateEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
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.pigeons.ReaderViewController
|
||||||
|
import eu.weblibre.flutter_mozilla_components.api.GeckoViewportApiImpl
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -45,6 +47,12 @@ object GlobalComponents {
|
|||||||
|
|
||||||
var onPullToRefreshEnabledChanged: ((Boolean) -> Unit)? = null
|
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
|
@DelicateCoroutinesApi
|
||||||
private fun restoreBrowserState(newComponents: Components) =
|
private fun restoreBrowserState(newComponents: Components) =
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
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.GeckoSuggestionEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabsApi
|
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.LogLevel
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController
|
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewEvents
|
||||||
@@ -263,6 +265,17 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
|||||||
GeckoFetchApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFetchApiImpl())
|
GeckoFetchApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFetchApiImpl())
|
||||||
GeckoBookmarksApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBookmarksApiImpl())
|
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(
|
ReaderViewEvents.setUp(
|
||||||
_flutterPluginBinding.binaryMessenger,
|
_flutterPluginBinding.binaryMessenger,
|
||||||
components.events.readerViewEvents
|
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. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface GeckoBookmarksApi {
|
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_suggestions.dart';
|
||||||
export 'src/domain/services/gecko_tab.dart';
|
export 'src/domain/services/gecko_tab.dart';
|
||||||
export 'src/domain/services/gecko_tab_content.dart';
|
export 'src/domain/services/gecko_tab_content.dart';
|
||||||
|
export 'src/domain/services/gecko_viewport.dart';
|
||||||
export 'src/geckoview_widget.dart';
|
export 'src/geckoview_widget.dart';
|
||||||
export 'src/pigeons/gecko.g.dart'
|
export 'src/pigeons/gecko.g.dart'
|
||||||
show
|
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 {
|
class GeckoBookmarksApi {
|
||||||
/// Constructor for [GeckoBookmarksApi]. The [binaryMessenger] named argument is
|
/// Constructor for [GeckoBookmarksApi]. The [binaryMessenger] named argument is
|
||||||
/// available for dependency injection. If it is left null, the default
|
/// 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()
|
@HostApi()
|
||||||
abstract class GeckoBookmarksApi {
|
abstract class GeckoBookmarksApi {
|
||||||
/// Produces a bookmarks tree for the given guid string.
|
/// 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:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
@@ -71,12 +73,12 @@ class _TorDemoPageState extends State<TorDemoPage> {
|
|||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_isRunning = status.isRunning;
|
_isRunning = status.isRunning;
|
||||||
final newPort = status.socksPort?.toInt();
|
final newPort = status.socksPort;
|
||||||
if (newPort != _socksPort) {
|
if (newPort != _socksPort) {
|
||||||
print('DEBUG: Port changed from $_socksPort to $newPort');
|
print('DEBUG: Port changed from $_socksPort to $newPort');
|
||||||
}
|
}
|
||||||
_socksPort = 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);
|
final socksPort = await _tor.start(config);
|
||||||
print('DEBUG startTor result: socksPort=${socksPort}');
|
print('DEBUG startTor result: socksPort=$socksPort');
|
||||||
setState(() {
|
setState(() {
|
||||||
_socksPort = socksPort.toInt();
|
_socksPort = socksPort;
|
||||||
print('DEBUG: Set _socksPort to $_socksPort from start result');
|
print('DEBUG: Set _socksPort to $_socksPort from start result');
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Tor started on port ${socksPort}')),
|
SnackBar(content: Text('Tor started on port $socksPort')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -218,7 +220,7 @@ class _TorDemoPageState extends State<TorDemoPage> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Your Tor IP: ${_currentIp}'),
|
content: Text('Your Tor IP: $_currentIp'),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
duration: const Duration(seconds: 5),
|
duration: const Duration(seconds: 5),
|
||||||
),
|
),
|
||||||
@@ -438,13 +440,10 @@ class _TorDemoPageState extends State<TorDemoPage> {
|
|||||||
switch (log.severity) {
|
switch (log.severity) {
|
||||||
case 'ERR':
|
case 'ERR':
|
||||||
color = Colors.red;
|
color = Colors.red;
|
||||||
break;
|
|
||||||
case 'WARN':
|
case 'WARN':
|
||||||
color = Colors.orange;
|
color = Colors.orange;
|
||||||
break;
|
|
||||||
case 'NOTICE':
|
case 'NOTICE':
|
||||||
color = Colors.blue;
|
color = Colors.blue;
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
color = Colors.black;
|
color = Colors.black;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,19 +17,9 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
flutter_tor:
|
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: ../
|
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 client for testing Tor connectivity
|
||||||
http: any
|
http: any
|
||||||
|
|
||||||
@@ -37,17 +27,11 @@ dependencies:
|
|||||||
socks5_proxy: any
|
socks5_proxy: any
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
integration_test:
|
flutter_lints: ^6.0.0
|
||||||
sdk: flutter
|
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
integration_test:
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
sdk: flutter
|
||||||
# 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
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
library flutter_tor;
|
|
||||||
|
|
||||||
export 'src/tor_api.g.dart'
|
export 'src/tor_api.g.dart'
|
||||||
show
|
show
|
||||||
TransportType,
|
TransportType,
|
||||||
|
|||||||
Reference in New Issue
Block a user