Skip to content

Commit ff6fdf9

Browse files
authored
Merge pull request #22 from headlines-toolkit/search_page_model_filter_ui_and_logic
Search page model filter UI and logic
2 parents 82daee5 + 7164230 commit ff6fdf9

15 files changed

+578
-179
lines changed

lib/headlines-feed/bloc/headlines_feed_bloc.dart

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@ import 'package:equatable/equatable.dart';
66
import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository
77
import 'package:ht_main/headlines-feed/models/headline_filter.dart';
88
import 'package:ht_shared/ht_shared.dart'
9-
show
10-
Category,
11-
// Country, // Removed as it's no longer used for headline filtering
12-
Headline,
13-
HtHttpException,
14-
Source; // Shared models and standardized exceptions
9+
show Headline, HtHttpException; // Shared models and standardized exceptions
1510

1611
part 'headlines_feed_event.dart';
1712
part 'headlines_feed_state.dart';

lib/headlines-feed/bloc/sources_filter_bloc.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import 'dart:async';
33
import 'package:bloc/bloc.dart';
44
import 'package:equatable/equatable.dart';
55
import 'package:ht_data_repository/ht_data_repository.dart';
6-
import 'package:ht_shared/ht_shared.dart'
7-
show Country, HtHttpException, Source, SourceType;
6+
import 'package:ht_shared/ht_shared.dart' show Country, Source, SourceType;
87

98
part 'sources_filter_event.dart';
109
part 'sources_filter_state.dart';

lib/headlines-feed/bloc/sources_filter_event.dart

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,3 @@ class SourceCheckboxToggled extends SourcesFilterEvent {
6464
class ClearSourceFiltersRequested extends SourcesFilterEvent {
6565
const ClearSourceFiltersRequested();
6666
}
67-
68-
// Internal event - not part of public API, hence leading underscore
69-
class _FetchFilteredSourcesRequested extends SourcesFilterEvent {
70-
const _FetchFilteredSourcesRequested();
71-
}

lib/headlines-feed/widgets/headline_item_widget.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'package:flutter/material.dart';
22
import 'package:go_router/go_router.dart';
3-
import 'package:ht_main/router/routes.dart';
43
import 'package:ht_main/shared/constants/constants.dart'; // Import AppSpacing
54
import 'package:ht_shared/ht_shared.dart'; // Import models from ht_shared
65
import 'package:intl/intl.dart'; // For date formatting

lib/headlines-search/bloc/headlines_search_bloc.dart

Lines changed: 120 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,108 +2,188 @@ import 'package:bloc/bloc.dart';
22
import 'package:bloc_concurrency/bloc_concurrency.dart';
33
import 'package:equatable/equatable.dart';
44
import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository
5+
import 'package:ht_main/headlines-search/models/search_model_type.dart'; // Import SearchModelType
56
import 'package:ht_shared/ht_shared.dart'; // Shared models, including Headline
67

78
part 'headlines_search_event.dart';
89
part 'headlines_search_state.dart';
910

1011
class HeadlinesSearchBloc
1112
extends Bloc<HeadlinesSearchEvent, HeadlinesSearchState> {
12-
HeadlinesSearchBloc({required HtDataRepository<Headline> headlinesRepository})
13-
: _headlinesRepository = headlinesRepository,
14-
super(const HeadlinesSearchInitial()) {
15-
// Start with Initial state
13+
HeadlinesSearchBloc({
14+
required HtDataRepository<Headline> headlinesRepository,
15+
required HtDataRepository<Category> categoryRepository,
16+
required HtDataRepository<Source> sourceRepository,
17+
required HtDataRepository<Country> countryRepository,
18+
}) : _headlinesRepository = headlinesRepository,
19+
_categoryRepository = categoryRepository,
20+
_sourceRepository = sourceRepository,
21+
_countryRepository = countryRepository,
22+
super(const HeadlinesSearchInitial()) {
23+
on<HeadlinesSearchModelTypeChanged>(_onHeadlinesSearchModelTypeChanged);
1624
on<HeadlinesSearchFetchRequested>(
1725
_onSearchFetchRequested,
1826
transformer: restartable(), // Process only the latest search
1927
);
2028
}
2129

2230
final HtDataRepository<Headline> _headlinesRepository;
31+
final HtDataRepository<Category> _categoryRepository;
32+
final HtDataRepository<Source> _sourceRepository;
33+
final HtDataRepository<Country> _countryRepository;
2334
static const _limit = 10;
2435

36+
Future<void> _onHeadlinesSearchModelTypeChanged(
37+
HeadlinesSearchModelTypeChanged event,
38+
Emitter<HeadlinesSearchState> emit,
39+
) async {
40+
// If there's an active search term, re-trigger search with new model type
41+
// ignore: unused_local_variable
42+
final currentSearchTerm =
43+
state is HeadlinesSearchLoading
44+
? (state as HeadlinesSearchLoading).lastSearchTerm
45+
: state is HeadlinesSearchSuccess
46+
? (state as HeadlinesSearchSuccess).lastSearchTerm
47+
: state is HeadlinesSearchFailure
48+
? (state as HeadlinesSearchFailure).lastSearchTerm
49+
: null;
50+
51+
emit(HeadlinesSearchInitial(selectedModelType: event.newModelType));
52+
53+
// Removed automatic re-search:
54+
// if (currentSearchTerm != null && currentSearchTerm.isNotEmpty) {
55+
// add(HeadlinesSearchFetchRequested(searchTerm: currentSearchTerm));
56+
// }
57+
}
58+
2559
Future<void> _onSearchFetchRequested(
2660
HeadlinesSearchFetchRequested event,
2761
Emitter<HeadlinesSearchState> emit,
2862
) async {
29-
if (event.searchTerm.isEmpty) {
63+
final searchTerm = event.searchTerm;
64+
final modelType = state.selectedModelType;
65+
66+
if (searchTerm.isEmpty) {
3067
emit(
31-
const HeadlinesSearchSuccess(
32-
headlines: [],
68+
HeadlinesSearchSuccess(
69+
results: const [],
3370
hasMore: false,
3471
lastSearchTerm: '',
72+
selectedModelType: modelType,
3573
),
3674
);
3775
return;
3876
}
3977

40-
// Check if current state is success and if the search term is the same for pagination
78+
// Handle pagination
4179
if (state is HeadlinesSearchSuccess) {
4280
final successState = state as HeadlinesSearchSuccess;
43-
if (event.searchTerm == successState.lastSearchTerm) {
44-
// This is a pagination request for the current search term
45-
if (!successState.hasMore) return; // No more items to paginate
81+
if (searchTerm == successState.lastSearchTerm &&
82+
modelType == successState.selectedModelType) {
83+
if (!successState.hasMore) return;
4684

47-
// It's a bit unusual to emit Loading here for pagination,
48-
// typically UI handles this. Let's keep it simple for now.
49-
// emit(HeadlinesSearchLoading(lastSearchTerm: event.searchTerm));
5085
try {
51-
final response = await _headlinesRepository.readAllByQuery(
52-
{'q': event.searchTerm},
53-
limit: _limit,
54-
startAfterId: successState.cursor,
55-
);
86+
PaginatedResponse<dynamic> response;
87+
switch (modelType) {
88+
case SearchModelType.headline:
89+
response = await _headlinesRepository.readAllByQuery(
90+
{'q': searchTerm, 'model': modelType.toJson()},
91+
limit: _limit,
92+
startAfterId: successState.cursor,
93+
);
94+
case SearchModelType.category:
95+
response = await _categoryRepository.readAllByQuery(
96+
{'q': searchTerm, 'model': modelType.toJson()},
97+
limit: _limit,
98+
startAfterId: successState.cursor,
99+
);
100+
case SearchModelType.source:
101+
response = await _sourceRepository.readAllByQuery(
102+
{'q': searchTerm, 'model': modelType.toJson()},
103+
limit: _limit,
104+
startAfterId: successState.cursor,
105+
);
106+
case SearchModelType.country:
107+
response = await _countryRepository.readAllByQuery(
108+
{'q': searchTerm, 'model': modelType.toJson()},
109+
limit: _limit,
110+
startAfterId: successState.cursor,
111+
);
112+
}
56113
emit(
57-
response.items.isEmpty
58-
? successState.copyWith(hasMore: false)
59-
: successState.copyWith(
60-
headlines: List.of(successState.headlines)
61-
..addAll(response.items),
62-
hasMore: response.hasMore,
63-
cursor: response.cursor,
64-
),
114+
successState.copyWith(
115+
results: List.of(successState.results)..addAll(response.items),
116+
hasMore: response.hasMore,
117+
cursor: response.cursor,
118+
),
65119
);
66120
} on HtHttpException catch (e) {
67121
emit(successState.copyWith(errorMessage: e.message));
68122
} catch (e, st) {
69-
print('Search pagination error: $e\n$st');
123+
print('Search pagination error ($modelType): $e\n$st');
70124
emit(
71125
successState.copyWith(errorMessage: 'Failed to load more results.'),
72126
);
73127
}
74-
return; // Pagination handled
128+
return;
75129
}
76130
}
77131

78-
// If not paginating for the same term, it's a new search or different term
132+
// New search
79133
emit(
80-
HeadlinesSearchLoading(lastSearchTerm: event.searchTerm),
81-
); // Show loading for new search
134+
HeadlinesSearchLoading(
135+
lastSearchTerm: searchTerm,
136+
selectedModelType: modelType,
137+
),
138+
);
82139
try {
83-
final response = await _headlinesRepository.readAllByQuery({
84-
'q': event.searchTerm,
85-
}, limit: _limit);
140+
PaginatedResponse<dynamic> response;
141+
switch (modelType) {
142+
case SearchModelType.headline:
143+
response = await _headlinesRepository.readAllByQuery({
144+
'q': searchTerm,
145+
'model': modelType.toJson(),
146+
}, limit: _limit,);
147+
case SearchModelType.category:
148+
response = await _categoryRepository.readAllByQuery({
149+
'q': searchTerm,
150+
'model': modelType.toJson(),
151+
}, limit: _limit,);
152+
case SearchModelType.source:
153+
response = await _sourceRepository.readAllByQuery({
154+
'q': searchTerm,
155+
'model': modelType.toJson(),
156+
}, limit: _limit,);
157+
case SearchModelType.country:
158+
response = await _countryRepository.readAllByQuery({
159+
'q': searchTerm,
160+
'model': modelType.toJson(),
161+
}, limit: _limit,);
162+
}
86163
emit(
87164
HeadlinesSearchSuccess(
88-
headlines: response.items,
165+
results: response.items,
89166
hasMore: response.hasMore,
90167
cursor: response.cursor,
91-
lastSearchTerm: event.searchTerm,
168+
lastSearchTerm: searchTerm,
169+
selectedModelType: modelType,
92170
),
93171
);
94172
} on HtHttpException catch (e) {
95173
emit(
96174
HeadlinesSearchFailure(
97175
errorMessage: e.message,
98-
lastSearchTerm: event.searchTerm,
176+
lastSearchTerm: searchTerm,
177+
selectedModelType: modelType,
99178
),
100179
);
101180
} catch (e, st) {
102-
print('Search error: $e\n$st');
181+
print('Search error ($modelType): $e\n$st');
103182
emit(
104183
HeadlinesSearchFailure(
105184
errorMessage: 'An unexpected error occurred during search.',
106-
lastSearchTerm: event.searchTerm,
185+
lastSearchTerm: searchTerm,
186+
selectedModelType: modelType,
107187
),
108188
);
109189
}

lib/headlines-search/bloc/headlines_search_event.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ sealed class HeadlinesSearchEvent extends Equatable {
77
List<Object> get props => [];
88
}
99

10+
final class HeadlinesSearchModelTypeChanged extends HeadlinesSearchEvent {
11+
const HeadlinesSearchModelTypeChanged(this.newModelType);
12+
13+
final SearchModelType newModelType;
14+
15+
@override
16+
List<Object> get props => [newModelType];
17+
}
18+
1019
final class HeadlinesSearchFetchRequested extends HeadlinesSearchEvent {
1120
const HeadlinesSearchFetchRequested({required this.searchTerm});
1221

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,74 @@
11
part of 'headlines_search_bloc.dart';
22

33
abstract class HeadlinesSearchState extends Equatable {
4-
const HeadlinesSearchState();
5-
// lastSearchTerm will be defined in specific states that need it.
4+
const HeadlinesSearchState({
5+
this.selectedModelType = SearchModelType.headline,
6+
});
7+
8+
final SearchModelType selectedModelType;
9+
610
@override
7-
List<Object?> get props => [];
11+
List<Object?> get props => [selectedModelType];
812
}
913

1014
/// Initial state before any search is performed.
1115
class HeadlinesSearchInitial extends HeadlinesSearchState {
12-
const HeadlinesSearchInitial();
13-
// No lastSearchTerm needed for initial state.
16+
const HeadlinesSearchInitial({super.selectedModelType});
1417
}
1518

1619
/// State when a search is actively in progress.
1720
class HeadlinesSearchLoading extends HeadlinesSearchState {
18-
const HeadlinesSearchLoading({this.lastSearchTerm});
19-
final String? lastSearchTerm; // Term being loaded
21+
const HeadlinesSearchLoading({
22+
required this.lastSearchTerm,
23+
required super.selectedModelType,
24+
});
25+
final String lastSearchTerm; // Term being loaded
2026

2127
@override
22-
List<Object?> get props => [lastSearchTerm];
28+
List<Object?> get props => [...super.props, lastSearchTerm];
2329
}
2430

2531
/// State when a search has successfully returned results.
2632
class HeadlinesSearchSuccess extends HeadlinesSearchState {
2733
const HeadlinesSearchSuccess({
28-
required this.headlines,
34+
required this.results,
2935
required this.hasMore,
3036
required this.lastSearchTerm,
37+
required super.selectedModelType, // The model type for these results
3138
this.cursor,
3239
this.errorMessage, // For non-critical errors like pagination failure
3340
});
3441

35-
final List<Headline> headlines;
42+
final List<dynamic> results; // Can hold Headline, Category, Source, Country
3643
final bool hasMore;
3744
final String? cursor;
3845
final String? errorMessage; // e.g., for pagination errors
3946
final String lastSearchTerm; // The term that yielded these results
4047

4148
HeadlinesSearchSuccess copyWith({
42-
List<Headline>? headlines,
49+
List<dynamic>? results,
4350
bool? hasMore,
4451
String? cursor,
45-
String? errorMessage, // Allow clearing/setting error
52+
String? errorMessage,
4653
String? lastSearchTerm,
54+
SearchModelType? selectedModelType,
4755
bool clearErrorMessage = false,
4856
}) {
4957
return HeadlinesSearchSuccess(
50-
headlines: headlines ?? this.headlines,
58+
results: results ?? this.results,
5159
hasMore: hasMore ?? this.hasMore,
5260
cursor: cursor ?? this.cursor,
5361
errorMessage:
5462
clearErrorMessage ? null : errorMessage ?? this.errorMessage,
5563
lastSearchTerm: lastSearchTerm ?? this.lastSearchTerm,
64+
selectedModelType: selectedModelType ?? this.selectedModelType,
5665
);
5766
}
5867

5968
@override
6069
List<Object?> get props => [
61-
headlines,
70+
...super.props,
71+
results,
6272
hasMore,
6373
cursor,
6474
errorMessage,
@@ -71,11 +81,12 @@ class HeadlinesSearchFailure extends HeadlinesSearchState {
7181
const HeadlinesSearchFailure({
7282
required this.errorMessage,
7383
required this.lastSearchTerm,
84+
required super.selectedModelType,
7485
});
7586

7687
final String errorMessage;
7788
final String lastSearchTerm; // The term that failed
7889

7990
@override
80-
List<Object?> get props => [errorMessage, lastSearchTerm];
91+
List<Object?> get props => [...super.props, errorMessage, lastSearchTerm];
8192
}

0 commit comments

Comments
 (0)