@@ -5,8 +5,8 @@ import 'package:ht_headlines_repository/ht_headlines_repository.dart';
5
5
import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart' ;
6
6
import 'package:ht_main/l10n/l10n.dart' ;
7
7
import 'package:ht_main/shared/widgets/failure_state_widget.dart' ;
8
- import 'package:ht_main/shared/widgets/initial_state_widget .dart' ;
9
- import 'package:ht_main/shared/widgets/loading_state_widget .dart' ;
8
+ import 'package:ht_main/l10n/l10n .dart' ;
9
+ import 'package:ht_main/shared/shared .dart' ; // Import shared barrel file
10
10
import 'package:intl/intl.dart' ;
11
11
import 'package:url_launcher/url_launcher_string.dart' ;
12
12
@@ -90,14 +90,21 @@ class _HeadlineDetailsView extends StatelessWidget {
90
90
91
91
Widget _buildLoaded (BuildContext context, Headline headline) {
92
92
final l10n = context.l10n;
93
+ final theme = Theme .of (context);
94
+ final textTheme = theme.textTheme;
95
+ final colorScheme = theme.colorScheme;
96
+
93
97
return SingleChildScrollView (
94
- child: Padding (
95
- padding: const EdgeInsets .all (16 ),
96
- child: Column (
97
- crossAxisAlignment: CrossAxisAlignment .start,
98
- children: [
99
- if (headline.imageUrl != null )
100
- Image .network (
98
+ // Use shared padding constant
99
+ padding: const EdgeInsets .all (AppSpacing .paddingLarge),
100
+ child: Column (
101
+ crossAxisAlignment: CrossAxisAlignment .start,
102
+ children: [
103
+ // --- Image ---
104
+ if (headline.imageUrl != null ) ...[
105
+ ClipRRect ( // Add rounded corners to the image
106
+ borderRadius: BorderRadius .circular (AppSpacing .md),
107
+ child: Image .network (
101
108
headline.imageUrl! ,
102
109
width: double .infinity,
103
110
height: 200 ,
@@ -107,96 +114,134 @@ class _HeadlineDetailsView extends StatelessWidget {
107
114
return Container (
108
115
width: double .infinity,
109
116
height: 200 ,
110
- color: Colors .grey[300 ],
117
+ // Use theme color for placeholder
118
+ color: colorScheme.surfaceVariant,
119
+ child: const Center (child: CircularProgressIndicator ()),
111
120
);
112
121
},
113
- errorBuilder:
114
- (context, error, stackTrace) => const Icon (Icons .error),
115
- ),
116
- const SizedBox (height: 16 ),
117
- Text (headline.title, style: Theme .of (context).textTheme.titleLarge),
118
- const SizedBox (height: 8 ),
119
- Column (
120
- children: [
121
- if (headline.source != null ) ...[
122
- Row (
123
- children: [
124
- const Icon (Icons .source),
125
- const SizedBox (width: 4 ),
126
- Text (
127
- headline.source! ,
128
- style: Theme .of (context).textTheme.bodyMedium,
129
- ),
130
- ],
122
+ errorBuilder: (context, error, stackTrace) => Container (
123
+ width: double .infinity,
124
+ height: 200 ,
125
+ color: colorScheme.surfaceVariant,
126
+ child: Icon (
127
+ Icons .broken_image,
128
+ color: colorScheme.onSurfaceVariant,
129
+ size: AppSpacing .xxl,
131
130
),
132
- const SizedBox (height: 8 ),
133
- ],
134
- if (headline.categories != null &&
135
- headline.categories! .isNotEmpty) ...[
136
- Row (
137
- children: [
138
- const Icon (Icons .category),
139
- const SizedBox (width: 4 ),
140
- Text (
141
- headline.categories! .join (', ' ),
142
- style: Theme .of (context).textTheme.bodyMedium,
143
- ),
144
- ],
145
- ),
146
- const SizedBox (height: 8 ),
147
- ],
148
- if (headline.eventCountry != null ) ...[
149
- Row (
150
- children: [
151
- const Icon (Icons .location_on),
152
- const SizedBox (width: 4 ),
153
- Text (
154
- headline.eventCountry! ,
155
- style: Theme .of (context).textTheme.bodyMedium,
156
- ),
157
- ],
158
- ),
159
- const SizedBox (height: 8 ),
160
- ],
161
- if (headline.publishedAt != null )
162
- Row (
163
- children: [
164
- const Icon (Icons .date_range),
165
- const SizedBox (width: 4 ),
166
- Text (
167
- DateFormat (
168
- 'MMMM dd, yyyy' ,
169
- ).format (headline.publishedAt! ),
170
- style: Theme .of (context).textTheme.bodyMedium,
171
- ),
172
- ],
173
- ),
174
- ],
175
- ),
176
- const SizedBox (height: 16 ),
177
- if (headline.description != null )
178
- Text (
179
- headline.description! ,
180
- style: Theme .of (context).textTheme.bodyLarge,
131
+ ),
181
132
),
182
- const SizedBox (height: 16 ),
183
- ElevatedButton (
184
- onPressed: () async {
185
- if (headline.url != null ) {
133
+ ),
134
+ // Use shared spacing constant
135
+ const SizedBox (height: AppSpacing .lg),
136
+ ],
137
+
138
+ // --- Title ---
139
+ Text (headline.title, style: textTheme.titleLarge),
140
+ // Use shared spacing constant
141
+ const SizedBox (height: AppSpacing .md), // Increased spacing before metadata
142
+
143
+ // --- Metadata Section ---
144
+ _buildMetadataSection (context, headline),
145
+ // Use shared spacing constant
146
+ const SizedBox (height: AppSpacing .lg),
147
+
148
+ // --- Description ---
149
+ if (headline.description != null ) ...[
150
+ Text (
151
+ headline.description! ,
152
+ style: textTheme.bodyLarge,
153
+ ),
154
+ // Use shared spacing constant
155
+ const SizedBox (height: AppSpacing .xl), // Increased spacing before button
156
+ ],
157
+
158
+ // --- Continue Reading Button ---
159
+ if (headline.url != null )
160
+ SizedBox ( // Make button full width
161
+ width: double .infinity,
162
+ child: ElevatedButton (
163
+ onPressed: () async {
186
164
await launchUrlString (headline.url! );
187
- }
188
- },
189
- style: ElevatedButton .styleFrom (
190
- backgroundColor: Theme .of (context).colorScheme.primary,
191
- ),
192
- child: Text (
193
- l10n.headlineDetailsContinueReadingButton,
194
- style: Theme .of (context).textTheme.labelLarge,
165
+ },
166
+ // Style is often handled by ElevatedButtonThemeData in AppTheme
167
+ // but explicitly setting background for clarity if needed.
168
+ // style: ElevatedButton.styleFrom(
169
+ // backgroundColor: colorScheme.primary,
170
+ // foregroundColor: colorScheme.onPrimary,
171
+ // ),
172
+ child: Text (
173
+ l10n.headlineDetailsContinueReadingButton,
174
+ // Ensure labelLarge has contrast if theme doesn't handle it
175
+ // style: textTheme.labelLarge?.copyWith(color: colorScheme.onPrimary),
176
+ ),
195
177
),
196
178
),
179
+ ],
180
+ ),
181
+ );
182
+ }
183
+
184
+ /// Builds the metadata section (Source, Date, Categories, Country).
185
+ Widget _buildMetadataSection (BuildContext context, Headline headline) {
186
+ final theme = Theme .of (context);
187
+ final textTheme = theme.textTheme;
188
+ final metadataStyle = textTheme.bodyMedium; // Or textTheme.caption
189
+
190
+ // Helper to create consistent metadata rows
191
+ Widget buildMetadataRow (IconData icon, String text) {
192
+ return Padding (
193
+ padding: const EdgeInsets .only (bottom: AppSpacing .sm),
194
+ child: Row (
195
+ children: [
196
+ Icon (icon, size: 16 , color: theme.colorScheme.onSurfaceVariant),
197
+ const SizedBox (width: AppSpacing .xs),
198
+ Expanded (child: Text (text, style: metadataStyle)),
197
199
],
198
200
),
199
- ),
201
+ );
202
+ }
203
+
204
+ return Column (
205
+ crossAxisAlignment: CrossAxisAlignment .start,
206
+ children: [
207
+ if (headline.source != null )
208
+ buildMetadataRow (Icons .source, headline.source! ),
209
+ if (headline.publishedAt != null )
210
+ buildMetadataRow (
211
+ Icons .date_range,
212
+ DateFormat ('MMMM dd, yyyy' ).format (headline.publishedAt! ),
213
+ ),
214
+ if (headline.eventCountry != null )
215
+ buildMetadataRow (Icons .location_on, headline.eventCountry! ),
216
+ if (headline.categories != null && headline.categories! .isNotEmpty)
217
+ Padding (
218
+ padding: const EdgeInsets .only (bottom: AppSpacing .sm),
219
+ child: Row (
220
+ crossAxisAlignment: CrossAxisAlignment .start, // Align icon top
221
+ children: [
222
+ Icon (Icons .category, size: 16 , color: theme.colorScheme.onSurfaceVariant),
223
+ const SizedBox (width: AppSpacing .xs),
224
+ Expanded (
225
+ child: Wrap (
226
+ spacing: AppSpacing .xs, // Horizontal spacing between chips
227
+ runSpacing: AppSpacing .xs, // Vertical spacing if wraps
228
+ children: headline.categories!
229
+ .map ((category) => Chip (
230
+ label: Text (category),
231
+ labelStyle: textTheme.labelSmall,
232
+ padding: EdgeInsets .zero,
233
+ materialTapTargetSize: MaterialTapTargetSize .shrinkWrap,
234
+ visualDensity: VisualDensity .compact,
235
+ backgroundColor: theme.colorScheme.secondaryContainer,
236
+ labelPadding: const EdgeInsets .symmetric (horizontal: AppSpacing .xs),
237
+ ))
238
+ .toList (),
239
+ ),
240
+ ),
241
+ ],
242
+ ),
243
+ ),
244
+ ],
200
245
);
201
246
}
202
247
}
0 commit comments