126 lines
3.5 KiB
Dart
126 lines
3.5 KiB
Dart
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'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|