import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../../../../api/mhsl/server/feedback/add_feedback.dart'; import '../../../../api/mhsl/server/feedback/add_feedback_params.dart'; import '../../../../model/account_data.dart'; import '../../../../widget/async_action_button.dart'; import '../../../../widget/file_pick.dart'; import '../../../../widget/focus_behaviour.dart'; import '../../../../widget/info_dialog.dart'; class FeedbackDialog extends StatefulWidget { const FeedbackDialog({super.key}); @override State createState() => _FeedbackDialogState(); } class _FeedbackDialogState extends State { final TextEditingController _feedbackInput = TextEditingController(); final AsyncActionController _sendController = AsyncActionController(); final AsyncActionController _pickController = AsyncActionController(); Uint8List? _image; bool _textFieldEmpty = false; @override void initState() { super.initState(); _feedbackInput.addListener(() { if (_textFieldEmpty && _feedbackInput.text.isNotEmpty) { setState(() => _textFieldEmpty = false); } }); } @override void dispose() { _feedbackInput.dispose(); _sendController.dispose(); _pickController.dispose(); super.dispose(); } Future _pickImage() async { final picked = await FilePick.singleGalleryPick(); if (picked == null) return; final data = await picked.readAsBytes(); if (!mounted) return; setState(() => _image = data); } Future _submit() async { if (_feedbackInput.text.trim().isEmpty) { setState(() => _textFieldEmpty = true); return; } final info = await PackageInfo.fromPlatform(); await AddFeedback( AddFeedbackParams( user: AccountData().getUserSecret(), feedback: _feedbackInput.text, screenshot: _image != null ? base64Encode(_image!) : null, appVersion: int.parse(info.buildNumber), ), ).run(); if (!mounted) return; Navigator.of(context).pop(); InfoDialog.show(context, 'Danke für dein Feedback!'); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar(title: const Text('Feedback')), body: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Feedback, Anregungen, Ideen, Fehler und Verbesserungen', textAlign: TextAlign.center, style: theme.textTheme.titleMedium, ), const SizedBox(height: 6), Text( 'Bitte gib keine geheimen Daten wie z.B. Passwörter weiter.', textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 20), TextField( controller: _feedbackInput, autofocus: true, minLines: 5, maxLines: 8, textCapitalization: TextCapitalization.sentences, onChanged: (value) { if (value.trim().toLowerCase() == 'ranzig') { _feedbackInput.text = 'selber'; } }, decoration: InputDecoration( border: const OutlineInputBorder(), label: const Text('Deine Nachricht'), hintText: 'Was möchtest du uns mitteilen?', errorText: _textFieldEmpty ? 'Bitte gib eine Beschreibung an!' : null, ), onTapOutside: (_) => FocusBehaviour.textFieldTapOutside(context), ), const SizedBox(height: 16), if (_image != null) Center( child: _ImagePreview( bytes: _image!, onRemove: () => setState(() => _image = null), ), ) else Align( alignment: Alignment.centerLeft, child: AsyncTextButton( controller: _pickController, onPressed: _pickImage, child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.attach_file_outlined, size: 18), SizedBox(width: 6), Text('Screenshot anhängen'), ], ), ), ), const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: AsyncTextButton( controller: _sendController, onPressed: _submit, child: const Text('Senden'), ), ), ], ), ), ); } } class _ImagePreview extends StatelessWidget { final Uint8List bytes; final VoidCallback onRemove; const _ImagePreview({required this.bytes, required this.onRemove}); @override Widget build(BuildContext context) => Stack( children: [ Padding( // Reserve space inside the Stack bounds for the close button. // Without it the button would sit on a negative Positioned and // Flutter wouldn't hit-test taps outside the Stack rect. padding: const EdgeInsets.only(top: 10, right: 10), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Container( decoration: BoxDecoration( border: Border.all( color: Theme.of(context).colorScheme.outlineVariant, ), borderRadius: BorderRadius.circular(8), ), constraints: const BoxConstraints(maxHeight: 200), child: Image.memory(bytes, fit: BoxFit.contain), ), ), ), Positioned( top: 0, right: 0, child: Material( color: Theme.of(context).colorScheme.surface, shape: const CircleBorder(), elevation: 2, child: InkWell( customBorder: const CircleBorder(), onTap: onRemove, child: const Padding( padding: EdgeInsets.all(4), child: Icon(Icons.close, size: 18), ), ), ), ), ], ); }