From c04b0ca7bbddaef3c53ed68c88a420db5344361b Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sat, 23 Aug 2025 07:23:45 +0200 Subject: [PATCH] improve tab bar auto hide; add autohide setting --- .../geckoview/domain/providers/tab_state.dart | 3 +- .../domain/providers/tab_state.g.dart | 88 ++++++----- .../browser/presentation/screens/browser.dart | 147 ++++++++++++------ .../screens/general_settings.dart | 14 ++ .../user/data/models/general_settings.dart | 8 +- .../user/data/models/general_settings.g.dart | 14 ++ .../domain/repositories/general_settings.dart | 4 + .../repositories/general_settings.g.dart | 2 +- 8 files changed, 186 insertions(+), 94 deletions(-) diff --git a/app/lib/features/geckoview/domain/providers/tab_state.dart b/app/lib/features/geckoview/domain/providers/tab_state.dart index c978d024..0647daac 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.dart @@ -256,8 +256,7 @@ AsyncValue selectedTabContainerId(Ref ref) { } @Riverpod() -Stream selectedTabScrollY(Ref ref, Duration sampleTime) { - final tabId = ref.watch(selectedTabProvider); +Stream tabScrollY(Ref ref, String? tabId, Duration sampleTime) { final eventService = ref.watch(eventServiceProvider); return eventService.scrollEvent diff --git a/app/lib/features/geckoview/domain/providers/tab_state.g.dart b/app/lib/features/geckoview/domain/providers/tab_state.g.dart index a8023ab1..eab67fbc 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.g.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.g.dart @@ -193,28 +193,27 @@ final selectedTabContainerIdProvider = Provider>.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef SelectedTabContainerIdRef = ProviderRef>; -String _$selectedTabScrollYHash() => - r'7ca7cccfa7ebb7f13e0d7c096ce6c93f9e220cb5'; +String _$tabScrollYHash() => r'dcb1e82b42bab98c4fc2ee4c7a22f37a28b8ce9e'; -/// See also [selectedTabScrollY]. -@ProviderFor(selectedTabScrollY) -const selectedTabScrollYProvider = SelectedTabScrollYFamily(); +/// See also [tabScrollY]. +@ProviderFor(tabScrollY) +const tabScrollYProvider = TabScrollYFamily(); -/// See also [selectedTabScrollY]. -class SelectedTabScrollYFamily extends Family> { - /// See also [selectedTabScrollY]. - const SelectedTabScrollYFamily(); +/// See also [tabScrollY]. +class TabScrollYFamily extends Family> { + /// See also [tabScrollY]. + const TabScrollYFamily(); - /// See also [selectedTabScrollY]. - SelectedTabScrollYProvider call(Duration sampleTime) { - return SelectedTabScrollYProvider(sampleTime); + /// See also [tabScrollY]. + TabScrollYProvider call(String? tabId, Duration sampleTime) { + return TabScrollYProvider(tabId, sampleTime); } @override - SelectedTabScrollYProvider getProviderOverride( - covariant SelectedTabScrollYProvider provider, + TabScrollYProvider getProviderOverride( + covariant TabScrollYProvider provider, ) { - return call(provider.sampleTime); + return call(provider.tabId, provider.sampleTime); } static const Iterable? _dependencies = null; @@ -229,51 +228,52 @@ class SelectedTabScrollYFamily extends Family> { _allTransitiveDependencies; @override - String? get name => r'selectedTabScrollYProvider'; + String? get name => r'tabScrollYProvider'; } -/// See also [selectedTabScrollY]. -class SelectedTabScrollYProvider extends AutoDisposeStreamProvider { - /// See also [selectedTabScrollY]. - SelectedTabScrollYProvider(Duration sampleTime) +/// See also [tabScrollY]. +class TabScrollYProvider extends AutoDisposeStreamProvider { + /// See also [tabScrollY]. + TabScrollYProvider(String? tabId, Duration sampleTime) : this._internal( - (ref) => selectedTabScrollY(ref as SelectedTabScrollYRef, sampleTime), - from: selectedTabScrollYProvider, - name: r'selectedTabScrollYProvider', + (ref) => tabScrollY(ref as TabScrollYRef, tabId, sampleTime), + from: tabScrollYProvider, + name: r'tabScrollYProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$selectedTabScrollYHash, - dependencies: SelectedTabScrollYFamily._dependencies, - allTransitiveDependencies: - SelectedTabScrollYFamily._allTransitiveDependencies, + : _$tabScrollYHash, + dependencies: TabScrollYFamily._dependencies, + allTransitiveDependencies: TabScrollYFamily._allTransitiveDependencies, + tabId: tabId, sampleTime: sampleTime, ); - SelectedTabScrollYProvider._internal( + TabScrollYProvider._internal( super._createNotifier, { required super.name, required super.dependencies, required super.allTransitiveDependencies, required super.debugGetCreateSourceHash, required super.from, + required this.tabId, required this.sampleTime, }) : super.internal(); + final String? tabId; final Duration sampleTime; @override - Override overrideWith( - Stream Function(SelectedTabScrollYRef provider) create, - ) { + Override overrideWith(Stream Function(TabScrollYRef provider) create) { return ProviderOverride( origin: this, - override: SelectedTabScrollYProvider._internal( - (ref) => create(ref as SelectedTabScrollYRef), + override: TabScrollYProvider._internal( + (ref) => create(ref as TabScrollYRef), from: from, name: null, dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, + tabId: tabId, sampleTime: sampleTime, ), ); @@ -281,18 +281,20 @@ class SelectedTabScrollYProvider extends AutoDisposeStreamProvider { @override AutoDisposeStreamProviderElement createElement() { - return _SelectedTabScrollYProviderElement(this); + return _TabScrollYProviderElement(this); } @override bool operator ==(Object other) { - return other is SelectedTabScrollYProvider && + return other is TabScrollYProvider && + other.tabId == tabId && other.sampleTime == sampleTime; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, tabId.hashCode); hash = _SystemHash.combine(hash, sampleTime.hashCode); return _SystemHash.finish(hash); @@ -301,18 +303,22 @@ class SelectedTabScrollYProvider extends AutoDisposeStreamProvider { @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -mixin SelectedTabScrollYRef on AutoDisposeStreamProviderRef { +mixin TabScrollYRef on AutoDisposeStreamProviderRef { + /// The parameter `tabId` of this provider. + String? get tabId; + /// The parameter `sampleTime` of this provider. Duration get sampleTime; } -class _SelectedTabScrollYProviderElement - extends AutoDisposeStreamProviderElement - with SelectedTabScrollYRef { - _SelectedTabScrollYProviderElement(super.provider); +class _TabScrollYProviderElement extends AutoDisposeStreamProviderElement + with TabScrollYRef { + _TabScrollYProviderElement(super.provider); @override - Duration get sampleTime => (origin as SelectedTabScrollYProvider).sampleTime; + String? get tabId => (origin as TabScrollYProvider).tabId; + @override + Duration get sampleTime => (origin as TabScrollYProvider).sampleTime; } String _$tabStatesHash() => r'66cff6a7b36328ee23b89fdd53046987b346bfcd'; diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index d4bf5590..52325d02 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -47,6 +47,7 @@ import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_ import 'package:weblibre/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart'; import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart'; import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_appearance_button.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/utils/ui_helper.dart' as ui_helper; class BrowserScreen extends HookConsumerWidget { @@ -80,38 +81,100 @@ class BrowserScreen extends HookConsumerWidget { }, ); + final themeData = useMemoized( + () => Theme.of(context).copyWith( + bottomSheetTheme: BottomSheetThemeData( + constraints: BoxConstraints( + maxWidth: + MediaQuery.of(context).size.width - + math.max( + MediaQuery.of(context).padding.left * 2, + MediaQuery.of(context).padding.right * 2, + ), + ), + ), + ), + [], + ); + + bool dismissOnThreshold(DraggableScrollableNotification notification) { + if (!context.mounted) return false; + + if (notification.extent <= 0.1) { + ref.read(bottomSheetControllerProvider.notifier).dismiss(); + return true; + } else { + ref.read(bottomSheetExtendProvider.notifier).add(notification.extent); + } + + return false; + } + return PopScope( //We need this for BackButtonListener to work downstream //No direct pop result will be handled here canPop: false, child: Theme( - data: Theme.of(context).copyWith( - bottomSheetTheme: BottomSheetThemeData( - //Remove M3 default of 640p - constraints: BoxConstraints( - maxWidth: - MediaQuery.of(context).size.width - - math.max( - MediaQuery.of(context).padding.left * 2, - MediaQuery.of(context).padding.right * 2, - ), - ), - ), - ), + data: themeData, child: Scaffold( bottomNavigationBar: HookConsumer( builder: (context, ref, child) { + final tabId = ref.watch(selectedTabProvider); + final displayedSheet = ref.watch(bottomSheetControllerProvider); + final tabInFullScreen = ref.watch( selectedTabStateProvider.select( (value) => value?.isFullScreen ?? false, ), ); + final autoHideTabBar = ref.watch( + generalSettingsWithDefaultsProvider.select( + (value) => value.autoHideTabBar, + ), + ); + + if (!autoHideTabBar) { + return Visibility( + visible: !tabInFullScreen, + child: BrowserBottomAppBar(displayedSheet: displayedSheet), + ); + } + final hidden = useState(false); final diffAcc = useRef(0.0); + useEffect(() { + hidden.value = false; + diffAcc.value = 0.0; + + return null; + }, [tabId]); + ref.listen( - selectedTabScrollYProvider(const Duration(milliseconds: 250)), + tabStateProvider(tabId).select((value) => value?.isLoading), + (previous, next) { + if (next == true) { + hidden.value = false; + diffAcc.value = 0.0; + } + }, + ); + + ref.listen( + tabStateProvider(tabId).select((value) => value?.historyState), + (previous, next) { + if (next != null && previous != null) { + if (previous != next) { + hidden.value = false; + diffAcc.value = 0.0; + } + } + }, + ); + + ref.listen( + tabScrollYProvider(tabId, const Duration(milliseconds: 50)), (previous, next) { if (previous?.valueOrNull != null && next.valueOrNull != null) { @@ -266,7 +329,7 @@ class BrowserScreen extends HookConsumerWidget { return true; } }, - child: _BrowserView(displayedSheet: displayedSheet), + child: _BrowserView(sheetDisplayed: displayedSheet != null), ), ), ); @@ -274,33 +337,25 @@ class BrowserScreen extends HookConsumerWidget { ), floatingActionButton: ReaderAppearanceButton(), bottomSheet: (displayedSheet != null) - ? NotificationListener( - onNotification: (notification) { - if (notification.extent <= 0.1) { - ref - .read(bottomSheetControllerProvider.notifier) - .dismiss(); - return true; - } else { - ref - .read(bottomSheetExtendProvider.notifier) - .add(notification.extent); - } - - return false; - }, - child: switch (displayedSheet) { - ViewTabsSheet() => _ViewTabsSheet( + ? switch (displayedSheet) { + ViewTabsSheet() => + NotificationListener( key: ValueKey(displayedSheet), - maxChildSize: MediaQuery.of(context).relativeSafeArea(), + onNotification: dismissOnThreshold, + child: _ViewTabsSheet( + maxChildSize: MediaQuery.of(context).relativeSafeArea(), + ), ), - final EditUrlSheet parameter => _ViewUrlSheet( - key: ValueKey(displayedSheet), - initialTabState: parameter.tabState, - maxChildSize: MediaQuery.of(context).relativeSafeArea(), + final EditUrlSheet parameter => + NotificationListener( + key: ValueKey(parameter), + onNotification: dismissOnThreshold, + child: _ViewUrlSheet( + initialTabState: parameter.tabState, + maxChildSize: MediaQuery.of(context).relativeSafeArea(), + ), ), - }, - ) + } : null, ), ), @@ -309,9 +364,9 @@ class BrowserScreen extends HookConsumerWidget { } class _BrowserView extends StatelessWidget { - const _BrowserView({required this.displayedSheet}); + const _BrowserView({required this.sheetDisplayed}); - final Sheet? displayedSheet; + final bool sheetDisplayed; @override Widget build(BuildContext context) { @@ -376,7 +431,7 @@ class _BrowserView extends StatelessWidget { ], ), ), - if (displayedSheet != null) + if (sheetDisplayed) ModalBarrier( color: Theme.of(context).dialogTheme.barrierColor ?? Colors.black54, ), @@ -389,11 +444,7 @@ class _ViewUrlSheet extends HookConsumerWidget { final double maxChildSize; final TabState initialTabState; - const _ViewUrlSheet({ - super.key, - required this.initialTabState, - this.maxChildSize = 1.0, - }); + const _ViewUrlSheet({required this.initialTabState, this.maxChildSize = 1.0}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -431,7 +482,7 @@ class _ViewUrlSheet extends HookConsumerWidget { class _ViewTabsSheet extends HookConsumerWidget { final double maxChildSize; - const _ViewTabsSheet({super.key, this.maxChildSize = 1.0}); + const _ViewTabsSheet({this.maxChildSize = 1.0}); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/app/lib/features/settings/presentation/screens/general_settings.dart b/app/lib/features/settings/presentation/screens/general_settings.dart index 86045552..bca73214 100644 --- a/app/lib/features/settings/presentation/screens/general_settings.dart +++ b/app/lib/features/settings/presentation/screens/general_settings.dart @@ -372,6 +372,20 @@ class GeneralSettingsScreen extends HookConsumerWidget { ); }, ), + SwitchListTile.adaptive( + title: const Text('Auto Hide Tab Bar'), + subtitle: const Text('Hide tab bar when scrolling'), + secondary: const Icon(MdiIcons.folderHidden), + value: generalSettings.autoHideTabBar, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.autoHideTabBar(value), + ); + }, + ), Consumer( builder: (context, ref, child) { final size = ref.watch( diff --git a/app/lib/features/user/data/models/general_settings.dart b/app/lib/features/user/data/models/general_settings.dart index 08801338..be12db15 100644 --- a/app/lib/features/user/data/models/general_settings.dart +++ b/app/lib/features/user/data/models/general_settings.dart @@ -57,8 +57,8 @@ class GeneralSettings with FastEquatable { final bool enableLocalAiFeatures; final TabType defaultCreateTabType; final TabType defaultIntentTabType; - final bool proxyPrivateTabsTor; + final bool autoHideTabBar; GeneralSettings({ required this.themeMode, @@ -73,6 +73,7 @@ class GeneralSettings with FastEquatable { required this.enableLocalAiFeatures, required this.defaultCreateTabType, required this.defaultIntentTabType, + required this.autoHideTabBar, }); GeneralSettings.withDefaults({ @@ -88,6 +89,7 @@ class GeneralSettings with FastEquatable { bool? enableLocalAiFeatures, TabType? defaultCreateTabType, TabType? defaultIntentTabType, + bool? autoHideTabBar, }) : themeMode = themeMode ?? ThemeMode.dark, enableReadability = enableReadability ?? true, enforceReadability = enforceReadability ?? false, @@ -99,7 +101,8 @@ class GeneralSettings with FastEquatable { showExtensionShortcut = showExtensionShortcut ?? false, enableLocalAiFeatures = enableLocalAiFeatures ?? true, defaultCreateTabType = defaultCreateTabType ?? TabType.regular, - defaultIntentTabType = defaultIntentTabType ?? TabType.regular; + defaultIntentTabType = defaultIntentTabType ?? TabType.regular, + autoHideTabBar = autoHideTabBar ?? true; factory GeneralSettings.fromJson(Map json) => _$GeneralSettingsFromJson(json); @@ -120,5 +123,6 @@ class GeneralSettings with FastEquatable { defaultCreateTabType, defaultIntentTabType, proxyPrivateTabsTor, + autoHideTabBar, ]; } diff --git a/app/lib/features/user/data/models/general_settings.g.dart b/app/lib/features/user/data/models/general_settings.g.dart index 3bd31280..e09eb02e 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -35,6 +35,8 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings defaultIntentTabType(TabType defaultIntentTabType); + GeneralSettings autoHideTabBar(bool autoHideTabBar); + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `GeneralSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// /// Usage @@ -54,6 +56,7 @@ abstract class _$GeneralSettingsCWProxy { bool enableLocalAiFeatures, TabType defaultCreateTabType, TabType defaultIntentTabType, + bool autoHideTabBar, }); } @@ -112,6 +115,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings defaultIntentTabType(TabType defaultIntentTabType) => this(defaultIntentTabType: defaultIntentTabType); + @override + GeneralSettings autoHideTabBar(bool autoHideTabBar) => + this(autoHideTabBar: autoHideTabBar); + @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `GeneralSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// @@ -132,6 +139,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? enableLocalAiFeatures = const $CopyWithPlaceholder(), Object? defaultCreateTabType = const $CopyWithPlaceholder(), Object? defaultIntentTabType = const $CopyWithPlaceholder(), + Object? autoHideTabBar = const $CopyWithPlaceholder(), }) { return GeneralSettings( themeMode: themeMode == const $CopyWithPlaceholder() @@ -188,6 +196,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.defaultIntentTabType // ignore: cast_nullable_to_non_nullable : defaultIntentTabType as TabType, + autoHideTabBar: autoHideTabBar == const $CopyWithPlaceholder() + ? _value.autoHideTabBar + // ignore: cast_nullable_to_non_nullable + : autoHideTabBar as bool, ); } } @@ -228,6 +240,7 @@ GeneralSettings _$GeneralSettingsFromJson(Map json) => _$TabTypeEnumMap, json['defaultIntentTabType'], ), + autoHideTabBar: json['autoHideTabBar'] as bool?, ); Map _$GeneralSettingsToJson(GeneralSettings instance) => @@ -248,6 +261,7 @@ Map _$GeneralSettingsToJson(GeneralSettings instance) => 'defaultCreateTabType': _$TabTypeEnumMap[instance.defaultCreateTabType]!, 'defaultIntentTabType': _$TabTypeEnumMap[instance.defaultIntentTabType]!, 'proxyPrivateTabsTor': instance.proxyPrivateTabsTor, + 'autoHideTabBar': instance.autoHideTabBar, }; const _$ThemeModeEnumMap = { diff --git a/app/lib/features/user/domain/repositories/general_settings.dart b/app/lib/features/user/domain/repositories/general_settings.dart index 9ad915e7..67b8fb68 100644 --- a/app/lib/features/user/domain/repositories/general_settings.dart +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -92,6 +92,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.string, db.typeMapping, ), + 'autoHideTabBar': settings['autoHideTabBar']?.readAs( + DriftSqlType.bool, + db.typeMapping, + ), }); } diff --git a/app/lib/features/user/domain/repositories/general_settings.g.dart b/app/lib/features/user/domain/repositories/general_settings.g.dart index 7997831e..4653bc01 100644 --- a/app/lib/features/user/domain/repositories/general_settings.g.dart +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -27,7 +27,7 @@ final generalSettingsWithDefaultsProvider = typedef GeneralSettingsWithDefaultsRef = AutoDisposeProviderRef; String _$generalSettingsRepositoryHash() => - r'7e1c4d6051c4c3972b6998dee89d13b49be32e48'; + r'43f314d1c0d318d8c28afd38fee5f2416a26f423'; /// See also [GeneralSettingsRepository]. @ProviderFor(GeneralSettingsRepository)