fix: show search filter bar only after results load

This commit is contained in:
zarzet
2026-02-04 11:48:38 +07:00
parent 9956f051ac
commit f8acd8f3b6
3 changed files with 243 additions and 40 deletions
+1
View File
@@ -29,6 +29,7 @@
### Fixed
- Search filter bar now only appears after results load, not during loading
- MP3/Ogg metadata parsing (ID3v2 extended headers, Ogg packet reassembly)
- Library scan metadata (ISRC, disc number, release date)
- Cover cache robustness (size + mtime cache key)
+2 -2
View File
@@ -631,8 +631,8 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
),
),
// Search filter bar (only shown when has search results or loading search)
if (searchFilters.isNotEmpty && (hasActualResults || isLoading))
// Search filter bar (only shown when has search results)
if (searchFilters.isNotEmpty && hasActualResults)
SliverToBoxAdapter(
child: _buildSearchFilterBar(
searchFilters,
+240 -38
View File
@@ -63,16 +63,16 @@ class _MainShellState extends ConsumerState<MainShell> {
void _handleSharedUrl(String url) {
Navigator.of(context).popUntil((route) => route.isFirst);
if (_currentIndex != 0) {
_onNavTap(0);
}
ref.read(trackProvider.notifier).fetchFromUrl(url);
ref.read(settingsProvider.notifier).setHasSearchedBefore();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.loadingSharedLink)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(context.l10n.loadingSharedLink)));
}
}
@@ -83,7 +83,9 @@ class _MainShellState extends ConsumerState<MainShell> {
final settings = ref.read(settingsProvider);
if (!settings.checkForUpdates) return;
final updateInfo = await UpdateChecker.checkForUpdate(channel: settings.updateChannel);
final updateInfo = await UpdateChecker.checkForUpdate(
channel: settings.updateChannel,
);
if (updateInfo != null && mounted) {
showUpdateDialog(
context,
@@ -104,6 +106,7 @@ class _MainShellState extends ConsumerState<MainShell> {
void _onNavTap(int index) {
if (_currentIndex != index) {
HapticFeedback.selectionClick();
setState(() => _currentIndex = index);
_pageController.animateToPage(
index,
@@ -122,35 +125,38 @@ class _MainShellState extends ConsumerState<MainShell> {
void _handleBackPress() {
final trackState = ref.read(trackProvider);
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
if (isKeyboardVisible) {
FocusManager.instance.primaryFocus?.unfocus();
return;
}
if (_currentIndex == 0 && trackState.isShowingRecentAccess) {
ref.read(trackProvider.notifier).setShowingRecentAccess(false);
FocusManager.instance.primaryFocus?.unfocus();
return;
}
if (_currentIndex == 0 && !trackState.isLoading && (trackState.hasSearchText || trackState.hasContent)) {
if (_currentIndex == 0 &&
!trackState.isLoading &&
(trackState.hasSearchText || trackState.hasContent)) {
ref.read(trackProvider.notifier).clear();
return;
}
if (_currentIndex != 0) {
_onNavTap(0);
return;
}
if (trackState.isLoading) {
return;
}
final now = DateTime.now();
if (_lastBackPress != null && now.difference(_lastBackPress!) < const Duration(seconds: 2)) {
if (_lastBackPress != null &&
now.difference(_lastBackPress!) < const Duration(seconds: 2)) {
SystemNavigator.pop();
} else {
_lastBackPress = now;
@@ -166,19 +172,26 @@ class _MainShellState extends ConsumerState<MainShell> {
@override
Widget build(BuildContext context) {
final queueState = ref.watch(downloadQueueProvider.select((s) => s.queuedCount));
final queueState = ref.watch(
downloadQueueProvider.select((s) => s.queuedCount),
);
final trackState = ref.watch(trackProvider);
final showStore = ref.watch(settingsProvider.select((s) => s.showExtensionStore));
final storeUpdatesCount = ref.watch(storeProvider.select((s) => s.updatesAvailableCount));
final showStore = ref.watch(
settingsProvider.select((s) => s.showExtensionStore),
);
final storeUpdatesCount = ref.watch(
storeProvider.select((s) => s.updatesAvailableCount),
);
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
final canPop = _currentIndex == 0 &&
!trackState.hasSearchText &&
!trackState.hasContent &&
!trackState.isLoading &&
!trackState.isShowingRecentAccess &&
!isKeyboardVisible;
final canPop =
_currentIndex == 0 &&
!trackState.hasSearchText &&
!trackState.hasContent &&
!trackState.isLoading &&
!trackState.isShowingRecentAccess &&
!isKeyboardVisible;
final tabs = <Widget>[
const HomeTab(),
@@ -195,7 +208,7 @@ class _MainShellState extends ConsumerState<MainShell> {
final destinations = <NavigationDestination>[
NavigationDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home),
selectedIcon: BouncingIcon(child: const Icon(Icons.home)),
label: l10n.navHome,
),
NavigationDestination(
@@ -204,10 +217,12 @@ class _MainShellState extends ConsumerState<MainShell> {
label: Text('$queueState'),
child: const Icon(Icons.library_music_outlined),
),
selectedIcon: Badge(
isLabelVisible: queueState > 0,
label: Text('$queueState'),
child: const Icon(Icons.library_music),
selectedIcon: SlidingIcon(
child: Badge(
isLabelVisible: queueState > 0,
label: Text('$queueState'),
child: const Icon(Icons.library_music),
),
),
label: l10n.navLibrary,
),
@@ -218,16 +233,18 @@ class _MainShellState extends ConsumerState<MainShell> {
label: Text('$storeUpdatesCount'),
child: const Icon(Icons.store_outlined),
),
selectedIcon: Badge(
isLabelVisible: storeUpdatesCount > 0,
label: Text('$storeUpdatesCount'),
child: const Icon(Icons.store),
selectedIcon: SwingIcon(
child: Badge(
isLabelVisible: storeUpdatesCount > 0,
label: Text('$storeUpdatesCount'),
child: const Icon(Icons.store),
),
),
label: l10n.navStore,
),
NavigationDestination(
icon: const Icon(Icons.settings_outlined),
selectedIcon: const Icon(Icons.settings),
selectedIcon: SpinIcon(child: const Icon(Icons.settings)),
label: l10n.navSettings,
),
];
@@ -248,7 +265,7 @@ class _MainShellState extends ConsumerState<MainShell> {
if (didPop) {
return;
}
_handleBackPress();
},
child: Scaffold(
@@ -261,13 +278,198 @@ class _MainShellState extends ConsumerState<MainShell> {
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex.clamp(0, maxIndex),
onDestinationSelected: _onNavTap,
animationDuration: const Duration(milliseconds: 200),
animationDuration: const Duration(milliseconds: 500),
backgroundColor: Theme.of(context).brightness == Brightness.dark
? Color.alphaBlend(Colors.white.withValues(alpha: 0.05), Theme.of(context).colorScheme.surface)
: Color.alphaBlend(Colors.black.withValues(alpha: 0.03), Theme.of(context).colorScheme.surface),
? Color.alphaBlend(
Colors.white.withValues(alpha: 0.05),
Theme.of(context).colorScheme.surface,
)
: Color.alphaBlend(
Colors.black.withValues(alpha: 0.03),
Theme.of(context).colorScheme.surface,
),
destinations: destinations,
),
),
);
}
}
class BouncingIcon extends StatefulWidget {
final Widget child;
const BouncingIcon({super.key, required this.child});
@override
State<BouncingIcon> createState() => _BouncingIconState();
}
class _BouncingIconState extends State<BouncingIcon>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.1,
end: 1.0,
).animate(CurvedAnimation(parent: _controller, curve: Curves.elasticOut));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(scale: _scaleAnimation, child: widget.child);
}
}
class SlidingIcon extends StatefulWidget {
final Widget child;
const SlidingIcon({super.key, required this.child});
@override
State<SlidingIcon> createState() => _SlidingIconState();
}
class _SlidingIconState extends State<SlidingIcon>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _offsetAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 350),
vsync: this,
);
_offsetAnimation = Tween<Offset>(
begin: const Offset(0, 0.5),
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(position: _offsetAnimation, child: widget.child),
);
}
}
class SwingIcon extends StatefulWidget {
final Widget child;
const SwingIcon({super.key, required this.child});
@override
State<SwingIcon> createState() => _SwingIconState();
}
class _SwingIconState extends State<SwingIcon>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
// Create a swinging motion (like a pendulum/sign)
_rotationAnimation = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 0.0, end: -0.2), weight: 20),
TweenSequenceItem(tween: Tween(begin: -0.2, end: 0.15), weight: 20),
TweenSequenceItem(tween: Tween(begin: 0.15, end: -0.1), weight: 20),
TweenSequenceItem(tween: Tween(begin: -0.1, end: 0.05), weight: 20),
TweenSequenceItem(tween: Tween(begin: 0.05, end: 0.0), weight: 20),
]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value,
alignment: Alignment.topCenter,
child: child,
);
},
child: widget.child,
);
}
}
class SpinIcon extends StatefulWidget {
final Widget child;
const SpinIcon({super.key, required this.child});
@override
State<SpinIcon> createState() => _SpinIconState();
}
class _SpinIconState extends State<SpinIcon>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 0.5,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RotationTransition(turns: _rotationAnimation, child: widget.child);
}
}