@@ -10,8 +10,7 @@ import 'package:ht_main/headlines-feed/bloc/headlines_feed_bloc.dart';
10
10
import 'package:ht_main/l10n/l10n.dart' ;
11
11
import 'package:ht_main/router/routes.dart' ;
12
12
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
15
14
16
15
/// {@template headlines_feed_view}
17
16
/// The core view widget for the headlines feed.
@@ -164,100 +163,184 @@ class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
164
163
return const SizedBox .shrink ();
165
164
166
165
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
170
167
return FailureStateWidget (
171
168
message:
172
169
'${l10n .headlinesFeedEmptyFilteredHeadline }\n ${l10n .headlinesFeedEmptyFilteredSubheadline }' ,
173
170
onRetry: () {
174
- // This will be our "Clear Filters" action
175
171
context.read <HeadlinesFeedBloc >().add (
176
172
HeadlinesFeedFiltersCleared (),
177
173
);
178
174
},
179
175
retryButtonText:
180
- l10n.headlinesFeedClearFiltersButton, // New l10n string
176
+ l10n.headlinesFeedClearFiltersButton,
181
177
);
182
178
}
183
- // Display the list of headlines with pull-to-refresh
184
179
return RefreshIndicator (
185
180
onRefresh: () async {
186
- // Dispatch refresh event to the BLoC
187
181
context.read <HeadlinesFeedBloc >().add (
188
182
HeadlinesFeedRefreshRequested (),
189
183
);
190
- // Note: BLoC handles emitting loading state during refresh
191
184
},
192
185
child: ListView .separated (
193
186
controller: _scrollController,
194
187
padding: const EdgeInsets .only (
195
188
top: AppSpacing .md,
196
- bottom:
197
- AppSpacing .xxl, // Ensure space below last item/loader
189
+ bottom: AppSpacing .xxl,
198
190
),
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
+ },
208
206
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
212
208
return const Padding (
213
209
padding: EdgeInsets .symmetric (vertical: AppSpacing .lg),
214
210
child: Center (child: CircularProgressIndicator ()),
215
211
);
216
212
}
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
226
214
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,
247
279
),
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,
257
283
),
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
+ );
259
342
}
260
- return tile;
343
+ return const SizedBox . shrink (); // Should not happen
261
344
},
262
345
),
263
346
);
0 commit comments