|
1 | 1 | // |
2 | 2 | // ignore_for_file: avoid_redundant_argument_values |
3 | 3 |
|
| 4 | +import 'package:collection/collection.dart'; |
4 | 5 | import 'package:core/core.dart'; |
5 | 6 | import 'package:flutter/foundation.dart' show kIsWeb; |
6 | 7 | import 'package:flutter/material.dart'; |
7 | 8 | import 'package:flutter_bloc/flutter_bloc.dart'; |
8 | 9 | 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'; |
9 | 13 | import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart'; |
10 | 14 | import 'package:flutter_news_app_mobile_client_full_source_code/headline-details/bloc/headline_details_bloc.dart'; |
11 | 15 | 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> { |
236 | 240 | }, |
237 | 241 | ); |
238 | 242 |
|
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, |
259 | 255 | ), |
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, |
268 | 275 | ), |
269 | 276 | ), |
270 | 277 | ), |
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( |
295 | 296 | 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), |
300 | 299 | ), |
| 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, |
301 | 308 | ), |
302 | 309 | ), |
303 | 310 | ), |
304 | 311 | ), |
305 | 312 | ), |
306 | 313 | ), |
| 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) |
307 | 359 | SliverPadding( |
308 | 360 | padding: horizontalPadding.copyWith(top: AppSpacing.lg), |
309 | 361 | 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 | + ), |
314 | 368 | ), |
315 | 369 | ), |
316 | 370 | ), |
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, |
327 | 397 | ), |
328 | 398 | ), |
329 | 399 | ), |
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, |
351 | 425 | ), |
352 | 426 | ), |
353 | 427 | ), |
354 | 428 | ), |
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( |
363 | 454 | 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, |
367 | 461 | ), |
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, |
373 | 481 | ), |
374 | 482 | ), |
375 | 483 | ), |
376 | 484 | ), |
377 | | - _buildSimilarHeadlinesSection(context, horizontalPadding), |
378 | | - ], |
379 | | - ); |
| 485 | + ), |
| 486 | + _buildSimilarHeadlinesSection(context, horizontalPadding), |
| 487 | + ]); |
| 488 | + |
| 489 | + return CustomScrollView(slivers: slivers); |
380 | 490 | } |
381 | 491 |
|
382 | 492 | List<Widget> _buildMetadataChips(BuildContext context, Headline headline) { |
|
0 commit comments