added many suggestion modules

This commit is contained in:
Fabian Freund
2026-03-09 03:10:02 +01:00
parent 84038c7b81
commit a7b4954da1
35 changed files with 4593 additions and 2077 deletions
@@ -66,8 +66,7 @@ class TranslationState with FastEquatable {
displayError: data.displayError,
);
bool get hasError =>
translationErrorName != null && (displayError ?? true);
bool get hasError => translationErrorName != null && (displayError ?? true);
@override
List<Object?> get hashParameters => [
@@ -136,6 +136,7 @@ class BookmarkSearchResults extends _$BookmarkSearchResults {
}
final results = await _service.searchBookmarks(query, limit: limit);
if (!ref.mounted) return;
state = results
.map(BookmarkItem.parseRecursive)
.whereType<BookmarkEntry>()
@@ -86,7 +86,7 @@ final class BookmarkSearchResultsProvider
}
String _$bookmarkSearchResultsHash() =>
r'f1413466db26c1e3407501453b8cec97504a771d';
r'44af94b2234c817b08b0bde2dc68f9ab8d2d0a9a';
abstract class _$BookmarkSearchResults extends $Notifier<List<BookmarkEntry>> {
List<BookmarkEntry> build();
@@ -47,7 +47,7 @@ import 'package:weblibre/features/user/domain/repositories/general_settings.dart
part 'providers.g.dart';
typedef TabStateWirthContainer = (TabState, ContainerData?);
typedef TabStateWithContainer = (TabState, ContainerData?);
@Riverpod()
bool canManualTabReorder(Ref ref) {
@@ -175,7 +175,7 @@ EquatableValue<Map<String, TabState>> containerTabStates(
}
@Riverpod(keepAlive: true)
EquatableValue<List<TabStateWirthContainer>> fifoTabStates(Ref ref) {
EquatableValue<List<TabStateWithContainer>> fifoTabStates(Ref ref) {
final containerData = ref
.watch(watchContainersWithCountProvider.select((value) => value.value))
.mapNotNull(
@@ -202,7 +202,7 @@ EquatableValue<List<TabStateWirthContainer>> fifoTabStates(Ref ref) {
}
@Riverpod()
EquatableValue<List<TabStateWirthContainer>>
EquatableValue<List<TabStateWithContainer>>
selectedContainerTabStatesWithContainer(Ref ref) {
final filter = ref.watch(
selectedContainerProvider.select(
@@ -256,7 +256,7 @@ selectedContainerTabStatesWithContainer(Ref ref) {
}
@Riverpod()
EquatableValue<List<TabStateWirthContainer>> quickTabSwitcherTabStates(
EquatableValue<List<TabStateWithContainer>> quickTabSwitcherTabStates(
Ref ref,
QuickTabSwitcherMode mode,
) {
@@ -430,11 +430,11 @@ final fifoTabStatesProvider = FifoTabStatesProvider._();
final class FifoTabStatesProvider
extends
$FunctionalProvider<
EquatableValue<List<TabStateWirthContainer>>,
EquatableValue<List<TabStateWirthContainer>>,
EquatableValue<List<TabStateWirthContainer>>
EquatableValue<List<TabStateWithContainer>>,
EquatableValue<List<TabStateWithContainer>>,
EquatableValue<List<TabStateWithContainer>>
>
with $Provider<EquatableValue<List<TabStateWirthContainer>>> {
with $Provider<EquatableValue<List<TabStateWithContainer>>> {
FifoTabStatesProvider._()
: super(
from: null,
@@ -451,30 +451,30 @@ final class FifoTabStatesProvider
@$internal
@override
$ProviderElement<EquatableValue<List<TabStateWirthContainer>>> $createElement(
$ProviderElement<EquatableValue<List<TabStateWithContainer>>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
EquatableValue<List<TabStateWirthContainer>> create(Ref ref) {
EquatableValue<List<TabStateWithContainer>> create(Ref ref) {
return fifoTabStates(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(
EquatableValue<List<TabStateWirthContainer>> value,
EquatableValue<List<TabStateWithContainer>> value,
) {
return $ProviderOverride(
origin: this,
providerOverride:
$SyncValueProvider<EquatableValue<List<TabStateWirthContainer>>>(
$SyncValueProvider<EquatableValue<List<TabStateWithContainer>>>(
value,
),
);
}
}
String _$fifoTabStatesHash() => r'03298f30559d55e986d97a17d517b9a9abb89be4';
String _$fifoTabStatesHash() => r'f8dc69382d307bc83193a3b839a16313e9c1ab69';
@ProviderFor(selectedContainerTabStatesWithContainer)
final selectedContainerTabStatesWithContainerProvider =
@@ -483,11 +483,11 @@ final selectedContainerTabStatesWithContainerProvider =
final class SelectedContainerTabStatesWithContainerProvider
extends
$FunctionalProvider<
EquatableValue<List<TabStateWirthContainer>>,
EquatableValue<List<TabStateWirthContainer>>,
EquatableValue<List<TabStateWirthContainer>>
EquatableValue<List<TabStateWithContainer>>,
EquatableValue<List<TabStateWithContainer>>,
EquatableValue<List<TabStateWithContainer>>
>
with $Provider<EquatableValue<List<TabStateWirthContainer>>> {
with $Provider<EquatableValue<List<TabStateWithContainer>>> {
SelectedContainerTabStatesWithContainerProvider._()
: super(
from: null,
@@ -505,23 +505,23 @@ final class SelectedContainerTabStatesWithContainerProvider
@$internal
@override
$ProviderElement<EquatableValue<List<TabStateWirthContainer>>> $createElement(
$ProviderElement<EquatableValue<List<TabStateWithContainer>>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
EquatableValue<List<TabStateWirthContainer>> create(Ref ref) {
EquatableValue<List<TabStateWithContainer>> create(Ref ref) {
return selectedContainerTabStatesWithContainer(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(
EquatableValue<List<TabStateWirthContainer>> value,
EquatableValue<List<TabStateWithContainer>> value,
) {
return $ProviderOverride(
origin: this,
providerOverride:
$SyncValueProvider<EquatableValue<List<TabStateWirthContainer>>>(
$SyncValueProvider<EquatableValue<List<TabStateWithContainer>>>(
value,
),
);
@@ -529,7 +529,7 @@ final class SelectedContainerTabStatesWithContainerProvider
}
String _$selectedContainerTabStatesWithContainerHash() =>
r'e2dc85fc72ee29736d8fdaabb1abadd23cc4bac0';
r'187e408857ddc5fe08f8e5344beb510c3c39abe1';
@ProviderFor(quickTabSwitcherTabStates)
final quickTabSwitcherTabStatesProvider = QuickTabSwitcherTabStatesFamily._();
@@ -537,11 +537,11 @@ final quickTabSwitcherTabStatesProvider = QuickTabSwitcherTabStatesFamily._();
final class QuickTabSwitcherTabStatesProvider
extends
$FunctionalProvider<
EquatableValue<List<TabStateWirthContainer>>,
EquatableValue<List<TabStateWirthContainer>>,
EquatableValue<List<TabStateWirthContainer>>
EquatableValue<List<TabStateWithContainer>>,
EquatableValue<List<TabStateWithContainer>>,
EquatableValue<List<TabStateWithContainer>>
>
with $Provider<EquatableValue<List<TabStateWirthContainer>>> {
with $Provider<EquatableValue<List<TabStateWithContainer>>> {
QuickTabSwitcherTabStatesProvider._({
required QuickTabSwitcherTabStatesFamily super.from,
required QuickTabSwitcherMode super.argument,
@@ -565,24 +565,24 @@ final class QuickTabSwitcherTabStatesProvider
@$internal
@override
$ProviderElement<EquatableValue<List<TabStateWirthContainer>>> $createElement(
$ProviderElement<EquatableValue<List<TabStateWithContainer>>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
EquatableValue<List<TabStateWirthContainer>> create(Ref ref) {
EquatableValue<List<TabStateWithContainer>> create(Ref ref) {
final argument = this.argument as QuickTabSwitcherMode;
return quickTabSwitcherTabStates(ref, argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(
EquatableValue<List<TabStateWirthContainer>> value,
EquatableValue<List<TabStateWithContainer>> value,
) {
return $ProviderOverride(
origin: this,
providerOverride:
$SyncValueProvider<EquatableValue<List<TabStateWirthContainer>>>(
$SyncValueProvider<EquatableValue<List<TabStateWithContainer>>>(
value,
),
);
@@ -601,12 +601,12 @@ final class QuickTabSwitcherTabStatesProvider
}
String _$quickTabSwitcherTabStatesHash() =>
r'df990fb96cca62a272572c925028725f81a0e96b';
r'2627bcd25718472f0c8057e836d76bdaa7317c1c';
final class QuickTabSwitcherTabStatesFamily extends $Family
with
$FunctionalFamilyOverride<
EquatableValue<List<TabStateWirthContainer>>,
EquatableValue<List<TabStateWithContainer>>,
QuickTabSwitcherMode
> {
QuickTabSwitcherTabStatesFamily._()
@@ -587,8 +587,9 @@ class _TranslatePageTile extends ConsumerWidget {
tabStateProvider(selectedTabId).select((s) => s?.translationState),
);
final readerActive = ref.watch(
tabStateProvider(selectedTabId)
.select((s) => s?.readerableState.active ?? false),
tabStateProvider(
selectedTabId,
).select((s) => s?.readerableState.active ?? false),
);
// Hide when reader mode is active (Fenix-aligned)
@@ -588,12 +588,14 @@ class TabMenu extends HookConsumerWidget {
builder: (context, ref, child) {
final engineState = ref.watch(translationEngineStateProvider);
final translationState = ref.watch(
tabStateProvider(selectedTabId)
.select((s) => s?.translationState),
tabStateProvider(
selectedTabId,
).select((s) => s?.translationState),
);
final readerActive = ref.watch(
tabStateProvider(selectedTabId)
.select((s) => s?.readerableState.active ?? false),
tabStateProvider(
selectedTabId,
).select((s) => s?.readerableState.active ?? false),
);
// Hide when reader mode is active (Fenix-aligned)
@@ -620,9 +622,7 @@ class TabMenu extends HookConsumerWidget {
);
}
},
child: Text(
isTranslated ? 'Translated' : 'Translate Page',
),
child: Text(isTranslated ? 'Translated' : 'Translate Page'),
);
},
),
@@ -132,8 +132,10 @@ extension HitResultX on HitResult {
HitResult withCleanedLink(String cleanedUrl) {
return switch (this) {
UnknownHitResult() => UnknownHitResult(src: cleanedUrl),
ImageSrcHitResult(:final src) =>
ImageSrcHitResult(src: src, uri: cleanedUrl),
ImageSrcHitResult(:final src) => ImageSrcHitResult(
src: src,
uri: cleanedUrl,
),
_ => this,
};
}
@@ -56,6 +56,20 @@ class HistoryRepository extends _$HistoryRepository {
return _service.deleteVisit(info);
}
Future<List<HistoryHighlight>> getHistoryHighlights({
double viewTimeWeight = 10.0,
double frequencyWeight = 4.0,
required int limit,
}) {
return _service.getHistoryHighlights(
weights: HistoryHighlightWeights(
viewTime: viewTimeWeight,
frequency: frequencyWeight,
),
limit: limit,
);
}
@override
void build() {}
}
@@ -41,7 +41,7 @@ final class HistoryRepositoryProvider
}
}
String _$historyRepositoryHash() => r'f9ccc32275bb2f95edbe53b2245b4843e8d0c439';
String _$historyRepositoryHash() => r'19f883fdb644e34bc89483b4a8ddb60f54c52114';
abstract class _$HistoryRepository extends $Notifier<void> {
void build();
@@ -0,0 +1,79 @@
/*
* 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_mozilla_components/flutter_mozilla_components.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
import 'package:weblibre/features/web_feed/data/models/feed_article.dart';
import 'package:weblibre/features/web_feed/domain/providers.dart';
part 'empty_state_content.g.dart';
@Riverpod()
Future<List<HistoryHighlight>> searchEmptyHistoryHighlights(
Ref ref, {
int count = 25,
}) async {
final highlights = await ref
.read(historyRepositoryProvider.notifier)
.getHistoryHighlights(limit: count);
if (!ref.mounted) return [];
return highlights.where((h) => Uri.tryParse(h.url) != null).toList();
}
@Riverpod()
Future<List<VisitInfo>> searchEmptyRecentHistory(
Ref ref, {
int count = 25,
}) async {
final visits = await ref
.read(historyRepositoryProvider.notifier)
.getVisitsPaginated(count: count);
if (!ref.mounted) return [];
return visits.where((v) => Uri.tryParse(v.url) != null).toList();
}
@Riverpod()
AsyncValue<List<FeedArticle>> searchEmptyRecentFeedArticles(
Ref ref, {
int count = 25,
}) {
final feeds = ref.watch(feedListProvider);
final hasFeeds = feeds.whenOrNull(data: (list) => list.isNotEmpty) ?? false;
if (!hasFeeds) {
return const AsyncValue.data([]);
}
return ref
.watch(feedArticleListProvider(null))
.whenData((articles) => articles.take(count).toList());
}
@Riverpod()
List<TabStateWithContainer> searchEmptyRecentTabs(Ref ref, {int count = 25}) {
final tabs = ref.watch(fifoTabStatesProvider).value;
return tabs.take(count).toList();
}
@@ -0,0 +1,339 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'empty_state_content.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(searchEmptyHistoryHighlights)
final searchEmptyHistoryHighlightsProvider =
SearchEmptyHistoryHighlightsFamily._();
final class SearchEmptyHistoryHighlightsProvider
extends
$FunctionalProvider<
AsyncValue<List<HistoryHighlight>>,
List<HistoryHighlight>,
FutureOr<List<HistoryHighlight>>
>
with
$FutureModifier<List<HistoryHighlight>>,
$FutureProvider<List<HistoryHighlight>> {
SearchEmptyHistoryHighlightsProvider._({
required SearchEmptyHistoryHighlightsFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'searchEmptyHistoryHighlightsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$searchEmptyHistoryHighlightsHash();
@override
String toString() {
return r'searchEmptyHistoryHighlightsProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<List<HistoryHighlight>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<HistoryHighlight>> create(Ref ref) {
final argument = this.argument as int;
return searchEmptyHistoryHighlights(ref, count: argument);
}
@override
bool operator ==(Object other) {
return other is SearchEmptyHistoryHighlightsProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$searchEmptyHistoryHighlightsHash() =>
r'f12206cb05c5c03ecdc2b84e3ffd20706aa4c986';
final class SearchEmptyHistoryHighlightsFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<List<HistoryHighlight>>, int> {
SearchEmptyHistoryHighlightsFamily._()
: super(
retry: null,
name: r'searchEmptyHistoryHighlightsProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
SearchEmptyHistoryHighlightsProvider call({int count = 25}) =>
SearchEmptyHistoryHighlightsProvider._(argument: count, from: this);
@override
String toString() => r'searchEmptyHistoryHighlightsProvider';
}
@ProviderFor(searchEmptyRecentHistory)
final searchEmptyRecentHistoryProvider = SearchEmptyRecentHistoryFamily._();
final class SearchEmptyRecentHistoryProvider
extends
$FunctionalProvider<
AsyncValue<List<VisitInfo>>,
List<VisitInfo>,
FutureOr<List<VisitInfo>>
>
with $FutureModifier<List<VisitInfo>>, $FutureProvider<List<VisitInfo>> {
SearchEmptyRecentHistoryProvider._({
required SearchEmptyRecentHistoryFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'searchEmptyRecentHistoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$searchEmptyRecentHistoryHash();
@override
String toString() {
return r'searchEmptyRecentHistoryProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<List<VisitInfo>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<VisitInfo>> create(Ref ref) {
final argument = this.argument as int;
return searchEmptyRecentHistory(ref, count: argument);
}
@override
bool operator ==(Object other) {
return other is SearchEmptyRecentHistoryProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$searchEmptyRecentHistoryHash() =>
r'bd7b9a67edfb1751d6d5cf6a8392516911796cb0';
final class SearchEmptyRecentHistoryFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<List<VisitInfo>>, int> {
SearchEmptyRecentHistoryFamily._()
: super(
retry: null,
name: r'searchEmptyRecentHistoryProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
SearchEmptyRecentHistoryProvider call({int count = 25}) =>
SearchEmptyRecentHistoryProvider._(argument: count, from: this);
@override
String toString() => r'searchEmptyRecentHistoryProvider';
}
@ProviderFor(searchEmptyRecentFeedArticles)
final searchEmptyRecentFeedArticlesProvider =
SearchEmptyRecentFeedArticlesFamily._();
final class SearchEmptyRecentFeedArticlesProvider
extends
$FunctionalProvider<
AsyncValue<List<FeedArticle>>,
AsyncValue<List<FeedArticle>>,
AsyncValue<List<FeedArticle>>
>
with $Provider<AsyncValue<List<FeedArticle>>> {
SearchEmptyRecentFeedArticlesProvider._({
required SearchEmptyRecentFeedArticlesFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'searchEmptyRecentFeedArticlesProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$searchEmptyRecentFeedArticlesHash();
@override
String toString() {
return r'searchEmptyRecentFeedArticlesProvider'
''
'($argument)';
}
@$internal
@override
$ProviderElement<AsyncValue<List<FeedArticle>>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
AsyncValue<List<FeedArticle>> create(Ref ref) {
final argument = this.argument as int;
return searchEmptyRecentFeedArticles(ref, count: argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AsyncValue<List<FeedArticle>> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AsyncValue<List<FeedArticle>>>(
value,
),
);
}
@override
bool operator ==(Object other) {
return other is SearchEmptyRecentFeedArticlesProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$searchEmptyRecentFeedArticlesHash() =>
r'ee6ac7336d58b78c6c9295cdbb8aeeca9a91c2ad';
final class SearchEmptyRecentFeedArticlesFamily extends $Family
with $FunctionalFamilyOverride<AsyncValue<List<FeedArticle>>, int> {
SearchEmptyRecentFeedArticlesFamily._()
: super(
retry: null,
name: r'searchEmptyRecentFeedArticlesProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
SearchEmptyRecentFeedArticlesProvider call({int count = 25}) =>
SearchEmptyRecentFeedArticlesProvider._(argument: count, from: this);
@override
String toString() => r'searchEmptyRecentFeedArticlesProvider';
}
@ProviderFor(searchEmptyRecentTabs)
final searchEmptyRecentTabsProvider = SearchEmptyRecentTabsFamily._();
final class SearchEmptyRecentTabsProvider
extends
$FunctionalProvider<
List<TabStateWithContainer>,
List<TabStateWithContainer>,
List<TabStateWithContainer>
>
with $Provider<List<TabStateWithContainer>> {
SearchEmptyRecentTabsProvider._({
required SearchEmptyRecentTabsFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'searchEmptyRecentTabsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$searchEmptyRecentTabsHash();
@override
String toString() {
return r'searchEmptyRecentTabsProvider'
''
'($argument)';
}
@$internal
@override
$ProviderElement<List<TabStateWithContainer>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
List<TabStateWithContainer> create(Ref ref) {
final argument = this.argument as int;
return searchEmptyRecentTabs(ref, count: argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(List<TabStateWithContainer> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<List<TabStateWithContainer>>(value),
);
}
@override
bool operator ==(Object other) {
return other is SearchEmptyRecentTabsProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$searchEmptyRecentTabsHash() =>
r'6000970dd261f243326900555967956508b84660';
final class SearchEmptyRecentTabsFamily extends $Family
with $FunctionalFamilyOverride<List<TabStateWithContainer>, int> {
SearchEmptyRecentTabsFamily._()
: super(
retry: null,
name: r'searchEmptyRecentTabsProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
SearchEmptyRecentTabsProvider call({int count = 25}) =>
SearchEmptyRecentTabsProvider._(argument: count, from: this);
@override
String toString() => r'searchEmptyRecentTabsProvider';
}
@@ -21,7 +21,17 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'search_modules_view.g.dart';
enum SearchModuleType { tabs, articles, bookmarks, history }
enum SearchModuleType {
tabs,
articles,
bookmarks,
history,
historyHighlights,
recentHistory,
recentArticles,
recentTabs,
containers,
}
enum SearchModuleDisplayState { preview, expanded, collapsed }
@@ -35,6 +35,11 @@ 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/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/search_field.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';
@@ -45,6 +50,8 @@ import 'package:weblibre/features/geckoview/features/tabs/data/entities/isolatio
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/compact_container_selector.dart';
import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart';
import 'package:weblibre/features/tor/presentation/widgets/tor_dialog.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/on_listenable_change_selector.dart';
import 'package:weblibre/presentation/hooks/sampled_value_notifier.dart';
@@ -135,6 +142,17 @@ class SearchScreen extends HookConsumerWidget {
source: searchTextController,
sampleDuration: const Duration(milliseconds: 150),
);
final hasUserProvidedInput = useState(
initialSearchText?.isNotEmpty == true,
);
useOnListenableChange(searchTextController, () {
hasUserProvidedInput.value = searchTextController.text.isNotEmpty;
});
final showNoInputSections =
!hasUserProvidedInput.value && searchTextController.text.isEmpty;
final searchFocusNode = useFocusNode();
final textFieldKey = useMemoized(() => GlobalKey());
final preferredHeight = useState<double>(kToolbarHeight);
@@ -484,86 +502,211 @@ class SearchScreen extends HookConsumerWidget {
SliverToBoxAdapter(
child: ClipboardFillLink(controller: searchTextController),
),
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 {
if (showNoInputSections) ...[
HistoryHighlightsSection(
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) {
FeedArticleRoute(articleId: article.id).go(context);
},
),
RecentTabsSection(
onTabSelected: (tabId) async {
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,
),
.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 (context.mounted) {
ref
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
if (dialogResult == true) {
await ref
.read(startProxyControllerProvider.notifier)
.startProxy();
}
}
}
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 &&
result != SetContainerResult.failed) {
const TabViewRoute().go(context);
}
},
),
],
if (!showNoInputSections) ...[
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();
if (context.mounted) {
ref
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
const BrowserRoute().go(context);
}
},
),
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);
}
},
),
],
],
),
),
@@ -0,0 +1,60 @@
/*
* 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:weblibre/features/geckoview/features/search/domain/providers/search_modules_view.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/search_module_section.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
class ContainersSection extends StatelessWidget {
final void Function(ContainerDataWithCount container) onContainerSelected;
const ContainersSection({super.key, required this.onContainerSelected});
@override
Widget build(BuildContext context) {
return SearchModuleSection(
title: 'Containers',
moduleType: SearchModuleType.containers,
totalCount: 1,
contentSliverBuilder:
({required bool isCollapsed, required int visibleCount}) => [
if (!isCollapsed)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ContainerChips(
selectedContainer: null,
onSelected: (container) {
if (container != null) {
onContainerSelected(container);
}
},
onDeleted: null,
displayMenu: false,
showUnassignedChip: false,
enableDragAndDrop: false,
),
),
),
],
);
}
}
@@ -0,0 +1,64 @@
/*
* 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/empty_state_content.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_section.dart';
import 'package:weblibre/presentation/widgets/url_list_tile.dart';
class HistoryHighlightsSection extends ConsumerWidget {
final void Function(Uri uri) onUriSelected;
const HistoryHighlightsSection({super.key, required this.onUriSelected});
@override
Widget build(BuildContext context, WidgetRef ref) {
final highlightsAsync = ref.watch(searchEmptyHistoryHighlightsProvider());
final highlights = highlightsAsync.value ?? [];
if (highlightsAsync.hasValue && highlights.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return SearchModuleSection(
title: 'History Highlights',
moduleType: SearchModuleType.historyHighlights,
totalCount: highlights.length,
contentSliverBuilder:
({required bool isCollapsed, required int visibleCount}) => [
if (!isCollapsed)
SliverList.builder(
itemCount: visibleCount,
itemBuilder: (context, index) {
final highlight = highlights[index];
final uri = Uri.parse(highlight.url);
return UrlListTile(
title: highlight.title ?? uri.authority,
uri: uri,
onTap: () => onUriSelected(uri),
);
},
),
],
);
}
}
@@ -0,0 +1,109 @@
/*
* 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/core/providers/format.dart';
import 'package:weblibre/extensions/uri.dart';
import 'package:weblibre/features/geckoview/features/search/domain/providers/empty_state_content.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_section.dart';
import 'package:weblibre/features/web_feed/data/models/feed_article.dart';
import 'package:weblibre/features/web_feed/data/models/feed_link.dart';
import 'package:weblibre/features/web_feed/extensions/atom.dart';
import 'package:weblibre/features/web_feed/extensions/feed_article.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart';
class RecentFeedArticlesSection extends ConsumerWidget {
final void Function(FeedArticle article) onArticleSelected;
const RecentFeedArticlesSection({super.key, required this.onArticleSelected});
@override
Widget build(BuildContext context, WidgetRef ref) {
final articlesAsync = ref.watch(searchEmptyRecentFeedArticlesProvider());
final articles = articlesAsync.value ?? [];
if (articlesAsync.hasValue && articles.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return SearchModuleSection(
title: 'Recent Articles',
moduleType: SearchModuleType.recentArticles,
totalCount: articles.length,
contentSliverBuilder:
({required bool isCollapsed, required int visibleCount}) => [
if (!isCollapsed)
SliverList.builder(
itemCount: visibleCount,
itemBuilder: (context, index) {
final article = articles[index];
final articleDate = article.updated ?? article.created;
return ListTile(
leading: RepaintBoundary(
child: UrlIcon([
article.icon ??
article.links
?.getRelation(FeedLinkRelation.alternate)
?.uri ??
article.siteLink ??
article.feedId.base,
], iconSize: 24.0),
),
title: Text(
article.displayTitle,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (article.summaryPlain != null)
Text(
article.summaryPlain!,
style: Theme.of(context).textTheme.bodySmall,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
if (articleDate != null)
Align(
alignment: Alignment.topRight,
child: Text(
ref
.read(formatProvider.notifier)
.fullDateTime(articleDate),
style: Theme.of(context).textTheme.bodySmall
?.copyWith(fontStyle: FontStyle.italic),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
onTap: () => onArticleSelected(article),
);
},
),
],
);
}
}
@@ -0,0 +1,64 @@
/*
* 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/empty_state_content.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_section.dart';
import 'package:weblibre/presentation/widgets/url_list_tile.dart';
class RecentHistorySection extends ConsumerWidget {
final void Function(Uri uri) onUriSelected;
const RecentHistorySection({super.key, required this.onUriSelected});
@override
Widget build(BuildContext context, WidgetRef ref) {
final historyAsync = ref.watch(searchEmptyRecentHistoryProvider());
final visits = historyAsync.value ?? [];
if (historyAsync.hasValue && visits.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return SearchModuleSection(
title: 'Recent History',
moduleType: SearchModuleType.recentHistory,
totalCount: visits.length,
contentSliverBuilder:
({required bool isCollapsed, required int visibleCount}) => [
if (!isCollapsed)
SliverList.builder(
itemCount: visibleCount,
itemBuilder: (context, index) {
final visit = visits[index];
final uri = Uri.parse(visit.url);
return UrlListTile(
title: visit.title ?? uri.authority,
uri: uri,
onTap: () => onUriSelected(uri),
);
},
),
],
);
}
}
@@ -0,0 +1,68 @@
/*
* 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/browser/presentation/widgets/tab_icon.dart';
import 'package:weblibre/features/geckoview/features/search/domain/providers/empty_state_content.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_section.dart';
import 'package:weblibre/presentation/widgets/url_list_tile.dart';
class RecentTabsSection extends ConsumerWidget {
final void Function(String tabId) onTabSelected;
const RecentTabsSection({super.key, required this.onTabSelected});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabs = ref.watch(searchEmptyRecentTabsProvider());
if (tabs.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return SearchModuleSection(
title: 'Recent Tabs',
moduleType: SearchModuleType.recentTabs,
totalCount: tabs.length,
contentSliverBuilder:
({required bool isCollapsed, required int visibleCount}) => [
if (!isCollapsed)
SliverList.builder(
itemCount: visibleCount,
itemBuilder: (context, index) {
final (tabState, containerData) = tabs[index];
return UrlListTile(
title: tabState.titleOrAuthority,
uri: tabState.url,
leading: TabIcon(
tabState: tabState,
iconSize: UrlListTile.iconSize,
),
borderColor: containerData?.color,
onTap: () => onTabSelected(tabState.id),
);
},
),
],
);
}
}