Skip to content

Commit 5dcf7ea

Browse files
committed
refactor(details): improve headline details UI
- Use shared spacing - Improve metadata display - Add image rounded corners - Improve theme usage
1 parent 2f230e0 commit 5dcf7ea

File tree

1 file changed

+135
-90
lines changed

1 file changed

+135
-90
lines changed

lib/headline-details/view/headline_details_page.dart

Lines changed: 135 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import 'package:ht_headlines_repository/ht_headlines_repository.dart';
55
import 'package:ht_main/headline-details/bloc/headline_details_bloc.dart';
66
import 'package:ht_main/l10n/l10n.dart';
77
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
1010
import 'package:intl/intl.dart';
1111
import 'package:url_launcher/url_launcher_string.dart';
1212

@@ -90,14 +90,21 @@ class _HeadlineDetailsView extends StatelessWidget {
9090

9191
Widget _buildLoaded(BuildContext context, Headline headline) {
9292
final l10n = context.l10n;
93+
final theme = Theme.of(context);
94+
final textTheme = theme.textTheme;
95+
final colorScheme = theme.colorScheme;
96+
9397
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(
101108
headline.imageUrl!,
102109
width: double.infinity,
103110
height: 200,
@@ -107,96 +114,134 @@ class _HeadlineDetailsView extends StatelessWidget {
107114
return Container(
108115
width: double.infinity,
109116
height: 200,
110-
color: Colors.grey[300],
117+
// Use theme color for placeholder
118+
color: colorScheme.surfaceVariant,
119+
child: const Center(child: CircularProgressIndicator()),
111120
);
112121
},
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,
131130
),
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+
),
181132
),
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 {
186164
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+
),
195177
),
196178
),
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)),
197199
],
198200
),
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+
],
200245
);
201246
}
202247
}

0 commit comments

Comments
 (0)