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
@@ -69,7 +69,8 @@ class _TabsSection extends StatelessWidget {
SettingSection(name: 'Tabs'),
_NewTabDefaultSection(),
_SmallWebTabDefaultSection(),
_NewTabPositionSection(),
_TabListDirectionSection(),
_TabBarDirectionSection(),
_ShowContainerUiTile(),
_ShowIsolatedTabUiTile(),
_CreateChildTabsTile(),
@@ -340,13 +341,13 @@ class _ExternalLinkHandlingSection extends HookConsumerWidget {
}
}
class _NewTabPositionSection extends HookConsumerWidget {
const _NewTabPositionSection();
class _TabListDirectionSection extends HookConsumerWidget {
const _TabListDirectionSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final newTabPosition = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.newTabPosition),
final direction = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabListDirection),
);
return Padding(
@@ -356,8 +357,65 @@ class _NewTabPositionSection extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('New Tab Position'),
subtitle: Text('Choose where newly created tabs appear by default'),
title: Text('Tab List Direction'),
subtitle: Text(
'Choose whether the newest tab appears at the top or bottom of the tab list',
),
leading: Icon(MdiIcons.formatListBulleted),
contentPadding: EdgeInsets.zero,
),
Center(
child: SegmentedButton(
showSelectedIcon: false,
segments: const [
ButtonSegment(
value: TabListDirection.newestFirst,
label: Text('Newest first'),
icon: Icon(MdiIcons.arrowCollapseUp),
),
ButtonSegment(
value: TabListDirection.oldestFirst,
label: Text('Oldest first'),
icon: Icon(MdiIcons.arrowCollapseDown),
),
],
selected: {direction},
onSelectionChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith
.tabListDirection(value.first),
);
},
),
),
],
),
);
}
}
class _TabBarDirectionSection extends HookConsumerWidget {
const _TabBarDirectionSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final direction = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.tabBarDirection),
);
return 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 Direction'),
subtitle: Text(
'Choose whether the newest tab appears on the left or right of the quick switcher',
),
leading: Icon(MdiIcons.reorderHorizontal),
contentPadding: EdgeInsets.zero,
),
@@ -366,23 +424,23 @@ class _NewTabPositionSection extends HookConsumerWidget {
showSelectedIcon: false,
segments: const [
ButtonSegment(
value: NewTabPosition.first,
label: Text('First'),
value: TabBarDirection.newestFirst,
label: Text('Newest first'),
icon: Icon(MdiIcons.arrowCollapseLeft),
),
ButtonSegment(
value: NewTabPosition.end,
label: Text('End'),
value: TabBarDirection.oldestFirst,
label: Text('Oldest first'),
icon: Icon(MdiIcons.arrowCollapseRight),
),
],
selected: {newTabPosition},
selected: {direction},
onSelectionChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.newTabPosition(value.first),
currentSettings.copyWith.tabBarDirection(value.first),
);
},
),
@@ -106,8 +106,13 @@ class ContextualToolbarSettingsScreen extends HookConsumerWidget {
else
SliverReorderableList(
itemCount: visibleConfigs.length,
onReorder: (oldIndex, newIndex) =>
_onReorder(visibleConfigs, oldIndex, newIndex, repository),
onReorder: (oldIndex, newIndex) => _onReorder(
visibleConfigs,
oldIndex,
newIndex,
repository,
isVisible: true,
),
itemBuilder: (context, index) {
final config = visibleConfigs[index];
return _ToolbarButtonConfigTile(
@@ -145,8 +150,13 @@ class ContextualToolbarSettingsScreen extends HookConsumerWidget {
else
SliverReorderableList(
itemCount: hiddenConfigs.length,
onReorder: (oldIndex, newIndex) =>
_onReorder(hiddenConfigs, oldIndex, newIndex, repository),
onReorder: (oldIndex, newIndex) => _onReorder(
hiddenConfigs,
oldIndex,
newIndex,
repository,
isVisible: false,
),
itemBuilder: (context, index) {
final config = hiddenConfigs[index];
return _ToolbarButtonConfigTile(
@@ -180,8 +190,9 @@ class ContextualToolbarSettingsScreen extends HookConsumerWidget {
List<ToolbarButtonConfig> configs,
int oldIndex,
int newIndex,
ContextualToolbarConfigRepository repository,
) {
ContextualToolbarConfigRepository repository, {
required bool isVisible,
}) {
if (oldIndex == newIndex) return;
final reorder = _resolveToolbarReorder(configs, oldIndex, newIndex);
if (reorder.targetIndex == oldIndex) return;
@@ -192,6 +203,7 @@ class ContextualToolbarSettingsScreen extends HookConsumerWidget {
oldIndex,
reorder.targetIndex,
reorder.movedId,
isVisible: isVisible,
),
);
}
@@ -201,22 +213,33 @@ class ContextualToolbarSettingsScreen extends HookConsumerWidget {
List<ToolbarButtonConfig> configs,
int oldIndex,
int targetIndex,
String movedId,
) async {
String movedId, {
required bool isVisible,
}) async {
// [configs] still contains the moved item. [targetIndex] is the
// post-removal destination index, clamped to [0, configs.length - 1].
// Because of the clamp, `>= configs.length - 1` only fires when the user
// dropped past the very last surviving item; `configs[targetIndex + 1]`
// is otherwise always a valid neighbour in the original list (since the
// moved item sits at oldIndex < targetIndex + 1 in the else branch).
final String orderKey;
if (targetIndex <= 0) {
orderKey = await repository.generateLeadingOrderKey();
orderKey = await repository.generateLeadingOrderKey(isVisible: isVisible);
} else if (targetIndex >= configs.length - 1) {
orderKey = await repository.generateTrailingOrderKey();
orderKey = await repository.generateTrailingOrderKey(
isVisible: isVisible,
);
} else if (targetIndex < oldIndex) {
orderKey =
await repository.generateOrderKeyAfterButtonId(
configs[targetIndex - 1].buttonId,
isVisible: isVisible,
) ??
await repository.generateLeadingOrderKey();
await repository.generateLeadingOrderKey(isVisible: isVisible);
} else {
orderKey = await repository.generateOrderKeyBeforeButtonId(
configs[targetIndex + 1].buttonId,
isVisible: isVisible,
);
}
await repository.assignOrderKey(movedId, orderKey: orderKey);