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,73 +15,109 @@ 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), backgroundColor: Colors.black.withAlpha(128),
extendBodyBehindAppBar: true, foregroundColor: Colors.white,
appBar: AppBar( );
elevation: 0, return GestureDetector(
leading: IconButton( onTap: () => Navigator.of(context).pop(),
style: IconButton.styleFrom( child: Scaffold(
backgroundColor: Colors.black.withAlpha(128), backgroundColor: Colors.black.withAlpha(128),
), extendBodyBehindAppBar: true,
icon: const Icon(Icons.close), appBar: AppBar(
onPressed: Navigator.of(context).pop, elevation: 0,
color: Colors.white, leading: IconButton(
tooltip: L10n.of(context).close, style: iconButtonStyle,
), icon: const Icon(Icons.close),
backgroundColor: Colors.transparent, onPressed: Navigator.of(context).pop,
actions: [
IconButton(
style: IconButton.styleFrom(
backgroundColor: Colors.black.withAlpha(128),
),
icon: const Icon(Icons.reply_outlined),
onPressed: controller.forwardAction,
color: Colors.white, color: Colors.white,
tooltip: L10n.of(context).share, tooltip: L10n.of(context).close,
), ),
const SizedBox(width: 8), backgroundColor: Colors.transparent,
IconButton( actions: [
style: IconButton.styleFrom( IconButton(
backgroundColor: Colors.black.withAlpha(128), 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), const SizedBox(width: 8),
onPressed: () => controller.saveFileAction(context), IconButton(
color: Colors.white, style: iconButtonStyle,
tooltip: L10n.of(context).downloadFile, icon: const Icon(Icons.download_outlined),
), onPressed: () => controller.saveFileAction(context),
const SizedBox(width: 8), color: Colors.white,
if (PlatformInfos.isMobile) tooltip: L10n.of(context).downloadFile,
// Use builder context to correctly position the share dialog on iPad ),
Padding( const SizedBox(width: 8),
padding: const EdgeInsets.only(right: 8.0), if (PlatformInfos.isMobile)
child: Builder( // Use builder context to correctly position the share dialog on iPad
builder: (context) => IconButton( Padding(
style: IconButton.styleFrom( padding: const EdgeInsets.only(right: 8.0),
backgroundColor: Colors.black.withAlpha(128), 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: HoverBuilder(
body: InteractiveViewer( builder: (context, hovered) => Stack(
minScale: 1.0, children: [
maxScale: 10.0, PageView.builder(
onInteractionEnd: controller.onInteractionEnds, controller: controller.pageController,
child: Center( itemCount: controller.allEvents.length,
child: Hero( itemBuilder: (context, i) => InteractiveViewer(
tag: controller.widget.event.eventId, minScale: 1.0,
child: MxcImage( maxScale: 10.0,
event: controller.widget.event, onInteractionEnd: controller.onInteractionEnds,
fit: BoxFit.contain, child: Center(
isThumbnail: false, child: Hero(
animated: true, 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,
),
),
),
],
), ),
), ),
), ),

Loading…
Cancel
Save