improved search ui

This commit is contained in:
Fabian Freund
2025-12-28 19:25:00 +01:00
parent 3aa1843a5e
commit eb6ad1f517
12 changed files with 395 additions and 306 deletions
@@ -45,7 +45,6 @@ class SiteSearch extends HookConsumerWidget {
final List<BangData> availableBangs;
final TextEditingController? controller;
final bool searchInNewTab;
final Widget? label;
const SiteSearch({
required this.domain,
@@ -53,7 +52,6 @@ class SiteSearch extends HookConsumerWidget {
super.key,
this.controller,
this.searchInNewTab = false,
this.label,
});
@override
@@ -119,6 +117,7 @@ class SiteSearch extends HookConsumerWidget {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Visibility(
visible: availableBangs.isNotEmpty,
@@ -169,7 +168,9 @@ class SiteSearch extends HookConsumerWidget {
maxLines: null,
activeBang: selectedBang,
showSuggestions: true,
label: label,
label: (selectedBang != null)
? const Text('Search')
: const Text('Address / Search'),
onTap: () {
if (!searchFocusNode.hasFocus) {
// Select all text when the field is tapped
@@ -188,6 +189,7 @@ class SiteSearch extends HookConsumerWidget {
searchTextController: searchTextController,
activeBang: selectedBang,
submitSearch: submitSearch,
limit: 25,
),
],
);
@@ -289,7 +289,7 @@ EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
}
@Riverpod()
EquatableValue<List<TabPreview>> seamlessFilteredTabPreviews(
EquatableValue<List<TabPreview>> filteredTabPreviews(
Ref ref,
TabSearchPartition searchPartition,
ContainerFilter containerFilter,
@@ -307,21 +307,7 @@ EquatableValue<List<TabPreview>> seamlessFilteredTabPreviews(
);
if (tabSearchResults == null) {
return EquatableValue(
availableTabStates.value.values
.map(
(state) => TabPreview(
id: state.id,
title: state.title,
icon: state.icon,
url: state.url,
highlightedUrl: null,
content: null,
sourceSearchQuery: null,
),
)
.toList(),
);
return EquatableValue([]);
}
return EquatableValue(
@@ -330,6 +316,7 @@ EquatableValue<List<TabPreview>> seamlessFilteredTabPreviews(
.map((tab) {
return TabPreview(
id: tab.id,
containerId: tab.containerId,
title: tab.title ?? availableTabStates.value[tab.id]!.title,
icon: null,
url: tab.cleanUrl ?? availableTabStates.value[tab.id]!.url,
@@ -640,11 +640,10 @@ final class SeamlessFilteredTabEntitiesFamily extends $Family
String toString() => r'seamlessFilteredTabEntitiesProvider';
}
@ProviderFor(seamlessFilteredTabPreviews)
const seamlessFilteredTabPreviewsProvider =
SeamlessFilteredTabPreviewsFamily._();
@ProviderFor(filteredTabPreviews)
const filteredTabPreviewsProvider = FilteredTabPreviewsFamily._();
final class SeamlessFilteredTabPreviewsProvider
final class FilteredTabPreviewsProvider
extends
$FunctionalProvider<
EquatableValue<List<TabPreview>>,
@@ -652,23 +651,23 @@ final class SeamlessFilteredTabPreviewsProvider
EquatableValue<List<TabPreview>>
>
with $Provider<EquatableValue<List<TabPreview>>> {
const SeamlessFilteredTabPreviewsProvider._({
required SeamlessFilteredTabPreviewsFamily super.from,
const FilteredTabPreviewsProvider._({
required FilteredTabPreviewsFamily super.from,
required (TabSearchPartition, ContainerFilter) super.argument,
}) : super(
retry: null,
name: r'seamlessFilteredTabPreviewsProvider',
name: r'filteredTabPreviewsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$seamlessFilteredTabPreviewsHash();
String debugGetCreateSourceHash() => _$filteredTabPreviewsHash();
@override
String toString() {
return r'seamlessFilteredTabPreviewsProvider'
return r'filteredTabPreviewsProvider'
''
'$argument';
}
@@ -682,7 +681,7 @@ final class SeamlessFilteredTabPreviewsProvider
@override
EquatableValue<List<TabPreview>> create(Ref ref) {
final argument = this.argument as (TabSearchPartition, ContainerFilter);
return seamlessFilteredTabPreviews(ref, argument.$1, argument.$2);
return filteredTabPreviews(ref, argument.$1, argument.$2);
}
/// {@macro riverpod.override_with_value}
@@ -697,8 +696,7 @@ final class SeamlessFilteredTabPreviewsProvider
@override
bool operator ==(Object other) {
return other is SeamlessFilteredTabPreviewsProvider &&
other.argument == argument;
return other is FilteredTabPreviewsProvider && other.argument == argument;
}
@override
@@ -707,32 +705,32 @@ final class SeamlessFilteredTabPreviewsProvider
}
}
String _$seamlessFilteredTabPreviewsHash() =>
r'ee1d441ca5f5822e201de8fc0b264ea7b8db0776';
String _$filteredTabPreviewsHash() =>
r'f2958369b4270f4c8165526c0ffad22f41ec0491';
final class SeamlessFilteredTabPreviewsFamily extends $Family
final class FilteredTabPreviewsFamily extends $Family
with
$FunctionalFamilyOverride<
EquatableValue<List<TabPreview>>,
(TabSearchPartition, ContainerFilter)
> {
const SeamlessFilteredTabPreviewsFamily._()
const FilteredTabPreviewsFamily._()
: super(
retry: null,
name: r'seamlessFilteredTabPreviewsProvider',
name: r'filteredTabPreviewsProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
SeamlessFilteredTabPreviewsProvider call(
FilteredTabPreviewsProvider call(
TabSearchPartition searchPartition,
ContainerFilter containerFilter,
) => SeamlessFilteredTabPreviewsProvider._(
) => FilteredTabPreviewsProvider._(
argument: (searchPartition, containerFilter),
from: this,
);
@override
String toString() => r'seamlessFilteredTabPreviewsProvider';
String toString() => r'filteredTabPreviewsProvider';
}
@@ -203,7 +203,6 @@ class ViewTabSheetWidget extends HookConsumerWidget {
domain: initialTabState.url.host,
availableBangs: availableBangs,
controller: searchTextController,
label: const Text('Address'),
),
),
const Divider(),
@@ -266,7 +266,7 @@ class ListTabPreview extends HookConsumerWidget {
decoration: BoxDecoration(
color: tabState.isPrivate ? const Color(0xFF25003E) : null,
border: isActive ? Border.all(color: colorScheme.primary) : null,
// borderRadius: const BorderRadius.all(Radius.circular(16.0)),
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
),
child: Row(
children: [
@@ -22,6 +22,7 @@ import 'package:weblibre/domain/entities/equatable_image.dart';
class TabPreview with FastEquatable {
final String id;
final String? containerId;
final String title;
final EquatableImage? icon;
@@ -35,6 +36,7 @@ class TabPreview with FastEquatable {
TabPreview({
required this.id,
required this.containerId,
required this.title,
required this.icon,
required this.url,
@@ -46,6 +48,7 @@ class TabPreview with FastEquatable {
@override
List<Object?> get hashParameters => [
id,
containerId,
title,
icon,
url,
@@ -17,6 +17,7 @@
* 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:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
@@ -125,142 +126,152 @@ class SearchScreen extends HookConsumerWidget {
}
return Scaffold(
body: Form(
key: formKey,
child: CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
pinned: true,
automaticallyImplyLeading: false,
title: Align(
child: Focus(
canRequestFocus: false,
child: SegmentedButton(
showSelectedIcon: false,
segments: [
const ButtonSegment(
value: TabType.regular,
label: Text('Regular'),
icon: Icon(MdiIcons.tab),
),
const ButtonSegment(
value: TabType.private,
label: Text('Private'),
icon: Icon(MdiIcons.tabUnselected),
),
if (createChildTabsOption)
const ButtonSegment(
value: TabType.child,
label: Text('Child'),
icon: Icon(MdiIcons.fileTree),
body: SafeArea(
child: Form(
key: formKey,
child: FadingScroll(
builder: (context, controller) {
return CustomScrollView(
controller: controller,
slivers: [
SliverAppBar(
floating: true,
pinned: true,
automaticallyImplyLeading: false,
title: Align(
child: Focus(
canRequestFocus: false,
child: SegmentedButton(
showSelectedIcon: false,
segments: [
const ButtonSegment(
value: TabType.regular,
label: Text('Regular'),
icon: Icon(MdiIcons.tab),
),
const ButtonSegment(
value: TabType.private,
label: Text('Private'),
icon: Icon(MdiIcons.tabUnselected),
),
if (createChildTabsOption)
const ButtonSegment(
value: TabType.child,
label: Text('Child'),
icon: Icon(MdiIcons.fileTree),
),
],
selected: {selectedTabType.value},
onSelectionChanged: (value) {
selectedTabType.value = value.first;
// Restore focus to search field after segment change
WidgetsBinding.instance.addPostFrameCallback((_) {
searchFocusNode.requestFocus();
});
},
style: switch (selectedTabType.value) {
TabType.regular => null,
TabType.private => SegmentedButton.styleFrom(
selectedBackgroundColor: const Color(0x648000D7),
),
TabType.child =>
(currentTabTabType == TabType.private)
? SegmentedButton.styleFrom(
selectedBackgroundColor: const Color(
0x648000D7,
),
)
: null,
},
),
],
selected: {selectedTabType.value},
onSelectionChanged: (value) {
selectedTabType.value = value.first;
// Restore focus to search field after segment change
WidgetsBinding.instance.addPostFrameCallback((_) {
searchFocusNode.requestFocus();
});
},
style: switch (selectedTabType.value) {
TabType.regular => null,
TabType.private => SegmentedButton.styleFrom(
selectedBackgroundColor: const Color(0x648000D7),
),
TabType.child =>
(currentTabTabType == TabType.private)
? SegmentedButton.styleFrom(
selectedBackgroundColor: const Color(
0x648000D7,
),
)
: null,
},
),
),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: SearchField(
showBangIcon: showBangIcon.value,
textEditingController: searchTextController,
focusNode: searchFocusNode,
autofocus: true,
label: const Text('Address / Search'),
onSubmitted: (value) async {
if (value.isNotEmpty) {
var newUrl = uri_parser.tryParseUrl(
value,
eagerParsing: true,
);
if (newUrl == null) {
final bang =
ref.read(selectedBangDataProvider()) ??
await ref.read(
defaultSearchBangDataProvider.future,
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: SearchField(
showBangIcon: showBangIcon.value,
textEditingController: searchTextController,
focusNode: searchFocusNode,
autofocus: true,
label: const Text('Address / Search'),
onSubmitted: (value) async {
if (value.isNotEmpty) {
var newUrl = uri_parser.tryParseUrl(
value,
eagerParsing: true,
);
if (bang != null) {
newUrl = bang.getTemplateUrl(value);
if (newUrl == null) {
final bang =
ref.read(selectedBangDataProvider()) ??
await ref.read(
defaultSearchBangDataProvider.future,
);
if (!privateTabMode) {
await ref
.read(bangSearchProvider.notifier)
.triggerBangSearch(bang, value);
if (bang != null) {
newUrl = bang.getTemplateUrl(value);
if (!privateTabMode) {
await ref
.read(bangSearchProvider.notifier)
.triggerBangSearch(bang, value);
}
}
}
if (newUrl != null) {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: newUrl,
private: privateTabMode,
parentId:
(selectedTabType.value ==
TabType.child)
? ref.read(selectedTabProvider)
: null,
launchedFromIntent: launchedFromIntent,
);
if (context.mounted) {
ref
.read(
bottomSheetControllerProvider.notifier,
)
.requestDismiss();
const BrowserRoute().go(context);
}
}
}
}
}
if (newUrl != null) {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: newUrl,
private: privateTabMode,
parentId:
(selectedTabType.value == TabType.child)
? ref.read(selectedTabProvider)
: null,
launchedFromIntent: launchedFromIntent,
);
if (context.mounted) {
ref
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
const BrowserRoute().go(context);
}
}
}
},
activeBang: activeBang,
showSuggestions: true,
},
activeBang: activeBang,
showSuggestions: true,
),
),
),
),
),
),
),
SliverToBoxAdapter(
child: ClipboardFillLink(controller: searchTextController),
),
const SliverToBoxAdapter(child: Divider()),
FullSearchTermSuggestions(
searchTextController: searchTextController,
activeBang: activeBang,
submitSearch: submitSearch,
),
TabSearch(searchTextListenable: sampledSearchText),
FeedSearch(searchTextNotifier: sampledSearchText),
HistorySuggestions(
isPrivate: privateTabMode,
searchTextListenable: sampledSearchText,
),
],
SliverToBoxAdapter(
child: ClipboardFillLink(controller: searchTextController),
),
const SliverToBoxAdapter(child: Divider()),
FullSearchTermSuggestions(
searchTextController: searchTextController,
activeBang: activeBang,
submitSearch: submitSearch,
),
TabSearch(searchTextListenable: sampledSearchText),
FeedSearch(searchTextNotifier: sampledSearchText),
HistorySuggestions(
isPrivate: privateTabMode,
searchTextListenable: sampledSearchText,
),
],
);
},
),
),
),
);
@@ -32,13 +32,13 @@ class FixedSearchTermSuggestions extends HookConsumerWidget {
final TextEditingController searchTextController;
final Future<void> Function(String query) submitSearch;
final BangData? activeBang;
final int count;
final int limit;
const FixedSearchTermSuggestions({
required this.searchTextController,
required this.submitSearch,
required this.activeBang,
this.count = 3,
required this.limit,
super.key,
});
@@ -76,27 +76,28 @@ class FixedSearchTermSuggestions extends HookConsumerWidget {
.whereNot(
(suggestion) => suggestion == searchTextController.text,
)
.take(count),
.take(limit),
]
: <String>[];
return Column(
mainAxisSize: MainAxisSize.min,
children: prioritizedSuggestions
.map(
(suggestion) => ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.search),
title: Text(suggestion),
onLongPress: () {
searchTextController.text = suggestion;
},
onTap: () async {
await submitSearch(suggestion);
},
),
)
.toList(),
return Wrap(
spacing: 8.0,
children: prioritizedSuggestions.map((query) {
return InkWell(
onLongPress: () {
searchTextController.text = query;
},
child: InputChip(
// avatar: const Icon(Icons.search),
label: Text(query),
onSelected: (value) async {
if (value) {
await submitSearch(query);
}
},
),
);
}).toList(),
);
}
}
@@ -61,35 +61,45 @@ class FullSearchTermSuggestions extends HookConsumerWidget {
.addQuery(searchTextController.text);
});
final Widget listSliver;
final MultiSliver listSliver;
if (!searchTextIsNotEmpty && (searchHistory.value.isNotEmpty)) {
final entries = searchHistory.value!;
listSliver = SliverList.builder(
itemCount: entries.length,
itemBuilder: (context, index) {
final query = entries[index].searchQuery;
listSliver = MultiSliver(
children: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Wrap(
spacing: 8.0,
children: entries.map((entry) {
final query = entry.searchQuery;
return ListTile(
leading: const Icon(Icons.history),
title: Text(query),
onLongPress: () {
searchTextController.text = query;
},
onTap: () async {
await submitSearch(query);
},
trailing: IconButton(
onPressed: () async {
await ref
.read(bangDataRepositoryProvider.notifier)
.removeSearchEntry(query);
},
icon: const Icon(Icons.close),
return InkWell(
onLongPress: () {
searchTextController.text = query;
},
child: InputChip(
avatar: const Icon(Icons.history),
label: Text(query),
onSelected: (value) async {
if (value) {
await submitSearch(query);
}
},
onDeleted: () async {
await ref
.read(bangDataRepositoryProvider.notifier)
.removeSearchEntry(query);
},
),
);
}).toList(),
),
),
);
},
),
],
);
} else {
final prioritizedSuggestions = [
@@ -100,22 +110,33 @@ class FullSearchTermSuggestions extends HookConsumerWidget {
),
];
listSliver = SliverList.builder(
itemCount: prioritizedSuggestions.length,
itemBuilder: (context, index) {
final suggestion = prioritizedSuggestions[index];
return ListTile(
leading: const Icon(Icons.search),
title: Text(suggestion),
onLongPress: () {
searchTextController.text = suggestion;
},
onTap: () async {
await submitSearch(suggestion);
},
);
},
listSliver = MultiSliver(
children: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Wrap(
spacing: 8.0,
children: prioritizedSuggestions.map((query) {
return InkWell(
onLongPress: () {
searchTextController.text = query;
},
child: InputChip(
// avatar: const Icon(Icons.search),
label: Text(query),
onSelected: (value) async {
if (value) {
await submitSearch(query);
}
},
),
);
}).toList(),
),
),
),
],
);
}
@@ -167,17 +188,20 @@ class FullSearchTermSuggestions extends HookConsumerWidget {
),
),
SliverToBoxAdapter(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 150),
child: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
return CustomScrollView(
shrinkWrap: true,
controller: controller,
slivers: [listSliver],
);
},
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 150),
child: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
return CustomScrollView(
shrinkWrap: true,
controller: controller,
slivers: [listSliver],
);
},
),
),
),
),
@@ -33,6 +33,7 @@ import 'package:weblibre/features/geckoview/utils/image_helper.dart';
import 'package:weblibre/presentation/hooks/cached_future.dart';
import 'package:weblibre/presentation/hooks/listenable_callback.dart';
import 'package:weblibre/presentation/widgets/failure_widget.dart';
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
class HistorySuggestions extends HookConsumerWidget {
final bool isPrivate;
@@ -82,6 +83,7 @@ class HistorySuggestions extends HookConsumerWidget {
itemCount: historySuggestions.length,
itemBuilder: (context, index) {
final suggestion = historySuggestions[index];
final uri = suggestion.description.mapNotNull(Uri.tryParse);
return HookBuilder(
key: ValueKey(suggestion.id),
@@ -107,30 +109,27 @@ class HistorySuggestions extends HookConsumerWidget {
title: suggestion.title.mapNotNull(
(title) => Text(title),
),
subtitle: suggestion.description.mapNotNull(
(description) => Text(
description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
subtitle:
uri.mapNotNull((uri) => UriBreadcrumb(uri: uri)) ??
suggestion.description.mapNotNull(
(description) => Text(
description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
onTap: () async {
if (suggestion.description != null) {
if (Uri.tryParse(suggestion.description!)
case final Uri url) {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: url, private: isPrivate);
if (uri != null) {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: uri, private: isPrivate);
if (context.mounted) {
ref
.read(
bottomSheetControllerProvider.notifier,
)
.requestDismiss();
if (context.mounted) {
ref
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
const BrowserRoute().go(context);
}
const BrowserRoute().go(context);
}
}
},
@@ -17,6 +17,10 @@
* 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:async';
import 'package:collection/collection.dart';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -33,6 +37,7 @@ import 'package:weblibre/features/geckoview/features/find_in_page/presentation/c
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
import 'package:weblibre/presentation/hooks/listenable_callback.dart';
@@ -53,15 +58,53 @@ class TabSearch extends HookConsumerWidget {
ref.read(selectedContainerDataProvider.select((value) => value.value)),
);
final tabs = ref
.watch(
seamlessFilteredTabPreviewsProvider(
TabSearchPartition.search,
// ignore: provider_parameters
ContainerFilterById(containerId: selectedContainer.value?.id),
),
)
.value;
final tabSearchResults = ref.watch(
filteredTabPreviewsProvider(
TabSearchPartition.search,
// ignore: provider_parameters
ContainerFilterDisabled(),
),
);
final containerIdsWithResults = useMemoized(
() => EquatableValue(
tabSearchResults.value
.map((result) => result.containerId)
.fold(
<String?, int>{},
(map, id) =>
map..update(id, (value) => ++value, ifAbsent: () => 1),
),
),
[tabSearchResults],
);
final filteredTabs = useMemoized(
() => tabSearchResults.value
.where((result) => result.containerId == selectedContainer.value?.id)
.toList(),
[tabSearchResults, selectedContainer.value?.id],
);
unawaited(
useValueChanged(containerIdsWithResults, (_, _) async {
if (containerIdsWithResults.value.isNotEmpty &&
!containerIdsWithResults.value.containsKey(
selectedContainer.value?.id,
)) {
selectedContainer.value = await ref
.read(containerRepositoryProvider.notifier)
.getAllContainersWithCount()
.then((containers) {
return containers.firstWhereOrNull(
(container) =>
!container.metadata.authSettings.authenticationRequired &&
containerIdsWithResults.value.containsKey(container.id),
);
});
}
}),
);
useListenableCallback(searchTextListenable, () async {
if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.search))) {
@@ -79,6 +122,10 @@ class TabSearch extends HookConsumerWidget {
}
});
if (tabSearchResults.value.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return MultiSliver(
children: [
const SliverToBoxAdapter(child: Divider()),
@@ -92,6 +139,9 @@ class TabSearch extends HookConsumerWidget {
ContainerChips(
displayMenu: false,
selectedContainer: selectedContainer.value,
showUnassignedChip: containerIdsWithResults.value.containsKey(
null,
),
onSelected: (container) async {
if (container != null) {
if (await ref
@@ -106,7 +156,10 @@ class TabSearch extends HookConsumerWidget {
onDeleted: (container) {
selectedContainer.value = null;
},
containerFilter: (container) => (container.tabCount ?? 0) > 0,
containerFilter: (container) =>
containerIdsWithResults.value.containsKey(container.id),
containerBadgeCount: (container) =>
containerIdsWithResults.value[container?.id] ?? 0,
searchTextListenable: searchTextListenable,
),
],
@@ -114,10 +167,11 @@ class TabSearch extends HookConsumerWidget {
),
),
SliverList.builder(
itemCount: tabs.length,
itemCount: filteredTabs.length,
itemBuilder: (context, index) {
final result = tabs[index];
final result = filteredTabs[index];
final titleHasMatch = result.title.contains(_matchPrefix);
final urlHasMatch =
result.highlightedUrl?.contains(_matchPrefix) ?? false;
final bodyHasMatch =
@@ -142,11 +196,13 @@ class TabSearch extends HookConsumerWidget {
),
),
),
subtitle: (bodyHasMatch || urlHasMatch)
subtitle: (bodyHasMatch || (urlHasMatch && !titleHasMatch))
? MarkdownBody(
data: bodyHasMatch
? result.content!
: result.highlightedUrl!,
data:
(bodyHasMatch
? result.content!
: result.highlightedUrl!)
.replaceAll(RegExp(r'\s+'), ' '),
styleSheet: MarkdownStyleSheet(
p: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
@@ -38,10 +38,12 @@ import 'package:weblibre/presentation/widgets/selectable_chips.dart';
class ContainerChips extends HookConsumerWidget {
final bool displayMenu;
final bool showUnassignedChip;
final bool showGroupSuggestions;
final ContainerData? selectedContainer;
final bool Function(ContainerDataWithCount)? containerFilter;
final int Function(ContainerDataWithCount?)? containerBadgeCount;
final void Function(ContainerDataWithCount?)? onSelected;
final void Function(ContainerDataWithCount)? onDeleted;
final void Function(ContainerDataWithCount)? onLongPress;
@@ -54,8 +56,10 @@ class ContainerChips extends HookConsumerWidget {
required this.onDeleted,
this.onLongPress,
this.containerFilter,
this.containerBadgeCount,
this.searchTextListenable,
this.displayMenu = true,
this.showUnassignedChip = true,
this.showGroupSuggestions = false,
});
@@ -97,7 +101,9 @@ class ContainerChips extends HookConsumerWidget {
container.color.withValues(alpha: 0.33),
itemLabel: (container) =>
ContainerTitle(container: container),
itemBadgeCount: (container) => container.tabCount,
itemBadgeCount: (container) =>
containerBadgeCount?.call(container) ??
container.tabCount,
itemWrap: (child, container) {
return TabDragContainerTarget(
container: container,
@@ -105,36 +111,39 @@ class ContainerChips extends HookConsumerWidget {
);
},
prefixListItems: [
TabDragContainerTarget(
container: null,
child: Consumer(
builder: (context, ref, child) {
final tabCount = ref.watch(
containerTabCountProvider(
// ignore: provider_parameters
ContainerFilterById(containerId: null),
).select((value) => value.value ?? 0),
);
if (showUnassignedChip)
TabDragContainerTarget(
container: null,
child: Consumer(
builder: (context, ref, child) {
final int tabCount =
containerBadgeCount?.call(null) ??
ref.watch(
containerTabCountProvider(
// ignore: provider_parameters
ContainerFilterById(containerId: null),
).select((value) => value.value ?? 0),
);
return FilterChip(
avatar: const Icon(MdiIcons.folderHidden),
labelPadding: (tabCount > 0)
? null
: const EdgeInsets.only(right: 2.0),
label: (tabCount > 0)
? Text(tabCount.toString())
: const SizedBox.shrink(),
selected: selectedContainer == null,
showCheckmark: false,
onSelected: (value) {
if (value) {
onSelected?.call(null);
}
},
);
},
return FilterChip(
avatar: const Icon(MdiIcons.folderHidden),
labelPadding: (tabCount > 0)
? null
: const EdgeInsets.only(right: 2.0),
label: (tabCount > 0)
? Text(tabCount.toString())
: const SizedBox.shrink(),
selected: selectedContainer == null,
showCheckmark: false,
onSelected: (value) {
if (value) {
onSelected?.call(null);
}
},
);
},
),
),
),
if (showGroupSuggestions)
Consumer(
builder: (context, ref, child) {