@@ -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 ),
0 commit comments