From 2b163f2cbbbb359e2e316ee6d7da467553042175 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Wed, 27 Aug 2025 10:46:45 +0200 Subject: [PATCH] new setting to define tab bar swipe action --- .../geckoview/domain/providers.g.dart | 2 +- .../geckoview/domain/repositories/tab.dart | 43 ++++++++++--- .../geckoview/domain/repositories/tab.g.dart | 2 +- .../browser_modules/bottom_app_bar.dart | 25 +++++++- .../tabs/data/database/database.drift | 32 +++++++++- .../tabs/data/database/database.g.dart | 24 +++++++ .../tabs/domain/repositories/tab.g.dart | 2 +- .../screens/general_settings.dart | 61 ++++++++++++++++++ .../user/data/models/general_settings.dart | 10 ++- .../user/data/models/general_settings.g.dart | 63 +++++++++++++------ .../domain/repositories/general_settings.dart | 4 ++ .../repositories/general_settings.g.dart | 2 +- 12 files changed, 234 insertions(+), 36 deletions(-) diff --git a/app/lib/features/geckoview/domain/providers.g.dart b/app/lib/features/geckoview/domain/providers.g.dart index d62efd1f..885d4bf4 100644 --- a/app/lib/features/geckoview/domain/providers.g.dart +++ b/app/lib/features/geckoview/domain/providers.g.dart @@ -7,7 +7,7 @@ part of 'providers.dart'; // ************************************************************************** String _$selectionActionServiceHash() => - r'b1deac5e0566443b4729656c7082903b529bc269'; + r'c89d981c2ba253ed940887fa59c11ad93d8589a8'; /// See also [selectionActionService]. @ProviderFor(selectionActionService) diff --git a/app/lib/features/geckoview/domain/repositories/tab.dart b/app/lib/features/geckoview/domain/repositories/tab.dart index 56913b80..2e87bbaf 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.dart @@ -48,7 +48,6 @@ part 'tab.g.dart'; class TabRepository extends _$TabRepository { final _tabsService = GeckoTabService(); - String? _previousTabId; final _tabFromIntent = {}; bool hasLaunchedFromIntent(String? tabId) { @@ -124,9 +123,40 @@ class TabRepository extends _$TabRepository { ); } - Future selectPreviousTab() async { - if (_previousTabId != null) { - return selectTab(_previousTabId!); + Future selectPreviouslyOpenedTab(String tabId) async { + final previousTabId = await ref + .read(tabDatabaseProvider) + .previousTabByTimestamp(tabId: tabId) + .getSingleOrNull(); + + if (previousTabId != null) { + return selectTab(previousTabId); + } + + return false; + } + + Future selectPreviousTab(String tabId) async { + final previousTabId = await ref + .read(tabDatabaseProvider) + .previousTabByOrderKey(tabId: tabId) + .getSingleOrNull(); + + if (previousTabId != null) { + return selectTab(previousTabId); + } + + return false; + } + + Future selectNextTab(String tabId) async { + final previousTabId = await ref + .read(tabDatabaseProvider) + .nextTabByOrderKey(tabId: tabId) + .getSingleOrNull(); + + if (previousTabId != null) { + return selectTab(previousTabId); } return false; @@ -283,7 +313,6 @@ class TabRepository extends _$TabRepository { selectedTabProvider, (previous, tabId) async { if (tabId != null) { - _previousTabId = previous; await db.tabDao.touchTab(tabId, timestamp: DateTime.now()); } }, @@ -303,10 +332,6 @@ class TabRepository extends _$TabRepository { final syncTabs = next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false); - if (_previousTabId != null && !next.value.contains(_previousTabId)) { - _previousTabId = null; - } - if (syncTabs) { await db.tabDao.syncTabs(retainTabIds: next.value); } diff --git a/app/lib/features/geckoview/domain/repositories/tab.g.dart b/app/lib/features/geckoview/domain/repositories/tab.g.dart index e5ae6a17..12642bf0 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.g.dart @@ -6,7 +6,7 @@ part of 'tab.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabRepositoryHash() => r'3465f351f1d36e0f21b4e8ef4ebe7fdc4fbc0b75'; +String _$tabRepositoryHash() => r'4f562c1456eadcda7986ea2048667dd083f37618'; /// See also [TabRepository]. @ProviderFor(TabRepository) diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index 0b6a7530..f3edaac3 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -42,6 +42,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/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_button.dart'; +import 'package:weblibre/features/user/data/models/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/icons/tor_icons.dart'; @@ -93,7 +94,29 @@ class BrowserBottomAppBar extends HookConsumerWidget { final distance = dragStartPosition.value - details.globalPosition; if (distance.dx.abs() > 50) { - await ref.read(tabRepositoryProvider.notifier).selectPreviousTab(); + final selectedTab = ref.read(selectedTabProvider); + final setting = await ref + .read(generalSettingsRepositoryProvider.notifier) + .fetchSettings(); + + if (selectedTab != null) { + switch (setting.tabBarSwipeAction) { + case TabBarSwipeAction.switchLastOpened: + await ref + .read(tabRepositoryProvider.notifier) + .selectPreviouslyOpenedTab(selectedTab); + case TabBarSwipeAction.navigateOrderedTabs: + if (distance.dx < 0) { + await ref + .read(tabRepositoryProvider.notifier) + .selectPreviousTab(selectedTab); + } else { + await ref + .read(tabRepositoryProvider.notifier) + .selectNextTab(selectedTab); + } + } + } } }, child: AppBar( diff --git a/app/lib/features/geckoview/features/tabs/data/database/database.drift b/app/lib/features/geckoview/features/tabs/data/database/database.drift index c7cffa69..30f71ae3 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/database.drift +++ b/app/lib/features/geckoview/features/tabs/data/database/database.drift @@ -240,4 +240,34 @@ unorderedTabDescendants: JOIN descendants d ON t.parent_id = d.id ) SELECT id, parent_id - FROM descendants; \ No newline at end of file + FROM descendants; + +previousTabByTimestamp: + WITH ranked_tabs AS ( + SELECT id, timestamp, + LAG(id) OVER (ORDER BY timestamp) as prev_tab_id + FROM tab + ) + SELECT prev_tab_id + FROM ranked_tabs + WHERE id = :tab_id; + +previousTabByOrderKey: + WITH ranked_tabs AS ( + SELECT id, order_key, + LAG(id) OVER (ORDER BY order_key) as prev_tab_id + FROM tab + ) + SELECT prev_tab_id + FROM ranked_tabs + WHERE id = :tab_id; + +nextTabByOrderKey: + WITH ranked_tabs AS ( + SELECT id, order_key, + LEAD(id) OVER (ORDER BY order_key) as next_tab_id + FROM tab + ) + SELECT next_tab_id + FROM ranked_tabs + WHERE id = :tab_id; diff --git a/app/lib/features/geckoview/features/tabs/data/database/database.g.dart b/app/lib/features/geckoview/features/tabs/data/database/database.g.dart index d8bd4bdd..cca3404e 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/database.g.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/database.g.dart @@ -1256,6 +1256,30 @@ abstract class _$TabDatabase extends GeneratedDatabase { ); } + Selectable previousTabByTimestamp({required String tabId}) { + return customSelect( + 'WITH ranked_tabs AS (SELECT id, timestamp, LAG(id)OVER (ORDER BY timestamp RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS prev_tab_id FROM tab) SELECT prev_tab_id FROM ranked_tabs WHERE id = ?1', + variables: [Variable(tabId)], + readsFrom: {tab}, + ).map((QueryRow row) => row.read('prev_tab_id')); + } + + Selectable previousTabByOrderKey({required String tabId}) { + return customSelect( + 'WITH ranked_tabs AS (SELECT id, order_key, LAG(id)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS prev_tab_id FROM tab) SELECT prev_tab_id FROM ranked_tabs WHERE id = ?1', + variables: [Variable(tabId)], + readsFrom: {tab}, + ).map((QueryRow row) => row.read('prev_tab_id')); + } + + Selectable nextTabByOrderKey({required String tabId}) { + return customSelect( + 'WITH ranked_tabs AS (SELECT id, order_key, LEAD(id)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS next_tab_id FROM tab) SELECT next_tab_id FROM ranked_tabs WHERE id = ?1', + variables: [Variable(tabId)], + readsFrom: {tab}, + ).map((QueryRow row) => row.read('next_tab_id')); + } + @override Iterable> get allTables => allSchemaEntities.whereType>(); diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart index 519416cb..7b521975 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart @@ -6,7 +6,7 @@ part of 'tab.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabDataRepositoryHash() => r'be1bb8a8118c7bfd90d0f1ac89a04c0a2149a0b0'; +String _$tabDataRepositoryHash() => r'295c1718ecf41202709517fb2c5b31527e885dfa'; /// See also [TabDataRepository]. @ProviderFor(TabDataRepository) diff --git a/app/lib/features/settings/presentation/screens/general_settings.dart b/app/lib/features/settings/presentation/screens/general_settings.dart index bca73214..c9aac65a 100644 --- a/app/lib/features/settings/presentation/screens/general_settings.dart +++ b/app/lib/features/settings/presentation/screens/general_settings.dart @@ -386,6 +386,67 @@ class GeneralSettingsScreen extends HookConsumerWidget { ); }, ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Tab Bar Swipe Behavior'), + leading: Icon(MdiIcons.gestureSwipeHorizontal), + contentPadding: EdgeInsets.zero, + ), + RadioGroup( + groupValue: generalSettings.tabBarSwipeAction, + onChanged: (value) async { + if (value != null) { + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .tabBarSwipeAction(value), + ); + } + }, + child: const RadioListTile.adaptive( + value: TabBarSwipeAction.switchLastOpened, + title: Text('Switch to Last Used Tab'), + subtitle: Text( + 'Swipe to toggle between current and previously opened tab', + ), + ), + ), + RadioGroup( + groupValue: generalSettings.tabBarSwipeAction, + onChanged: (value) async { + if (value != null) { + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .tabBarSwipeAction(value), + ); + } + }, + child: const RadioListTile.adaptive( + value: TabBarSwipeAction.navigateOrderedTabs, + title: Text('Navigate Sequential Tabs'), + subtitle: Text( + 'Swipe left/right to move through tabs in order', + ), + ), + ), + ], + ), + ), 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 be12db15..ac2a2018 100644 --- a/app/lib/features/user/data/models/general_settings.dart +++ b/app/lib/features/user/data/models/general_settings.dart @@ -29,6 +29,8 @@ part 'general_settings.g.dart'; const _fallbackSearchProvider = 'wikipedia'; const _fallbackAutocompleteProvider = SearchSuggestionProviders.none; +enum TabBarSwipeAction { switchLastOpened, navigateOrderedTabs } + enum DeleteBrowsingDataType { tabs('Open tabs'), history('Browsing history'), @@ -59,6 +61,7 @@ class GeneralSettings with FastEquatable { final TabType defaultIntentTabType; final bool proxyPrivateTabsTor; final bool autoHideTabBar; + final TabBarSwipeAction tabBarSwipeAction; GeneralSettings({ required this.themeMode, @@ -74,6 +77,7 @@ class GeneralSettings with FastEquatable { required this.defaultCreateTabType, required this.defaultIntentTabType, required this.autoHideTabBar, + required this.tabBarSwipeAction, }); GeneralSettings.withDefaults({ @@ -90,6 +94,7 @@ class GeneralSettings with FastEquatable { TabType? defaultCreateTabType, TabType? defaultIntentTabType, bool? autoHideTabBar, + TabBarSwipeAction? tabBarSwipeAction, }) : themeMode = themeMode ?? ThemeMode.dark, enableReadability = enableReadability ?? true, enforceReadability = enforceReadability ?? false, @@ -102,7 +107,9 @@ class GeneralSettings with FastEquatable { enableLocalAiFeatures = enableLocalAiFeatures ?? true, defaultCreateTabType = defaultCreateTabType ?? TabType.regular, defaultIntentTabType = defaultIntentTabType ?? TabType.regular, - autoHideTabBar = autoHideTabBar ?? true; + autoHideTabBar = autoHideTabBar ?? true, + tabBarSwipeAction = + tabBarSwipeAction ?? TabBarSwipeAction.switchLastOpened; factory GeneralSettings.fromJson(Map json) => _$GeneralSettingsFromJson(json); @@ -124,5 +131,6 @@ class GeneralSettings with FastEquatable { defaultIntentTabType, proxyPrivateTabsTor, autoHideTabBar, + tabBarSwipeAction, ]; } 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 e09eb02e..ddb78cbe 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -37,6 +37,8 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings autoHideTabBar(bool autoHideTabBar); + GeneralSettings tabBarSwipeAction(TabBarSwipeAction tabBarSwipeAction); + /// 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 @@ -57,6 +59,7 @@ abstract class _$GeneralSettingsCWProxy { TabType defaultCreateTabType, TabType defaultIntentTabType, bool autoHideTabBar, + TabBarSwipeAction tabBarSwipeAction, }); } @@ -119,6 +122,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings autoHideTabBar(bool autoHideTabBar) => this(autoHideTabBar: autoHideTabBar); + @override + GeneralSettings tabBarSwipeAction(TabBarSwipeAction tabBarSwipeAction) => + this(tabBarSwipeAction: tabBarSwipeAction); + @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. /// @@ -140,6 +147,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? defaultCreateTabType = const $CopyWithPlaceholder(), Object? defaultIntentTabType = const $CopyWithPlaceholder(), Object? autoHideTabBar = const $CopyWithPlaceholder(), + Object? tabBarSwipeAction = const $CopyWithPlaceholder(), }) { return GeneralSettings( themeMode: themeMode == const $CopyWithPlaceholder() @@ -200,6 +208,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.autoHideTabBar // ignore: cast_nullable_to_non_nullable : autoHideTabBar as bool, + tabBarSwipeAction: tabBarSwipeAction == const $CopyWithPlaceholder() + ? _value.tabBarSwipeAction + // ignore: cast_nullable_to_non_nullable + : tabBarSwipeAction as TabBarSwipeAction, ); } } @@ -241,28 +253,34 @@ GeneralSettings _$GeneralSettingsFromJson(Map json) => json['defaultIntentTabType'], ), autoHideTabBar: json['autoHideTabBar'] as bool?, + tabBarSwipeAction: $enumDecodeNullable( + _$TabBarSwipeActionEnumMap, + json['tabBarSwipeAction'], + ), ); -Map _$GeneralSettingsToJson(GeneralSettings instance) => - { - 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, - 'enableReadability': instance.enableReadability, - 'enforceReadability': instance.enforceReadability, - 'deleteBrowsingDataOnQuit': instance.deleteBrowsingDataOnQuit - ?.map((e) => _$DeleteBrowsingDataTypeEnumMap[e]!) - .toList(), - 'defaultSearchProvider': instance.defaultSearchProvider, - 'defaultSearchSuggestionsProvider': - _$SearchSuggestionProvidersEnumMap[instance - .defaultSearchSuggestionsProvider]!, - 'createChildTabsOption': instance.createChildTabsOption, - 'showExtensionShortcut': instance.showExtensionShortcut, - 'enableLocalAiFeatures': instance.enableLocalAiFeatures, - 'defaultCreateTabType': _$TabTypeEnumMap[instance.defaultCreateTabType]!, - 'defaultIntentTabType': _$TabTypeEnumMap[instance.defaultIntentTabType]!, - 'proxyPrivateTabsTor': instance.proxyPrivateTabsTor, - 'autoHideTabBar': instance.autoHideTabBar, - }; +Map _$GeneralSettingsToJson( + GeneralSettings instance, +) => { + 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, + 'enableReadability': instance.enableReadability, + 'enforceReadability': instance.enforceReadability, + 'deleteBrowsingDataOnQuit': instance.deleteBrowsingDataOnQuit + ?.map((e) => _$DeleteBrowsingDataTypeEnumMap[e]!) + .toList(), + 'defaultSearchProvider': instance.defaultSearchProvider, + 'defaultSearchSuggestionsProvider': + _$SearchSuggestionProvidersEnumMap[instance + .defaultSearchSuggestionsProvider]!, + 'createChildTabsOption': instance.createChildTabsOption, + 'showExtensionShortcut': instance.showExtensionShortcut, + 'enableLocalAiFeatures': instance.enableLocalAiFeatures, + 'defaultCreateTabType': _$TabTypeEnumMap[instance.defaultCreateTabType]!, + 'defaultIntentTabType': _$TabTypeEnumMap[instance.defaultIntentTabType]!, + 'proxyPrivateTabsTor': instance.proxyPrivateTabsTor, + 'autoHideTabBar': instance.autoHideTabBar, + 'tabBarSwipeAction': _$TabBarSwipeActionEnumMap[instance.tabBarSwipeAction]!, +}; const _$ThemeModeEnumMap = { ThemeMode.system: 'system', @@ -292,3 +310,8 @@ const _$TabTypeEnumMap = { TabType.private: 'private', TabType.child: 'child', }; + +const _$TabBarSwipeActionEnumMap = { + TabBarSwipeAction.switchLastOpened: 'switchLastOpened', + TabBarSwipeAction.navigateOrderedTabs: 'navigateOrderedTabs', +}; diff --git a/app/lib/features/user/domain/repositories/general_settings.dart b/app/lib/features/user/domain/repositories/general_settings.dart index 67b8fb68..c01eb5e7 100644 --- a/app/lib/features/user/domain/repositories/general_settings.dart +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -96,6 +96,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.bool, db.typeMapping, ), + 'tabBarSwipeAction': settings['tabBarSwipeAction']?.readAs( + DriftSqlType.string, + 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 4653bc01..5a1c8da2 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'43f314d1c0d318d8c28afd38fee5f2416a26f423'; + r'e311760a7773109a90c28275262883fcca348bbb'; /// See also [GeneralSettingsRepository]. @ProviderFor(GeneralSettingsRepository)