Bläddra i källkod

added media save to gallery, fixed app bars

Veloe 3 månader sedan
förälder
incheckning
f6063b5c02

+ 2 - 0
android/app/src/main/AndroidManifest.xml

@@ -1,4 +1,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <application
         android:label="veloe_kemono_party_flutter"
         android:name="${applicationName}"

+ 6 - 9
lib/app_layout.dart

@@ -9,6 +9,12 @@ class AppLayout extends ConsumerStatefulWidget {
 
   @override
   ConsumerState<AppLayout> createState() => _AppLayoutState();
+
+  static openDrawer(BuildContext context) {
+    final ScaffoldState? scaffoldState =
+        context.findRootAncestorStateOfType<ScaffoldState>();
+    scaffoldState?.openDrawer();
+  }
 }
 
 class _AppLayoutState extends ConsumerState<AppLayout> {
@@ -56,15 +62,6 @@ class _AppLayoutState extends ConsumerState<AppLayout> {
     final isDesktop = MediaQuery.of(context).size.width >= 600;
 
     return Scaffold(
-      appBar: AppBar(
-        title: Text(widget.navItems[selectedIndex].label),
-        leading: isDesktop ? null : Builder(
-          builder: (context) => IconButton(
-            icon: const Icon(Icons.menu),
-            onPressed: () => Scaffold.of(context).openDrawer(),
-          ),
-        ),
-      ),
       drawer: isDesktop ? null : _buildMobileDrawer(selectedIndex),
       body: Row(
         children: [

+ 5 - 3
lib/main.dart

@@ -1,3 +1,4 @@
+import 'dart:io';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:flutter_cache_manager/flutter_cache_manager.dart';
@@ -52,14 +53,14 @@ final postsProvider = StateNotifierProvider.autoDispose
     client: ref.read(kemonoClientProvider),
   );
 });
-
+final httpFileService = HttpFileService();
 // Image Caching
 final imageCacheManager = CacheManager(
   Config(
     'kemono_images',
     stalePeriod: const Duration(days: 30),
     maxNrOfCacheObjects: 1024,
-    fileService: HttpFileService(),
+    fileService: httpFileService,
   ),
 );
 final videoCacheManager = CacheManager(
@@ -67,7 +68,7 @@ final videoCacheManager = CacheManager(
     'kemono_videos',
     stalePeriod: const Duration(days: 30),
     maxNrOfCacheObjects: 1024,
-    fileService: HttpFileService(),
+    fileService: httpFileService,
   ),
 );
 final iconsCacheManager = CacheManager(
@@ -81,6 +82,7 @@ final iconsCacheManager = CacheManager(
 
 // Main Application
 void main() { 
+  httpFileService.concurrentFetches = 2;
   MediaKit.ensureInitialized();
   runApp(
     const ProviderScope(

+ 3 - 0
lib/models/post.dart

@@ -10,6 +10,7 @@ class Post {
   final String service;
   final String title;
   final String user;
+  final String baseUrl;
 
   Post({
     required this.added,
@@ -21,6 +22,7 @@ class Post {
     required this.service,
     required this.title,
     required this.user,
+    required this.baseUrl,
   });
 
   List<Attachment> get mediaAttachments => 
@@ -37,5 +39,6 @@ class Post {
     service: json['service'],
     title: json['title'],
     user: json['user'],
+    baseUrl: baseUrl,
   );
 }

+ 7 - 0
lib/pages/cache_settings_screen.dart

@@ -4,6 +4,7 @@ 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/app_layout.dart';
 import 'package:veloe_kemono_party_flutter/main.dart';
 
 
@@ -86,6 +87,12 @@ class _CacheSettingsScreenState extends ConsumerState<CacheSettingsScreen> {
     return Scaffold(
       appBar: AppBar(
         title: const Text('Content Settings'),
+        leading: Builder(
+          builder: (context) => IconButton(
+            icon: const Icon(Icons.menu),
+            onPressed: () => AppLayout.openDrawer(context),
+          ),
+        ),
         actions: [
           IconButton(
             icon: const Icon(Icons.refresh),

+ 7 - 0
lib/pages/home_screen.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:veloe_kemono_party_flutter/app_layout.dart';
 import 'package:veloe_kemono_party_flutter/main.dart';
 import 'package:veloe_kemono_party_flutter/models/creator.dart';
 import 'package:veloe_kemono_party_flutter/pages/widgets/creator_card.dart';
@@ -48,6 +49,12 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
       child: Scaffold(
         appBar: AppBar(
           title: _buildSearchField(ref),
+          leading: Builder(
+            builder: (context) => IconButton(
+              icon: const Icon(Icons.menu),
+              onPressed: () => AppLayout.openDrawer(context),
+            ),
+          ),
           actions: [
             IconButton(
               icon: const Icon(Icons.refresh),

+ 108 - 1
lib/pages/media_carousel_screen

@@ -1,7 +1,15 @@
 import 'package:flutter/material.dart';
 import 'package:photo_view/photo_view.dart';
+import 'package:flutter_cache_manager/flutter_cache_manager.dart';
+import 'package:cached_network_image/cached_network_image.dart';
 import 'package:veloe_kemono_party_flutter/pages/widgets/video_player_widget';
+import 'package:path_provider/path_provider.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:gallery_saver_plus/gallery_saver.dart';
+import 'package:path/path.dart' as path;
 import '../models/post.dart';
+import 'package:veloe_kemono_party_flutter/main.dart';
+import 'dart:io';
 
 class MediaCarouselItem {
   final String url;
@@ -40,6 +48,7 @@ class _MediaCarouselScreenState extends State<MediaCarouselScreen> {
   late PageController _pageController;
   late int _currentIndex;
   late List<MediaCarouselItem> _mediaItems;
+  bool _isSaving = false;
 
   @override
   void initState() {
@@ -80,15 +89,43 @@ class _MediaCarouselScreenState extends State<MediaCarouselScreen> {
             },
           ),
           Positioned(
-            top: 40,
+              top: 20,
             left: 20,
             child: IconButton(
               icon: const Icon(Icons.close, color: Colors.white),
               onPressed: () => Navigator.pop(context),
             ),
+            ),
+            Positioned(
+              top: 20,
+              right: 20,
+              child: PopupMenuButton<String>(
+                onSelected: (value) => _handleMenuSelection(value),
+                itemBuilder: (context) => [
+                  PopupMenuItem(
+                    value: 'save',
+                    child: Row(
+                      children: [
+                        Icon(Icons.save_alt, color: Theme.of(context).primaryColor),
+                        const SizedBox(width: 10),
+                        const Text('Save to Gallery'),
+                      ],
+                    ),
+                  ),
+                  // Add more options here if needed
+                ],
+                icon: const Icon(Icons.more_vert, color: Colors.white),
+              ),
+            ),
+            
+            // Saving indicator
+            if (_isSaving)
+              const Center(
+                child: CircularProgressIndicator(color: Colors.white),
           ),
         ],
       ),
+      )
     );
   }
 
@@ -107,6 +144,7 @@ class _MediaCarouselScreenState extends State<MediaCarouselScreen> {
       final post = posts[i];
       for (int j = 0; j < post.attachments.length; j++) {
         final attachment = post.attachments[j];
+        if (attachment.isImage || attachment.isVideo)
         items.add(
           MediaCarouselItem(
             url: attachment.link,
@@ -119,5 +157,74 @@ class _MediaCarouselScreenState extends State<MediaCarouselScreen> {
     }
     return items;
   }
+
+  void _handleMenuSelection(String value) async {
+    if (value == 'save') {
+      await _saveCurrentMedia();
+    }
+  }
+
+  Future<void> _saveCurrentMedia() async {
+    setState(() => _isSaving = true);
+    
+    try {
+      final item = _mediaItems[_currentIndex];
+      final status = await Permission.storage.request();
+      
+      if (status.isPermanentlyDenied) {
+        openAppSettings();
+      }
+      
+      if (!status.isGranted) {
+        _showSnackBar('Storage permission denied');
+        return;
+      }
+
+      final cachedFile = await imageCacheManager.getFileFromCache(item.url);
+      
+      if (cachedFile is FileInfo)
+      {
+        //if (Platform.isAndroid) {
+        if (item.type == "image"){
+          await GallerySaver.saveImage(cachedFile.file.path);
+        }
+        else {
+          await GallerySaver.saveVideo(cachedFile.file.path);
+        }
+        //}
+        
+        _showSnackBar('Media saved to gallery');
+      }
+      else
+        _showSnackBar('Cache media first!');
+    } catch (e) {
+      debugPrint('Save error: $e');
+      _showSnackBar('Failed to save media');
+    } finally {
+      setState(() => _isSaving = false);
+    }
+  }
+
+  Future<Directory> getGalleryDirectory(String type) async {
+  final externalDir = await getExternalStorageDirectory();
+  final galleryPath = type == 'image'
+      ? path.join(externalDir!.path, 'Pictures', 'Kemono Party')
+      : path.join(externalDir!.path, 'Movies', 'Kemono Party');
+  
+  final dir = Directory(galleryPath);
+  if (!await dir.exists()) {
+    await dir.create(recursive: true);
+  }
+  return dir;
+}
+
+  void _showSnackBar(String message) {
+    ScaffoldMessenger.of(context).showSnackBar(
+      SnackBar(
+        content: Text(message),
+        duration: const Duration(seconds: 2),
+      ),
+    );
+  }
 }
 

+ 10 - 2
lib/pages/posts_screen.dart

@@ -1,6 +1,7 @@
 import 'dart:async';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:veloe_kemono_party_flutter/app_layout.dart';
 import 'package:veloe_kemono_party_flutter/main.dart';
 import 'package:veloe_kemono_party_flutter/pages/widgets/post_container.dart';
 
@@ -72,10 +73,17 @@ class _PostsScreenState extends ConsumerState<PostsScreen> with PaginationMixin
     return Scaffold(
       appBar:  AppBar(
         title: _buildSearchField(),
-        leading: widget.withAppBar ? IconButton(
+        leading: widget.withAppBar ? 
+        IconButton(
           icon: const Icon(Icons.arrow_back),
           onPressed: () => Navigator.pop(context),
-        ) : null,
+        ) : 
+        Builder(
+          builder: (context) => IconButton(
+            icon: const Icon(Icons.menu),
+            onPressed: () => AppLayout.openDrawer(context),
+          ),
+        ),
       ),
       body: postsAsync.when(
         loading: () => const Center(child: CircularProgressIndicator()),

+ 8 - 9
lib/pages/widgets/post_container.dart

@@ -43,9 +43,7 @@ class PostContainer extends StatelessWidget {
   }
 
   Widget _buildContent(BuildContext context, List<Attachment> attachments) {
-    final imageAttachments = post.attachments
-      .where((a) => a.isImage || a.isVideo)
-      .toList();
+  final mediaAttachments = attachments.where((a) => a.isImage || a.isVideo).toList();
 
     return Column(
       mainAxisSize: MainAxisSize.min,
@@ -58,8 +56,7 @@ class PostContainer extends StatelessWidget {
           onTapUrl: (url) => _handleUrlLaunch(context, url),
         ),
 
-        // Attachments Grid
-        if (imageAttachments.isNotEmpty)
+      if (mediaAttachments.isNotEmpty)
           LayoutBuilder(
             builder: (context, constraints) {
               return Column(
@@ -71,11 +68,11 @@ class PostContainer extends StatelessWidget {
                     gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                       crossAxisCount: _calculateColumnCount(constraints.maxWidth),
                     ),
-                    itemCount: imageAttachments.length,
+                    itemCount: mediaAttachments.length,
                     itemBuilder: (context, index) => Padding(
                       padding: const EdgeInsets.all(4),
                       child: 
-                        _buildMediaPreview(context, imageAttachments[index], index, attachments)
+                        _buildMediaPreview(context, mediaAttachments[index], index, attachments)
                       /*SmartImageContainer(
                         imageUrl: imageAttachments[index].link,
                         //onTap: () => _handleAttachmentTap(index),
@@ -140,6 +137,7 @@ class PostContainer extends StatelessWidget {
       final post = allPosts[i];
       for (int j = 0; j < post.attachments.length; j++) {
         final attachment = post.attachments[j];
+        if (attachment.isImage || attachment.isVideo) {
         mediaItems.add(
           MediaCarouselItem(
             url: attachment.link,
@@ -148,12 +146,13 @@ class PostContainer extends StatelessWidget {
             attachmentIndex: j,
           ),
         );
+        }
       }
     }
 
     int globalIndex = 0;
     for (int i = 0; i < postIndex; i++) {
-      globalIndex += allPosts[i].attachments.length;
+      globalIndex += allPosts[i].attachments.where((x)=>x.isImage || x.isVideo).length;
     }
     globalIndex += attachmentIndex;
 
@@ -260,7 +259,7 @@ class _PostActionsFooter extends StatelessWidget {
 }
 
 void _sharePost(Post post) {
-  var link = 'https://kemono.su/${post.service}/user/${post.user}/post/${post.id}';
+  var link = '${post.baseUrl}/${post.service}/user/${post.user}/post/${post.id}';
   SharePlus.instance.share(ShareParams(uri: Uri.parse(link)));
 }
 

+ 1 - 1
lib/pages/widgets/video_player_widget

@@ -47,7 +47,7 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
           : Media(widget.videoUrl);
       
       await _player.open(source);
-      await _player.setVolume(0);
+      //await _player.setVolume(0);
       
       if (_cachedFile == null) {
         _cacheVideo();

+ 71 - 15
pubspec.lock

@@ -93,10 +93,10 @@ packages:
     dependency: "direct dev"
     description:
       name: build_runner
-      sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
+      sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.15"
+    version: "2.4.14"
   build_runner_core:
     dependency: transitive
     description:
@@ -330,10 +330,10 @@ packages:
     dependency: transitive
     description:
       name: flutter_svg
-      sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1
+      sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.0"
+    version: "2.0.9"
   flutter_test:
     dependency: "direct dev"
     description: flutter
@@ -416,6 +416,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.9.0+2"
+  gallery_saver_plus:
+    dependency: "direct main"
+    description:
+      name: gallery_saver_plus
+      sha256: "179a2cf0a8d5d61957724e39cf5664a336d5ef28a4222543462dd9fc4c1a6af9"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.2.4"
   glob:
     dependency: transitive
     description:
@@ -697,7 +705,7 @@ packages:
     source: hosted
     version: "1.1.0"
   path_provider:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: path_provider
       sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
@@ -744,6 +752,54 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.3.0"
+  permission_handler:
+    dependency: "direct main"
+    description:
+      name: permission_handler
+      sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "12.0.0+1"
+  permission_handler_android:
+    dependency: transitive
+    description:
+      name: permission_handler_android
+      sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
+      url: "https://pub.dev"
+    source: hosted
+    version: "13.0.1"
+  permission_handler_apple:
+    dependency: transitive
+    description:
+      name: permission_handler_apple
+      sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
+      url: "https://pub.dev"
+    source: hosted
+    version: "9.4.7"
+  permission_handler_html:
+    dependency: transitive
+    description:
+      name: permission_handler_html
+      sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.3+5"
+  permission_handler_platform_interface:
+    dependency: transitive
+    description:
+      name: permission_handler_platform_interface
+      sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.3.0"
+  permission_handler_windows:
+    dependency: transitive
+    description:
+      name: permission_handler_windows
+      sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.2.1"
   petitparser:
     dependency: transitive
     description:
@@ -828,10 +884,10 @@ packages:
     dependency: transitive
     description:
       name: rxdart
-      sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
+      sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
       url: "https://pub.dev"
     source: hosted
-    version: "0.28.0"
+    version: "0.27.7"
   safe_local_storage:
     dependency: transitive
     description:
@@ -884,10 +940,10 @@ packages:
     dependency: transitive
     description:
       name: shelf_web_socket
-      sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
+      sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.0"
+    version: "2.0.1"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -1137,26 +1193,26 @@ packages:
     dependency: transitive
     description:
       name: vector_graphics
-      sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de"
+      sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752"
       url: "https://pub.dev"
     source: hosted
-    version: "1.1.18"
+    version: "1.1.10+1"
   vector_graphics_codec:
     dependency: transitive
     description:
       name: vector_graphics_codec
-      sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
+      sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33
       url: "https://pub.dev"
     source: hosted
-    version: "1.1.13"
+    version: "1.1.10+1"
   vector_graphics_compiler:
     dependency: transitive
     description:
       name: vector_graphics_compiler
-      sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
+      sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a"
       url: "https://pub.dev"
     source: hosted
-    version: "1.1.16"
+    version: "1.1.10+1"
   vector_math:
     dependency: transitive
     description:

+ 6 - 3
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.6
+version: 1.0.7
 
 environment:
   sdk: ^3.7.2
@@ -37,8 +37,8 @@ dependencies:
   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
+  cached_network_image: ^3.4.1  # Image caching
+  flutter_cache_manager: ^3.4.1 # Cache management
   json_annotation: ^4.8.1       # JSON serialization
   intl: ^0.18.1
   flutter_widget_from_html: ^0.10.0
@@ -51,6 +51,9 @@ dependencies:
   media_kit_libs_android_video: ^1.1.0
   video_thumbnail: ^0.5.6
   localstorage: ^6.0.0
+  permission_handler: ^12.0.0
+  gallery_saver_plus: ^3.0.1
+  path_provider: ^2.1.1
 
 dev_dependencies:
   flutter_test:

+ 3 - 0
windows/flutter/generated_plugin_registrant.cc

@@ -7,6 +7,7 @@
 #include "generated_plugin_registrant.h"
 
 #include <media_kit_video/media_kit_video_plugin_c_api.h>
+#include <permission_handler_windows/permission_handler_windows_plugin.h>
 #include <share_plus/share_plus_windows_plugin_c_api.h>
 #include <url_launcher_windows/url_launcher_windows.h>
 #include <volume_controller/volume_controller_plugin_c_api.h>
@@ -14,6 +15,8 @@
 void RegisterPlugins(flutter::PluginRegistry* registry) {
   MediaKitVideoPluginCApiRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
+  PermissionHandlerWindowsPluginRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
   SharePlusWindowsPluginCApiRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
   UrlLauncherWindowsRegisterWithRegistrar(

+ 1 - 0
windows/flutter/generated_plugins.cmake

@@ -4,6 +4,7 @@
 
 list(APPEND FLUTTER_PLUGIN_LIST
   media_kit_video
+  permission_handler_windows
   share_plus
   url_launcher_windows
   volume_controller