Skip to content

Feature headlines details page simlilar headlines #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 31, 2025
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
64 changes: 64 additions & 0 deletions lib/headline-details/bloc/similar_headlines_bloc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:ht_data_repository/ht_data_repository.dart';
import 'package:ht_shared/ht_shared.dart' show Headline, HtHttpException;

part 'similar_headlines_event.dart';
part 'similar_headlines_state.dart';

class SimilarHeadlinesBloc
extends Bloc<SimilarHeadlinesEvent, SimilarHeadlinesState> {
SimilarHeadlinesBloc({
required HtDataRepository<Headline> headlinesRepository,
}) : _headlinesRepository = headlinesRepository,
super(SimilarHeadlinesInitial()) {
on<FetchSimilarHeadlines>(_onFetchSimilarHeadlines);
}

final HtDataRepository<Headline> _headlinesRepository;
static const int _similarHeadlinesLimit = 5;

Future<void> _onFetchSimilarHeadlines(
FetchSimilarHeadlines event,
Emitter<SimilarHeadlinesState> emit,
) async {
emit(SimilarHeadlinesLoading());
try {
final currentHeadline = event.currentHeadline;
if (currentHeadline.category == null ||
currentHeadline.category!.id.isEmpty) {
emit(SimilarHeadlinesEmpty());
return;
}

final queryParams = {
'categories': currentHeadline.category!.id,
};

final response = await _headlinesRepository.readAllByQuery(
queryParams,
limit: _similarHeadlinesLimit + 1, // Fetch one extra to check if current is there
);

// Filter out the current headline from the results
final similarHeadlines = response.items
.where((headline) => headline.id != currentHeadline.id)
.toList();

// Take only the required limit after filtering
final finalSimilarHeadlines = similarHeadlines.take(_similarHeadlinesLimit).toList();

if (finalSimilarHeadlines.isEmpty) {
emit(SimilarHeadlinesEmpty());
} else {
emit(SimilarHeadlinesLoaded(similarHeadlines: finalSimilarHeadlines));
}
} on HtHttpException catch (e) {
emit(SimilarHeadlinesError(message: e.message));
} catch (e) {
emit(SimilarHeadlinesError(message: 'An unexpected error occurred: $e'));
}
}
}
17 changes: 17 additions & 0 deletions lib/headline-details/bloc/similar_headlines_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
part of 'similar_headlines_bloc.dart';

abstract class SimilarHeadlinesEvent extends Equatable {
const SimilarHeadlinesEvent();

@override
List<Object> get props => [];
}

class FetchSimilarHeadlines extends SimilarHeadlinesEvent {
const FetchSimilarHeadlines({required this.currentHeadline});

final Headline currentHeadline;

@override
List<Object> get props => [currentHeadline];
}
32 changes: 32 additions & 0 deletions lib/headline-details/bloc/similar_headlines_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
part of 'similar_headlines_bloc.dart';

abstract class SimilarHeadlinesState extends Equatable {
const SimilarHeadlinesState();

@override
List<Object> get props => [];
}

class SimilarHeadlinesInitial extends SimilarHeadlinesState {}

class SimilarHeadlinesLoading extends SimilarHeadlinesState {}

class SimilarHeadlinesLoaded extends SimilarHeadlinesState {
const SimilarHeadlinesLoaded({required this.similarHeadlines});

final List<Headline> similarHeadlines;

@override
List<Object> get props => [similarHeadlines];
}

class SimilarHeadlinesEmpty extends SimilarHeadlinesState {}

class SimilarHeadlinesError extends SimilarHeadlinesState {
const SimilarHeadlinesError({required this.message});

final String message;

@override
List<Object> get props => [message];
}
75 changes: 75 additions & 0 deletions lib/headline-details/view/headline_details_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ht_main/account/bloc/account_bloc.dart'; // Import AccountBloc
import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart'; // Import BLoC
import 'package:ht_main/headline-details/bloc/similar_headlines_bloc.dart'; // Import SimilarHeadlinesBloc
import 'package:ht_main/headlines-feed/widgets/headline_item_widget.dart'; // Import HeadlineItemWidget
import 'package:ht_main/l10n/l10n.dart';
import 'package:ht_main/router/routes.dart'; // Import Routes
import 'package:ht_main/shared/shared.dart';
import 'package:ht_shared/ht_shared.dart'
show Headline; // Import Headline model
Expand Down Expand Up @@ -365,6 +368,17 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
padding: EdgeInsets.only(bottom: AppSpacing.paddingLarge),
sliver: SliverToBoxAdapter(child: SizedBox.shrink()),
),
// --- Similar Headlines Section ---
SliverToBoxAdapter(
child: Padding(
padding: horizontalPadding.copyWith(top: AppSpacing.xl),
child: Text(
l10n.similarHeadlinesSectionTitle, // Add this l10n key
style: textTheme.titleLarge,
),
),
),
_buildSimilarHeadlinesSection(context),
],
);
}
Expand Down Expand Up @@ -469,4 +483,65 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {

return chips;
}

Widget _buildSimilarHeadlinesSection(BuildContext context) {
final l10n = context.l10n;
return BlocBuilder<SimilarHeadlinesBloc, SimilarHeadlinesState>(
builder: (context, state) {
return switch (state) {
SimilarHeadlinesInitial() ||
SimilarHeadlinesLoading() =>
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(AppSpacing.lg),
child: Center(child: CircularProgressIndicator()),
),
),
final SimilarHeadlinesError errorState => SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Text(
errorState.message,
textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
),
SimilarHeadlinesEmpty() => SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Text(
l10n.similarHeadlinesEmpty, // Add this l10n key
textAlign: TextAlign.center,
),
),
),
final SimilarHeadlinesLoaded loadedState => SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final similarHeadline = loadedState.similarHeadlines[index];
// Use a more compact item or reuse HeadlineItemWidget
// For now, reusing HeadlineItemWidget for simplicity
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.paddingMedium,
vertical: AppSpacing.sm,
),
// Navigate to a new HeadlineDetailsPage instance
// Ensure the targetRouteName is appropriate or handle navigation differently
child: HeadlineItemWidget(
headline: similarHeadline,
targetRouteName: Routes.articleDetailsName,
),
);
},
childCount: loadedState.similarHeadlines.length,
),
),
// Add a default case to satisfy exhaustiveness for the switch statement
_ => const SliverToBoxAdapter(child: SizedBox.shrink()),
};
},
);
}
}
8 changes: 8 additions & 0 deletions lib/l10n/arb/app_ar.arb
Original file line number Diff line number Diff line change
Expand Up @@ -787,5 +787,13 @@
"sharingUnavailableSnackbar": "المشاركة غير متاحة على هذا الجهاز أو المنصة.",
"@sharingUnavailableSnackbar": {
"description": "Snackbar message shown when sharing is unavailable"
},
"similarHeadlinesSectionTitle": "قد يعجبك ايضا",
"@similarHeadlinesSectionTitle": {
"description": "Title for the similar headlines section on the details page"
},
"similarHeadlinesEmpty": "لم يتم العثور على عناوين مشابهة.",
"@similarHeadlinesEmpty": {
"description": "Message shown when no similar headlines are found"
}
}
8 changes: 8 additions & 0 deletions lib/l10n/arb/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -787,5 +787,13 @@
"sharingUnavailableSnackbar": "Sharing is not available on this device or platform.",
"@sharingUnavailableSnackbar": {
"description": "Snackbar message shown when sharing is unavailable"
},
"similarHeadlinesSectionTitle": "You Might Also Like",
"@similarHeadlinesSectionTitle": {
"description": "Title for the similar headlines section on the details page"
},
"similarHeadlinesEmpty": "No similar headlines found.",
"@similarHeadlinesEmpty": {
"description": "Message shown when no similar headlines are found"
}
}
61 changes: 46 additions & 15 deletions lib/router/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import 'package:ht_main/authentication/view/authentication_page.dart';
import 'package:ht_main/authentication/view/email_code_verification_page.dart';
import 'package:ht_main/authentication/view/request_code_page.dart';
import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart'; // Re-added
import 'package:ht_main/headline-details/bloc/similar_headlines_bloc.dart'; // Import SimilarHeadlinesBloc
import 'package:ht_main/headline-details/view/headline_details_page.dart';
import 'package:ht_main/headlines-feed/bloc/categories_filter_bloc.dart'; // Import new BLoC
// import 'package:ht_main/headlines-feed/bloc/countries_filter_bloc.dart'; // Import new BLoC - REMOVED
Expand Down Expand Up @@ -344,11 +345,21 @@ GoRouter createRouter({
final headlineFromExtra = state.extra as Headline?;
final headlineIdFromPath = state.pathParameters['id'];

return BlocProvider(
create: (context) => HeadlineDetailsBloc(
headlinesRepository:
context.read<HtDataRepository<Headline>>(),
),
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => HeadlineDetailsBloc(
headlinesRepository:
context.read<HtDataRepository<Headline>>(),
),
),
BlocProvider(
create: (context) => SimilarHeadlinesBloc(
headlinesRepository:
context.read<HtDataRepository<Headline>>(),
),
),
],
child: HeadlineDetailsPage(
initialHeadline: headlineFromExtra,
// Ensure headlineId is non-null if initialHeadline is null
Expand Down Expand Up @@ -471,11 +482,21 @@ GoRouter createRouter({
builder: (context, state) {
final headlineFromExtra = state.extra as Headline?;
final headlineIdFromPath = state.pathParameters['id'];
return BlocProvider(
create: (context) => HeadlineDetailsBloc(
headlinesRepository:
context.read<HtDataRepository<Headline>>(),
),
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => HeadlineDetailsBloc(
headlinesRepository:
context.read<HtDataRepository<Headline>>(),
),
),
BlocProvider(
create: (context) => SimilarHeadlinesBloc(
headlinesRepository:
context.read<HtDataRepository<Headline>>(),
),
),
],
child: HeadlineDetailsPage(
initialHeadline: headlineFromExtra,
headlineId: headlineFromExtra?.id ?? headlineIdFromPath,
Expand Down Expand Up @@ -625,11 +646,21 @@ GoRouter createRouter({
builder: (context, state) {
final headlineFromExtra = state.extra as Headline?;
final headlineIdFromPath = state.pathParameters['id'];
return BlocProvider(
create: (context) => HeadlineDetailsBloc(
headlinesRepository:
context.read<HtDataRepository<Headline>>(),
),
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => HeadlineDetailsBloc(
headlinesRepository:
context.read<HtDataRepository<Headline>>(),
),
),
BlocProvider(
create: (context) => SimilarHeadlinesBloc(
headlinesRepository:
context.read<HtDataRepository<Headline>>(),
),
),
],
child: HeadlineDetailsPage(
initialHeadline: headlineFromExtra,
headlineId: headlineFromExtra?.id ?? headlineIdFromPath,
Expand Down
Loading