FunyMeeAI/lib/features/report/report_screen.dart
2026-04-13 22:25:08 +08:00

337 lines
12 KiB
Dart

import 'dart:io';
import 'package:client_proxy_framework/client_proxy_framework.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:image_picker/image_picker.dart';
import '../../design/pencil_theme.dart';
import '../../widgets/pencil_chrome.dart';
import 'report_feedback_upload.dart';
/// Report / feedback screen.
///
/// API: [FeedbackApi.getUploadPresignedUrl] → PUT → [FeedbackApi.submit] (`fileUrls`, `content`, `contentType`), per FunyMee client guide — feedback section.
/// Design reference: `desgin/funymee_home.pen` node `Y9WlO` (Pencil); English copy only; **one** optional image.
class ReportScreen extends StatefulWidget {
const ReportScreen({super.key, required this.taskId});
final String taskId;
@override
State<ReportScreen> createState() => _ReportScreenState();
}
class _ReportScreenState extends State<ReportScreen> {
final _controller = TextEditingController();
final _picker = ImagePicker();
File? _imageFile;
bool _submitting = false;
/// Logical `contentType` for [FeedbackApi.submit] (maps via `fieldMapping` when sent).
static const _feedbackContentType = 'report';
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _pickImage() async {
if (_submitting) return;
final x = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 85,
);
if (x == null || !mounted) return;
setState(() => _imageFile = File(x.path));
}
void _clearImage() {
if (_submitting) return;
setState(() => _imageFile = null);
}
Future<void> _submit() async {
final text = _controller.text.trim();
if (text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please describe the issue.')),
);
return;
}
setState(() => _submitting = true);
try {
final urls = <String>[];
if (_imageFile != null) {
final path = await uploadFeedbackAttachment(_imageFile!);
urls.add(path);
}
final content = 'Task ID: ${widget.taskId}\n\n$text';
final res = await FeedbackApi.submit(
fileUrls: urls,
content: content,
contentType: _feedbackContentType,
);
if (!mounted) return;
if (!res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
res.msg.isNotEmpty ? res.msg : 'Submit failed (code ${res.code})',
),
),
);
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Thank you. Your report was sent.',
style: GoogleFonts.inter(fontWeight: FontWeight.w600),
),
),
);
Navigator.of(context).pop();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$e')));
} finally {
if (mounted) setState(() => _submitting = false);
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: PencilTheme.yellowWhitePageGradient,
),
child: Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(2, 0, 14, 10),
child: SizedBox(
height: 56,
child: Row(
children: [
PencilRoundBackButton(
onPressed: () {
if (_submitting) return;
Navigator.of(context).pop();
},
),
Expanded(
child: Center(
child: Text(
'Report',
style: GoogleFonts.inter(
fontSize: 19,
fontWeight: FontWeight.w700,
fontStyle: FontStyle.italic,
color: PencilTheme.stone900,
),
),
),
),
const SizedBox(width: 44),
],
),
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 24),
children: [
Text(
'Tell us what went wrong.',
style: GoogleFonts.inter(
fontSize: 14,
height: 1.4,
color: PencilTheme.stone600,
),
),
const SizedBox(height: 16),
Text(
'Related task',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: PencilTheme.stone900,
),
),
const SizedBox(height: 6),
SelectableText(
widget.taskId,
style: GoogleFonts.inter(
fontSize: 14,
color: PencilTheme.stone600,
),
),
const SizedBox(height: 20),
Text(
'Description',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: PencilTheme.stone900,
),
),
const SizedBox(height: 8),
TextField(
controller: _controller,
maxLines: 6,
readOnly: _submitting,
style: GoogleFonts.inter(
fontSize: 15,
height: 1.45,
color: PencilTheme.stone900,
),
cursorColor: PencilTheme.underlineGold,
decoration: InputDecoration(
hintText: 'Describe the issue in detail…',
hintStyle: GoogleFonts.inter(
fontSize: 15,
height: 1.45,
color: PencilTheme.stone700,
),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: PencilTheme.stone600.withValues(alpha: 0.55),
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: PencilTheme.underlineGold,
width: 1.5,
),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: PencilTheme.stone600.withValues(alpha: 0.4),
),
),
),
),
const SizedBox(height: 20),
Text(
'Screenshot (optional, one image only)',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: PencilTheme.stone900,
),
),
const SizedBox(height: 10),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Material(
color: Colors.transparent,
child: InkWell(
onTap: _submitting ? null : _pickImage,
borderRadius: BorderRadius.circular(14),
child: Ink(
width: 112,
height: 112,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: PencilTheme.genNavBackStroke,
),
),
child: _imageFile == null
? Icon(
Icons.add_photo_alternate_outlined,
size: 36,
color: PencilTheme.stone600.withValues(
alpha: 0.7,
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(
_imageFile!,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
),
),
),
),
),
if (_imageFile != null) ...[
const SizedBox(width: 12),
Padding(
padding: const EdgeInsets.only(top: 4),
child: TextButton(
onPressed: _submitting ? null : _clearImage,
child: Text(
'Remove',
style: GoogleFonts.inter(
fontWeight: FontWeight.w600,
color: PencilTheme.stone700,
),
),
),
),
],
],
),
const SizedBox(height: 28),
FilledButton(
style: FilledButton.styleFrom(
backgroundColor: PencilTheme.underlineGold,
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
onPressed: () {
if (_submitting) return;
_submit();
},
child: _submitting
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
'Submit',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
),
],
),
),
],
),
),
),
);
}
}