Browse Source

added source change, cache stats and cleaning

Veloe 3 months ago
parent
commit
9901fcf04e

+ 2 - 2
lib/app_layout.dart

@@ -33,8 +33,8 @@ class _AppLayoutState extends ConsumerState<AppLayout> {
       child: ListView(
         children: [
           const DrawerHeader(
-            decoration: BoxDecoration(color: Colors.blue),
-            child: Text('Navigation', style: TextStyle(color: Colors.white)),
+            decoration: BoxDecoration(color: Color.fromARGB(255,40, 42, 46)),
+            child: Text('Kemono', style: TextStyle(color: Colors.white)),
           ),
           ...widget.navItems.asMap().entries.map((entry) => ListTile(
             leading: Icon(entry.value.icon),

+ 7 - 3
lib/kemono_client.dart

@@ -4,7 +4,11 @@ import 'package:veloe_kemono_party_flutter/models/creator.dart';
 import 'package:veloe_kemono_party_flutter/models/post.dart';
 
 class KemonoClient {
-  final Dio _dio = Dio(BaseOptions(baseUrl: 'https://kemono.su/'));
+  KemonoClient(String sourceUrl) :_dio = Dio(BaseOptions(baseUrl: sourceUrl));
+
+  final Dio _dio;
+  
+  String sourceUrl() => _dio.options.baseUrl;
 
   Future<List<Creator>> getCreators() async {
     try {
@@ -25,7 +29,7 @@ class KemonoClient {
     _ => throw FormatException('Unprocessable data')
   };
 
-  return (parsedData as List).map((x) => Creator.fromJson(x)).toList();
+  return (parsedData as List).map((x) => Creator.fromJson(x,_dio.options.baseUrl)).toList();
 } on DioException catch (e) {
   throw Exception('Network error: ${e.message}');
 } on FormatException catch (e) {
@@ -48,7 +52,7 @@ class KemonoClient {
           _ => throw FormatException('Unprocessable data')
         };
 
-      return (parsedData as List).map((x) => Post.fromJson(x)).toList();
+      return (parsedData as List).map((x) => Post.fromJson(x,_dio.options.baseUrl)).toList();
     } on DioException catch (e) {
       throw Exception('Posts load failed: ${e.message}');
     } on Exception {

+ 16 - 3
lib/main.dart

@@ -7,6 +7,7 @@ import 'package:veloe_kemono_party_flutter/kemono_client.dart';
 import 'package:veloe_kemono_party_flutter/models/creator.dart';
 import 'package:veloe_kemono_party_flutter/models/nav_item.dart';
 import 'package:veloe_kemono_party_flutter/models/post.dart';
+import 'package:veloe_kemono_party_flutter/pages/cache_settings_screen.dart';
 import 'package:veloe_kemono_party_flutter/pages/home_screen.dart';
 import 'package:veloe_kemono_party_flutter/pages/posts_screen.dart';
 import 'package:veloe_kemono_party_flutter/posts_notifier.dart';
@@ -16,11 +17,18 @@ final searchQueryProvider = StateProvider<String>((ref) => '');
 final selectedCreatorProvider = StateProvider<Creator?>((ref) => null);
 final postsPageProvider = StateProvider<int>((ref) => 0);
 
-final kemonoClientProvider = Provider<KemonoClient>((ref) => KemonoClient());
+enum KemonoSource { kemono, coomer }
 
+final sourceProvider = StateProvider<KemonoSource>((ref) => KemonoSource.kemono);
+
+final kemonoClientProvider = Provider<KemonoClient>((ref) {
+  final source = ref.watch(sourceProvider);
+  final baseUrl = source == KemonoSource.kemono ? 'https://kemono.su' : 'https://coomer.su';
+  return KemonoClient(baseUrl);
+});
 final creatorsProvider = FutureProvider<List<Creator>>((ref) async {
   final query = ref.watch(searchQueryProvider);
-  final client = ref.read(kemonoClientProvider);
+  final client = ref.watch(kemonoClientProvider);
   final creators = await client.getCreators();
   
   return creators.where((creator) =>
@@ -29,6 +37,11 @@ final creatorsProvider = FutureProvider<List<Creator>>((ref) async {
   ).toList();
 });
 
+final sourceChangeProvider = Provider<void>((ref) {
+  // This will trigger rebuilds when source changes
+  ref.watch(sourceProvider);
+});
+
 final postsProvider = StateNotifierProvider.autoDispose
     .family<PostsNotifier, AsyncValue<List<Post>>, (String, String, String)>((ref, args) {
   final (creatorId, service, query) = args;
@@ -87,7 +100,7 @@ void main() {
             NavItem(
               label: 'Settings',
               icon: Icons.settings,
-              page: Placeholder(), // Replace with actual SettingsScreen
+              page: CacheSettingsScreen(),
             ),
           ],
         ),

+ 6 - 4
lib/models/attachment.dart

@@ -2,9 +2,10 @@ class Attachment {
   final String name;
   final String path;
   final int? size;
-  final String? mimeType; // Add mimeType if available
+  final String? mimeType;
+  final String? baseUrl; // Add mimeType if available
 
-  Attachment({required this.name, required this.path, this.size, this.mimeType});
+  Attachment({required this.name, required this.path, this.size, this.mimeType, this.baseUrl});
 
   bool get isImage {
     final ext = link.split('.').last.toLowerCase();
@@ -18,10 +19,11 @@ class Attachment {
         (mimeType?.startsWith('video/') ?? false);
   }
 
-  String get link => 'https://kemono.su/data$path';
+  String get link => '$baseUrl/data$path';
 
-  factory Attachment.fromJson(Map<String, dynamic> json) => Attachment(
+  factory Attachment.fromJson(Map<String, dynamic> json, String baseUrl) => Attachment(
     name: json['name'],
     path: json['path'],
+    baseUrl: baseUrl,
   );
 }

+ 5 - 2
lib/models/creator.dart

@@ -5,6 +5,7 @@ class Creator {
   final String name;
   final String service;
   final int updated;
+  final String baseUrl;
 
   Creator({
     required this.favorited,
@@ -13,16 +14,18 @@ class Creator {
     required this.name,
     required this.service,
     required this.updated,
+    required this.baseUrl,
   });
 
-  String get iconUrl => 'https://kemono.su/icons/$service/$id';
+  String get iconUrl => '$baseUrl/icons/$service/$id';
 
-  factory Creator.fromJson(Map<String, dynamic> json) => Creator(
+  factory Creator.fromJson(Map<String, dynamic> json, String baseUrl) => Creator(
     favorited: json['favorited'],
     id: json['id'],
     indexed: json['indexed'],
     name: json['name'],
     service: json['service'],
     updated: json['updated'],
+    baseUrl: baseUrl,
   );
 }

+ 2 - 2
lib/models/post.dart

@@ -26,10 +26,10 @@ class Post {
   List<Attachment> get mediaAttachments => 
       attachments.where((a) => a.isImage || a.isVideo).toList();
 
-  factory Post.fromJson(Map<String, dynamic> json) => Post(
+  factory Post.fromJson(Map<String, dynamic> json, String baseUrl) => Post(
     added: json['added'],
     attachments: List<Attachment>.from(
-      json['attachments'].map((x) => Attachment.fromJson(x))),
+      json['attachments'].map((x) => Attachment.fromJson(x,baseUrl))),
     content: json['content'],
     edited: json['edited'] ?? "",
     id: json['id'],

+ 223 - 0
lib/pages/cache_settings_screen.dart

@@ -0,0 +1,223 @@
+import 'dart:io';
+import 'dart:math';
+import 'package:flutter/material.dart';
+import 'package:flutter_cache_manager/flutter_cache_manager.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:veloe_kemono_party_flutter/main.dart';
+
+
+class CacheSettingsScreen extends ConsumerStatefulWidget {
+  const CacheSettingsScreen({super.key});
+
+  @override
+  ConsumerState<CacheSettingsScreen> createState() => _CacheSettingsScreenState();
+}
+
+class _CacheSettingsScreenState extends ConsumerState<CacheSettingsScreen> {
+  final Map<String, CacheStats> _cacheStats = {
+    'Images': CacheStats('Images', 0, 0),
+    'Videos': CacheStats('Videos', 0, 0),
+    'Icons': CacheStats('Icons', 0, 0),
+  };
+  
+  bool _isLoading = true;
+  
+  @override
+  void initState() {
+    super.initState();
+    _loadCacheStats();
+  }
+
+  Future<void> _loadCacheStats() async {
+    setState(() => _isLoading = true);
+    
+    final stats = await Future.wait([
+      _getCacheStats(imageCacheManager, 'Images'),
+      _getCacheStats(videoCacheManager, 'Videos'),
+      _getCacheStats(iconsCacheManager, 'Icons'),
+    ]);
+    
+    setState(() {
+      for (var stat in stats) {
+        _cacheStats[stat.name] = stat;
+      }
+      _isLoading = false;
+    });
+  }
+
+  Future<CacheStats> _getCacheStats(CacheManager manager, String name) async {
+    try {
+      final directory = await getTemporaryDirectory();
+      final cacheDir = Directory('${directory.path}/${manager.config.cacheKey}');
+      
+      if (!await cacheDir.exists()) return CacheStats(name, 0, 0);
+      
+      final files = await cacheDir.list().where((entity) => entity is File).toList();
+      int totalSize = 0;
+      
+      for (var file in files.cast<File>()) {
+        totalSize += await file.length();
+      }
+      
+      return CacheStats(name, files.length, totalSize);
+    } catch (e) {
+      debugPrint('Cache stats error: $e');
+      return CacheStats(name, 0, 0);
+    }
+  }
+
+  Future<void> _clearCache(BaseCacheManager manager) async {
+    await manager.emptyCache();
+    _loadCacheStats();
+  }
+
+  String _formatBytes(int bytes) {
+    if (bytes <= 0) return "0 B";
+    const suffixes = ["B", "KB", "MB", "GB", "TB"];
+    final i = (log(bytes) / log(1024)).floor();
+    return '${(bytes / pow(1024, i)).toStringAsFixed(2)} ${suffixes[i]}';
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final currentSource = ref.watch(sourceProvider);
+    
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Content Settings'),
+        actions: [
+          IconButton(
+            icon: const Icon(Icons.refresh),
+            onPressed: _loadCacheStats,
+            tooltip: 'Refresh Stats',
+          ),
+        ],
+      ),
+      body: _isLoading
+          ? const Center(child: CircularProgressIndicator())
+          : Padding(
+              padding: const EdgeInsets.all(16.0),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  // Source selection
+                  Text('Content Source', style: Theme.of(context).textTheme.titleMedium),
+                  const SizedBox(height: 10),
+                  SegmentedButton<KemonoSource>(
+                    segments: const [
+                      ButtonSegment(
+                        value: KemonoSource.kemono,
+                        label: Text('Kemono.su'),
+                        icon: Icon(Icons.image),
+                      ),
+                      ButtonSegment(
+                        value: KemonoSource.coomer,
+                        label: Text('Coomer.su'),
+                        icon: Icon(Icons.person),
+                      ),
+                    ],
+                    selected: <KemonoSource>{currentSource},
+                    onSelectionChanged: (Set<KemonoSource> newSelection) {
+                      ref.read(sourceProvider.notifier).state = newSelection.first;
+                      ref.invalidate(kemonoClientProvider); // Invalidate client
+                      ref.invalidate(creatorsProvider); // Refresh creators list
+                    },
+                  ),
+                  const SizedBox(height: 30),
+                  
+                  // Cache statistics header
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                    children: [
+                      Text('Cache Statistics', style: Theme.of(context).textTheme.titleMedium),
+                      TextButton(
+                        onPressed: () async {
+                          await imageCacheManager.emptyCache();
+                          await videoCacheManager.emptyCache();
+                          await iconsCacheManager.emptyCache();
+                          _loadCacheStats();
+                        },
+                        child: const Text('Clear All'),
+                      ),
+                    ],
+                  ),
+                  const SizedBox(height: 10),
+                  
+                  // Cache stats cards
+                  Expanded(
+                    child: ListView(
+                      children: [
+                        _buildCacheCard(_cacheStats['Images']!, imageCacheManager),
+                        _buildCacheCard(_cacheStats['Videos']!, videoCacheManager),
+                        _buildCacheCard(_cacheStats['Icons']!, iconsCacheManager),
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+            ),
+    );
+  }
+
+  Widget _buildCacheCard(CacheStats stats, BaseCacheManager manager) {
+    return Card(
+      child: Padding(
+        padding: const EdgeInsets.all(16.0),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                Text(
+                  stats.name,
+                  style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+                ),
+                IconButton(
+                  icon: const Icon(Icons.delete, size: 20),
+                  onPressed: () => _clearCache(manager),
+                  tooltip: 'Clear ${stats.name} Cache',
+                ),
+              ],
+            ),
+            const SizedBox(height: 10),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                const Text('Files:'),
+                Text('${stats.fileCount}', style: const TextStyle(fontWeight: FontWeight.bold)),
+              ],
+            ),
+            const SizedBox(height: 8),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                const Text('Size:'),
+                Text(_formatBytes(stats.totalSize), style: const TextStyle(fontWeight: FontWeight.bold)),
+              ],
+            ),
+            const SizedBox(height: 10),
+            LinearProgressIndicator(
+              value: stats.totalSize / (1024 * 1024 * 100), // 100MB scale
+              minHeight: 6,
+              backgroundColor: Colors.grey[200],
+              valueColor: AlwaysStoppedAnimation<Color>(
+                stats.name == 'Images' ? Colors.blue : 
+                stats.name == 'Videos' ? Colors.red : Colors.green,
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class CacheStats {
+  final String name;
+  final int fileCount;
+  final int totalSize;
+
+  CacheStats(this.name, this.fileCount, this.totalSize);
+}

+ 21 - 6
lib/pages/home_screen.dart

@@ -25,27 +25,42 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
   );
 
   @override
+  void initState() {
+    super.initState();
+    
+    ref.listenManual(sourceChangeProvider, (_, __) {
+      ref.invalidate(creatorsProvider);
+      ref.invalidate(_searchQuery);
+      _searchController.clear();
+    });
+  }
+
+   @override
   Widget build(BuildContext context) {
+    
+    ref.watch(sourceChangeProvider);
+    
+    
+    final filteredCreators = ref.watch(filteredCreatorsProvider(ref.watch(_searchQuery)));
+    
     return DefaultTabController(
       length: 2,
       child: Scaffold(
         appBar: AppBar(
-          title: _buildSearchField(), 
+          title: _buildSearchField(ref),
           actions: [
             IconButton(
               icon: const Icon(Icons.refresh),
-              onPressed: () => ref.invalidate(filteredCreatorsProvider),
+              onPressed: () => ref.invalidate(creatorsProvider),
             ),
           ],
         ),
-        body: _buildCreatorsList(ref.watch(
-              filteredCreatorsProvider(ref.watch(_searchQuery))
-        )),
+        body: _buildCreatorsList(filteredCreators),
       ),
     );
   }
 
-  Widget _buildSearchField() {
+  Widget _buildSearchField(WidgetRef ref) {
     return TextField(
       controller: _searchController,
       decoration: InputDecoration(

+ 1 - 1
lib/pages/widgets/post_container.dart

@@ -261,7 +261,7 @@ class _PostActionsFooter extends StatelessWidget {
 
 void _sharePost(Post post) {
   var link = 'https://kemono.su/${post.service}/user/${post.user}/post/${post.id}';
-  Share.share(link);
+  SharePlus.instance.share(ShareParams(uri: Uri.parse(link)));
 }
 
 Future<bool> _handleUrlLaunch(BuildContext context, String url) async {

+ 1 - 1
lib/pages/widgets/video_preview.dart

@@ -1,6 +1,6 @@
 import 'dart:typed_data';
 import 'package:flutter/material.dart';
-import 'package:veloe_kemono_party_flutter/video_thumbnail_generator';
+import 'package:veloe_kemono_party_flutter/video_thumbnail_generator.dart';
 import '../../main.dart';
 
 class VideoPreview extends StatefulWidget {

+ 0 - 0
lib/video_thumbnail_generator → lib/video_thumbnail_generator.dart


+ 16 - 8
pubspec.lock

@@ -322,10 +322,10 @@ packages:
     dependency: "direct main"
     description:
       name: flutter_riverpod
-      sha256: da9591d1f8d5881628ccd5c25c40e74fc3eef50ba45e40c3905a06e1712412d5
+      sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.9"
+    version: "2.6.1"
   flutter_svg:
     dependency: transitive
     description:
@@ -568,6 +568,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "5.1.1"
+  localstorage:
+    dependency: "direct main"
+    description:
+      name: localstorage
+      sha256: e037e1db61f846e007206ef27f37d1d0cf53f9e25db5de983034803dd3356734
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.0.0"
   logging:
     dependency: transitive
     description:
@@ -812,10 +820,10 @@ packages:
     dependency: "direct main"
     description:
       name: riverpod
-      sha256: "942999ee48b899f8a46a860f1e13cee36f2f77609eb54c5b7a669bb20d550b11"
+      sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.9"
+    version: "2.6.1"
   rxdart:
     dependency: transitive
     description:
@@ -852,18 +860,18 @@ packages:
     dependency: "direct main"
     description:
       name: share_plus
-      sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900"
+      sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0
       url: "https://pub.dev"
     source: hosted
-    version: "7.2.2"
+    version: "11.0.0"
   share_plus_platform_interface:
     dependency: transitive
     description:
       name: share_plus_platform_interface
-      sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496"
+      sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef"
       url: "https://pub.dev"
     source: hosted
-    version: "3.4.0"
+    version: "6.0.0"
   shelf:
     dependency: transitive
     description:

+ 5 - 4
pubspec.yaml

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 # In Windows, build-name is used as the major, minor, and patch parts
 # of the product and file versions while build-number is used as the build suffix.
-version: 1.0.5
+version: 1.0.6
 
 environment:
   sdk: ^3.7.2
@@ -34,8 +34,8 @@ dependencies:
   # The following adds the Cupertino Icons font to your application.
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.8
-  flutter_riverpod: 2.4.9
-  riverpod: 2.4.9
+  flutter_riverpod: 2.6.1
+  riverpod: 2.6.1
   dio: ^5.4.3          # HTTP client
   cached_network_image: ^3.2.3  # Image caching
   flutter_cache_manager: ^3.2.3 # Cache management
@@ -43,13 +43,14 @@ dependencies:
   intl: ^0.18.1
   flutter_widget_from_html: ^0.10.0
   url_launcher: ^6.2.0
-  share_plus: ^7.0.0
+  share_plus: ^11.0.0
   photo_view: ^0.14.0
   provider: ^6.0.0
   media_kit: ^1.1.0
   media_kit_video: ^1.1.0
   media_kit_libs_android_video: ^1.1.0
   video_thumbnail: ^0.5.6
+  localstorage: ^6.0.0
 
 dev_dependencies:
   flutter_test: