improved search ui
This commit is contained in:
@@ -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(),
|
||||
|
||||
+1
-1
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+21
-20
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+74
-50
@@ -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],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+20
-21
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+72
-16
@@ -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
-29
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user