Files
pyramid/lib/widgets/banner_crop_dialog.dart
T
Bernd Steckmeister 25ed765a03 wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.)
als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen
Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
2026-07-03 05:47:18 +02:00

185 lines
7.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<BannerCropDialog> createState() => _BannerCropDialogState();
}
class _BannerCropDialogState extends State<BannerCropDialog> {
final _transformController = TransformationController();
final _repaintKey = GlobalKey();
bool _exporting = false;
@override
void dispose() {
_transformController.dispose();
super.dispose();
}
Future<void> _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)),
),
],
),
),
],
),
),
);
}
}