💄 Optimize universal search screen
This commit is contained in:
@@ -191,6 +191,7 @@
|
|||||||
"statusActivityEndedTitle": "{} 正在 {} {} 直到 {}",
|
"statusActivityEndedTitle": "{} 正在 {} {} 直到 {}",
|
||||||
"appSettings": "应用设置",
|
"appSettings": "应用设置",
|
||||||
"accountSettings": "账号设置",
|
"accountSettings": "账号设置",
|
||||||
|
"accounts": "账号",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"accountLanguageHint": "此语言将用于电子邮件和推送通知。",
|
"accountLanguageHint": "此语言将用于电子邮件和推送通知。",
|
||||||
|
|||||||
@@ -39,10 +39,27 @@ class UniversalSearchScreen extends HookConsumerWidget {
|
|||||||
initialLength: 3,
|
initialLength: 3,
|
||||||
initialIndex: initialTab.index,
|
initialIndex: initialTab.index,
|
||||||
);
|
);
|
||||||
|
final searchQuery = useState<String>('');
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar: AppBar(title: Text('universalSearch'.tr()), elevation: 0),
|
appBar: AppBar(
|
||||||
|
title: SearchBar(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400, minHeight: 32),
|
||||||
|
hintText: 'search'.tr(),
|
||||||
|
hintStyle: WidgetStatePropertyAll(TextStyle(fontSize: 14)),
|
||||||
|
textStyle: WidgetStatePropertyAll(TextStyle(fontSize: 14)),
|
||||||
|
onChanged: (value) {
|
||||||
|
searchQuery.value = value;
|
||||||
|
},
|
||||||
|
leading: Icon(
|
||||||
|
Symbols.search,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
TabBar(
|
TabBar(
|
||||||
@@ -57,9 +74,9 @@ class UniversalSearchScreen extends HookConsumerWidget {
|
|||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
children: [
|
children: [
|
||||||
_PostsSearchTab(),
|
_PostsSearchTab(searchQuery: searchQuery),
|
||||||
_FediverseSearchTab(),
|
_AccountSearchTab(searchQuery: searchQuery),
|
||||||
_RealmsSearchTab(),
|
_RealmsSearchTab(searchQuery: searchQuery),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -70,72 +87,36 @@ class UniversalSearchScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RealmsSearchTab extends HookConsumerWidget {
|
class _RealmsSearchTab extends HookConsumerWidget {
|
||||||
const _RealmsSearchTab();
|
final ValueNotifier<String> searchQuery;
|
||||||
|
|
||||||
|
const _RealmsSearchTab({required this.searchQuery});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
Timer? debounceTimer;
|
|
||||||
final searchController = useTextEditingController();
|
|
||||||
final currentQuery = useState<String?>(null);
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
CustomScrollView(
|
CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
const SliverGap(88),
|
const SliverGap(8),
|
||||||
SliverRealmList(
|
SliverRealmList(
|
||||||
query: currentQuery.value,
|
query: searchQuery.value,
|
||||||
key: ValueKey(currentQuery.value),
|
key: ValueKey(searchQuery.value),
|
||||||
),
|
),
|
||||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: Center(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: SearchBar(
|
|
||||||
elevation: WidgetStateProperty.all(4),
|
|
||||||
controller: searchController,
|
|
||||||
hintText: 'search'.tr(),
|
|
||||||
leading: const Icon(Icons.search),
|
|
||||||
padding: WidgetStateProperty.all(
|
|
||||||
const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
if (debounceTimer?.isActive ?? false) {
|
|
||||||
debounceTimer?.cancel();
|
|
||||||
}
|
|
||||||
debounceTimer = Timer(
|
|
||||||
const Duration(milliseconds: 300),
|
|
||||||
() {
|
|
||||||
if (currentQuery.value != value) {
|
|
||||||
currentQuery.value = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PostsSearchTab extends HookConsumerWidget {
|
class _PostsSearchTab extends HookConsumerWidget {
|
||||||
const _PostsSearchTab();
|
final ValueNotifier<String> searchQuery;
|
||||||
|
|
||||||
|
const _PostsSearchTab({required this.searchQuery});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final searchController = useTextEditingController();
|
|
||||||
final debounce = useMemoized(() => Duration(milliseconds: 500));
|
final debounce = useMemoized(() => Duration(milliseconds: 500));
|
||||||
final debounceTimer = useRef<Timer?>(null);
|
final debounceTimer = useRef<Timer?>(null);
|
||||||
final showFilters = useState(false);
|
final showFilters = useState(false);
|
||||||
@@ -151,7 +132,6 @@ class _PostsSearchTab extends HookConsumerWidget {
|
|||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
return () {
|
return () {
|
||||||
searchController.dispose();
|
|
||||||
pubNameController.dispose();
|
pubNameController.dispose();
|
||||||
realmController.dispose();
|
realmController.dispose();
|
||||||
debounceTimer.value?.cancel();
|
debounceTimer.value?.cancel();
|
||||||
@@ -188,6 +168,18 @@ class _PostsSearchTab extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen to search query changes and update the search
|
||||||
|
useEffect(() {
|
||||||
|
final query = searchQuery.value;
|
||||||
|
if (query.isNotEmpty) {
|
||||||
|
// Use Future.delayed to defer the provider modification
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
onSearchChanged(query, skipDebounce: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [searchQuery.value]);
|
||||||
|
|
||||||
return Consumer(
|
return Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final searchState = ref.watch(
|
final searchState = ref.watch(
|
||||||
@@ -203,28 +195,7 @@ class _PostsSearchTab extends HookConsumerWidget {
|
|||||||
onRefresh: noti.refresh,
|
onRefresh: noti.refresh,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverGap(16),
|
SliverGap(4),
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
),
|
|
||||||
child: SearchBar(
|
|
||||||
elevation: WidgetStateProperty.all(4),
|
|
||||||
controller: searchController,
|
|
||||||
hintText: 'search'.tr(),
|
|
||||||
leading: const Icon(Icons.search),
|
|
||||||
padding: WidgetStateProperty.all(
|
|
||||||
const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
),
|
|
||||||
onChanged: onSearchChanged,
|
|
||||||
onSubmitted: (value) {
|
|
||||||
onSearchChanged(value, skipDebounce: true);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SliverGap(12),
|
|
||||||
PaginationList(
|
PaginationList(
|
||||||
provider: postListProvider(
|
provider: postListProvider(
|
||||||
PostListQueryConfig(id: kSearchPostListId),
|
PostListQueryConfig(id: kSearchPostListId),
|
||||||
@@ -256,14 +227,14 @@ class _PostsSearchTab extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (searchState.value?.items.isEmpty == true &&
|
if (searchState.value?.items.isEmpty == true &&
|
||||||
searchController.text.isNotEmpty &&
|
searchQuery.value.isNotEmpty &&
|
||||||
!searchState.isLoading)
|
!searchState.isLoading)
|
||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
child: Center(child: Text('noResultsFound'.tr())),
|
child: Center(child: Text('noResultsFound'.tr())),
|
||||||
),
|
),
|
||||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
).padding(left: 8),
|
).padding(left: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
@@ -271,10 +242,11 @@ class _PostsSearchTab extends HookConsumerWidget {
|
|||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(right: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Gap(16),
|
Gap(8),
|
||||||
Card(
|
Card(
|
||||||
margin: EdgeInsets.symmetric(horizontal: 8),
|
margin: EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -319,28 +291,6 @@ class _PostsSearchTab extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: CustomScrollView(
|
: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
const SliverGap(4),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
child: SearchBar(
|
|
||||||
elevation: WidgetStateProperty.all(4),
|
|
||||||
controller: searchController,
|
|
||||||
hintText: 'search'.tr(),
|
|
||||||
leading: const Icon(Icons.search),
|
|
||||||
padding: WidgetStateProperty.all(
|
|
||||||
const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
),
|
|
||||||
onChanged: onSearchChanged,
|
|
||||||
onSubmitted: (value) {
|
|
||||||
onSearchChanged(value, skipDebounce: true);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SliverGap(8),
|
const SliverGap(8),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -396,25 +346,17 @@ class _PostsSearchTab extends HookConsumerWidget {
|
|||||||
child: const PostItemSkeleton(maxWidth: double.infinity),
|
child: const PostItemSkeleton(maxWidth: double.infinity),
|
||||||
),
|
),
|
||||||
itemBuilder: (context, index, post) {
|
itemBuilder: (context, index, post) {
|
||||||
return Center(
|
return Card(
|
||||||
child: ConstrainedBox(
|
margin: EdgeInsets.symmetric(
|
||||||
constraints: BoxConstraints(maxWidth: 600),
|
horizontal: 8,
|
||||||
child: Card(
|
vertical: 4,
|
||||||
margin: EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
child: PostActionableItem(
|
|
||||||
item: post,
|
|
||||||
borderRadius: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
child: PostActionableItem(item: post, borderRadius: 8),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (searchState.value?.items.isEmpty == true &&
|
if (searchState.value?.items.isEmpty == true &&
|
||||||
searchController.text.isNotEmpty &&
|
searchQuery.value.isNotEmpty &&
|
||||||
!searchState.isLoading)
|
!searchState.isLoading)
|
||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
child: Center(child: Text('noResultsFound'.tr())),
|
child: Center(child: Text('noResultsFound'.tr())),
|
||||||
@@ -426,12 +368,13 @@ class _PostsSearchTab extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FediverseSearchTab extends HookConsumerWidget {
|
class _AccountSearchTab extends HookConsumerWidget {
|
||||||
const _FediverseSearchTab();
|
final ValueNotifier<String> searchQuery;
|
||||||
|
|
||||||
|
const _AccountSearchTab({required this.searchQuery});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final searchController = useTextEditingController();
|
|
||||||
final debounce = useMemoized(() => const Duration(milliseconds: 500));
|
final debounce = useMemoized(() => const Duration(milliseconds: 500));
|
||||||
final debounceTimer = useRef<Timer?>(null);
|
final debounceTimer = useRef<Timer?>(null);
|
||||||
final fediverseResults = useState<List<SnActivityPubActor>>([]);
|
final fediverseResults = useState<List<SnActivityPubActor>>([]);
|
||||||
@@ -440,7 +383,6 @@ class _FediverseSearchTab extends HookConsumerWidget {
|
|||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
return () {
|
return () {
|
||||||
searchController.dispose();
|
|
||||||
debounceTimer.value?.cancel();
|
debounceTimer.value?.cancel();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -531,6 +473,18 @@ class _FediverseSearchTab extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen to search query changes and update the search
|
||||||
|
useEffect(() {
|
||||||
|
final query = searchQuery.value;
|
||||||
|
if (query.isNotEmpty) {
|
||||||
|
// Use Future.delayed to defer the provider modification
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
onSearchChanged(query);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [searchQuery.value]);
|
||||||
|
|
||||||
// Combine and display results - local users first
|
// Combine and display results - local users first
|
||||||
final allResults = [
|
final allResults = [
|
||||||
...internalResults.value.map(
|
...internalResults.value.map(
|
||||||
@@ -543,19 +497,6 @@ class _FediverseSearchTab extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: SearchBar(
|
|
||||||
controller: searchController,
|
|
||||||
hintText: 'searchAccountsHint'.tr(),
|
|
||||||
leading: const Icon(Symbols.search).padding(horizontal: 24),
|
|
||||||
onChanged: onSearchChanged,
|
|
||||||
onSubmitted: (value) {
|
|
||||||
onSearchChanged(value);
|
|
||||||
performSearch(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: isSearching.value
|
child: isSearching.value
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
@@ -569,8 +510,8 @@ class _FediverseSearchTab extends HookConsumerWidget {
|
|||||||
size: 64,
|
size: 64,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const Gap(16),
|
||||||
if (searchController.text.isEmpty)
|
if (searchQuery.value.isEmpty)
|
||||||
Text(
|
Text(
|
||||||
'searchUsersEmpty'.tr(),
|
'searchUsersEmpty'.tr(),
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
@@ -584,7 +525,7 @@ class _FediverseSearchTab extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: ExtendedRefreshIndicator(
|
: ExtendedRefreshIndicator(
|
||||||
onRefresh: () => performSearch(searchController.text),
|
onRefresh: () => performSearch(searchQuery.value),
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
itemCount: allResults.length,
|
itemCount: allResults.length,
|
||||||
|
|||||||
Reference in New Issue
Block a user