improve tab (and other lexorank entities) sorting, ordering and reordering

This commit is contained in:
Fabian Freund
2026-05-02 04:50:53 +02:00
parent 222b920a47
commit 69f21bb7d8
43 changed files with 4965 additions and 418 deletions
@@ -33,6 +33,7 @@ import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_list.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/isolation_context.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_source.dart';
@@ -68,14 +69,6 @@ class TabRepository extends _$TabRepository {
_tabFromIntent.remove(tabId);
}
NewTabPosition _newTabPositionForParent(String? parentId) {
if (parentId != null) {
return NewTabPosition.first;
}
return ref.read(generalSettingsWithDefaultsProvider).newTabPosition;
}
Future<String?> _resolveParentIdForContext({
required String? parentId,
required String? targetContextId,
@@ -132,7 +125,6 @@ class TabRepository extends _$TabRepository {
final effectiveContextId = tabMode is IsolatedTabMode
? effectiveIsolationContextId
: assignedContainer?.metadata.contextualIdentity;
final newTabPosition = _newTabPositionForParent(validatedParentId);
final newTabId = await tabDao.upsertTabTransactional(
() {
@@ -150,7 +142,6 @@ class TabRepository extends _$TabRepository {
);
},
parentId: Value(validatedParentId),
newTabPosition: newTabPosition,
containerId: Value(assignedContainer?.id),
url: Value(url),
tabMode: Value(tabMode),
@@ -215,7 +206,6 @@ class TabRepository extends _$TabRepository {
tabId,
parentId: Value(validatedParentId),
source: TabSource.manual,
newTabPosition: _newTabPositionForParent(validatedParentId),
containerId: Value(assignedContainer?.id),
url: Value(Uri.tryParse(tab.url)),
tabMode: Value(
@@ -257,6 +247,22 @@ class TabRepository extends _$TabRepository {
? duplicateIsolationContextId
: containerData?.metadata.contextualIdentity;
// Place the duplicate as a sibling of the source — same parent — and
// insert it right after the source's full subtree, so existing
// children of the source are not split from their parent.
final sourceData = await tabDao
.getTabDataById(selectTabId)
.getSingleOrNull();
final sourceParentId = sourceData?.parentId;
final anchorTabId =
await tabDao
.lastSubtreeTabIdByOrderKey(
selectTabId,
containerId: containerData?.id,
)
.getSingleOrNull() ??
selectTabId;
return await tabDao.upsertTabTransactional(
() {
return _tabsService.duplicateTab(
@@ -265,8 +271,8 @@ class TabRepository extends _$TabRepository {
selectNewTab: selectTab,
);
},
parentId: const Value.absent(),
newTabPosition: _newTabPositionForParent(null),
parentId: Value(sourceParentId),
afterTabId: Value(anchorTabId),
containerId: Value(containerData?.id),
tabMode: Value(duplicateTabMode),
);
@@ -319,15 +325,12 @@ class TabRepository extends _$TabRepository {
String? containerId,
bool skipContainerCheck = true,
}) async {
final previousTabId = await ref
.read(tabDatabaseProvider)
.definitionsDrift
.previousTabByOrderKey(
tabId: tabId,
containerId: containerId,
skipContainerCheck: skipContainerCheck,
)
.getSingleOrNull();
final previousTabId = await _adjacentVisibleTabByOrder(
tabId,
containerId: containerId,
skipContainerCheck: skipContainerCheck,
selectPrevious: true,
);
if (ref.mounted && previousTabId != null) {
return selectTab(previousTabId);
@@ -341,15 +344,12 @@ class TabRepository extends _$TabRepository {
String? containerId,
bool skipContainerCheck = true,
}) async {
final previousTabId = await ref
.read(tabDatabaseProvider)
.definitionsDrift
.nextTabByOrderKey(
tabId: tabId,
containerId: containerId,
skipContainerCheck: skipContainerCheck,
)
.getSingleOrNull();
final previousTabId = await _adjacentVisibleTabByOrder(
tabId,
containerId: containerId,
skipContainerCheck: skipContainerCheck,
selectPrevious: false,
);
if (ref.mounted && previousTabId != null) {
return selectTab(previousTabId);
@@ -383,7 +383,80 @@ class TabRepository extends _$TabRepository {
return true;
}
Future<void> _selectNextTab(String tabId) async {
Future<String?> _adjacentVisibleTabByOrder(
String tabId, {
required String? containerId,
required bool skipContainerCheck,
required bool selectPrevious,
}) {
// "Previous/next" here is interpreted relative to the *tab bar*
// direction, even when the user triggered the navigation from the tab
// tray (which has its own `tabListDirection`). If the two settings
// disagree, "next tab" while looking at the tray flows by tab-bar
// direction. Treat as intentional — keyboard / gesture navigation is
// anchored to the bar's mental model.
final newestFirst =
ref.read(generalSettingsWithDefaultsProvider).tabBarDirection ==
TabBarDirection.newestFirst;
final TabDatabase tabDatabase = ref.read(tabDatabaseProvider);
final definitions = tabDatabase.definitionsDrift;
if (newestFirst == selectPrevious) {
return definitions
.nextTabByOrderKey(
tabId: tabId,
containerId: containerId,
skipContainerCheck: skipContainerCheck,
)
.getSingleOrNull();
}
return definitions
.previousTabByOrderKey(
tabId: tabId,
containerId: containerId,
skipContainerCheck: skipContainerCheck,
)
.getSingleOrNull();
}
Future<String?> _nearestAvailableVisibleTabByOrder(
String tabId, {
required String? containerId,
required Set<String> excludedTabIds,
}) async {
Future<String?> walkDirection({required bool selectPrevious}) async {
var candidate = await _adjacentVisibleTabByOrder(
tabId,
containerId: containerId,
skipContainerCheck: false,
selectPrevious: selectPrevious,
);
while (candidate != null) {
if (!excludedTabIds.contains(candidate)) {
return candidate;
}
candidate = await _adjacentVisibleTabByOrder(
candidate,
containerId: containerId,
skipContainerCheck: false,
selectPrevious: selectPrevious,
);
}
return null;
}
return await walkDirection(selectPrevious: true) ??
await walkDirection(selectPrevious: false);
}
Future<void> _selectNextTab(
String tabId, {
Set<String> excludedTabIds = const {},
}) async {
final tabState = ref.read(tabStatesProvider)[tabId];
final currentContainerId = await ref
@@ -395,13 +468,20 @@ class TabRepository extends _$TabRepository {
final sameContainerTabs = await ref
.read(containerRepositoryProvider.notifier)
.getContainerTabIds(currentContainerId)
.then((tabs) => tabs.where((tab) => tab != tabId).toList());
.then(
(tabs) => tabs
.where((tab) => tab != tabId && !excludedTabIds.contains(tab))
.toList(),
);
if (!ref.mounted) return;
// Priority 1: Check for parent tab first
if (tabState?.parentId != null) {
return _tabsService.selectTab(tabId: tabState!.parentId!);
final parentId = tabState!.parentId!;
if (!excludedTabIds.contains(parentId)) {
return _tabsService.selectTab(tabId: parentId);
}
}
// Priority 2: Check for previous tab by timestamp
@@ -419,34 +499,14 @@ class TabRepository extends _$TabRepository {
if (!ref.mounted) return;
final previousOrderedTabId = await ref
.read(tabDatabaseProvider)
.definitionsDrift
.previousTabByOrderKey(
tabId: tabId,
containerId: currentContainerId,
skipContainerCheck: false,
)
.getSingleOrNull();
final orderedNeighborTabId = await _nearestAvailableVisibleTabByOrder(
tabId,
containerId: currentContainerId,
excludedTabIds: excludedTabIds,
);
if (previousOrderedTabId != null) {
return _tabsService.selectTab(tabId: previousOrderedTabId);
}
if (!ref.mounted) return;
final nextOrderedTabId = await ref
.read(tabDatabaseProvider)
.definitionsDrift
.nextTabByOrderKey(
tabId: tabId,
containerId: currentContainerId,
skipContainerCheck: false,
)
.getSingleOrNull();
if (nextOrderedTabId != null) {
return _tabsService.selectTab(tabId: nextOrderedTabId);
if (orderedNeighborTabId != null) {
return _tabsService.selectTab(tabId: orderedNeighborTabId);
}
if (!ref.mounted) return;
@@ -454,7 +514,11 @@ class TabRepository extends _$TabRepository {
final unassignedTabs = await ref
.read(containerRepositoryProvider.notifier)
.getContainerTabIds(null)
.then((tabs) => tabs.where((tab) => tab != tabId).toList());
.then(
(tabs) => tabs
.where((tab) => tab != tabId && !excludedTabIds.contains(tab))
.toList(),
);
if (unassignedTabs.isNotEmpty) {
return _tabsService.selectTab(tabId: unassignedTabs.first);
@@ -474,7 +538,11 @@ class TabRepository extends _$TabRepository {
(container) => ref
.read(containerRepositoryProvider.notifier)
.getContainerTabIds(container.id)
.then((tabs) => tabs.where((tab) => tab != tabId).toList()),
.then(
(tabs) => tabs
.where((tab) => tab != tabId && !excludedTabIds.contains(tab))
.toList(),
),
);
if (nextContainerTabs.isNotEmpty) {
@@ -493,6 +561,8 @@ class TabRepository extends _$TabRepository {
await _selectNextTab(tabId);
}
await _preservePromotedChildOrderOnClose([tabId]);
await _tabsService.removeTab(tabId: tabId);
// Queue isolation cleanup — actual cleanup runs after syncTabs
@@ -517,13 +587,22 @@ class TabRepository extends _$TabRepository {
final selectedTab = ref.read(selectedTabProvider);
if (selectedTab.mapNotNull(tabIds.contains) ?? false) {
await _selectNextTab(selectedTab!);
await _selectNextTab(selectedTab!, excludedTabIds: tabIds.toSet());
}
await _preservePromotedChildOrderOnClose(tabIds);
await _tabsService.removeTabs(ids: tabIds);
});
}
Future<void> _preservePromotedChildOrderOnClose(List<String> tabIds) {
return ref
.read(tabDatabaseProvider)
.tabDao
.preservePromotedChildOrderOnClose(tabIds);
}
/// Clears Gecko browsing data and removes proxy alias for an isolation
/// context if no more tabs share it.
Future<void> _cleanupIsolationContextIfEmpty(String contextId) async {
@@ -638,7 +717,6 @@ class TabRepository extends _$TabRepository {
tabId,
parentId: const Value.absent(),
source: TabSource.addedEvent,
newTabPosition: _newTabPositionForParent(null),
containerId: Value(containerId),
);
},
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
}
}
String _$tabRepositoryHash() => r'ce4ac2f2efbb859ba66e98c6e6d21f8205d9b4ed';
String _$tabRepositoryHash() => r'4c5bda4d0ddcc66cfa420d3c2ca97db42b73b878';
abstract class _$TabRepository extends $Notifier<void> {
void build();