156 lines
5.3 KiB
Dart
156 lines
5.3 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../app_navigator.dart';
|
|
|
|
/// How long a non-gating placeholder stays up before closing itself —
|
|
/// short enough not to be a nuisance while testing, long enough to be
|
|
/// clearly noticed rather than flickering past.
|
|
const _autoDismissAfter = Duration(seconds: 4);
|
|
|
|
/// Stands in for a real ad while `adsEnabled` is false — see
|
|
/// lib/services/ad_config.dart for why that exists.
|
|
///
|
|
/// [mustConfirm] mirrors the difference between this app's two kinds of ad:
|
|
/// - `false` (the report-export and fuel-save interstitials): closes
|
|
/// itself after [_autoDismissAfter], the same "you didn't have to do
|
|
/// anything" feel as a plain interstitial.
|
|
/// - `true` (the cloud-sync gate): waits for the user to tap Continue,
|
|
/// standing in for a rewarded ad's watch-it-through requirement, since
|
|
/// the upload genuinely shouldn't proceed until they've acted.
|
|
///
|
|
/// Returns once the placeholder has been dismissed, so callers can treat it
|
|
/// exactly like awaiting a real ad's show-and-dismiss cycle. A no-op (and
|
|
/// an immediate return) if the navigator isn't mounted yet.
|
|
Future<void> showAdPlaceholder({required bool mustConfirm}) async {
|
|
final navigator = navigatorKey.currentState;
|
|
if (navigator == null) return;
|
|
|
|
await navigator.push(
|
|
PageRouteBuilder<void>(
|
|
opaque: true,
|
|
transitionDuration: const Duration(milliseconds: 180),
|
|
pageBuilder: (_, _, _) => AdPlaceholderScreen(mustConfirm: mustConfirm),
|
|
),
|
|
);
|
|
}
|
|
|
|
class AdPlaceholderScreen extends StatefulWidget {
|
|
/// See [showAdPlaceholder] — true for the sync gate, false otherwise.
|
|
final bool mustConfirm;
|
|
|
|
const AdPlaceholderScreen({super.key, required this.mustConfirm});
|
|
|
|
@override
|
|
State<AdPlaceholderScreen> createState() => _AdPlaceholderScreenState();
|
|
}
|
|
|
|
class _AdPlaceholderScreenState extends State<AdPlaceholderScreen> {
|
|
Timer? _ticker;
|
|
late int _secondsLeft = _autoDismissAfter.inSeconds;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (widget.mustConfirm) return;
|
|
_ticker = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
if (!mounted) {
|
|
timer.cancel();
|
|
return;
|
|
}
|
|
setState(() => _secondsLeft--);
|
|
if (_secondsLeft <= 0) _dismiss();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_ticker?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _dismiss() {
|
|
_ticker?.cancel();
|
|
if (mounted) Navigator.of(context).pop();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final colors = theme.colorScheme;
|
|
|
|
return PopScope(
|
|
// A real ad can't be dismissed with the system back gesture either;
|
|
// letting it through here would let the sync gate be bypassed.
|
|
canPop: false,
|
|
child: Scaffold(
|
|
backgroundColor: colors.surface,
|
|
body: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
children: [
|
|
Align(
|
|
alignment: Alignment.topCenter,
|
|
child: Text(
|
|
'ADVERTISEMENT',
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
color: colors.onSurfaceVariant,
|
|
letterSpacing: 1.6,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Image.asset(
|
|
'assets/icon/hero_icon.png',
|
|
height: 96,
|
|
fit: BoxFit.contain,
|
|
),
|
|
const SizedBox(height: 28),
|
|
Text(
|
|
'An advertisement will appear here',
|
|
textAlign: TextAlign.center,
|
|
style: theme.textTheme.titleMedium
|
|
?.copyWith(fontWeight: FontWeight.w700),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
'Ads are not running yet. This placeholder stands in '
|
|
'for one so the rest of the app behaves exactly as it '
|
|
'will once they are.',
|
|
textAlign: TextAlign.center,
|
|
style: theme.textTheme.bodyMedium
|
|
?.copyWith(color: colors.onSurfaceVariant),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (widget.mustConfirm)
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: FilledButton(
|
|
onPressed: _dismiss,
|
|
child: const Text('Continue'),
|
|
),
|
|
)
|
|
else
|
|
Text(
|
|
'Closing in $_secondsLeft…',
|
|
style: theme.textTheme.bodySmall
|
|
?.copyWith(color: colors.onSurfaceVariant),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|