search module reorder
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
+108
@@ -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));
|
||||
}
|
||||
}
|
||||
+66
-1
@@ -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;
|
||||
}
|
||||
|
||||
+52
@@ -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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+106
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
@@ -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),
|
||||
|
||||
+13
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ final class TopSiteRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$topSiteRepositoryHash() => r'b0cd825d479609ff0be3c9cf0f585ab9b4d6d86f';
|
||||
String _$topSiteRepositoryHash() => r'e32ddb20e61fcd3830c4eb88704824a57e7a536a';
|
||||
|
||||
abstract class _$TopSiteRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
Reference in New Issue
Block a user