import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/services.dart'; import 'dart:typed_data'; import 'package:http/http.dart' as http; import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart'; import 'package:tugas_akhir_supabase/utils/plugin_utils.dart'; class ImageDetailScreen extends StatefulWidget { final String imageUrl; final String senderName; final DateTime timestamp; final String? heroTag; const ImageDetailScreen({ Key? key, required this.imageUrl, required this.senderName, required this.timestamp, this.heroTag, }) : super(key: key); @override State createState() => _ImageDetailScreenState(); } class _ImageDetailScreenState extends State { final TransformationController _transformationController = TransformationController(); bool _isFullScreen = false; bool _isDownloading = false; @override void initState() { super.initState(); // Set preferred orientations to allow rotation SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); } @override void dispose() { // Reset to portrait only when exiting SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); _transformationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: _isFullScreen ? null : AppBar( backgroundColor: Colors.black, elevation: 0, title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.senderName, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), Text( _formatDateTime(widget.timestamp), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.normal, ), ), ], ), actions: [ _isDownloading ? Container( margin: const EdgeInsets.symmetric(horizontal: 16), width: 24, height: 24, child: const CircularProgressIndicator( color: Colors.white, strokeWidth: 2, ), ) : IconButton( icon: const Icon(Icons.download), onPressed: () => _downloadImage(), tooltip: 'Simpan', ), ], ), body: GestureDetector( onTap: () { setState(() { _isFullScreen = !_isFullScreen; }); }, child: Stack( children: [ // Image with zoom capability Center( child: InteractiveViewer( transformationController: _transformationController, minScale: 0.5, maxScale: 4.0, child: Hero( tag: widget.heroTag ?? widget.imageUrl, child: CachedNetworkImage( imageUrl: widget.imageUrl, fit: BoxFit.contain, placeholder: (context, url) => Center( child: CircularProgressIndicator( color: Colors.white, ), ), errorWidget: (context, url, error) => Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error, color: Colors.red, size: 48), SizedBox(height: 16), Text( 'Gagal memuat gambar', style: TextStyle(color: Colors.white), ), ], ), ), ), ), ), // Bottom controls if (!_isFullScreen) Positioned( bottom: 20, left: 0, right: 0, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildControlButton( icon: Icons.zoom_in, label: 'Perbesar', onTap: () => _zoomIn(), ), const SizedBox(width: 24), _buildControlButton( icon: Icons.zoom_out, label: 'Perkecil', onTap: () => _zoomOut(), ), const SizedBox(width: 24), _buildControlButton( icon: Icons.refresh, label: 'Reset', onTap: () => _resetZoom(), ), ], ), ), ], ), ), ); } Widget _buildControlButton({ required IconData icon, required String label, required VoidCallback onTap, }) { return InkWell( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: Colors.white, size: 20), const SizedBox(width: 4), Text( label, style: const TextStyle( color: Colors.white, fontSize: 12, ), ), ], ), ), ); } void _zoomIn() { final Matrix4 currentMatrix = _transformationController.value; final Matrix4 newMatrix = currentMatrix.clone()..scale(1.25); _transformationController.value = newMatrix; } void _zoomOut() { final Matrix4 currentMatrix = _transformationController.value; final Matrix4 newMatrix = currentMatrix.clone()..scale(0.8); _transformationController.value = newMatrix; } void _resetZoom() { _transformationController.value = Matrix4.identity(); } Future _downloadImage() async { if (_isDownloading) return; setState(() { _isDownloading = true; }); try { // Check storage permission final status = await Permission.storage.request(); if (!status.isGranted) { _showMessage('Izin penyimpanan ditolak'); setState(() => _isDownloading = false); return; } // Download image final response = await http.get(Uri.parse(widget.imageUrl)); if (response.statusCode != 200) { throw Exception('Gagal mengunduh gambar'); } // Save to temp file final tempDir = await PluginUtils.getSafeTemporaryDirectory(); final fileName = 'TaniSMART_${DateTime.now().millisecondsSinceEpoch}.jpg'; final tempFilePath = '${tempDir.path}/$fileName'; final file = File(tempFilePath); await file.writeAsBytes(response.bodyBytes); // Use share_plus to save to gallery // This will show the share sheet, which includes "Save to Gallery" option on most devices final result = await Share.shareXFiles( [XFile(tempFilePath)], text: 'Foto dari TaniSMART', subject: fileName, ); if (result.status == ShareResultStatus.success) { _showMessage('Gambar berhasil dibagikan'); } else if (result.status == ShareResultStatus.dismissed) { // User dismissed the share dialog // Also save to app documents as fallback try { final docDir = await getApplicationDocumentsDirectory(); final savedFilePath = '${docDir.path}/$fileName'; await file.copy(savedFilePath); _showMessage('Gambar disimpan di folder aplikasi'); } catch (e) { print('Error saving to documents: $e'); } } // Clean up temp file if (await file.exists()) { try { await file.delete(); } catch (e) { print('Error deleting temp file: $e'); } } } catch (e) { _showMessage('Gagal menyimpan gambar: ${e.toString()}'); print('Download error: $e'); } finally { if (mounted) { setState(() { _isDownloading = false; }); } } } void _showMessage(String message) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), duration: const Duration(seconds: 2), ), ); } String _formatDateTime(DateTime dateTime) { // Format date: "1 Jan 2023, 14:30" final day = dateTime.day.toString(); final months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Ags', 'Sep', 'Okt', 'Nov', 'Des']; final month = months[dateTime.month - 1]; final year = dateTime.year.toString(); final hour = dateTime.hour.toString().padLeft(2, '0'); final minute = dateTime.minute.toString().padLeft(2, '0'); return '$day $month $year, $hour:$minute'; } }