refactor: Projektstruktur in app/ und backend/ aufgeteilt
This commit is contained in:
260
app/lib/tasks/signature_capture_screen.dart
Normal file
260
app/lib/tasks/signature_capture_screen.dart
Normal file
@@ -0,0 +1,260 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:signature/signature.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../models/tasks/signature_task.dart';
|
||||
import '../widgets/offline_banner.dart';
|
||||
|
||||
class SignatureCaptureScreen extends StatefulWidget {
|
||||
final SignatureTask task;
|
||||
final void Function(String svg) onSignatureCompleted;
|
||||
|
||||
const SignatureCaptureScreen({
|
||||
super.key,
|
||||
required this.task,
|
||||
required this.onSignatureCompleted,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SignatureCaptureScreen> createState() => _SignatureCaptureScreenState();
|
||||
}
|
||||
|
||||
class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
|
||||
late final SignatureController _controller;
|
||||
bool _hasSignature = false;
|
||||
bool _isMobilePlatform = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = SignatureController(
|
||||
penStrokeWidth: 3,
|
||||
penColor: Colors.black,
|
||||
exportBackgroundColor: Colors.white,
|
||||
);
|
||||
|
||||
// Listen to signature controller changes
|
||||
_controller.addListener(_onSignatureChanged);
|
||||
|
||||
_detectPlatformAndSetOrientation();
|
||||
}
|
||||
|
||||
void _onSignatureChanged() {
|
||||
final bool hasPoints = _controller.points.isNotEmpty;
|
||||
if (hasPoints != _hasSignature) {
|
||||
setState(() {
|
||||
_hasSignature = hasPoints;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _detectPlatformAndSetOrientation() {
|
||||
// Check if we're on a mobile platform
|
||||
if (!kIsWeb) {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.iOS:
|
||||
_isMobilePlatform = true;
|
||||
// Rotate screen 90 degrees to the right (landscape left)
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeLeft,
|
||||
]);
|
||||
break;
|
||||
default:
|
||||
_isMobilePlatform = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _restoreOrientation() {
|
||||
// Restore original orientation when leaving the screen
|
||||
if (_isMobilePlatform) {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_onSignatureChanged);
|
||||
_controller.dispose();
|
||||
_restoreOrientation();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _buildSvgFromPoints(List<Point?> points, {double strokeWidth = 3.0, String strokeColor = '#000000'}) {
|
||||
// Convert collected signature points (with null separators for stroke breaks) into an SVG string
|
||||
// Determine bounds
|
||||
double? minX, minY, maxX, maxY;
|
||||
for (final p in points) {
|
||||
if (p == null) continue;
|
||||
final x = p.offset.dx;
|
||||
final y = p.offset.dy;
|
||||
if (minX == null || x < minX) minX = x;
|
||||
if (minY == null || y < minY) minY = y;
|
||||
if (maxX == null || x > maxX) maxX = x;
|
||||
if (maxY == null || y > maxY) maxY = y;
|
||||
}
|
||||
// Fallback bounds if empty or degenerate
|
||||
if (minX == null || minY == null || maxX == null || maxY == null) {
|
||||
minX = 0;
|
||||
minY = 0;
|
||||
maxX = 1;
|
||||
maxY = 1;
|
||||
}
|
||||
double width = (maxX - minX);
|
||||
double height = (maxY - minY);
|
||||
if (width <= 0) width = 1;
|
||||
if (height <= 0) height = 1;
|
||||
|
||||
final StringBuffer d = StringBuffer();
|
||||
bool newStroke = true;
|
||||
for (final p in points) {
|
||||
if (p == null) {
|
||||
newStroke = true;
|
||||
continue;
|
||||
}
|
||||
final x = (p.offset.dx - minX);
|
||||
final y = (p.offset.dy - minY);
|
||||
if (newStroke) {
|
||||
d.write('M${x.toStringAsFixed(2)} ${y.toStringAsFixed(2)} ');
|
||||
newStroke = false;
|
||||
} else {
|
||||
d.write('L${x.toStringAsFixed(2)} ${y.toStringAsFixed(2)} ');
|
||||
}
|
||||
}
|
||||
|
||||
final String svg = '<svg xmlns="http://www.w3.org/2000/svg" width="${width.toStringAsFixed(2)}" height="${height.toStringAsFixed(2)}" viewBox="0 0 ${width.toStringAsFixed(2)} ${height.toStringAsFixed(2)}"><path d="${d.toString().trim()}" fill="none" stroke="$strokeColor" stroke-width="$strokeWidth" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
return svg;
|
||||
}
|
||||
|
||||
Future<void> _finish() async {
|
||||
try {
|
||||
// Ensure there is at least one non-null point in the signature
|
||||
final hasAnyPoint = _controller.points.isNotEmpty;
|
||||
if (!hasAnyPoint) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context).signatureRequired)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build SVG from the captured signature points
|
||||
final String svg = _buildSvgFromPoints(_controller.points);
|
||||
|
||||
// Close this screen first to show the updated TaskView quickly
|
||||
if (!mounted) return;
|
||||
_restoreOrientation();
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Then notify the caller (SVG only)
|
||||
widget.onSignatureCompleted(svg);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${AppLocalizations.of(context).signatureError}: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context).signatureCapture),
|
||||
backgroundColor: Colors.deepPurple[100],
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
_restoreOrientation();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: AppLocalizations.of(context).delete,
|
||||
onPressed: () {
|
||||
_controller.clear();
|
||||
// The listener will automatically update _hasSignature when points are cleared
|
||||
},
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
OfflineBanner(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).signatureInstruction,
|
||||
style: TextStyle(color: Colors.grey[700]),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[400]!),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Signature(
|
||||
controller: _controller,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
_controller.clear();
|
||||
// The listener will automatically update _hasSignature when points are cleared
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: Text(AppLocalizations.of(context).clear),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
child: ElevatedButton(
|
||||
onPressed: _hasSignature ? _finish : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
child: Text(AppLocalizations.of(context).finish),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user