diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index a2fcf23b3..e175f5e4c 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -19,6 +19,7 @@ class ImageBubble extends StatelessWidget { final double height; final void Function()? onTap; final BorderRadius? borderRadius; + final Timeline? timeline; const ImageBubble( this.event, { @@ -32,6 +33,7 @@ class ImageBubble extends StatelessWidget { this.animated = false, this.onTap, this.borderRadius, + this.timeline, super.key, }); @@ -62,6 +64,7 @@ class ImageBubble extends StatelessWidget { context: context, builder: (_) => ImageViewer( event, + timeline: timeline, outerContext: context, ), ); diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index c89d617db..f661519ba 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -396,6 +396,7 @@ class Message extends StatelessWidget { textColor: textColor, onInfoTab: onInfoTab, borderRadius: borderRadius, + timeline: timeline, ), if (event.hasAggregatedEvents( timeline, diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 8f5d47485..3879e0bd9 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -28,11 +28,13 @@ class MessageContent extends StatelessWidget { final Color textColor; final void Function(Event)? onInfoTab; final BorderRadius borderRadius; + final Timeline timeline; const MessageContent( this.event, { this.onInfoTab, super.key, + required this.timeline, required this.textColor, required this.borderRadius, }); @@ -137,6 +139,7 @@ class MessageContent extends StatelessWidget { height: height, fit: fit, borderRadius: borderRadius, + timeline: timeline, ); case CuteEventContent.eventType: return CuteContent(event); diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index 07601ee60..f56b7212e 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -10,28 +10,61 @@ import '../../utils/matrix_sdk_extensions/event_extension.dart'; class ImageViewer extends StatefulWidget { final Event event; + final Timeline? timeline; final BuildContext outerContext; - const ImageViewer(this.event, {required this.outerContext, super.key}); + const ImageViewer( + this.event, { + required this.outerContext, + this.timeline, + super.key, + }); @override ImageViewerController createState() => ImageViewerController(); } class ImageViewerController extends State { + @override + void initState() { + super.initState(); + allEvents = widget.timeline?.events + .where((event) => event.messageType == MessageTypes.Image) + .toList() + .reversed + .toList() ?? + [widget.event]; + var index = + allEvents.indexWhere((event) => event.eventId == widget.event.eventId); + if (index < 0) index = 0; + pageController = PageController(initialPage: index); + } + + late final PageController pageController; + + late final List allEvents; + + int get _index => pageController.page?.toInt() ?? 0; + + Event get currentEvent => allEvents[_index]; + + bool get canGoNext => _index < allEvents.length - 1; + + bool get canGoBack => _index > 0; + /// Forward this image to another room. void forwardAction() => showScaffoldDialog( context: context, builder: (context) => ShareScaffoldDialog( - items: [ContentShareItem(widget.event.content)], + items: [ContentShareItem(currentEvent.content)], ), ); /// Save this file with a system call. - void saveFileAction(BuildContext context) => widget.event.saveFile(context); + void saveFileAction(BuildContext context) => currentEvent.saveFile(context); /// Save this file with a system call. - void shareFileAction(BuildContext context) => widget.event.shareFile(context); + void shareFileAction(BuildContext context) => currentEvent.shareFile(context); static const maxScaleFactor = 1.5; diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index b727cc92b..75c4af73a 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'image_viewer.dart'; @@ -13,73 +15,109 @@ class ImageViewerView extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( + final iconButtonStyle = IconButton.styleFrom( backgroundColor: Colors.black.withAlpha(128), - extendBodyBehindAppBar: true, - appBar: AppBar( - elevation: 0, - leading: IconButton( - style: IconButton.styleFrom( - backgroundColor: Colors.black.withAlpha(128), - ), - icon: const Icon(Icons.close), - onPressed: Navigator.of(context).pop, - color: Colors.white, - tooltip: L10n.of(context).close, - ), - backgroundColor: Colors.transparent, - actions: [ - IconButton( - style: IconButton.styleFrom( - backgroundColor: Colors.black.withAlpha(128), - ), - icon: const Icon(Icons.reply_outlined), - onPressed: controller.forwardAction, + foregroundColor: Colors.white, + ); + return GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Scaffold( + backgroundColor: Colors.black.withAlpha(128), + extendBodyBehindAppBar: true, + appBar: AppBar( + elevation: 0, + leading: IconButton( + style: iconButtonStyle, + icon: const Icon(Icons.close), + onPressed: Navigator.of(context).pop, color: Colors.white, - tooltip: L10n.of(context).share, + tooltip: L10n.of(context).close, ), - const SizedBox(width: 8), - IconButton( - style: IconButton.styleFrom( - backgroundColor: Colors.black.withAlpha(128), + backgroundColor: Colors.transparent, + actions: [ + IconButton( + style: iconButtonStyle, + icon: const Icon(Icons.reply_outlined), + onPressed: controller.forwardAction, + color: Colors.white, + tooltip: L10n.of(context).share, ), - icon: const Icon(Icons.download_outlined), - onPressed: () => controller.saveFileAction(context), - color: Colors.white, - tooltip: L10n.of(context).downloadFile, - ), - const SizedBox(width: 8), - if (PlatformInfos.isMobile) - // Use builder context to correctly position the share dialog on iPad - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Builder( - builder: (context) => IconButton( - style: IconButton.styleFrom( - backgroundColor: Colors.black.withAlpha(128), + const SizedBox(width: 8), + IconButton( + style: iconButtonStyle, + icon: const Icon(Icons.download_outlined), + onPressed: () => controller.saveFileAction(context), + color: Colors.white, + tooltip: L10n.of(context).downloadFile, + ), + const SizedBox(width: 8), + if (PlatformInfos.isMobile) + // Use builder context to correctly position the share dialog on iPad + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Builder( + builder: (context) => IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.black.withAlpha(128), + ), + onPressed: () => controller.shareFileAction(context), + tooltip: L10n.of(context).share, + color: Colors.white, + icon: Icon(Icons.adaptive.share_outlined), ), - onPressed: () => controller.shareFileAction(context), - tooltip: L10n.of(context).share, - color: Colors.white, - icon: Icon(Icons.adaptive.share_outlined), ), ), - ), - ], - ), - body: InteractiveViewer( - minScale: 1.0, - maxScale: 10.0, - onInteractionEnd: controller.onInteractionEnds, - child: Center( - child: Hero( - tag: controller.widget.event.eventId, - child: MxcImage( - event: controller.widget.event, - fit: BoxFit.contain, - isThumbnail: false, - animated: true, - ), + ], + ), + body: HoverBuilder( + builder: (context, hovered) => Stack( + children: [ + PageView.builder( + controller: controller.pageController, + itemCount: controller.allEvents.length, + itemBuilder: (context, i) => InteractiveViewer( + minScale: 1.0, + maxScale: 10.0, + onInteractionEnd: controller.onInteractionEnds, + child: Center( + child: Hero( + tag: controller.allEvents[i].eventId, + child: MxcImage( + key: ValueKey(controller.allEvents[i].eventId), + event: controller.allEvents[i], + fit: BoxFit.contain, + isThumbnail: false, + animated: true, + ), + ), + ), + ), + ), + if (hovered && controller.canGoBack) + Align( + alignment: Alignment.centerLeft, + child: IconButton( + style: iconButtonStyle, + icon: const Icon(Icons.chevron_left_outlined), + onPressed: () => controller.pageController.previousPage( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + ), + ), + ), + if (hovered && controller.canGoNext) + Align( + alignment: Alignment.centerRight, + child: IconButton( + style: iconButtonStyle, + icon: const Icon(Icons.chevron_right_outlined), + onPressed: () => controller.pageController.nextPage( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + ), + ), + ), + ], ), ), ),