@@ -54,14 +54,19 @@ class _SourceFilterView extends StatelessWidget {
54
54
@override
55
55
Widget build (BuildContext context) {
56
56
final l10n = context.l10n;
57
+ final theme = Theme .of (context); // Get theme
58
+ final textTheme = theme.textTheme; // Get textTheme
57
59
final state = context.watch <SourcesFilterBloc >().state;
58
60
59
61
return Scaffold (
60
62
appBar: AppBar (
61
- title: Text (l10n.headlinesFeedFilterSourceLabel),
63
+ title: Text (
64
+ l10n.headlinesFeedFilterSourceLabel,
65
+ style: textTheme.titleLarge, // Apply consistent title style
66
+ ),
62
67
actions: [
63
68
IconButton (
64
- icon: const Icon (Icons .clear_all),
69
+ icon: const Icon (Icons .clear_all_outlined), // Use outlined
65
70
tooltip: l10n.headlinesFeedFilterResetButton,
66
71
onPressed: () {
67
72
context.read <SourcesFilterBloc >().add (
@@ -98,97 +103,107 @@ class _SourceFilterView extends StatelessWidget {
98
103
SourcesFilterState state,
99
104
AppLocalizations l10n,
100
105
) {
106
+ final theme = Theme .of (context);
107
+ final textTheme = theme.textTheme;
108
+
101
109
if (state.dataLoadingStatus == SourceFilterDataLoadingStatus .loading &&
102
- state.availableCountries .isEmpty) {
110
+ state.allAvailableSources .isEmpty) { // Check allAvailableSources
103
111
return LoadingStateWidget (
104
- icon: Icons .filter_list_alt , // Added generic icon
105
- headline: l10n.headlinesFeedFilterLoadingCriteria,
106
- subheadline: l10n.pleaseWait , // Added generic subheadline ( l10n key)
112
+ icon: Icons .source_outlined , // More relevant icon
113
+ headline: l10n.sourceFilterLoadingHeadline, // Specific l10n
114
+ subheadline: l10n.sourceFilterLoadingSubheadline , // Specific l10n
107
115
);
108
116
}
109
117
if (state.dataLoadingStatus == SourceFilterDataLoadingStatus .failure &&
110
- state.availableCountries .isEmpty) {
118
+ state.allAvailableSources .isEmpty) { // Check allAvailableSources
111
119
return FailureStateWidget (
112
120
message: state.errorMessage ?? l10n.headlinesFeedFilterErrorCriteria,
113
121
onRetry: () {
114
- context.read <SourcesFilterBloc >().add (
115
- // When retrying, we don't have initial capsule states from arguments
116
- // So, we pass empty sets, BLoC will load all sources and countries.
117
- // User can then re-apply capsule filters if needed.
118
- // Or, we could try to persist/retrieve the last known good capsule state.
119
- // For now, simple retry reloads all.
120
- const LoadSourceFilterData (),
121
- );
122
+ context
123
+ .read <SourcesFilterBloc >()
124
+ .add (const LoadSourceFilterData ());
122
125
},
123
126
);
124
127
}
125
128
126
- return Padding (
127
- padding: const EdgeInsets .only (top: AppSpacing .md),
128
- child: Column (
129
- crossAxisAlignment: CrossAxisAlignment .start,
130
- children: [
131
- _buildCountryCapsules (context, state, l10n),
132
- const SizedBox (height: AppSpacing .lg),
133
- _buildSourceTypeCapsules (context, state, l10n),
134
- const SizedBox (height: AppSpacing .lg),
135
- Expanded (child: _buildSourcesList (context, state, l10n)),
136
- ],
137
- ),
129
+ return Column ( // Removed Padding, handled by children
130
+ crossAxisAlignment: CrossAxisAlignment .start,
131
+ children: [
132
+ _buildCountryCapsules (context, state, l10n, textTheme),
133
+ const SizedBox (height: AppSpacing .md), // Adjusted spacing
134
+ _buildSourceTypeCapsules (context, state, l10n, textTheme),
135
+ const SizedBox (height: AppSpacing .md), // Adjusted spacing
136
+ Padding (
137
+ padding: const EdgeInsets .symmetric (
138
+ horizontal: AppSpacing .paddingMedium,
139
+ ),
140
+ child: Text (
141
+ l10n.headlinesFeedFilterSourceLabel, // "Sources" title
142
+ style: textTheme.titleMedium? .copyWith (fontWeight: FontWeight .bold),
143
+ ),
144
+ ),
145
+ const SizedBox (height: AppSpacing .sm),
146
+ Expanded (child: _buildSourcesList (context, state, l10n, textTheme)),
147
+ ],
138
148
);
139
149
}
140
150
141
151
Widget _buildCountryCapsules (
142
152
BuildContext context,
143
153
SourcesFilterState state,
144
154
AppLocalizations l10n,
155
+ TextTheme textTheme, // Added textTheme
145
156
) {
146
157
return Padding (
147
- padding: const EdgeInsets .symmetric (horizontal: AppSpacing .paddingMedium),
148
- child: Row (
158
+ padding: const EdgeInsets .symmetric (horizontal: AppSpacing .paddingMedium)
159
+ .copyWith (top: AppSpacing .md), // Add top padding
160
+ child: Column ( // Use Column for label and then list
161
+ crossAxisAlignment: CrossAxisAlignment .start,
149
162
children: [
150
163
Text (
151
- '${ l10n .headlinesFeedFilterCountryLabel }:' ,
152
- style: Theme . of (context).textTheme.titleSmall ,
164
+ l10n.headlinesFeedFilterCountryLabel, // "Countries" label
165
+ style: textTheme.titleMedium ? . copyWith (fontWeight : FontWeight .bold) ,
153
166
),
154
- const SizedBox (width: AppSpacing .md),
155
- Expanded (
156
- child: SizedBox (
157
- height: 40 , // Fixed height for the capsule list
158
- child: ListView .separated (
159
- scrollDirection: Axis .horizontal,
160
- itemCount: state.availableCountries.length + 1 , // +1 for "All"
161
- separatorBuilder:
162
- (context, index) => const SizedBox (width: AppSpacing .sm),
163
- itemBuilder: (context, index) {
164
- if (index == 0 ) {
165
- // "All" chip
166
- return ChoiceChip (
167
- label: Text (l10n.headlinesFeedFilterAllLabel),
168
- selected: state.selectedCountryIsoCodes.isEmpty,
169
- onSelected: (_) {
170
- context.read <SourcesFilterBloc >().add (
171
- const CountryCapsuleToggled (
172
- '' ,
173
- ), // Special value for "All"
174
- );
175
- },
176
- );
177
- }
178
- final country = state.availableCountries[index - 1 ];
167
+ const SizedBox (height: AppSpacing .sm),
168
+ SizedBox (
169
+ height: AppSpacing .xl + AppSpacing .md, // Standardized height
170
+ child: ListView .separated (
171
+ scrollDirection: Axis .horizontal,
172
+ itemCount: state.availableCountries.length + 1 ,
173
+ separatorBuilder: (context, index) =>
174
+ const SizedBox (width: AppSpacing .sm),
175
+ itemBuilder: (context, index) {
176
+ if (index == 0 ) {
179
177
return ChoiceChip (
180
- label: Text (country.name),
181
- selected: state.selectedCountryIsoCodes.contains (
182
- country.isoCode,
183
- ),
178
+ label: Text (l10n.headlinesFeedFilterAllLabel),
179
+ labelStyle: textTheme.labelLarge,
180
+ selected: state.selectedCountryIsoCodes.isEmpty,
184
181
onSelected: (_) {
185
- context. read < SourcesFilterBloc >(). add (
186
- CountryCapsuleToggled (country.isoCode),
187
- );
182
+ context
183
+ . read < SourcesFilterBloc >()
184
+ . add ( const CountryCapsuleToggled ( '' ) );
188
185
},
189
186
);
190
- },
191
- ),
187
+ }
188
+ final country = state.availableCountries[index - 1 ];
189
+ return ChoiceChip (
190
+ avatar: country.flagUrl.isNotEmpty
191
+ ? CircleAvatar (
192
+ backgroundImage: NetworkImage (country.flagUrl),
193
+ radius: AppSpacing .sm + AppSpacing .xs,
194
+ )
195
+ : null ,
196
+ label: Text (country.name),
197
+ labelStyle: textTheme.labelLarge,
198
+ selected:
199
+ state.selectedCountryIsoCodes.contains (country.isoCode),
200
+ onSelected: (_) {
201
+ context
202
+ .read <SourcesFilterBloc >()
203
+ .add (CountryCapsuleToggled (country.isoCode));
204
+ },
205
+ );
206
+ },
192
207
),
193
208
),
194
209
],
@@ -200,62 +215,50 @@ class _SourceFilterView extends StatelessWidget {
200
215
BuildContext context,
201
216
SourcesFilterState state,
202
217
AppLocalizations l10n,
218
+ TextTheme textTheme, // Added textTheme
203
219
) {
204
220
return Padding (
205
221
padding: const EdgeInsets .symmetric (horizontal: AppSpacing .paddingMedium),
206
- child: Row (
222
+ child: Column ( // Use Column for label and then list
223
+ crossAxisAlignment: CrossAxisAlignment .start,
207
224
children: [
208
225
Text (
209
- '${ l10n .headlinesFeedFilterSourceTypeLabel }:' , // Assuming l10n key exists
210
- style: Theme . of (context).textTheme.titleSmall ,
226
+ l10n.headlinesFeedFilterSourceTypeLabel,
227
+ style: textTheme.titleMedium ? . copyWith (fontWeight : FontWeight .bold) ,
211
228
),
212
- const SizedBox (width: AppSpacing .md),
213
- Expanded (
214
- child: SizedBox (
215
- height: 40 , // Fixed height for the capsule list
216
- child: ListView .separated (
217
- scrollDirection: Axis .horizontal,
218
- itemCount:
219
- state.availableSourceTypes.length + 1 , // +1 for "All"
220
- separatorBuilder:
221
- (context, index) => const SizedBox (width: AppSpacing .sm),
222
- itemBuilder: (context, index) {
223
- if (index == 0 ) {
224
- // "All" chip
225
- return ChoiceChip (
226
- label: Text (l10n.headlinesFeedFilterAllLabel),
227
- selected: state.selectedSourceTypes.isEmpty,
228
- onSelected: (_) {
229
- // For "All", if it's selected, it means no specific types are chosen.
230
- // The BLoC should interpret an empty selectedSourceTypes set as "All".
231
- // Toggling "All" when it's already selected (meaning list is empty)
232
- // doesn't have a clear action here without more complex "select all" logic.
233
- // For now, if "All" is tapped, we ensure the specific selections are cleared.
234
- // This is best handled in the BLoC.
235
- // We can send a specific event or a toggle that the BLoC interprets.
236
- // For simplicity, let's make it so tapping "All" when selected does nothing,
237
- // Tapping "All" for source types should clear specific selections.
238
- // This is now handled by the AllSourceTypesCapsuleToggled event.
239
- context.read <SourcesFilterBloc >().add (
240
- const AllSourceTypesCapsuleToggled (),
241
- );
242
- },
243
- );
244
- }
245
- final sourceType = state.availableSourceTypes[index - 1 ];
229
+ const SizedBox (height: AppSpacing .sm),
230
+ SizedBox (
231
+ height: AppSpacing .xl + AppSpacing .md, // Standardized height
232
+ child: ListView .separated (
233
+ scrollDirection: Axis .horizontal,
234
+ itemCount: state.availableSourceTypes.length + 1 ,
235
+ separatorBuilder: (context, index) =>
236
+ const SizedBox (width: AppSpacing .sm),
237
+ itemBuilder: (context, index) {
238
+ if (index == 0 ) {
246
239
return ChoiceChip (
247
- label: Text (
248
- sourceType.name,
249
- ), // Or a more user-friendly name
250
- selected: state.selectedSourceTypes.contains (sourceType),
240
+ label: Text (l10n.headlinesFeedFilterAllLabel),
241
+ labelStyle: textTheme.labelLarge,
242
+ selected: state.selectedSourceTypes.isEmpty,
251
243
onSelected: (_) {
252
- context. read < SourcesFilterBloc >(). add (
253
- SourceTypeCapsuleToggled (sourceType),
254
- );
244
+ context
245
+ . read < SourcesFilterBloc >()
246
+ . add ( const AllSourceTypesCapsuleToggled () );
255
247
},
256
248
);
257
- },
258
- ),
249
+ }
250
+ final sourceType = state.availableSourceTypes[index - 1 ];
251
+ return ChoiceChip (
252
+ label: Text (sourceType.name), // Assuming SourceType.name is user-friendly
253
+ labelStyle: textTheme.labelLarge,
254
+ selected: state.selectedSourceTypes.contains (sourceType),
255
+ onSelected: (_) {
256
+ context
257
+ .read <SourcesFilterBloc >()
258
+ .add (SourceTypeCapsuleToggled (sourceType));
259
+ },
260
+ );
261
+ },
259
262
),
260
263
),
261
264
],
@@ -267,47 +270,57 @@ class _SourceFilterView extends StatelessWidget {
267
270
BuildContext context,
268
271
SourcesFilterState state,
269
272
AppLocalizations l10n,
273
+ TextTheme textTheme, // Added textTheme
270
274
) {
271
- if (state.dataLoadingStatus == SourceFilterDataLoadingStatus .loading) {
275
+ if (state.dataLoadingStatus == SourceFilterDataLoadingStatus .loading &&
276
+ state.displayableSources.isEmpty) { // Added check for displayableSources
272
277
return const Center (child: CircularProgressIndicator ());
273
278
}
274
279
if (state.dataLoadingStatus == SourceFilterDataLoadingStatus .failure &&
275
280
state.displayableSources.isEmpty) {
276
281
return FailureStateWidget (
277
282
message: state.errorMessage ?? l10n.headlinesFeedFilterErrorSources,
278
283
onRetry: () {
279
- // Dispatch a public event to reload/retry, BLoC will handle internally
280
- context.read <SourcesFilterBloc >().add (
281
- LoadSourceFilterData (
282
- initialSelectedSources:
283
- state.displayableSources
284
- .where (
285
- (s) => state.finallySelectedSourceIds.contains (s.id),
286
- )
287
- .toList (), // Or pass current selections if needed for retry context
288
- ),
289
- );
284
+ context
285
+ .read <SourcesFilterBloc >()
286
+ .add (const LoadSourceFilterData ());
290
287
},
291
288
);
292
289
}
293
- if (state.displayableSources.isEmpty) {
294
- return Center (child: Text (l10n.headlinesFeedFilterNoSourcesMatch));
290
+ if (state.displayableSources.isEmpty &&
291
+ state.dataLoadingStatus != SourceFilterDataLoadingStatus .loading) { // Avoid showing if still loading
292
+ return Center (
293
+ child: Padding (
294
+ padding: const EdgeInsets .all (AppSpacing .paddingLarge),
295
+ child: Text (
296
+ l10n.headlinesFeedFilterNoSourcesMatch,
297
+ style: textTheme.bodyLarge,
298
+ textAlign: TextAlign .center,
299
+ ),
300
+ ),
301
+ );
295
302
}
296
303
297
304
return ListView .builder (
305
+ padding: const EdgeInsets .symmetric (vertical: AppSpacing .paddingSmall)
306
+ .copyWith (bottom: AppSpacing .xxl),
298
307
itemCount: state.displayableSources.length,
299
308
itemBuilder: (context, index) {
300
309
final source = state.displayableSources[index];
301
310
return CheckboxListTile (
302
- title: Text (source.name),
311
+ title: Text (source.name, style : textTheme.titleMedium ),
303
312
value: state.finallySelectedSourceIds.contains (source.id),
304
313
onChanged: (bool ? value) {
305
314
if (value != null ) {
306
- context. read < SourcesFilterBloc >(). add (
307
- SourceCheckboxToggled (source.id, value),
308
- );
315
+ context
316
+ . read < SourcesFilterBloc >()
317
+ . add ( SourceCheckboxToggled (source.id, value) );
309
318
}
310
319
},
320
+ controlAffinity: ListTileControlAffinity .leading,
321
+ contentPadding: const EdgeInsets .symmetric (
322
+ horizontal: AppSpacing .paddingMedium,
323
+ ),
311
324
);
312
325
},
313
326
);
0 commit comments