✨ Search post
This commit is contained in:
parent
a8efd26262
commit
6f9de431b1
@ -676,5 +676,6 @@
|
|||||||
"publisherFeatureDevelopDescription": "Unlock development abilities for your publisher, including custom apps, API keys, and more.",
|
"publisherFeatureDevelopDescription": "Unlock development abilities for your publisher, including custom apps, API keys, and more.",
|
||||||
"publisherFeatureDevelopHint": "Currently, this feature is under active development, you need send a request to unlock this feature.",
|
"publisherFeatureDevelopHint": "Currently, this feature is under active development, you need send a request to unlock this feature.",
|
||||||
"learnMore": "Learn More",
|
"learnMore": "Learn More",
|
||||||
"discoverWebArticles": "Articles from external sites"
|
"discoverWebArticles": "Articles from external sites",
|
||||||
|
"webArticlesStand": "Article Stand"
|
||||||
}
|
}
|
||||||
|
@ -40,31 +40,31 @@ PODS:
|
|||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
- DKImagePickerController/PhotoGallery
|
- DKImagePickerController/PhotoGallery
|
||||||
- Flutter
|
- Flutter
|
||||||
- Firebase/CoreOnly (11.13.0):
|
- Firebase/CoreOnly (11.15.0):
|
||||||
- FirebaseCore (~> 11.13.0)
|
- FirebaseCore (~> 11.15.0)
|
||||||
- Firebase/Messaging (11.13.0):
|
- Firebase/Messaging (11.15.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 11.13.0)
|
- FirebaseMessaging (~> 11.15.0)
|
||||||
- firebase_core (3.14.0):
|
- firebase_core (3.15.0):
|
||||||
- Firebase/CoreOnly (= 11.13.0)
|
- Firebase/CoreOnly (= 11.15.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_messaging (15.2.7):
|
- firebase_messaging (15.2.8):
|
||||||
- Firebase/Messaging (= 11.13.0)
|
- Firebase/Messaging (= 11.15.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- FirebaseCore (11.13.0):
|
- FirebaseCore (11.15.0):
|
||||||
- FirebaseCoreInternal (~> 11.13.0)
|
- FirebaseCoreInternal (~> 11.15.0)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/Logger (~> 8.1)
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
- FirebaseCoreInternal (11.13.0):
|
- FirebaseCoreInternal (11.15.0):
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- FirebaseInstallations (11.13.0):
|
- FirebaseInstallations (11.15.0):
|
||||||
- FirebaseCore (~> 11.13.0)
|
- FirebaseCore (~> 11.15.0)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseMessaging (11.13.0):
|
- FirebaseMessaging (11.15.0):
|
||||||
- FirebaseCore (~> 11.13.0)
|
- FirebaseCore (~> 11.15.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleDataTransport (~> 10.0)
|
- GoogleDataTransport (~> 10.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
@ -128,8 +128,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- irondash_engine_context (0.0.1):
|
- irondash_engine_context (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.3.2)
|
- Kingfisher (8.3.3)
|
||||||
- livekit_client (2.4.8):
|
- livekit_client (2.4.9):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- WebRTC-SDK (= 125.6422.07)
|
- WebRTC-SDK (= 125.6422.07)
|
||||||
@ -351,13 +351,13 @@ SPEC CHECKSUMS:
|
|||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
Firebase: 3435bc66b4d494c2f22c79fd3aae4c1db6662327
|
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
|
||||||
firebase_core: 700bac7ed92bb754fd70fbf01d72b36ecdd6d450
|
firebase_core: c727a02c560a53f1f1e56e18f16515eb5753c492
|
||||||
firebase_messaging: 860c017fcfbb5e27c163062d1d3135388f3ef954
|
firebase_messaging: 4158969b04b667f5435731ec9d6e453bb58b0c4c
|
||||||
FirebaseCore: c692c7f1c75305ab6aff2b367f25e11d73aa8bd0
|
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
|
||||||
FirebaseCoreInternal: 29d7b3af4aaf0b8f3ed20b568c13df399b06f68c
|
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
|
||||||
FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02
|
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
|
||||||
FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4
|
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
@ -371,8 +371,8 @@ SPEC CHECKSUMS:
|
|||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||||
Kingfisher: 0621d0ac0c78fecb19f6dc5303bde2b52abaf2f5
|
Kingfisher: ff82cb91d9266ddb56cbb2f72d32c26f00d3e5be
|
||||||
livekit_client: 9e901890552514206e5ff828903ed271531da264
|
livekit_client: 3f79d79233a5bd13d5b541732624ef959d7c538e
|
||||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||||
|
@ -5,6 +5,7 @@ import 'package:island/screens/developers/apps.dart';
|
|||||||
import 'package:island/screens/developers/edit_app.dart';
|
import 'package:island/screens/developers/edit_app.dart';
|
||||||
import 'package:island/screens/developers/new_app.dart';
|
import 'package:island/screens/developers/new_app.dart';
|
||||||
import 'package:island/screens/developers/hub.dart';
|
import 'package:island/screens/developers/hub.dart';
|
||||||
|
import 'package:island/screens/posts/post_search.dart';
|
||||||
import 'package:island/widgets/app_wrapper.dart';
|
import 'package:island/widgets/app_wrapper.dart';
|
||||||
import 'package:island/screens/tabs.dart';
|
import 'package:island/screens/tabs.dart';
|
||||||
import 'package:island/screens/explore.dart';
|
import 'package:island/screens/explore.dart';
|
||||||
@ -263,6 +264,10 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
path: '/',
|
path: '/',
|
||||||
builder: (context, state) => const ExploreScreen(),
|
builder: (context, state) => const ExploreScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/posts/search',
|
||||||
|
builder: (context, state) => const PostSearchScreen(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/posts/:id',
|
path: '/posts/:id',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
@ -93,37 +93,67 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
extendBody: false, // Prevent conflicts with tabs navigation
|
extendBody: false, // Prevent conflicts with tabs navigation
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
toolbarHeight: 0,
|
toolbarHeight: 0,
|
||||||
bottom: TabBar(
|
bottom: PreferredSize(
|
||||||
controller: tabController,
|
preferredSize: const Size.fromHeight(48),
|
||||||
tabs: [
|
child: Row(
|
||||||
Tab(
|
children: [
|
||||||
child: Text(
|
Expanded(
|
||||||
'explore'.tr(),
|
child: TabBar(
|
||||||
textAlign: TextAlign.center,
|
controller: tabController,
|
||||||
style: TextStyle(
|
tabAlignment: TabAlignment.start,
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
isScrollable: true,
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
icon: Tooltip(
|
||||||
|
message: 'explore'.tr(),
|
||||||
|
child: Icon(
|
||||||
|
Symbols.explore,
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
icon: Tooltip(
|
||||||
|
message: 'exploreFilterSubscriptions'.tr(),
|
||||||
|
child: Icon(
|
||||||
|
Symbols.subscriptions,
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
icon: Tooltip(
|
||||||
|
message: 'exploreFilterFriends'.tr(),
|
||||||
|
child: Icon(
|
||||||
|
Symbols.people,
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Spacer(),
|
||||||
Tab(
|
IconButton(
|
||||||
child: Text(
|
onPressed: () {},
|
||||||
'exploreFilterSubscriptions'.tr(),
|
icon: Icon(
|
||||||
textAlign: TextAlign.center,
|
Symbols.auto_stories,
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
),
|
),
|
||||||
|
tooltip: 'webArticlesStand'.tr(),
|
||||||
),
|
),
|
||||||
),
|
IconButton(
|
||||||
Tab(
|
onPressed: () {
|
||||||
child: Text(
|
context.push('/posts/search');
|
||||||
'exploreFilterFriends'.tr(),
|
},
|
||||||
textAlign: TextAlign.center,
|
icon: Icon(
|
||||||
style: TextStyle(
|
Symbols.search,
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
),
|
),
|
||||||
|
tooltip: 'search'.tr(),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
).padding(horizontal: 8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
|
165
lib/screens/posts/post_search.dart
Normal file
165
lib/screens/posts/post_search.dart
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/post.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/widgets/post/post_item.dart';
|
||||||
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||||
|
|
||||||
|
final postSearchNotifierProvider = StateNotifierProvider.autoDispose<
|
||||||
|
PostSearchNotifier,
|
||||||
|
AsyncValue<CursorPagingData<SnPost>>
|
||||||
|
>((ref) => PostSearchNotifier(ref));
|
||||||
|
|
||||||
|
class PostSearchNotifier
|
||||||
|
extends StateNotifier<AsyncValue<CursorPagingData<SnPost>>> {
|
||||||
|
final AutoDisposeRef ref;
|
||||||
|
static const int _pageSize = 20;
|
||||||
|
String _currentQuery = '';
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
PostSearchNotifier(this.ref) : super(const AsyncValue.loading()) {
|
||||||
|
state = const AsyncValue.data(
|
||||||
|
CursorPagingData(items: [], hasMore: false, nextCursor: null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> search(String query) async {
|
||||||
|
if (_isLoading) return;
|
||||||
|
|
||||||
|
_currentQuery = query.trim();
|
||||||
|
if (_currentQuery.isEmpty) {
|
||||||
|
state = AsyncValue.data(
|
||||||
|
CursorPagingData(items: [], hasMore: false, nextCursor: null),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch(cursor: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetch({String? cursor}) async {
|
||||||
|
if (_isLoading) return;
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||||
|
|
||||||
|
final response = await client.get(
|
||||||
|
'/posts/search',
|
||||||
|
queryParameters: {
|
||||||
|
'query': _currentQuery,
|
||||||
|
'offset': offset,
|
||||||
|
'take': _pageSize,
|
||||||
|
'useVector': true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final data = response.data as List;
|
||||||
|
final posts = data.map((json) => SnPost.fromJson(json)).toList();
|
||||||
|
final hasMore = posts.length == _pageSize;
|
||||||
|
final nextCursor = hasMore ? (offset + posts.length).toString() : null;
|
||||||
|
|
||||||
|
state = AsyncValue.data(
|
||||||
|
CursorPagingData(
|
||||||
|
items: posts,
|
||||||
|
hasMore: hasMore,
|
||||||
|
nextCursor: nextCursor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e, stack) {
|
||||||
|
state = AsyncValue.error(e, stack);
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostSearchScreen extends ConsumerStatefulWidget {
|
||||||
|
const PostSearchScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<PostSearchScreen> createState() => _PostSearchScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostSearchScreenState extends ConsumerState<PostSearchScreen> {
|
||||||
|
final _searchController = TextEditingController();
|
||||||
|
final _debounce = Duration(milliseconds: 500);
|
||||||
|
Timer? _debounceTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
_debounceTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSearchChanged(String query) {
|
||||||
|
if (_debounceTimer?.isActive ?? false) _debounceTimer!.cancel();
|
||||||
|
|
||||||
|
_debounceTimer = Timer(_debounce, () {
|
||||||
|
ref.read(postSearchNotifierProvider.notifier).search(query);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search posts...',
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||||
|
),
|
||||||
|
onChanged: _onSearchChanged,
|
||||||
|
onSubmitted: (value) {
|
||||||
|
ref.read(postSearchNotifierProvider.notifier).search(value);
|
||||||
|
},
|
||||||
|
autofocus: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final searchState = ref.watch(postSearchNotifierProvider);
|
||||||
|
|
||||||
|
return searchState.when(
|
||||||
|
data: (data) {
|
||||||
|
if (data.items.isEmpty && _searchController.text.isNotEmpty) {
|
||||||
|
return const Center(child: Text('No results found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: data.items.length + (data.hasMore ? 1 : 0),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index >= data.items.length) {
|
||||||
|
ref
|
||||||
|
.read(postSearchNotifierProvider.notifier)
|
||||||
|
.fetch(cursor: data.nextCursor);
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
final post = data.items[index];
|
||||||
|
return Column(
|
||||||
|
children: [PostItem(item: post), const Divider(height: 1)],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user