import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:go_router/go_router.dart'; import 'package:image/image.dart' as img; import 'package:quotegen_client/services/quote_api.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; class HomePage extends StatefulWidget { static GoRoute route = GoRoute( path: "/", builder: (context, state) => HomePage(), ); @override State createState() => _HomePageState(); } class _HomePageState extends State { Map formData = { "display_name": "", "handle": "", "content": "", "timestamp": 0, "avatar_image": null, "post_image": null, }; Uint8List? postPreviewImage; @override Widget build(BuildContext context) { double topPadding = MediaQuery.of(context).padding.top; double bottomPadding = MediaQuery.of(context).padding.bottom; return Scaffold( child: Column( children: [ Gap(max(topPadding, 14)), AspectRatio( aspectRatio: 1, child: OutlinedContainer( backgroundColor: Colors.black, child: postPreviewImage == null ? Center( child: CircularProgressIndicator( color: Colors.white, strokeWidth: 3, ), ) : Image.memory( postPreviewImage!, fit: BoxFit.cover, ), ) ).withPadding( horizontal: 14 ), Gap(14), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Display Name" ).xSmall.semiBold, Gap(6), TextField( placeholder: Text( "e.g. John Doe" ), onChanged: (value) { setState(() { formData["display_name"] = value; }); onSubmit(); }, ), ], ), ), Gap(14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Handle" ).xSmall.semiBold, Gap(6), TextField( placeholder: Text( "e.g. @johndoe" ), onChanged: (value) { setState(() { formData["handle"] = value; }); onSubmit(); }, ), ], ), ), ], ), Gap(10), SizedBox( height: 96, child: Row( children: [ Expanded( child: Column( children: [ SizedBox( width: double.infinity, child: OutlineButton( child: Text("Select Avatar Image"), onPressed: () async { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: false, ); if (result != null) { Uint8List? imageBytes; // on web, bytes is availble directly if (result.files.single.bytes != null) { imageBytes = result.files.single.bytes!; } // on mobile (iOS/Android), need to raed from path else if (result.files.single.path != null) { File file = File(result.files.single.path!); imageBytes = await file.readAsBytes(); } if (imageBytes != null) { // compression disabled for testing // Uint8List? compressedBytes = await compressImage(imageBytes, maxWidth: 400); if (!mounted) return; setState(() { formData["avatar_image"] = imageBytes; }); onSubmit(); } } }, ), ), Gap(6), SizedBox( width: double.infinity, child: OutlineButton( child: Text("Select Post Image"), onPressed: () async { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: false, ); if (result != null) { Uint8List? imageBytes; // on web, bytes is availble directly if (result.files.single.bytes != null) { imageBytes = result.files.single.bytes!; } // on mobile (iOS/Android), need to raed from path else if (result.files.single.path != null) { File file = File(result.files.single.path!); imageBytes = await file.readAsBytes(); } if (imageBytes != null) { if (!mounted) return; setState(() { formData["post_image"] = imageBytes; }); onSubmit(); } } }, ), ), ] ), ), Gap(10), AspectRatio( aspectRatio: 1, child: ClipRRect( borderRadius: BorderRadius.circular(1000), child: formData["avatar_image"] != null ? Image.memory( formData["avatar_image"], width: 50, height: 50, fit: BoxFit.cover, ) : OutlinedContainer( borderRadius: BorderRadius.circular(1000), child: Center( child: Text( formData["display_name"].isNotEmpty ? formData["display_name"][0].toUpperCase() : "P", ) ), ), ) ), ], ), ), Gap(10), Text( "Content" ).xSmall.semiBold, Gap(6), TextArea( onChanged: (value) { setState(() { formData["content"] = value; }); onSubmit(); }, ), Gap(10), Text( "Timestamp" ).xSmall.semiBold, Gap(6), SizedBox( width: double.infinity, child: DatePicker( value: DateTime.now(), onChanged: (DateTime? date) { setState(() { formData["timestamp"] = date!.millisecondsSinceEpoch; }); onSubmit(); } ) ), ], ).withPadding( horizontal: 14 ) ], ), ); } // compress and resize image to reduce payload size Future compressImage(Uint8List bytes, {int maxWidth = 800, int quality = 85}) async { try { img.Image? image = img.decodeImage(bytes); if (image == null) return null; // resize if image is larger than maxWidth if (image.width > maxWidth) { image = img.copyResize(image, width: maxWidth); } // encode as JPEG with compression return Uint8List.fromList(img.encodeJpg(image, quality: quality)); } catch (e) { print("Error compresing image: $e"); return null; } } int _submitCount = 0; void onSubmit() async { int currentSubmit = ++_submitCount; await Future.delayed(Duration(milliseconds: 700)); // Only proceed if this is the latest submit if (currentSubmit != _submitCount) return; if (!mounted) return; setState(() { postPreviewImage = null; }); print("Form Data Submitted: $formData"); if (!formData["handle"].startsWith("@")) { formData["handle"] = "@" + formData["handle"]; } QuoteRequest request = QuoteRequest( displayName: formData["display_name"], username: formData["handle"].toLowerCase(), text: formData["content"], timestamp: formData["timestamp"], avatarUrl: formData["avatar_image"], imageUrl: formData["post_image"], ); Uint8List imageBytes = await QuoteGeneratorApi.generateQuotePost(request); if (!mounted) return; setState(() { postPreviewImage = imageBytes; }); } }