Skip to content

Commit 4f4fb6c

Browse files
authored
Merge pull request #19 from headlines-toolkit/headlines_filter_ui_logic_enhance
Headlines filter UI logic enhance
2 parents 71c95dd + e740bdf commit 4f4fb6c

16 files changed

+592
-411
lines changed

analysis_options.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ analyzer:
33
avoid_catches_without_on_clauses: ignore
44
avoid_print: ignore
55
document_ignores: ignore
6+
flutter_style_todos: ignore
67
lines_longer_than_80_chars: ignore
78
use_if_null_to_convert_nulls_to_bools: ignore
89
include: package:very_good_analysis/analysis_options.7.0.0.yaml

lib/account/view/content_preferences_page.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,8 @@ class ContentPreferencesPage extends StatelessWidget {
234234
ElevatedButton.icon(
235235
icon: const Icon(Icons.add_circle_outline),
236236
label: Text(l10n.headlinesFeedFilterEventCountryLabel), // "Country"
237-
onPressed: () {
238-
context.goNamed(Routes.feedFilterCountriesName);
239-
},
237+
onPressed:
238+
null, // TODO: Implement new navigation/management for followed countries
240239
),
241240
],
242241
);
@@ -279,9 +278,10 @@ class ContentPreferencesPage extends StatelessWidget {
279278
label: Text(
280279
'Manage ${l10n.headlinesFeedFilterEventCountryLabel}',
281280
), // "Manage Country"
282-
onPressed: () {
283-
context.goNamed(Routes.feedFilterCountriesName);
284-
},
281+
// onPressed: () {
282+
// context.goNamed(Routes.feedFilterCountriesName);
283+
// }, // TODO: Implement new navigation/management for followed countries
284+
onPressed: null, // Temporarily disable until new flow is defined
285285
style: ElevatedButton.styleFrom(
286286
minimumSize: const Size(double.infinity, 48),
287287
),

lib/headlines-feed/bloc/headlines_feed_bloc.dart

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'package:ht_main/headlines-feed/models/headline_filter.dart';
88
import 'package:ht_shared/ht_shared.dart'
99
show
1010
Category,
11-
Country,
11+
// Country, // Removed as it's no longer used for headline filtering
1212
Headline,
1313
HtHttpException,
1414
Source; // Shared models and standardized exceptions
@@ -73,12 +73,6 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
7373
.whereType<Source>()
7474
.map((s) => s.id)
7575
.toList(),
76-
if (event.filter.eventCountries?.isNotEmpty ?? false)
77-
'eventCountries':
78-
event.filter.eventCountries!
79-
.whereType<Country>()
80-
.map((c) => c.isoCode)
81-
.toList(),
8276
}, limit: _headlinesFetchLimit);
8377
emit(
8478
HeadlinesFeedLoaded(
@@ -189,12 +183,6 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
189183
.whereType<Source>()
190184
.map((s) => s.id)
191185
.toList(),
192-
if (currentFilter.eventCountries?.isNotEmpty ?? false)
193-
'eventCountries':
194-
currentFilter.eventCountries!
195-
.whereType<Country>()
196-
.map((c) => c.isoCode)
197-
.toList(),
198186
},
199187
limit: _headlinesFetchLimit,
200188
startAfterId: currentCursor, // Use determined cursor
@@ -248,12 +236,6 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
248236
.whereType<Source>()
249237
.map((s) => s.id)
250238
.toList(),
251-
if (currentFilter.eventCountries?.isNotEmpty ?? false)
252-
'eventCountries':
253-
currentFilter.eventCountries!
254-
.whereType<Country>()
255-
.map((c) => c.isoCode)
256-
.toList(),
257239
}, limit: _headlinesFetchLimit);
258240
emit(
259241
HeadlinesFeedLoaded(
Lines changed: 160 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,201 @@
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}
2112
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-
);
13+
SourcesFilterBloc({
14+
required HtDataRepository<Source> sourcesRepository,
15+
required HtDataRepository<Country> countriesRepository,
16+
}) : _sourcesRepository = sourcesRepository,
17+
_countriesRepository = countriesRepository,
18+
super(const SourcesFilterState()) {
19+
on<LoadSourceFilterData>(_onLoadSourceFilterData);
20+
on<CountryCapsuleToggled>(_onCountryCapsuleToggled);
21+
on<AllSourceTypesCapsuleToggled>(_onAllSourceTypesCapsuleToggled); // Added
22+
on<SourceTypeCapsuleToggled>(_onSourceTypeCapsuleToggled);
23+
on<SourceCheckboxToggled>(_onSourceCheckboxToggled);
24+
on<ClearSourceFiltersRequested>(_onClearSourceFiltersRequested);
25+
on<_FetchFilteredSourcesRequested>(_onFetchFilteredSourcesRequested);
3626
}
3727

3828
final HtDataRepository<Source> _sourcesRepository;
29+
final HtDataRepository<Country> _countriesRepository;
3930

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,
31+
Future<void> _onLoadSourceFilterData(
32+
LoadSourceFilterData event,
4633
Emitter<SourcesFilterState> emit,
4734
) async {
48-
// Prevent fetching if already loading or successful
49-
if (state.status == SourcesFilterStatus.loading ||
50-
state.status == SourcesFilterStatus.success) {
51-
return;
52-
}
35+
emit(
36+
state.copyWith(dataLoadingStatus: SourceFilterDataLoadingStatus.loading),
37+
);
38+
try {
39+
final availableCountries = await _countriesRepository.readAll();
40+
final initialSelectedSourceIds =
41+
event.initialSelectedSources.map((s) => s.id).toSet();
5342

54-
emit(state.copyWith(status: SourcesFilterStatus.loading));
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+
}
5557

56-
try {
57-
final response = await _sourcesRepository.readAll(limit: _sourcesLimit);
5858
emit(
5959
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
60+
availableCountries: availableCountries.items,
61+
finallySelectedSourceIds: initialSelectedSourceIds,
62+
selectedCountryIsoCodes: initialSelectedCountryIsoCodes,
63+
selectedSourceTypes: initialSelectedSourceTypes,
64+
// Keep loading status until sources are fetched
6565
),
6666
);
67-
} on HtHttpException catch (e) {
68-
emit(state.copyWith(status: SourcesFilterStatus.failure, error: e));
67+
// Trigger initial fetch of displayable sources
68+
add(const _FetchFilteredSourcesRequested());
6969
} catch (e) {
70-
// Catch unexpected errors
71-
emit(state.copyWith(status: SourcesFilterStatus.failure, error: e));
70+
emit(
71+
state.copyWith(
72+
dataLoadingStatus: SourceFilterDataLoadingStatus.failure,
73+
errorMessage: 'Failed to load filter criteria.',
74+
),
75+
);
7276
}
7377
}
7478

75-
/// Handles the request to load more sources for pagination.
76-
Future<void> _onSourcesFilterLoadMoreRequested(
77-
SourcesFilterLoadMoreRequested event,
79+
Future<void> _onCountryCapsuleToggled(
80+
CountryCapsuleToggled event,
81+
Emitter<SourcesFilterState> emit,
82+
) async {
83+
final currentSelected = Set<String>.from(state.selectedCountryIsoCodes);
84+
if (event.countryIsoCode.isEmpty) {
85+
// "All Countries" toggled
86+
// If "All" is tapped and it's already effectively "All" (empty set), or if it's tapped to select "All"
87+
// we clear the set. If specific items are selected and "All" is tapped, it also clears.
88+
// Essentially, tapping "All" always results in an empty set, meaning no country filter.
89+
currentSelected.clear();
90+
} else {
91+
// Specific country toggled
92+
if (currentSelected.contains(event.countryIsoCode)) {
93+
currentSelected.remove(event.countryIsoCode);
94+
} else {
95+
currentSelected.add(event.countryIsoCode);
96+
}
97+
}
98+
emit(state.copyWith(selectedCountryIsoCodes: currentSelected));
99+
add(const _FetchFilteredSourcesRequested());
100+
}
101+
102+
Future<void> _onAllSourceTypesCapsuleToggled(
103+
AllSourceTypesCapsuleToggled event,
104+
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());
110+
}
111+
112+
Future<void> _onSourceTypeCapsuleToggled(
113+
SourceTypeCapsuleToggled event,
78114
Emitter<SourcesFilterState> emit,
79115
) async {
80-
// Only proceed if currently successful and has more items
81-
if (state.status != SourcesFilterStatus.success || !state.hasMore) {
82-
return;
116+
final currentSelected = Set<SourceType>.from(state.selectedSourceTypes);
117+
if (currentSelected.contains(event.sourceType)) {
118+
currentSelected.remove(event.sourceType);
119+
} else {
120+
currentSelected.add(event.sourceType);
83121
}
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());
126+
}
84127

85-
emit(state.copyWith(status: SourcesFilterStatus.loadingMore));
128+
void _onSourceCheckboxToggled(
129+
SourceCheckboxToggled event,
130+
Emitter<SourcesFilterState> emit,
131+
) {
132+
final currentSelected = Set<String>.from(state.finallySelectedSourceIds);
133+
if (event.isSelected) {
134+
currentSelected.add(event.sourceId);
135+
} else {
136+
currentSelected.remove(event.sourceId);
137+
}
138+
emit(state.copyWith(finallySelectedSourceIds: currentSelected));
139+
}
86140

141+
Future<void> _onClearSourceFiltersRequested(
142+
ClearSourceFiltersRequested event,
143+
Emitter<SourcesFilterState> emit,
144+
) async {
145+
emit(
146+
state.copyWith(
147+
selectedCountryIsoCodes: {},
148+
selectedSourceTypes: {},
149+
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
164+
clearErrorMessage: true,
165+
),
166+
);
87167
try {
88-
final response = await _sourcesRepository.readAll(
89-
limit: _sourcesLimit,
90-
startAfterId: state.cursor, // Use the cursor from the current state
91-
);
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+
}
177+
178+
final response = await _sourcesRepository.readAllByQuery(queryParameters);
92179
emit(
93180
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,
181+
displayableSources: response.items,
182+
dataLoadingStatus: SourceFilterDataLoadingStatus.success,
99183
),
100184
);
101185
} on HtHttpException catch (e) {
102-
// Keep existing data but indicate failure
103-
emit(state.copyWith(status: SourcesFilterStatus.failure, error: e));
186+
emit(
187+
state.copyWith(
188+
dataLoadingStatus: SourceFilterDataLoadingStatus.failure,
189+
errorMessage: e.message,
190+
),
191+
);
104192
} catch (e) {
105-
// Catch unexpected errors
106-
emit(state.copyWith(status: SourcesFilterStatus.failure, error: e));
193+
emit(
194+
state.copyWith(
195+
dataLoadingStatus: SourceFilterDataLoadingStatus.failure,
196+
errorMessage: 'An unexpected error occurred while fetching sources.',
197+
),
198+
);
107199
}
108200
}
109201
}

0 commit comments

Comments
 (0)