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

46
.dockerignore Normal file
View File

@@ -0,0 +1,46 @@
# Build outputs
build/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
# IDE
.idea/
.vscode/
*.iml
*.ipr
*.iws
# OS
.DS_Store
Thumbs.db
# Git
.git/
.gitignore
# Docker
Dockerfile
docker-compose.yml
.dockerignore
# Docs
*.md
README.md
# Tests
test/
.test_coverage/
# Android/iOS (not needed for web)
android/
ios/
macos/
windows/
linux/
# Misc
*.log
*.swp
.env

View File

@@ -18,6 +18,7 @@ jobs:
env: env:
ANDROID_HOME: /root/android-sdk ANDROID_HOME: /root/android-sdk
ANDROID_SDK_ROOT: /root/android-sdk ANDROID_SDK_ROOT: /root/android-sdk
GRADLE_OPTS: -Xmx2g -XX:MaxMetaspaceSize=512m
steps: steps:
- name: Checkout code - name: Checkout code

28
Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# Stage 1: Build the Flutter web app
FROM ghcr.io/cirruslabs/flutter:stable AS build
WORKDIR /app
# Copy pubspec files and get dependencies
COPY pubspec.yaml pubspec.lock ./
RUN flutter pub get
# Copy the rest of the app
COPY . .
# Build web app
RUN flutter build web --release --web-renderer html
# Stage 2: Serve with nginx
FROM nginx:alpine
# Copy the built web app from build stage
COPY --from=build /app/build/web /usr/share/nginx/html
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -4,6 +4,10 @@
android:label="quotegen_client" android:label="quotegen_client"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<!-- AdMob App ID -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-5177609929140951~7846774739"/>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

17
docker-compose.yml Normal file
View File

@@ -0,0 +1,17 @@
services:
quotegen-web:
build:
context: .
dockerfile: Dockerfile
container_name: quotegen_web
ports:
- "8080:80"
restart: unless-stopped
environment:
- TZ=UTC
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

View File

@@ -34,6 +34,13 @@ PODS:
- DKImagePickerController/PhotoGallery - DKImagePickerController/PhotoGallery
- Flutter - Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- Google-Mobile-Ads-SDK (11.13.0):
- GoogleUserMessagingPlatform (>= 1.1)
- google_mobile_ads (5.3.1):
- Flutter
- Google-Mobile-Ads-SDK (~> 11.13.0)
- webview_flutter_wkwebview
- GoogleUserMessagingPlatform (3.1.0)
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -46,18 +53,25 @@ PODS:
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- SwiftyGif (5.4.5) - SwiftyGif (5.4.5)
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- google_mobile_ads (from `.symlinks/plugins/google_mobile_ads/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- DKImagePickerController - DKImagePickerController
- DKPhotoGallery - DKPhotoGallery
- Google-Mobile-Ads-SDK
- GoogleUserMessagingPlatform
- SDWebImage - SDWebImage
- SwiftyGif - SwiftyGif
@@ -66,23 +80,31 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/file_picker/ios" :path: ".symlinks/plugins/file_picker/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
google_mobile_ads:
:path: ".symlinks/plugins/google_mobile_ads/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS: SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
Google-Mobile-Ads-SDK: 14f57f2dc33532a24db288897e26494640810407
google_mobile_ads: fe0e2c1764ad95323dd0e3081d0bb2d58411f957
GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
SDWebImage: f29024626962457f3470184232766516dee8dfea SDWebImage: f29024626962457f3470184232766516dee8dfea
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

View File

@@ -199,6 +199,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
202FD1FBBA50227619E09BFC /* [CP] Embed Pods Frameworks */, 202FD1FBBA50227619E09BFC /* [CP] Embed Pods Frameworks */,
9EB32ED1B9C2EE1090869288 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -340,6 +341,23 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
}; };
9EB32ED1B9C2EE1090869288 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
F9A7E4B35A518B03A03269AF /* [CP] Check Pods Manifest.lock */ = { F9A7E4B35A518B03A03269AF /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;

View File

@@ -45,5 +45,14 @@
<true/> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-5177609929140951~5220611390</string>
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string>
</dict>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -2,8 +2,16 @@ 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';
import 'package:shadcn_flutter/shadcn_flutter.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()); runApp(const MyApp());
} }

View File

@@ -10,6 +10,7 @@ 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;
import 'package:quotegen_client/services/quote_api_v2.dart'; 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:shadcn_flutter/shadcn_flutter.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -148,16 +149,24 @@ class _HomePageState extends State<HomePage> {
height: 600, height: 600,
child: _buildPreview(context) child: _buildPreview(context)
), ),
Gap(14), Gap(14),
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: 400, maxWidth: 400,
), ),
child: _buildForm(context) child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(child: _buildForm(context)),
Gap(14),
AdBanner(),
],
),
), ),
Gap(max(bottomPadding, 14)), Gap(max(bottomPadding, 14)),
], ],
), ),
@@ -170,7 +179,7 @@ class _HomePageState extends State<HomePage> {
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
Gap(max(topPadding, 14)), Gap(max(topPadding, 14)),
_buildPreview(context), _buildPreview(context),
@@ -179,6 +188,11 @@ class _HomePageState extends State<HomePage> {
_buildForm(context), _buildForm(context),
Gap(14),
// Banner ad
AdBanner(),
Gap(max(bottomPadding, 14)), 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( final response = await http.post(
url, url,
headers: {"Content-Type": "application/json"}, headers: {
"Content-Type": "application/json",
"User-Agent": "QuoteGen-Flutter/1.0",
},
body: jsonEncode(requestBody), body: jsonEncode(requestBody),
); );
@@ -127,7 +130,10 @@ class QuoteGeneratorApi {
final queryString = request.toQueryString(); final queryString = request.toQueryString();
final url = Uri.parse("$_baseUrl/generate?$queryString"); 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) { if (response.statusCode == 200) {
return response.bodyBytes; return response.bodyBytes;
@@ -146,7 +152,10 @@ class QuoteGeneratorApi {
static Future<bool> checkHealth() async { static Future<bool> checkHealth() async {
try { try {
final url = Uri.parse("$_baseUrl/health"); 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) { if (response.statusCode == 200) {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);

View File

@@ -170,7 +170,10 @@ class QuoteGeneratorApiV2 {
final response = await http.post( final response = await http.post(
url, url,
headers: {"Content-Type": "application/json"}, headers: {
"Content-Type": "application/json",
"User-Agent": "QuoteGen-Flutter/1.0",
},
body: jsonEncode(request.toJson()), body: jsonEncode(request.toJson()),
); );
@@ -178,7 +181,7 @@ class QuoteGeneratorApiV2 {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
return QuoteSession.fromJson(data); return QuoteSession.fromJson(data);
} else { } 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 { static Future<QuoteSession> getSession(String sessionId) async {
final url = Uri.parse("$_baseUrl/v2/quote/$sessionId"); 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) { if (response.statusCode == 200) {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
@@ -206,7 +212,10 @@ class QuoteGeneratorApiV2 {
final response = await http.patch( final response = await http.patch(
url, url,
headers: {"Content-Type": "application/json"}, headers: {
"Content-Type": "application/json",
"User-Agent": "QuoteGen-Flutter/1.0",
},
body: jsonEncode(updates.toJson()), body: jsonEncode(updates.toJson()),
); );
@@ -214,7 +223,7 @@ class QuoteGeneratorApiV2 {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
return QuoteSession.fromJson(data); return QuoteSession.fromJson(data);
} else { } 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 { static Future<Uint8List> generateImage(String sessionId) async {
final url = Uri.parse("$_baseUrl/v2/quote/$sessionId/image"); 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) { if (response.statusCode == 200) {
return response.bodyBytes; return response.bodyBytes;
} else { } 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 { static Future<void> deleteSession(String sessionId) async {
final url = Uri.parse("$_baseUrl/v2/quote/$sessionId"); 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) { 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() { static int getCurrentTimestamp() {
return DateTime.now().millisecondsSinceEpoch ~/ 1000; 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();
}
}

View File

@@ -9,10 +9,12 @@ import file_picker
import path_provider_foundation import path_provider_foundation
import share_plus import share_plus
import shared_preferences_foundation import shared_preferences_foundation
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"))
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"))
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
} }

30
nginx.conf Normal file
View File

@@ -0,0 +1,30 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Main Flutter app
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}

View File

@@ -41,6 +41,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -89,6 +97,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -208,6 +224,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "17.0.1" version: "17.0.1"
google_mobile_ads:
dependency: "direct main"
description:
name: google_mobile_ads
sha256: "0d4a3744b5e8ed1b8be6a1b452d309f811688855a497c6113fc4400f922db603"
url: "https://pub.dev"
source: hosted
version: "5.3.1"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -589,6 +621,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
universal_html:
dependency: "direct main"
description:
name: universal_html
sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4
url: "https://pub.dev"
source: hosted
version: "2.3.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
url: "https://pub.dev"
source: hosted
version: "2.3.1"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@@ -653,6 +701,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
webview_flutter:
dependency: transitive
description:
name: webview_flutter
sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba
url: "https://pub.dev"
source: hosted
version: "4.13.0"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: eeeb3fcd5f0ff9f8446c9f4bbc18a99b809e40297528a3395597d03aafb9f510
url: "https://pub.dev"
source: hosted
version: "4.10.11"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0"
url: "https://pub.dev"
source: hosted
version: "2.14.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: e49f378ed066efb13fc36186bbe0bd2425630d4ea0dbc71a18fdd0e4d8ed8ebc
url: "https://pub.dev"
source: hosted
version: "3.23.5"
win32: win32:
dependency: transitive dependency: transitive
description: description:

View File

@@ -31,6 +31,10 @@
<title>quotegen_client</title> <title>quotegen_client</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<!-- Google AdSense -->
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXXXXXXXX"
crossorigin="anonymous"></script>
</head> </head>
<body> <body>
<script src="flutter_bootstrap.js" async></script> <script src="flutter_bootstrap.js" async></script>