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 'ad_media.dart'; class HomeAdBanner extends ConsumerStatefulWidget { const HomeAdBanner({super.key}); @override ConsumerState createState() => _HomeAdBannerState(); } class _HomeAdBannerState extends ConsumerState { final pages = PageController(); final impressed = {}; Timer? timer; int index = 0; int _length = 0; @override void dispose() { timer?.cancel(); pages.dispose(); super.dispose(); } void _sync(List ads) { if (_length == ads.length) return; _length = ads.length; timer?.cancel(); if (ads.length > 1) { timer = Timer.periodic(const Duration(seconds: 4), (_) { if (pages.hasClients) { pages.nextPage( duration: const Duration(milliseconds: 320), curve: Curves.easeOut, ); } }); } } void _impression(AdCreative ad, String? id) { if (!impressed.add(ad.adId)) return; unawaited( ref .read(adsApiClientProvider) .report(ad, 'ad_impression', trackingId: id), ); } Future _open(AdCreative ad, String? tracking) async { final client = ref.read(adsApiClientProvider); final id = client.newClickId(); final uri = ad.clickUri(id); if (uri == null) return; unawaited(client.report(ad, 'ad_click', clickId: id, trackingId: tracking)); if (!mounted) return; if (ad.openMode == AdOpenMode.inApp) { context.push( '${AppRoutes.web}?url=${Uri.encodeComponent(uri.toString())}', ); } else { await launchUrl(uri, mode: LaunchMode.externalApplication); } } @override Widget build(BuildContext context) { final async = ref.watch(featuredAdsProvider(homeBannerAdPlacement)); return async.maybeWhen( data: (value) { if (!value.hasAds) return const SizedBox.shrink(); final ads = value.ads; _sync(ads); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _impression(ads[index.clamp(0, ads.length - 1)], value.trackingId); } }); return Column( children: [ AspectRatio( aspectRatio: 750 / 200, child: ClipRRect( borderRadius: BorderRadius.circular(12), child: PageView.builder( controller: pages, onPageChanged: (page) { setState(() => index = page % ads.length); _impression(ads[index], value.trackingId); }, itemBuilder: (_, page) { final ad = ads[page % ads.length]; return GestureDetector( onTap: () => _open(ad, value.trackingId), child: AdMedia(ad: ad), ); }, ), ), ), if (ads.length > 1) Padding( padding: const EdgeInsets.only(top: 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( ads.length, (i) => Container( width: i == index ? 14 : 6, height: 6, margin: const EdgeInsets.symmetric(horizontal: 3), decoration: BoxDecoration( color: i == index ? Theme.of(context).colorScheme.primary : Theme.of(context).dividerColor, borderRadius: BorderRadius.circular(3), ), ), ), ), ), ], ); }, orElse: () => const SizedBox.shrink(), ); } }