import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pyramid/core/theme.dart'; import 'package:pyramid/features/chat/media_player.dart'; import 'package:pyramid/utils/gif_favorite_service.dart'; /// Overlay viewer for images, GIFs and videos. /// Uses a semi-transparent backdrop; tapping outside the content closes it. class MediaViewer extends StatefulWidget { final String senderName; final Uint8List? bytes; final String? networkUrl; final String filename; final bool isVideo; final Event? event; const MediaViewer({ super.key, required this.senderName, required this.filename, this.bytes, this.networkUrl, this.isVideo = false, this.event, }); static Future show( BuildContext context, { required String senderName, required String filename, Uint8List? bytes, String? networkUrl, bool isVideo = false, Event? event, }) { return showGeneralDialog( context: context, barrierDismissible: true, barrierLabel: 'Schließen', barrierColor: Colors.black.withAlpha(178), transitionDuration: const Duration(milliseconds: 180), transitionBuilder: (context, animation, secondaryAnimation, child) => FadeTransition(opacity: animation, child: child), pageBuilder: (context, animation, secondaryAnimation) => MediaViewer( senderName: senderName, filename: filename, bytes: bytes, networkUrl: networkUrl, isVideo: isVideo, event: event, ), ); } @override State createState() => _MediaViewerState(); } class _MediaViewerState extends State { final _tc = TransformationController(); bool _controlsVisible = true; bool _saving = false; bool _savingSticker = false; @override void dispose() { _tc.dispose(); super.dispose(); } void _toggle() => setState(() => _controlsVisible = !_controlsVisible); void _resetZoom() => _tc.value = Matrix4.identity(); Future _saveAsSticker() async { final event = widget.event; if (event == null || _savingSticker) return; setState(() => _savingSticker = true); final messenger = ScaffoldMessenger.maybeOf(context); try { final ok = await GifFavoriteService.addImageAsStickerFavorite(event); messenger?.showSnackBar(SnackBar( content: Text(ok ? 'Als Sticker gespeichert' : 'Fehler beim Speichern'), duration: const Duration(seconds: 2), )); } finally { if (mounted) setState(() => _savingSticker = false); } } Future _save() async { final b = widget.bytes; if (b == null || _saving) return; setState(() => _saving = true); try { Directory? dir; if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { dir = await getDownloadsDirectory(); } dir ??= await getTemporaryDirectory(); final ext = widget.filename.contains('.') ? widget.filename.split('.').last : 'jpg'; final name = 'pyramid_${DateTime.now().millisecondsSinceEpoch}.$ext'; final file = File('${dir.path}/$name'); await file.writeAsBytes(b); if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('Gespeichert: ${file.path}'), duration: const Duration(seconds: 4), )); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Fehler: $e')), ); } } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { final pt = PyramidTheme.of(context); final isVideo = widget.isVideo && widget.bytes != null; final size = MediaQuery.sizeOf(context); final header = AnimatedOpacity( opacity: _controlsVisible ? 1 : 0, duration: const Duration(milliseconds: 180), child: IgnorePointer( ignoring: !_controlsVisible, child: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.black54, Colors.transparent], ), ), child: SafeArea( bottom: false, child: Row( children: [ IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close_rounded, color: Colors.white), tooltip: 'Schließen', ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(widget.senderName, style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600)), Text(widget.filename, style: const TextStyle(color: Colors.white60, fontSize: 12), overflow: TextOverflow.ellipsis), ], ), ), if (widget.event != null && !isVideo) IconButton( onPressed: _savingSticker ? null : _saveAsSticker, tooltip: 'Als Sticker speichern', icon: _savingSticker ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Icon(Icons.sticky_note_2_outlined, color: Colors.white), ), if (widget.bytes != null) IconButton( onPressed: _saving ? null : _save, tooltip: 'Speichern', icon: _saving ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Icon(Icons.download_rounded, color: Colors.white), ), if (!isVideo) IconButton( onPressed: _resetZoom, tooltip: 'Zoom zurücksetzen', icon: const Icon(Icons.zoom_out_map_rounded, color: Colors.white), ), ], ), ), ), ), ); // Content area — constrained so the transparent Material outside it lets // pointer events through to the barrier (which dismisses on tap). final contentMaxW = (size.width - 48).clamp(200.0, 1200.0); final contentMaxH = (size.height - 48).clamp(200.0, 900.0); if (isVideo) { return Material( type: MaterialType.transparency, child: Center( child: SizedBox( width: contentMaxW, height: contentMaxH, child: Stack( fit: StackFit.expand, children: [ FullscreenVideoPlayer(bytes: widget.bytes!, filename: widget.filename), Positioned(top: 0, left: 0, right: 0, child: header), ], ), ), ), ); } Widget img; if (widget.bytes != null) { img = Image.memory(widget.bytes!, fit: BoxFit.contain, gaplessPlayback: true); } else if (widget.networkUrl != null) { img = Image.network(widget.networkUrl!, fit: BoxFit.contain); } else { img = Icon(Icons.broken_image_outlined, size: 64, color: pt.fgDim); } return Material( type: MaterialType.transparency, child: Center( child: SizedBox( width: contentMaxW, height: contentMaxH, child: GestureDetector( onTap: _toggle, child: Stack( fit: StackFit.expand, children: [ InteractiveViewer( transformationController: _tc, minScale: 0.5, maxScale: 10.0, child: Center(child: img), ), Positioned(top: 0, left: 0, right: 0, child: header), ], ), ), ), ), ); } }