Skip to content

Commit eb17e88

Browse files
committed
feat: add sources filter bloc
- Implemented bloc for source filtering - Added events and states - Handles fetching and pagination
1 parent c1f7697 commit eb17e88

File tree

3 files changed

+227
-0
lines changed

3 files changed

+227
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import 'dart:async';
2+
3+
import 'package:bloc/bloc.dart';
4+
import 'package:bloc_concurrency/bloc_concurrency.dart'; // For transformers
5+
import 'package:equatable/equatable.dart';
6+
import 'package:ht_shared/ht_shared.dart'; // For PaginatedResponse
7+
import 'package:ht_sources_client/ht_sources_client.dart'; // Keep existing, also for Source model
8+
import 'package:ht_sources_repository/ht_sources_repository.dart';
9+
10+
part 'sources_filter_event.dart';
11+
part 'sources_filter_state.dart';
12+
13+
/// {@template sources_filter_bloc}
14+
/// Manages the state for fetching and displaying sources for filtering.
15+
///
16+
/// Handles initial fetching and pagination of sources using the
17+
/// provided [HtSourcesRepository].
18+
/// {@endtemplate}
19+
class SourcesFilterBloc extends Bloc<SourcesFilterEvent, SourcesFilterState> {
20+
/// {@macro sources_filter_bloc}
21+
///
22+
/// Requires a [HtSourcesRepository] to interact with the data layer.
23+
SourcesFilterBloc({required HtSourcesRepository sourcesRepository})
24+
: _sourcesRepository = sourcesRepository,
25+
super(const SourcesFilterState()) {
26+
on<SourcesFilterRequested>(
27+
_onSourcesFilterRequested,
28+
transformer: restartable(), // Only process the latest request
29+
);
30+
on<SourcesFilterLoadMoreRequested>(
31+
_onSourcesFilterLoadMoreRequested,
32+
transformer: droppable(), // Ignore new requests while one is processing
33+
);
34+
}
35+
36+
final HtSourcesRepository _sourcesRepository;
37+
38+
/// Number of sources to fetch per page.
39+
static const _sourcesLimit = 20;
40+
41+
/// Handles the initial request to fetch sources.
42+
Future<void> _onSourcesFilterRequested(
43+
SourcesFilterRequested event,
44+
Emitter<SourcesFilterState> emit,
45+
) async {
46+
// Prevent fetching if already loading or successful
47+
if (state.status == SourcesFilterStatus.loading ||
48+
state.status == SourcesFilterStatus.success) {
49+
return;
50+
}
51+
52+
emit(state.copyWith(status: SourcesFilterStatus.loading));
53+
54+
try {
55+
final response = await _sourcesRepository.getSources(
56+
limit: _sourcesLimit,
57+
);
58+
emit(
59+
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
65+
),
66+
);
67+
} on SourceFetchFailure catch (e) {
68+
emit(
69+
state.copyWith(
70+
status: SourcesFilterStatus.failure,
71+
error: e,
72+
),
73+
);
74+
} catch (e) {
75+
// Catch unexpected errors
76+
emit(
77+
state.copyWith(
78+
status: SourcesFilterStatus.failure,
79+
error: e,
80+
),
81+
);
82+
}
83+
}
84+
85+
/// Handles the request to load more sources for pagination.
86+
Future<void> _onSourcesFilterLoadMoreRequested(
87+
SourcesFilterLoadMoreRequested event,
88+
Emitter<SourcesFilterState> emit,
89+
) async {
90+
// Only proceed if currently successful and has more items
91+
if (state.status != SourcesFilterStatus.success || !state.hasMore) {
92+
return;
93+
}
94+
95+
emit(state.copyWith(status: SourcesFilterStatus.loadingMore));
96+
97+
try {
98+
final response = await _sourcesRepository.getSources(
99+
limit: _sourcesLimit,
100+
startAfterId: state.cursor, // Use the cursor from the current state
101+
);
102+
emit(
103+
state.copyWith(
104+
status: SourcesFilterStatus.success,
105+
// Append new sources to the existing list
106+
sources: List.of(state.sources)..addAll(response.items),
107+
hasMore: response.hasMore,
108+
cursor: response.cursor,
109+
),
110+
);
111+
} on SourceFetchFailure catch (e) {
112+
// Keep existing data but indicate failure
113+
emit(
114+
state.copyWith(
115+
status: SourcesFilterStatus.failure,
116+
error: e,
117+
),
118+
);
119+
} catch (e) {
120+
// Catch unexpected errors
121+
emit(
122+
state.copyWith(
123+
status: SourcesFilterStatus.failure,
124+
error: e,
125+
),
126+
);
127+
}
128+
}
129+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
part of 'sources_filter_bloc.dart';
2+
3+
/// {@template sources_filter_event}
4+
/// Base class for events related to fetching and managing source filters.
5+
/// {@endtemplate}
6+
sealed class SourcesFilterEvent extends Equatable {
7+
/// {@macro sources_filter_event}
8+
const SourcesFilterEvent();
9+
10+
@override
11+
List<Object> get props => [];
12+
}
13+
14+
/// {@template sources_filter_requested}
15+
/// Event triggered to request the initial list of sources.
16+
/// {@endtemplate}
17+
final class SourcesFilterRequested extends SourcesFilterEvent {}
18+
19+
/// {@template sources_filter_load_more_requested}
20+
/// Event triggered to request the next page of sources for pagination.
21+
/// {@endtemplate}
22+
final class SourcesFilterLoadMoreRequested extends SourcesFilterEvent {}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
part of 'sources_filter_bloc.dart';
2+
3+
/// Enum representing the different statuses of the source filter data fetching.
4+
enum SourcesFilterStatus {
5+
/// Initial state, no data loaded yet.
6+
initial,
7+
8+
/// Currently fetching the first page of sources.
9+
loading,
10+
11+
/// Successfully loaded sources. May be loading more in the background.
12+
success,
13+
14+
/// An error occurred while fetching sources.
15+
failure,
16+
17+
/// Loading more sources for pagination (infinity scroll).
18+
loadingMore,
19+
}
20+
21+
/// {@template sources_filter_state}
22+
/// Represents the state for the source filter feature.
23+
///
24+
/// Contains the list of fetched sources, pagination information,
25+
/// loading/error status.
26+
/// {@endtemplate}
27+
final class SourcesFilterState extends Equatable {
28+
/// {@macro sources_filter_state}
29+
const SourcesFilterState({
30+
this.status = SourcesFilterStatus.initial,
31+
this.sources = const [],
32+
this.hasMore = true,
33+
this.cursor,
34+
this.error,
35+
});
36+
37+
/// The current status of fetching sources.
38+
final SourcesFilterStatus status;
39+
40+
/// The list of [Source] objects fetched so far.
41+
final List<Source> sources;
42+
43+
/// Flag indicating if there are more sources available to fetch.
44+
final bool hasMore;
45+
46+
/// The cursor string to fetch the next page of sources.
47+
/// This is typically the ID of the last fetched source.
48+
final String? cursor;
49+
50+
/// An optional error object if the status is [SourcesFilterStatus.failure].
51+
final Object? error;
52+
53+
/// Creates a copy of this state with the given fields replaced.
54+
SourcesFilterState copyWith({
55+
SourcesFilterStatus? status,
56+
List<Source>? sources,
57+
bool? hasMore,
58+
String? cursor,
59+
Object? error,
60+
bool clearError = false, // Flag to explicitly clear the error
61+
bool clearCursor = false, // Flag to explicitly clear the cursor
62+
}) {
63+
return SourcesFilterState(
64+
status: status ?? this.status,
65+
sources: sources ?? this.sources,
66+
hasMore: hasMore ?? this.hasMore,
67+
// Allow explicitly setting cursor to null or clearing it
68+
cursor: clearCursor ? null : (cursor ?? this.cursor),
69+
// Clear error if requested, otherwise keep existing or use new one
70+
error: clearError ? null : error ?? this.error,
71+
);
72+
}
73+
74+
@override
75+
List<Object?> get props => [status, sources, hasMore, cursor, error];
76+
}

0 commit comments

Comments
 (0)