Skip to content

Commit c91f6cb

Browse files
committed
feat(search): Support ads and account actions
- Added Ad and AccountAction support - Unified item list for search results - Improved list item spacing
1 parent 4ece27f commit c91f6cb

File tree

1 file changed

+136
-56
lines changed

1 file changed

+136
-56
lines changed

lib/headlines-search/view/headlines_search_page.dart

Lines changed: 136 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import 'package:ht_main/l10n/l10n.dart';
1616
import 'package:ht_main/router/routes.dart';
1717
import 'package:ht_main/shared/constants/app_spacing.dart';
1818
import 'package:ht_main/shared/shared.dart'; // Imports new headline tiles
19-
// Adjusted imports to only include what's necessary after country removal
20-
import 'package:ht_shared/ht_shared.dart' show Category, Headline, HeadlineImageStyle, SearchModelType, Source;
19+
import 'package:ht_shared/ht_shared.dart'; // Changed to general import
2120

2221
/// Page widget responsible for providing the BLoC for the headlines search feature.
2322
class HeadlinesSearchPage extends StatelessWidget {
@@ -240,7 +239,7 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> {
240239
'Searching ${state.selectedModelType.displayName.toLowerCase()}...',
241240
),
242241
HeadlinesSearchSuccess(
243-
results: final results,
242+
items: final items, // Changed from results: final results
244243
hasMore: final hasMore,
245244
errorMessage: final errorMessage,
246245
lastSearchTerm: final lastSearchTerm,
@@ -256,75 +255,156 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> {
256255
),
257256
),
258257
)
259-
: results.isEmpty
258+
: items.isEmpty
260259
? FailureStateWidget(
261260
message:
262261
'${l10n.headlinesSearchNoResultsHeadline} for "$lastSearchTerm" in ${resultsModelType.displayName.toLowerCase()}.\n${l10n.headlinesSearchNoResultsSubheadline}',
263262
)
264263
: ListView.separated(
265264
controller: _scrollController,
266265
padding: const EdgeInsets.all(AppSpacing.paddingMedium),
267-
itemCount: hasMore ? results.length + 1 : results.length,
268-
separatorBuilder:
269-
(context, index) => const SizedBox(height: AppSpacing.md),
266+
itemCount: hasMore ? items.length + 1 : items.length,
267+
separatorBuilder: (context, index) {
268+
// Add a bit more space if the next item is an Ad or AccountAction
269+
if (index < items.length -1) {
270+
final currentItem = items[index];
271+
final nextItem = items[index+1];
272+
if ((currentItem is Headline && (nextItem is Ad || nextItem is AccountAction)) ||
273+
((currentItem is Ad || currentItem is AccountAction) && nextItem is Headline)) {
274+
return const SizedBox(height: AppSpacing.md);
275+
}
276+
}
277+
return const SizedBox(height: AppSpacing.md);
278+
},
270279
itemBuilder: (context, index) {
271-
if (index >= results.length) {
280+
if (index >= items.length) {
272281
return const Padding(
273282
padding: EdgeInsets.symmetric(vertical: AppSpacing.lg),
274283
child: Center(child: CircularProgressIndicator()),
275284
);
276285
}
277-
final item = results[index];
278-
// The switch is now exhaustive for the remaining SearchModelType values
279-
switch (resultsModelType) {
280-
case SearchModelType.headline:
281-
final headline = item as Headline;
282-
final imageStyle =
283-
context
284-
.watch<AppBloc>()
285-
.state
286-
.settings
287-
.feedPreferences
288-
.headlineImageStyle;
289-
Widget tile;
290-
switch (imageStyle) {
291-
case HeadlineImageStyle.hidden:
292-
tile = HeadlineTileTextOnly(
293-
headline: headline,
294-
onHeadlineTap:
295-
() => context.goNamed(
296-
Routes.searchArticleDetailsName,
297-
pathParameters: {'id': headline.id},
298-
extra: headline,
299-
),
300-
);
301-
case HeadlineImageStyle.smallThumbnail:
302-
tile = HeadlineTileImageStart(
303-
headline: headline,
304-
onHeadlineTap:
305-
() => context.goNamed(
306-
Routes.searchArticleDetailsName,
307-
pathParameters: {'id': headline.id},
308-
extra: headline,
286+
final feedItem = items[index];
287+
288+
if (feedItem is Headline) {
289+
final imageStyle = context
290+
.watch<AppBloc>()
291+
.state
292+
.settings
293+
.feedPreferences
294+
.headlineImageStyle;
295+
Widget tile;
296+
switch (imageStyle) {
297+
case HeadlineImageStyle.hidden:
298+
tile = HeadlineTileTextOnly(
299+
headline: feedItem,
300+
onHeadlineTap: () => context.goNamed(
301+
Routes.searchArticleDetailsName,
302+
pathParameters: {'id': feedItem.id},
303+
extra: feedItem,
304+
),
305+
);
306+
break;
307+
case HeadlineImageStyle.smallThumbnail:
308+
tile = HeadlineTileImageStart(
309+
headline: feedItem,
310+
onHeadlineTap: () => context.goNamed(
311+
Routes.searchArticleDetailsName,
312+
pathParameters: {'id': feedItem.id},
313+
extra: feedItem,
314+
),
315+
);
316+
break;
317+
case HeadlineImageStyle.largeThumbnail:
318+
tile = HeadlineTileImageTop(
319+
headline: feedItem,
320+
onHeadlineTap: () => context.goNamed(
321+
Routes.searchArticleDetailsName,
322+
pathParameters: {'id': feedItem.id},
323+
extra: feedItem,
324+
),
325+
);
326+
break;
327+
}
328+
return tile;
329+
} else if (feedItem is Category) {
330+
return CategoryItemWidget(category: feedItem);
331+
} else if (feedItem is Source) {
332+
return SourceItemWidget(source: feedItem);
333+
} else if (feedItem is Ad) {
334+
// Placeholder UI for Ad
335+
return Card(
336+
margin: const EdgeInsets.symmetric(vertical: AppSpacing.xs),
337+
color: colorScheme.surfaceContainerHighest,
338+
child: Padding(
339+
padding: const EdgeInsets.all(AppSpacing.md),
340+
child: Column(
341+
children: [
342+
if (feedItem.imageUrl.isNotEmpty)
343+
Image.network(
344+
feedItem.imageUrl,
345+
height: 100,
346+
errorBuilder: (ctx, err, st) =>
347+
const Icon(Icons.broken_image, size: 50),
348+
),
349+
const SizedBox(height: AppSpacing.sm),
350+
Text(
351+
'Placeholder Ad: ${feedItem.adType?.name ?? 'Generic'}',
352+
style: theme.textTheme.titleSmall,
353+
),
354+
Text(
355+
'Placement: ${feedItem.placement?.name ?? 'Default'}',
356+
style: theme.textTheme.bodySmall,
357+
),
358+
],
359+
),
360+
),
361+
);
362+
} else if (feedItem is AccountAction) {
363+
// Placeholder UI for AccountAction
364+
return Card(
365+
margin: const EdgeInsets.symmetric(vertical: AppSpacing.xs),
366+
color: colorScheme.secondaryContainer,
367+
child: ListTile(
368+
leading: Icon(
369+
feedItem.accountActionType == AccountActionType.linkAccount
370+
? Icons.link
371+
: Icons.upgrade,
372+
color: colorScheme.onSecondaryContainer,
373+
),
374+
title: Text(
375+
feedItem.title,
376+
style: theme.textTheme.titleMedium?.copyWith(
377+
color: colorScheme.onSecondaryContainer,
378+
fontWeight: FontWeight.bold,
379+
),
380+
),
381+
subtitle: feedItem.description != null
382+
? Text(
383+
feedItem.description!,
384+
style: theme.textTheme.bodySmall?.copyWith(
385+
color: colorScheme.onSecondaryContainer.withOpacity(0.8),
309386
),
310-
);
311-
case HeadlineImageStyle.largeThumbnail:
312-
tile = HeadlineTileImageTop(
313-
headline: headline,
314-
onHeadlineTap:
315-
() => context.goNamed(
316-
Routes.searchArticleDetailsName,
317-
pathParameters: {'id': headline.id},
318-
extra: headline,
387+
)
388+
: null,
389+
trailing: feedItem.callToActionText != null
390+
? ElevatedButton(
391+
style: ElevatedButton.styleFrom(
392+
backgroundColor: colorScheme.secondary,
393+
foregroundColor: colorScheme.onSecondary,
319394
),
320-
);
321-
}
322-
return tile;
323-
case SearchModelType.category:
324-
return CategoryItemWidget(category: item as Category);
325-
case SearchModelType.source:
326-
return SourceItemWidget(source: item as Source);
395+
onPressed: () {
396+
if (feedItem.callToActionUrl != null) {
397+
context.push(feedItem.callToActionUrl!);
398+
}
399+
},
400+
child: Text(feedItem.callToActionText!),
401+
)
402+
: null,
403+
isThreeLine: feedItem.description != null && feedItem.description!.length > 50,
404+
),
405+
);
327406
}
407+
return const SizedBox.shrink(); // Should not happen
328408
},
329409
),
330410
HeadlinesSearchFailure(

0 commit comments

Comments
 (0)