mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-12 02:37:18 +02:00
release: v1.6.0 - Live search, quality picker, dependency updates
This commit is contained in:
1 parent
39bcc2c547
commit
bd9b527161
8 files changed
+181
-187
No files matched your search
+112
-123
@@ -38,30 +38,24 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Handle back button - returns true if handled, false to let system handle
|
||||
bool _handleBack() {
|
||||
final trackState = ref.read(trackProvider);
|
||||
|
||||
// If we have previous state, go back to it
|
||||
if (trackState.canGoBack) {
|
||||
ref.read(trackProvider.notifier).goBack();
|
||||
return true;
|
||||
/// Called when trackState changes - used to sync search bar with state
|
||||
void _onTrackStateChanged(TrackState? previous, TrackState next) {
|
||||
// If state was cleared (no content, no search text, not loading), clear the search bar
|
||||
if (previous != null &&
|
||||
!next.hasContent &&
|
||||
!next.hasSearchText &&
|
||||
!next.isLoading &&
|
||||
_urlController.text.isNotEmpty) {
|
||||
_urlController.clear();
|
||||
setState(() => _isTyping = false);
|
||||
}
|
||||
|
||||
// If we're in results view but no previous state, clear and go to idle
|
||||
if (_isTyping || trackState.hasContent) {
|
||||
_clearAndRefresh();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Let system handle (exit app)
|
||||
return false;
|
||||
}
|
||||
|
||||
void _onSearchChanged() {
|
||||
} void _onSearchChanged() {
|
||||
final text = _urlController.text.trim();
|
||||
final wasFocused = _searchFocusNode.hasFocus;
|
||||
|
||||
// Update search text state for MainShell back button handling
|
||||
ref.read(trackProvider.notifier).setSearchText(text.isNotEmpty);
|
||||
|
||||
// Update typing state immediately for UI transition
|
||||
if (text.isNotEmpty && !_isTyping) {
|
||||
setState(() => _isTyping = true);
|
||||
@@ -211,126 +205,121 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
// Listen for state changes to sync search bar
|
||||
ref.listen<TrackState>(trackProvider, _onTrackStateChanged);
|
||||
|
||||
final trackState = ref.watch(trackProvider);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final hasResults = _hasResults;
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final historyItems = ref.watch(downloadHistoryProvider).items;
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
if (!_handleBack()) {
|
||||
Navigator.of(context).maybePop();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
// App Bar - always present
|
||||
SliverAppBar(
|
||||
expandedHeight: 130,
|
||||
collapsedHeight: kToolbarHeight,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
backgroundColor: colorScheme.surface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
automaticallyImplyLeading: false,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
expandedTitleScale: 1.3,
|
||||
titlePadding: const EdgeInsets.only(left: 24, bottom: 16),
|
||||
title: Text(
|
||||
'Search',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
// App Bar - always present
|
||||
SliverAppBar(
|
||||
expandedHeight: 130,
|
||||
collapsedHeight: kToolbarHeight,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
backgroundColor: colorScheme.surface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
automaticallyImplyLeading: false,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
expandedTitleScale: 1.3,
|
||||
titlePadding: const EdgeInsets.only(left: 24, bottom: 16),
|
||||
title: Text(
|
||||
'Search',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Idle content (logo, title) - always in tree, animated size
|
||||
SliverToBoxAdapter(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOut,
|
||||
child: hasResults
|
||||
? const SizedBox.shrink()
|
||||
: Column(
|
||||
children: [
|
||||
SizedBox(height: screenHeight * 0.06),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.music_note, size: 48, color: colorScheme.primary),
|
||||
),
|
||||
|
||||
// Idle content (logo, title) - always in tree, animated size
|
||||
SliverToBoxAdapter(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOut,
|
||||
child: hasResults
|
||||
? const SizedBox.shrink()
|
||||
: Column(
|
||||
children: [
|
||||
SizedBox(height: screenHeight * 0.06),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Search Music',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
child: Icon(Icons.music_note, size: 48, color: colorScheme.primary),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Search Music',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Paste a Spotify link or search by name',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Paste a Spotify link or search by name',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Search bar - always present at same position in tree
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, hasResults ? 8 : 32, 16, hasResults ? 8 : 16),
|
||||
child: _buildSearchBar(colorScheme),
|
||||
),
|
||||
),
|
||||
|
||||
// Search bar - always present at same position in tree
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, hasResults ? 8 : 32, 16, hasResults ? 8 : 16),
|
||||
child: _buildSearchBar(colorScheme),
|
||||
),
|
||||
|
||||
// Idle content below search bar - always in tree
|
||||
SliverToBoxAdapter(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOut,
|
||||
child: hasResults
|
||||
? const SizedBox.shrink()
|
||||
: Column(
|
||||
children: [
|
||||
if (!ref.watch(settingsProvider).hasSearchedBefore)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
'Supports: Track, Album, Playlist, Artist URLs',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
|
||||
// Idle content below search bar - always in tree
|
||||
SliverToBoxAdapter(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOut,
|
||||
child: hasResults
|
||||
? const SizedBox.shrink()
|
||||
: Column(
|
||||
children: [
|
||||
if (!ref.watch(settingsProvider).hasSearchedBefore)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
'Supports: Track, Album, Playlist, Artist URLs',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (historyItems.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 24),
|
||||
child: _buildRecentDownloads(historyItems, colorScheme),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (historyItems.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 24),
|
||||
child: _buildRecentDownloads(historyItems, colorScheme),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Results content - always in tree
|
||||
..._buildResultsContent(trackState, colorScheme, hasResults),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Results content - always in tree
|
||||
..._buildResultsContent(trackState, colorScheme, hasResults),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,6 +150,12 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
return;
|
||||
}
|
||||
|
||||
// If on Search tab and has text in search bar or has content, clear it
|
||||
if (_currentIndex == 0 && (trackState.hasSearchText || trackState.hasContent || trackState.isLoading)) {
|
||||
ref.read(trackProvider.notifier).clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// If not on Search tab, go to Search tab first
|
||||
if (_currentIndex != 0) {
|
||||
_onNavTap(0);
|
||||
|
||||
@@ -938,9 +938,11 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
await Share.shareXFiles(
|
||||
[XFile(item.filePath)],
|
||||
text: '${item.trackName} - ${item.artistName}',
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(
|
||||
files: [XFile(item.filePath)],
|
||||
text: '${item.trackName} - ${item.artistName}',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user