Skip to content

Commit 596999f

Browse files
committed
refactor(sources): Refactor source filtering logic
- Implemented filter by country/type - Added data loading state - Added clear filters functionality
1 parent 99e9bd1 commit 596999f

File tree

1 file changed

+165
-69
lines changed

1 file changed

+165
-69
lines changed
Lines changed: 165 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,205 @@
11
import 'dart:async';
22

33
import 'package:bloc/bloc.dart';
4-
import 'package:bloc_concurrency/bloc_concurrency.dart'; // For transformers
54
import 'package:equatable/equatable.dart';
6-
import 'package:ht_data_repository/ht_data_repository.dart'; // Generic Data Repository
5+
import 'package:ht_data_repository/ht_data_repository.dart';
76
import 'package:ht_shared/ht_shared.dart'
8-
show
9-
HtHttpException,
10-
Source; // Shared models, including Source and standardized exceptions
7+
show Country, HtHttpException, Source, SourceType;
118

129
part 'sources_filter_event.dart';
1310
part 'sources_filter_state.dart';
1411

15-
/// {@template sources_filter_bloc}
16-
/// Manages the state for fetching and displaying sources for filtering.
17-
///
18-
/// Handles initial fetching and pagination of sources using the
19-
/// provided [HtDataRepository].
20-
/// {@endtemplate}
21-
class SourcesFilterBloc extends Bloc<SourcesFilterEvent, SourcesFilterState> {
22-
/// {@macro sources_filter_bloc}
23-
///
24-
/// Requires a [HtDataRepository<Source>] to interact with the data layer.
25-
SourcesFilterBloc({required HtDataRepository<Source> sourcesRepository})
26-
: _sourcesRepository = sourcesRepository,
27-
super(const SourcesFilterState()) {
28-
on<SourcesFilterRequested>(
29-
_onSourcesFilterRequested,
30-
transformer: restartable(), // Only process the latest request
31-
);
32-
on<SourcesFilterLoadMoreRequested>(
33-
_onSourcesFilterLoadMoreRequested,
34-
transformer: droppable(), // Ignore new requests while one is processing
35-
);
12+
class SourcesFilterBloc
13+
extends Bloc<SourcesFilterEvent, SourcesFilterState> {
14+
SourcesFilterBloc({
15+
required HtDataRepository<Source> sourcesRepository,
16+
required HtDataRepository<Country> countriesRepository,
17+
}) : _sourcesRepository = sourcesRepository,
18+
_countriesRepository = countriesRepository,
19+
super(const SourcesFilterState()) {
20+
on<LoadSourceFilterData>(_onLoadSourceFilterData);
21+
on<CountryCapsuleToggled>(_onCountryCapsuleToggled);
22+
on<AllSourceTypesCapsuleToggled>(_onAllSourceTypesCapsuleToggled); // Added
23+
on<SourceTypeCapsuleToggled>(_onSourceTypeCapsuleToggled);
24+
on<SourceCheckboxToggled>(_onSourceCheckboxToggled);
25+
on<ClearSourceFiltersRequested>(_onClearSourceFiltersRequested);
26+
on<_FetchFilteredSourcesRequested>(_onFetchFilteredSourcesRequested);
3627
}
3728

3829
final HtDataRepository<Source> _sourcesRepository;
30+
final HtDataRepository<Country> _countriesRepository;
3931

40-
/// Number of sources to fetch per page.
41-
static const _sourcesLimit = 20;
42-
43-
/// Handles the initial request to fetch sources.
44-
Future<void> _onSourcesFilterRequested(
45-
SourcesFilterRequested event,
32+
Future<void> _onLoadSourceFilterData(
33+
LoadSourceFilterData event,
4634
Emitter<SourcesFilterState> emit,
4735
) async {
48-
// Prevent fetching if already loading or successful
49-
if (state.status == SourcesFilterStatus.loading ||
50-
state.status == SourcesFilterStatus.success) {
51-
return;
52-
}
36+
emit(
37+
state.copyWith(
38+
dataLoadingStatus: SourceFilterDataLoadingStatus.loading,
39+
),
40+
);
41+
try {
42+
final availableCountries = await _countriesRepository.readAll();
43+
final initialSelectedSourceIds =
44+
event.initialSelectedSources.map((s) => s.id).toSet();
45+
46+
// Initialize selected capsules based on initialSelectedSources
47+
final Set<String> initialSelectedCountryIsoCodes = {};
48+
final Set<SourceType> initialSelectedSourceTypes = {};
5349

54-
emit(state.copyWith(status: SourcesFilterStatus.loading));
50+
if (event.initialSelectedSources.isNotEmpty) {
51+
for (final source in event.initialSelectedSources) {
52+
if (source.headquarters?.isoCode != null) {
53+
initialSelectedCountryIsoCodes.add(source.headquarters!.isoCode);
54+
}
55+
if (source.sourceType != null) {
56+
initialSelectedSourceTypes.add(source.sourceType!);
57+
}
58+
}
59+
}
5560

56-
try {
57-
final response = await _sourcesRepository.readAll(limit: _sourcesLimit);
5861
emit(
5962
state.copyWith(
60-
status: SourcesFilterStatus.success,
61-
sources: response.items,
62-
hasMore: response.hasMore,
63-
cursor: response.cursor,
64-
clearError: true, // Clear any previous error
63+
availableCountries: availableCountries.items,
64+
finallySelectedSourceIds: initialSelectedSourceIds,
65+
selectedCountryIsoCodes: initialSelectedCountryIsoCodes,
66+
selectedSourceTypes: initialSelectedSourceTypes,
67+
// Keep loading status until sources are fetched
6568
),
6669
);
67-
} on HtHttpException catch (e) {
68-
emit(state.copyWith(status: SourcesFilterStatus.failure, error: e));
70+
// Trigger initial fetch of displayable sources
71+
add(const _FetchFilteredSourcesRequested());
6972
} catch (e) {
70-
// Catch unexpected errors
71-
emit(state.copyWith(status: SourcesFilterStatus.failure, error: e));
73+
emit(
74+
state.copyWith(
75+
dataLoadingStatus: SourceFilterDataLoadingStatus.failure,
76+
errorMessage: 'Failed to load filter criteria.',
77+
),
78+
);
7279
}
7380
}
7481

75-
/// Handles the request to load more sources for pagination.
76-
Future<void> _onSourcesFilterLoadMoreRequested(
77-
SourcesFilterLoadMoreRequested event,
82+
Future<void> _onCountryCapsuleToggled(
83+
CountryCapsuleToggled event,
7884
Emitter<SourcesFilterState> emit,
7985
) async {
80-
// Only proceed if currently successful and has more items
81-
if (state.status != SourcesFilterStatus.success || !state.hasMore) {
82-
return;
86+
final currentSelected = Set<String>.from(state.selectedCountryIsoCodes);
87+
if (event.countryIsoCode.isEmpty) { // "All Countries" toggled
88+
// If "All" is tapped and it's already effectively "All" (empty set), or if it's tapped to select "All"
89+
// we clear the set. If specific items are selected and "All" is tapped, it also clears.
90+
// Essentially, tapping "All" always results in an empty set, meaning no country filter.
91+
currentSelected.clear();
92+
} else {
93+
// Specific country toggled
94+
if (currentSelected.contains(event.countryIsoCode)) {
95+
currentSelected.remove(event.countryIsoCode);
96+
} else {
97+
currentSelected.add(event.countryIsoCode);
98+
}
8399
}
100+
emit(state.copyWith(selectedCountryIsoCodes: currentSelected));
101+
add(const _FetchFilteredSourcesRequested());
102+
}
84103

85-
emit(state.copyWith(status: SourcesFilterStatus.loadingMore));
104+
Future<void> _onAllSourceTypesCapsuleToggled(
105+
AllSourceTypesCapsuleToggled event,
106+
Emitter<SourcesFilterState> emit,
107+
) async {
108+
// Toggling "All" for source types means clearing any specific selections.
109+
// If already clear, it remains clear.
110+
emit(state.copyWith(selectedSourceTypes: {}));
111+
add(const _FetchFilteredSourcesRequested());
112+
}
86113

114+
Future<void> _onSourceTypeCapsuleToggled(
115+
SourceTypeCapsuleToggled event,
116+
Emitter<SourcesFilterState> emit,
117+
) async {
118+
final currentSelected = Set<SourceType>.from(state.selectedSourceTypes);
119+
if (currentSelected.contains(event.sourceType)) {
120+
currentSelected.remove(event.sourceType);
121+
} else {
122+
currentSelected.add(event.sourceType);
123+
}
124+
// If specific types are selected, "All" is no longer true.
125+
// The UI will derive "All" state from selectedSourceTypes.isEmpty
126+
emit(state.copyWith(selectedSourceTypes: currentSelected));
127+
add(const _FetchFilteredSourcesRequested());
128+
}
129+
130+
void _onSourceCheckboxToggled(
131+
SourceCheckboxToggled event,
132+
Emitter<SourcesFilterState> emit,
133+
) {
134+
final currentSelected = Set<String>.from(state.finallySelectedSourceIds);
135+
if (event.isSelected) {
136+
currentSelected.add(event.sourceId);
137+
} else {
138+
currentSelected.remove(event.sourceId);
139+
}
140+
emit(state.copyWith(finallySelectedSourceIds: currentSelected));
141+
}
142+
143+
Future<void> _onClearSourceFiltersRequested(
144+
ClearSourceFiltersRequested event,
145+
Emitter<SourcesFilterState> emit,
146+
) async {
147+
emit(
148+
state.copyWith(
149+
selectedCountryIsoCodes: {},
150+
selectedSourceTypes: {},
151+
finallySelectedSourceIds: {},
152+
// Keep availableCountries and availableSourceTypes
153+
),
154+
);
155+
add(const _FetchFilteredSourcesRequested());
156+
}
157+
158+
Future<void> _onFetchFilteredSourcesRequested(
159+
_FetchFilteredSourcesRequested event,
160+
Emitter<SourcesFilterState> emit,
161+
) async {
162+
emit(
163+
state.copyWith(
164+
dataLoadingStatus: SourceFilterDataLoadingStatus.loading,
165+
displayableSources: [], // Clear previous sources
166+
errorMessage: null,
167+
clearErrorMessage: true,
168+
),
169+
);
87170
try {
88-
final response = await _sourcesRepository.readAll(
89-
limit: _sourcesLimit,
90-
startAfterId: state.cursor, // Use the cursor from the current state
91-
);
171+
final queryParameters = <String, dynamic>{};
172+
if (state.selectedCountryIsoCodes.isNotEmpty) {
173+
queryParameters['countries'] =
174+
state.selectedCountryIsoCodes.join(',');
175+
}
176+
if (state.selectedSourceTypes.isNotEmpty) {
177+
queryParameters['sourceTypes'] =
178+
state.selectedSourceTypes.map((st) => st.name).join(',');
179+
}
180+
181+
final response =
182+
await _sourcesRepository.readAllByQuery(queryParameters);
92183
emit(
93184
state.copyWith(
94-
status: SourcesFilterStatus.success,
95-
// Append new sources to the existing list
96-
sources: List.of(state.sources)..addAll(response.items),
97-
hasMore: response.hasMore,
98-
cursor: response.cursor,
185+
displayableSources: response.items,
186+
dataLoadingStatus: SourceFilterDataLoadingStatus.success,
99187
),
100188
);
101189
} on HtHttpException catch (e) {
102-
// Keep existing data but indicate failure
103-
emit(state.copyWith(status: SourcesFilterStatus.failure, error: e));
190+
emit(
191+
state.copyWith(
192+
dataLoadingStatus: SourceFilterDataLoadingStatus.failure,
193+
errorMessage: e.message,
194+
),
195+
);
104196
} catch (e) {
105-
// Catch unexpected errors
106-
emit(state.copyWith(status: SourcesFilterStatus.failure, error: e));
197+
emit(
198+
state.copyWith(
199+
dataLoadingStatus: SourceFilterDataLoadingStatus.failure,
200+
errorMessage: 'An unexpected error occurred while fetching sources.',
201+
),
202+
);
107203
}
108204
}
109205
}

0 commit comments

Comments
 (0)