Browse Source

fixed api changes, refactor video player

Veloe 2 weeks ago
parent
commit
cf2f5d2fa0

+ 14 - 5
lib/kemono_client.dart

@@ -4,7 +4,16 @@ import 'package:veloe_kemono_party_flutter/models/creator.dart';
 import 'package:veloe_kemono_party_flutter/models/post.dart';
 
 class KemonoClient {
-  KemonoClient(String sourceUrl) :_dio = Dio(BaseOptions(baseUrl: sourceUrl));
+  KemonoClient(String sourceUrl) :_dio = Dio(BaseOptions(baseUrl: sourceUrl, headers: 
+  {
+    'Accept': 'text/css',
+    'Accept-Encoding': 'gzip, deflate, br',
+    'Accept-Language': 'en-US,en;q=0.5',
+    'Cache-Control': 'no-cache',
+    'Connection': 'keep-alive',
+    'Pragma': 'no-cache',
+    'user-agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0'
+  }));
 
   final Dio _dio;
   
@@ -12,7 +21,7 @@ class KemonoClient {
 
   Future<List<Creator>> getCreators() async {
     try {
-  final response = await _dio.get('/api/v1/creators.txt');
+  final response = await _dio.get('/api/v1/creators');
   
   if (response.statusCode != 200) {
     throw DioException(
@@ -31,7 +40,7 @@ class KemonoClient {
 
   return (parsedData as List).map((x) => Creator.fromJson(x,_dio.options.baseUrl)).toList();
 } on DioException catch (e) {
-  throw Exception('Network error: ${e.message}');
+  throw Exception('Network error: ${e.message} ${e.response?.data as String}');
 } on FormatException catch (e) {
   throw Exception('Data format error: ${e.message}');
 }
@@ -41,7 +50,7 @@ class KemonoClient {
     try {
 
       final response = await _dio.get(
-        '/api/v1/$service/user/$creatorId',
+        '/api/v1/$service/user/$creatorId/posts',
         queryParameters: query.isEmpty ? {'o': start} : {'o':start, 'q': query},
       );
 
@@ -54,7 +63,7 @@ class KemonoClient {
 
       return (parsedData as List).map((x) => Post.fromJson(x,_dio.options.baseUrl)).toList();
     } on DioException catch (e) {
-      throw Exception('Posts load failed: ${e.message}');
+      throw Exception('Posts load failed: ${e.message} ${e.stackTrace}');
     } on Exception {
         throw Exception('Oops something go wrong}');
     }

+ 4 - 12
lib/main.dart

@@ -1,8 +1,9 @@
 import 'dart:io';
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:cached_video_player_plus/src/video_cache_manager.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:flutter_cache_manager/flutter_cache_manager.dart';
-import 'package:media_kit/media_kit.dart';
 import 'package:veloe_kemono_party_flutter/app_layout.dart';
 import 'package:veloe_kemono_party_flutter/kemono_client.dart';
 import 'package:veloe_kemono_party_flutter/models/creator.dart';
@@ -24,7 +25,7 @@ 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';
+  final baseUrl = source == KemonoSource.kemono ? 'https://kemono.cr' : 'https://coomer.st';
   return KemonoClient(baseUrl);
 });
 final creatorsProvider = FutureProvider<List<Creator>>((ref) async {
@@ -63,14 +64,7 @@ final imageCacheManager = CacheManager(
     fileService: httpFileService,
   ),
 );
-final videoCacheManager = CacheManager(
-  Config(
-    'kemono_videos',
-    stalePeriod: const Duration(days: 30),
-    maxNrOfCacheObjects: 1024,
-    fileService: httpFileService,
-  ),
-);
+final videoCacheManager = VideoCacheManager();
 final iconsCacheManager = CacheManager(
   Config(
     'kemono_icons',
@@ -79,11 +73,9 @@ final iconsCacheManager = CacheManager(
     fileService: HttpFileService(),
   ),
 );
-
 // Main Application
 void main() { 
   httpFileService.concurrentFetches = 2;
-  MediaKit.ensureInitialized();
   runApp(
     const ProviderScope(
       child: MaterialApp(

+ 35 - 14
lib/models/post.dart

@@ -3,6 +3,7 @@ import 'attachment.dart';
 class Post {
   final String added;
   final List<Attachment> attachments;
+  final Attachment? files;
   final String content;
   final String edited;
   final String id;
@@ -15,6 +16,7 @@ class Post {
   Post({
     required this.added,
     required this.attachments,
+    required this.files,
     required this.content,
     required this.edited,
     required this.id,
@@ -25,20 +27,39 @@ class Post {
     required this.baseUrl,
   });
 
-  List<Attachment> get mediaAttachments => 
-      attachments.where((a) => a.isImage || a.isVideo).toList();
+  List<Attachment> get mediaAttachments => files == null ? [
+        ...attachments.where((a) => a.isImage || a.isVideo)
+      ] : [
+        ...attachments.where((a) => a.isImage || a.isVideo),files!
+      ];
 
   factory Post.fromJson(Map<String, dynamic> json, String baseUrl) => Post(
-    added: json['added'],
-    attachments: List<Attachment>.from(
-      json['attachments'].map((x) => Attachment.fromJson(x,baseUrl))),
-    content: json['content'],
-    edited: json['edited'] ?? "",
-    id: json['id'],
-    published: json['published'],
-    service: json['service'],
-    title: json['title'],
-    user: json['user'],
-    baseUrl: baseUrl,
-  );
+        added: _parseString(json['added']),
+        attachments: _parseAttachments(json['attachments'], baseUrl),
+        files: (json["file"] as Map)?.isNotEmpty == true ?  Attachment.fromJson(json["file"], baseUrl) : null,
+        content: _parseString(json['substring']),
+        edited: _parseString(json['edited']),
+        id: _parseString(json['id']),
+        published: _parseString(json['published']),
+        service: _parseString(json['service']),
+        title: _parseString(json['title']),
+        user: _parseString(json['user']),
+        baseUrl: baseUrl,
+      );
+
+  static String _parseString(dynamic value) => 
+      (value is String) ? value : '';
+
+  static List<Attachment> _parseAttachments(
+    dynamic jsonData, 
+    String baseUrl
+  ) {
+    if (jsonData is! List) return [];
+    
+    return jsonData
+        .whereType<Map<String, dynamic>>()
+        .map((x) => Attachment.fromJson(x, baseUrl))
+        .whereType<Attachment>()
+        .toList();
+  }
 }

+ 3 - 3
lib/pages/cache_settings_screen.dart

@@ -110,7 +110,7 @@ class _CacheSettingsScreenState extends ConsumerState<CacheSettingsScreen> {
                 children: [
                   // Source selection
                   Text('Content Source', style: Theme.of(context).textTheme.titleMedium),
-                  const SizedBox(height: 10),
+                  const SizedBox(height: 5),
                   SegmentedButton<KemonoSource>(
                     segments: const [
                       ButtonSegment(
@@ -131,7 +131,7 @@ class _CacheSettingsScreenState extends ConsumerState<CacheSettingsScreen> {
                       ref.invalidate(creatorsProvider); // Refresh creators list
                     },
                   ),
-                  const SizedBox(height: 30),
+                  const SizedBox(height: 5),
                   
                   // Cache statistics header
                   Row(
@@ -149,7 +149,7 @@ class _CacheSettingsScreenState extends ConsumerState<CacheSettingsScreen> {
                       ),
                     ],
                   ),
-                  const SizedBox(height: 10),
+                  const SizedBox(height: 5),
                   
                   // Cache stats cards
                   Expanded(

+ 70 - 43
lib/pages/media_carousel_screen

@@ -10,6 +10,7 @@ import 'package:path/path.dart' as path;
 import '../models/post.dart';
 import 'package:veloe_kemono_party_flutter/main.dart';
 import 'dart:io';
+import 'package:flutter/services.dart';
 
 class MediaCarouselItem {
   final String url;
@@ -60,41 +61,67 @@ class _MediaCarouselScreenState extends State<MediaCarouselScreen> {
 
   @override
   Widget build(BuildContext context) {
-    return Scaffold(
-      backgroundColor: Colors.black,
-      body: Stack(
-        children: [
-          PageView.builder(
-            controller: _pageController,
-            itemCount: _mediaItems.length,
-            onPageChanged: (index) {
-              setState(() {
-                _currentIndex = index;
-              });
-              // Check if we are near the end and load more
-              if (index >= _mediaItems.length - 3) {
-                _loadMoreIfNeeded();
-              }
-            },
-            itemBuilder: (context, index) {
-              final item = _mediaItems[index];
-              if (item.type == 'image') {
-                return PhotoView(
-                  imageProvider: NetworkImage(item.url),
-                  backgroundDecoration: const BoxDecoration(color: Colors.black),
-                );
-              } else {
-                return VideoPlayerWidget(videoUrl: item.url);
-              }
-            },
+    return Theme(
+      // 👇 Local theme override ONLY for this screen
+      data: ThemeData.dark().copyWith(
+        appBarTheme: const AppBarTheme(
+          backgroundColor: Colors.black,
+          iconTheme: IconThemeData(color: Colors.white),
+          titleTextStyle: TextStyle(
+            color: Colors.white,
+            fontSize: 18,
+            fontWeight: FontWeight.w600
           ),
-          Positioned(
-              top: 20,
-            left: 20,
-            child: IconButton(
-              icon: const Icon(Icons.close, color: Colors.white),
-              onPressed: () => Navigator.pop(context),
+        ),
+        scaffoldBackgroundColor: Colors.black,
+      ),
+      child: Scaffold(
+
+        backgroundColor: Colors.black,
+        body: Stack(
+          children: [
+            PageView.builder(
+              controller: _pageController,
+              itemCount: _mediaItems.length,
+              onPageChanged: (index) {
+                setState(() {
+                  _currentIndex = index;
+                });
+                // Check if we are near the end and load more
+                if (index >= _mediaItems.length - 3) {
+                  _loadMoreIfNeeded();
+                }
+              },
+              itemBuilder: (context, index) {
+                final item = _mediaItems[index];
+                if (item.type == 'image') {
+                  ThemeData theme = Theme.of(context);
+                  CachedNetworkImageProvider provider = CachedNetworkImageProvider(item.url,cacheManager : imageCacheManager);
+                  return Theme(
+                      data: theme.copyWith(
+                      appBarTheme: AppBarTheme(
+                        systemOverlayStyle: SystemUiOverlayStyle(
+                          statusBarIconBrightness: Brightness.light,
+                          statusBarColor: Colors.black26,
+                        ),
+                      ),
+                    ),
+                    child:PhotoView(
+                        imageProvider: provider,
+                        backgroundDecoration: const BoxDecoration(color: Colors.black),
+                  ));
+                } else {
+                  return VideoPlayerWidget(videoUrl: item.url);
+                }
+              },
             ),
+            Positioned(
+              top: 20,
+              left: 20,
+              child: IconButton(
+                icon: const Icon(Icons.close, color: Colors.white),
+                onPressed: () => Navigator.pop(context),
+              ),
             ),
             Positioned(
               top: 20,
@@ -122,9 +149,9 @@ class _MediaCarouselScreenState extends State<MediaCarouselScreen> {
             if (_isSaving)
               const Center(
                 child: CircularProgressIndicator(color: Colors.white),
-          ),
-        ],
-      ),
+              ),
+          ],
+        ),
       )
     );
   }
@@ -145,13 +172,13 @@ class _MediaCarouselScreenState extends State<MediaCarouselScreen> {
       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,
-            type: attachment.isImage ? "image" : "video",
-            postIndex: i,
-            attachmentIndex: j,
-          ),
+          items.add(
+            MediaCarouselItem(
+              url: attachment.link,
+              type: attachment.isImage ? "image" : "video",
+              postIndex: i,
+              attachmentIndex: j,
+            ),
         );
       }
     }

+ 53 - 29
lib/pages/widgets/post_container.dart

@@ -33,7 +33,7 @@ class PostContainer extends StatelessWidget {
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             _PostHeader(post: post),
-            _buildContent(context, post.attachments),
+            _buildContent(context, post.mediaAttachments),
             //_PostContentSection(post: post),
             _PostActionsFooter(post: post),
           ],
@@ -45,19 +45,19 @@ class PostContainer extends StatelessWidget {
   Widget _buildContent(BuildContext context, List<Attachment> attachments) {
   final mediaAttachments = attachments.where((a) => a.isImage || a.isVideo).toList();
 
-    return Column(
-      mainAxisSize: MainAxisSize.min,
-      crossAxisAlignment: CrossAxisAlignment.start,
-      children: [
-        // Text Content
-        HtmlWidget(
-          post.content,
-          textStyle: Theme.of(context).textTheme.bodyMedium,
-          onTapUrl: (url) => _handleUrlLaunch(context, url),
-        ),
-
+  return Column(
+    mainAxisSize: MainAxisSize.min,
+    crossAxisAlignment: CrossAxisAlignment.start,
+    children: [
+    // Text Content
+      HtmlWidget(
+        post.content,
+        textStyle: Theme.of(context).textTheme.bodyMedium,
+        onTapUrl: (url) => _handleUrlLaunch(context, url),
+      ),
+      
       if (mediaAttachments.isNotEmpty)
-          LayoutBuilder(
+        LayoutBuilder(
             builder: (context, constraints) {
               return Column(
                 children: [
@@ -72,7 +72,7 @@ class PostContainer extends StatelessWidget {
                     itemBuilder: (context, index) => Padding(
                       padding: const EdgeInsets.all(4),
                       child: 
-                        _buildMediaPreview(context, mediaAttachments[index], index, attachments)
+                        _buildMediaPreview(context, mediaAttachments[index], index, mediaAttachments)
                       /*SmartImageContainer(
                         imageUrl: imageAttachments[index].link,
                         //onTap: () => _handleAttachmentTap(index),
@@ -80,16 +80,40 @@ class PostContainer extends StatelessWidget {
                     ),
                   ),
                   // Attachment links list
-                  ...post.attachments.map(
+                  ...post.mediaAttachments.map(
                     (attachment) => _buildAttachmentLink(context,attachment),
                   ),
                 ],
               );
             },
           ),
-      ],
-    );
-  }
+      /*
+        LayoutBuilder(
+          builder: (context, constraints) {
+            final itemWidth = constraints.maxWidth;
+
+            return Column(
+              children: [
+                const SizedBox(height: 12),
+                Wrap(
+                spacing: 8, // горизонтальный отступ между элементами
+                runSpacing: 8, // вертикальный отступ
+                children: mediaAttachments.map((attachment) {
+                  return SizedBox(
+                    width: itemWidth,
+                    child: _buildMediaPreview(context, attachment, mediaAttachments.indexOf(attachment), post.attachments),
+                  );
+                }).toList(),
+              ),
+                ...attachments.map((a) => _buildAttachmentLink(context, a)),
+              ],
+            );
+          },
+        ),
+        */
+    ],
+  );
+}
 
   Widget _buildAttachmentLink(BuildContext context,Attachment attachment) {
     return Padding(
@@ -135,24 +159,24 @@ class PostContainer extends StatelessWidget {
     List<MediaCarouselItem> mediaItems = [];
     for (int i = 0; i < allPosts.length; i++) {
       final post = allPosts[i];
-      for (int j = 0; j < post.attachments.length; j++) {
-        final attachment = post.attachments[j];
+      for (int j = 0; j < post.mediaAttachments.length; j++) {
+        final attachment = post.mediaAttachments[j];
         if (attachment.isImage || attachment.isVideo) {
-        mediaItems.add(
-          MediaCarouselItem(
-            url: attachment.link,
-            type: attachment.isVideo ? "video" : "image",
-            postIndex: i,
-            attachmentIndex: j,
-          ),
-        );
+          mediaItems.add(
+            MediaCarouselItem(
+              url: attachment.link,
+              type: attachment.isVideo ? "video" : "image",
+              postIndex: i,
+              attachmentIndex: j,
+            ),
+          );
         }
       }
     }
 
     int globalIndex = 0;
     for (int i = 0; i < postIndex; i++) {
-      globalIndex += allPosts[i].attachments.where((x)=>x.isImage || x.isVideo).length;
+      globalIndex += allPosts[i].mediaAttachments.where((x)=>x.isImage || x.isVideo).length;
     }
     globalIndex += attachmentIndex;
 

+ 231 - 64
lib/pages/widgets/video_player_widget

@@ -1,10 +1,10 @@
-import 'dart:io';
 import 'dart:async';
+import 'dart:io';
 import 'package:flutter/material.dart';
-import 'package:media_kit/media_kit.dart';
-import 'package:media_kit_video/media_kit_video.dart';
+import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 import 'package:flutter_cache_manager/flutter_cache_manager.dart';
-import 'package:path/path.dart' as path;
+import 'package:visibility_detector/visibility_detector.dart';
+import 'package:video_player/video_player.dart';
 import '../../main.dart';
 
 class VideoPlayerWidget extends StatefulWidget {
@@ -17,8 +17,7 @@ class VideoPlayerWidget extends StatefulWidget {
 }
 
 class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
-  late final Player _player;
-  late final VideoController _controller;
+  late final CachedVideoPlayerPlus _player;
   bool _isInitialized = false;
   bool _hasError = false;
   bool _usingCache = false;
@@ -27,56 +26,177 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
   bool _isBuffering = false;
   File? _cachedFile;
   StreamSubscription? _cacheSubscription;
+  bool _showControls = true;
+  Timer? _hideTimer;
+  bool _isPlaying = true;
 
   @override
   void initState() {
     super.initState();
     _initializePlayer();
+    _startHideTimer();
   }
 
   Future<void> _initializePlayer() async {
     try {
-      _cachedFile = await videoCacheManager.getSingleFile(widget.videoUrl);
-      _usingCache = _cachedFile != null;
-      
-      _player = Player();
-      _controller = VideoController(_player);
-      
-      final source = _cachedFile != null 
-          ? Media(_cachedFile!.path, httpHeaders: {}) 
-          : Media(widget.videoUrl);
-      
-      await _player.open(source);
-      //await _player.setVolume(0);
-      
-      if (_cachedFile == null) {
-        _cacheVideo();
-      }
-
-      _player.stream.error.listen(_handlePlayerError);
-      _player.stream.buffering.listen(_handleBufferingUpdate);
+      _player = CachedVideoPlayerPlus.networkUrl(
+        Uri.parse(widget.videoUrl),
+        invalidateCacheIfOlderThan: const Duration(days: 60),
+      );
       
+      await _player.initialize();
+      _player.controller.addListener(_videoListener);
+      setState(() => _isInitialized = true);
+      _player.controller.play();
     } catch (e) {
       _handleError("Player init error: $e");
     }
   }
 
-  void _handleBufferingUpdate(bool isBuffering) {
+  void _videoListener() {
+    if (!mounted) return;
+    
+    // Update playing state
+    if (_isPlaying != _player.controller.value.isPlaying) {
+      setState(() => _isPlaying = _player.controller.value.isPlaying);
+    }
+    
+    // Handle buffering updates
+    final isBuffering = _player.controller.value.isBuffering;
+    if (isBuffering != _isBuffering) {
+      setState(() => _isBuffering = isBuffering);
+    }
+  }
+
+  void _toggleControls() {
+    setState(() => _showControls = !_showControls);
+    if (_showControls) {
+      _startHideTimer();
+    } else {
+      _hideTimer?.cancel();
+    }
+  }
+
+  void _startHideTimer() {
+    _hideTimer?.cancel();
+    _hideTimer = Timer(const Duration(seconds: 3), () {
+      if (mounted) setState(() => _showControls = false);
+    });
+  }
+
+  void _togglePlayPause() {
     setState(() {
-      _isBuffering = isBuffering;
-      _bufferingProgress = 0;
-      
-      if (!isBuffering && !_isInitialized && !_hasError) {
-        _isInitialized = true;
-      }
+      _isPlaying ? _player.controller.pause() : _player.controller.play();
+      _isPlaying = !_isPlaying;
     });
+    _startHideTimer();
   }
 
-  void _handlePlayerError(String error) {
-    _handleError("Player error: ${error})");
+  Widget _buildControlsOverlay() {
+    return AnimatedOpacity(
+      opacity: _showControls ? 1.0 : 0.0,
+      duration: const Duration(milliseconds: 300),
+      child: Stack(
+        children: [
+          // Play/Pause Center Button
+          Positioned.fill(
+            child: Center(
+              child: IconButton(
+                icon: Icon(
+                  _isPlaying ? Icons.pause : Icons.play_arrow,
+                  size: 48,
+                  color: Colors.white.withOpacity(0.8),
+                ),
+                onPressed: _togglePlayPause,
+              ),
+            ),
+          ),
+
+          // Bottom Controls Bar
+          Positioned(
+            bottom: 0,
+            left: 0,
+            right: 0,
+            child: Container(
+              height: 60,
+              decoration: BoxDecoration(
+                gradient: LinearGradient(
+                  begin: Alignment.bottomCenter,
+                  end: Alignment.topCenter,
+                  colors: [
+                    Colors.black.withOpacity(0.7),
+                    Colors.transparent,
+                  ],
+                ),
+              ),
+              child: Row(
+                children: [
+                  Expanded(
+                    child: VideoProgressBar(
+                      controller: _player.controller,
+                      onSeek: (duration) {
+                        _player.controller.seekTo(duration);
+                        _startHideTimer();
+                      },
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return VisibilityDetector(
+      key: Key(widget.videoUrl),
+      onVisibilityChanged: (info) => _handleVisibilityChange(info.visibleFraction > 0.5),
+      child: GestureDetector(
+        onTap: _toggleControls,
+        child: Stack(
+          alignment: Alignment.center,
+          children: [
+            // Video Display
+            if (_isInitialized)
+              Center(
+                child: AspectRatio(
+                  aspectRatio: _player.controller.value.aspectRatio,
+                  child: VideoPlayer(_player.controller),
+                ),
+              ),
+
+            // Controls Overlay
+            if (_isInitialized && !_hasError) _buildControlsOverlay(),
+
+            // Loading/buffering states
+            if (!_isInitialized || _isBuffering)
+              _buildLoadingState(),
+
+            // Error state
+            if (_hasError)
+              _buildErrorState(),
+
+            // Cache progress indicator
+            if (_downloadProgress > 0 && _downloadProgress < 1)
+              _buildCacheProgress(),
+
+            // Buffering indicator
+            if (_isBuffering && _isInitialized)
+              _buildBufferingOverlay(),
+          ],
+        ),
+      ),
+    );
   }
 
-  void _handleError(String message) {
+  // Keep existing helper methods (_handleError, _cacheVideo, _retryPlayback, 
+  // _handleVisibilityChange, _buildLoadingState, _buildBufferingOverlay,
+  // _buildCacheProgress, _buildErrorState) as provided in original code
+
+   void _handleError(String message) {
     print(message);
     if (!_hasError) setState(() => _hasError = true);
   }
@@ -111,33 +231,10 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
     _initializePlayer();
   }
 
-  @override
-  Widget build(BuildContext context) {
-    return GestureDetector(
-      child: Stack(
-        children: [
-          // Video display
-          if (_isInitialized)
-            Video(controller: _controller),
-          
-          // Loading/buffering states
-          if (!_isInitialized || _isBuffering)
-            _buildLoadingState(),
-          
-          // Error state
-          if (_hasError)
-            _buildErrorState(),
-          
-          // Cache progress indicator
-          if (_downloadProgress > 0 && _downloadProgress < 1)
-            _buildCacheProgress(),
-            
-          // Buffering indicator
-          if (_isBuffering && _isInitialized)
-            _buildBufferingOverlay(),
-        ],
-      ),
-    );
+  Future<void> _handleVisibilityChange(bool visible) async {
+    if (!visible) {
+      await _player.controller.pause();
+    }
   }
 
   Widget _buildLoadingState() {
@@ -247,8 +344,78 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
 
   @override
   void dispose() {
+    _hideTimer?.cancel();
+    _player.controller.removeListener(_videoListener);
     _player.dispose();
-    _cacheSubscription?.cancel();
     super.dispose();
   }
-}
+}
+
+class VideoProgressBar extends StatelessWidget {
+  final VideoPlayerController controller;
+  final Function(Duration) onSeek;
+
+  const VideoProgressBar({
+    super.key,
+    required this.controller,
+    required this.onSeek,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return ValueListenableBuilder(
+      valueListenable: controller,
+      builder: (context, value, child) {
+        final position = value.position;
+        final duration = value.duration;
+        
+        return Padding(
+          padding: const EdgeInsets.symmetric(horizontal: 16),
+          child: Row(
+            children: [
+              Text(
+                _formatDuration(position),
+                style: const TextStyle(color: Colors.white),
+              ),
+              Expanded(
+                child: SliderTheme(
+                  data: SliderTheme.of(context).copyWith(
+                    trackHeight: 2,
+                    thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
+                  ),
+                  child: Slider(
+                    min: 0,
+                    max: duration.inSeconds.toDouble(),
+                    value: position.inSeconds.toDouble().clamp(0, duration.inSeconds.toDouble()),
+                    onChanged: (value) => onSeek(Duration(seconds: value.toInt())),
+                    onChangeStart: (_) => controller.pause(),
+                    onChangeEnd: (_) => controller.play(),
+                    activeColor: Colors.red,
+                    inactiveColor: Colors.white.withOpacity(0.3),
+                  ),
+                ),
+              ),
+              Text(
+                _formatDuration(duration),
+                style: const TextStyle(color: Colors.white),
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+
+  String _formatDuration(Duration duration) {
+    String twoDigits(int n) => n.toString().padLeft(2, '0');
+    final hours = duration.inHours;
+    final minutes = duration.inMinutes.remainder(60);
+    final seconds = duration.inSeconds.remainder(60);
+    
+    return [
+      if (hours > 0) twoDigits(hours),
+      twoDigits(minutes),
+      twoDigits(seconds),
+    ].join(':').replaceFirst(RegExp(r'^0:'), '');
+  }
+}

+ 0 - 8
linux/flutter/generated_plugin_registrant.cc

@@ -6,18 +6,10 @@
 
 #include "generated_plugin_registrant.h"
 
-#include <media_kit_video/media_kit_video_plugin.h>
 #include <url_launcher_linux/url_launcher_plugin.h>
-#include <volume_controller/volume_controller_plugin.h>
 
 void fl_register_plugins(FlPluginRegistry* registry) {
-  g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
-      fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
-  media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
   g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
       fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
   url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
-  g_autoptr(FlPluginRegistrar) volume_controller_registrar =
-      fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin");
-  volume_controller_plugin_register_with_registrar(volume_controller_registrar);
 }

+ 0 - 2
linux/flutter/generated_plugins.cmake

@@ -3,9 +3,7 @@
 #
 
 list(APPEND FLUTTER_PLUGIN_LIST
-  media_kit_video
   url_launcher_linux
-  volume_controller
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST

+ 2 - 4
macos/Flutter/GeneratedPluginRegistrant.swift

@@ -7,28 +7,26 @@ import Foundation
 
 import audio_session
 import just_audio
-import media_kit_video
 import package_info_plus
 import path_provider_foundation
 import share_plus
+import shared_preferences_foundation
 import sqflite_darwin
 import url_launcher_macos
 import video_player_avfoundation
-import volume_controller
 import wakelock_plus
 import webview_flutter_wkwebview
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
   JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
-  MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
   FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
   SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
+  SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
   UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
   FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
-  VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
   WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
   WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
 }

+ 88 - 96
pubspec.lock

@@ -17,14 +17,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "7.4.5"
-  archive:
-    dependency: transitive
-    description:
-      name: archive
-      sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
-      url: "https://pub.dev"
-    source: hosted
-    version: "4.0.7"
   args:
     dependency: transitive
     description:
@@ -145,6 +137,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.1"
+  cached_video_player_plus:
+    dependency: "direct main"
+    description:
+      name: cached_video_player_plus
+      sha256: dcdf341d6c037b54c5e29b6a2fecb4cd2c9b9c19389d4b4402713b58008de028
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.3"
   characters:
     dependency: transitive
     description:
@@ -424,6 +424,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.2.4"
+  get:
+    dependency: transitive
+    description:
+      name: get
+      sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.7.2"
+  get_storage:
+    dependency: transitive
+    description:
+      name: get_storage
+      sha256: "39db1fffe779d0c22b3a744376e86febe4ade43bf65e06eab5af707dc84185a2"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
   glob:
     dependency: transitive
     description:
@@ -472,14 +488,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.1.2"
-  image:
-    dependency: transitive
-    description:
-      name: image
-      sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
-      url: "https://pub.dev"
-    source: hosted
-    version: "4.5.4"
   intl:
     dependency: "direct main"
     description:
@@ -608,30 +616,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.11.1"
-  media_kit:
-    dependency: "direct main"
-    description:
-      name: media_kit
-      sha256: "48c10c3785df5d88f0eef970743f8c99b2e5da2b34b9d8f9876e598f62d9e776"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.2.0"
-  media_kit_libs_android_video:
-    dependency: "direct main"
-    description:
-      name: media_kit_libs_android_video
-      sha256: adff9b571b8ead0867f9f91070f8df39562078c0eb3371d88b9029a2d547d7b7
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.3.7"
-  media_kit_video:
-    dependency: "direct main"
-    description:
-      name: media_kit_video
-      sha256: a656a9463298c1adc64c57f2d012874f7f2900f0c614d9545a3e7b8bb9e2137b
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.3.0"
   meta:
     dependency: transitive
     description:
@@ -840,14 +824,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.5.1"
-  posix:
-    dependency: transitive
-    description:
-      name: posix
-      sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62
-      url: "https://pub.dev"
-    source: hosted
-    version: "6.0.2"
   provider:
     dependency: "direct main"
     description:
@@ -888,46 +864,78 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.27.7"
-  safe_local_storage:
+  share_plus:
+    dependency: "direct main"
+    description:
+      name: share_plus
+      sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0
+      url: "https://pub.dev"
+    source: hosted
+    version: "11.0.0"
+  share_plus_platform_interface:
     dependency: transitive
     description:
-      name: safe_local_storage
-      sha256: e9a21b6fec7a8aa62cc2585ff4c1b127df42f3185adbd2aca66b47abe2e80236
+      name: share_plus_platform_interface
+      sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.1"
-  screen_brightness_android:
+    version: "6.0.0"
+  shared_preferences:
     dependency: transitive
     description:
-      name: screen_brightness_android
-      sha256: "6ba1b5812f66c64e9e4892be2d36ecd34210f4e0da8bdec6a2ea34f1aa42683e"
+      name: shared_preferences
+      sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.1"
-  screen_brightness_platform_interface:
+    version: "2.5.3"
+  shared_preferences_android:
     dependency: transitive
     description:
-      name: screen_brightness_platform_interface
-      sha256: "737bd47b57746bc4291cab1b8a5843ee881af499514881b0247ec77447ee769c"
+      name: shared_preferences_android
+      sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.0"
-  share_plus:
-    dependency: "direct main"
+    version: "2.4.11"
+  shared_preferences_foundation:
+    dependency: transitive
     description:
-      name: share_plus
-      sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0
+      name: shared_preferences_foundation
+      sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
       url: "https://pub.dev"
     source: hosted
-    version: "11.0.0"
-  share_plus_platform_interface:
+    version: "2.5.4"
+  shared_preferences_linux:
     dependency: transitive
     description:
-      name: share_plus_platform_interface
-      sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef"
+      name: shared_preferences_linux
+      sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
       url: "https://pub.dev"
     source: hosted
-    version: "6.0.0"
+    version: "2.4.1"
+  shared_preferences_platform_interface:
+    dependency: transitive
+    description:
+      name: shared_preferences_platform_interface
+      sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.1"
+  shared_preferences_web:
+    dependency: transitive
+    description:
+      name: shared_preferences_web
+      sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.3"
+  shared_preferences_windows:
+    dependency: transitive
+    description:
+      name: shared_preferences_windows
+      sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.1"
   shelf:
     dependency: transitive
     description:
@@ -1101,22 +1109,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.4.0"
-  universal_platform:
-    dependency: transitive
-    description:
-      name: universal_platform
-      sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.1.0"
-  uri_parser:
-    dependency: transitive
-    description:
-      name: uri_parser
-      sha256: ff4d2c720aca3f4f7d5445e23b11b2d15ef8af5ddce5164643f38ff962dcb270
-      url: "https://pub.dev"
-    source: hosted
-    version: "3.0.0"
   url_launcher:
     dependency: "direct main"
     description:
@@ -1222,13 +1214,13 @@ packages:
     source: hosted
     version: "2.1.4"
   video_player:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: video_player
-      sha256: "7d78f0cfaddc8c19d4cb2d3bebe1bfef11f2103b0a03e5398b303a1bf65eeb14"
+      sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a"
       url: "https://pub.dev"
     source: hosted
-    version: "2.9.5"
+    version: "2.10.0"
   video_player_android:
     dependency: transitive
     description:
@@ -1269,6 +1261,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.5.6"
+  visibility_detector:
+    dependency: "direct main"
+    description:
+      name: visibility_detector
+      sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.4.0+2"
   vm_service:
     dependency: transitive
     description:
@@ -1277,14 +1277,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "14.3.1"
-  volume_controller:
-    dependency: transitive
-    description:
-      name: volume_controller
-      sha256: d75039e69c0d90e7810bfd47e3eedf29ff8543ea7a10392792e81f9bded7edf5
-      url: "https://pub.dev"
-    source: hosted
-    version: "3.4.0"
   wakelock_plus:
     dependency: transitive
     description:

+ 4 - 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.7
+version: 1.0.9
 
 environment:
   sdk: ^3.7.2
@@ -46,14 +46,14 @@ dependencies:
   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
+  cached_video_player_plus: ^4.0.3
+  video_player: ^2.10.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
+  visibility_detector: ^0.4.0
 
 dev_dependencies:
   flutter_test:

+ 0 - 6
windows/flutter/generated_plugin_registrant.cc

@@ -6,21 +6,15 @@
 
 #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>
 
 void RegisterPlugins(flutter::PluginRegistry* registry) {
-  MediaKitVideoPluginCApiRegisterWithRegistrar(
-      registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
   PermissionHandlerWindowsPluginRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
   SharePlusWindowsPluginCApiRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
   UrlLauncherWindowsRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("UrlLauncherWindows"));
-  VolumeControllerPluginCApiRegisterWithRegistrar(
-      registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
 }

+ 0 - 2
windows/flutter/generated_plugins.cmake

@@ -3,11 +3,9 @@
 #
 
 list(APPEND FLUTTER_PLUGIN_LIST
-  media_kit_video
   permission_handler_windows
   share_plus
   url_launcher_windows
-  volume_controller
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST