|
@@ -1,7 +1,15 @@
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:photo_view/photo_view.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: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 '../models/post.dart';
|
|
|
|
+import 'package:veloe_kemono_party_flutter/main.dart';
|
|
|
|
+import 'dart:io';
|
|
|
|
|
|
class MediaCarouselItem {
|
|
class MediaCarouselItem {
|
|
final String url;
|
|
final String url;
|
|
@@ -40,6 +48,7 @@ class _MediaCarouselScreenState extends State<MediaCarouselScreen> {
|
|
late PageController _pageController;
|
|
late PageController _pageController;
|
|
late int _currentIndex;
|
|
late int _currentIndex;
|
|
late List<MediaCarouselItem> _mediaItems;
|
|
late List<MediaCarouselItem> _mediaItems;
|
|
|
|
+ bool _isSaving = false;
|
|
|
|
|
|
@override
|
|
@override
|
|
void initState() {
|
|
void initState() {
|
|
@@ -80,15 +89,43 @@ class _MediaCarouselScreenState extends State<MediaCarouselScreen> {
|
|
},
|
|
},
|
|
),
|
|
),
|
|
Positioned(
|
|
Positioned(
|
|
- top: 40,
|
|
|
|
|
|
+ top: 20,
|
|
left: 20,
|
|
left: 20,
|
|
child: IconButton(
|
|
child: IconButton(
|
|
icon: const Icon(Icons.close, color: Colors.white),
|
|
icon: const Icon(Icons.close, color: Colors.white),
|
|
onPressed: () => Navigator.pop(context),
|
|
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];
|
|
final post = posts[i];
|
|
for (int j = 0; j < post.attachments.length; j++) {
|
|
for (int j = 0; j < post.attachments.length; j++) {
|
|
final attachment = post.attachments[j];
|
|
final attachment = post.attachments[j];
|
|
|
|
+ if (attachment.isImage || attachment.isVideo)
|
|
items.add(
|
|
items.add(
|
|
MediaCarouselItem(
|
|
MediaCarouselItem(
|
|
url: attachment.link,
|
|
url: attachment.link,
|
|
@@ -119,5 +157,74 @@ class _MediaCarouselScreenState extends State<MediaCarouselScreen> {
|
|
}
|
|
}
|
|
return items;
|
|
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),
|
|
|
|
+ ),
|
|
|
|
+ );
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|