media_carousel_screen 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import 'package:flutter/material.dart';
  2. import 'package:photo_view/photo_view.dart';
  3. import 'package:flutter_cache_manager/flutter_cache_manager.dart';
  4. import 'package:cached_network_image/cached_network_image.dart';
  5. import 'package:veloe_kemono_party_flutter/pages/widgets/video_player_widget';
  6. import 'package:path_provider/path_provider.dart';
  7. import 'package:permission_handler/permission_handler.dart';
  8. import 'package:gallery_saver_plus/gallery_saver.dart';
  9. import 'package:path/path.dart' as path;
  10. import '../models/post.dart';
  11. import 'package:veloe_kemono_party_flutter/main.dart';
  12. import 'dart:io';
  13. class MediaCarouselItem {
  14. final String url;
  15. final String type; // 'image' or 'video'
  16. final int postIndex;
  17. final int attachmentIndex;
  18. MediaCarouselItem({
  19. required this.url,
  20. required this.type,
  21. required this.postIndex,
  22. required this.attachmentIndex,
  23. });
  24. }
  25. class MediaCarouselScreen extends StatefulWidget {
  26. final int initialIndex;
  27. final List<MediaCarouselItem> mediaItems;
  28. final Future<void> Function() loadMorePosts;
  29. final List<Post> allPosts;
  30. const MediaCarouselScreen({
  31. super.key,
  32. required this.initialIndex,
  33. required this.mediaItems,
  34. required this.loadMorePosts,
  35. required this.allPosts,
  36. });
  37. @override
  38. State<MediaCarouselScreen> createState() => _MediaCarouselScreenState();
  39. }
  40. class _MediaCarouselScreenState extends State<MediaCarouselScreen> {
  41. late PageController _pageController;
  42. late int _currentIndex;
  43. late List<MediaCarouselItem> _mediaItems;
  44. bool _isSaving = false;
  45. @override
  46. void initState() {
  47. super.initState();
  48. _currentIndex = widget.initialIndex;
  49. _mediaItems = widget.mediaItems;
  50. _pageController = PageController(initialPage: widget.initialIndex);
  51. }
  52. @override
  53. Widget build(BuildContext context) {
  54. return Scaffold(
  55. backgroundColor: Colors.black,
  56. body: Stack(
  57. children: [
  58. PageView.builder(
  59. controller: _pageController,
  60. itemCount: _mediaItems.length,
  61. onPageChanged: (index) {
  62. setState(() {
  63. _currentIndex = index;
  64. });
  65. // Check if we are near the end and load more
  66. if (index >= _mediaItems.length - 3) {
  67. _loadMoreIfNeeded();
  68. }
  69. },
  70. itemBuilder: (context, index) {
  71. final item = _mediaItems[index];
  72. if (item.type == 'image') {
  73. return PhotoView(
  74. imageProvider: NetworkImage(item.url),
  75. backgroundDecoration: const BoxDecoration(color: Colors.black),
  76. );
  77. } else {
  78. return VideoPlayerWidget(videoUrl: item.url);
  79. }
  80. },
  81. ),
  82. Positioned(
  83. top: 20,
  84. left: 20,
  85. child: IconButton(
  86. icon: const Icon(Icons.close, color: Colors.white),
  87. onPressed: () => Navigator.pop(context),
  88. ),
  89. ),
  90. Positioned(
  91. top: 20,
  92. right: 20,
  93. child: PopupMenuButton<String>(
  94. onSelected: (value) => _handleMenuSelection(value),
  95. itemBuilder: (context) => [
  96. PopupMenuItem(
  97. value: 'save',
  98. child: Row(
  99. children: [
  100. Icon(Icons.save_alt, color: Theme.of(context).primaryColor),
  101. const SizedBox(width: 10),
  102. const Text('Save to Gallery'),
  103. ],
  104. ),
  105. ),
  106. // Add more options here if needed
  107. ],
  108. icon: const Icon(Icons.more_vert, color: Colors.white),
  109. ),
  110. ),
  111. // Saving indicator
  112. if (_isSaving)
  113. const Center(
  114. child: CircularProgressIndicator(color: Colors.white),
  115. ),
  116. ],
  117. ),
  118. )
  119. );
  120. }
  121. Future<void> _loadMoreIfNeeded() async {
  122. if (_currentIndex >= _mediaItems.length - 3) {
  123. await widget.loadMorePosts();
  124. setState(() {
  125. _mediaItems = _compileMediaItems(widget.allPosts);
  126. });
  127. }
  128. }
  129. List<MediaCarouselItem> _compileMediaItems(List<Post> posts) {
  130. List<MediaCarouselItem> items = [];
  131. for (int i = 0; i < posts.length; i++) {
  132. final post = posts[i];
  133. for (int j = 0; j < post.attachments.length; j++) {
  134. final attachment = post.attachments[j];
  135. if (attachment.isImage || attachment.isVideo)
  136. items.add(
  137. MediaCarouselItem(
  138. url: attachment.link,
  139. type: attachment.isImage ? "image" : "video",
  140. postIndex: i,
  141. attachmentIndex: j,
  142. ),
  143. );
  144. }
  145. }
  146. return items;
  147. }
  148. void _handleMenuSelection(String value) async {
  149. if (value == 'save') {
  150. await _saveCurrentMedia();
  151. }
  152. }
  153. Future<void> _saveCurrentMedia() async {
  154. setState(() => _isSaving = true);
  155. try {
  156. final item = _mediaItems[_currentIndex];
  157. final status = await Permission.storage.request();
  158. if (status.isPermanentlyDenied) {
  159. openAppSettings();
  160. }
  161. if (!status.isGranted) {
  162. _showSnackBar('Storage permission denied');
  163. return;
  164. }
  165. final cachedFile = await imageCacheManager.getFileFromCache(item.url);
  166. if (cachedFile is FileInfo)
  167. {
  168. //if (Platform.isAndroid) {
  169. if (item.type == "image"){
  170. await GallerySaver.saveImage(cachedFile.file.path);
  171. }
  172. else {
  173. await GallerySaver.saveVideo(cachedFile.file.path);
  174. }
  175. //}
  176. _showSnackBar('Media saved to gallery');
  177. }
  178. else
  179. _showSnackBar('Cache media first!');
  180. } catch (e) {
  181. debugPrint('Save error: $e');
  182. _showSnackBar('Failed to save media');
  183. } finally {
  184. setState(() => _isSaving = false);
  185. }
  186. }
  187. Future<Directory> getGalleryDirectory(String type) async {
  188. final externalDir = await getExternalStorageDirectory();
  189. final galleryPath = type == 'image'
  190. ? path.join(externalDir!.path, 'Pictures', 'Kemono Party')
  191. : path.join(externalDir!.path, 'Movies', 'Kemono Party');
  192. final dir = Directory(galleryPath);
  193. if (!await dir.exists()) {
  194. await dir.create(recursive: true);
  195. }
  196. return dir;
  197. }
  198. void _showSnackBar(String message) {
  199. ScaffoldMessenger.of(context).showSnackBar(
  200. SnackBar(
  201. content: Text(message),
  202. duration: const Duration(seconds: 2),
  203. ),
  204. );
  205. }
  206. }