import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:pyramid/core/theme.dart'; class BannerCropDialog extends StatefulWidget { final Uint8List imageBytes; // Width-to-height ratio for the crop frame (e.g. 2.4 = 2.4× wider than tall) final double aspectRatio; const BannerCropDialog({ super.key, required this.imageBytes, this.aspectRatio = 2.4, }); @override State createState() => _BannerCropDialogState(); } class _BannerCropDialogState extends State { final _transformController = TransformationController(); final _repaintKey = GlobalKey(); bool _exporting = false; @override void dispose() { _transformController.dispose(); super.dispose(); } Future _confirm() async { setState(() => _exporting = true); try { final boundary = _repaintKey.currentContext!.findRenderObject() as RenderRepaintBoundary; // pixelRatio 2 gives enough resolution for a banner final image = await boundary.toImage(pixelRatio: 2.0); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); if (byteData == null || !mounted) return; Navigator.of(context).pop(byteData.buffer.asUint8List()); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Fehler beim Zuschneiden: $e'))); setState(() => _exporting = false); } } } @override Widget build(BuildContext context) { final pt = PyramidTheme.of(context); return Dialog( backgroundColor: pt.bg1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(pt.rLg)), insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 520), child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── Header ──────────────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(20, 20, 12, 12), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Banner zuschneiden', style: TextStyle( color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700)), Text( 'Ziehe und zoome das Bild zum gewünschten Ausschnitt', style: TextStyle(color: pt.fgDim, fontSize: 12)), ], ), ), IconButton( icon: Icon(Icons.close, color: pt.fgDim, size: 20), onPressed: () => Navigator.of(context).pop(), ), ], ), ), // ── Crop frame ──────────────────────────────────────────────── Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: AspectRatio( aspectRatio: widget.aspectRatio, child: ClipRRect( borderRadius: BorderRadius.circular(pt.rBase), child: RepaintBoundary( key: _repaintKey, child: Container( color: Colors.black, child: InteractiveViewer( transformationController: _transformController, minScale: 0.25, maxScale: 8.0, child: Image.memory( widget.imageBytes, fit: BoxFit.cover, width: double.infinity, height: double.infinity, ), ), ), ), ), ), ), // ── Hint ────────────────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(20, 10, 20, 0), child: Row( children: [ Icon(Icons.info_outline, size: 13, color: pt.fgDim), const SizedBox(width: 6), Expanded( child: Text( 'Pinch/Scroll zum Zoomen, Drag zum Positionieren', style: TextStyle(color: pt.fgDim, fontSize: 11), ), ), TextButton( onPressed: () { _transformController.value = Matrix4.identity(); }, child: Text('Zurücksetzen', style: TextStyle(color: pt.fgDim, fontSize: 11)), ), ], ), ), // ── Actions ─────────────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(20, 12, 20, 20), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: _exporting ? null : () => Navigator.of(context).pop(), child: const Text('Abbrechen'), ), const SizedBox(width: 8), FilledButton( onPressed: _exporting ? null : _confirm, style: FilledButton.styleFrom( backgroundColor: pt.accent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(pt.rBase)), ), child: _exporting ? SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: pt.bg0)) : const Text('Übernehmen', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600)), ), ], ), ), ], ), ), ); } }