added guardian letters with chat and multiple answer functionalities

This commit is contained in:
2026-09-20 16:12:45 +02:00
parent 67c935c05b
commit 2423c1a75e
68 changed files with 8348 additions and 226 deletions
@@ -0,0 +1,125 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:signature/signature.dart';
import '../../../../widget/info_dialog.dart';
/// Upper bound the server accepts for a signature image.
const int maxSignatureBytes = 256 * 1024;
/// Lets the guardian draw a signature. Resolves to the PNG (transparent
/// background, cropped to the strokes) or null when dismissed.
Future<Uint8List?> showSignatureSheet(
BuildContext context, {
required String signerHint,
}) => showModalBottomSheet<Uint8List>(
context: context,
isScrollControlled: true,
enableDrag: false,
showDragHandle: false,
builder: (_) => _SignatureSheet(signerHint: signerHint),
);
class _SignatureSheet extends StatefulWidget {
final String signerHint;
const _SignatureSheet({required this.signerHint});
@override
State<_SignatureSheet> createState() => _SignatureSheetState();
}
class _SignatureSheetState extends State<_SignatureSheet> {
// Paper-like pad in both themes: what is drawn is what gets exported.
final SignatureController _controller = SignatureController(
penStrokeWidth: 2.5,
penColor: Colors.black,
strokeCap: StrokeCap.round,
strokeJoin: StrokeJoin.round,
);
@override
void initState() {
super.initState();
_controller.addListener(_onChanged);
}
bool _isEmpty = true;
// The controller notifies per drawn point; only the buttons depend on it.
void _onChanged() {
if (_controller.isEmpty == _isEmpty) return;
setState(() => _isEmpty = _controller.isEmpty);
}
@override
void dispose() {
_controller.removeListener(_onChanged);
_controller.dispose();
super.dispose();
}
Future<void> _accept() async {
var png = await _controller.toPngBytes();
if (png != null && png.lengthInBytes > maxSignatureBytes) {
png = await _controller.toPngBytes(width: 600);
}
if (!mounted) return;
if (png == null || png.lengthInBytes > maxSignatureBytes) {
InfoDialog.show(
context,
'Die Unterschrift konnte nicht übernommen werden. Bitte versuche es '
'erneut.',
title: 'Unterschrift',
);
return;
}
Navigator.pop(context, png);
}
@override
Widget build(BuildContext context) => SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Unterschrift', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 4),
Text(widget.signerHint),
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Signature(
controller: _controller,
height: 220,
backgroundColor: Colors.white,
),
),
const SizedBox(height: 8),
Row(
children: [
TextButton.icon(
onPressed: _isEmpty ? null : _controller.clear,
icon: const Icon(Icons.undo),
label: const Text('Löschen'),
),
const Spacer(),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Abbrechen'),
),
const SizedBox(width: 8),
FilledButton(
onPressed: _isEmpty ? null : _accept,
child: const Text('Übernehmen'),
),
],
),
],
),
),
);
}