search module reorder

This commit is contained in:
Fabian Freund
2026-03-09 12:30:25 +01:00
parent e0c59f68ef
commit 62aed84fd8
9 changed files with 620 additions and 202 deletions
@@ -0,0 +1,105 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:convert';
import 'package:riverpod/experimental/persist.dart';
import 'package:riverpod_annotation/experimental/persist.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/geckoview/features/search/domain/providers/search_modules_view.dart';
import 'package:weblibre/features/user/data/providers.dart';
part 'search_module_order.g.dart';
typedef ModuleOrderEntry = ({SearchModuleType type, bool visible});
List<ModuleOrderEntry> _mergeWithDefaults(
List<ModuleOrderEntry>? persisted,
List<SearchModuleType> defaults,
) {
if (persisted == null) {
return defaults.map((type) => (type: type, visible: true)).toList();
}
final defaultSet = defaults.toSet();
// Keep persisted entries that are still valid
final result = persisted.where((e) => defaultSet.contains(e.type)).toList();
// Add any new defaults not in persisted
final persistedTypes = result.map((e) => e.type).toSet();
for (final type in defaults) {
if (!persistedTypes.contains(type)) {
result.add((type: type, visible: true));
}
}
return result;
}
@Riverpod(keepAlive: true)
class SearchModuleOrder extends _$SearchModuleOrder {
void reorder(int oldIndex, int newIndex) {
final list = [...state];
final item = list.removeAt(oldIndex);
final insertIndex = newIndex > oldIndex ? newIndex - 1 : newIndex;
list.insert(insertIndex, item);
state = list;
}
void toggleVisibility(SearchModuleType type) {
state = [
for (final e in state)
if (e.type == type) (type: e.type, visible: !e.visible) else e,
];
}
@override
List<ModuleOrderEntry> build(SearchModuleGroup group) {
persist(
ref.watch(riverpodDatabaseStorageProvider),
key: group.key,
encode: (state) => jsonEncode(
state
.map((e) => {'type': e.type.name, 'visible': e.visible})
.toList(),
),
decode: (encoded) {
final decoded = (jsonDecode(encoded) as List<dynamic>)
.cast<Map<String, dynamic>>()
.map((e) {
try {
return (
type: SearchModuleType.values.byName(e['type']! as String),
visible: e['visible']! as bool,
);
} catch (_) {
return null;
}
})
.whereType<ModuleOrderEntry>()
.toList();
// Merge with defaults to pick up newly added or remove deleted modules
return _mergeWithDefaults(decoded, group.defaultModules);
},
);
return stateOrNull ??
group.defaultModules
.map((type) => (type: type, visible: true))
.toList();
}
}
@@ -0,0 +1,108 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'search_module_order.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(SearchModuleOrder)
final searchModuleOrderProvider = SearchModuleOrderFamily._();
final class SearchModuleOrderProvider
extends $NotifierProvider<SearchModuleOrder, List<ModuleOrderEntry>> {
SearchModuleOrderProvider._({
required SearchModuleOrderFamily super.from,
required SearchModuleGroup super.argument,
}) : super(
retry: null,
name: r'searchModuleOrderProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$searchModuleOrderHash();
@override
String toString() {
return r'searchModuleOrderProvider'
''
'($argument)';
}
@$internal
@override
SearchModuleOrder create() => SearchModuleOrder();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(List<ModuleOrderEntry> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<List<ModuleOrderEntry>>(value),
);
}
@override
bool operator ==(Object other) {
return other is SearchModuleOrderProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$searchModuleOrderHash() => r'7e7822de86688cf87dff1c9991bc2551dc44e002';
final class SearchModuleOrderFamily extends $Family
with
$ClassFamilyOverride<
SearchModuleOrder,
List<ModuleOrderEntry>,
List<ModuleOrderEntry>,
List<ModuleOrderEntry>,
SearchModuleGroup
> {
SearchModuleOrderFamily._()
: super(
retry: null,
name: r'searchModuleOrderProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: false,
);
SearchModuleOrderProvider call(SearchModuleGroup group) =>
SearchModuleOrderProvider._(argument: group, from: this);
@override
String toString() => r'searchModuleOrderProvider';
}
abstract class _$SearchModuleOrder extends $Notifier<List<ModuleOrderEntry>> {
late final _$args = ref.$arg as SearchModuleGroup;
SearchModuleGroup get group => _$args;
List<ModuleOrderEntry> build(SearchModuleGroup group);
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref as $Ref<List<ModuleOrderEntry>, List<ModuleOrderEntry>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<List<ModuleOrderEntry>, List<ModuleOrderEntry>>,
List<ModuleOrderEntry>,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
}
@@ -31,7 +31,62 @@ enum SearchModuleType {
recentHistory,
recentArticles,
recentTabs,
containers,
containers;
String get label => switch (this) {
tabs => 'Tabs',
articles => 'Articles',
bookmarks => 'Bookmarks',
history => 'History',
historyHighlights => 'History Highlights',
topSites => 'Top Sites',
recentHistory => 'Recent History',
recentArticles => 'Recent Articles',
recentTabs => 'Recent Tabs',
containers => 'Containers',
};
}
enum SearchModuleGroup {
emptyState(
key: 'EmptyStateModuleOrder',
defaultModules: [
SearchModuleType.topSites,
SearchModuleType.recentArticles,
SearchModuleType.recentTabs,
SearchModuleType.recentHistory,
SearchModuleType.historyHighlights,
SearchModuleType.containers,
],
),
search(
key: 'SearchModuleOrder',
defaultModules: [
SearchModuleType.tabs,
SearchModuleType.bookmarks,
SearchModuleType.articles,
SearchModuleType.history,
],
);
const SearchModuleGroup({required this.key, required this.defaultModules});
final String key;
final List<SearchModuleType> defaultModules;
}
extension SearchModuleTypeGroup on SearchModuleType {
SearchModuleGroup get group => switch (this) {
SearchModuleType.topSites ||
SearchModuleType.recentArticles ||
SearchModuleType.recentTabs ||
SearchModuleType.recentHistory ||
SearchModuleType.historyHighlights ||
SearchModuleType.containers => SearchModuleGroup.emptyState,
SearchModuleType.tabs ||
SearchModuleType.bookmarks ||
SearchModuleType.articles ||
SearchModuleType.history => SearchModuleGroup.search,
};
}
enum SearchModuleDisplayState { preview, expanded, collapsed }
@@ -67,3 +122,13 @@ class SearchModuleDisplayStateController
return SearchModuleDisplayState.preview;
}
}
@Riverpod()
class SearchReorderMode extends _$SearchReorderMode {
// ignore: use_setters_to_change_properties
void activate(SearchModuleGroup group) => state = group;
void deactivate() => state = null;
@override
SearchModuleGroup? build() => null;
}
@@ -119,3 +119,55 @@ abstract class _$SearchModuleDisplayStateController
element.handleCreate(ref, () => build(_$args));
}
}
@ProviderFor(SearchReorderMode)
final searchReorderModeProvider = SearchReorderModeProvider._();
final class SearchReorderModeProvider
extends $NotifierProvider<SearchReorderMode, SearchModuleGroup?> {
SearchReorderModeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'searchReorderModeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$searchReorderModeHash();
@$internal
@override
SearchReorderMode create() => SearchReorderMode();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(SearchModuleGroup? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<SearchModuleGroup?>(value),
);
}
}
String _$searchReorderModeHash() => r'eda188e53e5b5f1a331ce94c3cb8808c79e358d3';
abstract class _$SearchReorderMode extends $Notifier<SearchModuleGroup?> {
SearchModuleGroup? build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<SearchModuleGroup?, SearchModuleGroup?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<SearchModuleGroup?, SearchModuleGroup?>,
SearchModuleGroup?,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -33,14 +33,18 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/search/domain/providers/search_module_order.dart';
import 'package:weblibre/features/geckoview/features/search/domain/providers/search_modules_view.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/animated_tab_type_switcher.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/clipboard_fill.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/empty_state/containers_section.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/empty_state/history_highlights_section.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/empty_state/recent_feed_articles_section.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/empty_state/recent_history_section.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/empty_state/recent_tabs_section.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/empty_state/top_sites_section.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_field.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_module_reorder_view.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/bookmark_search.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/feed_search.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart';
@@ -305,6 +309,125 @@ class SearchScreen extends HookConsumerWidget {
}
}
final reorderGroup = ref.watch(searchReorderModeProvider);
final emptyStateOrder = ref.watch(
searchModuleOrderProvider(SearchModuleGroup.emptyState),
);
final searchOrder = ref.watch(
searchModuleOrderProvider(SearchModuleGroup.search),
);
Future<void> openUriInTab(Uri uri) async {
if (isEditMode) {
await ref
.read(tabSessionProvider(tabId: tabId).notifier)
.loadUrl(url: uri);
} else {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: uri,
tabMode: effectiveTabMode,
parentId: (selectedTabType.value == TabType.child)
? ref.read(selectedTabProvider)
: null,
launchedFromIntent: launchedFromIntent,
selectTab: true,
containerSelection: selectedContainer == null
? const TabContainerSelection.unassigned()
: TabContainerSelection.specific(selectedContainer),
);
}
if (context.mounted) {
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
const BrowserRoute().go(context);
}
}
final emptyStateWidgets = <SearchModuleType, Widget>{
SearchModuleType.topSites: TopSitesSection(
onUriSelected: openUriInTab,
),
SearchModuleType.recentArticles: RecentFeedArticlesSection(
onArticleSelected: (article) {
unawaited(
FeedArticleRoute(articleId: article.id).push(context),
);
},
),
SearchModuleType.recentTabs: RecentTabsSection(
onTabSelected: (tabId) async {
await ref
.read(tabRepositoryProvider.notifier)
.selectTab(tabId);
if (context.mounted) {
ref
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
const BrowserRoute().go(context);
}
},
),
SearchModuleType.recentHistory: RecentHistorySection(
onUriSelected: openUriInTab,
),
SearchModuleType.historyHighlights: HistoryHighlightsSection(
onUriSelected: openUriInTab,
),
SearchModuleType.containers: ContainersSection(
onContainerSelected: (container) async {
final result = await ref
.read(selectedContainerProvider.notifier)
.setContainerId(container.id);
if (!context.mounted) return;
if (result == SetContainerResult.successHasProxy) {
final shouldStartProxy = await ref
.read(startProxyControllerProvider.notifier)
.shouldPromptProxyStart();
if (context.mounted && shouldStartProxy) {
final dialogResult = await showDialog<bool>(
context: context,
builder: (_) => const TorDialog(),
);
if (dialogResult == true) {
await ref
.read(startProxyControllerProvider.notifier)
.startProxy();
}
}
}
if (context.mounted && result != SetContainerResult.failed) {
const TabViewRoute().go(context);
}
},
),
};
final searchWidgets = <SearchModuleType, Widget>{
SearchModuleType.tabs: TabSearch(
searchTextListenable: sampledSearchText,
),
SearchModuleType.bookmarks: BookmarkSearch(
searchTextListenable: sampledSearchText,
onUriSelected: openUriInTab,
),
SearchModuleType.articles: FeedSearch(
searchTextNotifier: sampledSearchText,
),
SearchModuleType.history: HistorySuggestions(
searchTextListenable: sampledSearchText,
onUriSelected: openUriInTab,
),
};
return Scaffold(
body: SafeArea(
child: Form(
@@ -502,212 +625,30 @@ class SearchScreen extends HookConsumerWidget {
SliverToBoxAdapter(
child: ClipboardFillLink(controller: searchTextController),
),
if (showNoInputSections) ...[
TopSitesSection(
onUriSelected: (uri) async {
if (isEditMode) {
await ref
.read(tabSessionProvider(tabId: tabId).notifier)
.loadUrl(url: uri);
} else {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: uri,
tabMode: effectiveTabMode,
parentId: (selectedTabType.value == TabType.child)
? ref.read(selectedTabProvider)
: null,
launchedFromIntent: launchedFromIntent,
selectTab: true,
containerSelection: selectedContainer == null
? const TabContainerSelection.unassigned()
: TabContainerSelection.specific(
selectedContainer,
),
);
}
if (context.mounted) {
ref
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
const BrowserRoute().go(context);
}
},
),
RecentFeedArticlesSection(
onArticleSelected: (article) {
unawaited(
FeedArticleRoute(articleId: article.id).push(context),
);
},
),
RecentTabsSection(
onTabSelected: (tabId) async {
await ref
.read(tabRepositoryProvider.notifier)
.selectTab(tabId);
if (context.mounted) {
ref
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
const BrowserRoute().go(context);
}
},
),
RecentHistorySection(
onUriSelected: (uri) async {
if (isEditMode) {
await ref
.read(tabSessionProvider(tabId: tabId).notifier)
.loadUrl(url: uri);
} else {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: uri,
tabMode: effectiveTabMode,
parentId: (selectedTabType.value == TabType.child)
? ref.read(selectedTabProvider)
: null,
launchedFromIntent: launchedFromIntent,
selectTab: true,
containerSelection: selectedContainer == null
? const TabContainerSelection.unassigned()
: TabContainerSelection.specific(
selectedContainer,
),
);
}
if (context.mounted) {
ref
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
const BrowserRoute().go(context);
}
},
),
ContainersSection(
onContainerSelected: (container) async {
final result = await ref
.read(selectedContainerProvider.notifier)
.setContainerId(container.id);
if (!context.mounted) return;
if (result == SetContainerResult.successHasProxy) {
final shouldStartProxy = await ref
.read(startProxyControllerProvider.notifier)
.shouldPromptProxyStart();
if (context.mounted && shouldStartProxy) {
final dialogResult = await showDialog<bool>(
context: context,
builder: (_) => const TorDialog(),
);
if (dialogResult == true) {
await ref
.read(startProxyControllerProvider.notifier)
.startProxy();
}
}
}
if (context.mounted &&
result != SetContainerResult.failed) {
const TabViewRoute().go(context);
}
},
),
],
if (!showNoInputSections) ...[
if (reorderGroup != null)
SearchModuleReorderView(group: reorderGroup)
else if (showNoInputSections) ...[
for (final entry in emptyStateOrder)
if (emptyStateWidgets.containsKey(entry.type))
emptyStateWidgets[entry.type]!,
if (!emptyStateOrder.any((e) => e.visible))
const _CustomizeSectionsButton(
group: SearchModuleGroup.emptyState,
),
] else ...[
FullSearchTermSuggestions(
searchTextController: searchTextController,
activeBang: activeBang,
submitSearch: submitSearch,
domain: isEditMode ? existingTabState.url.host : null,
),
TabSearch(searchTextListenable: sampledSearchText),
BookmarkSearch(
searchTextListenable: sampledSearchText,
onUriSelected: (uri) async {
if (isEditMode) {
await ref
.read(tabSessionProvider(tabId: tabId).notifier)
.loadUrl(url: uri);
} else {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: uri,
tabMode: effectiveTabMode,
parentId: (selectedTabType.value == TabType.child)
? ref.read(selectedTabProvider)
: null,
launchedFromIntent: launchedFromIntent,
selectTab: true,
containerSelection: selectedContainer == null
? const TabContainerSelection.unassigned()
: TabContainerSelection.specific(
selectedContainer,
),
);
}
if (context.mounted) {
ref
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
const BrowserRoute().go(context);
}
},
),
FeedSearch(searchTextNotifier: sampledSearchText),
HistorySuggestions(
searchTextListenable: sampledSearchText,
onUriSelected: (uri) async {
if (isEditMode) {
// Load into existing tab
await ref
.read(tabSessionProvider(tabId: tabId).notifier)
.loadUrl(url: uri);
} else {
// Create new tab
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: uri,
tabMode: effectiveTabMode,
parentId: (selectedTabType.value == TabType.child)
? ref.read(selectedTabProvider)
: null,
launchedFromIntent: launchedFromIntent,
selectTab: true,
containerSelection: selectedContainer == null
? const TabContainerSelection.unassigned()
: TabContainerSelection.specific(
selectedContainer,
),
);
}
if (context.mounted) {
ref
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
const BrowserRoute().go(context);
}
},
),
for (final entry in searchOrder)
if (searchWidgets.containsKey(entry.type))
searchWidgets[entry.type]!,
if (!searchOrder.any((e) => e.visible))
const _CustomizeSectionsButton(
group: SearchModuleGroup.search,
),
],
],
),
@@ -716,3 +657,26 @@ class SearchScreen extends HookConsumerWidget {
);
}
}
class _CustomizeSectionsButton extends ConsumerWidget {
final SearchModuleGroup group;
const _CustomizeSectionsButton({required this.group});
@override
Widget build(BuildContext context, WidgetRef ref) {
return SliverToBoxAdapter(
child: Center(
child: Padding(
padding: const EdgeInsets.only(top: 24),
child: TextButton.icon(
onPressed: () =>
ref.read(searchReorderModeProvider.notifier).activate(group),
icon: const Icon(Icons.tune, size: 18),
label: const Text('Customize sections'),
),
),
),
);
}
}
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/features/geckoview/features/search/domain/providers/search_module_order.dart';
import 'package:weblibre/features/geckoview/features/search/domain/providers/search_modules_view.dart';
class SearchModuleReorderView extends ConsumerWidget {
final SearchModuleGroup group;
const SearchModuleReorderView({super.key, required this.group});
@override
Widget build(BuildContext context, WidgetRef ref) {
final entries = ref.watch(searchModuleOrderProvider(group));
final colorScheme = Theme.of(context).colorScheme;
return SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 8, 4),
child: Row(
children: [
Expanded(
child: Text(
'Customize Sections',
style: Theme.of(context).textTheme.titleSmall,
),
),
TextButton(
onPressed: () => ref
.read(searchReorderModeProvider.notifier)
.deactivate(),
child: const Text('Done'),
),
],
),
),
),
SliverReorderableList(
itemCount: entries.length,
onReorder: (oldIndex, newIndex) {
ref
.read(searchModuleOrderProvider(group).notifier)
.reorder(oldIndex, newIndex);
},
itemBuilder: (context, index) {
final entry = entries[index];
return Material(
key: ValueKey(entry.type),
color: Colors.transparent,
child: ListTile(
leading: IconButton(
icon: Icon(
entry.visible
? Icons.visibility
: Icons.visibility_off,
color: entry.visible
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
onPressed: () => ref
.read(searchModuleOrderProvider(group).notifier)
.toggleVisibility(entry.type),
),
title: Text(
entry.type.label.toUpperCase(),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: entry.visible
? null
: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
),
),
trailing: ReorderableDragStartListener(
index: index,
child: Icon(
Icons.drag_handle,
color: colorScheme.onSurfaceVariant,
),
),
),
);
},
),
],
);
}
}
@@ -37,6 +37,9 @@ class SearchModuleHeader extends StatelessWidget {
/// The trailing button is hidden when totalCount <= this value.
final int previewLimit;
/// Called when the header is long-pressed (e.g. to enter reorder mode).
final VoidCallback? onLongPress;
const SearchModuleHeader({
super.key,
required this.title,
@@ -46,6 +49,7 @@ class SearchModuleHeader extends StatelessWidget {
required this.onToggleExpansion,
this.headerTrailing,
this.previewLimit = 3,
this.onLongPress,
});
@override
@@ -61,6 +65,7 @@ class SearchModuleHeader extends StatelessWidget {
Expanded(
child: InkWell(
onTap: onToggleCollapse,
onLongPress: onLongPress,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
@@ -20,6 +20,7 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:weblibre/features/geckoview/features/search/domain/providers/search_module_order.dart';
import 'package:weblibre/features/geckoview/features/search/domain/providers/search_modules_view.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/search_module_header.dart';
@@ -63,6 +64,15 @@ class SearchModuleSection extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final moduleOrder = ref.watch(
searchModuleOrderProvider(moduleType.group),
);
final isVisible = moduleOrder
.any((e) => e.type == moduleType && e.visible);
if (!isVisible) {
return MultiSliver(children: const []);
}
final displayState = ref.watch(
searchModuleDisplayStateControllerProvider(moduleType),
);
@@ -102,6 +112,9 @@ class SearchModuleSection extends ConsumerWidget {
).notifier,
)
.toggleExpansion(),
onLongPress: () => ref
.read(searchReorderModeProvider.notifier)
.activate(moduleType.group),
),
),
),
@@ -41,7 +41,7 @@ final class TopSiteRepositoryProvider
}
}
String _$topSiteRepositoryHash() => r'b0cd825d479609ff0be3c9cf0f585ab9b4d6d86f';
String _$topSiteRepositoryHash() => r'e32ddb20e61fcd3830c4eb88704824a57e7a536a';
abstract class _$TopSiteRepository extends $Notifier<void> {
void build();