Skip to content

Commit 3a844d8

Browse files
committed
feat(headline-details): add in-article ads to headline details page
- Integrate ad loading and display into the headline details page - Add logic to determine ad placement based on configuration - Implement sliver list adjustments for ad insertion - Include necessary imports and service injections for ad functionality
1 parent 485b677 commit 3a844d8

File tree

1 file changed

+220
-110
lines changed

1 file changed

+220
-110
lines changed

lib/headline-details/view/headline_details_page.dart

Lines changed: 220 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
//
22
// ignore_for_file: avoid_redundant_argument_values
33

4+
import 'package:collection/collection.dart';
45
import 'package:core/core.dart';
56
import 'package:flutter/foundation.dart' show kIsWeb;
67
import 'package:flutter/material.dart';
78
import 'package:flutter_bloc/flutter_bloc.dart';
89
import 'package:flutter_news_app_mobile_client_full_source_code/account/bloc/account_bloc.dart';
10+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_service.dart';
11+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
12+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/in_article_ad_loader_widget.dart';
913
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
1014
import 'package:flutter_news_app_mobile_client_full_source_code/headline-details/bloc/headline_details_bloc.dart';
1115
import 'package:flutter_news_app_mobile_client_full_source_code/headline-details/bloc/similar_headlines_bloc.dart';
@@ -236,147 +240,253 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
236240
},
237241
);
238242

239-
return CustomScrollView(
240-
slivers: [
241-
SliverAppBar(
242-
leading: IconButton(
243-
icon: const Icon(Icons.arrow_back_ios_new),
244-
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
245-
onPressed: () => context.pop(),
246-
color: colorScheme.onSurface,
247-
),
248-
actions: [
249-
bookmarkButton,
250-
shareButtonWidget,
251-
const SizedBox(width: AppSpacing.sm),
252-
],
253-
pinned: false,
254-
floating: true,
255-
snap: true,
256-
backgroundColor: Colors.transparent,
257-
elevation: 0,
258-
foregroundColor: colorScheme.onSurface,
243+
final appBlocState = context.watch<AppBloc>().state;
244+
final adConfig = appBlocState.remoteConfig?.adConfig;
245+
final adService = context.read<AdService>();
246+
final adThemeStyle = AdThemeStyle.fromTheme(Theme.of(context));
247+
248+
final List<Widget> slivers = [
249+
SliverAppBar(
250+
leading: IconButton(
251+
icon: const Icon(Icons.arrow_back_ios_new),
252+
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
253+
onPressed: () => context.pop(),
254+
color: colorScheme.onSurface,
259255
),
260-
SliverPadding(
261-
padding: horizontalPadding.copyWith(top: AppSpacing.sm),
262-
sliver: SliverToBoxAdapter(
263-
child: Text(
264-
headline.title,
265-
style: textTheme.headlineSmall?.copyWith(
266-
fontWeight: FontWeight.bold,
267-
),
256+
actions: [
257+
bookmarkButton,
258+
shareButtonWidget,
259+
const SizedBox(width: AppSpacing.sm),
260+
],
261+
pinned: false,
262+
floating: true,
263+
snap: true,
264+
backgroundColor: Colors.transparent,
265+
elevation: 0,
266+
foregroundColor: colorScheme.onSurface,
267+
),
268+
SliverPadding(
269+
padding: horizontalPadding.copyWith(top: AppSpacing.sm),
270+
sliver: SliverToBoxAdapter(
271+
child: Text(
272+
headline.title,
273+
style: textTheme.headlineSmall?.copyWith(
274+
fontWeight: FontWeight.bold,
268275
),
269276
),
270277
),
271-
SliverPadding(
272-
padding: EdgeInsets.only(
273-
top: AppSpacing.md,
274-
left: horizontalPadding.left,
275-
right: horizontalPadding.right,
276-
),
277-
sliver: SliverToBoxAdapter(
278-
child: ClipRRect(
279-
borderRadius: BorderRadius.circular(AppSpacing.md),
280-
child: AspectRatio(
281-
aspectRatio: 16 / 9,
282-
child: Image.network(
283-
headline.imageUrl,
284-
fit: BoxFit.cover,
285-
loadingBuilder: (context, child, loadingProgress) {
286-
if (loadingProgress == null) return child;
287-
return ColoredBox(
288-
color: colorScheme.surfaceContainerHighest,
289-
child: const Center(
290-
child: CircularProgressIndicator(strokeWidth: 2),
291-
),
292-
);
293-
},
294-
errorBuilder: (context, error, stackTrace) => ColoredBox(
278+
),
279+
SliverPadding(
280+
padding: EdgeInsets.only(
281+
top: AppSpacing.md,
282+
left: horizontalPadding.left,
283+
right: horizontalPadding.right,
284+
),
285+
sliver: SliverToBoxAdapter(
286+
child: ClipRRect(
287+
borderRadius: BorderRadius.circular(AppSpacing.md),
288+
child: AspectRatio(
289+
aspectRatio: 16 / 9,
290+
child: Image.network(
291+
headline.imageUrl,
292+
fit: BoxFit.cover,
293+
loadingBuilder: (context, child, loadingProgress) {
294+
if (loadingProgress == null) return child;
295+
return ColoredBox(
295296
color: colorScheme.surfaceContainerHighest,
296-
child: Icon(
297-
Icons.broken_image_outlined,
298-
color: colorScheme.onSurfaceVariant,
299-
size: AppSpacing.xxl * 1.5,
297+
child: const Center(
298+
child: CircularProgressIndicator(strokeWidth: 2),
300299
),
300+
);
301+
},
302+
errorBuilder: (context, error, stackTrace) => ColoredBox(
303+
color: colorScheme.surfaceContainerHighest,
304+
child: Icon(
305+
Icons.broken_image_outlined,
306+
color: colorScheme.onSurfaceVariant,
307+
size: AppSpacing.xxl * 1.5,
301308
),
302309
),
303310
),
304311
),
305312
),
306313
),
314+
),
315+
];
316+
317+
// Add ad below main article image if configured
318+
if (adConfig != null &&
319+
adConfig.enabled &&
320+
adConfig.articleAdConfiguration.enabled) {
321+
final belowMainImageSlot = adConfig
322+
.articleAdConfiguration
323+
.inArticleAdSlotConfigurations
324+
.firstWhereOrNull(
325+
(slot) =>
326+
slot.slotType == InArticleAdSlotType.belowMainArticleImage &&
327+
slot.enabled,
328+
);
329+
330+
if (belowMainImageSlot != null) {
331+
slivers.add(
332+
SliverToBoxAdapter(
333+
child: Padding(
334+
padding: horizontalPadding.copyWith(top: AppSpacing.lg),
335+
child: InArticleAdLoaderWidget(
336+
slotConfiguration: belowMainImageSlot,
337+
adService: adService,
338+
adThemeStyle: adThemeStyle,
339+
adConfig: adConfig,
340+
),
341+
),
342+
),
343+
);
344+
}
345+
}
346+
347+
slivers.addAll([
348+
SliverPadding(
349+
padding: horizontalPadding.copyWith(top: AppSpacing.lg),
350+
sliver: SliverToBoxAdapter(
351+
child: Wrap(
352+
spacing: AppSpacing.md,
353+
runSpacing: AppSpacing.sm,
354+
children: _buildMetadataChips(context, headline),
355+
),
356+
),
357+
),
358+
if (headline.excerpt.isNotEmpty)
307359
SliverPadding(
308360
padding: horizontalPadding.copyWith(top: AppSpacing.lg),
309361
sliver: SliverToBoxAdapter(
310-
child: Wrap(
311-
spacing: AppSpacing.md,
312-
runSpacing: AppSpacing.sm,
313-
children: _buildMetadataChips(context, headline),
362+
child: Text(
363+
headline.excerpt,
364+
style: textTheme.bodyLarge?.copyWith(
365+
color: colorScheme.onSurfaceVariant,
366+
height: 1.6,
367+
),
314368
),
315369
),
316370
),
317-
if (headline.excerpt.isNotEmpty)
318-
SliverPadding(
319-
padding: horizontalPadding.copyWith(top: AppSpacing.lg),
320-
sliver: SliverToBoxAdapter(
321-
child: Text(
322-
headline.excerpt,
323-
style: textTheme.bodyLarge?.copyWith(
324-
color: colorScheme.onSurfaceVariant,
325-
height: 1.6,
326-
),
371+
]);
372+
373+
// Add ad above continue reading button if configured
374+
if (adConfig != null &&
375+
adConfig.enabled &&
376+
adConfig.articleAdConfiguration.enabled) {
377+
final aboveContinueReadingSlot = adConfig
378+
.articleAdConfiguration
379+
.inArticleAdSlotConfigurations
380+
.firstWhereOrNull(
381+
(slot) =>
382+
slot.slotType ==
383+
InArticleAdSlotType.aboveArticleContinueReadingButton &&
384+
slot.enabled,
385+
);
386+
387+
if (aboveContinueReadingSlot != null) {
388+
slivers.add(
389+
SliverToBoxAdapter(
390+
child: Padding(
391+
padding: horizontalPadding.copyWith(top: AppSpacing.xl),
392+
child: InArticleAdLoaderWidget(
393+
slotConfiguration: aboveContinueReadingSlot,
394+
adService: adService,
395+
adThemeStyle: adThemeStyle,
396+
adConfig: adConfig,
327397
),
328398
),
329399
),
330-
if (headline.url.isNotEmpty)
331-
SliverPadding(
332-
padding: horizontalPadding.copyWith(
333-
top: AppSpacing.xl,
334-
bottom: AppSpacing.xl,
335-
),
336-
sliver: SliverToBoxAdapter(
337-
child: ElevatedButton.icon(
338-
icon: const Icon(Icons.open_in_new_outlined),
339-
onPressed: () async {
340-
await launchUrlString(headline.url);
341-
},
342-
label: Text(l10n.headlineDetailsContinueReadingButton),
343-
style: ElevatedButton.styleFrom(
344-
padding: const EdgeInsets.symmetric(
345-
horizontal: AppSpacing.lg,
346-
vertical: AppSpacing.md,
347-
),
348-
textStyle: textTheme.labelLarge?.copyWith(
349-
fontWeight: FontWeight.bold,
350-
),
400+
);
401+
}
402+
}
403+
404+
slivers.addAll([
405+
if (headline.url.isNotEmpty)
406+
SliverPadding(
407+
padding: horizontalPadding.copyWith(
408+
top: AppSpacing.xl,
409+
bottom: AppSpacing.xl,
410+
),
411+
sliver: SliverToBoxAdapter(
412+
child: ElevatedButton.icon(
413+
icon: const Icon(Icons.open_in_new_outlined),
414+
onPressed: () async {
415+
await launchUrlString(headline.url);
416+
},
417+
label: Text(l10n.headlineDetailsContinueReadingButton),
418+
style: ElevatedButton.styleFrom(
419+
padding: const EdgeInsets.symmetric(
420+
horizontal: AppSpacing.lg,
421+
vertical: AppSpacing.md,
422+
),
423+
textStyle: textTheme.labelLarge?.copyWith(
424+
fontWeight: FontWeight.bold,
351425
),
352426
),
353427
),
354428
),
355-
if (headline.url.isEmpty) // Ensure bottom padding
356-
const SliverPadding(
357-
padding: EdgeInsets.only(bottom: AppSpacing.xl),
358-
sliver: SliverToBoxAdapter(child: SizedBox.shrink()),
359-
),
360-
SliverPadding(
361-
padding: horizontalPadding,
362-
sliver: SliverToBoxAdapter(
429+
),
430+
if (headline.url.isEmpty) // Ensure bottom padding
431+
const SliverPadding(
432+
padding: EdgeInsets.only(bottom: AppSpacing.xl),
433+
sliver: SliverToBoxAdapter(child: SizedBox.shrink()),
434+
),
435+
]);
436+
437+
// Add ad below continue reading button if configured
438+
if (adConfig != null &&
439+
adConfig.enabled &&
440+
adConfig.articleAdConfiguration.enabled) {
441+
final belowContinueReadingSlot = adConfig
442+
.articleAdConfiguration
443+
.inArticleAdSlotConfigurations
444+
.firstWhereOrNull(
445+
(slot) =>
446+
slot.slotType ==
447+
InArticleAdSlotType.belowArticleContinueReadingButton &&
448+
slot.enabled,
449+
);
450+
451+
if (belowContinueReadingSlot != null) {
452+
slivers.add(
453+
SliverToBoxAdapter(
363454
child: Padding(
364-
padding: EdgeInsets.only(
365-
top: (headline.url.isNotEmpty) ? AppSpacing.sm : AppSpacing.xl,
366-
bottom: AppSpacing.md,
455+
padding: horizontalPadding.copyWith(top: AppSpacing.xl),
456+
child: InArticleAdLoaderWidget(
457+
slotConfiguration: belowContinueReadingSlot,
458+
adService: adService,
459+
adThemeStyle: adThemeStyle,
460+
adConfig: adConfig,
367461
),
368-
child: Text(
369-
l10n.similarHeadlinesSectionTitle,
370-
style: textTheme.titleLarge?.copyWith(
371-
fontWeight: FontWeight.bold,
372-
),
462+
),
463+
),
464+
);
465+
}
466+
}
467+
468+
slivers.addAll([
469+
SliverPadding(
470+
padding: horizontalPadding,
471+
sliver: SliverToBoxAdapter(
472+
child: Padding(
473+
padding: EdgeInsets.only(
474+
top: (headline.url.isNotEmpty) ? AppSpacing.sm : AppSpacing.xl,
475+
bottom: AppSpacing.md,
476+
),
477+
child: Text(
478+
l10n.similarHeadlinesSectionTitle,
479+
style: textTheme.titleLarge?.copyWith(
480+
fontWeight: FontWeight.bold,
373481
),
374482
),
375483
),
376484
),
377-
_buildSimilarHeadlinesSection(context, horizontalPadding),
378-
],
379-
);
485+
),
486+
_buildSimilarHeadlinesSection(context, horizontalPadding),
487+
]);
488+
489+
return CustomScrollView(slivers: slivers);
380490
}
381491

382492
List<Widget> _buildMetadataChips(BuildContext context, Headline headline) {

0 commit comments

Comments
 (0)