Browse Source

added posts search

Veloe 3 months ago
parent
commit
f3ee8525bd

+ 3 - 2
lib/kemono_client.dart

@@ -33,11 +33,12 @@ class KemonoClient {
 }
   }
 
-  Future<List<Post>> getPosts(String creatorId, String service, int start) async {
+  Future<List<Post>> getPosts(String creatorId, String service, int start, String query) async {
     try {
+
       final response = await _dio.get(
         '/api/v1/$service/user/$creatorId',
-        queryParameters: {'o': start},
+        queryParameters: query.isEmpty ? {'o': start} : {'o':start, 'q': query},
       );
 
       final parsedData = switch(response.data) {

+ 4 - 3
lib/main.dart

@@ -29,11 +29,12 @@ final creatorsProvider = FutureProvider<List<Creator>>((ref) async {
 });
 
 final postsProvider = StateNotifierProvider.autoDispose
-    .family<PostsNotifier, AsyncValue<List<Post>>, (String, String)>((ref, args) {
-  final (creatorId, service) = args;
+    .family<PostsNotifier, AsyncValue<List<Post>>, (String, String, String)>((ref, args) {
+  final (creatorId, service, query) = args;
   return PostsNotifier(
     creatorId: creatorId,
     service: service,
+    query: query,
     client: ref.read(kemonoClientProvider),
   );
 });
@@ -216,7 +217,7 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
   }
 
   Widget _buildGlobalPostsGrid() {
-    final postsAsync = ref.watch(postsProvider(('82522', 'patreon')));
+    final postsAsync = ref.watch(postsProvider(('82522', 'patreon','')));
 
     return postsAsync.when(
       loading: () => const Center(child: CircularProgressIndicator()),

+ 72 - 15
lib/pages/posts_screen.dart

@@ -1,8 +1,30 @@
+import 'dart:async';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:veloe_kemono_party_flutter/main.dart';
 import 'package:veloe_kemono_party_flutter/pages/widgets/post_container.dart';
 
+final searchQueryProvider = StateProvider.autoDispose<String>((ref) => '');
+final debouncedSearchQueryProvider = Provider.autoDispose<String>((ref) {
+  final query = ref.watch(searchQueryProvider);
+  final debouncer = Debouncer(delay: const Duration(milliseconds: 500));
+  debouncer.run(() {});
+  return query;
+});
+
+// Add Debouncer class
+class Debouncer {
+  final Duration delay;
+  Timer? _timer;
+
+  Debouncer({required this.delay});
+
+  void run(VoidCallback action) {
+    _timer?.cancel();
+    _timer = Timer(delay, action);
+  }
+}
+
 class PostsScreen extends ConsumerStatefulWidget {
   final String creatorId;
   final String service;
@@ -20,27 +42,41 @@ class PostsScreen extends ConsumerStatefulWidget {
 }
 
 class _PostsScreenState extends ConsumerState<PostsScreen> with PaginationMixin {
+ late final TextEditingController _searchController;
+
   @override
   void initState() {
     super.initState();
+    _searchController = TextEditingController();
     Future.microtask(() => ref.read(postsProvider((
-      widget.creatorId, // Direct access now valid
-      widget.service
+      widget.creatorId,
+      widget.service,
+      ref.read(searchQueryProvider) // Initial query
     )).notifier).loadInitial());
   }
 
   @override
-  Widget build(BuildContext context) {
-    final postsAsync = ref.watch(postsProvider((widget.creatorId, widget.service)));
+  void dispose() {
+    _searchController.dispose();
+    super.dispose();
+  }
 
+  @override
+  Widget build(BuildContext context) {
+    final currentQuery = ref.watch(debouncedSearchQueryProvider);
+    final postsAsync = ref.watch(postsProvider((
+      widget.creatorId,
+      widget.service,
+      currentQuery
+    )));
     return Scaffold(
-      appBar: widget.withAppBar ? AppBar(
-        title: const Text('Posts'),
-        leading: IconButton(
+      appBar:  AppBar(
+        title: _buildSearchField(),
+        leading: widget.withAppBar ? IconButton(
           icon: const Icon(Icons.arrow_back),
           onPressed: () => Navigator.pop(context),
-        ),
-      ) : null,
+        ) : null,
+      ),
       body: postsAsync.when(
         loading: () => const Center(child: CircularProgressIndicator()),
         error: (error, _) => Center(
@@ -49,7 +85,7 @@ class _PostsScreenState extends ConsumerState<PostsScreen> with PaginationMixin
             children: [
               Text('Error: ${error.toString()}'),
               ElevatedButton(
-                onPressed: () => ref.invalidate(postsProvider((widget.creatorId, widget.service))),
+                onPressed: () => ref.invalidate(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider)))),
                 child: const Text('Retry'),
               )
             ],
@@ -84,8 +120,25 @@ class _PostsScreenState extends ConsumerState<PostsScreen> with PaginationMixin
     );
   }
 
+  Widget _buildSearchField() {
+    return TextField(
+      controller: _searchController,
+      decoration: InputDecoration(
+        hintText: 'Search posts...',
+        suffixIcon: IconButton(
+          icon: const Icon(Icons.clear),
+          onPressed: () {
+            _searchController.clear();
+            ref.read(searchQueryProvider.notifier).state = '';
+          },
+        ),
+      ),
+      onChanged: (value) => ref.read(searchQueryProvider.notifier).state = value,
+    );
+  }
+
   Widget _buildLoadMoreIndicator() {
-    return ref.watch(postsProvider((widget.creatorId, widget.service))).maybeWhen(
+    return ref.watch(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider)))).maybeWhen(
           loading: () => const Padding(
             padding: EdgeInsets.all(16),
             child: Center(child: CircularProgressIndicator()),
@@ -94,7 +147,7 @@ class _PostsScreenState extends ConsumerState<PostsScreen> with PaginationMixin
             children: [
               Text('Load more failed: ${error.toString()}'),
               ElevatedButton(
-                onPressed: () => ref.read(postsProvider((widget.creatorId, widget.service)).notifier).loadNext(),
+                onPressed: () => ref.read(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider))).notifier).loadNext(),
                 child: const Text('Retry'),
               )
             ],
@@ -124,20 +177,24 @@ mixin PaginationMixin<T extends ConsumerStatefulWidget> on ConsumerState<T> {
   PostsScreen get postsWidget => widget as PostsScreen;
 
   Future<void> _loadNextPage() async {
-    if (_isLoading) return;
+    if (_isLoading || !mounted) return;
     _isLoading = true;
     
     final notifier = ref.read(postsProvider((
       postsWidget.creatorId, // Use converted widget reference
-      postsWidget.service
+      postsWidget.service,
+      ref.read(searchQueryProvider)
     )).notifier);
-    
+
+    if (!notifier.mounted) return;
+
     await notifier.loadNext();
     _isLoading = false;
   }
 
   @override
   void dispose() {
+    _scrollController.removeListener(_scrollListener);
     _scrollController.dispose();
     super.dispose();
   }

+ 0 - 4
lib/pages/widgets/post_container.dart

@@ -64,10 +64,6 @@ class _PostHeader extends StatelessWidget {
                         fontWeight: FontWeight.w600,
                       ),
                 ),
-                Text(
-                  post.published,
-                  style: Theme.of(context).textTheme.bodySmall,
-                ),
               ],
             ),
           ),

+ 23 - 1
lib/pages/widgets/post_detail_view.dart

@@ -67,7 +67,9 @@ class PostDetailView extends StatelessWidget {
 
   Widget _buildImageColumn(List<Attachment> attachments) {
     return Column(
-      children: attachments.map((attachment) {
+      children: attachments
+        .where((attachment) => _isImageFile(attachment.link))
+        .map((attachment) {
         return Padding(
           padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
           child: SmartImageContainer(imageUrl: attachment.link),
@@ -76,6 +78,26 @@ class PostDetailView extends StatelessWidget {
     );
   }
 
+  bool _isImageFile(String url) {
+  try {
+    final uri = Uri.parse(url);
+    final path = uri.path.toLowerCase();
+    
+    // Handle URLs with query parameters or fragments
+    final cleanPath = path.split('?').first.split('#').first;
+    
+    // Supported image extensions
+    const imageExtensions = {
+      'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg',
+      'tiff', 'heic', 'heif' // Added modern formats
+    };
+    
+    return imageExtensions.any((ext) => cleanPath.endsWith('.$ext'));
+  } catch (e) {
+    return false; // Invalid URL format
+  }
+}
+
   Widget _buildHtmlContent() {
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),

+ 58 - 32
lib/pages/widgets/smart_image_container.dart

@@ -1,44 +1,64 @@
+import 'dart:io';
 import 'package:flutter/material.dart';
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter_cache_manager/flutter_cache_manager.dart';
+import '../../main.dart';
 
 class SmartImageContainer extends StatefulWidget {
   final String imageUrl;
 
-  const SmartImageContainer({required this.imageUrl});
+  const SmartImageContainer({super.key, required this.imageUrl});
 
   @override
-  State<SmartImageContainer> createState() => SmartImageContainerState();
+  State<SmartImageContainer> createState() => _SmartImageContainerState();
 }
 
-class SmartImageContainerState extends State<SmartImageContainer> {
+class _SmartImageContainerState extends State<SmartImageContainer> {
   Size? _imageSize;
   ImageStream? _imageStream;
+  ImageStreamListener? _imageListener;
 
   @override
   void initState() {
     super.initState();
-    _loadImageDimensions();
+    _loadCachedImageDimensions();
   }
 
-  void _loadImageDimensions() async {
-    final Image image = Image.network(widget.imageUrl);
-    _imageStream = image.image.resolve(ImageConfiguration.empty);
+  void _loadCachedImageDimensions() async {
+  try {
+    final response = await imageCacheManager.getFileFromCache(widget.imageUrl);
     
-    _imageStream!.addListener(
-      ImageStreamListener((ImageInfo info, _) {
-        if (!mounted) return;
-        setState(() {
-          _imageSize = Size(
-            info.image.width.toDouble(),
-            info.image.height.toDouble(),
-          );
-        });
-      }),
-    );
+    if (response is FileInfo && mounted) {
+      final image = Image.file(File(response.file.path));
+      _processImage(image);
+    }
+    else if (response is DownloadProgress) {
+      // Handle download progress if needed
+    }
+  } catch (e) {
+    if (mounted) {
+      setState(() => _imageSize = null);
+    }
   }
+}
+
+void _processImage(Image image) {
+  _imageStream = image.image.resolve(ImageConfiguration.empty);
+  _imageListener = ImageStreamListener((ImageInfo info, _) {
+    if (!mounted) return;
+    setState(() {
+      _imageSize = Size(
+        info.image.width.toDouble(),
+        info.image.height.toDouble(),
+      );
+    });
+  });
+  _imageStream!.addListener(_imageListener!);
+}
 
   @override
   void dispose() {
-    //_imageStream?.removeListener(ImageStreamListener(_updateSize));
+    _imageStream?.removeListener(_imageListener!);
     super.dispose();
   }
 
@@ -51,13 +71,10 @@ class SmartImageContainerState extends State<SmartImageContainer> {
         double calculatedHeight = maxWidth;
 
         if (_imageSize != null) {
-          // Auto-sizing logic
           if (_imageSize!.width > maxWidth) {
-            // Scale down large images
             final ratio = _imageSize!.height / _imageSize!.width;
             calculatedHeight = maxWidth * ratio;
           } else {
-            // Keep original size for smaller images
             calculatedWidth = _imageSize!.width;
             calculatedHeight = _imageSize!.height;
           }
@@ -67,15 +84,14 @@ class SmartImageContainerState extends State<SmartImageContainer> {
           duration: const Duration(milliseconds: 300),
           width: calculatedWidth,
           height: calculatedHeight,
-          alignment: Alignment.center,
-          child: _imageSize != null
-              ? Image.network(
-                  widget.imageUrl,
-                  fit: BoxFit.contain,
-                  width: calculatedWidth,
-                  height: calculatedHeight,
-                )
-              : _buildLoadingPlaceholder(maxWidth),
+          child: CachedNetworkImage(
+            cacheManager: imageCacheManager,
+            imageUrl: widget.imageUrl,
+            fit: BoxFit.contain,
+            placeholder: (context, url) => _buildLoadingPlaceholder(maxWidth),
+            errorWidget: (context, url, error) => 
+                _buildErrorPlaceholder(maxWidth),
+          ),
         );
       },
     );
@@ -84,8 +100,18 @@ class SmartImageContainerState extends State<SmartImageContainer> {
   Widget _buildLoadingPlaceholder(double maxWidth) {
     return Container(
       width: maxWidth,
-      height: maxWidth * 0.75, // Default 4:3 ratio
+      height: maxWidth * 0.75,
       color: Colors.grey[200],
+      child: const Center(child: CircularProgressIndicator()),
+    );
+  }
+
+  Widget _buildErrorPlaceholder(double maxWidth) {
+    return Container(
+      width: maxWidth,
+      height: maxWidth * 0.75,
+      color: Colors.red[100],
+      child: const Icon(Icons.error_outline, color: Colors.red),
     );
   }
 }

+ 12 - 6
lib/posts_notifier.dart

@@ -5,21 +5,29 @@ import 'package:veloe_kemono_party_flutter/models/post.dart';
 class PostsNotifier extends StateNotifier<AsyncValue<List<Post>>> {
   final String creatorId;
   final String service;
+  final String query; // Add query parameter
   final KemonoClient client;
   int _currentPage = 0;
 
   PostsNotifier({
     required this.creatorId,
     required this.service,
+    required this.query, 
     required this.client,
   }) : super(const AsyncValue.loading()) {
     loadInitial();
   }
 
   Future<void> loadInitial() async {
+    if (!mounted) return;
     state = const AsyncValue.loading();
     try {
-      final posts = await client.getPosts(creatorId, service, 0);
+      final posts = await client.getPosts(
+        creatorId, 
+        service, 
+        _currentPage * 50,
+        query // Pass query to client
+      );
       state = AsyncValue.data(posts);
       _currentPage = 1;
     } catch (e) {
@@ -28,23 +36,21 @@ class PostsNotifier extends StateNotifier<AsyncValue<List<Post>>> {
   }
 
   Future<void> loadNext() async {
-    if (state.isLoading) return;
+    if (!mounted ||state.isLoading) return;
     
     final previousPosts = state.valueOrNull ?? [];
-    // Add explicit type parameter
     state = AsyncValue<List<Post>>.loading().copyWithPrevious(state);
     
     try {
       final newPosts = await client.getPosts(
         creatorId, 
         service, 
-        _currentPage * 50
+        _currentPage * 50,
+        query 
       );
-      print('Loading page $_currentPage with offset ${_currentPage * 20}');
       state = AsyncValue.data([...previousPosts, ...newPosts]);
       _currentPage++;
     } catch (e) {
-      // Specify type for error state
       state = AsyncValue<List<Post>>.error(e, StackTrace.current)
         .copyWithPrevious(AsyncValue.data(previousPosts));
     }