Skip to content

Commit c6d9bd4

Browse files
committed
feat: Add HeadlineTileTextOnly widget
- Displays headline without image - Supports category, source, date - Uses timeago for date formatting
1 parent e2e7eb9 commit c6d9bd4

File tree

1 file changed

+205
-0
lines changed

1 file changed

+205
-0
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:ht_main/l10n/l10n.dart';
3+
import 'package:ht_main/shared/constants/app_spacing.dart';
4+
import 'package:ht_shared/ht_shared.dart' show Headline;
5+
import 'package:timeago/timeago.dart' as timeago;
6+
7+
/// {@template headline_tile_text_only}
8+
/// A widget to display a headline item with text only.
9+
///
10+
/// Used in feeds, search results, etc., when the image style is set to hidden.
11+
/// {@endtemplate}
12+
class HeadlineTileTextOnly extends StatelessWidget {
13+
/// {@macro headline_tile_text_only}
14+
const HeadlineTileTextOnly({
15+
required this.headline,
16+
super.key,
17+
this.onHeadlineTap,
18+
this.trailing,
19+
});
20+
21+
/// The headline data to display.
22+
final Headline headline;
23+
24+
/// Callback when the main content of the headline (e.g., title) is tapped.
25+
final VoidCallback? onHeadlineTap;
26+
27+
/// An optional widget to display at the end of the tile.
28+
final Widget? trailing;
29+
30+
@override
31+
Widget build(BuildContext context) {
32+
final l10n = context.l10n;
33+
final theme = Theme.of(context);
34+
final textTheme = theme.textTheme;
35+
final colorScheme = theme.colorScheme;
36+
37+
return Card(
38+
margin: const EdgeInsets.symmetric(
39+
horizontal: AppSpacing.paddingMedium,
40+
vertical: AppSpacing.xs,
41+
),
42+
child: InkWell(
43+
onTap: onHeadlineTap,
44+
child: Padding(
45+
padding: const EdgeInsets.all(AppSpacing.md),
46+
child: Row(
47+
crossAxisAlignment: CrossAxisAlignment.start,
48+
children: [
49+
Expanded(
50+
child: Column(
51+
crossAxisAlignment: CrossAxisAlignment.start,
52+
children: [
53+
Text(
54+
headline.title,
55+
style: textTheme.titleMedium?.copyWith(
56+
fontWeight: FontWeight.w500,
57+
),
58+
maxLines: 3, // Allow more lines for text-only
59+
overflow: TextOverflow.ellipsis,
60+
),
61+
const SizedBox(height: AppSpacing.sm),
62+
_HeadlineMetadataRow(
63+
headline: headline,
64+
l10n: l10n,
65+
colorScheme: colorScheme,
66+
textTheme: textTheme,
67+
),
68+
],
69+
),
70+
),
71+
if (trailing != null) ...[
72+
const SizedBox(width: AppSpacing.sm),
73+
trailing!,
74+
],
75+
],
76+
),
77+
),
78+
),
79+
);
80+
}
81+
}
82+
83+
/// Private helper widget to build the metadata row.
84+
class _HeadlineMetadataRow extends StatelessWidget {
85+
const _HeadlineMetadataRow({
86+
required this.headline,
87+
required this.l10n,
88+
required this.colorScheme,
89+
required this.textTheme,
90+
});
91+
92+
final Headline headline;
93+
final AppLocalizations l10n;
94+
final ColorScheme colorScheme;
95+
final TextTheme textTheme;
96+
97+
@override
98+
Widget build(BuildContext context) {
99+
final String formattedDate;
100+
if (headline.publishedAt != null) {
101+
formattedDate = timeago.format(
102+
headline.publishedAt!,
103+
locale: Localizations.localeOf(context).languageCode,
104+
);
105+
} else {
106+
formattedDate = '';
107+
}
108+
109+
final metadataStyle = textTheme.bodySmall?.copyWith(
110+
color: colorScheme.onSurfaceVariant,
111+
);
112+
const iconSize = 12.0;
113+
114+
return Wrap(
115+
spacing: AppSpacing.md, // Spacing between items in the row
116+
runSpacing: AppSpacing.xs, // Spacing if items wrap
117+
crossAxisAlignment: WrapCrossAlignment.center,
118+
children: [
119+
if (headline.category?.name != null)
120+
GestureDetector(
121+
onTap: () {
122+
ScaffoldMessenger.of(context)
123+
..hideCurrentSnackBar()
124+
..showSnackBar(
125+
SnackBar(
126+
content: Text(
127+
'Tapped Category: ${headline.category!.name}',
128+
),
129+
),
130+
);
131+
},
132+
child: Chip(
133+
avatar: Icon(
134+
Icons.label_outline,
135+
size: iconSize,
136+
color: colorScheme.onSurfaceVariant, // Changed color
137+
),
138+
label: Text(headline.category!.name),
139+
labelStyle: textTheme.labelSmall?.copyWith(
140+
color: colorScheme.onSurfaceVariant, // Changed color
141+
),
142+
// backgroundColor removed
143+
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm),
144+
visualDensity: VisualDensity.compact,
145+
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
146+
),
147+
),
148+
if (headline.source?.name != null)
149+
GestureDetector(
150+
onTap: () {
151+
ScaffoldMessenger.of(context)
152+
..hideCurrentSnackBar()
153+
..showSnackBar(
154+
SnackBar(
155+
content: Text('Tapped Source: ${headline.source!.name}'),
156+
),
157+
);
158+
},
159+
child: Row(
160+
mainAxisSize: MainAxisSize.min,
161+
children: [
162+
Icon(
163+
Icons.source_outlined,
164+
size: iconSize,
165+
color: colorScheme.onSurfaceVariant,
166+
),
167+
const SizedBox(width: AppSpacing.xs),
168+
Flexible(
169+
child: Text(
170+
headline.source!.name,
171+
style: metadataStyle,
172+
overflow: TextOverflow.ellipsis,
173+
),
174+
),
175+
],
176+
),
177+
),
178+
if (formattedDate.isNotEmpty)
179+
GestureDetector(
180+
onTap: () {
181+
ScaffoldMessenger.of(context)
182+
..hideCurrentSnackBar()
183+
..showSnackBar(
184+
SnackBar(
185+
content: Text('Tapped Date: $formattedDate'),
186+
),
187+
);
188+
},
189+
child: Row(
190+
mainAxisSize: MainAxisSize.min,
191+
children: [
192+
Icon(
193+
Icons.calendar_today_outlined,
194+
size: iconSize,
195+
color: colorScheme.onSurfaceVariant,
196+
),
197+
const SizedBox(width: AppSpacing.xs),
198+
Text(formattedDate, style: metadataStyle),
199+
],
200+
),
201+
),
202+
],
203+
);
204+
}
205+
}

0 commit comments

Comments
 (0)