Skip to content

Commit f6942ff

Browse files
authored
Merge pull request #20 from headlines-toolkit/eadlines_feed_source_filter_ui_fix
Eadlines feed source filter UI fix
2 parents 4f4fb6c + a0bf11c commit f6942ff

File tree

8 files changed

+302
-180
lines changed

8 files changed

+302
-180
lines changed

lib/headlines-feed/bloc/headlines_feed_bloc.dart

Lines changed: 48 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,22 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
6060
) async {
6161
emit(HeadlinesFeedLoading()); // Show loading for filter application
6262
try {
63-
final response = await _headlinesRepository.readAllByQuery({
64-
if (event.filter.categories?.isNotEmpty ?? false)
65-
'categories':
66-
event.filter.categories!
67-
.whereType<Category>()
68-
.map((c) => c.id)
69-
.toList(),
70-
if (event.filter.sources?.isNotEmpty ?? false)
71-
'sources':
72-
event.filter.sources!
73-
.whereType<Source>()
74-
.map((s) => s.id)
75-
.toList(),
76-
}, limit: _headlinesFetchLimit);
63+
final queryParams = <String, dynamic>{};
64+
if (event.filter.categories?.isNotEmpty ?? false) {
65+
queryParams['categories'] = event.filter.categories!
66+
.map((c) => c.id)
67+
.join(',');
68+
}
69+
if (event.filter.sources?.isNotEmpty ?? false) {
70+
queryParams['sources'] = event.filter.sources!
71+
.map((s) => s.id)
72+
.join(',');
73+
}
74+
75+
final response = await _headlinesRepository.readAllByQuery(
76+
queryParams,
77+
limit: _headlinesFetchLimit,
78+
);
7779
emit(
7880
HeadlinesFeedLoaded(
7981
headlines: response.items,
@@ -133,8 +135,8 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
133135
Emitter<HeadlinesFeedState> emit,
134136
) async {
135137
// Determine current filter and cursor based on state
136-
var currentFilter = const HeadlineFilter();
137-
var currentCursor =
138+
var currentFilter = const HeadlineFilter(); // Made type explicit
139+
var currentCursor = // Made type explicit
138140
event.cursor; // Use event's cursor if provided (for pagination)
139141
var currentHeadlines = <Headline>[];
140142
var isPaginating = false;
@@ -169,21 +171,20 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
169171
}
170172

171173
try {
174+
final queryParams = <String, dynamic>{};
175+
if (currentFilter.categories?.isNotEmpty ?? false) {
176+
queryParams['categories'] = currentFilter.categories!
177+
.map((c) => c.id)
178+
.join(',');
179+
}
180+
if (currentFilter.sources?.isNotEmpty ?? false) {
181+
queryParams['sources'] = currentFilter.sources!
182+
.map((s) => s.id)
183+
.join(',');
184+
}
185+
172186
final response = await _headlinesRepository.readAllByQuery(
173-
{
174-
if (currentFilter.categories?.isNotEmpty ?? false)
175-
'categories':
176-
currentFilter.categories!
177-
.whereType<Category>()
178-
.map((c) => c.id)
179-
.toList(),
180-
if (currentFilter.sources?.isNotEmpty ?? false)
181-
'sources':
182-
currentFilter.sources!
183-
.whereType<Source>()
184-
.map((s) => s.id)
185-
.toList(),
186-
},
187+
queryParams,
187188
limit: _headlinesFetchLimit,
188189
startAfterId: currentCursor, // Use determined cursor
189190
);
@@ -216,27 +217,29 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
216217
emit(HeadlinesFeedLoading()); // Show loading indicator for refresh
217218

218219
// Determine the filter currently applied in the state
219-
var currentFilter = const HeadlineFilter();
220+
var currentFilter = const HeadlineFilter(); // Made type explicit
220221
if (state is HeadlinesFeedLoaded) {
221222
currentFilter = (state as HeadlinesFeedLoaded).filter;
222223
}
223224

224225
try {
226+
final queryParams = <String, dynamic>{};
227+
if (currentFilter.categories?.isNotEmpty ?? false) {
228+
queryParams['categories'] = currentFilter.categories!
229+
.map((c) => c.id)
230+
.join(',');
231+
}
232+
if (currentFilter.sources?.isNotEmpty ?? false) {
233+
queryParams['sources'] = currentFilter.sources!
234+
.map((s) => s.id)
235+
.join(',');
236+
}
237+
225238
// Fetch the first page using the current filter
226-
final response = await _headlinesRepository.readAllByQuery({
227-
if (currentFilter.categories?.isNotEmpty ?? false)
228-
'categories':
229-
currentFilter.categories!
230-
.whereType<Category>()
231-
.map((c) => c.id)
232-
.toList(),
233-
if (currentFilter.sources?.isNotEmpty ?? false)
234-
'sources':
235-
currentFilter.sources!
236-
.whereType<Source>()
237-
.map((s) => s.id)
238-
.toList(),
239-
}, limit: _headlinesFetchLimit);
239+
final response = await _headlinesRepository.readAllByQuery(
240+
queryParams,
241+
limit: _headlinesFetchLimit,
242+
);
240243
emit(
241244
HeadlinesFeedLoaded(
242245
headlines: response.items, // Replace headlines on refresh

lib/headlines-feed/bloc/sources_filter_bloc.dart

Lines changed: 86 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ class SourcesFilterBloc extends Bloc<SourcesFilterEvent, SourcesFilterState> {
1818
super(const SourcesFilterState()) {
1919
on<LoadSourceFilterData>(_onLoadSourceFilterData);
2020
on<CountryCapsuleToggled>(_onCountryCapsuleToggled);
21-
on<AllSourceTypesCapsuleToggled>(_onAllSourceTypesCapsuleToggled); // Added
21+
on<AllSourceTypesCapsuleToggled>(_onAllSourceTypesCapsuleToggled);
2222
on<SourceTypeCapsuleToggled>(_onSourceTypeCapsuleToggled);
2323
on<SourceCheckboxToggled>(_onSourceCheckboxToggled);
2424
on<ClearSourceFiltersRequested>(_onClearSourceFiltersRequested);
25-
on<_FetchFilteredSourcesRequested>(_onFetchFilteredSourcesRequested);
25+
// Removed _FetchFilteredSourcesRequested event listener
2626
}
2727

2828
final HtDataRepository<Source> _sourcesRepository;
@@ -40,32 +40,37 @@ class SourcesFilterBloc extends Bloc<SourcesFilterEvent, SourcesFilterState> {
4040
final initialSelectedSourceIds =
4141
event.initialSelectedSources.map((s) => s.id).toSet();
4242

43-
// Initialize selected capsules based on initialSelectedSources
44-
final initialSelectedCountryIsoCodes = <String>{};
45-
final initialSelectedSourceTypes = <SourceType>{};
46-
47-
if (event.initialSelectedSources.isNotEmpty) {
48-
for (final source in event.initialSelectedSources) {
49-
if (source.headquarters?.isoCode != null) {
50-
initialSelectedCountryIsoCodes.add(source.headquarters!.isoCode);
51-
}
52-
if (source.sourceType != null) {
53-
initialSelectedSourceTypes.add(source.sourceType!);
54-
}
55-
}
56-
}
43+
// Use the passed-in initial capsule selections directly
44+
final initialSelectedCountryIsoCodes =
45+
event.initialSelectedCountryIsoCodes;
46+
final initialSelectedSourceTypes = event.initialSelectedSourceTypes;
47+
48+
final allSourcesResponse = await _sourcesRepository.readAll();
49+
final allAvailableSources = allSourcesResponse.items;
50+
51+
// Initially, display all sources. Capsules are visually set but don't filter the list yet.
52+
// Filtering will occur if a capsule is manually toggled.
53+
// However, if initial capsule filters ARE provided, we should respect them for the initial display.
54+
final displayableSources = _getFilteredSources(
55+
allSources: allAvailableSources,
56+
selectedCountries: initialSelectedCountryIsoCodes, // Use event's data
57+
selectedTypes: initialSelectedSourceTypes, // Use event's data
58+
);
5759

5860
emit(
5961
state.copyWith(
6062
availableCountries: availableCountries.items,
63+
allAvailableSources: allAvailableSources,
64+
displayableSources:
65+
displayableSources, // Now correctly filtered if initial capsules were set
6166
finallySelectedSourceIds: initialSelectedSourceIds,
62-
selectedCountryIsoCodes: initialSelectedCountryIsoCodes,
63-
selectedSourceTypes: initialSelectedSourceTypes,
64-
// Keep loading status until sources are fetched
67+
selectedCountryIsoCodes:
68+
initialSelectedCountryIsoCodes, // Use event's data
69+
selectedSourceTypes: initialSelectedSourceTypes, // Use event's data
70+
dataLoadingStatus: SourceFilterDataLoadingStatus.success,
71+
clearErrorMessage: true,
6572
),
6673
);
67-
// Trigger initial fetch of displayable sources
68-
add(const _FetchFilteredSourcesRequested());
6974
} catch (e) {
7075
emit(
7176
state.copyWith(
@@ -95,34 +100,57 @@ class SourcesFilterBloc extends Bloc<SourcesFilterEvent, SourcesFilterState> {
95100
currentSelected.add(event.countryIsoCode);
96101
}
97102
}
98-
emit(state.copyWith(selectedCountryIsoCodes: currentSelected));
99-
add(const _FetchFilteredSourcesRequested());
103+
final newDisplayableSources = _getFilteredSources(
104+
allSources: state.allAvailableSources,
105+
selectedCountries: currentSelected,
106+
selectedTypes: state.selectedSourceTypes,
107+
);
108+
emit(
109+
state.copyWith(
110+
selectedCountryIsoCodes: currentSelected,
111+
displayableSources: newDisplayableSources,
112+
),
113+
);
100114
}
101115

102-
Future<void> _onAllSourceTypesCapsuleToggled(
116+
void _onAllSourceTypesCapsuleToggled(
103117
AllSourceTypesCapsuleToggled event,
104118
Emitter<SourcesFilterState> emit,
105-
) async {
106-
// Toggling "All" for source types means clearing any specific selections.
107-
// If already clear, it remains clear.
108-
emit(state.copyWith(selectedSourceTypes: {}));
109-
add(const _FetchFilteredSourcesRequested());
119+
) {
120+
final newDisplayableSources = _getFilteredSources(
121+
allSources: state.allAvailableSources,
122+
selectedCountries: state.selectedCountryIsoCodes,
123+
selectedTypes: {}, // Cleared source types
124+
);
125+
emit(
126+
state.copyWith(
127+
selectedSourceTypes: {},
128+
displayableSources: newDisplayableSources,
129+
),
130+
);
110131
}
111132

112-
Future<void> _onSourceTypeCapsuleToggled(
133+
void _onSourceTypeCapsuleToggled(
113134
SourceTypeCapsuleToggled event,
114135
Emitter<SourcesFilterState> emit,
115-
) async {
136+
) {
116137
final currentSelected = Set<SourceType>.from(state.selectedSourceTypes);
117138
if (currentSelected.contains(event.sourceType)) {
118139
currentSelected.remove(event.sourceType);
119140
} else {
120141
currentSelected.add(event.sourceType);
121142
}
122-
// If specific types are selected, "All" is no longer true.
123-
// The UI will derive "All" state from selectedSourceTypes.isEmpty
124-
emit(state.copyWith(selectedSourceTypes: currentSelected));
125-
add(const _FetchFilteredSourcesRequested());
143+
final newDisplayableSources = _getFilteredSources(
144+
allSources: state.allAvailableSources,
145+
selectedCountries: state.selectedCountryIsoCodes,
146+
selectedTypes: currentSelected,
147+
);
148+
emit(
149+
state.copyWith(
150+
selectedSourceTypes: currentSelected,
151+
displayableSources: newDisplayableSources,
152+
),
153+
);
126154
}
127155

128156
void _onSourceCheckboxToggled(
@@ -147,55 +175,33 @@ class SourcesFilterBloc extends Bloc<SourcesFilterEvent, SourcesFilterState> {
147175
selectedCountryIsoCodes: {},
148176
selectedSourceTypes: {},
149177
finallySelectedSourceIds: {},
150-
// Keep availableCountries and availableSourceTypes
151-
),
152-
);
153-
add(const _FetchFilteredSourcesRequested());
154-
}
155-
156-
Future<void> _onFetchFilteredSourcesRequested(
157-
_FetchFilteredSourcesRequested event,
158-
Emitter<SourcesFilterState> emit,
159-
) async {
160-
emit(
161-
state.copyWith(
162-
dataLoadingStatus: SourceFilterDataLoadingStatus.loading,
163-
displayableSources: [], // Clear previous sources
178+
displayableSources: List.from(state.allAvailableSources), // Reset
179+
dataLoadingStatus: SourceFilterDataLoadingStatus.success,
164180
clearErrorMessage: true,
165181
),
166182
);
167-
try {
168-
final queryParameters = <String, dynamic>{};
169-
if (state.selectedCountryIsoCodes.isNotEmpty) {
170-
queryParameters['countries'] = state.selectedCountryIsoCodes.join(',');
171-
}
172-
if (state.selectedSourceTypes.isNotEmpty) {
173-
queryParameters['sourceTypes'] = state.selectedSourceTypes
174-
.map((st) => st.name)
175-
.join(',');
176-
}
183+
}
177184

178-
final response = await _sourcesRepository.readAllByQuery(queryParameters);
179-
emit(
180-
state.copyWith(
181-
displayableSources: response.items,
182-
dataLoadingStatus: SourceFilterDataLoadingStatus.success,
183-
),
184-
);
185-
} on HtHttpException catch (e) {
186-
emit(
187-
state.copyWith(
188-
dataLoadingStatus: SourceFilterDataLoadingStatus.failure,
189-
errorMessage: e.message,
190-
),
191-
);
192-
} catch (e) {
193-
emit(
194-
state.copyWith(
195-
dataLoadingStatus: SourceFilterDataLoadingStatus.failure,
196-
errorMessage: 'An unexpected error occurred while fetching sources.',
197-
),
198-
);
185+
// Helper method to filter sources based on selected countries and types
186+
List<Source> _getFilteredSources({
187+
required List<Source> allSources,
188+
required Set<String> selectedCountries,
189+
required Set<SourceType> selectedTypes,
190+
}) {
191+
if (selectedCountries.isEmpty && selectedTypes.isEmpty) {
192+
return List.from(allSources); // Return all if no filters
199193
}
194+
195+
return allSources.where((source) {
196+
final matchesCountry =
197+
selectedCountries.isEmpty ||
198+
(source.headquarters != null &&
199+
selectedCountries.contains(source.headquarters!.isoCode));
200+
final matchesType =
201+
selectedTypes.isEmpty ||
202+
(source.sourceType != null &&
203+
selectedTypes.contains(source.sourceType));
204+
return matchesCountry && matchesType;
205+
}).toList();
200206
}
201207
}

lib/headlines-feed/bloc/sources_filter_event.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,22 @@ abstract class SourcesFilterEvent extends Equatable {
1010
}
1111

1212
class LoadSourceFilterData extends SourcesFilterEvent {
13-
const LoadSourceFilterData({this.initialSelectedSources = const []});
13+
const LoadSourceFilterData({
14+
this.initialSelectedSources = const [],
15+
this.initialSelectedCountryIsoCodes = const {},
16+
this.initialSelectedSourceTypes = const {},
17+
});
1418

1519
final List<Source> initialSelectedSources;
20+
final Set<String> initialSelectedCountryIsoCodes;
21+
final Set<SourceType> initialSelectedSourceTypes;
1622

1723
@override
18-
List<Object?> get props => [initialSelectedSources];
24+
List<Object?> get props => [
25+
initialSelectedSources,
26+
initialSelectedCountryIsoCodes,
27+
initialSelectedSourceTypes,
28+
];
1929
}
2030

2131
class CountryCapsuleToggled extends SourcesFilterEvent {

0 commit comments

Comments
 (0)