feat: Swipe to next or previous image in image viewer

pull/1535/merge
Krille 7 months ago
parent 234998a46e
commit fb685c03cf
No known key found for this signature in database
GPG Key ID: E067ECD60F1A0652

@ -19,6 +19,7 @@ class ImageBubble extends StatelessWidget {
final double height; final double height;
final void Function()? onTap; final void Function()? onTap;
final BorderRadius? borderRadius; final BorderRadius? borderRadius;
final Timeline? timeline;
const ImageBubble( const ImageBubble(
this.event, { this.event, {
@ -32,6 +33,7 @@ class ImageBubble extends StatelessWidget {
this.animated = false, this.animated = false,
this.onTap, this.onTap,
this.borderRadius, this.borderRadius,
this.timeline,
super.key, super.key,
}); });
@ -62,6 +64,7 @@ class ImageBubble extends StatelessWidget {
context: context, context: context,
builder: (_) => ImageViewer( builder: (_) => ImageViewer(
event, event,
timeline: timeline,
outerContext: context, outerContext: context,
), ),
); );

@ -396,6 +396,7 @@ class Message extends StatelessWidget {
textColor: textColor, textColor: textColor,
onInfoTab: onInfoTab, onInfoTab: onInfoTab,
borderRadius: borderRadius, borderRadius: borderRadius,
timeline: timeline,
), ),
if (event.hasAggregatedEvents( if (event.hasAggregatedEvents(
timeline, timeline,

@ -28,11 +28,13 @@ class MessageContent extends StatelessWidget {
final Color textColor; final Color textColor;
final void Function(Event)? onInfoTab; final void Function(Event)? onInfoTab;
final BorderRadius borderRadius; final BorderRadius borderRadius;
final Timeline timeline;
const MessageContent( const MessageContent(
this.event, { this.event, {
this.onInfoTab, this.onInfoTab,
super.key, super.key,
required this.timeline,
required this.textColor, required this.textColor,
required this.borderRadius, required this.borderRadius,
}); });
@ -137,6 +139,7 @@ class MessageContent extends StatelessWidget {
height: height, height: height,
fit: fit, fit: fit,
borderRadius: borderRadius, borderRadius: borderRadius,
timeline: timeline,
); );
case CuteEventContent.eventType: case CuteEventContent.eventType:
return CuteContent(event); return CuteContent(event);

@ -10,28 +10,61 @@ import '../../utils/matrix_sdk_extensions/event_extension.dart';
class ImageViewer extends StatefulWidget { class ImageViewer extends StatefulWidget {
final Event event; final Event event;
final Timeline? timeline;
final BuildContext outerContext; final BuildContext outerContext;
const ImageViewer(this.event, {required this.outerContext, super.key}); const ImageViewer(
this.event, {
required this.outerContext,
this.timeline,
super.key,
});
@override @override
ImageViewerController createState() => ImageViewerController(); ImageViewerController createState() => ImageViewerController();
} }
class ImageViewerController extends State<ImageViewer> { class ImageViewerController extends State<ImageViewer> {
@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<Event> 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. /// Forward this image to another room.
void forwardAction() => showScaffoldDialog( void forwardAction() => showScaffoldDialog(
context: context, context: context,
builder: (context) => ShareScaffoldDialog( builder: (context) => ShareScaffoldDialog(
items: [ContentShareItem(widget.event.content)], items: [ContentShareItem(currentEvent.content)],
), ),
); );
/// Save this file with a system call. /// 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. /// 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; static const maxScaleFactor = 1.5;

@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.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/utils/platform_infos.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:fluffychat/widgets/mxc_image.dart';
import 'image_viewer.dart'; import 'image_viewer.dart';
@ -13,15 +15,19 @@ class ImageViewerView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( final iconButtonStyle = IconButton.styleFrom(
backgroundColor: Colors.black.withAlpha(128),
foregroundColor: Colors.white,
);
return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Scaffold(
backgroundColor: Colors.black.withAlpha(128), backgroundColor: Colors.black.withAlpha(128),
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
style: IconButton.styleFrom( style: iconButtonStyle,
backgroundColor: Colors.black.withAlpha(128),
),
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop, onPressed: Navigator.of(context).pop,
color: Colors.white, color: Colors.white,
@ -30,9 +36,7 @@ class ImageViewerView extends StatelessWidget {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
actions: [ actions: [
IconButton( IconButton(
style: IconButton.styleFrom( style: iconButtonStyle,
backgroundColor: Colors.black.withAlpha(128),
),
icon: const Icon(Icons.reply_outlined), icon: const Icon(Icons.reply_outlined),
onPressed: controller.forwardAction, onPressed: controller.forwardAction,
color: Colors.white, color: Colors.white,
@ -40,9 +44,7 @@ class ImageViewerView extends StatelessWidget {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
IconButton( IconButton(
style: IconButton.styleFrom( style: iconButtonStyle,
backgroundColor: Colors.black.withAlpha(128),
),
icon: const Icon(Icons.download_outlined), icon: const Icon(Icons.download_outlined),
onPressed: () => controller.saveFileAction(context), onPressed: () => controller.saveFileAction(context),
color: Colors.white, color: Colors.white,
@ -67,15 +69,22 @@ class ImageViewerView extends StatelessWidget {
), ),
], ],
), ),
body: InteractiveViewer( body: HoverBuilder(
builder: (context, hovered) => Stack(
children: [
PageView.builder(
controller: controller.pageController,
itemCount: controller.allEvents.length,
itemBuilder: (context, i) => InteractiveViewer(
minScale: 1.0, minScale: 1.0,
maxScale: 10.0, maxScale: 10.0,
onInteractionEnd: controller.onInteractionEnds, onInteractionEnd: controller.onInteractionEnds,
child: Center( child: Center(
child: Hero( child: Hero(
tag: controller.widget.event.eventId, tag: controller.allEvents[i].eventId,
child: MxcImage( child: MxcImage(
event: controller.widget.event, key: ValueKey(controller.allEvents[i].eventId),
event: controller.allEvents[i],
fit: BoxFit.contain, fit: BoxFit.contain,
isThumbnail: false, isThumbnail: false,
animated: true, animated: true,
@ -83,6 +92,35 @@ class ImageViewerView extends StatelessWidget {
), ),
), ),
), ),
),
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,
),
),
),
],
),
),
),
); );
} }
} }

Loading…
Cancel
Save