impelement geckoview intent handling via viewport api; fix lints and ui issues

This commit is contained in:
Fabian Freund
2026-01-13 11:03:26 +01:00
parent 5e446f1b92
commit 849823f5df
18 changed files with 960 additions and 33 deletions
+5 -1
View File
@@ -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,
};
}
}
+5 -2
View File
@@ -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,