@@ -4,6 +4,7 @@ import 'package:core/core.dart';
44import 'package:data_repository/data_repository.dart' ;
55import 'package:flutter/material.dart' ;
66import 'package:flutter_bloc/flutter_bloc.dart' ;
7+ import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart' ; // Import AppBloc
78import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/bloc/sources_filter_bloc.dart' ;
89import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart' ;
910import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart' ;
@@ -32,15 +33,17 @@ class SourceFilterPage extends StatelessWidget {
3233 @override
3334 Widget build (BuildContext context) {
3435 return BlocProvider (
35- create: (context) =>
36- SourcesFilterBloc (
37- sourcesRepository: context.read <DataRepository <Source >>(),
38- countriesRepository: context.read <DataRepository <Country >>(),
39- )..add (
40- LoadSourceFilterData (
41- initialSelectedSources: initialSelectedSources,
42- ),
36+ create: (context) => SourcesFilterBloc (
37+ sourcesRepository: context.read <DataRepository <Source >>(),
38+ countriesRepository: context.read <DataRepository <Country >>(),
39+ userContentPreferencesRepository:
40+ context.read <DataRepository <UserContentPreferences >>(),
41+ appBloc: context.read <AppBloc >(),
42+ )..add (
43+ LoadSourceFilterData (
44+ initialSelectedSources: initialSelectedSources,
4345 ),
46+ ),
4447 child: const _SourceFilterView (),
4548 );
4649 }
@@ -63,13 +66,44 @@ class _SourceFilterView extends StatelessWidget {
6366 style: textTheme.titleLarge,
6467 ),
6568 actions: [
69+ // Apply My Followed Sources Button
70+ BlocBuilder <SourcesFilterBloc , SourcesFilterState >(
71+ builder: (context, state) {
72+ // Determine if the "Apply My Followed" icon should be filled
73+ final bool isFollowedFilterActive =
74+ state.followedSources.isNotEmpty &&
75+ state.finallySelectedSourceIds.length ==
76+ state.followedSources.length &&
77+ state.followedSources.every (
78+ (source) =>
79+ state.finallySelectedSourceIds.contains (source.id),
80+ );
81+
82+ return IconButton (
83+ icon: isFollowedFilterActive
84+ ? const Icon (Icons .favorite)
85+ : const Icon (Icons .favorite_border),
86+ color: isFollowedFilterActive ? theme.colorScheme.primary : null ,
87+ tooltip: l10n.headlinesFeedFilterApplyFollowedLabel,
88+ onPressed: state.followedSourcesStatus ==
89+ SourceFilterDataLoadingStatus .loading
90+ ? null // Disable while loading
91+ : () {
92+ // Dispatch event to BLoC to fetch and apply followed sources
93+ context.read <SourcesFilterBloc >().add (
94+ SourcesFilterApplyFollowedRequested (),
95+ );
96+ },
97+ );
98+ },
99+ ),
66100 IconButton (
67101 icon: const Icon (Icons .clear_all_outlined),
68102 tooltip: l10n.headlinesFeedFilterResetButton,
69103 onPressed: () {
70104 context.read <SourcesFilterBloc >().add (
71- const ClearSourceFiltersRequested (),
72- );
105+ const ClearSourceFiltersRequested (),
106+ );
73107 },
74108 ),
75109 IconButton (
@@ -85,60 +119,111 @@ class _SourceFilterView extends StatelessWidget {
85119 ),
86120 ],
87121 ),
88- body: _buildBody (context, state, l10n),
89- );
90- }
91-
92- Widget _buildBody (
93- BuildContext context,
94- SourcesFilterState state,
95- AppLocalizations l10n,
96- ) {
97- final theme = Theme .of (context);
98- final textTheme = theme.textTheme;
99-
100- if (state.dataLoadingStatus == SourceFilterDataLoadingStatus .loading &&
101- state.allAvailableSources.isEmpty) {
102- // Check allAvailableSources
103- return LoadingStateWidget (
104- icon: Icons .source_outlined,
105- headline: l10n.sourceFilterLoadingHeadline,
106- subheadline: l10n.sourceFilterLoadingSubheadline,
107- );
108- }
109- if (state.dataLoadingStatus == SourceFilterDataLoadingStatus .failure &&
110- state.allAvailableSources.isEmpty) {
111- // Check allAvailableSources
112- return FailureStateWidget (
113- exception:
114- state.error ??
115- const UnknownException ('Failed to load source filter data.' ),
116- onRetry: () {
117- context.read <SourcesFilterBloc >().add (const LoadSourceFilterData ());
122+ body: BlocListener <SourcesFilterBloc , SourcesFilterState >(
123+ listenWhen: (previous, current) =>
124+ previous.followedSourcesStatus != current.followedSourcesStatus ||
125+ previous.followedSources != current.followedSources,
126+ listener: (context, state) {
127+ if (state.followedSourcesStatus == SourceFilterDataLoadingStatus .success) {
128+ if (state.followedSources.isEmpty) {
129+ ScaffoldMessenger .of (context)
130+ ..hideCurrentSnackBar ()
131+ ..showSnackBar (
132+ SnackBar (
133+ content: Text (l10n.noFollowedItemsForFilterSnackbar),
134+ duration: const Duration (seconds: 3 ),
135+ ),
136+ );
137+ }
138+ } else if (state.followedSourcesStatus == SourceFilterDataLoadingStatus .failure) {
139+ ScaffoldMessenger .of (context)
140+ ..hideCurrentSnackBar ()
141+ ..showSnackBar (
142+ SnackBar (
143+ content: Text (state.error? .message ?? l10n.unknownError),
144+ duration: const Duration (seconds: 3 ),
145+ ),
146+ );
147+ }
118148 },
119- );
120- }
149+ child: BlocBuilder <SourcesFilterBloc , SourcesFilterState >(
150+ builder: (context, state) {
151+ final bool isLoadingMainList =
152+ state.dataLoadingStatus == SourceFilterDataLoadingStatus .loading &&
153+ state.allAvailableSources.isEmpty;
154+ final bool isLoadingFollowedSources =
155+ state.followedSourcesStatus == SourceFilterDataLoadingStatus .loading;
121156
122- return Column (
123- // Removed Padding, handled by children
124- crossAxisAlignment: CrossAxisAlignment .start,
125- children: [
126- _buildCountryCapsules (context, state, l10n, textTheme),
127- const SizedBox (height: AppSpacing .md),
128- _buildSourceTypeCapsules (context, state, l10n, textTheme),
129- const SizedBox (height: AppSpacing .md),
130- Padding (
131- padding: const EdgeInsets .symmetric (
132- horizontal: AppSpacing .paddingMedium,
133- ),
134- child: Text (
135- l10n.headlinesFeedFilterSourceLabel,
136- style: textTheme.titleMedium? .copyWith (fontWeight: FontWeight .bold),
137- ),
157+ if (isLoadingMainList) {
158+ return LoadingStateWidget (
159+ icon: Icons .source_outlined,
160+ headline: l10n.sourceFilterLoadingHeadline,
161+ subheadline: l10n.sourceFilterLoadingSubheadline,
162+ );
163+ }
164+ if (state.dataLoadingStatus == SourceFilterDataLoadingStatus .failure &&
165+ state.allAvailableSources.isEmpty) {
166+ return FailureStateWidget (
167+ exception: state.error ??
168+ const UnknownException ('Failed to load source filter data.' ),
169+ onRetry: () {
170+ context.read <SourcesFilterBloc >().add (const LoadSourceFilterData ());
171+ },
172+ );
173+ }
174+
175+ return Stack (
176+ children: [
177+ Column (
178+ crossAxisAlignment: CrossAxisAlignment .start,
179+ children: [
180+ _buildCountryCapsules (context, state, l10n, textTheme),
181+ const SizedBox (height: AppSpacing .md),
182+ _buildSourceTypeCapsules (context, state, l10n, textTheme),
183+ const SizedBox (height: AppSpacing .md),
184+ Padding (
185+ padding: const EdgeInsets .symmetric (
186+ horizontal: AppSpacing .paddingMedium,
187+ ),
188+ child: Text (
189+ l10n.headlinesFeedFilterSourceLabel,
190+ style: textTheme.titleMedium
191+ ? .copyWith (fontWeight: FontWeight .bold),
192+ ),
193+ ),
194+ const SizedBox (height: AppSpacing .sm),
195+ Expanded (
196+ child: _buildSourcesList (context, state, l10n, textTheme),
197+ ),
198+ ],
199+ ),
200+ // Show loading overlay if followed sources are being fetched
201+ if (isLoadingFollowedSources)
202+ Positioned .fill (
203+ child: Container (
204+ color: Colors .black54, // Semi-transparent overlay
205+ child: Center (
206+ child: Column (
207+ mainAxisAlignment: MainAxisAlignment .center,
208+ children: [
209+ const CircularProgressIndicator (),
210+ const SizedBox (height: AppSpacing .md),
211+ Text (
212+ l10n.headlinesFeedLoadingHeadline,
213+ style: theme.textTheme.titleMedium? .copyWith (
214+ color: Colors .white,
215+ ),
216+ ),
217+ ],
218+ ),
219+ ),
220+ ),
221+ ),
222+ ],
223+ );
224+ },
138225 ),
139- const SizedBox (height: AppSpacing .sm),
140- Expanded (child: _buildSourcesList (context, state, l10n, textTheme)),
141- ],
226+ ),
142227 );
143228 }
144229
@@ -153,7 +238,6 @@ class _SourceFilterView extends StatelessWidget {
153238 horizontal: AppSpacing .paddingMedium,
154239 ).copyWith (top: AppSpacing .md),
155240 child: Column (
156- // Use Column for label and then list
157241 crossAxisAlignment: CrossAxisAlignment .start,
158242 children: [
159243 Text (
@@ -176,8 +260,8 @@ class _SourceFilterView extends StatelessWidget {
176260 selected: state.selectedCountryIsoCodes.isEmpty,
177261 onSelected: (_) {
178262 context.read <SourcesFilterBloc >().add (
179- const CountryCapsuleToggled ('' ),
180- );
263+ const CountryCapsuleToggled ('' ),
264+ );
181265 },
182266 );
183267 }
@@ -196,8 +280,8 @@ class _SourceFilterView extends StatelessWidget {
196280 ),
197281 onSelected: (_) {
198282 context.read <SourcesFilterBloc >().add (
199- CountryCapsuleToggled (country.isoCode),
200- );
283+ CountryCapsuleToggled (country.isoCode),
284+ );
201285 },
202286 );
203287 },
@@ -217,7 +301,6 @@ class _SourceFilterView extends StatelessWidget {
217301 return Padding (
218302 padding: const EdgeInsets .symmetric (horizontal: AppSpacing .paddingMedium),
219303 child: Column (
220- // Use Column for label and then list
221304 crossAxisAlignment: CrossAxisAlignment .start,
222305 children: [
223306 Text (
@@ -240,8 +323,8 @@ class _SourceFilterView extends StatelessWidget {
240323 selected: state.selectedSourceTypes.isEmpty,
241324 onSelected: (_) {
242325 context.read <SourcesFilterBloc >().add (
243- const AllSourceTypesCapsuleToggled (),
244- );
326+ const AllSourceTypesCapsuleToggled (),
327+ );
245328 },
246329 );
247330 }
@@ -252,8 +335,8 @@ class _SourceFilterView extends StatelessWidget {
252335 selected: state.selectedSourceTypes.contains (sourceType),
253336 onSelected: (_) {
254337 context.read <SourcesFilterBloc >().add (
255- SourceTypeCapsuleToggled (sourceType),
256- );
338+ SourceTypeCapsuleToggled (sourceType),
339+ );
257340 },
258341 );
259342 },
@@ -272,14 +355,12 @@ class _SourceFilterView extends StatelessWidget {
272355 ) {
273356 if (state.dataLoadingStatus == SourceFilterDataLoadingStatus .loading &&
274357 state.displayableSources.isEmpty) {
275- // Added check for displayableSources
276358 return const Center (child: CircularProgressIndicator ());
277359 }
278360 if (state.dataLoadingStatus == SourceFilterDataLoadingStatus .failure &&
279361 state.displayableSources.isEmpty) {
280362 return FailureStateWidget (
281- exception:
282- state.error ??
363+ exception: state.error ??
283364 const UnknownException ('Failed to load displayable sources.' ),
284365 onRetry: () {
285366 context.read <SourcesFilterBloc >().add (const LoadSourceFilterData ());
@@ -288,7 +369,6 @@ class _SourceFilterView extends StatelessWidget {
288369 }
289370 if (state.displayableSources.isEmpty &&
290371 state.dataLoadingStatus != SourceFilterDataLoadingStatus .loading) {
291- // Avoid showing if still loading
292372 return Center (
293373 child: Padding (
294374 padding: const EdgeInsets .all (AppSpacing .paddingLarge),
@@ -314,8 +394,8 @@ class _SourceFilterView extends StatelessWidget {
314394 onChanged: (bool ? value) {
315395 if (value != null ) {
316396 context.read <SourcesFilterBloc >().add (
317- SourceCheckboxToggled (source.id, value),
318- );
397+ SourceCheckboxToggled (source.id, value),
398+ );
319399 }
320400 },
321401 controlAffinity: ListTileControlAffinity .leading,
0 commit comments