add ad services and Docker configuration for web app
Some checks failed
Build Android App / build (push) Has been cancelled
Some checks failed
Build Android App / build (push) Has been cancelled
This commit is contained in:
46
.dockerignore
Normal file
46
.dockerignore
Normal 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
|
||||
@@ -18,6 +18,7 @@ jobs:
|
||||
env:
|
||||
ANDROID_HOME: /root/android-sdk
|
||||
ANDROID_SDK_ROOT: /root/android-sdk
|
||||
GRADLE_OPTS: -Xmx2g -XX:MaxMetaspaceSize=512m
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
28
Dockerfile
Normal file
28
Dockerfile
Normal 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;"]
|
||||
@@ -4,6 +4,10 @@
|
||||
android:label="quotegen_client"
|
||||
android:name="${applicationName}"
|
||||
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
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal 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
|
||||
@@ -34,6 +34,13 @@ PODS:
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- 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):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -46,18 +53,25 @@ PODS:
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- SwiftyGif (5.4.5)
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- google_mobile_ads (from `.symlinks/plugins/google_mobile_ads/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- Google-Mobile-Ads-SDK
|
||||
- GoogleUserMessagingPlatform
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
|
||||
@@ -66,23 +80,31 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
google_mobile_ads:
|
||||
:path: ".symlinks/plugins/google_mobile_ads/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
webview_flutter_wkwebview:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
Google-Mobile-Ads-SDK: 14f57f2dc33532a24db288897e26494640810407
|
||||
google_mobile_ads: fe0e2c1764ad95323dd0e3081d0bb2d58411f957
|
||||
GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c
|
||||
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
|
||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4
|
||||
|
||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
202FD1FBBA50227619E09BFC /* [CP] Embed Pods Frameworks */,
|
||||
9EB32ED1B9C2EE1090869288 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -340,6 +341,23 @@
|
||||
shellPath = /bin/sh;
|
||||
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 */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
||||
@@ -45,5 +45,14 @@
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<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>
|
||||
</plist>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -149,13 +150,21 @@ class _HomePageState extends State<HomePage> {
|
||||
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)),
|
||||
@@ -179,6 +188,11 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
_buildForm(context),
|
||||
|
||||
Gap(14),
|
||||
|
||||
// Banner ad
|
||||
AdBanner(),
|
||||
|
||||
Gap(max(bottomPadding, 14)),
|
||||
],
|
||||
),
|
||||
|
||||
19
lib/services/ad_service.dart
Normal file
19
lib/services/ad_service.dart
Normal 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;
|
||||
}
|
||||
73
lib/services/admob_service.dart
Normal file
73
lib/services/admob_service.dart
Normal 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;
|
||||
}
|
||||
3
lib/services/adsense_service.dart
Normal file
3
lib/services/adsense_service.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
// Conditional export based on platform
|
||||
export "adsense_service_stub.dart"
|
||||
if (dart.library.js_interop) "adsense_service_web.dart";
|
||||
32
lib/services/adsense_service_stub.dart
Normal file
32
lib/services/adsense_service_stub.dart
Normal 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;
|
||||
}
|
||||
80
lib/services/adsense_service_web.dart
Normal file
80
lib/services/adsense_service_web.dart
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
75
lib/widgets/ad_banner.dart
Normal file
75
lib/widgets/ad_banner.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,12 @@ import file_picker
|
||||
import path_provider_foundation
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import webview_flutter_wkwebview
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||
}
|
||||
|
||||
30
nginx.conf
Normal file
30
nginx.conf
Normal 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;
|
||||
}
|
||||
80
pubspec.lock
80
pubspec.lock
@@ -41,6 +41,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -89,6 +97,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -208,6 +224,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -589,6 +621,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -653,6 +701,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
|
||||
<title>quotegen_client</title>
|
||||
<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>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
|
||||
Reference in New Issue
Block a user