✨ Basic track search
This commit is contained in:
parent
81f5a2f5cc
commit
3f41573f00
@ -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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user