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() =>
r'b1deac5e0566443b4729656c7082903b529bc269';
r'c89d981c2ba253ed940887fa59c11ad93d8589a8';
/// See also [selectionActionService].
@ProviderFor(selectionActionService)
@@ -48,7 +48,6 @@ part 'tab.g.dart';
class TabRepository extends _$TabRepository {
final _tabsService = GeckoTabService();
String? _previousTabId;
final _tabFromIntent = <String>{};
bool hasLaunchedFromIntent(String? tabId) {
@@ -124,9 +123,40 @@ class TabRepository extends _$TabRepository {
);
}
Future<bool> selectPreviousTab() async {
if (_previousTabId != null) {
return selectTab(_previousTabId!);
Future<bool> selectPreviouslyOpenedTab(String tabId) async {
final previousTabId = await ref
.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;
@@ -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);
}
@@ -6,7 +6,7 @@ part of 'tab.dart';
// RiverpodGenerator
// **************************************************************************
String _$tabRepositoryHash() => r'3465f351f1d36e0f21b4e8ef4ebe7fdc4fbc0b75';
String _$tabRepositoryHash() => r'4f562c1456eadcda7986ea2048667dd083f37618';
/// See also [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/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(
@@ -240,4 +240,34 @@ unorderedTabDescendants:
JOIN descendants d ON t.parent_id = d.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
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@@ -6,7 +6,7 @@ part of 'tab.dart';
// RiverpodGenerator
// **************************************************************************
String _$tabDataRepositoryHash() => r'be1bb8a8118c7bfd90d0f1ac89a04c0a2149a0b0';
String _$tabDataRepositoryHash() => r'295c1718ecf41202709517fb2c5b31527e885dfa';
/// See also [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(
builder: (context, ref, child) {
final size = ref.watch(
@@ -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<String, dynamic> json) =>
_$GeneralSettingsFromJson(json);
@@ -124,5 +131,6 @@ class GeneralSettings with FastEquatable {
defaultIntentTabType,
proxyPrivateTabsTor,
autoHideTabBar,
tabBarSwipeAction,
];
}
@@ -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<String, dynamic> json) =>
json['defaultIntentTabType'],
),
autoHideTabBar: json['autoHideTabBar'] as bool?,
tabBarSwipeAction: $enumDecodeNullable(
_$TabBarSwipeActionEnumMap,
json['tabBarSwipeAction'],
),
);
Map<String, dynamic> _$GeneralSettingsToJson(GeneralSettings instance) =>
<String, dynamic>{
'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<String, dynamic> _$GeneralSettingsToJson(
GeneralSettings instance,
) => <String, dynamic>{
'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',
};
@@ -96,6 +96,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
DriftSqlType.bool,
db.typeMapping,
),
'tabBarSwipeAction': settings['tabBarSwipeAction']?.readAs(
DriftSqlType.string,
db.typeMapping,
),
});
}
@@ -27,7 +27,7 @@ final generalSettingsWithDefaultsProvider =
typedef GeneralSettingsWithDefaultsRef =
AutoDisposeProviderRef<GeneralSettings>;
String _$generalSettingsRepositoryHash() =>
r'43f314d1c0d318d8c28afd38fee5f2416a26f423';
r'e311760a7773109a90c28275262883fcca348bbb';
/// See also [GeneralSettingsRepository].
@ProviderFor(GeneralSettingsRepository)