respect pinned tabs and direction when sorting

This commit is contained in:
Fabian Freund
2026-05-04 18:15:31 +02:00
parent d503f032ab
commit 8220fad626
12 changed files with 206 additions and 80 deletions
@@ -401,7 +401,7 @@ class TabRepository extends _$TabRepository {
// anchored to the bar's mental model.
final newestFirst =
ref.read(generalSettingsWithDefaultsProvider).tabBarDirection ==
TabBarDirection.newestFirst;
TabDirection.newestFirst;
final TabDatabase tabDatabase = ref.read(tabDatabaseProvider);
final definitions = tabDatabase.definitionsDrift;
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
}
}
String _$tabRepositoryHash() => r'84ecfed72f17c367fc7bf0100f6367d4e7609596';
String _$tabRepositoryHash() => r'a68941d373f348201e3d8b1cab26343acca3852f';
abstract class _$TabRepository extends $Notifier<void> {
void build();
@@ -227,11 +227,74 @@ selectedContainerTabStatesWithContainer(Ref ref) {
.watch(groupedTabListItemsProvider(containerId: filter.containerId))
.value;
final tabListDirection = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabListDirection),
);
final tabBarDirection = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabBarDirection),
);
final pinnedTabIds = ref.watch(
watchPinnedTabIdsProvider.select(
(value) => value.value ?? const <String>{},
),
);
final sortPinnedFirst = ref.watch(
tabViewFilterControllerProvider.select((v) => v.sortPinnedFirst),
);
// Build an order map from groupedItems. When the tab bar direction differs
// from the tab list direction, reverse root group order within each
// pinned/unpinned partition so the bar respects the user's direction
// preference while keeping parentchild groups intact.
List<TabListItemEntity> orderedItems = groupedItems;
if (tabBarDirection != tabListDirection) {
// Split groupedItems into root groups (each: root + its children).
final rootGroups = <(bool isPinned, List<TabListItemEntity>)>[];
var currentGroup = <TabListItemEntity>[];
for (final item in groupedItems) {
final isRoot =
item is TabListStandaloneItem || item is TabListParentGroup;
if (isRoot && currentGroup.isNotEmpty) {
final isPinned =
sortPinnedFirst && pinnedTabIds.contains(currentGroup.first.tabId);
rootGroups.add((isPinned, currentGroup));
currentGroup = [];
}
currentGroup.add(item);
}
if (currentGroup.isNotEmpty) {
final isPinned =
sortPinnedFirst && pinnedTabIds.contains(currentGroup.first.tabId);
rootGroups.add((isPinned, currentGroup));
}
// Reverse root groups within each partition (pinned / unpinned).
final result = <TabListItemEntity>[];
if (sortPinnedFirst) {
final pinned = rootGroups
.where((g) => g.$1)
.toList()
.reversed
.expand((g) => g.$2);
final unpinned = rootGroups
.where((g) => !g.$1)
.toList()
.reversed
.expand((g) => g.$2);
result
..addAll(pinned)
..addAll(unpinned);
} else {
result.addAll(rootGroups.reversed.expand((g) => g.$2));
}
orderedItems = result;
}
final groupedOrder = {
for (var i = 0; i < groupedItems.length; i++) groupedItems[i].tabId: i,
for (var i = 0; i < orderedItems.length; i++) orderedItems[i].tabId: i,
};
final items = [
var items = [
for (final tabEntity in sortedTabs)
if (tabStates.containsKey(tabEntity.tabId))
(
@@ -243,11 +306,21 @@ selectedContainerTabStatesWithContainer(Ref ref) {
];
items.sort((a, b) {
final aIndex = groupedOrder[a.$1.id] ?? groupedItems.length;
final bIndex = groupedOrder[b.$1.id] ?? groupedItems.length;
final aIndex = groupedOrder[a.$1.id] ?? orderedItems.length;
final bIndex = groupedOrder[b.$1.id] ?? orderedItems.length;
return aIndex.compareTo(bIndex);
});
// Flat pinned-first: move all pinned tabs before unpinned regardless of
// hierarchy, preserving relative order within each partition.
if (sortPinnedFirst && pinnedTabIds.isNotEmpty) {
final pinned = items.where((i) => pinnedTabIds.contains(i.$1.id)).toList();
final unpinned = items
.where((i) => !pinnedTabIds.contains(i.$1.id))
.toList();
items = [...pinned, ...unpinned];
}
return EquatableValue(items);
}
@@ -277,12 +350,35 @@ EquatableValue<List<TabStateWithContainer>> quickTabSwitcherTabStates(
generalSettingsWithDefaultsProvider.select((s) => s.tabBarDirection),
);
final pinnedTabIds = ref.watch(
watchPinnedTabIdsProvider.select(
(value) => value.value ?? const <String>{},
),
);
final sortPinnedFirst = ref.watch(
tabViewFilterControllerProvider.select((v) => v.sortPinnedFirst),
);
return EquatableValue(switch (effectiveMode) {
QuickTabSwitcherMode.lastUsedTabs => () {
final filtered = tabStates
.where((state) => state.$1.id != selectedTabId)
.toList();
return tabBarDirection == TabBarDirection.oldestFirst
if (sortPinnedFirst && pinnedTabIds.isNotEmpty) {
final pinned = filtered
.where((s) => pinnedTabIds.contains(s.$1.id))
.toList();
final unpinned = filtered
.where((s) => !pinnedTabIds.contains(s.$1.id))
.toList();
// Each partition is MRU-first from fifoTabStates. For oldestFirst,
// reverse each partition independently so pinned stays on top.
if (tabBarDirection == TabDirection.oldestFirst) {
return [...pinned.reversed, ...unpinned.reversed];
}
return [...pinned, ...unpinned];
}
return tabBarDirection == TabDirection.oldestFirst
? filtered.reversed.toList()
: filtered;
}(),
@@ -605,7 +701,7 @@ EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
// database order (ascending order_key) is oldest-first. Flip to
// newest-first when the user selects that direction.
List<TabEntity> applyDirection(List<TabEntity> entities) {
return tabListDirection == TabListDirection.newestFirst
return tabListDirection == TabDirection.newestFirst
? entities.reversed.toList()
: entities;
}
@@ -697,7 +793,7 @@ EquatableValue<List<TabPreview>> filteredTabPreviews(
/// Grouped flat-list rendering for the list and grid views.
///
/// Parent rows always render before their descendants. [TabListDirection]
/// Parent rows always render before their descendants. [TabDirection]
/// applies both to root group ordering and to sibling ordering below each
/// parent, so parent-child pairs stay together while child order still follows
/// the configured direction.
@@ -867,7 +963,7 @@ EquatableValue<List<TabListItemEntity>> groupedTabListItems(
// order_key ASC; newest-first reverses the group list. Pinned and
// unpinned partitions are reversed independently so pinned-first stays
// intact while still flipping the relative order within each partition.
if (sortField == null && tabListDirection == TabListDirection.newestFirst) {
if (sortField == null && tabListDirection == TabDirection.newestFirst) {
if (filterOptions.sortPinnedFirst) {
final pinned = groupRecords.where((g) => g.isPinned).toList()
..sort((a, b) => b.rootOrderKey.compareTo(a.rootOrderKey));
@@ -931,17 +1027,36 @@ EquatableValue<List<TabListItemEntity>> groupedTabListItems(
.add(member);
}
// Children are always sorted by storage `order_key` (optionally reversed
// for newest-first) — even when an explicit `sortField` (title/url/date)
// is active. The explicit sort applies only to root groups; descendants
// remain in insertion order so a parent's children stay grouped in the
// order they were opened rather than alphabetically interleaving across
// siblings.
// Children are sorted by storage `order_key` (optionally reversed for
// newest-first) — even when an explicit `sortField` is active. The
// explicit sort applies only to root groups; descendants remain in
// insertion order. When `sortPinnedFirst` is true, pinned children are
// sorted before unpinned siblings within each parent group.
for (final children in childrenByVisibleParent.values) {
children.sort((a, b) {
final cmp = a.row.orderKey.compareTo(b.row.orderKey);
return tabListDirection == TabListDirection.newestFirst ? -cmp : cmp;
});
if (filterOptions.sortPinnedFirst) {
final pinned = children
.where((c) => pinnedTabIds.contains(c.row.id))
.toList();
final unpinned = children
.where((c) => !pinnedTabIds.contains(c.row.id))
.toList();
final cmp = (_GroupedRow a, _GroupedRow b) =>
a.row.orderKey.compareTo(b.row.orderKey);
final directionCmp = tabListDirection == TabDirection.newestFirst
? (_GroupedRow a, _GroupedRow b) => -cmp(a, b)
: cmp;
pinned.sort(directionCmp);
unpinned.sort(directionCmp);
children
..clear()
..addAll(pinned)
..addAll(unpinned);
} else {
children.sort((a, b) {
final cmp = a.row.orderKey.compareTo(b.row.orderKey);
return tabListDirection == TabDirection.newestFirst ? -cmp : cmp;
});
}
}
void addChildren(String parentId) {
@@ -49,7 +49,7 @@ final class CanManualTabReorderProvider
}
String _$canManualTabReorderHash() =>
r'598bc67a750f45893ba555b9b7866499b1808896';
r'ba5d961933464b6e005d7945802908a9a4ae034b';
@ProviderFor(SelectedBangTrigger)
final selectedBangTriggerProvider = SelectedBangTriggerFamily._();
@@ -529,7 +529,7 @@ final class SelectedContainerTabStatesWithContainerProvider
}
String _$selectedContainerTabStatesWithContainerHash() =>
r'6b630cdfc589af6961d4c96c90d09bd107560f15';
r'e6fd0e1e0ca4cfb0a0a46e8ff4e63d41032bfad9';
@ProviderFor(quickTabSwitcherTabStates)
final quickTabSwitcherTabStatesProvider = QuickTabSwitcherTabStatesFamily._();
@@ -601,7 +601,7 @@ final class QuickTabSwitcherTabStatesProvider
}
String _$quickTabSwitcherTabStatesHash() =>
r'32e928b4596ef2388f8c0e4cfdcf8f3563a1e24a';
r'3a1a195f6b2fc4cc54e689fab7bacaf4e6422196';
final class QuickTabSwitcherTabStatesFamily extends $Family
with
@@ -962,7 +962,7 @@ final class SeamlessFilteredTabEntitiesProvider
}
String _$seamlessFilteredTabEntitiesHash() =>
r'79abca3ad2753af4482c0e3bcfa317a6f336ca17';
r'83113c04be7932c60c7c97b70595fde0145f8040';
final class SeamlessFilteredTabEntitiesFamily extends $Family
with
@@ -1097,7 +1097,7 @@ final class FilteredTabPreviewsFamily extends $Family
/// Grouped flat-list rendering for the list and grid views.
///
/// Parent rows always render before their descendants. [TabListDirection]
/// Parent rows always render before their descendants. [TabDirection]
/// applies both to root group ordering and to sibling ordering below each
/// parent, so parent-child pairs stay together while child order still follows
/// the configured direction.
@@ -1109,7 +1109,7 @@ final groupedTabListItemsProvider = GroupedTabListItemsFamily._();
/// Grouped flat-list rendering for the list and grid views.
///
/// Parent rows always render before their descendants. [TabListDirection]
/// Parent rows always render before their descendants. [TabDirection]
/// applies both to root group ordering and to sibling ordering below each
/// parent, so parent-child pairs stay together while child order still follows
/// the configured direction.
@@ -1126,7 +1126,7 @@ final class GroupedTabListItemsProvider
with $Provider<EquatableValue<List<TabListItemEntity>>> {
/// Grouped flat-list rendering for the list and grid views.
///
/// Parent rows always render before their descendants. [TabListDirection]
/// Parent rows always render before their descendants. [TabDirection]
/// applies both to root group ordering and to sibling ordering below each
/// parent, so parent-child pairs stay together while child order still follows
/// the configured direction.
@@ -1186,11 +1186,11 @@ final class GroupedTabListItemsProvider
}
String _$groupedTabListItemsHash() =>
r'48c7c971d95934afb48aa19b7400e208ee2f10e4';
r'15106bfa42146270245c7178928a834237042c1c';
/// Grouped flat-list rendering for the list and grid views.
///
/// Parent rows always render before their descendants. [TabListDirection]
/// Parent rows always render before their descendants. [TabDirection]
/// applies both to root group ordering and to sibling ordering below each
/// parent, so parent-child pairs stay together while child order still follows
/// the configured direction.
@@ -1214,7 +1214,7 @@ final class GroupedTabListItemsFamily extends $Family
/// Grouped flat-list rendering for the list and grid views.
///
/// Parent rows always render before their descendants. [TabListDirection]
/// Parent rows always render before their descendants. [TabDirection]
/// applies both to root group ordering and to sibling ordering below each
/// parent, so parent-child pairs stay together while child order still follows
/// the configured direction.
@@ -41,7 +41,7 @@ TabViewReorderResult? buildTabViewReorderResult({
required Set<String> pinnedTabIds,
required int oldIndex,
required int newIndex,
required TabListDirection tabListDirection,
required TabDirection tabListDirection,
required bool hierarchical,
required bool sortPinnedFirst,
}) {
@@ -178,7 +178,7 @@ TabViewReorderResult? buildTabViewReorderResult({
// order before picking anchors so genBetween receives prev.orderKey <
// next.orderKey. Block contents (e.g. the moving subtree from _subtreeIds)
// are already storage-ordered internally, so we reverse blocks as units.
if (tabListDirection == TabListDirection.newestFirst) {
if (tabListDirection == TabDirection.newestFirst) {
for (final blocks in blocksByRoot.values) {
if (blocks.length > 1) {
final reversedTail = blocks.sublist(1).reversed.toList();
@@ -189,7 +189,7 @@ TabViewReorderResult? buildTabViewReorderResult({
}
}
final orderedRootIds = tabListDirection == TabListDirection.newestFirst
final orderedRootIds = tabListDirection == TabDirection.newestFirst
? rootOrder.reversed
: rootOrder;
@@ -213,13 +213,13 @@ TabViewReorderResult? buildTabViewReorderResult({
List<String> _orderedIdsForStorageAnchors(
List<String> orderedTabIds, {
required TabListDirection tabListDirection,
required TabDirection tabListDirection,
required Set<String> pinnedTabIds,
required Map<String, String?> parentById,
required String movingPartitionRootId,
required bool sortPinnedFirst,
}) {
var storageOrderedIds = tabListDirection == TabListDirection.newestFirst
var storageOrderedIds = tabListDirection == TabDirection.newestFirst
// Rendering flips root group order for newest-first; convert the
// display order back to storage order before choosing anchors.
? orderedTabIds.reversed.toList()
@@ -200,7 +200,7 @@ class _TabGridView extends HookConsumerWidget {
).select((value) => (value.value?.query ?? '').isNotEmpty),
);
final List<TabViewItem> primaryRows;
List<TabViewItem> primaryRows;
if (hasActiveSearch) {
final flat = ref.watch(
seamlessFilteredTabEntitiesProvider(
@@ -242,6 +242,15 @@ class _TabGridView extends HookConsumerWidget {
: TabViewItem.standalone(tabId: c.tabId),
},
];
if (!showHierarchicalTabs && filterOptions.sortPinnedFirst) {
final pinned = primaryRows
.where((r) => pinnedTabIds.contains(r.tabId))
.toList();
final unpinned = primaryRows
.where((r) => !pinnedTabIds.contains(r.tabId))
.toList();
primaryRows = [...pinned, ...unpinned];
}
}
final tabSuggestionsEnabled = ref.watch(
@@ -261,7 +261,7 @@ class _TabListView extends HookConsumerWidget {
).select((value) => (value.value?.query ?? '').isNotEmpty),
);
final List<TabViewItem> primaryRows;
List<TabViewItem> primaryRows;
if (hasActiveSearch) {
final flat = ref.watch(
seamlessFilteredTabEntitiesProvider(
@@ -303,6 +303,15 @@ class _TabListView extends HookConsumerWidget {
: TabViewItem.standalone(tabId: c.tabId),
},
];
if (!showHierarchicalTabs && filterOptions.sortPinnedFirst) {
final pinned = primaryRows
.where((r) => pinnedTabIds.contains(r.tabId))
.toList();
final unpinned = primaryRows
.where((r) => !pinnedTabIds.contains(r.tabId))
.toList();
primaryRows = [...pinned, ...unpinned];
}
}
final tabSuggestionsEnabled = ref.watch(