add setting to control tab insertion position

This commit is contained in:
Fabian Freund
2026-03-13 05:07:48 +01:00
parent 8dc7151c45
commit 8a07d7df1a
10 changed files with 132 additions and 10 deletions
@@ -57,7 +57,7 @@ final class TabSessionProvider extends $NotifierProvider<TabSession, void> {
} }
} }
String _$tabSessionHash() => r'be44598ad023c2fb23e48d3fa4bf805c4640c599'; String _$tabSessionHash() => r'636f0940425d689ff98fabfc61687de828a8eeb9';
final class TabSessionFamily extends $Family final class TabSessionFamily extends $Family
with $ClassFamilyOverride<TabSession, void, void, void, String?> { with $ClassFamilyOverride<TabSession, void, void, void, String?> {
@@ -42,6 +42,8 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selec
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:weblibre/features/tor/domain/repositories/tor_proxy.dart'; import 'package:weblibre/features/tor/domain/repositories/tor_proxy.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/utils/debouncer.dart'; import 'package:weblibre/utils/debouncer.dart';
part 'tab.g.dart'; part 'tab.g.dart';
@@ -66,6 +68,14 @@ class TabRepository extends _$TabRepository {
_tabFromIntent.remove(tabId); _tabFromIntent.remove(tabId);
} }
NewTabPosition _newTabPositionForParent(String? parentId) {
if (parentId != null) {
return NewTabPosition.first;
}
return ref.read(generalSettingsWithDefaultsProvider).newTabPosition;
}
Future<String?> _resolveParentIdForContext({ Future<String?> _resolveParentIdForContext({
required String? parentId, required String? parentId,
required String? targetContextId, required String? targetContextId,
@@ -122,6 +132,7 @@ class TabRepository extends _$TabRepository {
final effectiveContextId = tabMode is IsolatedTabMode final effectiveContextId = tabMode is IsolatedTabMode
? effectiveIsolationContextId ? effectiveIsolationContextId
: assignedContainer?.metadata.contextualIdentity; : assignedContainer?.metadata.contextualIdentity;
final newTabPosition = _newTabPositionForParent(validatedParentId);
final newTabId = await tabDao.upsertTabTransactional( final newTabId = await tabDao.upsertTabTransactional(
() { () {
@@ -139,6 +150,7 @@ class TabRepository extends _$TabRepository {
); );
}, },
parentId: Value(validatedParentId), parentId: Value(validatedParentId),
newTabPosition: newTabPosition,
containerId: Value(assignedContainer?.id), containerId: Value(assignedContainer?.id),
url: Value(url), url: Value(url),
tabMode: Value(tabMode), tabMode: Value(tabMode),
@@ -171,7 +183,6 @@ class TabRepository extends _$TabRepository {
tabs: tabs, tabs: tabs,
selectTabId: selectTabId, selectTabId: selectTabId,
); );
// Build sets for validation // Build sets for validation
final creatingTabIds = createdTabIds.toSet(); final creatingTabIds = createdTabIds.toSet();
final parentIdsToValidate = tabs final parentIdsToValidate = tabs
@@ -204,6 +215,7 @@ class TabRepository extends _$TabRepository {
tabId, tabId,
parentId: Value(validatedParentId), parentId: Value(validatedParentId),
source: TabSource.manual, source: TabSource.manual,
newTabPosition: _newTabPositionForParent(validatedParentId),
containerId: Value(assignedContainer?.id), containerId: Value(assignedContainer?.id),
url: Value(Uri.tryParse(tab.url)), url: Value(Uri.tryParse(tab.url)),
tabMode: Value( tabMode: Value(
@@ -254,6 +266,7 @@ class TabRepository extends _$TabRepository {
); );
}, },
parentId: const Value.absent(), parentId: const Value.absent(),
newTabPosition: _newTabPositionForParent(null),
containerId: Value(containerData?.id), containerId: Value(containerData?.id),
tabMode: Value(duplicateTabMode), tabMode: Value(duplicateTabMode),
); );
@@ -597,6 +610,7 @@ class TabRepository extends _$TabRepository {
tabId, tabId,
parentId: const Value.absent(), parentId: const Value.absent(),
source: TabSource.addedEvent, source: TabSource.addedEvent,
newTabPosition: _newTabPositionForParent(null),
containerId: Value(containerId), containerId: Value(containerId),
); );
}, },
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
} }
} }
String _$tabRepositoryHash() => r'1c866a1f5b1872ba07ebac82ff92455a7af316a3'; String _$tabRepositoryHash() => r'4dd31901b4d02ca075d3dcec3822f5a2832d164c';
abstract class _$TabRepository extends $Notifier<void> { abstract class _$TabRepository extends $Notifier<void> {
void build(); void build();
@@ -601,7 +601,7 @@ final class QuickTabSwitcherTabStatesProvider
} }
String _$quickTabSwitcherTabStatesHash() => String _$quickTabSwitcherTabStatesHash() =>
r'2627bcd25718472f0c8057e836d76bdaa7317c1c'; r'd83ea34d366238681baaad0b36699ee0742edc3d';
final class QuickTabSwitcherTabStatesFamily extends $Family final class QuickTabSwitcherTabStatesFamily extends $Family
with with
@@ -30,6 +30,7 @@ import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_source.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_source.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/tab_query_result.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/models/tab_query_result.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
class SyncTabsResult { class SyncTabsResult {
final Set<String> deletedIsolationContextIds; final Set<String> deletedIsolationContextIds;
@@ -139,6 +140,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
Future<String> _generateOrderKey({ Future<String> _generateOrderKey({
required Value<String?> parentId, required Value<String?> parentId,
required Value<String?> containerId, required Value<String?> containerId,
required NewTabPosition newTabPosition,
}) async { }) async {
if (parentId.value.isNotEmpty) { if (parentId.value.isNotEmpty) {
return await db.containerDao return await db.containerDao
@@ -148,15 +150,23 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
.generateLeadingOrderKey(containerId.value) .generateLeadingOrderKey(containerId.value)
.getSingle(); .getSingle();
} else { } else {
return db.containerDao return switch (newTabPosition) {
.generateLeadingOrderKey(containerId.value) NewTabPosition.first =>
.getSingle(); db.containerDao
.generateLeadingOrderKey(containerId.value)
.getSingle(),
NewTabPosition.end =>
db.containerDao
.generateTrailingOrderKey(containerId.value)
.getSingle(),
};
} }
} }
Future<String> upsertTabTransactional( Future<String> upsertTabTransactional(
Future<String> Function() createTab, { Future<String> Function() createTab, {
required Value<String?> parentId, required Value<String?> parentId,
NewTabPosition newTabPosition = NewTabPosition.first,
Value<String?> containerId = const Value.absent(), Value<String?> containerId = const Value.absent(),
Value<String?> orderKey = const Value.absent(), Value<String?> orderKey = const Value.absent(),
Value<Uri?> url = const Value.absent(), Value<Uri?> url = const Value.absent(),
@@ -167,7 +177,11 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
final tabId = await createTab(); final tabId = await createTab();
final currentOrderKey = final currentOrderKey =
orderKey.value ?? orderKey.value ??
await _generateOrderKey(parentId: parentId, containerId: containerId); await _generateOrderKey(
parentId: parentId,
containerId: containerId,
newTabPosition: newTabPosition,
);
final Value<TabModeDbValue> persistedTabMode = tabMode.present final Value<TabModeDbValue> persistedTabMode = tabMode.present
? Value(tabMode.value.toDbValue()) ? Value(tabMode.value.toDbValue())
: const Value.absent(); : const Value.absent();
@@ -211,6 +225,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
String tabId, { String tabId, {
required TabSource source, required TabSource source,
required Value<String?> parentId, required Value<String?> parentId,
NewTabPosition newTabPosition = NewTabPosition.first,
Value<String?> containerId = const Value.absent(), Value<String?> containerId = const Value.absent(),
Value<String?> orderKey = const Value.absent(), Value<String?> orderKey = const Value.absent(),
Value<Uri?> url = const Value.absent(), Value<Uri?> url = const Value.absent(),
@@ -220,7 +235,11 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
return db.transaction(() async { return db.transaction(() async {
final currentOrderKey = final currentOrderKey =
orderKey.value ?? orderKey.value ??
await _generateOrderKey(parentId: parentId, containerId: containerId); await _generateOrderKey(
parentId: parentId,
containerId: containerId,
newTabPosition: newTabPosition,
);
final Value<TabModeDbValue> persistedTabMode = tabMode.present final Value<TabModeDbValue> persistedTabMode = tabMode.present
? Value(tabMode.value.toDbValue()) ? Value(tabMode.value.toDbValue())
: const Value.absent(); : const Value.absent();
@@ -68,6 +68,7 @@ class _TabCreationSection extends StatelessWidget {
children: [ children: [
SettingSection(name: 'Tab Creation'), SettingSection(name: 'Tab Creation'),
_NewTabDefaultSection(), _NewTabDefaultSection(),
_NewTabPositionSection(),
_ExternalLinkHandlingSection(), _ExternalLinkHandlingSection(),
_AppLinksModeSection(), _AppLinksModeSection(),
], ],
@@ -255,6 +256,59 @@ class _ExternalLinkHandlingSection extends HookConsumerWidget {
} }
} }
class _NewTabPositionSection extends HookConsumerWidget {
const _NewTabPositionSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final newTabPosition = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.newTabPosition),
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('New Tab Position'),
subtitle: Text('Choose where newly created tabs appear by default'),
leading: Icon(MdiIcons.reorderHorizontal),
contentPadding: EdgeInsets.zero,
),
Center(
child: SegmentedButton(
showSelectedIcon: false,
segments: const [
ButtonSegment(
value: NewTabPosition.first,
label: Text('First'),
icon: Icon(MdiIcons.arrowCollapseLeft),
),
ButtonSegment(
value: NewTabPosition.end,
label: Text('End'),
icon: Icon(MdiIcons.arrowCollapseRight),
),
],
selected: {newTabPosition},
onSelectionChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.newTabPosition(value.first),
);
},
),
),
],
),
);
}
}
class _CreateChildTabsTile extends HookConsumerWidget { class _CreateChildTabsTile extends HookConsumerWidget {
const _CreateChildTabsTile(); const _CreateChildTabsTile();
@@ -45,6 +45,8 @@ enum QuickTabSwitcherMode { lastUsedTabs, containerTabs }
enum TabIntentOpenSetting { regular, private, ask } enum TabIntentOpenSetting { regular, private, ask }
enum NewTabPosition { first, end }
enum TabBarPosition { top, bottom } enum TabBarPosition { top, bottom }
enum TabBarLayout { withTitle, compact } enum TabBarLayout { withTitle, compact }
@@ -82,6 +84,7 @@ class GeneralSettings with FastEquatable {
final bool showIsolatedTabUi; final bool showIsolatedTabUi;
@JsonKey(name: 'defaultCreateTabType') @JsonKey(name: 'defaultCreateTabType')
final TabType storedDefaultCreateTabType; final TabType storedDefaultCreateTabType;
final NewTabPosition newTabPosition;
final TabIntentOpenSetting tabIntentOpenSetting; final TabIntentOpenSetting tabIntentOpenSetting;
final bool autoHideTabBar; final bool autoHideTabBar;
final TabBarSwipeAction tabBarSwipeAction; final TabBarSwipeAction tabBarSwipeAction;
@@ -129,6 +132,7 @@ class GeneralSettings with FastEquatable {
required this.showContainerUi, required this.showContainerUi,
required this.showIsolatedTabUi, required this.showIsolatedTabUi,
required this.storedDefaultCreateTabType, required this.storedDefaultCreateTabType,
required this.newTabPosition,
required this.tabIntentOpenSetting, required this.tabIntentOpenSetting,
required this.autoHideTabBar, required this.autoHideTabBar,
required this.tabBarSwipeAction, required this.tabBarSwipeAction,
@@ -177,6 +181,7 @@ class GeneralSettings with FastEquatable {
bool? showContainerUi, bool? showContainerUi,
bool? showIsolatedTabUi, bool? showIsolatedTabUi,
TabType? storedDefaultCreateTabType, TabType? storedDefaultCreateTabType,
NewTabPosition? newTabPosition,
TabIntentOpenSetting? tabIntentOpenSetting, TabIntentOpenSetting? tabIntentOpenSetting,
bool? autoHideTabBar, bool? autoHideTabBar,
TabBarSwipeAction? tabBarSwipeAction, TabBarSwipeAction? tabBarSwipeAction,
@@ -223,6 +228,7 @@ class GeneralSettings with FastEquatable {
showIsolatedTabUi = showIsolatedTabUi ?? true, showIsolatedTabUi = showIsolatedTabUi ?? true,
storedDefaultCreateTabType = storedDefaultCreateTabType =
storedDefaultCreateTabType ?? TabType.regular, storedDefaultCreateTabType ?? TabType.regular,
newTabPosition = newTabPosition ?? NewTabPosition.first,
tabIntentOpenSetting = tabIntentOpenSetting ?? TabIntentOpenSetting.ask, tabIntentOpenSetting = tabIntentOpenSetting ?? TabIntentOpenSetting.ask,
autoHideTabBar = autoHideTabBar ?? true, autoHideTabBar = autoHideTabBar ?? true,
tabBarSwipeAction = tabBarSwipeAction =
@@ -300,6 +306,7 @@ class GeneralSettings with FastEquatable {
showContainerUi, showContainerUi,
showIsolatedTabUi, showIsolatedTabUi,
storedDefaultCreateTabType, storedDefaultCreateTabType,
newTabPosition,
tabIntentOpenSetting, tabIntentOpenSetting,
autoHideTabBar, autoHideTabBar,
tabBarSwipeAction, tabBarSwipeAction,
@@ -41,6 +41,8 @@ abstract class _$GeneralSettingsCWProxy {
TabType storedDefaultCreateTabType, TabType storedDefaultCreateTabType,
); );
GeneralSettings newTabPosition(NewTabPosition newTabPosition);
GeneralSettings tabIntentOpenSetting( GeneralSettings tabIntentOpenSetting(
TabIntentOpenSetting tabIntentOpenSetting, TabIntentOpenSetting tabIntentOpenSetting,
); );
@@ -137,6 +139,7 @@ abstract class _$GeneralSettingsCWProxy {
bool showContainerUi, bool showContainerUi,
bool showIsolatedTabUi, bool showIsolatedTabUi,
TabType storedDefaultCreateTabType, TabType storedDefaultCreateTabType,
NewTabPosition newTabPosition,
TabIntentOpenSetting tabIntentOpenSetting, TabIntentOpenSetting tabIntentOpenSetting,
bool autoHideTabBar, bool autoHideTabBar,
TabBarSwipeAction tabBarSwipeAction, TabBarSwipeAction tabBarSwipeAction,
@@ -236,6 +239,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
TabType storedDefaultCreateTabType, TabType storedDefaultCreateTabType,
) => call(storedDefaultCreateTabType: storedDefaultCreateTabType); ) => call(storedDefaultCreateTabType: storedDefaultCreateTabType);
@override
GeneralSettings newTabPosition(NewTabPosition newTabPosition) =>
call(newTabPosition: newTabPosition);
@override @override
GeneralSettings tabIntentOpenSetting( GeneralSettings tabIntentOpenSetting(
TabIntentOpenSetting tabIntentOpenSetting, TabIntentOpenSetting tabIntentOpenSetting,
@@ -393,6 +400,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
Object? showContainerUi = const $CopyWithPlaceholder(), Object? showContainerUi = const $CopyWithPlaceholder(),
Object? showIsolatedTabUi = const $CopyWithPlaceholder(), Object? showIsolatedTabUi = const $CopyWithPlaceholder(),
Object? storedDefaultCreateTabType = const $CopyWithPlaceholder(), Object? storedDefaultCreateTabType = const $CopyWithPlaceholder(),
Object? newTabPosition = const $CopyWithPlaceholder(),
Object? tabIntentOpenSetting = const $CopyWithPlaceholder(), Object? tabIntentOpenSetting = const $CopyWithPlaceholder(),
Object? autoHideTabBar = const $CopyWithPlaceholder(), Object? autoHideTabBar = const $CopyWithPlaceholder(),
Object? tabBarSwipeAction = const $CopyWithPlaceholder(), Object? tabBarSwipeAction = const $CopyWithPlaceholder(),
@@ -506,6 +514,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
? _value.storedDefaultCreateTabType ? _value.storedDefaultCreateTabType
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: storedDefaultCreateTabType as TabType, : storedDefaultCreateTabType as TabType,
newTabPosition:
newTabPosition == const $CopyWithPlaceholder() ||
newTabPosition == null
? _value.newTabPosition
// ignore: cast_nullable_to_non_nullable
: newTabPosition as NewTabPosition,
tabIntentOpenSetting: tabIntentOpenSetting:
tabIntentOpenSetting == const $CopyWithPlaceholder() || tabIntentOpenSetting == const $CopyWithPlaceholder() ||
tabIntentOpenSetting == null tabIntentOpenSetting == null
@@ -733,6 +747,10 @@ GeneralSettings _$GeneralSettingsFromJson(
_$TabTypeEnumMap, _$TabTypeEnumMap,
json['defaultCreateTabType'], json['defaultCreateTabType'],
), ),
newTabPosition: $enumDecodeNullable(
_$NewTabPositionEnumMap,
json['newTabPosition'],
),
tabIntentOpenSetting: $enumDecodeNullable( tabIntentOpenSetting: $enumDecodeNullable(
_$TabIntentOpenSettingEnumMap, _$TabIntentOpenSettingEnumMap,
json['tabIntentOpenSetting'], json['tabIntentOpenSetting'],
@@ -818,6 +836,7 @@ Map<String, dynamic> _$GeneralSettingsToJson(
'showIsolatedTabUi': instance.showIsolatedTabUi, 'showIsolatedTabUi': instance.showIsolatedTabUi,
'defaultCreateTabType': 'defaultCreateTabType':
_$TabTypeEnumMap[instance.storedDefaultCreateTabType]!, _$TabTypeEnumMap[instance.storedDefaultCreateTabType]!,
'newTabPosition': _$NewTabPositionEnumMap[instance.newTabPosition]!,
'tabIntentOpenSetting': 'tabIntentOpenSetting':
_$TabIntentOpenSettingEnumMap[instance.tabIntentOpenSetting]!, _$TabIntentOpenSettingEnumMap[instance.tabIntentOpenSetting]!,
'autoHideTabBar': instance.autoHideTabBar, 'autoHideTabBar': instance.autoHideTabBar,
@@ -885,6 +904,11 @@ const _$TabTypeEnumMap = {
TabType.isolated: 'isolated', TabType.isolated: 'isolated',
}; };
const _$NewTabPositionEnumMap = {
NewTabPosition.first: 'first',
NewTabPosition.end: 'end',
};
const _$TabIntentOpenSettingEnumMap = { const _$TabIntentOpenSettingEnumMap = {
TabIntentOpenSetting.regular: 'regular', TabIntentOpenSetting.regular: 'regular',
TabIntentOpenSetting.private: 'private', TabIntentOpenSetting.private: 'private',
@@ -100,6 +100,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
DriftSqlType.string, DriftSqlType.string,
db.typeMapping, db.typeMapping,
), ),
'newTabPosition': settings['newTabPosition']?.readAs(
DriftSqlType.string,
db.typeMapping,
),
'tabIntentOpenSetting': settings['tabIntentOpenSetting']?.readAs( 'tabIntentOpenSetting': settings['tabIntentOpenSetting']?.readAs(
DriftSqlType.string, DriftSqlType.string,
db.typeMapping, db.typeMapping,
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
} }
String _$generalSettingsRepositoryHash() => String _$generalSettingsRepositoryHash() =>
r'44aaf9f3c8c6e1a8c6569ed59b76fa0360528578'; r'04764c3b9a71aec077ab2b7894d117f993678b2e';
abstract class _$GeneralSettingsRepository abstract class _$GeneralSettingsRepository
extends $StreamNotifier<GeneralSettings> { extends $StreamNotifier<GeneralSettings> {