348 lines
11 KiB
Dart
348 lines
11 KiB
Dart
|
|
|
|
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<HomePage> createState() => _HomePageState();
|
|
}
|
|
|
|
class _HomePageState extends State<HomePage> {
|
|
|
|
Map<String, dynamic> 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<Uint8List?> 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;
|
|
});
|
|
|
|
}
|
|
} |