Skip to content

Commit 903946c

Browse files
committed
feat(feed): support ads and account actions
- Display ads in headlines feed - Added account actions support - Improved feed items handling
1 parent cfca236 commit 903946c

File tree

1 file changed

+147
-64
lines changed

1 file changed

+147
-64
lines changed

lib/headlines-feed/view/headlines_feed_page.dart

Lines changed: 147 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import 'package:ht_main/headlines-feed/bloc/headlines_feed_bloc.dart';
1010
import 'package:ht_main/l10n/l10n.dart';
1111
import 'package:ht_main/router/routes.dart';
1212
import 'package:ht_main/shared/shared.dart';
13-
import 'package:ht_shared/ht_shared.dart'
14-
show HeadlineImageStyle; // Added HeadlineImageStyle
13+
import 'package:ht_shared/ht_shared.dart'; // Import all of ht_shared
1514

1615
/// {@template headlines_feed_view}
1716
/// The core view widget for the headlines feed.
@@ -164,100 +163,184 @@ class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
164163
return const SizedBox.shrink();
165164

166165
case HeadlinesFeedLoaded():
167-
if (state.headlines.isEmpty) {
168-
// If the list is empty after filters, show a message
169-
// with a "Clear Filters" button using FailureStateWidget.
166+
if (state.feedItems.isEmpty) { // Changed from state.headlines
170167
return FailureStateWidget(
171168
message:
172169
'${l10n.headlinesFeedEmptyFilteredHeadline}\n${l10n.headlinesFeedEmptyFilteredSubheadline}',
173170
onRetry: () {
174-
// This will be our "Clear Filters" action
175171
context.read<HeadlinesFeedBloc>().add(
176172
HeadlinesFeedFiltersCleared(),
177173
);
178174
},
179175
retryButtonText:
180-
l10n.headlinesFeedClearFiltersButton, // New l10n string
176+
l10n.headlinesFeedClearFiltersButton,
181177
);
182178
}
183-
// Display the list of headlines with pull-to-refresh
184179
return RefreshIndicator(
185180
onRefresh: () async {
186-
// Dispatch refresh event to the BLoC
187181
context.read<HeadlinesFeedBloc>().add(
188182
HeadlinesFeedRefreshRequested(),
189183
);
190-
// Note: BLoC handles emitting loading state during refresh
191184
},
192185
child: ListView.separated(
193186
controller: _scrollController,
194187
padding: const EdgeInsets.only(
195188
top: AppSpacing.md,
196-
bottom:
197-
AppSpacing.xxl, // Ensure space below last item/loader
189+
bottom: AppSpacing.xxl,
198190
),
199-
itemCount:
200-
state.hasMore
201-
? state.headlines.length +
202-
1 // +1 for loading indicator
203-
: state.headlines.length,
204-
separatorBuilder:
205-
(context, index) => const SizedBox(
206-
height: AppSpacing.lg,
207-
), // Consistent spacing
191+
itemCount: state.hasMore
192+
? state.feedItems.length + 1 // Changed
193+
: state.feedItems.length, // Changed
194+
separatorBuilder: (context, index) {
195+
// Add a bit more space if the next item is an Ad or AccountAction
196+
if (index < state.feedItems.length -1) {
197+
final currentItem = state.feedItems[index];
198+
final nextItem = state.feedItems[index+1];
199+
if ((currentItem is Headline && (nextItem is Ad || nextItem is AccountAction)) ||
200+
((currentItem is Ad || currentItem is AccountAction) && nextItem is Headline)) {
201+
return const SizedBox(height: AppSpacing.md);
202+
}
203+
}
204+
return const SizedBox(height: AppSpacing.lg);
205+
},
208206
itemBuilder: (context, index) {
209-
// Check if it's the loading indicator item
210-
if (index >= state.headlines.length) {
211-
// Show loading indicator at the bottom if more items exist
207+
if (index >= state.feedItems.length) { // Changed
212208
return const Padding(
213209
padding: EdgeInsets.symmetric(vertical: AppSpacing.lg),
214210
child: Center(child: CircularProgressIndicator()),
215211
);
216212
}
217-
// Otherwise, build the headline item
218-
final headline = state.headlines[index];
219-
final imageStyle =
220-
context
221-
.watch<AppBloc>()
222-
.state
223-
.settings
224-
.feedPreferences
225-
.headlineImageStyle;
213+
final item = state.feedItems[index]; // Changed
226214

227-
Widget tile;
228-
switch (imageStyle) {
229-
case HeadlineImageStyle.hidden:
230-
tile = HeadlineTileTextOnly(
231-
headline: headline,
232-
onHeadlineTap:
233-
() => context.goNamed(
234-
Routes.articleDetailsName,
235-
pathParameters: {'id': headline.id},
236-
extra: headline,
237-
),
238-
);
239-
case HeadlineImageStyle.smallThumbnail:
240-
tile = HeadlineTileImageStart(
241-
headline: headline,
242-
onHeadlineTap:
243-
() => context.goNamed(
244-
Routes.articleDetailsName,
245-
pathParameters: {'id': headline.id},
246-
extra: headline,
215+
if (item is Headline) {
216+
final imageStyle = context
217+
.watch<AppBloc>()
218+
.state
219+
.settings
220+
.feedPreferences
221+
.headlineImageStyle;
222+
Widget tile;
223+
switch (imageStyle) {
224+
case HeadlineImageStyle.hidden:
225+
tile = HeadlineTileTextOnly(
226+
headline: item,
227+
onHeadlineTap: () => context.goNamed(
228+
Routes.articleDetailsName,
229+
pathParameters: {'id': item.id},
230+
extra: item,
231+
),
232+
);
233+
break;
234+
case HeadlineImageStyle.smallThumbnail:
235+
tile = HeadlineTileImageStart(
236+
headline: item,
237+
onHeadlineTap: () => context.goNamed(
238+
Routes.articleDetailsName,
239+
pathParameters: {'id': item.id},
240+
extra: item,
241+
),
242+
);
243+
break;
244+
case HeadlineImageStyle.largeThumbnail:
245+
tile = HeadlineTileImageTop(
246+
headline: item,
247+
onHeadlineTap: () => context.goNamed(
248+
Routes.articleDetailsName,
249+
pathParameters: {'id': item.id},
250+
extra: item,
251+
),
252+
);
253+
break;
254+
}
255+
return tile;
256+
} else if (item is Ad) {
257+
// Placeholder UI for Ad
258+
return Card(
259+
margin: const EdgeInsets.symmetric(
260+
horizontal: AppSpacing.paddingMedium,
261+
vertical: AppSpacing.xs,
262+
),
263+
color: colorScheme.surfaceContainerHighest,
264+
child: Padding(
265+
padding: const EdgeInsets.all(AppSpacing.md),
266+
child: Column(
267+
children: [
268+
if (item.imageUrl.isNotEmpty)
269+
Image.network(
270+
item.imageUrl,
271+
height: 100,
272+
errorBuilder: (ctx, err, st) =>
273+
const Icon(Icons.broken_image, size: 50),
274+
),
275+
const SizedBox(height: AppSpacing.sm),
276+
Text(
277+
'Placeholder Ad: ${item.adType?.name ?? 'Generic'}',
278+
style: textTheme.titleSmall,
247279
),
248-
);
249-
case HeadlineImageStyle.largeThumbnail:
250-
tile = HeadlineTileImageTop(
251-
headline: headline,
252-
onHeadlineTap:
253-
() => context.goNamed(
254-
Routes.articleDetailsName,
255-
pathParameters: {'id': headline.id},
256-
extra: headline,
280+
Text(
281+
'Placement: ${item.placement?.name ?? 'Default'}',
282+
style: textTheme.bodySmall,
257283
),
258-
);
284+
if (item.targetUrl.isNotEmpty)
285+
TextButton(
286+
onPressed: () {
287+
// TODO: Launch URL
288+
},
289+
child: const Text('Visit Advertiser'),
290+
),
291+
],
292+
),
293+
),
294+
);
295+
} else if (item is AccountAction) {
296+
// Placeholder UI for AccountAction
297+
return Card(
298+
margin: const EdgeInsets.symmetric(
299+
horizontal: AppSpacing.paddingMedium,
300+
vertical: AppSpacing.xs,
301+
),
302+
color: colorScheme.secondaryContainer,
303+
child: ListTile(
304+
leading: Icon(
305+
item.accountActionType == AccountActionType.linkAccount
306+
? Icons.link
307+
: Icons.upgrade,
308+
color: colorScheme.onSecondaryContainer,
309+
),
310+
title: Text(
311+
item.title,
312+
style: textTheme.titleMedium?.copyWith(
313+
color: colorScheme.onSecondaryContainer,
314+
fontWeight: FontWeight.bold,
315+
),
316+
),
317+
subtitle: item.description != null
318+
? Text(
319+
item.description!,
320+
style: textTheme.bodySmall?.copyWith(
321+
color: colorScheme.onSecondaryContainer.withOpacity(0.8),
322+
),
323+
)
324+
: null,
325+
trailing: item.callToActionText != null
326+
? ElevatedButton(
327+
style: ElevatedButton.styleFrom(
328+
backgroundColor: colorScheme.secondary,
329+
foregroundColor: colorScheme.onSecondary,
330+
),
331+
onPressed: () {
332+
if (item.callToActionUrl != null) {
333+
context.push(item.callToActionUrl!);
334+
}
335+
},
336+
child: Text(item.callToActionText!),
337+
)
338+
: null,
339+
isThreeLine: item.description != null && item.description!.length > 50,
340+
),
341+
);
259342
}
260-
return tile;
343+
return const SizedBox.shrink(); // Should not happen
261344
},
262345
),
263346
);

0 commit comments

Comments
 (0)