add macOS support for ad loading and improve timestamp handling
Some checks failed
Build Android App / build (push) Failing after 49s
Some checks failed
Build Android App / build (push) Failing after 49s
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:io' show Platform;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:quotegen_client/pages/home/page.dart';
|
import 'package:quotegen_client/pages/home/page.dart';
|
||||||
@@ -7,8 +8,8 @@ import 'package:google_mobile_ads/google_mobile_ads.dart';
|
|||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
// Initialize mobile ads (AdMob) only on mobile platforms
|
// Initialize mobile ads (AdMob) only on mobile platforms (not web or macOS)
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb && !Platform.isMacOS) {
|
||||||
await MobileAds.instance.initialize();
|
await MobileAds.instance.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'dart:math';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:image/image.dart' as img;
|
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:path/path.dart' as path;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:file_saver/file_saver.dart';
|
||||||
|
|
||||||
// number formatter for engagement fields
|
// number formatter for engagement fields
|
||||||
class NumberTextInputFormatter extends TextInputFormatter {
|
class NumberTextInputFormatter extends TextInputFormatter {
|
||||||
@@ -68,7 +70,8 @@ class _HomePageState extends State<HomePage> {
|
|||||||
"display_name": "",
|
"display_name": "",
|
||||||
"handle": "",
|
"handle": "",
|
||||||
"content": "",
|
"content": "",
|
||||||
"timestamp": 0,
|
"date": DateTime.now(),
|
||||||
|
"time": TimeOfDay.now(),
|
||||||
"avatar_image": null,
|
"avatar_image": null,
|
||||||
"post_image": null,
|
"post_image": null,
|
||||||
"replies": "",
|
"replies": "",
|
||||||
@@ -134,6 +137,9 @@ class _HomePageState extends State<HomePage> {
|
|||||||
double topPadding = MediaQuery.of(context).padding.top;
|
double topPadding = MediaQuery.of(context).padding.top;
|
||||||
double bottomPadding = MediaQuery.of(context).padding.bottom;
|
double bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||||
|
|
||||||
|
// Check if we're on mobile (iOS/Android)
|
||||||
|
bool isMobilePlatform = !kIsWeb && (Platform.isIOS || Platform.isAndroid);
|
||||||
|
|
||||||
if (isWideLayout) {
|
if (isWideLayout) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -142,15 +148,11 @@ class _HomePageState extends State<HomePage> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
Gap(max(topPadding, 14)),
|
|
||||||
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 600,
|
height: 600,
|
||||||
child: _buildPreview(context)
|
child: _buildPreview(context)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
Gap(14),
|
Gap(14),
|
||||||
|
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
@@ -166,8 +168,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Gap(max(bottomPadding, 14)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -175,8 +175,8 @@ class _HomePageState extends State<HomePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
// Mobile layout content
|
||||||
child: SingleChildScrollView(
|
Widget mainContent = SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
@@ -190,13 +190,40 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
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(),
|
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,13 +246,108 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
child: IconButton.secondary(
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if ([TargetPlatform.iOS, TargetPlatform.android, TargetPlatform.macOS].contains(defaultTargetPlatform))...[
|
||||||
|
IconButton.secondary(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.share,
|
Icons.share,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await shareImage();
|
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(
|
||||||
|
LucideIcons.link
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
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(
|
).withPadding(
|
||||||
all: 12
|
all: 12
|
||||||
@@ -258,6 +380,13 @@ class _HomePageState extends State<HomePage> {
|
|||||||
placeholder: Text(
|
placeholder: Text(
|
||||||
"Display Name"
|
"Display Name"
|
||||||
),
|
),
|
||||||
|
features: [
|
||||||
|
InputFeature.leading(
|
||||||
|
Icon(
|
||||||
|
LucideIcons.user
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
initialValue: formData["display_name"],
|
initialValue: formData["display_name"],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -295,6 +424,13 @@ class _HomePageState extends State<HomePage> {
|
|||||||
placeholder: Text(
|
placeholder: Text(
|
||||||
"Handle"
|
"Handle"
|
||||||
),
|
),
|
||||||
|
features: [
|
||||||
|
InputFeature.leading(
|
||||||
|
Icon(
|
||||||
|
LucideIcons.atSign
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
initialValue: formData["handle"],
|
initialValue: formData["handle"],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -413,44 +549,46 @@ class _HomePageState extends State<HomePage> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DatePicker(
|
child: DatePicker(
|
||||||
value: formData["timestamp"] > 0
|
value: formData["date"],
|
||||||
? DateTime.fromMillisecondsSinceEpoch(formData["timestamp"])
|
|
||||||
: DateTime.now(),
|
|
||||||
onChanged: (DateTime? date) {
|
onChanged: (DateTime? date) {
|
||||||
if (date != null) {
|
if (date != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
formData["timestamp"] = date.millisecondsSinceEpoch;
|
formData["date"] = date;
|
||||||
});
|
});
|
||||||
_saveFormData();
|
_saveFormData();
|
||||||
onSubmit(changedField: "timestamp");
|
onSubmit(changedField: "datetime");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
TimePicker(
|
TimePicker(
|
||||||
value: formData["timestamp"] > 0
|
value: formData["time"],
|
||||||
? TimeOfDay.fromDateTime(DateTime.fromMillisecondsSinceEpoch(formData["timestamp"]))
|
|
||||||
: TimeOfDay.now(),
|
|
||||||
onChanged: (TimeOfDay? time) {
|
onChanged: (TimeOfDay? time) {
|
||||||
if (time != null) {
|
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(() {
|
setState(() {
|
||||||
formData["timestamp"] = newDateTime.millisecondsSinceEpoch;
|
formData["time"] = time;
|
||||||
});
|
});
|
||||||
_saveFormData();
|
_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;
|
int _submitCount = 0;
|
||||||
String? _currentSessionId;
|
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 {
|
void onSubmit({String? changedField}) async {
|
||||||
|
|
||||||
int currentSubmit = ++_submitCount;
|
int currentSubmit = ++_submitCount;
|
||||||
@@ -802,10 +956,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
print("Form Data Submitted: $formData");
|
print("Form Data Submitted: $formData");
|
||||||
|
|
||||||
if (!formData["handle"].startsWith("@")) {
|
|
||||||
formData["handle"] = "@" + formData["handle"];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for session to be ready
|
// Wait for session to be ready
|
||||||
while (_currentSessionId == null) {
|
while (_currentSessionId == null) {
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
@@ -831,7 +981,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
displayName: formData["display_name"],
|
displayName: formData["display_name"],
|
||||||
username: formData["handle"].toLowerCase(),
|
username: formData["handle"].toLowerCase(),
|
||||||
text: formData["content"],
|
text: formData["content"],
|
||||||
timestamp: formData["timestamp"],
|
timestamp: _combineDateTime(),
|
||||||
avatarUrl: formData["avatar_image"],
|
avatarUrl: formData["avatar_image"],
|
||||||
imageUrl: formData["post_image"],
|
imageUrl: formData["post_image"],
|
||||||
verified: formData["verified"] ?? false,
|
verified: formData["verified"] ?? false,
|
||||||
@@ -846,7 +996,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
displayName: changedField == "display_name" ? formData["display_name"] : null,
|
displayName: changedField == "display_name" ? formData["display_name"] : null,
|
||||||
username: changedField == "handle" ? formData["handle"].toLowerCase() : null,
|
username: changedField == "handle" ? formData["handle"].toLowerCase() : null,
|
||||||
text: changedField == "content" ? formData["content"] : 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,
|
avatarUrl: changedField == "avatar_image" ? formData["avatar_image"] : null,
|
||||||
imageUrl: changedField == "post_image" ? formData["post_image"] : null,
|
imageUrl: changedField == "post_image" ? formData["post_image"] : null,
|
||||||
verified: changedField == "verified" ? (formData["verified"] ?? false) : null,
|
verified: changedField == "verified" ? (formData["verified"] ?? false) : null,
|
||||||
@@ -1002,13 +1152,19 @@ class _HomePageState extends State<HomePage> {
|
|||||||
postImage = base64Decode(postBase64);
|
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");
|
print("Loaded form data: name=$displayName, handle=$handle, content=$content");
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
formData["display_name"] = displayName;
|
formData["display_name"] = displayName;
|
||||||
formData["handle"] = handle;
|
formData["handle"] = handle;
|
||||||
formData["content"] = content;
|
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["verified"] = verified;
|
||||||
formData["replies"] = replies;
|
formData["replies"] = replies;
|
||||||
formData["retweets"] = retweets;
|
formData["retweets"] = retweets;
|
||||||
@@ -1035,17 +1191,13 @@ class _HomePageState extends State<HomePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix handle format
|
// create session with full form data (API expects timestamp in seconds)
|
||||||
if (handle.isNotEmpty && !handle.startsWith("@")) {
|
// Use _combineDateTime() to get timestamp from formData (defaults to now if nothing saved)
|
||||||
handle = "@$handle";
|
|
||||||
}
|
|
||||||
|
|
||||||
// create session with full form data
|
|
||||||
QuoteSessionRequest initialRequest = QuoteSessionRequest(
|
QuoteSessionRequest initialRequest = QuoteSessionRequest(
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
username: handle.toLowerCase(),
|
username: handle.toLowerCase(),
|
||||||
text: content,
|
text: content,
|
||||||
timestamp: timestamp,
|
timestamp: _combineDateTime(),
|
||||||
verified: verified,
|
verified: verified,
|
||||||
avatarUrl: avatarImage,
|
avatarUrl: avatarImage,
|
||||||
imageUrl: postImage,
|
imageUrl: postImage,
|
||||||
@@ -1054,8 +1206,8 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
await _initializeSession(initialRequest);
|
await _initializeSession(initialRequest);
|
||||||
|
|
||||||
// genrate preview image if we have data
|
// genrate preview image if we have session
|
||||||
if (_currentSessionId != null && (displayName.isNotEmpty || content.isNotEmpty)) {
|
if (_currentSessionId != null) {
|
||||||
Uint8List imageBytes = await QuoteGeneratorApiV2.generateImage(_currentSessionId!);
|
Uint8List imageBytes = await QuoteGeneratorApiV2.generateImage(_currentSessionId!);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -1080,11 +1232,14 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
print("Saving form data: name=${formData["display_name"]}, handle=${formData["handle"]}, content=${formData["content"]}");
|
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
|
// save text felds
|
||||||
await prefs.setString("display_name", formData["display_name"]);
|
await prefs.setString("display_name", formData["display_name"]);
|
||||||
await prefs.setString("handle", formData["handle"]);
|
await prefs.setString("handle", formData["handle"]);
|
||||||
await prefs.setString("content", formData["content"]);
|
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("replies", formData["replies"]);
|
||||||
await prefs.setString("retweets", formData["retweets"]);
|
await prefs.setString("retweets", formData["retweets"]);
|
||||||
await prefs.setString("likes", formData["likes"]);
|
await prefs.setString("likes", formData["likes"]);
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ class AdSenseService implements AdService {
|
|||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
// AdSense is loaded via script tag in index.html
|
// AdSense is loaded via script tag in index.html
|
||||||
print("AdSense initialized (web)");
|
print("AdSense initialized (web)");
|
||||||
_isLoaded = true;
|
_isLoaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> loadBannerAd() async {
|
Future<void> loadBannerAd() async {
|
||||||
// AdSense ads are loaded automatically when widget renders
|
// AdSense ads are loaded automatically when widget renders
|
||||||
_isLoaded = true;
|
_isLoaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -159,9 +159,36 @@ class QuoteSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class QuoteSnapshot {
|
||||||
|
final String token;
|
||||||
|
final String url;
|
||||||
|
final String sessionId;
|
||||||
|
final int createdAt;
|
||||||
|
final int expiresAt;
|
||||||
|
|
||||||
|
QuoteSnapshot({
|
||||||
|
required this.token,
|
||||||
|
required this.url,
|
||||||
|
required this.sessionId,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory QuoteSnapshot.fromJson(Map<String, dynamic> json) {
|
||||||
|
return QuoteSnapshot(
|
||||||
|
token: json["token"],
|
||||||
|
url: json["url"],
|
||||||
|
sessionId: json["sessionId"],
|
||||||
|
createdAt: json["createdAt"],
|
||||||
|
expiresAt: json["expiresAt"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class QuoteGeneratorApiV2 {
|
class QuoteGeneratorApiV2 {
|
||||||
static const String _baseUrl = "https://quotes.imbenji.net";
|
// static const String _baseUrl = "https://quotes.imbenji.net";
|
||||||
// static const String _baseUrl = "http://localhost:3000";
|
static const String _baseUrl = "http://localhost:3000";
|
||||||
|
|
||||||
|
|
||||||
// create new session
|
// create new session
|
||||||
@@ -259,6 +286,40 @@ class QuoteGeneratorApiV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// create persistant snapshot link
|
||||||
|
static Future<QuoteSnapshot> createSnapshotLink(String sessionId) async {
|
||||||
|
final url = Uri.parse("$_baseUrl/v2/quote/$sessionId/snapshot-link");
|
||||||
|
|
||||||
|
final response = await http.post(
|
||||||
|
url,
|
||||||
|
headers: {"User-Agent": "QuoteGen-Flutter/1.0"},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 201) {
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
|
return QuoteSnapshot.fromJson(data);
|
||||||
|
} else {
|
||||||
|
throw Exception("Failed to create snapshot link: ${response.statusCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get snapshot image by token
|
||||||
|
static Future<Uint8List> getSnapshot(String token) async {
|
||||||
|
final url = Uri.parse("$_baseUrl/v2/snapshot/$token");
|
||||||
|
|
||||||
|
final response = await http.get(
|
||||||
|
url,
|
||||||
|
headers: {"User-Agent": "QuoteGen-Flutter/1.0"},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return response.bodyBytes;
|
||||||
|
} else {
|
||||||
|
throw Exception("Failed to get snapshot: ${response.statusCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// helper to get current timestamp in seconds
|
// helper to get current timestamp in seconds
|
||||||
static int getCurrentTimestamp() {
|
static int getCurrentTimestamp() {
|
||||||
return DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
return DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import "dart:io" show Platform;
|
||||||
import "package:flutter/foundation.dart" show kIsWeb;
|
import "package:flutter/foundation.dart" show kIsWeb;
|
||||||
import "package:flutter/widgets.dart";
|
import "package:flutter/widgets.dart";
|
||||||
import "package:shadcn_flutter/shadcn_flutter.dart";
|
import "package:shadcn_flutter/shadcn_flutter.dart";
|
||||||
@@ -15,12 +16,20 @@ class AdBanner extends StatefulWidget {
|
|||||||
class _AdBannerState extends State<AdBanner> {
|
class _AdBannerState extends State<AdBanner> {
|
||||||
late AdService _adService;
|
late AdService _adService;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
|
bool _isMacOS = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
// check if running on macOS
|
||||||
|
if (!kIsWeb && Platform.isMacOS) {
|
||||||
|
_isMacOS = true;
|
||||||
|
_isLoading = false;
|
||||||
|
} else {
|
||||||
_initializeAds();
|
_initializeAds();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _initializeAds() async {
|
Future<void> _initializeAds() async {
|
||||||
// Platform detection
|
// Platform detection
|
||||||
@@ -46,12 +55,42 @@ class _AdBannerState extends State<AdBanner> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
if (!_isMacOS) {
|
||||||
_adService.dispose();
|
_adService.dispose();
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
// Check if we're on mobile (iOS/Android)
|
||||||
|
bool isMobile = !kIsWeb && !_isMacOS && (Platform.isIOS || Platform.isAndroid);
|
||||||
|
|
||||||
|
// Show placeholder only on macOS and web when ads fail to load
|
||||||
|
// On mobile, we want real ads or nothing
|
||||||
|
if (_isMacOS || (!isMobile && !_isLoading && !_adService.isLoaded)) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minHeight: 80,
|
||||||
|
),
|
||||||
|
child: OutlinedContainer(
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
"ADVERTISEMENT SPACE",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.mutedForeground,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).withPadding(
|
||||||
|
horizontal: 8
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 50,
|
height: 50,
|
||||||
@@ -65,11 +104,6 @@ class _AdBannerState extends State<AdBanner> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_adService.isLoaded) {
|
|
||||||
// Ad failed to load, show nothing
|
|
||||||
return SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _adService.getBannerWidget();
|
return _adService.getBannerWidget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,13 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_saver/file_saver_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) file_saver_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
|
||||||
|
file_saver_plugin_register_with_registrar(file_saver_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_saver
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import file_picker
|
import file_picker
|
||||||
|
import file_saver
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
@@ -13,6 +14,7 @@ import webview_flutter_wkwebview
|
|||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
|
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ PODS:
|
|||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- webview_flutter_wkwebview (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||||
@@ -17,6 +20,7 @@ DEPENDENCIES:
|
|||||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
|
- webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
file_picker:
|
file_picker:
|
||||||
@@ -29,6 +33,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||||
|
webview_flutter_wkwebview:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
|
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
|
||||||
@@ -36,6 +42,7 @@ SPEC CHECKSUMS:
|
|||||||
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
|
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
|
||||||
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
|
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
|
||||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||||
|
webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4
|
||||||
|
|
||||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||||
|
|
||||||
|
|||||||
24
pubspec.lock
24
pubspec.lock
@@ -121,6 +121,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.2"
|
version: "0.0.2"
|
||||||
|
dio:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dio
|
||||||
|
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.9.0"
|
||||||
|
dio_web_adapter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dio_web_adapter
|
||||||
|
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
email_validator:
|
email_validator:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -169,6 +185,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.3.7"
|
version: "8.3.7"
|
||||||
|
file_saver:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_saver
|
||||||
|
sha256: "017a127de686af2d2fbbd64afea97052d95f2a0f87d19d25b87e097407bf9c1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.14"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ dependencies:
|
|||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
shared_preferences: ^2.3.4
|
shared_preferences: ^2.3.4
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
|
file_saver: ^0.2.14
|
||||||
|
|
||||||
# Ad integration
|
# Ad integration
|
||||||
google_mobile_ads: ^5.2.0
|
google_mobile_ads: ^5.2.0
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_saver/file_saver_plugin.h>
|
||||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
FileSaverPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSaverPlugin"));
|
||||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_saver
|
||||||
share_plus
|
share_plus
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user