add macOS support for ad loading and improve timestamp handling
Some checks failed
Build Android App / build (push) Failing after 49s

This commit is contained in:
ImBenji
2026-01-02 18:46:12 +00:00
parent 016f7911f3
commit 7d8e19d1f5
13 changed files with 369 additions and 75 deletions

View File

@@ -6,6 +6,7 @@ import 'dart:math';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:image/image.dart' as img;
@@ -17,6 +18,7 @@ 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';
import 'package:file_saver/file_saver.dart';
// number formatter for engagement fields
class NumberTextInputFormatter extends TextInputFormatter {
@@ -68,7 +70,8 @@ class _HomePageState extends State<HomePage> {
"display_name": "",
"handle": "",
"content": "",
"timestamp": 0,
"date": DateTime.now(),
"time": TimeOfDay.now(),
"avatar_image": null,
"post_image": null,
"replies": "",
@@ -134,6 +137,9 @@ class _HomePageState extends State<HomePage> {
double topPadding = MediaQuery.of(context).padding.top;
double bottomPadding = MediaQuery.of(context).padding.bottom;
// Check if we're on mobile (iOS/Android)
bool isMobilePlatform = !kIsWeb && (Platform.isIOS || Platform.isAndroid);
if (isWideLayout) {
return Scaffold(
child: Center(
@@ -142,15 +148,11 @@ class _HomePageState extends State<HomePage> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Gap(max(topPadding, 14)),
SizedBox(
height: 600,
child: _buildPreview(context)
),
Gap(14),
ConstrainedBox(
@@ -166,8 +168,6 @@ class _HomePageState extends State<HomePage> {
],
),
),
Gap(max(bottomPadding, 14)),
],
),
),
@@ -175,28 +175,55 @@ class _HomePageState extends State<HomePage> {
);
}
return Scaffold(
child: SingleChildScrollView(
child: Column(
children: [
// Mobile layout content
Widget mainContent = SingleChildScrollView(
child: Column(
children: [
Gap(max(topPadding, 14)),
Gap(max(topPadding, 14)),
_buildPreview(context),
_buildPreview(context),
Gap(14),
Gap(14),
_buildForm(context),
_buildForm(context),
Gap(14),
Gap(14),
// Banner ad
// Add bottom padding on mobile to avoid content being hidden behind ad
if (isMobilePlatform)
Gap(100), // Space for the overlay ad (80px min height + padding)
// Only show inline ad on non-mobile platforms
if (!isMobilePlatform)
AdBanner(),
Gap(max(bottomPadding, 14)),
Gap(max(bottomPadding, 14)),
],
),
);
// Wrap in Stack with overlay ad for mobile platforms
if (isMobilePlatform) {
return Scaffold(
child: Stack(
children: [
mainContent,
// Bottom overlay ad for mobile
Positioned(
left: 0,
right: 0,
bottom: max(bottomPadding, 0),
child: AdBanner(),
),
],
),
),
);
}
return Scaffold(
child: mainContent,
);
}
@@ -218,15 +245,110 @@ class _HomePageState extends State<HomePage> {
),
Align(
alignment: Alignment.topRight,
child: IconButton.secondary(
alignment: Alignment.topRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if ([TargetPlatform.iOS, TargetPlatform.android, TargetPlatform.macOS].contains(defaultTargetPlatform))...[
IconButton.secondary(
icon: Icon(
Icons.share,
),
onPressed: () async {
await shareImage();
}
),
Gap(8),
],
IconButton.secondary(
icon: Icon(
LucideIcons.download,
),
onPressed: () async {
if (postPreviewImage == null) {
print("No image avalable to download");
return;
}
try {
// genrate filename with timestmp
final fileName = "quote_${DateTime.now().millisecondsSinceEpoch}";
await FileSaver.instance.saveFile(
name: fileName,
bytes: postPreviewImage!,
ext: "png",
mimeType: MimeType.png,
);
print("Image downloaded succesfully");
} catch (e) {
print("Error downloadin image: $e");
}
}
),
Gap(8),
IconButton.outline(
icon: Icon(
Icons.share,
LucideIcons.link
),
onPressed: () async {
await shareImage();
}
)
if (_currentSessionId == null) {
print("No session available");
return;
}
try {
// create snapshot link
QuoteSnapshot snapshot = await QuoteGeneratorApiV2.createSnapshotLink(_currentSessionId!);
// copy URL to clipboard
await Clipboard.setData(ClipboardData(text: snapshot.url));
if (!mounted) return;
// show toast notification
showToast(
context: context,
builder: (context, overlay) {
return SurfaceCard(
child: Basic(
title: const Text('Snapshot Link Copied'),
subtitle: const Text('The snapshot link has been copied to clipboard'),
trailingAlignment: Alignment.center,
),
);
}
);
print("Snapshot link created: ${snapshot.url}");
} catch (e) {
print("Error creating snapshot link: $e");
if (!mounted) return;
showToast(
context: context,
builder: (context, overlay) {
return SurfaceCard(
child: Basic(
title: const Text('Error'),
subtitle: const Text('Failed to create snapshot link'),
trailingAlignment: Alignment.center,
),
);
}
);
}
},
)
].reversed.toList(),
)
).withPadding(
all: 12
)
@@ -258,6 +380,13 @@ class _HomePageState extends State<HomePage> {
placeholder: Text(
"Display Name"
),
features: [
InputFeature.leading(
Icon(
LucideIcons.user
),
)
],
initialValue: formData["display_name"],
onChanged: (value) {
setState(() {
@@ -295,6 +424,13 @@ class _HomePageState extends State<HomePage> {
placeholder: Text(
"Handle"
),
features: [
InputFeature.leading(
Icon(
LucideIcons.atSign
)
)
],
initialValue: formData["handle"],
onChanged: (value) {
setState(() {
@@ -413,44 +549,46 @@ class _HomePageState extends State<HomePage> {
children: [
Expanded(
child: DatePicker(
value: formData["timestamp"] > 0
? DateTime.fromMillisecondsSinceEpoch(formData["timestamp"])
: DateTime.now(),
value: formData["date"],
onChanged: (DateTime? date) {
if (date != null) {
setState(() {
formData["timestamp"] = date.millisecondsSinceEpoch;
formData["date"] = date;
});
_saveFormData();
onSubmit(changedField: "timestamp");
onSubmit(changedField: "datetime");
}
},
),
),
TimePicker(
value: formData["timestamp"] > 0
? TimeOfDay.fromDateTime(DateTime.fromMillisecondsSinceEpoch(formData["timestamp"]))
: TimeOfDay.now(),
value: formData["time"],
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;
formData["time"] = time;
});
_saveFormData();
onSubmit(changedField: "timestamp");
onSubmit(changedField: "datetime");
}
},
),
Button.secondary(
child: Text(
"Now"
),
onPressed: () {
DateTime now = DateTime.now();
TimeOfDay nowTime = TimeOfDay.now();
setState(() {
formData["date"] = now;
formData["time"] = nowTime;
});
_saveFormData();
onSubmit(changedField: "datetime");
},
)
]
@@ -786,6 +924,22 @@ class _HomePageState extends State<HomePage> {
int _submitCount = 0;
String? _currentSessionId;
// combine date and time into unix timestamp (in seconds for API)
int _combineDateTime() {
DateTime date = formData["date"];
TimeOfDay time = formData["time"];
DateTime combined = DateTime(
date.year,
date.month,
date.day,
time.hour,
time.minute,
);
return combined.millisecondsSinceEpoch ~/ 1000;
}
void onSubmit({String? changedField}) async {
int currentSubmit = ++_submitCount;
@@ -802,10 +956,6 @@ class _HomePageState extends State<HomePage> {
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));
@@ -831,7 +981,7 @@ class _HomePageState extends State<HomePage> {
displayName: formData["display_name"],
username: formData["handle"].toLowerCase(),
text: formData["content"],
timestamp: formData["timestamp"],
timestamp: _combineDateTime(),
avatarUrl: formData["avatar_image"],
imageUrl: formData["post_image"],
verified: formData["verified"] ?? false,
@@ -846,7 +996,7 @@ class _HomePageState extends State<HomePage> {
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,
timestamp: changedField == "datetime" ? _combineDateTime() : null,
avatarUrl: changedField == "avatar_image" ? formData["avatar_image"] : null,
imageUrl: changedField == "post_image" ? formData["post_image"] : null,
verified: changedField == "verified" ? (formData["verified"] ?? false) : null,
@@ -1002,13 +1152,19 @@ class _HomePageState extends State<HomePage> {
postImage = base64Decode(postBase64);
}
// split timestamp into seperate date and time
DateTime dateTime = timestamp > 0
? DateTime.fromMillisecondsSinceEpoch(timestamp)
: DateTime.now();
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["date"] = DateTime(dateTime.year, dateTime.month, dateTime.day);
formData["time"] = TimeOfDay(hour: dateTime.hour, minute: dateTime.minute);
formData["verified"] = verified;
formData["replies"] = replies;
formData["retweets"] = retweets;
@@ -1035,17 +1191,13 @@ class _HomePageState extends State<HomePage> {
);
}
// fix handle format
if (handle.isNotEmpty && !handle.startsWith("@")) {
handle = "@$handle";
}
// create session with full form data
// create session with full form data (API expects timestamp in seconds)
// Use _combineDateTime() to get timestamp from formData (defaults to now if nothing saved)
QuoteSessionRequest initialRequest = QuoteSessionRequest(
displayName: displayName,
username: handle.toLowerCase(),
text: content,
timestamp: timestamp,
timestamp: _combineDateTime(),
verified: verified,
avatarUrl: avatarImage,
imageUrl: postImage,
@@ -1054,8 +1206,8 @@ class _HomePageState extends State<HomePage> {
await _initializeSession(initialRequest);
// genrate preview image if we have data
if (_currentSessionId != null && (displayName.isNotEmpty || content.isNotEmpty)) {
// genrate preview image if we have session
if (_currentSessionId != null) {
Uint8List imageBytes = await QuoteGeneratorApiV2.generateImage(_currentSessionId!);
if (!mounted) return;
setState(() {
@@ -1080,11 +1232,14 @@ class _HomePageState extends State<HomePage> {
print("Saving form data: name=${formData["display_name"]}, handle=${formData["handle"]}, content=${formData["content"]}");
// combine date and time into timestamp (in milliseconds for storage)
int timestampMillis = _combineDateTime() * 1000;
// 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.setInt("timestamp", timestampMillis);
await prefs.setString("replies", formData["replies"]);
await prefs.setString("retweets", formData["retweets"]);
await prefs.setString("likes", formData["likes"]);