From 3f41573f00594e28c1c166b5a639555587ad9873 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 28 Aug 2024 21:07:58 +0800 Subject: [PATCH] :sparkles: Basic track search --- README.md | 8 ++++ lib/router.dart | 6 +++ lib/screens/search/view.dart | 76 ++++++++++++++++++++++++++++++ lib/shells/nav_shell.dart | 3 +- lib/translations/en_us.dart | 1 + lib/translations/zh_cn.dart | 1 + lib/widgets/tracks/track_list.dart | 47 ++++++++++++++++++ 7 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 lib/screens/search/view.dart create mode 100644 lib/widgets/tracks/track_list.dart diff --git a/README.md b/README.md index 3b62112..72fe188 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/router.dart b/lib/router.dart index 0efd5cc..fe25c24 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -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', diff --git a/lib/screens/search/view.dart b/lib/screens/search/view.dart new file mode 100644 index 0000000..457fe27 --- /dev/null +++ b/lib/screens/search/view.dart @@ -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 createState() => _SearchScreenState(); +} + +class _SearchScreenState extends State { + late final SpotifyProvider _spotify = Get.find(); + + bool _isLoading = false; + + String? _searchTerm; + List? _searchResult; + + Future _search(String? term) async { + if (term != null) { + _searchTerm = term.trim(); + } + if (_searchTerm == null) { + return; + } + + setState(() => _isLoading = true); + + final prefs = Get.find().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.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.from(_searchResult!)), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/shells/nav_shell.dart b/lib/shells/nav_shell.dart index 9295f89..1948265 100644 --- a/lib/shells/nav_shell.dart +++ b/lib/shells/nav_shell.dart @@ -24,7 +24,8 @@ class _NavShellState extends State { final List _allDestinations = [ 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 diff --git a/lib/translations/en_us.dart b/lib/translations/en_us.dart index 61f7712..cf597c3 100644 --- a/lib/translations/en_us.dart +++ b/lib/translations/en_us.dart @@ -2,4 +2,5 @@ const i18nEnglish = { 'appName': 'RhythmBox', 'explore': 'Explore', 'settings': 'Settings', + 'search': 'Search', }; diff --git a/lib/translations/zh_cn.dart b/lib/translations/zh_cn.dart index f0841dd..6a3f035 100644 --- a/lib/translations/zh_cn.dart +++ b/lib/translations/zh_cn.dart @@ -2,4 +2,5 @@ const i18nSimplifiedChinese = { 'appName': '韵律盒', 'explore': '探索', 'settings': '设置', + 'search': '搜索', }; diff --git a/lib/widgets/tracks/track_list.dart b/lib/widgets/tracks/track_list.dart new file mode 100644 index 0000000..032437a --- /dev/null +++ b/lib/widgets/tracks/track_list.dart @@ -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 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().load( + [item], + autoPlay: true, + ); + }, + ); + }, + ); + } +}