fix:第一轮跑
This commit is contained in:
1 parent
e834ec7586
commit
33235f450b
56 files changed
+4357
-210
No files matched your search
@@ -0,0 +1,32 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import '../../mall/application/mall_tab_config.dart';
|
||||
import '../data/ads_api_client.dart';
|
||||
import 'splash_ad_controller.dart';
|
||||
|
||||
final adConfigForegroundRefresherProvider = Provider(
|
||||
(ref) => AdConfigForegroundRefresher(
|
||||
() => ref.invalidate(featuredAdsProvider(homeBannerAdPlacement)),
|
||||
() => ref.read(splashAdControllerProvider).refresh(),
|
||||
() => ref.read(mallTabConfigProvider.notifier).refresh(),
|
||||
),
|
||||
);
|
||||
|
||||
class AdConfigForegroundRefresher {
|
||||
AdConfigForegroundRefresher(this._banner, this._splash, this._mall);
|
||||
final void Function() _banner;
|
||||
final Future<void> Function() _splash, _mall;
|
||||
Future<void>? _running;
|
||||
Future<void> refresh() =>
|
||||
_running ??= _refresh().whenComplete(() => _running = null);
|
||||
Future<void> _refresh() async {
|
||||
try {
|
||||
_banner();
|
||||
} catch (_) {}
|
||||
try {
|
||||
await _splash();
|
||||
} catch (_) {}
|
||||
try {
|
||||
await _mall();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' show File;
|
||||
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../../common/storage/storage_service.dart';
|
||||
import '../data/ads_api_client.dart';
|
||||
|
||||
class SplashAdEntry {
|
||||
const SplashAdEntry(this.ad, this.dailyCap, this.assetPath, this.cachedAtMs);
|
||||
final AdCreative ad;
|
||||
final int dailyCap;
|
||||
final String? assetPath;
|
||||
final int cachedAtMs;
|
||||
bool get valid =>
|
||||
ad.isRenderable &&
|
||||
DateTime.now().millisecondsSinceEpoch <
|
||||
cachedAtMs + const Duration(hours: 24).inMilliseconds &&
|
||||
(ad.endAtMs <= 0 || ad.endAtMs > DateTime.now().millisecondsSinceEpoch);
|
||||
Map<String, dynamic> toJson() => {
|
||||
'ad': ad.toJson(),
|
||||
'dailyCap': dailyCap,
|
||||
'assetPath': assetPath,
|
||||
'cachedAtMs': cachedAtMs,
|
||||
};
|
||||
factory SplashAdEntry.fromJson(Map<String, dynamic> map) => SplashAdEntry(
|
||||
AdCreative.fromJson(Map<String, dynamic>.from(map['ad'] as Map)),
|
||||
(map['dailyCap'] as num?)?.toInt() ?? 0,
|
||||
map['assetPath'] as String?,
|
||||
(map['cachedAtMs'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
class SplashAdController {
|
||||
SplashAdController(this._api);
|
||||
final AdsApiClient _api;
|
||||
static const _entryKey = 'ads.splash.entry';
|
||||
static const _dailyKey = 'ads.splash.daily';
|
||||
Future<SplashAdEntry?> coldLaunchDecision() async {
|
||||
final raw = StorageService.to.preferences.getString(_entryKey);
|
||||
if (raw == null) return null;
|
||||
try {
|
||||
final entry = SplashAdEntry.fromJson(
|
||||
Map<String, dynamic>.from(jsonDecode(raw) as Map),
|
||||
);
|
||||
if (!entry.valid || !_underCap(entry) || !await _ready(entry)) {
|
||||
return null;
|
||||
}
|
||||
return entry;
|
||||
} catch (_) {
|
||||
await clearCache();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool _underCap(SplashAdEntry entry) {
|
||||
final value = StorageService.to.preferences.getString(_dailyKey);
|
||||
final today = DateTime.now().toIso8601String().substring(0, 10);
|
||||
if (value == null) return true;
|
||||
final map = Map<String, dynamic>.from(jsonDecode(value) as Map);
|
||||
return map['date'] != today ||
|
||||
entry.dailyCap <= 0 ||
|
||||
((map['count'] as num?)?.toInt() ?? 0) < entry.dailyCap;
|
||||
}
|
||||
|
||||
Future<bool> _ready(SplashAdEntry entry) async =>
|
||||
entry.assetPath != null && await File(entry.assetPath!).exists();
|
||||
Future<void> markShown(SplashAdEntry entry) async {
|
||||
final today = DateTime.now().toIso8601String().substring(0, 10);
|
||||
final raw = StorageService.to.preferences.getString(_dailyKey);
|
||||
var count = 0;
|
||||
if (raw != null) {
|
||||
final m = Map<String, dynamic>.from(jsonDecode(raw) as Map);
|
||||
if (m['date'] == today) count = (m['count'] as num?)?.toInt() ?? 0;
|
||||
}
|
||||
await StorageService.to.preferences.setString(
|
||||
_dailyKey,
|
||||
jsonEncode({'date': today, 'count': count + 1}),
|
||||
);
|
||||
await _report(entry.ad, 'ad_impression');
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
try {
|
||||
final result = await _api.getAdResult(splashAdPlacement);
|
||||
if (!result.enabled || result.ad == null) {
|
||||
await _clearEntry();
|
||||
return;
|
||||
}
|
||||
final file = await DefaultCacheManager().getSingleFile(
|
||||
result.ad!.imageUrl,
|
||||
);
|
||||
if (!await file.exists()) return;
|
||||
await StorageService.to.preferences.setString(
|
||||
_entryKey,
|
||||
jsonEncode(
|
||||
SplashAdEntry(
|
||||
result.ad!,
|
||||
result.dailyCap,
|
||||
file.path,
|
||||
DateTime.now().millisecondsSinceEpoch,
|
||||
).toJson(),
|
||||
),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _report(AdCreative ad, String event, {String? clickId}) async {
|
||||
try {
|
||||
await _api.report(ad, event, clickId: clickId);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> skip(SplashAdEntry entry) => _report(entry.ad, 'ad_skip');
|
||||
Future<void> clearCache() async {
|
||||
await _clearEntry();
|
||||
await StorageService.to.preferences.remove(_dailyKey);
|
||||
}
|
||||
|
||||
Future<void> _clearEntry() async {
|
||||
await StorageService.to.preferences.remove(_entryKey);
|
||||
}
|
||||
}
|
||||
|
||||
final splashAdControllerProvider = Provider(
|
||||
(ref) => SplashAdController(ref.watch(adsApiClientProvider)),
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../common/router/app_router.dart';
|
||||
import '../data/ads_api_client.dart';
|
||||
import '../presentation/splash_ad_dialog.dart';
|
||||
import 'splash_ad_controller.dart';
|
||||
|
||||
final startupAdResolvedProvider = StateProvider<bool>((_) => false);
|
||||
|
||||
abstract final class StartupAdService {
|
||||
static Future<void>? _running;
|
||||
static Future<void> showIfAvailable(BuildContext context, WidgetRef ref) =>
|
||||
_running ??= _show(context, ref);
|
||||
static Future<void> _show(BuildContext context, WidgetRef ref) async {
|
||||
final controller = ref.read(splashAdControllerProvider);
|
||||
try {
|
||||
final entry = await controller.coldLaunchDecision();
|
||||
if (entry != null && context.mounted) {
|
||||
final result = await showGeneralDialog<SplashAdResult>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
barrierDismissible: false,
|
||||
barrierColor: Colors.black,
|
||||
pageBuilder: (ctx, _, _) => SplashAdDialog(
|
||||
entry: entry,
|
||||
onShown: () => unawaited(controller.markShown(entry)),
|
||||
onClick: () async {
|
||||
final id = ref.read(adsApiClientProvider).newClickId();
|
||||
final uri = entry.ad.clickUri(id);
|
||||
if (uri != null) {
|
||||
unawaited(
|
||||
ref
|
||||
.read(adsApiClientProvider)
|
||||
.report(entry.ad, 'ad_click', clickId: id),
|
||||
);
|
||||
if (entry.ad.openMode == AdOpenMode.inApp) {
|
||||
context.push(
|
||||
'${AppRoutes.web}?url=${Uri.encodeComponent(uri.toString())}',
|
||||
);
|
||||
} else {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
if (result == SplashAdResult.skipped) unawaited(controller.skip(entry));
|
||||
}
|
||||
} finally {
|
||||
ref.read(startupAdResolvedProvider.notifier).state = true;
|
||||
unawaited(controller.refresh());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user