✨ 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).
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
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/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/shells/nav_shell.dart';
 | 
			
		||||
 | 
			
		||||
@@ -17,6 +18,11 @@ final router = GoRouter(routes: [
 | 
			
		||||
        name: 'explore',
 | 
			
		||||
        builder: (context, state) => const ExploreScreen(),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/search',
 | 
			
		||||
        name: 'search',
 | 
			
		||||
        builder: (context, state) => const SearchScreen(),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/playlist/:id',
 | 
			
		||||
        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>[
 | 
			
		||||
    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
 | 
			
		||||
 
 | 
			
		||||
@@ -2,4 +2,5 @@ const i18nEnglish = {
 | 
			
		||||
  'appName': 'RhythmBox',
 | 
			
		||||
  'explore': 'Explore',
 | 
			
		||||
  'settings': 'Settings',
 | 
			
		||||
  'search': 'Search',
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -2,4 +2,5 @@ const i18nSimplifiedChinese = {
 | 
			
		||||
  'appName': '韵律盒',
 | 
			
		||||
  'explore': '探索',
 | 
			
		||||
  '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