Skip to content

Commit 12f0dd0

Browse files
authored
Merge pull request #194 from flutter-news-app-full-source-code/refactor/ui-ux
Refactor/UI ux
2 parents f8ab42e + 7404d63 commit 12f0dd0

File tree

14 files changed

+407
-147
lines changed

14 files changed

+407
-147
lines changed

lib/account/view/account_page.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,16 @@ class AccountPage extends StatelessWidget {
5050
)
5151
else
5252
IconButton(
53-
icon: const Icon(Icons.logout),
53+
icon: const Icon(
54+
Icons.logout,
55+
), // Non-directional icon for logout
5456
tooltip: l10n.accountSignOutTile,
5557
onPressed: () =>
5658
context.read<AppBloc>().add(const AppLogoutRequested()),
5759
),
60+
const SizedBox(
61+
width: AppSpacing.lg,
62+
), // Consistent right padding for the AppBar actions
5863
],
5964
),
6065
body: SingleChildScrollView(

lib/account/view/followed_contents/sources/add_source_to_follow_page.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ class AddSourceToFollowPage extends StatelessWidget {
7070
return Card(
7171
margin: const EdgeInsets.only(bottom: AppSpacing.sm),
7272
child: ListTile(
73+
leading: SizedBox(
74+
width: 40,
75+
height: 40,
76+
child: ClipRRect(
77+
borderRadius: BorderRadius.circular(AppSpacing.sm),
78+
child: Image.network(
79+
source.logoUrl,
80+
fit: BoxFit.cover,
81+
errorBuilder: (context, error, stackTrace) =>
82+
const Icon(Icons.source_outlined),
83+
),
84+
),
85+
),
7386
title: Text(source.name),
7487
trailing: IconButton(
7588
icon: isFollowed

lib/account/view/followed_contents/sources/followed_sources_list_page.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,19 @@ class FollowedSourcesListPage extends StatelessWidget {
7272
itemBuilder: (context, index) {
7373
final source = followedSources[index];
7474
return ListTile(
75-
leading: const Icon(Icons.source_outlined),
75+
leading: SizedBox(
76+
width: 40,
77+
height: 40,
78+
child: ClipRRect(
79+
borderRadius: BorderRadius.circular(AppSpacing.sm),
80+
child: Image.network(
81+
source.logoUrl,
82+
fit: BoxFit.cover,
83+
errorBuilder: (context, error, stackTrace) =>
84+
const Icon(Icons.source_outlined),
85+
),
86+
),
87+
),
7688
title: Text(source.name),
7789
subtitle: Text(
7890
source.description,

lib/discover/view/discover_page.dart

Lines changed: 116 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,41 @@ class _DiscoverViewState extends State<_DiscoverView> {
111111
}
112112

113113
/// A widget that displays a single category of sources as a horizontal row.
114-
class _SourceCategoryRow extends StatelessWidget {
114+
class _SourceCategoryRow extends StatefulWidget {
115115
const _SourceCategoryRow({required this.sourceType, required this.sources});
116116

117117
final SourceType sourceType;
118118
final List<Source> sources;
119119

120+
@override
121+
State<_SourceCategoryRow> createState() => _SourceCategoryRowState();
122+
}
123+
124+
class _SourceCategoryRowState extends State<_SourceCategoryRow> {
125+
final _scrollController = ScrollController();
126+
bool _showEndFade = false;
127+
128+
@override
129+
void initState() {
130+
super.initState();
131+
_scrollController.addListener(() => setState(() {}));
132+
WidgetsBinding.instance.addPostFrameCallback((_) {
133+
if (mounted &&
134+
_scrollController.hasClients &&
135+
_scrollController.position.maxScrollExtent > 0) {
136+
setState(() {
137+
_showEndFade = true;
138+
});
139+
}
140+
});
141+
}
142+
143+
@override
144+
void dispose() {
145+
_scrollController.dispose();
146+
super.dispose();
147+
}
148+
120149
@override
121150
Widget build(BuildContext context) {
122151
final l10n = AppLocalizationsX(context).l10n;
@@ -129,19 +158,30 @@ class _SourceCategoryRow extends StatelessWidget {
129158
children: [
130159
// Header with category title and "See all" button.
131160
Padding(
132-
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
161+
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
133162
child: Row(
134163
mainAxisAlignment: MainAxisAlignment.spaceBetween,
135164
children: [
136-
Text(
137-
sourceType.l10nPlural(l10n), // This will now work correctly
138-
style: theme.textTheme.titleLarge,
165+
Expanded(
166+
child: Text(
167+
widget.sourceType.l10nPlural(l10n),
168+
style: theme.textTheme.titleLarge,
169+
maxLines: 1,
170+
overflow: TextOverflow.ellipsis,
171+
),
139172
),
140173
TextButton(
174+
style: TextButton.styleFrom(
175+
// Remove default padding to align the icon perfectly
176+
// with the right edge.
177+
padding: const EdgeInsets.symmetric(
178+
vertical: AppSpacing.sm,
179+
),
180+
),
141181
onPressed: () {
142182
context.pushNamed(
143183
Routes.sourceListName,
144-
pathParameters: {'sourceType': sourceType.name},
184+
pathParameters: {'sourceType': widget.sourceType.name},
145185
);
146186
},
147187
child: Row(
@@ -158,13 +198,59 @@ class _SourceCategoryRow extends StatelessWidget {
158198
const SizedBox(height: AppSpacing.sm),
159199
// Horizontally scrolling list of source cards.
160200
SizedBox(
161-
height: 120,
162-
child: ListView.builder(
163-
scrollDirection: Axis.horizontal,
164-
itemCount: sources.length,
165-
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
166-
itemBuilder: (context, index) {
167-
return _SourceCard(source: sources[index]);
201+
height: 140,
202+
child: LayoutBuilder(
203+
builder: (context, constraints) {
204+
final listView = ListView.builder(
205+
controller: _scrollController,
206+
scrollDirection: Axis.horizontal,
207+
itemCount: widget.sources.length,
208+
padding: const EdgeInsets.symmetric(
209+
horizontal: AppSpacing.lg,
210+
),
211+
itemBuilder: (context, index) {
212+
return _SourceCard(source: widget.sources[index]);
213+
},
214+
);
215+
216+
var showStartFade = false;
217+
var showEndFade = false;
218+
if (_scrollController.hasClients &&
219+
_scrollController.position.maxScrollExtent > 0) {
220+
final pixels = _scrollController.position.pixels;
221+
222+
if (pixels > _scrollController.position.minScrollExtent) {
223+
showStartFade = true;
224+
}
225+
if (pixels < _scrollController.position.maxScrollExtent) {
226+
showEndFade = true;
227+
} else {
228+
showEndFade = true;
229+
}
230+
}
231+
232+
final colors = <Color>[
233+
if (showStartFade) Colors.transparent,
234+
theme.scaffoldBackgroundColor,
235+
theme.scaffoldBackgroundColor,
236+
if (_showEndFade) Colors.transparent,
237+
];
238+
239+
final stops = <double>[
240+
if (showStartFade) 0.0,
241+
if (showStartFade) 0.02 else 0.0,
242+
if (showEndFade) 0.98 else 1.0,
243+
if (showEndFade) 1.0,
244+
];
245+
246+
return ShaderMask(
247+
shaderCallback: (bounds) => LinearGradient(
248+
colors: colors,
249+
stops: stops,
250+
).createShader(bounds),
251+
blendMode: BlendMode.dstIn,
252+
child: listView,
253+
);
168254
},
169255
),
170256
),
@@ -185,8 +271,9 @@ class _SourceCard extends StatelessWidget {
185271
final theme = Theme.of(context);
186272

187273
return SizedBox(
188-
width: 120,
274+
width: 150,
189275
child: Card(
276+
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.sm),
190277
clipBehavior: Clip.antiAlias,
191278
child: InkWell(
192279
onTap: () => context.pushNamed(
@@ -196,8 +283,20 @@ class _SourceCard extends StatelessWidget {
196283
child: Column(
197284
mainAxisAlignment: MainAxisAlignment.center,
198285
children: [
199-
const Icon(Icons.source_outlined, size: AppSpacing.xxl),
200-
const SizedBox(height: AppSpacing.sm),
286+
Expanded(
287+
child: Padding(
288+
padding: const EdgeInsets.all(AppSpacing.md),
289+
child: Image.network(
290+
source.logoUrl,
291+
fit: BoxFit.contain,
292+
errorBuilder: (context, error, stackTrace) => Icon(
293+
Icons.source_outlined,
294+
size: AppSpacing.xxl,
295+
color: theme.colorScheme.onSurfaceVariant,
296+
),
297+
),
298+
),
299+
),
201300
Padding(
202301
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs),
203302
child: Text(
@@ -208,6 +307,7 @@ class _SourceCard extends StatelessWidget {
208307
overflow: TextOverflow.ellipsis,
209308
),
210309
),
310+
const SizedBox(height: AppSpacing.sm),
211311
],
212312
),
213313
),

lib/discover/view/source_list_page.dart

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,14 +216,32 @@ class _SourceListTile extends StatelessWidget {
216216
);
217217

218218
return ListTile(
219-
leading: const Icon(Icons.source_outlined),
219+
leading: SizedBox(
220+
width: 40,
221+
height: 40,
222+
child: ClipRRect(
223+
borderRadius: BorderRadius.circular(AppSpacing.sm),
224+
child: Image.network(
225+
source.logoUrl,
226+
fit: BoxFit.cover,
227+
errorBuilder: (context, error, stackTrace) =>
228+
const Icon(Icons.source_outlined),
229+
),
230+
),
231+
),
220232
title: Text(
221233
source.name,
222234
style: theme.textTheme.titleMedium,
223235
maxLines: 1,
224236
overflow: TextOverflow.ellipsis,
225237
),
226-
trailing: TextButton(
238+
trailing: IconButton(
239+
icon: isFollowing
240+
? Icon(Icons.check_circle, color: theme.colorScheme.primary)
241+
: const Icon(Icons.add_circle_outline),
242+
tooltip: isFollowing
243+
? l10n.unfollowSourceTooltip(source.name)
244+
: l10n.followSourceTooltip(source.name),
227245
onPressed: () {
228246
// If the user is unfollowing, always allow it.
229247
if (isFollowing) {
@@ -250,14 +268,15 @@ class _SourceListTile extends StatelessWidget {
250268
}
251269
}
252270
},
253-
child: Text(
254-
isFollowing ? l10n.unfollowButtonText : l10n.followButtonText,
255-
),
256271
),
257272
onTap: () => context.pushNamed(
258273
Routes.entityDetailsName,
259274
pathParameters: {'type': ContentType.source.name, 'id': source.id},
260275
),
276+
contentPadding: const EdgeInsets.symmetric(
277+
horizontal: AppSpacing.lg,
278+
vertical: AppSpacing.sm,
279+
),
261280
);
262281
}
263282
}

lib/entity_details/view/entity_details_page.dart

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,12 @@ class _EntityDetailsViewState extends State<EntityDetailsView> {
208208
},
209209
);
210210

211-
final entityIconUrl = (state.entity is Topic)
212-
? (state.entity! as Topic).iconUrl
213-
: (state.entity is Country)
214-
? (state.entity! as Country).flagUrl
215-
: null;
211+
final entityIconUrl = switch (state.entity) {
212+
final Topic topic => topic.iconUrl,
213+
final Country country => country.flagUrl,
214+
final Source source => source.logoUrl,
215+
_ => null,
216+
};
216217

217218
final Widget appBarTitleWidget = Row(
218219
mainAxisSize: MainAxisSize.min,
@@ -229,21 +230,13 @@ class _EntityDetailsViewState extends State<EntityDetailsView> {
229230
width: AppSpacing.xxl,
230231
height: AppSpacing.xxl,
231232
fit: BoxFit.contain,
232-
errorBuilder: (context, error, stackTrace) =>
233-
const SizedBox(),
233+
errorBuilder: (context, error, stackTrace) => Icon(
234+
appBarIconData,
235+
size: AppSpacing.xxl,
236+
color: colorScheme.onSurface,
237+
),
234238
),
235239
),
236-
)
237-
else if (state.entity is Source && appBarIconData != null)
238-
Padding(
239-
padding: Directionality.of(context) == TextDirection.ltr
240-
? const EdgeInsets.only(right: AppSpacing.md)
241-
: const EdgeInsets.only(left: AppSpacing.md),
242-
child: Icon(
243-
appBarIconData,
244-
size: AppSpacing.xxl,
245-
color: colorScheme.onSurface,
246-
),
247240
),
248241
Expanded(
249242
child: Text(

lib/headline-details/view/headline_details_page.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -417,15 +417,15 @@ class _HeadlineDetailsPageState extends State<HeadlineDetailsPage> {
417417

418418
final colors = <Color>[
419419
if (showStartFade) Colors.transparent,
420-
Colors.black,
421-
Colors.black,
420+
theme.scaffoldBackgroundColor,
421+
theme.scaffoldBackgroundColor,
422422
if (showEndFade) Colors.transparent,
423423
];
424424

425425
final stops = <double>[
426426
if (showStartFade) 0.0,
427-
if (showStartFade) 0.05 else 0.0,
428-
if (showEndFade) 0.95 else 1.0,
427+
if (showStartFade) 0.02 else 0.0,
428+
if (showEndFade) 0.98 else 1.0,
429429
if (showEndFade) 1.0,
430430
];
431431

0 commit comments

Comments
 (0)