import 'dart:convert'; 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'; import 'package:share_plus/share_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:shared_preferences/shared_preferences.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; final TextEditingController _displayNameController = TextEditingController(); final TextEditingController _handleController = TextEditingController(); final TextEditingController _contentController = TextEditingController(); @override void initState() { super.initState(); _loadFormData(); } @override void dispose() { _displayNameController.dispose(); _handleController.dispose(); _contentController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { double topPadding = MediaQuery.of(context).padding.top; double bottomPadding = MediaQuery.of(context).padding.bottom; return Scaffold( child: SingleChildScrollView( 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, ), ) : Stack( children: [ Image.memory( postPreviewImage!, fit: BoxFit.cover, ), Align( alignment: Alignment.bottomRight, child: IconButton.secondary( icon: Icon( Icons.share, ), onPressed: () async { await shareImage(); } ) ).withPadding( all: 12 ) ], ), ) ).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( controller: _displayNameController, placeholder: Text( "e.g. John Doe" ), onChanged: (value) { setState(() { formData["display_name"] = value; }); _saveFormData(); onSubmit(); }, ), ], ), ), Gap(14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Handle" ).xSmall.semiBold, Gap(6), TextField( controller: _handleController, placeholder: Text( "e.g. @johndoe" ), onChanged: (value) { setState(() { formData["handle"] = value; }); _saveFormData(); onSubmit(); }, ), ], ), ), ], ), Gap(10), SizedBox( height: 132, child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, 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) { // downscale avatar image Uint8List? resizedBytes = await compressImage(imageBytes, maxWidth: 400); if (!mounted) return; setState(() { formData["avatar_image"] = resizedBytes ?? imageBytes; }); _saveFormData(); onSubmit(); } } }, ), ), Gap(8), Divider(), Gap(6), Text( "Timestamp" ).xSmall.semiBold, Gap(6), SizedBox( width: double.infinity, child: DatePicker( value: formData["timestamp"] > 0 ? DateTime.fromMillisecondsSinceEpoch(formData["timestamp"]) : DateTime.now(), onChanged: (DateTime? date) { setState(() { formData["timestamp"] = date!.millisecondsSinceEpoch; }); _saveFormData(); onSubmit(); } ) ).withPadding( right: 10 ), ] ), ), // Gap(10), VerticalDivider().withPadding( top: 54 ), 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( controller: _contentController, onChanged: (value) { setState(() { formData["content"] = value; }); _saveFormData(); onSubmit(); }, ), Gap(10), Divider(), Gap(10), if (formData["post_image"] != null)...[ OutlinedContainer( child: formData["post_image"] != null ? Image.memory( formData["post_image"], width: double.infinity, // height: 200, fit: BoxFit.cover, ) : Center( child: Text( "No Post Image Selected", ).small.semiBold, ), ), Gap(10), ], Row( children: [ Expanded( child: SizedBox( width: double.infinity, child: OutlineButton( child: formData["post_image"] == null ? Text("Select Post Image") : Text("Change 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; }); _saveFormData(); onSubmit(); } } }, ), ), ), if (formData["post_image"] != null)...[ Gap(10), IconButton.destructive( icon: Icon( Icons.delete, ), onPressed: () { if (!mounted) return; setState(() { formData["post_image"] = null; }); _saveFormData(); onSubmit(); }, ) ] ], ), Gap(bottomPadding) ], ).withPadding( horizontal: 14 ) ], ), ), ); } // resize image to reduce payload size without comression 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 PNG (lossles, no compression) return Uint8List.fromList(img.encodePng(image)); } catch (e) { print("Error resizng 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; }); } Future shareImage() async { if (postPreviewImage == null) { print("No image to share"); return; } try { // get temporary directry final tempDir = await getTemporaryDirectory(); // create a unique filename with timestmp final fileName = "quote_${DateTime.now().millisecondsSinceEpoch}.png"; final filePath = path.join(tempDir.path, fileName); // write the image bytes to a temporry file final file = File(filePath); await file.writeAsBytes(postPreviewImage!); // Share the file using the systm share dialog final result = await Share.shareXFiles( [XFile(filePath)], text: "Check out this quote!", ); // optinal: log share result if (result.status == ShareResultStatus.success) { print("Share succesful"); } } catch (e) { print("Error sharin image: $e"); } } // load form data from shared preferences Future _loadFormData() async { try { final prefs = await SharedPreferences.getInstance(); setState(() { // load text fields formData["display_name"] = prefs.getString("display_name") ?? ""; formData["handle"] = prefs.getString("handle") ?? ""; formData["content"] = prefs.getString("content") ?? ""; formData["timestamp"] = prefs.getInt("timestamp") ?? 0; // load images from base64 String? avatarBase64 = prefs.getString("avatar_image"); if (avatarBase64 != null && avatarBase64.isNotEmpty) { formData["avatar_image"] = base64Decode(avatarBase64); } String? postBase64 = prefs.getString("post_image"); if (postBase64 != null && postBase64.isNotEmpty) { formData["post_image"] = base64Decode(postBase64); } // update controllers _displayNameController.text = formData["display_name"]; _handleController.text = formData["handle"]; _contentController.text = formData["content"]; }); // regenerate preview if we have data if (formData["display_name"].isNotEmpty || formData["content"].isNotEmpty) { onSubmit(); } } catch (e) { print("Error loading form data: $e"); } } // save form data to shared preferences Future _saveFormData() async { try { final prefs = await SharedPreferences.getInstance(); // save text felds await prefs.setString("display_name", formData["display_name"]); await prefs.setString("handle", formData["handle"]); await prefs.setString("content", formData["content"]); await prefs.setInt("timestamp", formData["timestamp"]); // save images as base64 if (formData["avatar_image"] != null) { String avatarBase64 = base64Encode(formData["avatar_image"]); await prefs.setString("avatar_image", avatarBase64); } if (formData["post_image"] != null) { String postBase64 = base64Encode(formData["post_image"]); await prefs.setString("post_image", postBase64); } } catch (e) { print("Error savin form data: $e"); } } }