Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions lib/ads/inline_ad_cache_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,10 @@ class InlineAdCacheService {
'Clearing all cached inline ads and disposing their resources.',
);
for (final ad in _cache.values.whereType<InlineAd>()) {

// Delegate disposal to AdService
_adService.disposeAd(ad);
}

// Ensure cache is empty after disposal attempts.
_cache.clear();
_logger.info('All cached inline ads cleared.');
Expand Down
1 change: 0 additions & 1 deletion lib/app/view/app.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'package:auth_repository/auth_repository.dart';
import 'package:core/core.dart' hide AppStatus;
import 'package:data_repository/data_repository.dart';
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_service.dart';
Expand Down
166 changes: 72 additions & 94 deletions lib/headline-details/view/headline_details_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,17 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
),
),
),
SliverPadding(
padding: horizontalPadding.copyWith(top: AppSpacing.md),
sliver: SliverToBoxAdapter(
child: Text(
DateFormat('yyyy/MM/dd').format(headline.createdAt),
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
),
),
),
),
SliverPadding(
padding: EdgeInsets.only(
top: AppSpacing.md,
Expand Down Expand Up @@ -350,10 +361,25 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
SliverPadding(
padding: horizontalPadding.copyWith(top: AppSpacing.lg),
sliver: SliverToBoxAdapter(
child: Wrap(
spacing: AppSpacing.md,
runSpacing: AppSpacing.sm,
children: _buildMetadataChips(context, headline, onEntityChipTap),
child: SizedBox(
height: 36,
child: BlocBuilder<HeadlineDetailsBloc, HeadlineDetailsState>(
builder: (context, state) {
final chips = _buildMetadataChips(
context,
headline,
onEntityChipTap,
);
return ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: chips.length,
separatorBuilder: (context, index) =>
const SizedBox(width: AppSpacing.sm),
itemBuilder: (context, index) => chips[index],
clipBehavior: Clip.none,
);
},
),
),
),
),
Expand Down Expand Up @@ -540,102 +566,54 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
final colorScheme = theme.colorScheme;
final chipLabelStyle = textTheme.labelMedium?.copyWith(
color: colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
);
final chipBackgroundColor = colorScheme.secondaryContainer;
final chipBackgroundColor = colorScheme.secondaryContainer.withOpacity(0.6);
final chipAvatarColor = colorScheme.onSecondaryContainer;
const chipAvatarSize = AppSpacing.md;
const chipPadding = EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
);
final chipShape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppSpacing.sm),
side: BorderSide(color: colorScheme.outlineVariant.withOpacity(0.3)),
);

final chips = <Widget>[];

final formattedDate = DateFormat('MMM d, yyyy').format(headline.createdAt);
chips
..add(
Chip(
avatar: Icon(
Icons.calendar_today_outlined,
size: chipAvatarSize,
color: chipAvatarColor,
),
label: Text(formattedDate),
labelStyle: chipLabelStyle,
backgroundColor: chipBackgroundColor,
padding: chipPadding,
shape: chipShape,
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
)
..add(
InkWell(
onTap: () => onEntityChipTap(ContentType.source, headline.source.id),
borderRadius: BorderRadius.circular(AppSpacing.sm),
child: Chip(
avatar: Icon(
Icons.source_outlined,
size: chipAvatarSize,
color: chipAvatarColor,
),
label: Text(headline.source.name),
labelStyle: chipLabelStyle,
backgroundColor: chipBackgroundColor,
padding: chipPadding,
shape: chipShape,
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
const chipAvatarSize = 18.0;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a hardcoded value 18.0 for chipAvatarSize makes the code harder to maintain and less consistent with the app's design system. The previous implementation used AppSpacing.md.

If 18.0 is a new standard size, consider adding it as a constant to your AppSpacing class (e.g., AppSpacing.mdLg). If an existing constant from AppSpacing is suitable, please use that instead to maintain consistency.


Widget buildChip({
required IconData icon,
required String label,
required VoidCallback onPressed,
}) {
return ActionChip(
avatar: Icon(icon, size: chipAvatarSize, color: chipAvatarColor),
label: Text(label),
labelStyle: chipLabelStyle,
backgroundColor: chipBackgroundColor,
onPressed: onPressed,
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
)
..add(
InkWell(
onTap: () => onEntityChipTap(ContentType.topic, headline.topic.id),
borderRadius: BorderRadius.circular(AppSpacing.sm),
child: Chip(
avatar: Icon(
Icons.category_outlined,
size: chipAvatarSize,
color: chipAvatarColor,
),
label: Text(headline.topic.name),
labelStyle: chipLabelStyle,
backgroundColor: chipBackgroundColor,
padding: chipPadding,
shape: chipShape,
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
)
..add(
InkWell(
onTap: () =>
onEntityChipTap(ContentType.country, headline.eventCountry.id),
borderRadius: BorderRadius.circular(AppSpacing.sm),
child: Chip(
avatar: Icon(
Icons.location_city_outlined,
size: chipAvatarSize,
color: chipAvatarColor,
),
label: Text(headline.eventCountry.name),
labelStyle: chipLabelStyle,
backgroundColor: chipBackgroundColor,
padding: chipPadding,
shape: chipShape,
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppSpacing.lg),
side: BorderSide.none,
),
);
}

return chips;
return [
buildChip(
icon: Icons.source_outlined,
label: headline.source.name,
onPressed: () =>
onEntityChipTap(ContentType.source, headline.source.id),
),
buildChip(
icon: Icons.category_outlined,
label: headline.topic.name,
onPressed: () => onEntityChipTap(ContentType.topic, headline.topic.id),
),
buildChip(
icon: Icons.location_city_outlined,
label: headline.eventCountry.name,
onPressed: () =>
onEntityChipTap(ContentType.country, headline.eventCountry.id),
),
];
}

Widget _buildSimilarHeadlinesSection(
Expand Down
56 changes: 24 additions & 32 deletions lib/headlines-feed/view/headlines_feed_page.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
//
// ignore_for_file: lines_longer_than_80_chars

import 'package:core/core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/ads/interstitial_ad_manager.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_placeholder.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/feed_ad_loader_widget.dart';
Expand Down Expand Up @@ -237,23 +233,6 @@ class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
);
}

Future<void> onHeadlineTap(Headline headline) async {
// Await for the ad to be shown and dismissed.
await context
.read<InterstitialAdManager>()
.onPotentialAdTrigger();

// Check if the widget is still in the tree before navigating.
if (!context.mounted) return;

// Proceed with navigation after the ad is closed.
await context.pushNamed(
Routes.articleDetailsName,
pathParameters: {'id': headline.id},
extra: headline,
);
}

return RefreshIndicator(
onRefresh: () async {
context.read<HeadlinesFeedBloc>().add(
Expand All @@ -272,15 +251,16 @@ class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
? state.feedItems.length + 1
: state.feedItems.length,
separatorBuilder: (context, index) {
if (index < state.feedItems.length - 1) {
final currentItem = state.feedItems[index];
final nextItem = state.feedItems[index + 1];
// Adjust spacing around any decorator or ad
if (currentItem is! Headline || nextItem is! Headline) {
return const SizedBox(height: AppSpacing.md);
}
if (index >= state.feedItems.length - 1) {
return const SizedBox.shrink();
}
final currentItem = state.feedItems[index];
final nextItem = state.feedItems[index + 1];

if (currentItem is! Headline || nextItem is! Headline) {
return const SizedBox(height: AppSpacing.md);
}
return const SizedBox(height: AppSpacing.lg);
return const SizedBox(height: AppSpacing.sm);
},
itemBuilder: (context, index) {
if (index >= state.feedItems.length) {
Expand All @@ -305,17 +285,29 @@ class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
case HeadlineImageStyle.hidden:
tile = HeadlineTileTextOnly(
headline: item,
onHeadlineTap: () => onHeadlineTap(item),
onHeadlineTap: () =>
HeadlineTapHandler.handleHeadlineTap(
context,
item,
),
);
case HeadlineImageStyle.smallThumbnail:
tile = HeadlineTileImageStart(
headline: item,
onHeadlineTap: () => onHeadlineTap(item),
onHeadlineTap: () =>
HeadlineTapHandler.handleHeadlineTap(
context,
item,
),
);
case HeadlineImageStyle.largeThumbnail:
tile = HeadlineTileImageTop(
headline: item,
onHeadlineTap: () => onHeadlineTap(item),
onHeadlineTap: () =>
HeadlineTapHandler.handleHeadlineTap(
context,
item,
),
);
}
return tile;
Expand Down
2 changes: 1 addition & 1 deletion lib/shared/widgets/feed_core/feed_core.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export 'headline_metadata_row.dart';
export 'headline_source_row.dart';
export 'headline_tap_handler.dart';
export 'headline_tile_image_start.dart';
export 'headline_tile_image_top.dart';
Expand Down
84 changes: 84 additions & 0 deletions lib/shared/widgets/feed_core/headline_source_row.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import 'package:core/core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/ads/interstitial_ad_manager.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
import 'package:go_router/go_router.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'package:ui_kit/ui_kit.dart';

/// {@template headline_source_row}
/// A widget to display the source and publish date of a headline.
/// {@endtemplate}
class HeadlineSourceRow extends StatelessWidget {
/// {@macro headline_source_row}
const HeadlineSourceRow({required this.headline, super.key});

/// The headline data to display.
final Headline headline;

Future<void> _handleEntityTap(BuildContext context) async {
await context.read<InterstitialAdManager>().onPotentialAdTrigger();
if (!context.mounted) return;
await context.pushNamed(
Routes.entityDetailsName,
pathParameters: {
'type': ContentType.source.name,
'id': headline.source.id,
},
);
}

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final colorScheme = theme.colorScheme;
final currentLocale = context.watch<AppBloc>().state.locale;

final formattedDate = timeago.format(
headline.createdAt,
locale: currentLocale.languageCode,
);

final sourceTextStyle = textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
);

final dateTextStyle = textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
);

return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: InkWell(
onTap: () => _handleEntityTap(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.source_outlined,
size: AppSpacing.md,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(width: AppSpacing.xs),
Flexible(
child: Text(
headline.source.name,
style: sourceTextStyle,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
if (formattedDate.isNotEmpty) Text(formattedDate, style: dateTextStyle),
],
);
}
}
Loading
Loading