import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dio/dio.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:veloe_kemono_party_flutter/models/creator.dart'; import 'package:veloe_kemono_party_flutter/models/post.dart'; import 'package:veloe_kemono_party_flutter/pages/posts_screen.dart'; import 'package:veloe_kemono_party_flutter/pages/widgets/post_container.dart'; import 'dart:convert'; // Providers final searchQueryProvider = StateProvider((ref) => ''); final selectedCreatorProvider = StateProvider((ref) => null); final postsPageProvider = StateProvider((ref) => 0); final kemonoClientProvider = Provider((ref) => KemonoClient()); final creatorsProvider = FutureProvider>((ref) async { final query = ref.watch(searchQueryProvider); final client = ref.read(kemonoClientProvider); final creators = await client.getCreators(); return creators.where((creator) => creator.name.toLowerCase().contains(query.toLowerCase()) || creator.service.toLowerCase().contains(query.toLowerCase()) ).toList(); }); final postsProvider = StateNotifierProvider.autoDispose .family>, (String, String)>((ref, args) { final (creatorId, service) = args; return PostsNotifier( creatorId: creatorId, service: service, client: ref.read(kemonoClientProvider), ); }); class PostsNotifier extends StateNotifier>> { final String creatorId; final String service; final KemonoClient client; int _currentPage = 0; PostsNotifier({ required this.creatorId, required this.service, required this.client, }) : super(const AsyncValue.loading()) { loadInitial(); } Future loadInitial() async { state = const AsyncValue.loading(); try { final posts = await client.getPosts(creatorId, service, 0); state = AsyncValue.data(posts); _currentPage = 1; } catch (e) { state = AsyncValue.error(e, StackTrace.current); } } Future loadNext() async { if (state.isLoading) return; final previousPosts = state.valueOrNull ?? []; // Add explicit type parameter state = AsyncValue>.loading().copyWithPrevious(state); try { final newPosts = await client.getPosts( creatorId, service, _currentPage * 50 ); print('Loading page $_currentPage with offset ${_currentPage * 20}'); state = AsyncValue.data([...previousPosts, ...newPosts]); _currentPage++; } catch (e) { // Specify type for error state state = AsyncValue>.error(e, StackTrace.current) .copyWithPrevious(AsyncValue.data(previousPosts)); } } } // API Client class KemonoClient { final Dio _dio = Dio(BaseOptions(baseUrl: 'https://kemono.su/')); Future> getCreators() async { try { final response = await _dio.get('/api/v1/creators.txt'); if (response.statusCode != 200) { throw DioException( requestOptions: response.requestOptions, response: response, error: 'Unexpected status code: ${response.statusCode}' ); } final parsedData = switch(response.data) { String s => jsonDecode(s), List list => list, Map map => [map], _ => throw FormatException('Unprocessable data') }; return (parsedData as List).map((x) => Creator.fromJson(x)).toList(); } on DioException catch (e) { throw Exception('Network error: ${e.message}'); } on FormatException catch (e) { throw Exception('Data format error: ${e.message}'); } } Future> getPosts(String creatorId, String service, int start) async { try { final response = await _dio.get( '/api/v1/$service/user/$creatorId', queryParameters: {'o': start}, ); final parsedData = switch(response.data) { String s => jsonDecode(s), List list => list, Map map => [map], _ => throw FormatException('Unprocessable data') }; return (parsedData as List).map((x) => Post.fromJson(x)).toList(); } on DioException catch (e) { throw Exception('Posts load failed: ${e.message}'); } on Exception catch (e) { throw Exception('Oops something go wrong}'); } } } // Image Caching final imageCacheManager = CacheManager( Config( 'kemono_images', stalePeriod: const Duration(days: 30), maxNrOfCacheObjects: 1024, fileService: HttpFileService(), ), ); class ResizedImage extends StatelessWidget { final String imageUrl; final BoxFit fit; const ResizedImage({super.key, required this.imageUrl, this.fit = BoxFit.cover}); @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; return CachedNetworkImage( cacheManager: imageCacheManager, imageUrl: imageUrl, imageBuilder: (context, imageProvider) => Image( image: ResizeImage( imageProvider, width: screenSize.width.toInt(), ), fit: fit, ), placeholder: (context, url) => const CircularProgressIndicator(), errorWidget: (context, url, error) => const Icon(Icons.error), ); } } // UI Components class CreatorCard extends ConsumerWidget { final Creator creator; const CreatorCard({super.key, required this.creator}); @override Widget build(BuildContext context, WidgetRef ref) { return Card( child: ListTile( leading: Hero( tag: 'creator-${creator.id}', child: CircleAvatar( backgroundColor: Colors.grey[200], child: ResizedImage( imageUrl: creator.iconUrl, fit: BoxFit.contain, ), ), ), title: Text(creator.name), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(creator.service.toUpperCase()), Text('Last updated: ${_formatDate(creator.updated)}'), ], ), onTap: () => _showCreatorPosts(context, ref), ), ); } String _formatDate(int timestamp) { return DateTime.fromMillisecondsSinceEpoch((timestamp * 1000).toInt()).toString(); } void _showCreatorPosts(BuildContext context, WidgetRef ref) { ref.read(selectedCreatorProvider.notifier).state = creator; Navigator.push( context, MaterialPageRoute( builder: (context) => PostsScreen( creatorId: creator.id, service: creator.service, ), ), ); } } // Main Screens class HomeScreen extends ConsumerStatefulWidget { const HomeScreen({super.key}); @override ConsumerState createState() => _HomeScreenState(); } class _HomeScreenState extends ConsumerState { final TextEditingController _searchController = TextEditingController(); final _searchQuery = StateProvider((ref) => ''); // Added search state // Modified provider with search filtering final filteredCreatorsProvider = FutureProvider.autoDispose.family, String>( (ref, query) async { final originalList = await ref.watch(creatorsProvider.future); return originalList.where( (creator) => creator.name.toLowerCase().contains(query.toLowerCase()) ).toList(); }, ); @override Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( appBar: AppBar( title: _buildSearchField(), // Replaced title with search field actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: () => ref.invalidate(filteredCreatorsProvider), ), ], bottom: const TabBar( tabs: [ Tab(icon: Icon(Icons.people_outline), text: 'Creators'), Tab(icon: Icon(Icons.grid_view), text: 'Posts'), ], ), ), body: TabBarView( children: [ _buildCreatorsList(ref.watch( filteredCreatorsProvider(ref.watch(_searchQuery)) // Added query param )), _buildGlobalPostsGrid(), ], ), ), ); } // New search field widget Widget _buildSearchField() { return TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Search creators...', border: InputBorder.none, suffixIcon: IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); ref.read(_searchQuery.notifier).state = ''; }, ), ), onChanged: (value) => ref.read(_searchQuery.notifier).state = value, ); } // Updated creators list with search Widget _buildCreatorsList(AsyncValue> creatorsAsync) { return RefreshIndicator( onRefresh: () => ref.refresh(filteredCreatorsProvider(ref.read(_searchQuery)).future), child: creatorsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) => Center(child: Text(error.toString())), data: (creators) => creators.isEmpty ? const Center(child: Text('No matching creators found')) : ListView.separated( padding: const EdgeInsets.all(16), itemCount: creators.length, separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, index) => CreatorCard(creator: creators[index]), ), ), ); } Widget _buildGlobalPostsGrid() { final postsAsync = ref.watch(postsProvider(('82522', 'patreon'))); return postsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) => Center(child: Text(error.toString())), data: (posts) => CustomScrollView( slivers: [ SliverAppBar(), SliverList( delegate: SliverChildBuilderDelegate( (context, index) => PostContainer(post: posts[index]), childCount: posts.length, ), ), ], ), ); } } void main() => runApp( const ProviderScope( child: MaterialApp( home: HomeScreen(), ), ), );