322 lines
9.8 KiB
Dart
322 lines
9.8 KiB
Dart
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<ImageDetailScreen> createState() => _ImageDetailScreenState();
|
|
}
|
|
|
|
class _ImageDetailScreenState extends State<ImageDetailScreen> {
|
|
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<void> _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';
|
|
}
|
|
} |