✨ Basic track search
This commit is contained in:
		@@ -5,6 +5,14 @@ Yet another spotify third-party client. Support multi-platform because built wit
 | 
				
			|||||||
This project is inspired by and taken supported by [spotube](https://spotube.krtirtho.dev).
 | 
					This project is inspired by and taken supported by [spotube](https://spotube.krtirtho.dev).
 | 
				
			||||||
Their original app is good enough. But I just want to redesign the user interface and make it ready add to more features and more backend support.
 | 
					Their original app is good enough. But I just want to redesign the user interface and make it ready add to more features and more backend support.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Roadmap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [x] Playing music
 | 
				
			||||||
 | 
					    - [ ] Add netease music as source
 | 
				
			||||||
 | 
					- [x] Re-design user interface
 | 
				
			||||||
 | 
					    - [x] Simplified UI and UX
 | 
				
			||||||
 | 
					    - [ ] Support for large screen device
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## License
 | 
					## License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This project is open-sourced under APGLv3 license. The original spotube project is open-sourced under license BSD-Clause4 and copyright by Kingkor Roy Tirtho.
 | 
					This project is open-sourced under APGLv3 license. The original spotube project is open-sourced under license BSD-Clause4 and copyright by Kingkor Roy Tirtho.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ import 'package:rhythm_box/screens/explore.dart';
 | 
				
			|||||||
import 'package:rhythm_box/screens/player/lyrics.dart';
 | 
					import 'package:rhythm_box/screens/player/lyrics.dart';
 | 
				
			||||||
import 'package:rhythm_box/screens/player/view.dart';
 | 
					import 'package:rhythm_box/screens/player/view.dart';
 | 
				
			||||||
import 'package:rhythm_box/screens/playlist/view.dart';
 | 
					import 'package:rhythm_box/screens/playlist/view.dart';
 | 
				
			||||||
 | 
					import 'package:rhythm_box/screens/search/view.dart';
 | 
				
			||||||
import 'package:rhythm_box/screens/settings.dart';
 | 
					import 'package:rhythm_box/screens/settings.dart';
 | 
				
			||||||
import 'package:rhythm_box/shells/nav_shell.dart';
 | 
					import 'package:rhythm_box/shells/nav_shell.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -17,6 +18,11 @@ final router = GoRouter(routes: [
 | 
				
			|||||||
        name: 'explore',
 | 
					        name: 'explore',
 | 
				
			||||||
        builder: (context, state) => const ExploreScreen(),
 | 
					        builder: (context, state) => const ExploreScreen(),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 | 
					      GoRoute(
 | 
				
			||||||
 | 
					        path: '/search',
 | 
				
			||||||
 | 
					        name: 'search',
 | 
				
			||||||
 | 
					        builder: (context, state) => const SearchScreen(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/playlist/:id',
 | 
					        path: '/playlist/:id',
 | 
				
			||||||
        name: 'playlistView',
 | 
					        name: 'playlistView',
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										76
									
								
								lib/screens/search/view.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								lib/screens/search/view.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					import 'package:rhythm_box/providers/spotify.dart';
 | 
				
			||||||
 | 
					import 'package:rhythm_box/providers/user_preferences.dart';
 | 
				
			||||||
 | 
					import 'package:rhythm_box/widgets/tracks/track_list.dart';
 | 
				
			||||||
 | 
					import 'package:spotify/spotify.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SearchScreen extends StatefulWidget {
 | 
				
			||||||
 | 
					  const SearchScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<SearchScreen> createState() => _SearchScreenState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _SearchScreenState extends State<SearchScreen> {
 | 
				
			||||||
 | 
					  late final SpotifyProvider _spotify = Get.find();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _isLoading = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String? _searchTerm;
 | 
				
			||||||
 | 
					  List<dynamic>? _searchResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _search(String? term) async {
 | 
				
			||||||
 | 
					    if (term != null) {
 | 
				
			||||||
 | 
					      _searchTerm = term.trim();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (_searchTerm == null) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState(() => _isLoading = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final prefs = Get.find<UserPreferencesProvider>().state.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _searchResult = (await _spotify.api.search
 | 
				
			||||||
 | 
					            .get(_searchTerm!, types: [SearchType.track], market: prefs.market)
 | 
				
			||||||
 | 
					            .getPage(20))
 | 
				
			||||||
 | 
					        .mapMany((x) => x.items)
 | 
				
			||||||
 | 
					        .toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState(() => _isLoading = false);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Material(
 | 
				
			||||||
 | 
					      color: Theme.of(context).colorScheme.surface,
 | 
				
			||||||
 | 
					      child: SafeArea(
 | 
				
			||||||
 | 
					        child: Column(
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            SearchBar(
 | 
				
			||||||
 | 
					              padding: const WidgetStatePropertyAll<EdgeInsets>(
 | 
				
			||||||
 | 
					                EdgeInsets.symmetric(horizontal: 16.0),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              onSubmitted: (value) {
 | 
				
			||||||
 | 
					                if (_isLoading) return;
 | 
				
			||||||
 | 
					                _search(value);
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              leading: const Icon(Icons.search),
 | 
				
			||||||
 | 
					              onTapOutside: (_) =>
 | 
				
			||||||
 | 
					                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					            ).paddingSymmetric(horizontal: 24, vertical: 8),
 | 
				
			||||||
 | 
					            Expanded(
 | 
				
			||||||
 | 
					              child: CustomScrollView(
 | 
				
			||||||
 | 
					                slivers: [
 | 
				
			||||||
 | 
					                  if (_searchResult != null)
 | 
				
			||||||
 | 
					                    TrackSliverList(tracks: List<Track>.from(_searchResult!)),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -24,7 +24,8 @@ class _NavShellState extends State<NavShell> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  final List<Destination> _allDestinations = <Destination>[
 | 
					  final List<Destination> _allDestinations = <Destination>[
 | 
				
			||||||
    Destination('explore'.tr, 'explore', Icons.explore),
 | 
					    Destination('explore'.tr, 'explore', Icons.explore),
 | 
				
			||||||
    Destination('settings'.tr, 'settings', Icons.settings)
 | 
					    Destination('search'.tr, 'search', Icons.search),
 | 
				
			||||||
 | 
					    Destination('settings'.tr, 'settings', Icons.settings),
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,4 +2,5 @@ const i18nEnglish = {
 | 
				
			|||||||
  'appName': 'RhythmBox',
 | 
					  'appName': 'RhythmBox',
 | 
				
			||||||
  'explore': 'Explore',
 | 
					  'explore': 'Explore',
 | 
				
			||||||
  'settings': 'Settings',
 | 
					  'settings': 'Settings',
 | 
				
			||||||
 | 
					  'search': 'Search',
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,4 +2,5 @@ const i18nSimplifiedChinese = {
 | 
				
			|||||||
  'appName': '韵律盒',
 | 
					  'appName': '韵律盒',
 | 
				
			||||||
  'explore': '探索',
 | 
					  'explore': '探索',
 | 
				
			||||||
  'settings': '设置',
 | 
					  'settings': '设置',
 | 
				
			||||||
 | 
					  'search': '搜索',
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										47
									
								
								lib/widgets/tracks/track_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/widgets/tracks/track_list.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					import 'package:rhythm_box/providers/audio_player.dart';
 | 
				
			||||||
 | 
					import 'package:rhythm_box/widgets/auto_cache_image.dart';
 | 
				
			||||||
 | 
					import 'package:spotify/spotify.dart';
 | 
				
			||||||
 | 
					import 'package:rhythm_box/services/artist.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TrackSliverList extends StatelessWidget {
 | 
				
			||||||
 | 
					  final List<Track> tracks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const TrackSliverList({
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					    required this.tracks,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return SliverList.builder(
 | 
				
			||||||
 | 
					      itemCount: tracks.length,
 | 
				
			||||||
 | 
					      itemBuilder: (context, idx) {
 | 
				
			||||||
 | 
					        final item = tracks[idx];
 | 
				
			||||||
 | 
					        return ListTile(
 | 
				
			||||||
 | 
					          leading: ClipRRect(
 | 
				
			||||||
 | 
					            borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					            child: AutoCacheImage(
 | 
				
			||||||
 | 
					              item.album!.images!.first.url!,
 | 
				
			||||||
 | 
					              width: 64.0,
 | 
				
			||||||
 | 
					              height: 64.0,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          title: Text(item.name ?? 'Loading...'),
 | 
				
			||||||
 | 
					          subtitle: Text(
 | 
				
			||||||
 | 
					            item.artists?.asString() ?? 'Please stand by...',
 | 
				
			||||||
 | 
					            maxLines: 1,
 | 
				
			||||||
 | 
					            overflow: TextOverflow.ellipsis,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          onTap: () {
 | 
				
			||||||
 | 
					            Get.find<AudioPlayerProvider>().load(
 | 
				
			||||||
 | 
					              [item],
 | 
				
			||||||
 | 
					              autoPlay: true,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user