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
This commit is contained in:
Bernd Steckmeister
2026-07-03 05:47:18 +02:00
parent d706ace35f
commit 25ed765a03
195 changed files with 47784 additions and 1236 deletions
+184
View File
@@ -0,0 +1,184 @@
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)),
),
],
),
),
],
),
),
);
}
}