refactor tab bar behaviour; respect scroll preferences of websites;
This commit is contained in:
@@ -64,6 +64,7 @@ class TabState extends WebPageInfo {
|
||||
final bool isPrivate;
|
||||
final bool isFullScreen;
|
||||
final bool isLoading;
|
||||
final bool showToolbarAsExpanded;
|
||||
|
||||
bool get isFinishedLoading => !isLoading && progress == 100;
|
||||
|
||||
@@ -84,6 +85,7 @@ class TabState extends WebPageInfo {
|
||||
required this.isPrivate,
|
||||
required this.isFullScreen,
|
||||
required this.isLoading,
|
||||
required this.showToolbarAsExpanded,
|
||||
required this.securityInfoState,
|
||||
required this.historyState,
|
||||
required this.readerableState,
|
||||
@@ -102,6 +104,7 @@ class TabState extends WebPageInfo {
|
||||
isPrivate: false,
|
||||
isFullScreen: false,
|
||||
isLoading: false,
|
||||
showToolbarAsExpanded: false,
|
||||
securityInfoState: SecurityState.$default(),
|
||||
historyState: HistoryState.$default(),
|
||||
readerableState: ReaderableState.$default(),
|
||||
@@ -120,6 +123,7 @@ class TabState extends WebPageInfo {
|
||||
isPrivate,
|
||||
isFullScreen,
|
||||
isLoading,
|
||||
showToolbarAsExpanded,
|
||||
securityInfoState,
|
||||
historyState,
|
||||
readerableState,
|
||||
|
||||
@@ -27,6 +27,8 @@ abstract class _$TabStateCWProxy {
|
||||
|
||||
TabState isLoading(bool isLoading);
|
||||
|
||||
TabState showToolbarAsExpanded(bool showToolbarAsExpanded);
|
||||
|
||||
TabState securityInfoState(SecurityState securityInfoState);
|
||||
|
||||
TabState historyState(HistoryState historyState);
|
||||
@@ -53,6 +55,7 @@ abstract class _$TabStateCWProxy {
|
||||
bool isPrivate,
|
||||
bool isFullScreen,
|
||||
bool isLoading,
|
||||
bool showToolbarAsExpanded,
|
||||
SecurityState securityInfoState,
|
||||
HistoryState historyState,
|
||||
ReaderableState readerableState,
|
||||
@@ -97,6 +100,10 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
|
||||
@override
|
||||
TabState isLoading(bool isLoading) => call(isLoading: isLoading);
|
||||
|
||||
@override
|
||||
TabState showToolbarAsExpanded(bool showToolbarAsExpanded) =>
|
||||
call(showToolbarAsExpanded: showToolbarAsExpanded);
|
||||
|
||||
@override
|
||||
TabState securityInfoState(SecurityState securityInfoState) =>
|
||||
call(securityInfoState: securityInfoState);
|
||||
@@ -132,6 +139,7 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
|
||||
Object? isPrivate = const $CopyWithPlaceholder(),
|
||||
Object? isFullScreen = const $CopyWithPlaceholder(),
|
||||
Object? isLoading = const $CopyWithPlaceholder(),
|
||||
Object? showToolbarAsExpanded = const $CopyWithPlaceholder(),
|
||||
Object? securityInfoState = const $CopyWithPlaceholder(),
|
||||
Object? historyState = const $CopyWithPlaceholder(),
|
||||
Object? readerableState = const $CopyWithPlaceholder(),
|
||||
@@ -180,6 +188,12 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
|
||||
? _value.isLoading
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: isLoading as bool,
|
||||
showToolbarAsExpanded:
|
||||
showToolbarAsExpanded == const $CopyWithPlaceholder() ||
|
||||
showToolbarAsExpanded == null
|
||||
? _value.showToolbarAsExpanded
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: showToolbarAsExpanded as bool,
|
||||
securityInfoState:
|
||||
securityInfoState == const $CopyWithPlaceholder() ||
|
||||
securityInfoState == null
|
||||
|
||||
@@ -87,6 +87,7 @@ class TabStates extends _$TabStates {
|
||||
isPrivate: contentState.isPrivate,
|
||||
isFullScreen: contentState.isFullScreen,
|
||||
isLoading: contentState.isLoading,
|
||||
showToolbarAsExpanded: contentState.showToolbarAsExpanded,
|
||||
);
|
||||
|
||||
_updateState({...state}..[contentState.id] = newState);
|
||||
|
||||
@@ -41,7 +41,7 @@ final class TabStatesProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabStatesHash() => r'985d2eb87c59c3a113b8bbcf9d96267c42a2f543';
|
||||
String _$tabStatesHash() => r'975de1242f42e2017754dc560125788b83ad8b72';
|
||||
|
||||
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
||||
Map<String, TabState> build();
|
||||
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'tab_bar_dismissable.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class TabBarDismissableController extends _$TabBarDismissableController {
|
||||
void show() {
|
||||
state = false;
|
||||
}
|
||||
|
||||
void dismiss() {
|
||||
if (!state) {
|
||||
state = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool build() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tab_bar_dismissable.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(TabBarDismissableController)
|
||||
final tabBarDismissableControllerProvider =
|
||||
TabBarDismissableControllerProvider._();
|
||||
|
||||
final class TabBarDismissableControllerProvider
|
||||
extends $NotifierProvider<TabBarDismissableController, bool> {
|
||||
TabBarDismissableControllerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'tabBarDismissableControllerProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$tabBarDismissableControllerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
TabBarDismissableController create() => TabBarDismissableController();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabBarDismissableControllerHash() =>
|
||||
r'e0c2f700ccebc35bc8029370cdebd63e98bf95e1';
|
||||
|
||||
abstract class _$TabBarDismissableController extends $Notifier<bool> {
|
||||
bool build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<bool, bool>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<bool, bool>,
|
||||
bool,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
part 'toolbar_visibility.g.dart';
|
||||
|
||||
enum ToolbarVisibility { visible, hidden, dismissed }
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ToolbarVisibilityController extends _$ToolbarVisibilityController {
|
||||
@override
|
||||
ToolbarVisibility build(String? tabId) {
|
||||
// Show toolbar when loading starts
|
||||
ref.listen(tabStatesProvider.select((tabs) => tabs[tabId]?.isLoading), (
|
||||
previous,
|
||||
next,
|
||||
) {
|
||||
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) {
|
||||
return;
|
||||
}
|
||||
if (next == true) {
|
||||
show();
|
||||
}
|
||||
});
|
||||
|
||||
// Show toolbar on navigation (history state change)
|
||||
ref.listen(tabStatesProvider.select((tabs) => tabs[tabId]?.historyState), (
|
||||
previous,
|
||||
next,
|
||||
) {
|
||||
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) {
|
||||
return;
|
||||
}
|
||||
if (next != null && previous != null && previous != next) {
|
||||
show();
|
||||
}
|
||||
});
|
||||
|
||||
// Force-show when GeckoView requests toolbar expansion
|
||||
// (e.g. touch on form input)
|
||||
ref.listen<bool>(
|
||||
tabStatesProvider.select(
|
||||
(tabs) => tabs[tabId]?.showToolbarAsExpanded ?? false,
|
||||
),
|
||||
(previous, next) {
|
||||
if (next && previous != next) {
|
||||
forceShow();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return ToolbarVisibility.visible;
|
||||
}
|
||||
|
||||
/// Hide toolbar via scroll. All guards checked internally.
|
||||
void requestHide() {
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
if (!settings.autoHideTabBar) return;
|
||||
|
||||
final isLoading = ref.read(tabStatesProvider)[tabId]?.isLoading ?? false;
|
||||
if (isLoading) return;
|
||||
|
||||
final viewportService = ref.read(viewportServiceProvider);
|
||||
if (!viewportService.isBrowserHandlingScrollEnabled) return;
|
||||
|
||||
if (state == ToolbarVisibility.dismissed) return;
|
||||
|
||||
state = ToolbarVisibility.hidden;
|
||||
}
|
||||
|
||||
/// Show toolbar (scroll-up, tab change, loading start, etc).
|
||||
/// Won't show if manually dismissed — use forceShow() for that.
|
||||
void show() {
|
||||
if (state == ToolbarVisibility.dismissed) return;
|
||||
state = ToolbarVisibility.visible;
|
||||
}
|
||||
|
||||
/// Force-show unconditionally + un-dismiss.
|
||||
void forceShow() {
|
||||
state = ToolbarVisibility.visible;
|
||||
}
|
||||
|
||||
/// Dismiss toolbar (user swipe). Only affects this tab.
|
||||
void dismiss() {
|
||||
state = ToolbarVisibility.dismissed;
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'toolbar_visibility.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(ToolbarVisibilityController)
|
||||
final toolbarVisibilityControllerProvider =
|
||||
ToolbarVisibilityControllerFamily._();
|
||||
|
||||
final class ToolbarVisibilityControllerProvider
|
||||
extends $NotifierProvider<ToolbarVisibilityController, ToolbarVisibility> {
|
||||
ToolbarVisibilityControllerProvider._({
|
||||
required ToolbarVisibilityControllerFamily super.from,
|
||||
required String? super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'toolbarVisibilityControllerProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$toolbarVisibilityControllerHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'toolbarVisibilityControllerProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
ToolbarVisibilityController create() => ToolbarVisibilityController();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(ToolbarVisibility value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<ToolbarVisibility>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ToolbarVisibilityControllerProvider &&
|
||||
other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$toolbarVisibilityControllerHash() =>
|
||||
r'242449893afd6780d977544659e83d4641c8a0ad';
|
||||
|
||||
final class ToolbarVisibilityControllerFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
ToolbarVisibilityController,
|
||||
ToolbarVisibility,
|
||||
ToolbarVisibility,
|
||||
ToolbarVisibility,
|
||||
String?
|
||||
> {
|
||||
ToolbarVisibilityControllerFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'toolbarVisibilityControllerProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: false,
|
||||
);
|
||||
|
||||
ToolbarVisibilityControllerProvider call(String? tabId) =>
|
||||
ToolbarVisibilityControllerProvider._(argument: tabId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'toolbarVisibilityControllerProvider';
|
||||
}
|
||||
|
||||
abstract class _$ToolbarVisibilityController
|
||||
extends $Notifier<ToolbarVisibility> {
|
||||
late final _$args = ref.$arg as String?;
|
||||
String? get tabId => _$args;
|
||||
|
||||
ToolbarVisibility build(String? tabId);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<ToolbarVisibility, ToolbarVisibility>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<ToolbarVisibility, ToolbarVisibility>,
|
||||
ToolbarVisibility,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, () => build(_$args));
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
|
||||
@@ -136,7 +136,6 @@ class _TabBar extends HookConsumerWidget {
|
||||
final bool showMainToolbar;
|
||||
final bool showContextualToolbar;
|
||||
final bool showQuickTabSwitcherBar;
|
||||
final ValueNotifier<bool> displayAppBar;
|
||||
final Stream<Offset>? pointerMoveEvents;
|
||||
final TabBarPosition tabBarPosition;
|
||||
|
||||
@@ -144,7 +143,6 @@ class _TabBar extends HookConsumerWidget {
|
||||
required this.showMainToolbar,
|
||||
required this.showContextualToolbar,
|
||||
required this.showQuickTabSwitcherBar,
|
||||
required this.displayAppBar,
|
||||
required this.tabBarPosition,
|
||||
required this.pointerMoveEvents,
|
||||
});
|
||||
@@ -154,30 +152,34 @@ class _TabBar extends HookConsumerWidget {
|
||||
final tabId = ref.watch(selectedTabProvider);
|
||||
final displayedSheet = ref.watch(bottomSheetControllerProvider);
|
||||
|
||||
final autoHideTabBar = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(value) => value.autoHideTabBar,
|
||||
),
|
||||
);
|
||||
|
||||
// Auto-hide scroll detection hooks (run unconditionally per hook rules)
|
||||
final diffAcc = useRef(0.0);
|
||||
|
||||
void resetHiddenState() {
|
||||
if (!ref.read(tabBarDismissableControllerProvider)) {
|
||||
displayAppBar.value = true;
|
||||
// Reset scroll accumulator on tab switch
|
||||
useEffect(() {
|
||||
diffAcc.value = 0.0;
|
||||
return null;
|
||||
}, [tabId]);
|
||||
|
||||
// Reset scroll accumulator when controller shows toolbar
|
||||
// (covers: loading start, navigation, showToolbarAsExpanded, forceShow)
|
||||
ref.listen(toolbarVisibilityControllerProvider(tabId), (previous, next) {
|
||||
if (next == ToolbarVisibility.visible && previous != next) {
|
||||
diffAcc.value = 0.0;
|
||||
}
|
||||
});
|
||||
|
||||
void resetHiddenState() {
|
||||
ref
|
||||
.read(
|
||||
toolbarVisibilityControllerProvider(
|
||||
ref.read(selectedTabProvider),
|
||||
).notifier,
|
||||
)
|
||||
.show();
|
||||
diffAcc.value = 0.0;
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
if (!autoHideTabBar) return null;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
resetHiddenState();
|
||||
});
|
||||
return null;
|
||||
}, [tabId, autoHideTabBar]);
|
||||
|
||||
useOnAppLifecycleStateChange((previous, current) {
|
||||
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
|
||||
if (current == AppLifecycleState.resumed) {
|
||||
@@ -185,38 +187,13 @@ class _TabBar extends HookConsumerWidget {
|
||||
}
|
||||
});
|
||||
|
||||
ref.listen(tabStateProvider(tabId).select((value) => value?.isLoading), (
|
||||
previous,
|
||||
next,
|
||||
) {
|
||||
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
|
||||
if (next == true) {
|
||||
resetHiddenState();
|
||||
}
|
||||
});
|
||||
|
||||
ref.listen(tabStateProvider(tabId).select((value) => value?.historyState), (
|
||||
previous,
|
||||
next,
|
||||
) {
|
||||
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) return;
|
||||
if (next != null && previous != null) {
|
||||
if (previous != next) {
|
||||
resetHiddenState();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useOnStreamChange(
|
||||
pointerMoveEvents,
|
||||
onData: (event) {
|
||||
if (!ref.read(generalSettingsWithDefaultsProvider).autoHideTabBar) {
|
||||
return;
|
||||
}
|
||||
// Don't apply auto-hide when toolbar is manually dismissed
|
||||
if (ref.read(tabBarDismissableControllerProvider)) {
|
||||
return;
|
||||
}
|
||||
final autoHide = ref
|
||||
.read(generalSettingsWithDefaultsProvider)
|
||||
.autoHideTabBar;
|
||||
if (!autoHide) return;
|
||||
|
||||
final diff = event.dy;
|
||||
if (diff < 0) {
|
||||
@@ -225,7 +202,13 @@ class _TabBar extends HookConsumerWidget {
|
||||
}
|
||||
diffAcc.value += diff;
|
||||
if (diffAcc.value.abs() > kToolbarHeight * 1.5) {
|
||||
displayAppBar.value = false;
|
||||
ref
|
||||
.read(
|
||||
toolbarVisibilityControllerProvider(
|
||||
ref.read(selectedTabProvider),
|
||||
).notifier,
|
||||
)
|
||||
.requestHide();
|
||||
}
|
||||
} else if (diff > 0) {
|
||||
if (diffAcc.value < 0) {
|
||||
@@ -267,6 +250,9 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
final tabInFullScreen = ref.watch(
|
||||
selectedTabStateProvider.select((value) => value?.isFullScreen ?? false),
|
||||
);
|
||||
final tabIsLoading = ref.watch(
|
||||
selectedTabStateProvider.select((value) => value?.isLoading ?? false),
|
||||
);
|
||||
|
||||
final overlayController = useOverlayPortalController();
|
||||
|
||||
@@ -300,12 +286,6 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
|
||||
final displayAppBar = useValueNotifier(true);
|
||||
|
||||
ref.listen(tabBarDismissableControllerProvider, (previous, next) {
|
||||
displayAppBar.value = !next;
|
||||
});
|
||||
|
||||
ref.listen(overlayControllerProvider, (previous, next) {
|
||||
if (next != null) {
|
||||
overlayController.show();
|
||||
@@ -330,14 +310,19 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
final sheetDisplayed = displayedSheet != null;
|
||||
|
||||
// Compute visibility states for toolbars
|
||||
final appBarVisible = useValueListenable(displayAppBar);
|
||||
final selectedTabId = ref.watch(selectedTabProvider);
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
|
||||
// Toolbar is visible when: sheet is shown OR (not fullscreen AND app bar visible)
|
||||
// appBarVisible reflects both scroll-based auto-hide and swipe dismiss state
|
||||
// Toolbar is visible when: sheet is shown OR (not fullscreen AND controller says visible)
|
||||
// The controller handles loading-start show internally via ref.listen on isLoading
|
||||
final effectiveAppBarVisible =
|
||||
toolbarState == ToolbarVisibility.visible;
|
||||
final topToolbarVisible =
|
||||
sheetDisplayed || (!tabInFullScreen && appBarVisible);
|
||||
sheetDisplayed || (!tabInFullScreen && effectiveAppBarVisible);
|
||||
final bottomToolbarVisible =
|
||||
sheetDisplayed || (!tabInFullScreen && appBarVisible);
|
||||
sheetDisplayed || (!tabInFullScreen && effectiveAppBarVisible);
|
||||
|
||||
// Calculate relative safe area for sheet max size
|
||||
final relativeSafeArea = MediaQuery.of(context).relativeSafeArea();
|
||||
@@ -382,7 +367,6 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
// Watch find-in-page visibility for the selected tab
|
||||
final selectedTabId = ref.watch(selectedTabProvider);
|
||||
final findInPageVisible =
|
||||
selectedTabId != null &&
|
||||
ref.watch(
|
||||
@@ -446,7 +430,8 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
// Keep clipping state in sync with keyboard and auto-hide states.
|
||||
// While keyboard is visible, do not apply toolbar clipping.
|
||||
useEffect(() {
|
||||
final targetClippingPx = autoHideTabBar && !keyboardVisible
|
||||
final targetClippingPx =
|
||||
autoHideTabBar && !keyboardVisible && !tabIsLoading
|
||||
? desiredToolbarClippingPx.value
|
||||
: 0;
|
||||
|
||||
@@ -456,7 +441,7 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [autoHideTabBar, keyboardVisible]);
|
||||
}, [autoHideTabBar, keyboardVisible, tabIsLoading]);
|
||||
|
||||
final animationProgressCallback = useCallback((
|
||||
double progress,
|
||||
@@ -467,13 +452,15 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
final clippingPx = -((1.0 - progress) * heightPx * pixelRatio).round();
|
||||
desiredToolbarClippingPx.value = clippingPx;
|
||||
|
||||
final targetClippingPx = keyboardVisible ? 0 : clippingPx;
|
||||
final targetClippingPx = (keyboardVisible || tabIsLoading)
|
||||
? 0
|
||||
: clippingPx;
|
||||
|
||||
if (targetClippingPx != lastClippingPx.value) {
|
||||
lastClippingPx.value = targetClippingPx;
|
||||
unawaited(viewportService.setVerticalClipping(targetClippingPx));
|
||||
}
|
||||
}, [keyboardVisible, pixelRatio]);
|
||||
}, [keyboardVisible, pixelRatio, tabIsLoading]);
|
||||
|
||||
// Theme with dynamic snackbar margin to position above bottom toolbar
|
||||
final themeData = Theme.of(context).copyWith(
|
||||
@@ -522,7 +509,6 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
bottom: browserBottomOffset,
|
||||
child: _Browser(
|
||||
overlayController: overlayController,
|
||||
displayAppBar: displayAppBar,
|
||||
tabInFullScreen: tabInFullScreen,
|
||||
pointerMoveEventSink: pointerMoveEventsController.sink,
|
||||
sheetDisplayed: sheetDisplayed,
|
||||
@@ -558,7 +544,6 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
: null,
|
||||
child: _TabBar(
|
||||
tabBarPosition: TabBarPosition.bottom,
|
||||
displayAppBar: displayAppBar,
|
||||
showMainToolbar: tabBarPosition == TabBarPosition.bottom,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
|
||||
@@ -583,7 +568,6 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
child: _TabBar(
|
||||
tabBarPosition: TabBarPosition.top,
|
||||
showMainToolbar: true,
|
||||
displayAppBar: displayAppBar,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
|
||||
pointerMoveEvents: pointerMoveEventsController.stream,
|
||||
@@ -729,7 +713,6 @@ class _Browser extends HookConsumerWidget {
|
||||
Duration get _backButtonPressTimeout => const Duration(seconds: 2);
|
||||
|
||||
final OverlayPortalController overlayController;
|
||||
final ValueNotifier<bool> displayAppBar;
|
||||
final StreamSink<Offset> pointerMoveEventSink;
|
||||
final bool tabInFullScreen;
|
||||
final bool sheetDisplayed;
|
||||
@@ -737,7 +720,6 @@ class _Browser extends HookConsumerWidget {
|
||||
|
||||
const _Browser({
|
||||
required this.overlayController,
|
||||
required this.displayAppBar,
|
||||
required this.tabInFullScreen,
|
||||
required this.pointerMoveEventSink,
|
||||
required this.sheetDisplayed,
|
||||
@@ -822,9 +804,10 @@ class _Browser extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
//Make sure app bar is visible
|
||||
if (!ref.read(tabBarDismissableControllerProvider)) {
|
||||
displayAppBar.value = true;
|
||||
}
|
||||
final tabId = ref.read(selectedTabProvider);
|
||||
ref
|
||||
.read(toolbarVisibilityControllerProvider(tabId).notifier)
|
||||
.show();
|
||||
|
||||
if (tabState?.isLoading == true) {
|
||||
lastBackButtonPress.value = null;
|
||||
|
||||
+26
-6
@@ -32,12 +32,13 @@ import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/menu_item_buttons.dart';
|
||||
@@ -59,6 +60,7 @@ import 'package:weblibre/presentation/icons/weblibre_icons.dart';
|
||||
import 'package:weblibre/presentation/widgets/safe_raw_image.dart';
|
||||
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
|
||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart';
|
||||
|
||||
class BrowserTopAppBar extends HookConsumerWidget {
|
||||
final bool showMainToolbar;
|
||||
@@ -68,7 +70,8 @@ class BrowserTopAppBar extends HookConsumerWidget {
|
||||
late final BrowserTabBar _tabBar;
|
||||
late final _size = Size.fromHeight(_tabBar.getToolbarHeight());
|
||||
|
||||
BrowserTopAppBar({super.key,
|
||||
BrowserTopAppBar({
|
||||
super.key,
|
||||
required this.showMainToolbar,
|
||||
required this.showContextualToolbar,
|
||||
required this.showQuickTabSwitcherBar,
|
||||
@@ -103,7 +106,8 @@ class BrowserBottomAppBar extends HookConsumerWidget {
|
||||
late final BrowserTabBar _tabBar;
|
||||
late final _size = Size.fromHeight(_tabBar.getToolbarHeight());
|
||||
|
||||
BrowserBottomAppBar({super.key,
|
||||
BrowserBottomAppBar({
|
||||
super.key,
|
||||
required this.showMainToolbar,
|
||||
required this.displayedSheet,
|
||||
required this.showContextualToolbar,
|
||||
@@ -148,7 +152,8 @@ class BrowserTabBar extends HookConsumerWidget {
|
||||
final bool showMainToolbarNavigationButton;
|
||||
final bool showMainToolbarTabActionButton;
|
||||
|
||||
const BrowserTabBar({super.key,
|
||||
const BrowserTabBar({
|
||||
super.key,
|
||||
required this.showMainToolbar,
|
||||
required this.displayedSheet,
|
||||
required this.showContextualToolbar,
|
||||
@@ -270,8 +275,23 @@ class BrowserTabBar extends HookConsumerWidget {
|
||||
!distance.dy.isNegative && distance.dy.abs() > dismissThreshold,
|
||||
};
|
||||
if (shouldDismiss && ref.read(bottomSheetControllerProvider) == null) {
|
||||
unawaited(HapticFeedback.lightImpact());
|
||||
ref.read(tabBarDismissableControllerProvider.notifier).dismiss();
|
||||
final viewportService = ref.read(viewportServiceProvider);
|
||||
if (viewportService.isBrowserHandlingScrollEnabled) {
|
||||
unawaited(HapticFeedback.lightImpact());
|
||||
ref
|
||||
.read(
|
||||
toolbarVisibilityControllerProvider(selectedTabId).notifier,
|
||||
)
|
||||
.dismiss();
|
||||
} else if (context.mounted) {
|
||||
showDismissOverrideMessage(context, () {
|
||||
ref
|
||||
.read(
|
||||
toolbarVisibilityControllerProvider(selectedTabId).notifier,
|
||||
)
|
||||
.dismiss();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
|
||||
+12
-4
@@ -21,8 +21,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
||||
import 'package:weblibre/features/geckoview/features/readerview/domain/providers/readerable.dart';
|
||||
|
||||
class BrowserFab extends HookConsumerWidget {
|
||||
@@ -30,7 +31,10 @@ class BrowserFab extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabBarDismissed = ref.watch(tabBarDismissableControllerProvider);
|
||||
final selectedTabId = ref.watch(selectedTabProvider);
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
|
||||
final appearanceButtonVisible = ref.watch(
|
||||
appearanceButtonVisibilityProvider.select(
|
||||
@@ -54,12 +58,16 @@ class BrowserFab extends HookConsumerWidget {
|
||||
},
|
||||
child: const Icon(MdiIcons.formatFont),
|
||||
);
|
||||
} else if (tabBarDismissed) {
|
||||
} else if (toolbarState == ToolbarVisibility.dismissed) {
|
||||
child = FloatingActionButton(
|
||||
key: const ValueKey('dock_fab'),
|
||||
heroTag: 'dock_fab',
|
||||
onPressed: () {
|
||||
ref.read(tabBarDismissableControllerProvider.notifier).show();
|
||||
ref
|
||||
.read(
|
||||
toolbarVisibilityControllerProvider(selectedTabId).notifier,
|
||||
)
|
||||
.forceShow();
|
||||
},
|
||||
child: const Icon(MdiIcons.dockBottom),
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ final class ProfileRepositoryProvider
|
||||
ProfileRepository create() => ProfileRepository();
|
||||
}
|
||||
|
||||
String _$profileRepositoryHash() => r'c17702af1e59727ab3fec26e9ca659048e92c8bb';
|
||||
String _$profileRepositoryHash() => r'b770e7406e1602f808cc8076c1eda67b4fce6b2d';
|
||||
|
||||
abstract class _$ProfileRepository extends $AsyncNotifier<List<Profile>> {
|
||||
FutureOr<List<Profile>> build();
|
||||
|
||||
@@ -192,3 +192,19 @@ void showTabUndoClose(
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
|
||||
void showDismissOverrideMessage(
|
||||
BuildContext context,
|
||||
VoidCallback onDismiss, {
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
|
||||
final snackBar = _createFloatingSnackBar(
|
||||
content: const Text('Hiding disabled by site'),
|
||||
action: SnackBarAction(label: 'Dismiss', onPressed: onDismiss),
|
||||
duration: duration,
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
|
||||
+12
@@ -26,6 +26,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.BrowserHandlingScrollFeature
|
||||
import eu.weblibre.flutter_mozilla_components.feature.KeyboardVisibilityFeature
|
||||
import eu.weblibre.flutter_mozilla_components.feature.ReadabilityExtractFeature
|
||||
import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature
|
||||
@@ -97,6 +98,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
// Keyboard visibility detection feature
|
||||
private var keyboardVisibilityFeature: KeyboardVisibilityFeature? = null
|
||||
|
||||
// Browser scroll-handling detection feature
|
||||
private var browserHandlingScrollFeature: BrowserHandlingScrollFeature? = null
|
||||
|
||||
// Registers a photo picker activity launcher in single-select mode.
|
||||
private val singleMediaPicker =
|
||||
AndroidPhotoPicker.singleMediaPicker(
|
||||
@@ -500,6 +504,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
keyboardVisibilityFeature = KeyboardVisibilityFeature(viewportEvents).also {
|
||||
it.start(binding.root)
|
||||
}
|
||||
|
||||
browserHandlingScrollFeature = BrowserHandlingScrollFeature(viewportEvents).also {
|
||||
it.start()
|
||||
}
|
||||
}
|
||||
|
||||
onEngineSetupComplete()
|
||||
@@ -614,6 +622,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
keyboardVisibilityFeature?.stop()
|
||||
keyboardVisibilityFeature = null
|
||||
|
||||
// Stop browser scroll-handling detection
|
||||
browserHandlingScrollFeature?.stop()
|
||||
browserHandlingScrollFeature = null
|
||||
|
||||
GlobalComponents.onPullToRefreshEnabledChanged = null
|
||||
val engineView = fragmentEngineView
|
||||
engineView?.setActivityContext(null)
|
||||
|
||||
+2
-1
@@ -216,7 +216,8 @@ class GeckoTabsApiImpl : GeckoTabsApi {
|
||||
progress = tab.content.progress.toLong(),
|
||||
isPrivate = tab.content.private,
|
||||
isFullScreen = tab.content.fullScreen,
|
||||
isLoading = tab.content.loading
|
||||
isLoading = tab.content.loading,
|
||||
showToolbarAsExpanded = tab.content.showToolbarAsExpanded,
|
||||
)
|
||||
) { }
|
||||
}
|
||||
|
||||
+14
-2
@@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import mozilla.components.browser.state.action.BrowserAction
|
||||
import mozilla.components.browser.state.action.ContentAction
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.feature.addons.logger
|
||||
@@ -188,7 +189,8 @@ class Events(
|
||||
it.content.private,
|
||||
it.content.fullScreen,
|
||||
it.content.progress,
|
||||
it.content.loading
|
||||
it.content.loading,
|
||||
it.content.showToolbarAsExpanded,
|
||||
)
|
||||
}
|
||||
.debounce(15)
|
||||
@@ -204,9 +206,19 @@ class Events(
|
||||
progress = tab.content.progress.toLong(),
|
||||
isPrivate = tab.content.private,
|
||||
isFullScreen = tab.content.fullScreen,
|
||||
isLoading = tab.content.loading
|
||||
isLoading = tab.content.loading,
|
||||
showToolbarAsExpanded = tab.content.showToolbarAsExpanded,
|
||||
)
|
||||
) { _ -> }
|
||||
|
||||
// Reset showToolbarAsExpanded after forwarding to Flutter,
|
||||
// mirroring Fenix ToolbarBehaviorController behavior.
|
||||
// This ensures subsequent expand events trigger a new state change.
|
||||
if (tab.content.showToolbarAsExpanded) {
|
||||
stateFlow.dispatch(
|
||||
ContentAction.UpdateExpandedToolbarStateAction(tab.id, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
|
||||
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
|
||||
|
||||
/**
|
||||
* Feature that reports whether GeckoView is currently handling scroll input.
|
||||
*
|
||||
* Checks InputResultDetail while active and only emits on state changes.
|
||||
*/
|
||||
class BrowserHandlingScrollFeature(
|
||||
private val flutterEvents: GeckoViewportEvents,
|
||||
) {
|
||||
private var running = false
|
||||
private var touchSessionActive = false
|
||||
private var lastValue: Boolean? = null
|
||||
private var touchListener: View.OnTouchListener? = null
|
||||
private var touchTargetView: View? = null
|
||||
|
||||
fun start() {
|
||||
if (running) return
|
||||
running = true
|
||||
touchSessionActive = false
|
||||
lastValue = null
|
||||
attachTouchListenerIfPossible()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (!running) return
|
||||
running = false
|
||||
touchSessionActive = false
|
||||
detachTouchListenerIfPossible()
|
||||
lastValue = null
|
||||
}
|
||||
|
||||
private fun attachTouchListenerIfPossible() {
|
||||
val engineViewRoot = GlobalComponents.components?.mainBrowserEngineView?.asView()
|
||||
if (engineViewRoot == null) return
|
||||
|
||||
if (touchListener != null) return
|
||||
|
||||
val targetView = resolveTouchTargetView(engineViewRoot)
|
||||
|
||||
touchListener = View.OnTouchListener { _, event ->
|
||||
if (!running) return@OnTouchListener false
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
touchSessionActive = true
|
||||
emitIfChanged()
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (touchSessionActive) {
|
||||
emitIfChanged()
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
touchSessionActive = false
|
||||
}
|
||||
}
|
||||
|
||||
// Never consume touch; GeckoView must keep handling it.
|
||||
false
|
||||
}
|
||||
|
||||
targetView.setOnTouchListener(touchListener)
|
||||
touchTargetView = targetView
|
||||
}
|
||||
|
||||
private fun detachTouchListenerIfPossible() {
|
||||
if (touchTargetView != null && touchListener != null) {
|
||||
touchTargetView?.setOnTouchListener(null)
|
||||
}
|
||||
touchListener = null
|
||||
touchTargetView = null
|
||||
}
|
||||
|
||||
private fun resolveTouchTargetView(root: View): View {
|
||||
if (root !is ViewGroup) return root
|
||||
|
||||
// GeckoEngineView wraps the actual NestedGeckoView as its first child.
|
||||
if (root.childCount > 0) {
|
||||
return root.getChildAt(0)
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
private fun emitIfChanged() {
|
||||
val isHandling = try {
|
||||
val engineView = GlobalComponents.components?.mainBrowserEngineView
|
||||
if (engineView == null) {
|
||||
false
|
||||
} else {
|
||||
val detail = engineView.getInputResultDetail()
|
||||
detail.canScrollToTop() || detail.canScrollToBottom()
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
false
|
||||
}
|
||||
|
||||
if (lastValue == isHandling) return
|
||||
lastValue = isHandling
|
||||
|
||||
val sequence = EventSequence.next()
|
||||
runOnUiThread {
|
||||
flutterEvents.onBrowserHandlingScrollChanged(sequence, isHandling) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-2
@@ -1597,7 +1597,8 @@ data class TabContentState (
|
||||
val progress: Long,
|
||||
val isPrivate: Boolean,
|
||||
val isFullScreen: Boolean,
|
||||
val isLoading: Boolean
|
||||
val isLoading: Boolean,
|
||||
val showToolbarAsExpanded: Boolean
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
@@ -1611,7 +1612,8 @@ data class TabContentState (
|
||||
val isPrivate = pigeonVar_list[6] as Boolean
|
||||
val isFullScreen = pigeonVar_list[7] as Boolean
|
||||
val isLoading = pigeonVar_list[8] as Boolean
|
||||
return TabContentState(id, parentId, contextId, url, title, progress, isPrivate, isFullScreen, isLoading)
|
||||
val showToolbarAsExpanded = pigeonVar_list[9] as Boolean
|
||||
return TabContentState(id, parentId, contextId, url, title, progress, isPrivate, isFullScreen, isLoading, showToolbarAsExpanded)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
@@ -1625,6 +1627,7 @@ data class TabContentState (
|
||||
isPrivate,
|
||||
isFullScreen,
|
||||
isLoading,
|
||||
showToolbarAsExpanded,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -6919,6 +6922,31 @@ class GeckoViewportEvents(private val binaryMessenger: BinaryMessenger, private
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Called when GeckoView scroll-handling eligibility changes.
|
||||
*
|
||||
* [sequence] Event sequence number for ordering.
|
||||
* [isHandling] True when browser content can consume scrolling for
|
||||
* dynamic toolbar behavior. False when content is not scrollable or
|
||||
* the page consumed touch input.
|
||||
*/
|
||||
fun onBrowserHandlingScrollChanged(sequenceArg: Long, isHandlingArg: Boolean, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(listOf(sequenceArg, isHandlingArg)) {
|
||||
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 @@ class GeckoViewportService extends GeckoViewportEvents {
|
||||
isVisible: false,
|
||||
isAnimating: false,
|
||||
));
|
||||
final _browserHandlingScrollSubject = BehaviorSubject<bool>.seeded(false);
|
||||
|
||||
/// Stream of keyboard visibility changes.
|
||||
///
|
||||
@@ -46,6 +47,17 @@ class GeckoViewportService extends GeckoViewportEvents {
|
||||
/// Whether the keyboard is currently visible.
|
||||
bool get isKeyboardVisible => _keyboardSubject.value.isVisible;
|
||||
|
||||
/// Stream of browser scroll-handling eligibility.
|
||||
///
|
||||
/// Emits true when GeckoView reports that browser content can handle
|
||||
/// scrolling for dynamic toolbar behavior.
|
||||
ValueStream<bool> get browserHandlingScrollEvents =>
|
||||
_browserHandlingScrollSubject.stream;
|
||||
|
||||
/// Current browser scroll-handling eligibility.
|
||||
bool get isBrowserHandlingScrollEnabled =>
|
||||
_browserHandlingScrollSubject.value;
|
||||
|
||||
/// Creates a new viewport service.
|
||||
///
|
||||
/// Call [setUp] to register the event handlers after construction.
|
||||
@@ -113,8 +125,14 @@ class GeckoViewportService extends GeckoViewportEvents {
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void onBrowserHandlingScrollChanged(int sequence, bool isHandling) {
|
||||
_browserHandlingScrollSubject.addWhenMoreRecent(sequence, null, isHandling);
|
||||
}
|
||||
|
||||
/// Disposes the service and closes all streams.
|
||||
Future<void> dispose() async {
|
||||
await _keyboardSubject.close();
|
||||
await _browserHandlingScrollSubject.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1735,6 +1735,7 @@ class TabContentState {
|
||||
required this.isPrivate,
|
||||
required this.isFullScreen,
|
||||
required this.isLoading,
|
||||
required this.showToolbarAsExpanded,
|
||||
});
|
||||
|
||||
String id;
|
||||
@@ -1755,6 +1756,8 @@ class TabContentState {
|
||||
|
||||
bool isLoading;
|
||||
|
||||
bool showToolbarAsExpanded;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
id,
|
||||
@@ -1766,6 +1769,7 @@ class TabContentState {
|
||||
isPrivate,
|
||||
isFullScreen,
|
||||
isLoading,
|
||||
showToolbarAsExpanded,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1784,6 +1788,7 @@ class TabContentState {
|
||||
isPrivate: result[6]! as bool,
|
||||
isFullScreen: result[7]! as bool,
|
||||
isLoading: result[8]! as bool,
|
||||
showToolbarAsExpanded: result[9]! as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8133,6 +8138,14 @@ abstract class GeckoViewportEvents {
|
||||
/// [isAnimating] Whether the keyboard is currently animating.
|
||||
void onKeyboardVisibilityChanged(int sequence, int heightPx, bool isVisible, bool isAnimating);
|
||||
|
||||
/// Called when GeckoView scroll-handling eligibility changes.
|
||||
///
|
||||
/// [sequence] Event sequence number for ordering.
|
||||
/// [isHandling] True when browser content can consume scrolling for
|
||||
/// dynamic toolbar behavior. False when content is not scrollable or
|
||||
/// the page consumed touch input.
|
||||
void onBrowserHandlingScrollChanged(int sequence, bool isHandling);
|
||||
|
||||
static void setUp(GeckoViewportEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
{
|
||||
@@ -8169,6 +8182,34 @@ abstract class GeckoViewportEvents {
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged$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.onBrowserHandlingScrollChanged was null.');
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final int? arg_sequence = (args[0] as int?);
|
||||
assert(arg_sequence != null,
|
||||
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged was null, expected non-null int.');
|
||||
final bool? arg_isHandling = (args[1] as bool?);
|
||||
assert(arg_isHandling != null,
|
||||
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoViewportEvents.onBrowserHandlingScrollChanged was null, expected non-null bool.');
|
||||
try {
|
||||
api.onBrowserHandlingScrollChanged(arg_sequence!, arg_isHandling!);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -517,6 +517,7 @@ class TabContentState {
|
||||
final bool isPrivate;
|
||||
final bool isFullScreen;
|
||||
final bool isLoading;
|
||||
final bool showToolbarAsExpanded;
|
||||
|
||||
TabContentState(
|
||||
this.id,
|
||||
@@ -528,6 +529,7 @@ class TabContentState {
|
||||
this.isPrivate,
|
||||
this.isFullScreen,
|
||||
this.isLoading,
|
||||
this.showToolbarAsExpanded,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1746,6 +1748,14 @@ abstract class GeckoViewportEvents {
|
||||
bool isVisible,
|
||||
bool isAnimating,
|
||||
);
|
||||
|
||||
/// Called when GeckoView scroll-handling eligibility changes.
|
||||
///
|
||||
/// [sequence] Event sequence number for ordering.
|
||||
/// [isHandling] True when browser content can consume scrolling for
|
||||
/// dynamic toolbar behavior. False when content is not scrollable or
|
||||
/// the page consumed touch input.
|
||||
void onBrowserHandlingScrollChanged(int sequence, bool isHandling);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user