diff --git a/lib/controls/entry_media_attachments/gallery_selector_control.dart b/lib/controls/entry_media_attachments/gallery_selector_control.dart index 2a08b86..dd74e2c 100644 --- a/lib/controls/entry_media_attachments/gallery_selector_control.dart +++ b/lib/controls/entry_media_attachments/gallery_selector_control.dart @@ -4,7 +4,7 @@ import '../../globals.dart'; import '../../models/image_entry.dart'; import '../../models/visibility.dart'; import '../../screens/existing_image_selector_screen.dart'; -import '../../screens/image_viewer_screen.dart'; +import '../../screens/media_viewer_screen.dart'; import '../../serializers/friendica/image_entry_friendica_extensions.dart'; import '../login_aware_cached_network_image.dart'; import '../padding.dart'; @@ -66,7 +66,7 @@ class _GallerySelectorControlState extends State { onDoubleTap: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => ImageViewerScreen( + builder: (context) => MediaViewerScreen( attachments: widget.entries .map((i) => i.toMediaAttachment()) .toList(), diff --git a/lib/controls/media_attachment_viewer_control.dart b/lib/controls/media_attachment_viewer_control.dart index 583a575..d75b9c1 100644 --- a/lib/controls/media_attachment_viewer_control.dart +++ b/lib/controls/media_attachment_viewer_control.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import '../models/attachment_media_type_enum.dart'; import '../models/media_attachment.dart'; -import '../screens/image_viewer_screen.dart'; +import '../screens/media_viewer_screen.dart'; import 'audio_video/av_control.dart'; import 'image_control.dart'; @@ -49,7 +49,7 @@ class _MediaAttachmentViewerControlState altText: item.description, onTap: () async { Navigator.push(context, MaterialPageRoute(builder: (context) { - return ImageViewerScreen( + return MediaViewerScreen( attachments: widget.attachments, initialIndex: widget.index, ); diff --git a/lib/main.dart b/lib/main.dart index ef3f5a1..dd60652 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,7 +29,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); // await dotenv.load(fileName: '.env'); - const enablePreview = false; + const enablePreview = true; Logger.root.level = Level.FINER; Logger.root.onRecord.listen((event) { final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName; diff --git a/lib/screens/existing_image_selector_screen.dart b/lib/screens/existing_image_selector_screen.dart index b3bab38..c0222d6 100644 --- a/lib/screens/existing_image_selector_screen.dart +++ b/lib/screens/existing_image_selector_screen.dart @@ -8,7 +8,7 @@ import '../models/visibility.dart'; import '../serializers/friendica/image_entry_friendica_extensions.dart'; import '../services/gallery_service.dart'; import '../utils/active_profile_selector.dart'; -import 'image_viewer_screen.dart'; +import 'media_viewer_screen.dart'; class ExistingImageSelectorScreen extends StatefulWidget { final Visibility? visibilityFilter; @@ -171,7 +171,7 @@ class _ExistingImageSelectorScreenState onDoubleTap: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => ImageViewerScreen( + builder: (context) => MediaViewerScreen( attachments: [image.toMediaAttachment()], ), ), diff --git a/lib/screens/gallery_screen.dart b/lib/screens/gallery_screen.dart index 3b9b6d7..7365be7 100644 --- a/lib/screens/gallery_screen.dart +++ b/lib/screens/gallery_screen.dart @@ -11,7 +11,7 @@ import '../serializers/friendica/image_entry_friendica_extensions.dart'; import '../services/gallery_service.dart'; import '../services/network_status_service.dart'; import '../utils/active_profile_selector.dart'; -import 'image_viewer_screen.dart'; +import 'media_viewer_screen.dart'; class GalleryScreen extends StatelessWidget { static const thumbnailDimension = 350.0; @@ -117,7 +117,7 @@ class GalleryScreen extends StatelessWidget { child: GestureDetector( onTap: () { Navigator.push(context, MaterialPageRoute(builder: (context) { - return ImageViewerScreen( + return MediaViewerScreen( attachments: attachments, initialIndex: index, ); diff --git a/lib/screens/image_viewer_screen.dart b/lib/screens/image_viewer_screen.dart deleted file mode 100644 index 9050df9..0000000 --- a/lib/screens/image_viewer_screen.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'dart:io'; - -import 'package:carousel_slider/carousel_slider.dart'; -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/login_aware_cached_network_image.dart'; -import '../friendica_client/friendica_client.dart'; -import '../globals.dart'; -import '../models/media_attachment.dart'; -import '../services/auth_service.dart'; -import '../utils/clipboard_utils.dart'; -import '../utils/snackbar_builder.dart'; - -class ImageViewerScreen extends StatefulWidget { - final List attachments; - final int initialIndex; - - const ImageViewerScreen({ - super.key, - required this.attachments, - this.initialIndex = 0, - }); - - @override - State createState() => _ImageViewerScreenState(); -} - -class _ImageViewerScreenState extends State { - MediaAttachment? currentAttachment; - - @override - void initState() { - super.initState(); - currentAttachment = widget.attachments[widget.initialIndex]; - } - - 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 width = MediaQuery.of(context).size.width; - final height = MediaQuery.of(context).size.height; - final carouselHeight = - widget.attachments.length == 1 ? height * 0.9 : height * 0.8; - return Scaffold( - appBar: AppBar( - actions: [ - IconButton( - onPressed: currentAttachment == null - ? null - : () => saveImage(context, currentAttachment!), - icon: const Icon(Icons.download)) - ], - ), - body: SafeArea( - child: Column( - children: [ - CarouselSlider.builder( - disableGesture: true, - itemCount: widget.attachments.length, - itemBuilder: (context, index, realIndex) { - return SizedBox( - width: width, - height: carouselHeight, - child: InteractiveViewer( - maxScale: 10.0, - scaleFactor: 400, - child: LoginAwareCachedNetworkImage( - imageUrl: widget.attachments[index].uri.toString()), - ), - ); - }, - options: CarouselOptions( - height: carouselHeight, - initialPage: widget.initialIndex, - enableInfiniteScroll: false, - enlargeCenterPage: true, - viewportFraction: 0.95, - onPageChanged: (index, reason) { - setState(() { - currentAttachment = widget.attachments[index]; - }); - }), - ), - if (currentAttachment != null) buildTextArea(currentAttachment!), - ], - ), - ), - ); - } - - Widget buildTextArea(MediaAttachment attachment) { - return Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: SingleChildScrollView( - child: Text(attachment.description), - )), - IconButton( - onPressed: () async { - await copyToClipboard( - context: context, - text: attachment.description, - message: 'Image description copied to clipboard'); - }, - icon: Icon(Icons.copy), - ), - ], - ), - ), - ); - } -} diff --git a/lib/screens/media_viewer_screen.dart b/lib/screens/media_viewer_screen.dart new file mode 100644 index 0000000..e69907c --- /dev/null +++ b/lib/screens/media_viewer_screen.dart @@ -0,0 +1,183 @@ +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/login_aware_cached_network_image.dart'; +import '../friendica_client/friendica_client.dart'; +import '../globals.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; + } + + 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 height = MediaQuery.of(context).size.height; + return Scaffold( + appBar: AppBar( + actions: [ + IconButton( + onPressed: () => saveImage(context, currentAttachment), + icon: const Icon(Icons.download)) + ], + ), + body: SafeArea( + child: Stack( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: InteractiveViewer( + maxScale: 10.0, + scaleFactor: 400, + child: LoginAwareCachedNetworkImage( + imageUrl: + widget.attachments[currentIndex].uri.toString()), + ), + ), + if (currentAttachment.description.isNotEmpty) + buildTextArea(currentAttachment), + ], + ), + if (widget.attachments.length > 1) ...[ + Positioned( + bottom: height * 0.5, + child: Opacity( + opacity: 0.8, + child: currentIndex < 1 + ? null + : Container( + color: Colors.black, + child: IconButton( + color: Colors.white, + onPressed: () { + setState(() { + currentIndex--; + }); + }, + icon: const Icon(Icons.chevron_left), + ), + ), + ), + ), + Positioned( + bottom: height * 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: () { + setState(() { + currentIndex++; + }); + }, + icon: const Icon(Icons.chevron_right), + ), + ), + ), + ), + ] + ], + ), + ), + ); + } + + Widget buildTextArea(MediaAttachment attachment) { + final height = MediaQuery.of(context).size.height * 0.1; + return Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + height: height, + child: attachment.description.isEmpty + ? null + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SingleChildScrollView( + child: Text(attachment.description), + )), + IconButton( + onPressed: () async { + await copyToClipboard( + context: context, + text: attachment.description, + message: 'Image description copied to clipboard'); + }, + icon: Icon(Icons.copy), + ), + ], + ), + ), + ); + } +}