add ad services and Docker configuration for web app
Some checks failed
Build Android App / build (push) Has been cancelled

This commit is contained in:
ImBenji
2026-01-02 15:21:27 +00:00
parent 7a88585b6e
commit f1ce1c77a4
22 changed files with 608 additions and 19 deletions

View File

@@ -2,8 +2,16 @@ import 'package:flutter/foundation.dart';
import 'package:go_router/go_router.dart';
import 'package:quotegen_client/pages/home/page.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize mobile ads (AdMob) only on mobile platforms
if (!kIsWeb) {
await MobileAds.instance.initialize();
}
void main() {
runApp(const MyApp());
}

View File

@@ -10,6 +10,7 @@ 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:quotegen_client/widgets/ad_banner.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:path_provider/path_provider.dart';
@@ -148,16 +149,24 @@ class _HomePageState extends State<HomePage> {
height: 600,
child: _buildPreview(context)
),
Gap(14),
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 400,
),
child: _buildForm(context)
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(child: _buildForm(context)),
Gap(14),
AdBanner(),
],
),
),
Gap(max(bottomPadding, 14)),
],
),
@@ -170,7 +179,7 @@ class _HomePageState extends State<HomePage> {
child: SingleChildScrollView(
child: Column(
children: [
Gap(max(topPadding, 14)),
_buildPreview(context),
@@ -179,6 +188,11 @@ class _HomePageState extends State<HomePage> {
_buildForm(context),
Gap(14),
// Banner ad
AdBanner(),
Gap(max(bottomPadding, 14)),
],
),

View File

@@ -0,0 +1,19 @@
import "package:flutter/widgets.dart";
abstract class AdService {
/// Initialize ad SDK
Future<void> initialize();
/// Load banner ad
Future<void> loadBannerAd();
/// Get banner widget to display
Widget getBannerWidget();
/// Dispose resources
void dispose();
/// Check if ads are loaded
bool get isLoaded;
}

View File

@@ -0,0 +1,73 @@
import "package:flutter/foundation.dart";
import "package:flutter/widgets.dart";
import "package:google_mobile_ads/google_mobile_ads.dart";
import "ad_service.dart";
class AdMobService implements AdService {
BannerAd? _bannerAd;
bool _isLoaded = false;
// Ad unit IDs
static const String _androidBannerAdUnitId = "ca-app-pub-5177609929140951/8415596856";
static const String _iosBannerAdUnitId = "ca-app-pub-5177609929140951/7102515188";
String get _bannerAdUnitId {
if (defaultTargetPlatform == TargetPlatform.android) {
return _androidBannerAdUnitId;
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return _iosBannerAdUnitId;
} else {
throw UnsupportedError("Platform not supported");
}
}
@override
Future<void> initialize() async {
await MobileAds.instance.initialize();
print("AdMob initialized");
}
@override
Future<void> loadBannerAd() async {
_bannerAd = BannerAd(
adUnitId: _bannerAdUnitId,
size: AdSize.banner,
request: const AdRequest(),
listener: BannerAdListener(
onAdLoaded: (ad) {
print("Banner ad loaded");
_isLoaded = true;
},
onAdFailedToLoad: (ad, error) {
print("Banner ad failed to load: $error");
ad.dispose();
_isLoaded = false;
},
),
);
await _bannerAd!.load();
}
@override
Widget getBannerWidget() {
if (_bannerAd == null || !_isLoaded) {
return SizedBox.shrink();
}
return SizedBox(
height: 50,
child: AdWidget(ad: _bannerAd!),
);
}
@override
void dispose() {
_bannerAd?.dispose();
_bannerAd = null;
_isLoaded = false;
}
@override
bool get isLoaded => _isLoaded;
}

View File

@@ -0,0 +1,3 @@
// Conditional export based on platform
export "adsense_service_stub.dart"
if (dart.library.js_interop) "adsense_service_web.dart";

View File

@@ -0,0 +1,32 @@
import "package:flutter/widgets.dart";
import "ad_service.dart";
/// Stub implementation for non-web platforms
class AdSenseService implements AdService {
bool _isLoaded = false;
@override
Future<void> initialize() async {
// Not supported on non-web platforms
_isLoaded = false;
}
@override
Future<void> loadBannerAd() async {
// Not supported on non-web platforms
_isLoaded = false;
}
@override
Widget getBannerWidget() {
return SizedBox.shrink();
}
@override
void dispose() {
_isLoaded = false;
}
@override
bool get isLoaded => _isLoaded;
}

View File

@@ -0,0 +1,80 @@
import "dart:ui_web" as ui_web;
import "dart:js" as js;
import "package:flutter/widgets.dart";
import "package:universal_html/html.dart" as html;
import "ad_service.dart";
/// Web implementation of AdSense service
class AdSenseService implements AdService {
bool _isLoaded = false;
static const String _adSlotId = "XXXXXXXXXX"; // Replace with your ad slot ID
@override
Future<void> initialize() async {
// AdSense is loaded via script tag in index.html
print("AdSense initialized (web)");
_isLoaded = true;
}
@override
Future<void> loadBannerAd() async {
// AdSense ads are loaded automatically when widget renders
_isLoaded = true;
}
@override
Widget getBannerWidget() {
final viewType = "adsense-banner-${DateTime.now().millisecondsSinceEpoch}";
// Register the view factory
// ignore: undefined_prefixed_name
ui_web.platformViewRegistry.registerViewFactory(
viewType,
(int viewId) => _createAdElement(),
);
return SizedBox(
height: 90,
child: HtmlElementView(viewType: viewType),
);
}
html.Element _createAdElement() {
// Create AdSense div
final adContainer = html.DivElement()
..style.width = "100%"
..style.height = "90px"
..style.textAlign = "center";
final adElement = html.Element.html("""
<ins class="adsbygoogle"
style="display:block"
data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"
data-ad-slot="$_adSlotId"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
""");
adContainer.append(adElement);
// Push ad using dart:js instead of eval
try {
final adsbygoogle = js.context["adsbygoogle"];
if (adsbygoogle != null) {
adsbygoogle.callMethod("push", [js.JsObject.jsify({})]);
}
} catch (e) {
print("Error pushing AdSense ad: $e");
}
return adContainer;
}
@override
void dispose() {
_isLoaded = false;
}
@override
bool get isLoaded => _isLoaded;
}

View File

@@ -111,7 +111,10 @@ class QuoteGeneratorApi {
final response = await http.post(
url,
headers: {"Content-Type": "application/json"},
headers: {
"Content-Type": "application/json",
"User-Agent": "QuoteGen-Flutter/1.0",
},
body: jsonEncode(requestBody),
);
@@ -127,7 +130,10 @@ class QuoteGeneratorApi {
final queryString = request.toQueryString();
final url = Uri.parse("$_baseUrl/generate?$queryString");
final response = await http.get(url);
final response = await http.get(
url,
headers: {"User-Agent": "QuoteGen-Flutter/1.0"},
);
if (response.statusCode == 200) {
return response.bodyBytes;
@@ -146,7 +152,10 @@ class QuoteGeneratorApi {
static Future<bool> checkHealth() async {
try {
final url = Uri.parse("$_baseUrl/health");
final response = await http.get(url);
final response = await http.get(
url,
headers: {"User-Agent": "QuoteGen-Flutter/1.0"},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);

View File

@@ -170,7 +170,10 @@ class QuoteGeneratorApiV2 {
final response = await http.post(
url,
headers: {"Content-Type": "application/json"},
headers: {
"Content-Type": "application/json",
"User-Agent": "QuoteGen-Flutter/1.0",
},
body: jsonEncode(request.toJson()),
);
@@ -178,7 +181,7 @@ class QuoteGeneratorApiV2 {
final data = jsonDecode(response.body);
return QuoteSession.fromJson(data);
} else {
throw Exception("Failed to create sesion: ${response.statusCode}");
throw Exception("Failed to create session: ${response.statusCode}");
}
}
@@ -187,7 +190,10 @@ class QuoteGeneratorApiV2 {
static Future<QuoteSession> getSession(String sessionId) async {
final url = Uri.parse("$_baseUrl/v2/quote/$sessionId");
final response = await http.get(url);
final response = await http.get(
url,
headers: {"User-Agent": "QuoteGen-Flutter/1.0"},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
@@ -206,7 +212,10 @@ class QuoteGeneratorApiV2 {
final response = await http.patch(
url,
headers: {"Content-Type": "application/json"},
headers: {
"Content-Type": "application/json",
"User-Agent": "QuoteGen-Flutter/1.0",
},
body: jsonEncode(updates.toJson()),
);
@@ -214,7 +223,7 @@ class QuoteGeneratorApiV2 {
final data = jsonDecode(response.body);
return QuoteSession.fromJson(data);
} else {
throw Exception("Failed to updte session: ${response.statusCode}");
throw Exception("Failed to update session: ${response.statusCode}");
}
}
@@ -222,12 +231,15 @@ class QuoteGeneratorApiV2 {
static Future<Uint8List> generateImage(String sessionId) async {
final url = Uri.parse("$_baseUrl/v2/quote/$sessionId/image");
final response = await http.get(url);
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 genrate image: ${response.statusCode}");
throw Exception("Failed to generate image: ${response.statusCode}");
}
}
@@ -236,15 +248,18 @@ class QuoteGeneratorApiV2 {
static Future<void> deleteSession(String sessionId) async {
final url = Uri.parse("$_baseUrl/v2/quote/$sessionId");
final response = await http.delete(url);
final response = await http.delete(
url,
headers: {"User-Agent": "QuoteGen-Flutter/1.0"},
);
if (response.statusCode != 204) {
throw Exception("Failed to delte session: ${response.statusCode}");
throw Exception("Failed to delete session: ${response.statusCode}");
}
}
// helper to get curren timestamp in seconds
// helper to get current timestamp in seconds
static int getCurrentTimestamp() {
return DateTime.now().millisecondsSinceEpoch ~/ 1000;
}

View File

@@ -0,0 +1,75 @@
import "package:flutter/foundation.dart" show kIsWeb;
import "package:flutter/widgets.dart";
import "package:shadcn_flutter/shadcn_flutter.dart";
import "../services/ad_service.dart";
import "../services/admob_service.dart";
import "../services/adsense_service.dart";
class AdBanner extends StatefulWidget {
const AdBanner({Key? key}) : super(key: key);
@override
State<AdBanner> createState() => _AdBannerState();
}
class _AdBannerState extends State<AdBanner> {
late AdService _adService;
bool _isLoading = true;
@override
void initState() {
super.initState();
_initializeAds();
}
Future<void> _initializeAds() async {
// Platform detection
if (kIsWeb) {
_adService = AdSenseService();
} else {
_adService = AdMobService();
}
try {
await _adService.initialize();
await _adService.loadBannerAd();
} catch (e) {
print("Error initializng ads: $e");
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override
void dispose() {
_adService.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return SizedBox(
height: 50,
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}
if (!_adService.isLoaded) {
// Ad failed to load, show nothing
return SizedBox.shrink();
}
return _adService.getBannerWidget();
}
}