1100 lines
35 KiB
Dart
1100 lines
35 KiB
Dart
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:image/image.dart' as img;
|
|
import 'package:quotegen_client/services/quote_api_v2.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';
|
|
import 'package:intl/intl.dart';
|
|
|
|
// number formatter for engagement fields
|
|
class NumberTextInputFormatter extends TextInputFormatter {
|
|
final NumberFormat _formatter = NumberFormat('#,###');
|
|
|
|
@override
|
|
TextEditingValue formatEditUpdate(
|
|
TextEditingValue oldValue,
|
|
TextEditingValue newValue,
|
|
) {
|
|
if (newValue.text.isEmpty) {
|
|
return newValue;
|
|
}
|
|
|
|
// remove all non-digits
|
|
String digitsOnly = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');
|
|
|
|
if (digitsOnly.isEmpty) {
|
|
return TextEditingValue.empty;
|
|
}
|
|
|
|
// format with commas
|
|
int number = int.parse(digitsOnly);
|
|
String formatted = _formatter.format(number);
|
|
|
|
return TextEditingValue(
|
|
text: formatted,
|
|
selection: TextSelection.collapsed(offset: formatted.length),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HomePage extends StatefulWidget {
|
|
|
|
static GoRoute route = GoRoute(
|
|
path: "/",
|
|
builder: (context, state) => HomePage(),
|
|
);
|
|
|
|
@override
|
|
State<HomePage> createState() => _HomePageState();
|
|
}
|
|
|
|
class _HomePageState extends State<HomePage> {
|
|
|
|
bool _isLoading = true;
|
|
|
|
Map<String, dynamic> formData = {
|
|
"display_name": "",
|
|
"handle": "",
|
|
"content": "",
|
|
"timestamp": 0,
|
|
"avatar_image": null,
|
|
"post_image": null,
|
|
"replies": "",
|
|
"retweets": "",
|
|
"likes": "",
|
|
"views": "",
|
|
};
|
|
|
|
late TextEditingController _repliesController;
|
|
late TextEditingController _retweetsController;
|
|
late TextEditingController _likesController;
|
|
late TextEditingController _viewsController;
|
|
|
|
|
|
Uint8List? postPreviewImage;
|
|
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_repliesController = TextEditingController();
|
|
_retweetsController = TextEditingController();
|
|
_likesController = TextEditingController();
|
|
_viewsController = TextEditingController();
|
|
_loadFormData();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_repliesController.dispose();
|
|
_retweetsController.dispose();
|
|
_likesController.dispose();
|
|
_viewsController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _initializeSession(QuoteSessionRequest initialData) async {
|
|
try {
|
|
QuoteSession session = await QuoteGeneratorApiV2.createSession(initialData);
|
|
_currentSessionId = session.id;
|
|
print("Session created: ${session.id}");
|
|
} catch (e) {
|
|
print("Failed to creat session: $e");
|
|
}
|
|
}
|
|
|
|
bool get isWideLayout {
|
|
return MediaQuery.of(context).size.width >= 800;
|
|
}
|
|
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
|
|
if (_isLoading) {
|
|
return Scaffold(
|
|
child: Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
);
|
|
}
|
|
|
|
double topPadding = MediaQuery.of(context).padding.top;
|
|
double bottomPadding = MediaQuery.of(context).padding.bottom;
|
|
|
|
if (isWideLayout) {
|
|
return Scaffold(
|
|
child: Center(
|
|
child: IntrinsicHeight(
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
|
|
Gap(max(topPadding, 14)),
|
|
|
|
SizedBox(
|
|
height: 600,
|
|
child: _buildPreview(context)
|
|
),
|
|
|
|
Gap(14),
|
|
|
|
ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
maxWidth: 400,
|
|
),
|
|
child: _buildForm(context)
|
|
),
|
|
|
|
Gap(max(bottomPadding, 14)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
|
|
Gap(max(topPadding, 14)),
|
|
|
|
_buildPreview(context),
|
|
|
|
Gap(14),
|
|
|
|
_buildForm(context),
|
|
|
|
Gap(max(bottomPadding, 14)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPreview(BuildContext context) {
|
|
return 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.topRight,
|
|
child: IconButton.secondary(
|
|
icon: Icon(
|
|
Icons.share,
|
|
),
|
|
onPressed: () async {
|
|
await shareImage();
|
|
}
|
|
)
|
|
).withPadding(
|
|
all: 12
|
|
)
|
|
],
|
|
),
|
|
)
|
|
).withPadding(
|
|
horizontal: 14
|
|
);
|
|
}
|
|
|
|
Widget _buildForm(BuildContext context) {
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
|
|
SizedBox(
|
|
height: isWideLayout ? 72 : 90,
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: ButtonGroup.vertical(
|
|
children: [
|
|
|
|
ButtonGroup.horizontal(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
placeholder: Text(
|
|
"Display Name"
|
|
),
|
|
initialValue: formData["display_name"],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
formData["display_name"] = value;
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "display_name");
|
|
},
|
|
),
|
|
),
|
|
|
|
VerticalDivider(),
|
|
|
|
SizedBox(
|
|
height: 10,
|
|
child: Toggle(
|
|
value: formData["verified"] ?? false,
|
|
child: Icon(
|
|
LucideIcons.badgeCheck,
|
|
),
|
|
style: ButtonStyle.primaryIcon(),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
formData["verified"] = value;
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "verified");
|
|
},
|
|
),
|
|
)
|
|
],
|
|
),
|
|
|
|
TextField(
|
|
placeholder: Text(
|
|
"Handle"
|
|
),
|
|
initialValue: formData["handle"],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
formData["handle"] = value;
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "handle");
|
|
},
|
|
)
|
|
|
|
],
|
|
)
|
|
),
|
|
|
|
Gap(14),
|
|
|
|
AspectRatio(
|
|
aspectRatio: 1,
|
|
child: formData["avatar_image"] == null ? Button.card(
|
|
onPressed: () async {
|
|
Uint8List? imageBytes = await pickImageFile();
|
|
|
|
if (imageBytes != null) {
|
|
// downscale avatar image
|
|
Uint8List? resizedBytes = await compressImage(imageBytes, maxWidth: 400);
|
|
|
|
if (!mounted) return;
|
|
setState(() {
|
|
formData["avatar_image"] = resizedBytes ?? imageBytes;
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "avatar_image");
|
|
}
|
|
},
|
|
child: FittedBox(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
LucideIcons.plus
|
|
),
|
|
Gap(4),
|
|
Text(
|
|
"Avatar\nImage",
|
|
textAlign: TextAlign.center,
|
|
).xSmall.semiBold
|
|
],
|
|
),
|
|
),
|
|
) : GestureDetector(
|
|
onTap: () {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
formData["avatar_image"] = null;
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "avatar_image");
|
|
},
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Stack(
|
|
children: [
|
|
Image.memory(
|
|
formData["avatar_image"],
|
|
fit: BoxFit.cover,
|
|
),
|
|
Center(
|
|
child: Icon(
|
|
LucideIcons.trash2,
|
|
shadows: [
|
|
Shadow(
|
|
color: Colors.black.withOpacity(0.9),
|
|
blurRadius: 6,
|
|
)
|
|
],
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
).withPadding(
|
|
horizontal: 14
|
|
),
|
|
|
|
Gap(14),
|
|
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ButtonGroup.vertical(
|
|
children: [
|
|
|
|
TextArea(
|
|
placeholder: Text(
|
|
"Content"
|
|
),
|
|
initialValue: formData["content"],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
formData["content"] = value;
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "content");
|
|
},
|
|
expands: false,
|
|
expandableHeight: false,
|
|
expandableWidth: false,
|
|
textAlignVertical: TextAlignVertical.top,
|
|
),
|
|
|
|
ButtonGroup.horizontal(
|
|
children: [
|
|
Expanded(
|
|
child: DatePicker(
|
|
value: formData["timestamp"] > 0
|
|
? DateTime.fromMillisecondsSinceEpoch(formData["timestamp"])
|
|
: DateTime.now(),
|
|
onChanged: (DateTime? date) {
|
|
if (date != null) {
|
|
setState(() {
|
|
formData["timestamp"] = date.millisecondsSinceEpoch;
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "timestamp");
|
|
}
|
|
},
|
|
),
|
|
),
|
|
|
|
TimePicker(
|
|
value: formData["timestamp"] > 0
|
|
? TimeOfDay.fromDateTime(DateTime.fromMillisecondsSinceEpoch(formData["timestamp"]))
|
|
: TimeOfDay.now(),
|
|
onChanged: (TimeOfDay? time) {
|
|
if (time != null) {
|
|
DateTime currentDate = formData["timestamp"] > 0
|
|
? DateTime.fromMillisecondsSinceEpoch(formData["timestamp"])
|
|
: DateTime.now();
|
|
DateTime newDateTime = DateTime(
|
|
currentDate.year,
|
|
currentDate.month,
|
|
currentDate.day,
|
|
time.hour,
|
|
time.minute,
|
|
);
|
|
setState(() {
|
|
formData["timestamp"] = newDateTime.millisecondsSinceEpoch;
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "timestamp");
|
|
}
|
|
},
|
|
)
|
|
|
|
]
|
|
),
|
|
|
|
],
|
|
),
|
|
).withPadding(
|
|
horizontal: 14
|
|
),
|
|
|
|
Gap(14),
|
|
|
|
ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
minWidth: double.infinity,
|
|
minHeight: 150,
|
|
),
|
|
child: formData["post_image"] == null ? CardButton(
|
|
child: Column(
|
|
children: [
|
|
Icon(
|
|
LucideIcons.plus
|
|
),
|
|
Text(
|
|
"Add Image"
|
|
).xSmall.semiBold
|
|
],
|
|
),
|
|
onPressed: () async {
|
|
Uint8List? imageBytes = await pickImageFile();
|
|
|
|
if (imageBytes != null) {
|
|
|
|
if (!mounted) return;
|
|
setState(() {
|
|
formData["post_image"] = imageBytes;
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "post_image");
|
|
}
|
|
},
|
|
) : ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Stack(
|
|
children: [
|
|
Image.memory(
|
|
formData["post_image"],
|
|
fit: BoxFit.cover,
|
|
),
|
|
Positioned.fill(
|
|
child: Align(
|
|
alignment: Alignment.bottomRight,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
|
|
IconButton.secondary(
|
|
icon: Icon(
|
|
LucideIcons.pen,
|
|
),
|
|
onPressed: () async {
|
|
Uint8List? imageBytes = await pickImageFile();
|
|
|
|
if (imageBytes != null) {
|
|
|
|
if (!mounted) return;
|
|
setState(() {
|
|
formData["post_image"] = imageBytes;
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "post_image");
|
|
}
|
|
},
|
|
),
|
|
|
|
Gap(8),
|
|
|
|
IconButton.destructive(
|
|
icon: Icon(
|
|
LucideIcons.trash2,
|
|
),
|
|
onPressed: () {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
formData["post_image"] = null;
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "post_image");
|
|
},
|
|
),
|
|
|
|
],
|
|
),
|
|
).withPadding(
|
|
all: 4
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
).withPadding(
|
|
horizontal: 14
|
|
),
|
|
|
|
Gap(14),
|
|
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ButtonGroup.horizontal(
|
|
children: [
|
|
|
|
Expanded(
|
|
child: ButtonGroup.vertical(
|
|
children: [
|
|
|
|
ButtonGroup.horizontal(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
features: [
|
|
InputFeature.leading(
|
|
Icon(
|
|
LucideIcons.messageCircle
|
|
),
|
|
)
|
|
],
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [
|
|
NumberTextInputFormatter(),
|
|
],
|
|
controller: _repliesController,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
formData["replies"] = value;
|
|
_fillEmptyEngagement();
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "engagement");
|
|
},
|
|
),
|
|
),
|
|
|
|
Expanded(
|
|
child: TextField(
|
|
features: [
|
|
InputFeature.leading(
|
|
Icon(
|
|
LucideIcons.repeat
|
|
),
|
|
)
|
|
],
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [
|
|
NumberTextInputFormatter(),
|
|
],
|
|
controller: _retweetsController,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
formData["retweets"] = value;
|
|
_fillEmptyEngagement();
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "engagement");
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
ButtonGroup.horizontal(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
features: [
|
|
InputFeature.leading(
|
|
Icon(
|
|
LucideIcons.heart
|
|
),
|
|
)
|
|
],
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [
|
|
NumberTextInputFormatter(),
|
|
],
|
|
controller: _likesController,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
formData["likes"] = value;
|
|
_fillEmptyEngagement();
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "engagement");
|
|
},
|
|
),
|
|
),
|
|
|
|
Expanded(
|
|
child: TextField(
|
|
features: [
|
|
InputFeature.leading(
|
|
Icon(
|
|
LucideIcons.chartColumn
|
|
),
|
|
)
|
|
],
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [
|
|
NumberTextInputFormatter(),
|
|
],
|
|
controller: _viewsController,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
formData["views"] = value;
|
|
_fillEmptyEngagement();
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "engagement");
|
|
},
|
|
),
|
|
),
|
|
],
|
|
)
|
|
|
|
],
|
|
),
|
|
),
|
|
|
|
Button.primary(
|
|
onPressed: () {
|
|
// randomize engagement metrics
|
|
Random random = Random();
|
|
final formatter = NumberFormat("#,###");
|
|
final replies = formatter.format(random.nextInt(10000));
|
|
final retweets = formatter.format(random.nextInt(50000));
|
|
final likes = formatter.format(random.nextInt(100000));
|
|
final views = formatter.format(random.nextInt(1000000));
|
|
|
|
setState(() {
|
|
formData["replies"] = replies;
|
|
formData["retweets"] = retweets;
|
|
formData["likes"] = likes;
|
|
formData["views"] = views;
|
|
|
|
_repliesController.text = replies;
|
|
_retweetsController.text = retweets;
|
|
_likesController.text = likes;
|
|
_viewsController.text = views;
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "engagement");
|
|
},
|
|
child: Icon(
|
|
LucideIcons.dices
|
|
),
|
|
),
|
|
|
|
Button.destructive(
|
|
onPressed: () {
|
|
setState(() {
|
|
formData["replies"] = "";
|
|
formData["retweets"] = "";
|
|
formData["likes"] = "";
|
|
formData["views"] = "";
|
|
|
|
_repliesController.clear();
|
|
_retweetsController.clear();
|
|
_likesController.clear();
|
|
_viewsController.clear();
|
|
});
|
|
_saveFormData();
|
|
onSubmit(changedField: "engagement");
|
|
},
|
|
child: Icon(
|
|
LucideIcons.trash2
|
|
),
|
|
)
|
|
|
|
],
|
|
),
|
|
).withPadding(
|
|
horizontal: 14
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// pick image from file system
|
|
Future<Uint8List?> pickImageFile() 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();
|
|
}
|
|
|
|
return imageBytes;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// resize image to reduce payload size without comression
|
|
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 PNG (lossles, no compression)
|
|
return Uint8List.fromList(img.encodePng(image));
|
|
} catch (e) {
|
|
print("Error resizng image: $e");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
int _submitCount = 0;
|
|
String? _currentSessionId;
|
|
|
|
void onSubmit({String? changedField}) 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"];
|
|
}
|
|
|
|
// Wait for session to be ready
|
|
while (_currentSessionId == null) {
|
|
await Future.delayed(Duration(milliseconds: 100));
|
|
}
|
|
|
|
// Build request with only changed field
|
|
QuoteSessionRequest request;
|
|
|
|
if (changedField == null) {
|
|
// Initial load - send everything
|
|
QuoteEngagement? engagement;
|
|
if (formData["likes"] != "" || formData["retweets"] != "" ||
|
|
formData["replies"] != "" || formData["views"] != "") {
|
|
engagement = QuoteEngagement(
|
|
likes: _parseNum(formData["likes"]),
|
|
retweets: _parseNum(formData["retweets"]),
|
|
replies: _parseNum(formData["replies"]),
|
|
views: _parseNum(formData["views"]),
|
|
);
|
|
}
|
|
|
|
request = QuoteSessionRequest(
|
|
displayName: formData["display_name"],
|
|
username: formData["handle"].toLowerCase(),
|
|
text: formData["content"],
|
|
timestamp: formData["timestamp"],
|
|
avatarUrl: formData["avatar_image"],
|
|
imageUrl: formData["post_image"],
|
|
verified: formData["verified"] ?? false,
|
|
engagement: engagement,
|
|
);
|
|
} else {
|
|
// Only send the changed field
|
|
final engagement = (changedField == "engagement" || changedField?.startsWith("engagement_") == true) ? _buildEngagement(changedField!) : null;
|
|
final shouldClearEngagement = changedField == "engagement" && engagement == null;
|
|
|
|
request = QuoteSessionRequest(
|
|
displayName: changedField == "display_name" ? formData["display_name"] : null,
|
|
username: changedField == "handle" ? formData["handle"].toLowerCase() : null,
|
|
text: changedField == "content" ? formData["content"] : null,
|
|
timestamp: changedField == "timestamp" ? formData["timestamp"] : null,
|
|
avatarUrl: changedField == "avatar_image" ? formData["avatar_image"] : null,
|
|
imageUrl: changedField == "post_image" ? formData["post_image"] : null,
|
|
verified: changedField == "verified" ? (formData["verified"] ?? false) : null,
|
|
engagement: engagement,
|
|
clearAvatarUrl: changedField == "avatar_image" && formData["avatar_image"] == null,
|
|
clearImageUrl: changedField == "post_image" && formData["post_image"] == null,
|
|
clearEngagement: shouldClearEngagement,
|
|
);
|
|
}
|
|
|
|
// Update session and generate image
|
|
await QuoteGeneratorApiV2.updateSession(_currentSessionId!, request);
|
|
|
|
Uint8List imageBytes = await QuoteGeneratorApiV2.generateImage(_currentSessionId!);
|
|
|
|
if (!mounted) return;
|
|
setState(() {
|
|
postPreviewImage = imageBytes;
|
|
});
|
|
|
|
}
|
|
|
|
// parse number string, stripping commas first
|
|
int? _parseNum(String val) {
|
|
if (val.isEmpty) return null;
|
|
return int.tryParse(val.replaceAll(",", ""));
|
|
}
|
|
|
|
// fill empty engagment fields with 1 if any field has value
|
|
// returns true if any fields were auto-filled
|
|
bool _fillEmptyEngagement() {
|
|
bool hasValue = formData["replies"] != "" || formData["retweets"] != "" ||
|
|
formData["likes"] != "" || formData["views"] != "";
|
|
|
|
bool didFill = false;
|
|
|
|
if (hasValue) {
|
|
if (formData["replies"] == "") {
|
|
formData["replies"] = "1";
|
|
_repliesController.text = "1";
|
|
didFill = true;
|
|
}
|
|
if (formData["retweets"] == "") {
|
|
formData["retweets"] = "1";
|
|
_retweetsController.text = "1";
|
|
didFill = true;
|
|
}
|
|
if (formData["likes"] == "") {
|
|
formData["likes"] = "1";
|
|
_likesController.text = "1";
|
|
didFill = true;
|
|
}
|
|
if (formData["views"] == "") {
|
|
formData["views"] = "1";
|
|
_viewsController.text = "1";
|
|
didFill = true;
|
|
}
|
|
}
|
|
|
|
return didFill;
|
|
}
|
|
|
|
QuoteEngagement? _buildEngagement(String field) {
|
|
// If field is "engagement", send all engagement fields (randomize button)
|
|
if (field == "engagement") {
|
|
// if all fields are empty, return null
|
|
if (formData["likes"] == "" && formData["retweets"] == "" &&
|
|
formData["replies"] == "" && formData["views"] == "") {
|
|
return null;
|
|
}
|
|
|
|
return QuoteEngagement(
|
|
likes: _parseNum(formData["likes"]),
|
|
retweets: _parseNum(formData["retweets"]),
|
|
replies: _parseNum(formData["replies"]),
|
|
views: _parseNum(formData["views"]),
|
|
);
|
|
}
|
|
|
|
// Otherwise only send the specific engagement field that changed
|
|
return QuoteEngagement(
|
|
likes: field == "engagement_likes" ? _parseNum(formData["likes"]) : null,
|
|
retweets: field == "engagement_retweets" ? _parseNum(formData["retweets"]) : null,
|
|
replies: field == "engagement_replies" ? _parseNum(formData["replies"]) : null,
|
|
views: field == "engagement_views" ? _parseNum(formData["views"]) : null,
|
|
);
|
|
}
|
|
|
|
Future<void> 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<void> _loadFormData() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
if (!mounted) return;
|
|
|
|
// load text fields
|
|
final displayName = prefs.getString("display_name") ?? "";
|
|
var handle = prefs.getString("handle") ?? "";
|
|
final content = prefs.getString("content") ?? "";
|
|
final timestamp = prefs.getInt("timestamp") ?? 0;
|
|
final replies = prefs.getString("replies") ?? "";
|
|
final retweets = prefs.getString("retweets") ?? "";
|
|
final likes = prefs.getString("likes") ?? "";
|
|
final views = prefs.getString("views") ?? "";
|
|
final verified = prefs.getBool("verified") ?? false;
|
|
|
|
// load images from base64
|
|
Uint8List? avatarImage;
|
|
Uint8List? postImage;
|
|
|
|
String? avatarBase64 = prefs.getString("avatar_image");
|
|
if (avatarBase64 != null && avatarBase64.isNotEmpty) {
|
|
avatarImage = base64Decode(avatarBase64);
|
|
}
|
|
|
|
String? postBase64 = prefs.getString("post_image");
|
|
if (postBase64 != null && postBase64.isNotEmpty) {
|
|
postImage = base64Decode(postBase64);
|
|
}
|
|
|
|
print("Loaded form data: name=$displayName, handle=$handle, content=$content");
|
|
|
|
setState(() {
|
|
formData["display_name"] = displayName;
|
|
formData["handle"] = handle;
|
|
formData["content"] = content;
|
|
formData["timestamp"] = timestamp;
|
|
formData["verified"] = verified;
|
|
formData["replies"] = replies;
|
|
formData["retweets"] = retweets;
|
|
formData["likes"] = likes;
|
|
formData["views"] = views;
|
|
formData["avatar_image"] = avatarImage;
|
|
formData["post_image"] = postImage;
|
|
|
|
// Set controller text
|
|
_repliesController.text = replies;
|
|
_retweetsController.text = retweets;
|
|
_likesController.text = likes;
|
|
_viewsController.text = views;
|
|
});
|
|
|
|
// build engagment object if we have any values
|
|
QuoteEngagement? engagement;
|
|
if (likes != "" || retweets != "" || replies != "" || views != "") {
|
|
engagement = QuoteEngagement(
|
|
likes: _parseNum(likes),
|
|
retweets: _parseNum(retweets),
|
|
replies: _parseNum(replies),
|
|
views: _parseNum(views),
|
|
);
|
|
}
|
|
|
|
// fix handle format
|
|
if (handle.isNotEmpty && !handle.startsWith("@")) {
|
|
handle = "@$handle";
|
|
}
|
|
|
|
// create session with full form data
|
|
QuoteSessionRequest initialRequest = QuoteSessionRequest(
|
|
displayName: displayName,
|
|
username: handle.toLowerCase(),
|
|
text: content,
|
|
timestamp: timestamp,
|
|
verified: verified,
|
|
avatarUrl: avatarImage,
|
|
imageUrl: postImage,
|
|
engagement: engagement,
|
|
);
|
|
|
|
await _initializeSession(initialRequest);
|
|
|
|
// genrate preview image if we have data
|
|
if (_currentSessionId != null && (displayName.isNotEmpty || content.isNotEmpty)) {
|
|
Uint8List imageBytes = await QuoteGeneratorApiV2.generateImage(_currentSessionId!);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
postPreviewImage = imageBytes;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
print("Error loading form data: $e");
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// save form data to shared preferences
|
|
Future<void> _saveFormData() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
print("Saving form data: name=${formData["display_name"]}, handle=${formData["handle"]}, content=${formData["content"]}");
|
|
|
|
// 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"]);
|
|
await prefs.setString("replies", formData["replies"]);
|
|
await prefs.setString("retweets", formData["retweets"]);
|
|
await prefs.setString("likes", formData["likes"]);
|
|
await prefs.setString("views", formData["views"]);
|
|
await prefs.setBool("verified", formData["verified"] ?? false);
|
|
|
|
// save images as base64
|
|
if (formData["avatar_image"] != null) {
|
|
String avatarBase64 = base64Encode(formData["avatar_image"]);
|
|
await prefs.setString("avatar_image", avatarBase64);
|
|
} else {
|
|
await prefs.remove("avatar_image");
|
|
}
|
|
|
|
if (formData["post_image"] != null) {
|
|
String postBase64 = base64Encode(formData["post_image"]);
|
|
await prefs.setString("post_image", postBase64);
|
|
} else {
|
|
await prefs.remove("post_image");
|
|
}
|
|
|
|
print("Form data saved successfully");
|
|
} catch (e) {
|
|
print("Error savin form data: $e");
|
|
}
|
|
}
|
|
} |