implement playFromSearch and playFromUri for GAssistant

change package name to deezer.android.app for the same reason (will probably be changed back if we can get freezer to work even without this "hack")
implement topResult in search screen
This commit is contained in:
Pato05 2024-02-18 16:05:35 +01:00
parent 14ff02b65c
commit 6816bdc112
No known key found for this signature in database
GPG key ID: ED4C6F9C3D574FB6
9 changed files with 642 additions and 277 deletions

View file

@ -40,8 +40,8 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "f.f.freezer"
// Workaround to make Google assistant work with Freezer
applicationId "deezer.android.app" // "f.f.freezer"
minSdkVersion 21
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
@ -64,7 +64,7 @@ android {
minifyEnabled true
}
debug {
applicationIdSuffix ".debug"
// applicationIdSuffix ".debug"
shrinkResources false
minifyEnabled false
}

View file

@ -40,7 +40,7 @@
android:label="Freezer"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"
tools:targetApi="m">
android:targetSandboxVersion="2">
<service
android:name=".DownloadService"
@ -88,9 +88,12 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.APP_MUSIC"/>
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
</intent-filter>
<!-- App links -->

1
devtools_options.yaml Normal file
View file

@ -0,0 +1 @@
extensions:

View file

@ -235,23 +235,28 @@ class DeezerAPI {
}
//URL/Link parser
Future<DeezerLinkResponse?> parseLink(String url) async {
Uri uri = Uri.parse(url);
//https://www.deezer.com/NOTHING_OR_COUNTRY/TYPE/ID
Future<DeezerLinkResponse?> parseLink(Uri uri) async {
//https://www.deezer.com/NOTHING_OR_COUNTRY/TYPE/ID[/radio?autoplay=true]
if (uri.host == 'www.deezer.com' || uri.host == 'deezer.com') {
if (uri.pathSegments.length < 2) return null;
DeezerLinkType? type = DeezerLinkResponse.typeFromString(
uri.pathSegments[uri.pathSegments.length - 2]);
if (uri.pathSegments[uri.pathSegments.length - 1] == 'radio') {
return DeezerLinkResponse(
type: DeezerLinkResponse.typeFromString(
uri.pathSegments[uri.pathSegments.length - 3]),
id: uri.pathSegments[uri.pathSegments.length - 2]);
}
return DeezerLinkResponse(
type: type, id: uri.pathSegments[uri.pathSegments.length - 1]);
type: DeezerLinkResponse.typeFromString(
uri.pathSegments[uri.pathSegments.length - 2]),
id: uri.pathSegments[uri.pathSegments.length - 1]);
}
//Share URL
if (uri.host == 'deezer.page.link' || uri.host == 'www.deezer.page.link') {
http.BaseRequest request = http.Request('HEAD', Uri.parse(url));
http.BaseRequest request = http.Request('HEAD', uri);
request.followRedirects = false;
http.StreamedResponse response = await request.send();
String newUrl = response.headers['location']!;
return parseLink(newUrl);
return parseLink(Uri.parse(newUrl));
}
//Spotify
if (uri.host == 'open.spotify.com') {
@ -333,6 +338,7 @@ class DeezerAPI {
Future<SearchResults> search(String? query) async {
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch',
params: {'nb': 128, 'query': query, 'start': 0});
print(data['results']['TOP_RESULT']);
return SearchResults.fromPrivateJson(data['results']);
}

View file

@ -22,6 +22,22 @@ part 'definitions.g.dart';
abstract class DeezerMediaItem {
String? get id;
static DeezerMediaItem? fromDeezerObject(Map data) {
switch (data['__TYPE__']!) {
case 'song':
return Track.fromPrivateJson(data);
case 'artist':
return Artist.fromPrivateJson(data);
case 'playlist':
return Playlist.fromPrivateJson(data);
case 'album':
return Album.fromPrivateJson(data);
default:
print('UNKNOWN MEDIA ITEM TYPE ${data['__TYPE__']}');
return null;
}
}
}
@HiveType(typeId: 5)
@ -662,6 +678,7 @@ class DeezerImageDetails extends ImageDetails {
}
class SearchResults {
List<DeezerMediaItem>? topResult;
List<Track>? tracks;
List<Album>? albums;
List<Artist>? artists;
@ -670,7 +687,8 @@ class SearchResults {
List<ShowEpisode>? episodes;
SearchResults(
{this.tracks,
{this.topResult,
this.tracks,
this.albums,
this.artists,
this.playlists,
@ -689,6 +707,11 @@ class SearchResults {
factory SearchResults.fromPrivateJson(Map<dynamic, dynamic> json) =>
SearchResults(
topResult: (json['TOP_RESULT'] as List?)
?.map<DeezerMediaItem?>(
(e) => DeezerMediaItem.fromDeezerObject(e as Map))
.whereType<DeezerMediaItem>()
.toList(growable: false),
tracks: json['TRACK']['data']
.map<Track>((dynamic data) => Track.fromPrivateJson(data))
.toList(),

View file

@ -492,15 +492,15 @@ class AudioPlayerTask extends BaseAudioHandler {
return const MediaControl(
androidIcon: 'drawable/ic_heart',
label: 'unfavorite',
action: MediaAction.custom);
action: MediaAction.custom,
customAction: CustomMediaAction(name: 'favorite'));
}
return const MediaControl(
androidIcon: 'drawable/ic_heart_outline',
label: 'favourite',
action: MediaAction
.custom, // this acts as favourite/unfavourite as it's not already used.
);
androidIcon: 'drawable/ic_heart_outline',
label: 'favourite',
action: MediaAction.custom,
customAction: CustomMediaAction(name: 'favorite'));
}
//Update state on all clients
@ -535,7 +535,6 @@ class AudioPlayerTask extends BaseAudioHandler {
systemActions: !currentMediaItemIsShow
? {
MediaAction.seek,
MediaAction.seekBackward,
if (queue.hasValue && _queueIndex != queue.value.length - 1)
MediaAction.skipToNext,
if (_queueIndex != 0) MediaAction.skipToPrevious,
@ -552,6 +551,12 @@ class AudioPlayerTask extends BaseAudioHandler {
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
queueIndex: _queueIndex,
androidCompactActionIndices:
!currentMediaItemIsShow ? const [1, 2, 3] : const [0, 1, 2],
repeatMode: _repeatMode,
shuffleMode: _originalQueue == null
? AudioServiceShuffleMode.none
: AudioServiceShuffleMode.all,
));
}
@ -863,6 +868,114 @@ class AudioPlayerTask extends BaseAudioHandler {
await skipToQueueItem(queue.value.indexWhere((item) => item.id == mediaId));
}
@override
Future<List<MediaItem>> search(String query, [Map<String, dynamic>? extras]) {
_logger.fine('search($query, $extras)');
return Future.value([]);
}
@override
Future<void> playFromSearch(String query,
[Map<String, dynamic>? extras]) async {
_logger.fine('playFromSearch($query, $extras)');
// play <query> from Freezer
final res = await _deezerAPI.search(query);
print(res);
if (res.topResult != null && res.topResult!.isNotEmpty) {
_logger.fine('playing from top result: ${jsonEncode(res.topResult![0])}');
final top = res.topResult![0];
switch (top) {
case final Track track:
unawaited(playerHelper.playSearchMix(track.id, track.title!));
break;
case final Artist artist:
final fullArtist = await _deezerAPI.artist(artist.id);
unawaited(playerHelper.playFromTopTracks(
fullArtist.topTracks!, null, fullArtist));
break;
case final Album album:
final fullAlbum = await _deezerAPI.album(album.id);
unawaited(playerHelper.playFromAlbum(fullAlbum));
break;
case final Playlist playlist:
final fullPlaylist = await _deezerAPI.playlist(playlist.id);
unawaited(playerHelper.playFromPlaylist(fullPlaylist));
break;
}
} else if (res.tracks != null && res.tracks!.isNotEmpty) {
_logger.fine('playing from track mix: ${jsonEncode(res.tracks![0])}');
unawaited(
playerHelper.playSearchMix(res.tracks![0].id, res.tracks![0].title!));
} else if (res.albums != null && res.albums!.isNotEmpty) {
_logger.fine('playing from album: ${jsonEncode(res.albums![0])}');
unawaited(playerHelper
.playFromAlbum(await _deezerAPI.album(res.albums![0].id)));
} else if (res.artists != null && res.artists!.isNotEmpty) {
_logger.fine('playing from artist top: ${jsonEncode(res.artists![0])}');
final artist = await _deezerAPI.artist(res.artists![0].id);
unawaited(
playerHelper.playFromTopTracks(artist.topTracks!, null, artist));
} else if (res.shows != null && res.shows!.isNotEmpty) {
_logger.fine('playing from show: ${jsonEncode(res.shows![0])}');
final episodes = await _deezerAPI.allShowEpisodes(res.shows![0].id);
unawaited(playerHelper.playShowEpisode(res.shows![0], episodes!));
}
}
@override
Future<void> playFromUri(Uri uri, [Map<String, dynamic>? extras]) async {
_logger.fine('playFromUri($uri, $extras)');
if (uri.host != 'www.deezer.com' && uri.host != 'deezer.com') {
return;
}
// parse common urls
if (uri.path.startsWith('/user/me/') && uri.pathSegments.length == 3) {
String stlId = uri.pathSegments[2];
if ([
'new-releases',
'inspired-by-1',
'inspired-by-2',
'inspired-by-3',
'inspired-by-4',
'discovery',
'flow'
].contains(stlId)) {
await playerHelper.playFromSmartTrackList(SmartTrackList(id: stlId));
return;
}
}
final parsed = await _deezerAPI.parseLink(uri);
if (parsed == null) return;
switch (parsed.type!) {
case DeezerLinkType.TRACK:
final track = await _deezerAPI.track(parsed.id!);
_logger.fine('playing from track mix: ${jsonEncode(track)}');
unawaited(playerHelper.playSearchMix(track.id, track.title!));
break;
case DeezerLinkType.ALBUM:
final album = await _deezerAPI.album(parsed.id!);
_logger.fine('playing from album: ${album.title}');
unawaited(playerHelper.playFromAlbum(album));
break;
case DeezerLinkType.ARTIST:
final artist = await _deezerAPI.artist(parsed.id!);
_logger.fine('playing from artist top: ${artist.name!}');
unawaited(
playerHelper.playFromTopTracks(artist.topTracks!, null, artist));
break;
case DeezerLinkType.PLAYLIST:
final fullPlaylist = await _deezerAPI.playlist(parsed.id!);
_logger.fine('playing from playlist: ${fullPlaylist.title!}');
unawaited(playerHelper.playFromPlaylist(fullPlaylist));
break;
}
}
@override
Future<MediaItem?> getMediaItem(String mediaId) async =>
queue.value.firstWhereOrNull((mediaItem) => mediaItem.id == mediaId);

View file

@ -78,7 +78,12 @@ class PlayerHelper {
androidNotificationChannelDescription: 'Freezer',
androidNotificationChannelName: 'Freezer',
androidNotificationIcon: 'drawable/ic_logo',
androidNotificationChannelId: 'f.f.freezer.audio',
preloadArtwork: true,
androidBrowsableRootExtras: <String, dynamic>{
"android.media.browse.SEARCH_SUPPORTED":
true, // support showing search button on Android Auto as well as alternative search results on the player screen after voice search
},
),
cacheManager: cacheManager,
);
@ -197,13 +202,18 @@ class PlayerHelper {
}
//Play track from album
Future playFromAlbum(Album album, [String? trackId]) async {
Future<void> playFromAlbum(Album album, [String? trackId]) async {
if (album.tracks!.length == 1) {
// play mix based on track
await playSearchMixDeferred(album.tracks![0]);
return;
}
await playFromTrackList(album.tracks!, trackId,
QueueSource(id: album.id, text: album.title, source: 'album'));
}
//Play mix by track
Future playMix(String trackId, String trackTitle) async {
Future<void> playMix(String trackId, String trackTitle) async {
List<Track> tracks = await deezerAPI.playMix(trackId);
await playFromTrackList(
tracks,
@ -244,8 +254,8 @@ class PlayerHelper {
}
//Play from artist top tracks
Future playFromTopTracks(
List<Track> tracks, String trackId, Artist artist) async {
Future<void> playFromTopTracks(
List<Track> tracks, String? trackId, Artist artist) async {
await playFromTrackList(
tracks,
trackId,
@ -253,7 +263,7 @@ class PlayerHelper {
id: artist.id, text: 'Top ${artist.name}', source: 'topTracks'));
}
Future playFromPlaylist(Playlist playlist, [String? trackId]) async {
Future<void> playFromPlaylist(Playlist playlist, [String? trackId]) async {
await playFromTrackList(playlist.tracks!, trackId,
QueueSource(id: playlist.id, text: playlist.title, source: 'playlist'));
}

View file

@ -100,8 +100,10 @@ void main() async {
try {
cache = await Cache.load();
settings = await Settings.load();
settings.save();
await settings.save();
} catch (e) {
// ignore: avoid_print
print(e);
// ignore: avoid_print
print(
'Cannot load cache or settings box. Maybe another instance of the app is running?');
@ -126,7 +128,7 @@ void main() async {
'${record.level.name}: ${record.time}: [${record.loggerName}] ${record.message}');
});
if (kDebugMode) {
if (kDebugMode || true) {
Logger.root.level = Level.ALL;
}
@ -470,12 +472,14 @@ class MainScreenState extends State<MainScreen>
//Listen to URLs
_urlLinkStream = linkStream.listen((String? link) {
if (link == null) return;
_logger.fine('received link: $link');
if (!context.mounted) return;
openScreenByURL(context, link);
}, onError: (err) {});
//Get initial link on cold start
try {
String? link = await getInitialLink();
_logger.fine('initial link: $link');
if (!context.mounted) return;
if (link != null && link.length > 4) openScreenByURL(context, link);

View file

@ -22,7 +22,7 @@ import '../api/definitions.dart';
import 'error.dart';
FutureOr openScreenByURL(BuildContext context, String url) async {
DeezerLinkResponse? res = await deezerAPI.parseLink(url);
DeezerLinkResponse? res = await deezerAPI.parseLink(Uri.parse(url));
if (res == null) return;
switch (res.type) {
@ -435,271 +435,476 @@ class SearchBrowseCard extends StatelessWidget {
}
}
class SearchResultsScreen extends StatelessWidget {
class SearchResultsScreen extends StatefulWidget {
final String? query;
final bool? offline;
const SearchResultsScreen(this.query, {super.key, this.offline});
@override
State<SearchResultsScreen> createState() => _SearchResultsScreenState();
}
class _SearchResultsScreenState extends State<SearchResultsScreen> {
final _tracksKey = GlobalKey();
final _albumsKey = GlobalKey();
final _artistsKey = GlobalKey();
final _playlistsKey = GlobalKey();
final _showsKey = GlobalKey();
final _episodesKey = GlobalKey();
SearchResults? _results;
Object? _error;
Future _search() async {
if (offline ?? false) {
return await downloadManager.search(query);
try {
final SearchResults results;
if (widget.offline ?? false) {
results = await downloadManager.search(widget.query);
} else {
results = await deezerAPI.search(widget.query);
}
setState(() {
_results = results;
});
} catch (e) {
setState(() {
_error = e;
});
}
return await deezerAPI.search(query);
}
Widget buildFromDeezerItem(DeezerMediaItem item) {
switch (item) {
case final Track track:
return const SizedBox.square(dimension: 128.0, child: Placeholder());
// return TrackTile.fromTrack(
// track,
// onTap: () => playerHelper.playSearchMixDeferred(track),
// onSecondary: (details) =>
// MenuSheet(context).defaultTrackMenu(track, details: details),
// );
case final Album album:
return AlbumCard(
album,
onTap: () => Navigator.of(context)
.pushRoute(builder: (context) => AlbumDetails(album)),
onSecondary: (details) =>
MenuSheet(context).defaultAlbumMenu(album, details: details),
);
case final Playlist playlist:
return PlaylistCardTile(
playlist,
onTap: () => Navigator.of(context)
.pushRoute(builder: (context) => PlaylistDetails(playlist)),
onSecondary: (details) => MenuSheet(context)
.defaultPlaylistMenu(playlist, details: details),
);
case final Artist artist:
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ArtistTile(
artist,
onTap: () => Navigator.of(context)
.pushRoute(builder: (ctx) => ArtistDetails(artist)),
onSecondary: (details) =>
MenuSheet(context).defaultArtistMenu(artist, details: details),
),
);
default:
throw Exception();
}
}
@override
void initState() {
_search();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(query!)),
body: FutureBuilder(
future: _search(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) return const ErrorScreen();
SearchResults results = snapshot.data;
if (results.empty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
Icons.warning,
size: 64.sp,
),
Text('No results!'.i18n)
],
),
);
}
return ListView(
children: <Widget>[
if (results.tracks != null && results.tracks!.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 4.0),
child: Text(
'Tracks'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0, fontWeight: FontWeight.bold),
appBar: AppBar(
title: Text(widget.query!),
bottom: PreferredSize(
preferredSize: Size.fromHeight(_results == null ? 0.0 : 50.0),
child: _results == null
? const SizedBox.shrink()
: Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
child:
ListView(scrollDirection: Axis.horizontal, children: [
if (_results!.tracks != null &&
_results!.tracks!.isNotEmpty) ...[
ActionChip(
elevation: 1.0,
label: Text('Tracks'.i18n),
onPressed: () => Scrollable.ensureVisible(
_tracksKey.currentContext!,
duration: const Duration(milliseconds: 500))),
const SizedBox(width: 8.0),
],
if (_results!.albums != null &&
_results!.albums!.isNotEmpty) ...[
ActionChip(
elevation: 1.0,
label: Text('Albums'.i18n),
onPressed: () => Scrollable.ensureVisible(
_albumsKey.currentContext!,
duration: const Duration(milliseconds: 500))),
const SizedBox(width: 8.0),
],
if (_results!.artists != null &&
_results!.artists!.isNotEmpty) ...[
ActionChip(
elevation: 1.0,
label: Text('Artists'.i18n),
onPressed: () => Scrollable.ensureVisible(
_artistsKey.currentContext!,
duration: const Duration(milliseconds: 500))),
const SizedBox(width: 8.0),
],
if (_results!.playlists != null &&
_results!.playlists!.isNotEmpty) ...[
ActionChip(
elevation: 1.0,
label: Text('Playlists'.i18n),
onPressed: () => Scrollable.ensureVisible(
_playlistsKey.currentContext!,
duration: const Duration(milliseconds: 500))),
const SizedBox(width: 8.0),
],
if (_results!.shows != null &&
_results!.shows!.isNotEmpty) ...[
ActionChip(
elevation: 1.0,
label: Text('Shows'.i18n),
onPressed: () => Scrollable.ensureVisible(
_showsKey.currentContext!,
duration: const Duration(milliseconds: 500))),
const SizedBox(width: 8.0),
],
if (_results!.episodes != null &&
_results!.episodes!.isNotEmpty) ...[
ActionChip(
elevation: 1.0,
label: Text('Episodes'.i18n),
onPressed: () => Scrollable.ensureVisible(
_episodesKey.currentContext!,
duration: const Duration(milliseconds: 500))),
const SizedBox(width: 8.0),
],
]),
),
),
for (final track in results.tracks!
.getRange(0, min(results.tracks!.length, 3)))
TrackTile.fromTrack(track, onTap: () {
cache.addToSearchHistory(track);
playerHelper.playSearchMixDeferred(track);
}, onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultTrackMenu(track, details: details);
}),
ListTile(
title: Text('Show all tracks'.i18n),
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) =>
TrackListScreen(results.tracks, null));
},
),
const FreezerDivider(),
],
if (results.albums != null && results.albums!.isNotEmpty) ...[
const SizedBox(height: 8.0),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 4.0),
child: Text(
'Albums'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0, fontWeight: FontWeight.bold),
),
),
for (final album in results.albums!
.getRange(0, min(results.albums!.length, 3)))
AlbumTile(album, onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultAlbumMenu(album, details: details);
}, onTap: () {
cache.addToSearchHistory(album);
Navigator.of(context)
.pushRoute(builder: (context) => AlbumDetails(album));
}),
ListTile(
title: Text('Show all albums'.i18n),
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) =>
AlbumListScreen(results.albums));
},
),
const FreezerDivider()
],
if (results.artists != null && results.artists!.isNotEmpty) ...[
const SizedBox(height: 8.0),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 4.0, horizontal: 16.0),
child: Text(
'Artists'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 4.0),
SizedBox(
height: 136.0,
child: ListView.builder(
primary: false,
scrollDirection: Axis.horizontal,
itemCount: results.artists!.length,
itemBuilder: (context, index) {
final artist = results.artists![index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ArtistTile(
artist,
onTap: () {
cache.addToSearchHistory(artist);
Navigator.of(context).pushRoute(
builder: (context) => ArtistDetails(artist));
},
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultArtistMenu(artist, details: details);
},
),
),
body: _error != null
? ErrorScreen(message: _error.toString())
: _results == null
? const Center(child: CircularProgressIndicator())
: Builder(
builder: (context) {
final results = _results!;
if (results.empty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
Icons.warning,
size: 64.sp,
),
Text('No results!'.i18n)
],
),
);
},
),
),
const FreezerDivider()
],
if (results.playlists != null &&
results.playlists!.isNotEmpty) ...[
const SizedBox(height: 8.0),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 4.0, horizontal: 16.0),
child: Text(
'Playlists'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0, fontWeight: FontWeight.bold),
),
),
for (final playlist in results.playlists!
.getRange(0, min(results.playlists!.length, 3)))
PlaylistTile(
playlist,
onTap: () {
cache.addToSearchHistory(playlist);
Navigator.of(context).pushRoute(
builder: (context) => PlaylistDetails(playlist));
},
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultPlaylistMenu(playlist, details: details);
},
),
ListTile(
title: Text('Show all playlists'.i18n),
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) =>
SearchResultPlaylists(results.playlists));
},
),
const FreezerDivider(),
],
if (results.shows != null && results.shows!.isNotEmpty) ...[
const SizedBox(height: 8.0),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 4.0, horizontal: 16.0),
child: Text(
'Shows'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0, fontWeight: FontWeight.bold),
),
),
for (final show in results.shows!
.getRange(0, min(results.shows!.length, 3)))
ShowTile(
show,
onTap: () async {
Navigator.of(context)
.pushRoute(builder: (context) => ShowScreen(show));
},
),
ListTile(
title: Text('Show all shows'.i18n),
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) => ShowListScreen(results.shows));
},
),
const FreezerDivider()
],
if (results.episodes != null &&
results.episodes!.isNotEmpty) ...[
const SizedBox(height: 8.0),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 4.0, horizontal: 16.0),
child: Text(
'Episodes'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0, fontWeight: FontWeight.bold),
),
),
for (final episode in results.episodes!
.getRange(0, min(results.episodes!.length, 3)))
ShowEpisodeTile(
episode,
trailing: IconButton(
icon: Icon(
Icons.more_vert,
semanticLabel: "Options".i18n,
}
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (results.topResult != null &&
results.topResult!.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 4.0),
child: Text(
'Top results'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: results.topResult!
.map(buildFromDeezerItem)
.toList(growable: false),
),
),
),
],
if (results.tracks != null &&
results.tracks!.isNotEmpty) ...[
Padding(
key: _tracksKey,
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 4.0),
child: Text(
'Tracks'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold),
),
),
for (final track in results.tracks!
.getRange(0, min(results.tracks!.length, 3)))
TrackTile.fromTrack(track, onTap: () {
cache.addToSearchHistory(track);
playerHelper.playSearchMixDeferred(track);
}, onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultTrackMenu(track, details: details);
}),
ListTile(
title: Text('Show all tracks'.i18n),
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) => TrackListScreen(
results.tracks, null));
},
),
const FreezerDivider(),
],
if (results.albums != null &&
results.albums!.isNotEmpty) ...[
const SizedBox(height: 8.0),
Padding(
key: _albumsKey,
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 4.0),
child: Text(
'Albums'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold),
),
),
for (final album in results.albums!
.getRange(0, min(results.albums!.length, 3)))
AlbumTile(album, onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultAlbumMenu(album, details: details);
}, onTap: () {
cache.addToSearchHistory(album);
Navigator.of(context).pushRoute(
builder: (context) =>
AlbumDetails(album));
}),
ListTile(
title: Text('Show all albums'.i18n),
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) =>
AlbumListScreen(results.albums));
},
),
const FreezerDivider()
],
if (results.artists != null &&
results.artists!.isNotEmpty) ...[
const SizedBox(height: 8.0),
Padding(
key: _artistsKey,
padding: const EdgeInsets.symmetric(
vertical: 4.0, horizontal: 16.0),
child: Text(
'Artists'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 4.0),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: SizedBox(
height: 136.0,
child: ListView.builder(
primary: false,
scrollDirection: Axis.horizontal,
itemCount: results.artists!.length,
itemBuilder: (context, index) {
final artist = results.artists![index];
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0),
child: ArtistTile(
artist,
onTap: () {
cache.addToSearchHistory(artist);
Navigator.of(context).pushRoute(
builder: (context) =>
ArtistDetails(artist));
},
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultArtistMenu(artist,
details: details);
},
),
);
},
),
),
),
const FreezerDivider()
],
if (results.playlists != null &&
results.playlists!.isNotEmpty) ...[
const SizedBox(height: 8.0),
Padding(
key: _playlistsKey,
padding: const EdgeInsets.symmetric(
vertical: 4.0, horizontal: 16.0),
child: Text(
'Playlists'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold),
),
),
for (final playlist in results.playlists!
.getRange(
0, min(results.playlists!.length, 3)))
PlaylistTile(
playlist,
onTap: () {
cache.addToSearchHistory(playlist);
Navigator.of(context).pushRoute(
builder: (context) =>
PlaylistDetails(playlist));
},
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultPlaylistMenu(playlist,
details: details);
},
),
ListTile(
title: Text('Show all playlists'.i18n),
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) =>
SearchResultPlaylists(
results.playlists));
},
),
const FreezerDivider(),
],
if (results.shows != null &&
results.shows!.isNotEmpty) ...[
const SizedBox(height: 8.0),
Padding(
key: _showsKey,
padding: const EdgeInsets.symmetric(
vertical: 4.0, horizontal: 16.0),
child: Text(
'Shows'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold),
),
),
for (final show in results.shows!
.getRange(0, min(results.shows!.length, 3)))
ShowTile(
show,
onTap: () async {
Navigator.of(context).pushRoute(
builder: (context) => ShowScreen(show));
},
),
ListTile(
title: Text('Show all shows'.i18n),
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) =>
ShowListScreen(results.shows));
},
),
const FreezerDivider()
],
if (results.episodes != null &&
results.episodes!.isNotEmpty) ...[
const SizedBox(height: 8.0),
Padding(
key: _episodesKey,
padding: const EdgeInsets.symmetric(
vertical: 4.0, horizontal: 16.0),
child: Text(
'Episodes'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold),
),
),
for (final episode in results.episodes!.getRange(
0, min(results.episodes!.length, 3)))
ShowEpisodeTile(
episode,
trailing: IconButton(
icon: Icon(
Icons.more_vert,
semanticLabel: "Options".i18n,
),
onPressed: () {
MenuSheet m = MenuSheet(context);
m.defaultShowEpisodeMenu(
episode.show!, episode);
},
),
onTap: () async {
//Load entire show, then play
List<ShowEpisode> episodes =
(await deezerAPI.allShowEpisodes(
episode.show!.id))!;
await playerHelper.playShowEpisode(
episode.show!, episodes,
index: episodes.indexWhere(
(ep) => episode.id == ep.id));
},
),
ListTile(
title: Text('Show all episodes'.i18n),
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) => EpisodeListScreen(
results.episodes));
})
]
],
),
onPressed: () {
MenuSheet m = MenuSheet(context);
m.defaultShowEpisodeMenu(episode.show!, episode);
},
),
onTap: () async {
//Load entire show, then play
List<ShowEpisode> episodes = (await deezerAPI
.allShowEpisodes(episode.show!.id))!;
await playerHelper.playShowEpisode(
episode.show!, episodes,
index: episodes
.indexWhere((ep) => episode.id == ep.id));
},
),
ListTile(
title: Text('Show all episodes'.i18n),
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) =>
EpisodeListScreen(results.episodes));
})
]
],
);
},
));
);
},
));
}
}