media_carousel_screen 7.8 KB

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