new setting to define tab bar swipe action

This commit is contained in:
Fabian Freund
2025-08-27 10:46:45 +02:00
parent 9a8e9c228b
commit 2b163f2cbb
12 changed files with 234 additions and 36 deletions
@@ -7,7 +7,7 @@ part of 'providers.dart';
// ************************************************************************** // **************************************************************************
String _$selectionActionServiceHash() => String _$selectionActionServiceHash() =>
r'b1deac5e0566443b4729656c7082903b529bc269'; r'c89d981c2ba253ed940887fa59c11ad93d8589a8';
/// See also [selectionActionService]. /// See also [selectionActionService].
@ProviderFor(selectionActionService) @ProviderFor(selectionActionService)
@@ -48,7 +48,6 @@ part 'tab.g.dart';
class TabRepository extends _$TabRepository { class TabRepository extends _$TabRepository {
final _tabsService = GeckoTabService(); final _tabsService = GeckoTabService();
String? _previousTabId;
final _tabFromIntent = <String>{}; final _tabFromIntent = <String>{};
bool hasLaunchedFromIntent(String? tabId) { bool hasLaunchedFromIntent(String? tabId) {
@@ -124,9 +123,40 @@ class TabRepository extends _$TabRepository {
); );
} }
Future<bool> selectPreviousTab() async { Future<bool> selectPreviouslyOpenedTab(String tabId) async {
if (_previousTabId != null) { final previousTabId = await ref
return selectTab(_previousTabId!); .read(tabDatabaseProvider)
.previousTabByTimestamp(tabId: tabId)
.getSingleOrNull();
if (previousTabId != null) {
return selectTab(previousTabId);
}
return false;
}
Future<bool> selectPreviousTab(String tabId) async {
final previousTabId = await ref
.read(tabDatabaseProvider)
.previousTabByOrderKey(tabId: tabId)
.getSingleOrNull();
if (previousTabId != null) {
return selectTab(previousTabId);
}
return false;
}
Future<bool> selectNextTab(String tabId) async {
final previousTabId = await ref
.read(tabDatabaseProvider)
.nextTabByOrderKey(tabId: tabId)
.getSingleOrNull();
if (previousTabId != null) {
return selectTab(previousTabId);
} }
return false; return false;
@@ -283,7 +313,6 @@ class TabRepository extends _$TabRepository {
selectedTabProvider, selectedTabProvider,
(previous, tabId) async { (previous, tabId) async {
if (tabId != null) { if (tabId != null) {
_previousTabId = previous;
await db.tabDao.touchTab(tabId, timestamp: DateTime.now()); await db.tabDao.touchTab(tabId, timestamp: DateTime.now());
} }
}, },
@@ -303,10 +332,6 @@ class TabRepository extends _$TabRepository {
final syncTabs = final syncTabs =
next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false); next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false);
if (_previousTabId != null && !next.value.contains(_previousTabId)) {
_previousTabId = null;
}
if (syncTabs) { if (syncTabs) {
await db.tabDao.syncTabs(retainTabIds: next.value); await db.tabDao.syncTabs(retainTabIds: next.value);
} }
@@ -6,7 +6,7 @@ part of 'tab.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$tabRepositoryHash() => r'3465f351f1d36e0f21b4e8ef4ebe7fdc4fbc0b75'; String _$tabRepositoryHash() => r'4f562c1456eadcda7986ea2048667dd083f37618';
/// See also [TabRepository]. /// See also [TabRepository].
@ProviderFor(TabRepository) @ProviderFor(TabRepository)
@@ -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/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/controllers/readerable.dart';
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.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/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart';
import 'package:weblibre/presentation/icons/tor_icons.dart'; import 'package:weblibre/presentation/icons/tor_icons.dart';
@@ -93,7 +94,29 @@ class BrowserBottomAppBar extends HookConsumerWidget {
final distance = dragStartPosition.value - details.globalPosition; final distance = dragStartPosition.value - details.globalPosition;
if (distance.dx.abs() > 50) { 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( child: AppBar(
@@ -241,3 +241,33 @@ unorderedTabDescendants:
) )
SELECT id, parent_id SELECT id, parent_id
FROM descendants; 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;
@@ -1256,6 +1256,30 @@ abstract class _$TabDatabase extends GeneratedDatabase {
); );
} }
Selectable<String> 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<String>(tabId)],
readsFrom: {tab},
).map((QueryRow row) => row.read<String>('prev_tab_id'));
}
Selectable<String> 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<String>(tabId)],
readsFrom: {tab},
).map((QueryRow row) => row.read<String>('prev_tab_id'));
}
Selectable<String> 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<String>(tabId)],
readsFrom: {tab},
).map((QueryRow row) => row.read<String>('next_tab_id'));
}
@override @override
Iterable<TableInfo<Table, Object?>> get allTables => Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>(); allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@@ -6,7 +6,7 @@ part of 'tab.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$tabDataRepositoryHash() => r'be1bb8a8118c7bfd90d0f1ac89a04c0a2149a0b0'; String _$tabDataRepositoryHash() => r'295c1718ecf41202709517fb2c5b31527e885dfa';
/// See also [TabDataRepository]. /// See also [TabDataRepository].
@ProviderFor(TabDataRepository) @ProviderFor(TabDataRepository)
@@ -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( Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final size = ref.watch( final size = ref.watch(
@@ -29,6 +29,8 @@ part 'general_settings.g.dart';
const _fallbackSearchProvider = 'wikipedia'; const _fallbackSearchProvider = 'wikipedia';
const _fallbackAutocompleteProvider = SearchSuggestionProviders.none; const _fallbackAutocompleteProvider = SearchSuggestionProviders.none;
enum TabBarSwipeAction { switchLastOpened, navigateOrderedTabs }
enum DeleteBrowsingDataType { enum DeleteBrowsingDataType {
tabs('Open tabs'), tabs('Open tabs'),
history('Browsing history'), history('Browsing history'),
@@ -59,6 +61,7 @@ class GeneralSettings with FastEquatable {
final TabType defaultIntentTabType; final TabType defaultIntentTabType;
final bool proxyPrivateTabsTor; final bool proxyPrivateTabsTor;
final bool autoHideTabBar; final bool autoHideTabBar;
final TabBarSwipeAction tabBarSwipeAction;
GeneralSettings({ GeneralSettings({
required this.themeMode, required this.themeMode,
@@ -74,6 +77,7 @@ class GeneralSettings with FastEquatable {
required this.defaultCreateTabType, required this.defaultCreateTabType,
required this.defaultIntentTabType, required this.defaultIntentTabType,
required this.autoHideTabBar, required this.autoHideTabBar,
required this.tabBarSwipeAction,
}); });
GeneralSettings.withDefaults({ GeneralSettings.withDefaults({
@@ -90,6 +94,7 @@ class GeneralSettings with FastEquatable {
TabType? defaultCreateTabType, TabType? defaultCreateTabType,
TabType? defaultIntentTabType, TabType? defaultIntentTabType,
bool? autoHideTabBar, bool? autoHideTabBar,
TabBarSwipeAction? tabBarSwipeAction,
}) : themeMode = themeMode ?? ThemeMode.dark, }) : themeMode = themeMode ?? ThemeMode.dark,
enableReadability = enableReadability ?? true, enableReadability = enableReadability ?? true,
enforceReadability = enforceReadability ?? false, enforceReadability = enforceReadability ?? false,
@@ -102,7 +107,9 @@ class GeneralSettings with FastEquatable {
enableLocalAiFeatures = enableLocalAiFeatures ?? true, enableLocalAiFeatures = enableLocalAiFeatures ?? true,
defaultCreateTabType = defaultCreateTabType ?? TabType.regular, defaultCreateTabType = defaultCreateTabType ?? TabType.regular,
defaultIntentTabType = defaultIntentTabType ?? TabType.regular, defaultIntentTabType = defaultIntentTabType ?? TabType.regular,
autoHideTabBar = autoHideTabBar ?? true; autoHideTabBar = autoHideTabBar ?? true,
tabBarSwipeAction =
tabBarSwipeAction ?? TabBarSwipeAction.switchLastOpened;
factory GeneralSettings.fromJson(Map<String, dynamic> json) => factory GeneralSettings.fromJson(Map<String, dynamic> json) =>
_$GeneralSettingsFromJson(json); _$GeneralSettingsFromJson(json);
@@ -124,5 +131,6 @@ class GeneralSettings with FastEquatable {
defaultIntentTabType, defaultIntentTabType,
proxyPrivateTabsTor, proxyPrivateTabsTor,
autoHideTabBar, autoHideTabBar,
tabBarSwipeAction,
]; ];
} }
@@ -37,6 +37,8 @@ abstract class _$GeneralSettingsCWProxy {
GeneralSettings autoHideTabBar(bool autoHideTabBar); 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. /// 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 /// Usage
@@ -57,6 +59,7 @@ abstract class _$GeneralSettingsCWProxy {
TabType defaultCreateTabType, TabType defaultCreateTabType,
TabType defaultIntentTabType, TabType defaultIntentTabType,
bool autoHideTabBar, bool autoHideTabBar,
TabBarSwipeAction tabBarSwipeAction,
}); });
} }
@@ -119,6 +122,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
GeneralSettings autoHideTabBar(bool autoHideTabBar) => GeneralSettings autoHideTabBar(bool autoHideTabBar) =>
this(autoHideTabBar: autoHideTabBar); this(autoHideTabBar: autoHideTabBar);
@override
GeneralSettings tabBarSwipeAction(TabBarSwipeAction tabBarSwipeAction) =>
this(tabBarSwipeAction: tabBarSwipeAction);
@override @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. /// 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? defaultCreateTabType = const $CopyWithPlaceholder(),
Object? defaultIntentTabType = const $CopyWithPlaceholder(), Object? defaultIntentTabType = const $CopyWithPlaceholder(),
Object? autoHideTabBar = const $CopyWithPlaceholder(), Object? autoHideTabBar = const $CopyWithPlaceholder(),
Object? tabBarSwipeAction = const $CopyWithPlaceholder(),
}) { }) {
return GeneralSettings( return GeneralSettings(
themeMode: themeMode == const $CopyWithPlaceholder() themeMode: themeMode == const $CopyWithPlaceholder()
@@ -200,6 +208,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
? _value.autoHideTabBar ? _value.autoHideTabBar
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: autoHideTabBar as bool, : autoHideTabBar as bool,
tabBarSwipeAction: tabBarSwipeAction == const $CopyWithPlaceholder()
? _value.tabBarSwipeAction
// ignore: cast_nullable_to_non_nullable
: tabBarSwipeAction as TabBarSwipeAction,
); );
} }
} }
@@ -241,10 +253,15 @@ GeneralSettings _$GeneralSettingsFromJson(Map<String, dynamic> json) =>
json['defaultIntentTabType'], json['defaultIntentTabType'],
), ),
autoHideTabBar: json['autoHideTabBar'] as bool?, autoHideTabBar: json['autoHideTabBar'] as bool?,
tabBarSwipeAction: $enumDecodeNullable(
_$TabBarSwipeActionEnumMap,
json['tabBarSwipeAction'],
),
); );
Map<String, dynamic> _$GeneralSettingsToJson(GeneralSettings instance) => Map<String, dynamic> _$GeneralSettingsToJson(
<String, dynamic>{ GeneralSettings instance,
) => <String, dynamic>{
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'enableReadability': instance.enableReadability, 'enableReadability': instance.enableReadability,
'enforceReadability': instance.enforceReadability, 'enforceReadability': instance.enforceReadability,
@@ -262,7 +279,8 @@ Map<String, dynamic> _$GeneralSettingsToJson(GeneralSettings instance) =>
'defaultIntentTabType': _$TabTypeEnumMap[instance.defaultIntentTabType]!, 'defaultIntentTabType': _$TabTypeEnumMap[instance.defaultIntentTabType]!,
'proxyPrivateTabsTor': instance.proxyPrivateTabsTor, 'proxyPrivateTabsTor': instance.proxyPrivateTabsTor,
'autoHideTabBar': instance.autoHideTabBar, 'autoHideTabBar': instance.autoHideTabBar,
}; 'tabBarSwipeAction': _$TabBarSwipeActionEnumMap[instance.tabBarSwipeAction]!,
};
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {
ThemeMode.system: 'system', ThemeMode.system: 'system',
@@ -292,3 +310,8 @@ const _$TabTypeEnumMap = {
TabType.private: 'private', TabType.private: 'private',
TabType.child: 'child', TabType.child: 'child',
}; };
const _$TabBarSwipeActionEnumMap = {
TabBarSwipeAction.switchLastOpened: 'switchLastOpened',
TabBarSwipeAction.navigateOrderedTabs: 'navigateOrderedTabs',
};
@@ -96,6 +96,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
DriftSqlType.bool, DriftSqlType.bool,
db.typeMapping, db.typeMapping,
), ),
'tabBarSwipeAction': settings['tabBarSwipeAction']?.readAs(
DriftSqlType.string,
db.typeMapping,
),
}); });
} }
@@ -27,7 +27,7 @@ final generalSettingsWithDefaultsProvider =
typedef GeneralSettingsWithDefaultsRef = typedef GeneralSettingsWithDefaultsRef =
AutoDisposeProviderRef<GeneralSettings>; AutoDisposeProviderRef<GeneralSettings>;
String _$generalSettingsRepositoryHash() => String _$generalSettingsRepositoryHash() =>
r'43f314d1c0d318d8c28afd38fee5f2416a26f423'; r'e311760a7773109a90c28275262883fcca348bbb';
/// See also [GeneralSettingsRepository]. /// See also [GeneralSettingsRepository].
@ProviderFor(GeneralSettingsRepository) @ProviderFor(GeneralSettingsRepository)