media_carousel_screen 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  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. 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:PhotoView(
  100. imageProvider: provider,
  101. backgroundDecoration: const BoxDecoration(color: Colors.black),
  102. ));
  103. } else {
  104. return VideoPlayerWidget(videoUrl: item.url);
  105. }
  106. },
  107. ),
  108. Positioned(
  109. top: 20,
  110. left: 20,
  111. child: IconButton(
  112. icon: const Icon(Icons.close, color: Colors.white),
  113. onPressed: () => Navigator.pop(context),
  114. ),
  115. ),
  116. Positioned(
  117. top: 20,
  118. right: 20,
  119. child: PopupMenuButton<String>(
  120. onSelected: (value) => _handleMenuSelection(value),
  121. itemBuilder: (context) => [
  122. PopupMenuItem(
  123. value: 'save',
  124. child: Row(
  125. children: [
  126. Icon(Icons.save_alt, color: Theme.of(context).primaryColor),
  127. const SizedBox(width: 10),
  128. const Text('Save to Gallery'),
  129. ],
  130. ),
  131. ),
  132. // Add more options here if needed
  133. ],
  134. icon: const Icon(Icons.more_vert, color: Colors.white),
  135. ),
  136. ),
  137. // Saving indicator
  138. if (_isSaving)
  139. const Center(
  140. child: CircularProgressIndicator(color: Colors.white),
  141. ),
  142. ],
  143. ),
  144. )
  145. );
  146. }
  147. Future<void> _loadMoreIfNeeded() async {
  148. if (_currentIndex >= _mediaItems.length - 3) {
  149. await widget.loadMorePosts();
  150. setState(() {
  151. _mediaItems = _compileMediaItems(widget.allPosts);
  152. });
  153. }
  154. }
  155. List<MediaCarouselItem> _compileMediaItems(List<Post> posts) {
  156. List<MediaCarouselItem> items = [];
  157. for (int i = 0; i < posts.length; i++) {
  158. final post = posts[i];
  159. for (int j = 0; j < post.attachments.length; j++) {
  160. final attachment = post.attachments[j];
  161. if (attachment.isImage || attachment.isVideo)
  162. items.add(
  163. MediaCarouselItem(
  164. url: attachment.link,
  165. type: attachment.isImage ? "image" : "video",
  166. postIndex: i,
  167. attachmentIndex: j,
  168. ),
  169. );
  170. }
  171. }
  172. return items;
  173. }
  174. void _handleMenuSelection(String value) async {
  175. if (value == 'save') {
  176. await _saveCurrentMedia();
  177. }
  178. }
  179. Future<void> _saveCurrentMedia() async {
  180. setState(() => _isSaving = true);
  181. try {
  182. final item = _mediaItems[_currentIndex];
  183. final status = await Permission.storage.request();
  184. if (status.isPermanentlyDenied) {
  185. openAppSettings();
  186. }
  187. if (!status.isGranted) {
  188. _showSnackBar('Storage permission denied');
  189. return;
  190. }
  191. final cachedFile = await imageCacheManager.getFileFromCache(item.url);
  192. if (cachedFile is FileInfo)
  193. {
  194. //if (Platform.isAndroid) {
  195. if (item.type == "image"){
  196. await GallerySaver.saveImage(cachedFile.file.path);
  197. }
  198. else {
  199. await GallerySaver.saveVideo(cachedFile.file.path);
  200. }
  201. //}
  202. _showSnackBar('Media saved to gallery');
  203. }
  204. else
  205. _showSnackBar('Cache media first!');
  206. } catch (e) {
  207. debugPrint('Save error: $e');
  208. _showSnackBar('Failed to save media');
  209. } finally {
  210. setState(() => _isSaving = false);
  211. }
  212. }
  213. Future<Directory> getGalleryDirectory(String type) async {
  214. final externalDir = await getExternalStorageDirectory();
  215. final galleryPath = type == 'image'
  216. ? path.join(externalDir!.path, 'Pictures', 'Kemono Party')
  217. : path.join(externalDir!.path, 'Movies', 'Kemono Party');
  218. final dir = Directory(galleryPath);
  219. if (!await dir.exists()) {
  220. await dir.create(recursive: true);
  221. }
  222. return dir;
  223. }
  224. void _showSnackBar(String message) {
  225. ScaffoldMessenger.of(context).showSnackBar(
  226. SnackBar(
  227. content: Text(message),
  228. duration: const Duration(seconds: 2),
  229. ),
  230. );
  231. }
  232. }