Skip to content

Commit 946db0f

Browse files
committed
feat: Implement EntityDetailsBloc
- Handles entity details loading - Implements follow/unfollow logic - Loads headlines for entity - Listens to account preferences
1 parent c670fd0 commit 946db0f

File tree

1 file changed

+286
-0
lines changed

1 file changed

+286
-0
lines changed
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
import 'dart:async';
2+
3+
import 'package:bloc/bloc.dart';
4+
import 'package:equatable/equatable.dart';
5+
import 'package:ht_main/account/bloc/account_bloc.dart'; // Corrected import
6+
import 'package:ht_data_repository/ht_data_repository.dart';
7+
import 'package:ht_main/entity_details/models/entity_type.dart';
8+
import 'package:ht_shared/ht_shared.dart';
9+
10+
part 'entity_details_event.dart';
11+
part 'entity_details_state.dart';
12+
13+
class EntityDetailsBloc extends Bloc<EntityDetailsEvent, EntityDetailsState> {
14+
EntityDetailsBloc({
15+
required HtDataRepository<Headline> headlinesRepository,
16+
required HtDataRepository<Category> categoryRepository,
17+
required HtDataRepository<Source> sourceRepository,
18+
required AccountBloc accountBloc, // Changed to AccountBloc
19+
}) : _headlinesRepository = headlinesRepository,
20+
_categoryRepository = categoryRepository,
21+
_sourceRepository = sourceRepository,
22+
_accountBloc = accountBloc,
23+
super(const EntityDetailsState()) {
24+
on<EntityDetailsLoadRequested>(_onEntityDetailsLoadRequested);
25+
on<EntityDetailsToggleFollowRequested>(_onEntityDetailsToggleFollowRequested);
26+
on<EntityDetailsLoadMoreHeadlinesRequested>(
27+
_onEntityDetailsLoadMoreHeadlinesRequested,
28+
);
29+
on<_EntityDetailsUserPreferencesChanged>(
30+
_onEntityDetailsUserPreferencesChanged,
31+
);
32+
33+
// Listen to AccountBloc for changes in user preferences
34+
_accountBlocSubscription = _accountBloc.stream.listen((accountState) {
35+
if (accountState.preferences != null) {
36+
add(_EntityDetailsUserPreferencesChanged(accountState.preferences!));
37+
}
38+
});
39+
}
40+
41+
final HtDataRepository<Headline> _headlinesRepository;
42+
final HtDataRepository<Category> _categoryRepository;
43+
final HtDataRepository<Source> _sourceRepository;
44+
final AccountBloc _accountBloc; // Changed to AccountBloc
45+
late final StreamSubscription<AccountState> _accountBlocSubscription;
46+
47+
static const _headlinesLimit = 10;
48+
49+
Future<void> _onEntityDetailsLoadRequested(
50+
EntityDetailsLoadRequested event,
51+
Emitter<EntityDetailsState> emit,
52+
) async {
53+
emit(state.copyWith(status: EntityDetailsStatus.loading, clearEntity: true));
54+
55+
dynamic entityToLoad = event.entity;
56+
EntityType? entityTypeToLoad = event.entityType;
57+
58+
try {
59+
// 1. Determine/Fetch Entity
60+
if (entityToLoad == null &&
61+
event.entityId != null &&
62+
event.entityType != null) {
63+
entityTypeToLoad = event.entityType; // Ensure type is set
64+
if (event.entityType == EntityType.category) {
65+
entityToLoad =
66+
await _categoryRepository.read(id: event.entityId!);
67+
} else if (event.entityType == EntityType.source) {
68+
entityToLoad = await _sourceRepository.read(id: event.entityId!);
69+
} else {
70+
throw Exception('Unknown entity type for ID fetch');
71+
}
72+
} else if (entityToLoad != null) {
73+
// If entity is directly provided, determine its type
74+
if (entityToLoad is Category) {
75+
entityTypeToLoad = EntityType.category;
76+
} else if (entityToLoad is Source) {
77+
entityTypeToLoad = EntityType.source;
78+
} else {
79+
throw Exception('Provided entity is of unknown type');
80+
}
81+
}
82+
83+
if (entityToLoad == null || entityTypeToLoad == null) {
84+
emit(
85+
state.copyWith(
86+
status: EntityDetailsStatus.failure,
87+
errorMessage: 'Entity could not be determined or loaded.',
88+
),
89+
);
90+
return;
91+
}
92+
93+
// 2. Fetch Initial Headlines
94+
final queryParams = <String, dynamic>{};
95+
if (entityTypeToLoad == EntityType.category) {
96+
queryParams['categories'] = (entityToLoad as Category).id;
97+
} else if (entityTypeToLoad == EntityType.source) {
98+
queryParams['sources'] = (entityToLoad as Source).id;
99+
}
100+
101+
final headlinesResponse = await _headlinesRepository.readAllByQuery(
102+
queryParams,
103+
limit: _headlinesLimit,
104+
);
105+
106+
// 3. Determine isFollowing status
107+
bool isCurrentlyFollowing = false;
108+
final currentAccountState = _accountBloc.state;
109+
if (currentAccountState.preferences != null) {
110+
if (entityTypeToLoad == EntityType.category &&
111+
entityToLoad is Category) {
112+
isCurrentlyFollowing = currentAccountState.preferences!.followedCategories
113+
.any((cat) => cat.id == entityToLoad.id);
114+
} else if (entityTypeToLoad == EntityType.source &&
115+
entityToLoad is Source) {
116+
isCurrentlyFollowing = currentAccountState.preferences!.followedSources
117+
.any((src) => src.id == entityToLoad.id);
118+
}
119+
}
120+
121+
emit(
122+
state.copyWith(
123+
status: EntityDetailsStatus.success,
124+
entityType: entityTypeToLoad,
125+
entity: entityToLoad,
126+
isFollowing: isCurrentlyFollowing,
127+
headlines: headlinesResponse.items,
128+
headlinesStatus: EntityHeadlinesStatus.success,
129+
hasMoreHeadlines: headlinesResponse.hasMore,
130+
headlinesCursor: headlinesResponse.cursor,
131+
clearErrorMessage: true,
132+
),
133+
);
134+
} on HtHttpException catch (e) {
135+
emit(
136+
state.copyWith(
137+
status: EntityDetailsStatus.failure,
138+
errorMessage: e.message,
139+
entityType: entityTypeToLoad, // Keep type if known
140+
),
141+
);
142+
} catch (e) {
143+
emit(
144+
state.copyWith(
145+
status: EntityDetailsStatus.failure,
146+
errorMessage: 'An unexpected error occurred: $e',
147+
entityType: entityTypeToLoad, // Keep type if known
148+
),
149+
);
150+
}
151+
}
152+
153+
Future<void> _onEntityDetailsToggleFollowRequested(
154+
EntityDetailsToggleFollowRequested event,
155+
Emitter<EntityDetailsState> emit,
156+
) async {
157+
if (state.entity == null || state.entityType == null) {
158+
// Cannot toggle follow if no entity is loaded
159+
emit(
160+
state.copyWith(
161+
errorMessage: 'No entity loaded to follow/unfollow.',
162+
clearErrorMessage: false, // Keep existing error if any, or set new
163+
),
164+
);
165+
return;
166+
}
167+
168+
// Optimistic update of UI can be handled by listening to AccountBloc state changes
169+
// which will trigger _onEntityDetailsUserPreferencesChanged.
170+
171+
if (state.entityType == EntityType.category && state.entity is Category) {
172+
_accountBloc.add(
173+
AccountFollowCategoryToggled(category: state.entity as Category),
174+
);
175+
} else if (state.entityType == EntityType.source &&
176+
state.entity is Source) {
177+
_accountBloc.add(
178+
AccountFollowSourceToggled(source: state.entity as Source),
179+
);
180+
} else {
181+
// Should not happen if entity and entityType are consistent
182+
emit(
183+
state.copyWith(
184+
errorMessage: 'Cannot determine entity type to follow/unfollow.',
185+
clearErrorMessage: false,
186+
),
187+
);
188+
}
189+
// Note: We don't emit a new state here for `isFollowing` directly.
190+
// The change will propagate from AccountBloc -> _accountBlocSubscription
191+
// -> _EntityDetailsUserPreferencesChanged -> update state.isFollowing.
192+
// This keeps AccountBloc as the source of truth for preferences.
193+
}
194+
195+
Future<void> _onEntityDetailsLoadMoreHeadlinesRequested(
196+
EntityDetailsLoadMoreHeadlinesRequested event,
197+
Emitter<EntityDetailsState> emit,
198+
) async {
199+
if (!state.hasMoreHeadlines ||
200+
state.headlinesStatus == EntityHeadlinesStatus.loadingMore) {
201+
return;
202+
}
203+
if (state.entity == null || state.entityType == null) return;
204+
205+
emit(state.copyWith(headlinesStatus: EntityHeadlinesStatus.loadingMore));
206+
207+
try {
208+
final queryParams = <String, dynamic>{};
209+
if (state.entityType == EntityType.category) {
210+
queryParams['categories'] = (state.entity as Category).id;
211+
} else if (state.entityType == EntityType.source) {
212+
queryParams['sources'] = (state.entity as Source).id;
213+
} else {
214+
// Should not happen
215+
emit(
216+
state.copyWith(
217+
headlinesStatus: EntityHeadlinesStatus.failure,
218+
errorMessage: 'Cannot load more headlines: Unknown entity type.',
219+
),
220+
);
221+
return;
222+
}
223+
224+
final headlinesResponse = await _headlinesRepository.readAllByQuery(
225+
queryParams,
226+
limit: _headlinesLimit,
227+
startAfterId: state.headlinesCursor,
228+
);
229+
230+
emit(
231+
state.copyWith(
232+
headlines: List.of(state.headlines)..addAll(headlinesResponse.items),
233+
headlinesStatus: EntityHeadlinesStatus.success,
234+
hasMoreHeadlines: headlinesResponse.hasMore,
235+
headlinesCursor: headlinesResponse.cursor,
236+
clearHeadlinesCursor: !headlinesResponse.hasMore, // Clear if no more
237+
),
238+
);
239+
} on HtHttpException catch (e) {
240+
emit(
241+
state.copyWith(
242+
headlinesStatus: EntityHeadlinesStatus.failure,
243+
errorMessage: e.message,
244+
),
245+
);
246+
} catch (e) {
247+
emit(
248+
state.copyWith(
249+
headlinesStatus: EntityHeadlinesStatus.failure,
250+
errorMessage: 'An unexpected error occurred: $e',
251+
),
252+
);
253+
}
254+
}
255+
256+
void _onEntityDetailsUserPreferencesChanged(
257+
_EntityDetailsUserPreferencesChanged event,
258+
Emitter<EntityDetailsState> emit,
259+
) {
260+
if (state.entity == null || state.entityType == null) return;
261+
262+
bool isCurrentlyFollowing = false;
263+
final preferences = event.preferences;
264+
265+
if (state.entityType == EntityType.category && state.entity is Category) {
266+
final currentCategory = state.entity as Category;
267+
isCurrentlyFollowing = preferences.followedCategories
268+
.any((cat) => cat.id == currentCategory.id);
269+
} else if (state.entityType == EntityType.source &&
270+
state.entity is Source) {
271+
final currentSource = state.entity as Source;
272+
isCurrentlyFollowing = preferences.followedSources
273+
.any((src) => src.id == currentSource.id);
274+
}
275+
276+
if (state.isFollowing != isCurrentlyFollowing) {
277+
emit(state.copyWith(isFollowing: isCurrentlyFollowing));
278+
}
279+
}
280+
281+
@override
282+
Future<void> close() {
283+
_accountBlocSubscription.cancel();
284+
return super.close();
285+
}
286+
}

0 commit comments

Comments
 (0)