Search post

This commit is contained in:
LittleSheep 2025-07-02 00:40:35 +08:00
parent a8efd26262
commit 6f9de431b1
5 changed files with 251 additions and 50 deletions

View File

@ -676,5 +676,6 @@
"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.",
"learnMore": "Learn More",
"discoverWebArticles": "Articles from external sites"
"discoverWebArticles": "Articles from external sites",
"webArticlesStand": "Article Stand"
}

View File

@ -40,31 +40,31 @@ PODS:
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/CoreOnly (11.13.0):
- FirebaseCore (~> 11.13.0)
- Firebase/Messaging (11.13.0):
- Firebase/CoreOnly (11.15.0):
- FirebaseCore (~> 11.15.0)
- Firebase/Messaging (11.15.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.13.0)
- firebase_core (3.14.0):
- Firebase/CoreOnly (= 11.13.0)
- FirebaseMessaging (~> 11.15.0)
- firebase_core (3.15.0):
- Firebase/CoreOnly (= 11.15.0)
- Flutter
- firebase_messaging (15.2.7):
- Firebase/Messaging (= 11.13.0)
- firebase_messaging (15.2.8):
- Firebase/Messaging (= 11.15.0)
- firebase_core
- Flutter
- FirebaseCore (11.13.0):
- FirebaseCoreInternal (~> 11.13.0)
- FirebaseCore (11.15.0):
- FirebaseCoreInternal (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (11.13.0):
- FirebaseCoreInternal (11.15.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (11.13.0):
- FirebaseCore (~> 11.13.0)
- FirebaseInstallations (11.15.0):
- FirebaseCore (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.13.0):
- FirebaseCore (~> 11.13.0)
- FirebaseMessaging (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
@ -128,8 +128,8 @@ PODS:
- Flutter
- irondash_engine_context (0.0.1):
- Flutter
- Kingfisher (8.3.2)
- livekit_client (2.4.8):
- Kingfisher (8.3.3)
- livekit_client (2.4.9):
- Flutter
- flutter_webrtc
- WebRTC-SDK (= 125.6422.07)
@ -351,13 +351,13 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: 3435bc66b4d494c2f22c79fd3aae4c1db6662327
firebase_core: 700bac7ed92bb754fd70fbf01d72b36ecdd6d450
firebase_messaging: 860c017fcfbb5e27c163062d1d3135388f3ef954
FirebaseCore: c692c7f1c75305ab6aff2b367f25e11d73aa8bd0
FirebaseCoreInternal: 29d7b3af4aaf0b8f3ed20b568c13df399b06f68c
FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02
FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: c727a02c560a53f1f1e56e18f16515eb5753c492
firebase_messaging: 4158969b04b667f5435731ec9d6e453bb58b0c4c
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
@ -371,8 +371,8 @@ SPEC CHECKSUMS:
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
Kingfisher: 0621d0ac0c78fecb19f6dc5303bde2b52abaf2f5
livekit_client: 9e901890552514206e5ff828903ed271531da264
Kingfisher: ff82cb91d9266ddb56cbb2f72d32c26f00d3e5be
livekit_client: 3f79d79233a5bd13d5b541732624ef959d7c538e
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474

View File

@ -5,6 +5,7 @@ import 'package:island/screens/developers/apps.dart';
import 'package:island/screens/developers/edit_app.dart';
import 'package:island/screens/developers/new_app.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/screens/tabs.dart';
import 'package:island/screens/explore.dart';
@ -263,6 +264,10 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/',
builder: (context, state) => const ExploreScreen(),
),
GoRoute(
path: '/posts/search',
builder: (context, state) => const PostSearchScreen(),
),
GoRoute(
path: '/posts/:id',
builder: (context, state) {

View File

@ -93,37 +93,67 @@ class ExploreScreen extends HookConsumerWidget {
extendBody: false, // Prevent conflicts with tabs navigation
appBar: AppBar(
toolbarHeight: 0,
bottom: TabBar(
controller: tabController,
tabs: [
Tab(
child: Text(
'explore'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: Row(
children: [
Expanded(
child: TabBar(
controller: tabController,
tabAlignment: TabAlignment.start,
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!,
),
),
),
],
),
),
),
Tab(
child: Text(
'exploreFilterSubscriptions'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
Spacer(),
IconButton(
onPressed: () {},
icon: Icon(
Symbols.auto_stories,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
tooltip: 'webArticlesStand'.tr(),
),
),
Tab(
child: Text(
'exploreFilterFriends'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
IconButton(
onPressed: () {
context.push('/posts/search');
},
icon: Icon(
Symbols.search,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
tooltip: 'search'.tr(),
),
),
],
],
).padding(horizontal: 8),
),
),
floatingActionButton: FloatingActionButton(

View 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')),
);
},
),
);
}
}