feat(library): show a trailing fade on the filter chips row

Extend ScrollEdgeFade with a horizontal axis and wrap the All/Albums/
Singles/Playlists chips so a right-edge fade signals the row scrolls
when chips overflow the screen.
This commit is contained in:
zarzet
2026-07-27 02:34:13 +07:00
parent ef3e019a3a
commit 3b901461f6
3 changed files with 73 additions and 52 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ class AppInfo {
static String get displayVersion => kDebugMode ? 'Internal' : version;
static const String appName = 'SpotiFLAC Mobile';
static const String copyright = '© 2026 SpotiFLAC';
static const String copyright = '© 2026 Zarz Eleutherius';
static const String mobileAuthor = 'zarzet';
static const String originalAuthor = 'afkarxyz';
+46 -40
View File
@@ -47,6 +47,7 @@ import 'package:spotiflac_android/widgets/animation_utils.dart';
import 'package:spotiflac_android/widgets/selection_action_button.dart';
import 'package:spotiflac_android/widgets/selection_bottom_bar.dart';
import 'package:spotiflac_android/widgets/smoothed_progress.dart';
import 'package:spotiflac_android/widgets/scroll_edge_fade.dart';
part 'queue_tab_helpers.dart';
part 'queue_tab_widgets.dart';
@@ -1472,46 +1473,51 @@ class _QueueTabState extends ConsumerState<QueueTab> {
filteredAlbumCount = queueCounts.albumCount;
filteredSingleCount = queueCounts.singleTrackCount;
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_FilterChip(
label: context.l10n.historyFilterAll,
count: filteredAllCount,
isSelected: historyFilterMode == 'all',
onTap: () {
_animateToFilterPage(0);
},
),
const SizedBox(width: 8),
_FilterChip(
label: context.l10n.historyFilterAlbums,
count: filteredAlbumCount,
isSelected: historyFilterMode == 'albums',
onTap: () {
_animateToFilterPage(1);
},
),
const SizedBox(width: 8),
_FilterChip(
label: context.l10n.historyFilterSingles,
count: filteredSingleCount,
isSelected: historyFilterMode == 'singles',
onTap: () {
_animateToFilterPage(2);
},
),
const SizedBox(width: 8),
_FilterChip(
label: context.l10n.searchPlaylists,
count: collectionState.playlists.length,
isSelected: historyFilterMode == 'playlists',
onTap: () {
_animateToFilterPage(3);
},
),
],
return ScrollEdgeFade(
axis: Axis.horizontal,
size: 48,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_FilterChip(
label: context.l10n.historyFilterAll,
count: filteredAllCount,
isSelected: historyFilterMode == 'all',
onTap: () {
_animateToFilterPage(0);
},
),
const SizedBox(width: 8),
_FilterChip(
label: context.l10n.historyFilterAlbums,
count: filteredAlbumCount,
isSelected: historyFilterMode == 'albums',
onTap: () {
_animateToFilterPage(1);
},
),
const SizedBox(width: 8),
_FilterChip(
label: context.l10n.historyFilterSingles,
count: filteredSingleCount,
isSelected: historyFilterMode == 'singles',
onTap: () {
_animateToFilterPage(2);
},
),
const SizedBox(width: 8),
_FilterChip(
label: context.l10n.searchPlaylists,
count: collectionState.playlists.length,
isSelected:
historyFilterMode == 'playlists',
onTap: () {
_animateToFilterPage(3);
},
),
],
),
),
);
},
+26 -11
View File
@@ -1,12 +1,20 @@
import 'package:flutter/material.dart';
/// Overlays a bottom gradient on a vertical scrollable while more content
/// remains below, signalling that the view can scroll.
/// Overlays a gradient on the trailing edge of a scrollable while more
/// content remains beyond it, signalling that the view can scroll.
class ScrollEdgeFade extends StatefulWidget {
final Widget child;
final double height;
final Axis axis;
const ScrollEdgeFade({super.key, required this.child, this.height = 96});
/// Thickness of the fade: height for vertical, width for horizontal.
final double size;
const ScrollEdgeFade({
super.key,
required this.child,
this.axis = Axis.vertical,
this.size = 96,
});
@override
State<ScrollEdgeFade> createState() => _ScrollEdgeFadeState();
@@ -16,7 +24,7 @@ class _ScrollEdgeFadeState extends State<ScrollEdgeFade> {
bool _hasMore = false;
bool _onMetrics(ScrollMetrics metrics) {
if (metrics.axis == Axis.vertical) {
if (metrics.axis == widget.axis) {
final hasMore = metrics.extentAfter > 8;
if (hasMore != _hasMore) setState(() => _hasMore = hasMore);
}
@@ -26,6 +34,7 @@ class _ScrollEdgeFadeState extends State<ScrollEdgeFade> {
@override
Widget build(BuildContext context) {
final background = Theme.of(context).scaffoldBackgroundColor;
final horizontal = widget.axis == Axis.horizontal;
return Stack(
children: [
NotificationListener<ScrollMetricsNotification>(
@@ -35,20 +44,26 @@ class _ScrollEdgeFadeState extends State<ScrollEdgeFade> {
child: widget.child,
),
),
Positioned(
left: 0,
right: 0,
PositionedDirectional(
start: horizontal ? null : 0,
end: 0,
top: horizontal ? 0 : null,
bottom: 0,
child: IgnorePointer(
child: AnimatedOpacity(
opacity: _hasMore ? 1 : 0,
duration: const Duration(milliseconds: 200),
child: Container(
height: widget.height,
width: horizontal ? widget.size : null,
height: horizontal ? null : widget.size,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
begin: horizontal
? AlignmentDirectional.centerStart
: Alignment.topCenter,
end: horizontal
? AlignmentDirectional.centerEnd
: Alignment.bottomCenter,
colors: [background.withValues(alpha: 0), background],
),
),