Skip to content

Commit c7ab168

Browse files
committed
feat(entity_details): inject feed items
- Added FeedInjectorService - Display ads and account actions - Refactored list building - Displayed generic loading/error
1 parent 9f4178f commit c7ab168

File tree

1 file changed

+174
-96
lines changed

1 file changed

+174
-96
lines changed

lib/entity_details/view/entity_details_page.dart

Lines changed: 174 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:ht_main/entity_details/models/entity_type.dart';
99
import 'package:ht_main/l10n/l10n.dart';
1010
import 'package:ht_main/router/routes.dart'; // Added
1111
import 'package:ht_main/shared/constants/app_spacing.dart';
12+
import 'package:ht_main/shared/services/feed_injector_service.dart'; // Added
1213
import 'package:ht_main/shared/widgets/widgets.dart';
1314
import 'package:ht_shared/ht_shared.dart';
1415

@@ -40,21 +41,27 @@ class EntityDetailsPage extends StatelessWidget {
4041

4142
@override
4243
Widget build(BuildContext context) {
43-
return BlocProvider(
44-
create:
45-
(context) => EntityDetailsBloc(
46-
headlinesRepository: context.read<HtDataRepository<Headline>>(),
47-
categoryRepository: context.read<HtDataRepository<Category>>(),
48-
sourceRepository: context.read<HtDataRepository<Source>>(),
49-
accountBloc: context.read<AccountBloc>(),
50-
)..add(
51-
EntityDetailsLoadRequested(
52-
entityId: args.entityId,
53-
entityType: args.entityType,
54-
entity: args.entity,
55-
),
44+
return BlocProvider<EntityDetailsBloc>( // Explicitly type BlocProvider
45+
create: (context) {
46+
final feedInjectorService = FeedInjectorService();
47+
final entityDetailsBloc = EntityDetailsBloc(
48+
headlinesRepository: context.read<HtDataRepository<Headline>>(),
49+
categoryRepository: context.read<HtDataRepository<Category>>(),
50+
sourceRepository: context.read<HtDataRepository<Source>>(),
51+
accountBloc: context.read<AccountBloc>(),
52+
appBloc: context.read<AppBloc>(),
53+
feedInjectorService: feedInjectorService,
54+
);
55+
entityDetailsBloc.add(
56+
EntityDetailsLoadRequested(
57+
entityId: args.entityId,
58+
entityType: args.entityType,
59+
entity: args.entity,
5660
),
57-
child: EntityDetailsView(args: args), // Pass args
61+
);
62+
return entityDetailsBloc;
63+
},
64+
child: EntityDetailsView(args: args),
5865
);
5966
}
6067
}
@@ -112,17 +119,17 @@ class _EntityDetailsViewState extends State<EntityDetailsView> {
112119
if (state.status == EntityDetailsStatus.initial ||
113120
(state.status == EntityDetailsStatus.loading &&
114121
state.entity == null)) {
115-
return const LoadingStateWidget(
116-
icon: Icons.info_outline, // Or a more specific icon
117-
headline: 'Loading Details', // Replace with l10n
118-
subheadline: 'Please wait...', // Replace with l10n
122+
return LoadingStateWidget(
123+
icon: Icons.info_outline,
124+
headline: l10n.headlineDetailsLoadingHeadline, // Used generic loading
125+
subheadline: l10n.pleaseWait, // Used generic please wait
119126
);
120127
}
121128

122129
if (state.status == EntityDetailsStatus.failure &&
123130
state.entity == null) {
124131
return FailureStateWidget(
125-
message: state.errorMessage ?? 'Failed to load details.', // l10n
132+
message: state.errorMessage ?? l10n.unknownError, // Used generic error
126133
onRetry:
127134
() => context.read<EntityDetailsBloc>().add(
128135
EntityDetailsLoadRequested(
@@ -248,7 +255,7 @@ class _EntityDetailsViewState extends State<EntityDetailsView> {
248255
),
249256
const SizedBox(height: AppSpacing.lg),
250257
],
251-
if (state.headlines.isNotEmpty ||
258+
if (state.feedItems.isNotEmpty || // Changed
252259
state.headlinesStatus ==
253260
EntityHeadlinesStatus.loadingMore) ...[
254261
Text(
@@ -261,12 +268,11 @@ class _EntityDetailsViewState extends State<EntityDetailsView> {
261268
),
262269
),
263270
),
264-
if (state.headlines.isEmpty &&
271+
if (state.feedItems.isEmpty && // Changed
265272
state.headlinesStatus != EntityHeadlinesStatus.initial &&
266273
state.headlinesStatus != EntityHeadlinesStatus.loadingMore &&
267274
state.status == EntityDetailsStatus.success)
268275
SliverFillRemaining(
269-
// Use SliverFillRemaining for empty state
270276
child: Center(
271277
child: Text(
272278
l10n.noHeadlinesFoundMessage,
@@ -278,89 +284,161 @@ class _EntityDetailsViewState extends State<EntityDetailsView> {
278284
SliverList(
279285
delegate: SliverChildBuilderDelegate(
280286
(context, index) {
281-
if (index >= state.headlines.length) {
282-
return state.hasMoreHeadlines &&
287+
if (index >= state.feedItems.length) { // Changed
288+
return state.hasMoreHeadlines && // hasMoreHeadlines still refers to original headlines
283289
state.headlinesStatus ==
284290
EntityHeadlinesStatus.loadingMore
285291
? const Center(
286-
child: Padding(
287-
padding: EdgeInsets.all(AppSpacing.md),
288-
child: CircularProgressIndicator(),
289-
),
290-
)
292+
child: Padding(
293+
padding: EdgeInsets.all(AppSpacing.md),
294+
child: CircularProgressIndicator(),
295+
),
296+
)
291297
: const SizedBox.shrink();
292298
}
293-
final headline = state.headlines[index];
294-
final imageStyle =
295-
context
296-
.watch<AppBloc>()
297-
.state
298-
.settings
299-
.feedPreferences
300-
.headlineImageStyle;
299+
final item = state.feedItems[index]; // Changed
301300

302-
Widget tile;
303-
switch (imageStyle) {
304-
case HeadlineImageStyle.hidden:
305-
tile = HeadlineTileTextOnly(
306-
headline: headline,
307-
onHeadlineTap:
308-
() => context.pushNamed(
309-
Routes
310-
.globalArticleDetailsName, // Use new global route
311-
pathParameters: {'id': headline.id},
312-
extra: headline,
313-
),
314-
currentContextEntityType: state.entityType,
315-
currentContextEntityId:
316-
state.entity is Category
317-
? (state.entity as Category).id
318-
: state.entity is Source
319-
? (state.entity as Source).id
320-
: null,
321-
);
322-
case HeadlineImageStyle.smallThumbnail:
323-
tile = HeadlineTileImageStart(
324-
headline: headline,
325-
onHeadlineTap:
326-
() => context.pushNamed(
327-
Routes
328-
.globalArticleDetailsName, // Use new global route
329-
pathParameters: {'id': headline.id},
330-
extra: headline,
301+
if (item is Headline) {
302+
final imageStyle = context
303+
.watch<AppBloc>()
304+
.state
305+
.settings
306+
.feedPreferences
307+
.headlineImageStyle;
308+
Widget tile;
309+
switch (imageStyle) {
310+
case HeadlineImageStyle.hidden:
311+
tile = HeadlineTileTextOnly(
312+
headline: item,
313+
onHeadlineTap: () => context.pushNamed(
314+
Routes.globalArticleDetailsName,
315+
pathParameters: {'id': item.id},
316+
extra: item,
317+
),
318+
currentContextEntityType: state.entityType,
319+
currentContextEntityId: state.entity is Category
320+
? (state.entity as Category).id
321+
: state.entity is Source
322+
? (state.entity as Source).id
323+
: null,
324+
);
325+
break;
326+
case HeadlineImageStyle.smallThumbnail:
327+
tile = HeadlineTileImageStart(
328+
headline: item,
329+
onHeadlineTap: () => context.pushNamed(
330+
Routes.globalArticleDetailsName,
331+
pathParameters: {'id': item.id},
332+
extra: item,
333+
),
334+
currentContextEntityType: state.entityType,
335+
currentContextEntityId: state.entity is Category
336+
? (state.entity as Category).id
337+
: state.entity is Source
338+
? (state.entity as Source).id
339+
: null,
340+
);
341+
break;
342+
case HeadlineImageStyle.largeThumbnail:
343+
tile = HeadlineTileImageTop(
344+
headline: item,
345+
onHeadlineTap: () => context.pushNamed(
346+
Routes.globalArticleDetailsName,
347+
pathParameters: {'id': item.id},
348+
extra: item,
349+
),
350+
currentContextEntityType: state.entityType,
351+
currentContextEntityId: state.entity is Category
352+
? (state.entity as Category).id
353+
: state.entity is Source
354+
? (state.entity as Source).id
355+
: null,
356+
);
357+
break;
358+
}
359+
return tile;
360+
} else if (item is Ad) {
361+
return Card(
362+
margin: const EdgeInsets.symmetric(
363+
horizontal: AppSpacing.paddingMedium,
364+
vertical: AppSpacing.xs,
365+
),
366+
color: theme.colorScheme.surfaceContainerHighest,
367+
child: Padding(
368+
padding: const EdgeInsets.all(AppSpacing.md),
369+
child: Column(
370+
children: [
371+
if (item.imageUrl.isNotEmpty)
372+
Image.network(
373+
item.imageUrl,
374+
height: 100,
375+
errorBuilder: (ctx, err, st) =>
376+
const Icon(Icons.broken_image, size: 50),
377+
),
378+
const SizedBox(height: AppSpacing.sm),
379+
Text(
380+
'Placeholder Ad: ${item.adType?.name ?? 'Generic'}',
381+
style: theme.textTheme.titleSmall,
331382
),
332-
currentContextEntityType: state.entityType,
333-
currentContextEntityId:
334-
state.entity is Category
335-
? (state.entity as Category).id
336-
: state.entity is Source
337-
? (state.entity as Source).id
338-
: null,
339-
);
340-
case HeadlineImageStyle.largeThumbnail:
341-
tile = HeadlineTileImageTop(
342-
headline: headline,
343-
onHeadlineTap:
344-
() => context.pushNamed(
345-
Routes
346-
.globalArticleDetailsName, // Use new global route
347-
pathParameters: {'id': headline.id},
348-
extra: headline,
383+
Text(
384+
'Placement: ${item.placement?.name ?? 'Default'}',
385+
style: theme.textTheme.bodySmall,
349386
),
350-
currentContextEntityType: state.entityType,
351-
currentContextEntityId:
352-
state.entity is Category
353-
? (state.entity as Category).id
354-
: state.entity is Source
355-
? (state.entity as Source).id
356-
: null,
357-
);
387+
],
388+
),
389+
),
390+
);
391+
} else if (item is AccountAction) {
392+
return Card(
393+
margin: const EdgeInsets.symmetric(
394+
horizontal: AppSpacing.paddingMedium,
395+
vertical: AppSpacing.xs,
396+
),
397+
color: theme.colorScheme.secondaryContainer,
398+
child: ListTile(
399+
leading: Icon(
400+
item.accountActionType == AccountActionType.linkAccount
401+
? Icons.link
402+
: Icons.upgrade,
403+
color: theme.colorScheme.onSecondaryContainer,
404+
),
405+
title: Text(
406+
item.title,
407+
style: theme.textTheme.titleMedium?.copyWith(
408+
color: theme.colorScheme.onSecondaryContainer,
409+
fontWeight: FontWeight.bold,
410+
),
411+
),
412+
subtitle: item.description != null
413+
? Text(
414+
item.description!,
415+
style: theme.textTheme.bodySmall?.copyWith(
416+
color: theme.colorScheme.onSecondaryContainer.withOpacity(0.8),
417+
),
418+
)
419+
: null,
420+
trailing: item.callToActionText != null
421+
? ElevatedButton(
422+
style: ElevatedButton.styleFrom(
423+
backgroundColor: theme.colorScheme.secondary,
424+
foregroundColor: theme.colorScheme.onSecondary,
425+
),
426+
onPressed: () {
427+
if (item.callToActionUrl != null) {
428+
context.push(item.callToActionUrl!);
429+
}
430+
},
431+
child: Text(item.callToActionText!),
432+
)
433+
: null,
434+
isThreeLine: item.description != null && item.description!.length > 50,
435+
),
436+
);
358437
}
359-
return tile;
438+
return const SizedBox.shrink(); // Should not happen
360439
},
361-
childCount:
362-
state.headlines.length +
363-
(state.hasMoreHeadlines &&
440+
childCount: state.feedItems.length + // Changed
441+
(state.hasMoreHeadlines && // hasMoreHeadlines still refers to original headlines
364442
state.headlinesStatus ==
365443
EntityHeadlinesStatus.loadingMore
366444
? 1
@@ -369,7 +447,7 @@ class _EntityDetailsViewState extends State<EntityDetailsView> {
369447
),
370448
// Error display for headline loading specifically
371449
if (state.headlinesStatus == EntityHeadlinesStatus.failure &&
372-
state.headlines.isNotEmpty)
450+
state.feedItems.isNotEmpty) // Changed
373451
SliverToBoxAdapter(
374452
child: Padding(
375453
padding: const EdgeInsets.all(AppSpacing.md),

0 commit comments

Comments
 (0)