import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_file_dialog/flutter_file_dialog.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import '../controls/audio_video/av_control.dart'; import '../controls/login_aware_cached_network_image.dart'; import '../friendica_client/friendica_client.dart'; import '../globals.dart'; import '../models/attachment_media_type_enum.dart'; import '../models/media_attachment.dart'; import '../services/auth_service.dart'; import '../utils/clipboard_utils.dart'; import '../utils/snackbar_builder.dart'; class MediaViewerScreen extends StatefulWidget { final List attachments; final int initialIndex; const MediaViewerScreen({ super.key, required this.attachments, this.initialIndex = 0, }); @override State createState() => _MediaViewerScreenState(); } class _MediaViewerScreenState extends State { var currentIndex = 0; @override void initState() { super.initState(); currentIndex = widget.initialIndex; } void nextAttachment() { if (currentIndex >= widget.attachments.length - 1) { return; } setState(() { currentIndex++; }); } void previousAttachment() { if (currentIndex < 1) { return; } setState(() { currentIndex--; }); } Future saveImage( BuildContext context, MediaAttachment attachment, ) async { buildSnackbar(context, 'Downloading full image to save locally'); final appsDir = await getApplicationDocumentsDirectory(); final filename = p.basename(attachment.fullFileUri.path); final bytesResult = await RemoteFileClient(getIt().currentProfile) .getFileBytes(attachment.uri); if (bytesResult.isFailure && mounted) { buildSnackbar(context, 'Error getting full size version of file: ${bytesResult.error}'); return; } if (Platform.isAndroid || Platform.isIOS) { final params = SaveFileDialogParams( data: bytesResult.value, fileName: filename, ); await FlutterFileDialog.saveFile(params: params); } else { final location = await FilePicker.platform.saveFile( dialogTitle: 'Save Image', fileName: filename, initialDirectory: appsDir.path, ); if (location != null) { await File(location).writeAsBytes(bytesResult.value); } } } @override Widget build(BuildContext context) { final currentAttachment = widget.attachments[currentIndex]; final width = MediaQuery.of(context).size.width; final height = MediaQuery.of(context).size.height * 0.9; final descriptionHeightPct = widget.attachments.isEmpty ? 0.0 : 0.1; final mediaHeight = height * (1 - descriptionHeightPct); final descriptionHeight = height * descriptionHeightPct; late final bool canSave; late final Widget mediaViewer; switch (currentAttachment.explicitType) { case AttachmentMediaType.image: canSave = true; mediaViewer = SizedBox( width: width, height: mediaHeight, child: InteractiveViewer( maxScale: 10.0, scaleFactor: 400, child: LoginAwareCachedNetworkImage( imageUrl: currentAttachment.uri.toString()), )); break; case AttachmentMediaType.unknown: case AttachmentMediaType.video: canSave = false; if (Platform.isLinux) { mediaViewer = Text( 'No media viewer for ${currentAttachment.explicitType.name} type for link ${currentAttachment.fullFileUri}'); } else { mediaViewer = SizedBox( width: width, height: mediaHeight, child: AVControl( videoUrl: currentAttachment.fullFileUri.toString(), description: currentAttachment.description, ), ); } break; } return Scaffold( appBar: AppBar( actions: [ if (canSave) IconButton( onPressed: () => saveImage(context, currentAttachment), icon: const Icon(Icons.download)) ], ), body: SafeArea( child: Stack( children: [ Column( mainAxisAlignment: MainAxisAlignment.start, children: [ mediaViewer, if (currentAttachment.description.isNotEmpty) buildTextArea(currentAttachment, descriptionHeight), ], ), if (widget.attachments.length > 1) ...[ Positioned( bottom: mediaHeight * 0.5, child: Opacity( opacity: 0.8, child: currentIndex < 1 ? null : Container( color: Colors.black, child: IconButton( color: Colors.white, onPressed: previousAttachment, icon: const Icon(Icons.chevron_left), ), ), ), ), Positioned( bottom: mediaHeight * 0.5, right: 0.0, child: Opacity( opacity: 0.8, child: currentIndex >= widget.attachments.length - 1 ? null : Container( color: Colors.black, child: IconButton( color: Colors.white, onPressed: nextAttachment, icon: const Icon(Icons.chevron_right), ), ), ), ), ] ], ), ), ); } Widget buildTextArea(MediaAttachment attachment, double height) { return Padding( padding: const EdgeInsets.only(left: 8.0, right: 8.0), child: SizedBox( height: height, child: attachment.description.isEmpty ? null : Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Scrollbar( controller: ScrollController(), thumbVisibility: true, child: SingleChildScrollView( child: Text(attachment.description), ), )), IconButton( onPressed: () async { await copyToClipboard( context: context, text: attachment.description, message: 'Image description copied to clipboard'); }, icon: const Icon(Icons.copy), ), ], ), ), ); } }