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:
parent
14ff02b65c
commit
6816bdc112
|
@ -40,8 +40,8 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// Workaround to make Google assistant work with Freezer
|
||||||
applicationId "f.f.freezer"
|
applicationId "deezer.android.app" // "f.f.freezer"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
|
@ -64,7 +64,7 @@ android {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix ".debug"
|
// applicationIdSuffix ".debug"
|
||||||
shrinkResources false
|
shrinkResources false
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
android:label="Freezer"
|
android:label="Freezer"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="m">
|
android:targetSandboxVersion="2">
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".DownloadService"
|
android:name=".DownloadService"
|
||||||
|
@ -88,9 +88,12 @@
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.APP_MUSIC"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
|
||||||
<!-- App links -->
|
<!-- App links -->
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
extensions:
|
|
@ -235,23 +235,28 @@ class DeezerAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
//URL/Link parser
|
//URL/Link parser
|
||||||
Future<DeezerLinkResponse?> parseLink(String url) async {
|
Future<DeezerLinkResponse?> parseLink(Uri uri) async {
|
||||||
Uri uri = Uri.parse(url);
|
//https://www.deezer.com/NOTHING_OR_COUNTRY/TYPE/ID[/radio?autoplay=true]
|
||||||
//https://www.deezer.com/NOTHING_OR_COUNTRY/TYPE/ID
|
|
||||||
if (uri.host == 'www.deezer.com' || uri.host == 'deezer.com') {
|
if (uri.host == 'www.deezer.com' || uri.host == 'deezer.com') {
|
||||||
if (uri.pathSegments.length < 2) return null;
|
if (uri.pathSegments.length < 2) return null;
|
||||||
DeezerLinkType? type = DeezerLinkResponse.typeFromString(
|
if (uri.pathSegments[uri.pathSegments.length - 1] == 'radio') {
|
||||||
uri.pathSegments[uri.pathSegments.length - 2]);
|
return DeezerLinkResponse(
|
||||||
|
type: DeezerLinkResponse.typeFromString(
|
||||||
|
uri.pathSegments[uri.pathSegments.length - 3]),
|
||||||
|
id: uri.pathSegments[uri.pathSegments.length - 2]);
|
||||||
|
}
|
||||||
return DeezerLinkResponse(
|
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
|
//Share URL
|
||||||
if (uri.host == 'deezer.page.link' || uri.host == 'www.deezer.page.link') {
|
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;
|
request.followRedirects = false;
|
||||||
http.StreamedResponse response = await request.send();
|
http.StreamedResponse response = await request.send();
|
||||||
String newUrl = response.headers['location']!;
|
String newUrl = response.headers['location']!;
|
||||||
return parseLink(newUrl);
|
return parseLink(Uri.parse(newUrl));
|
||||||
}
|
}
|
||||||
//Spotify
|
//Spotify
|
||||||
if (uri.host == 'open.spotify.com') {
|
if (uri.host == 'open.spotify.com') {
|
||||||
|
@ -333,6 +338,7 @@ class DeezerAPI {
|
||||||
Future<SearchResults> search(String? query) async {
|
Future<SearchResults> search(String? query) async {
|
||||||
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch',
|
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch',
|
||||||
params: {'nb': 128, 'query': query, 'start': 0});
|
params: {'nb': 128, 'query': query, 'start': 0});
|
||||||
|
print(data['results']['TOP_RESULT']);
|
||||||
return SearchResults.fromPrivateJson(data['results']);
|
return SearchResults.fromPrivateJson(data['results']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,22 @@ part 'definitions.g.dart';
|
||||||
|
|
||||||
abstract class DeezerMediaItem {
|
abstract class DeezerMediaItem {
|
||||||
String? get id;
|
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)
|
@HiveType(typeId: 5)
|
||||||
|
@ -662,6 +678,7 @@ class DeezerImageDetails extends ImageDetails {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchResults {
|
class SearchResults {
|
||||||
|
List<DeezerMediaItem>? topResult;
|
||||||
List<Track>? tracks;
|
List<Track>? tracks;
|
||||||
List<Album>? albums;
|
List<Album>? albums;
|
||||||
List<Artist>? artists;
|
List<Artist>? artists;
|
||||||
|
@ -670,7 +687,8 @@ class SearchResults {
|
||||||
List<ShowEpisode>? episodes;
|
List<ShowEpisode>? episodes;
|
||||||
|
|
||||||
SearchResults(
|
SearchResults(
|
||||||
{this.tracks,
|
{this.topResult,
|
||||||
|
this.tracks,
|
||||||
this.albums,
|
this.albums,
|
||||||
this.artists,
|
this.artists,
|
||||||
this.playlists,
|
this.playlists,
|
||||||
|
@ -689,6 +707,11 @@ class SearchResults {
|
||||||
|
|
||||||
factory SearchResults.fromPrivateJson(Map<dynamic, dynamic> json) =>
|
factory SearchResults.fromPrivateJson(Map<dynamic, dynamic> json) =>
|
||||||
SearchResults(
|
SearchResults(
|
||||||
|
topResult: (json['TOP_RESULT'] as List?)
|
||||||
|
?.map<DeezerMediaItem?>(
|
||||||
|
(e) => DeezerMediaItem.fromDeezerObject(e as Map))
|
||||||
|
.whereType<DeezerMediaItem>()
|
||||||
|
.toList(growable: false),
|
||||||
tracks: json['TRACK']['data']
|
tracks: json['TRACK']['data']
|
||||||
.map<Track>((dynamic data) => Track.fromPrivateJson(data))
|
.map<Track>((dynamic data) => Track.fromPrivateJson(data))
|
||||||
.toList(),
|
.toList(),
|
||||||
|
|
|
@ -492,15 +492,15 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
return const MediaControl(
|
return const MediaControl(
|
||||||
androidIcon: 'drawable/ic_heart',
|
androidIcon: 'drawable/ic_heart',
|
||||||
label: 'unfavorite',
|
label: 'unfavorite',
|
||||||
action: MediaAction.custom);
|
action: MediaAction.custom,
|
||||||
|
customAction: CustomMediaAction(name: 'favorite'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return const MediaControl(
|
return const MediaControl(
|
||||||
androidIcon: 'drawable/ic_heart_outline',
|
androidIcon: 'drawable/ic_heart_outline',
|
||||||
label: 'favourite',
|
label: 'favourite',
|
||||||
action: MediaAction
|
action: MediaAction.custom,
|
||||||
.custom, // this acts as favourite/unfavourite as it's not already used.
|
customAction: CustomMediaAction(name: 'favorite'));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Update state on all clients
|
//Update state on all clients
|
||||||
|
@ -535,7 +535,6 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
systemActions: !currentMediaItemIsShow
|
systemActions: !currentMediaItemIsShow
|
||||||
? {
|
? {
|
||||||
MediaAction.seek,
|
MediaAction.seek,
|
||||||
MediaAction.seekBackward,
|
|
||||||
if (queue.hasValue && _queueIndex != queue.value.length - 1)
|
if (queue.hasValue && _queueIndex != queue.value.length - 1)
|
||||||
MediaAction.skipToNext,
|
MediaAction.skipToNext,
|
||||||
if (_queueIndex != 0) MediaAction.skipToPrevious,
|
if (_queueIndex != 0) MediaAction.skipToPrevious,
|
||||||
|
@ -552,6 +551,12 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
bufferedPosition: _player.bufferedPosition,
|
bufferedPosition: _player.bufferedPosition,
|
||||||
speed: _player.speed,
|
speed: _player.speed,
|
||||||
queueIndex: _queueIndex,
|
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));
|
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
|
@override
|
||||||
Future<MediaItem?> getMediaItem(String mediaId) async =>
|
Future<MediaItem?> getMediaItem(String mediaId) async =>
|
||||||
queue.value.firstWhereOrNull((mediaItem) => mediaItem.id == mediaId);
|
queue.value.firstWhereOrNull((mediaItem) => mediaItem.id == mediaId);
|
||||||
|
|
|
@ -78,7 +78,12 @@ class PlayerHelper {
|
||||||
androidNotificationChannelDescription: 'Freezer',
|
androidNotificationChannelDescription: 'Freezer',
|
||||||
androidNotificationChannelName: 'Freezer',
|
androidNotificationChannelName: 'Freezer',
|
||||||
androidNotificationIcon: 'drawable/ic_logo',
|
androidNotificationIcon: 'drawable/ic_logo',
|
||||||
|
androidNotificationChannelId: 'f.f.freezer.audio',
|
||||||
preloadArtwork: true,
|
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,
|
cacheManager: cacheManager,
|
||||||
);
|
);
|
||||||
|
@ -197,13 +202,18 @@ class PlayerHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Play track from album
|
//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,
|
await playFromTrackList(album.tracks!, trackId,
|
||||||
QueueSource(id: album.id, text: album.title, source: 'album'));
|
QueueSource(id: album.id, text: album.title, source: 'album'));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Play mix by track
|
//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);
|
List<Track> tracks = await deezerAPI.playMix(trackId);
|
||||||
await playFromTrackList(
|
await playFromTrackList(
|
||||||
tracks,
|
tracks,
|
||||||
|
@ -244,8 +254,8 @@ class PlayerHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Play from artist top tracks
|
//Play from artist top tracks
|
||||||
Future playFromTopTracks(
|
Future<void> playFromTopTracks(
|
||||||
List<Track> tracks, String trackId, Artist artist) async {
|
List<Track> tracks, String? trackId, Artist artist) async {
|
||||||
await playFromTrackList(
|
await playFromTrackList(
|
||||||
tracks,
|
tracks,
|
||||||
trackId,
|
trackId,
|
||||||
|
@ -253,7 +263,7 @@ class PlayerHelper {
|
||||||
id: artist.id, text: 'Top ${artist.name}', source: 'topTracks'));
|
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,
|
await playFromTrackList(playlist.tracks!, trackId,
|
||||||
QueueSource(id: playlist.id, text: playlist.title, source: 'playlist'));
|
QueueSource(id: playlist.id, text: playlist.title, source: 'playlist'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,8 +100,10 @@ void main() async {
|
||||||
try {
|
try {
|
||||||
cache = await Cache.load();
|
cache = await Cache.load();
|
||||||
settings = await Settings.load();
|
settings = await Settings.load();
|
||||||
settings.save();
|
await settings.save();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print(e);
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print(
|
print(
|
||||||
'Cannot load cache or settings box. Maybe another instance of the app is running?');
|
'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}');
|
'${record.level.name}: ${record.time}: [${record.loggerName}] ${record.message}');
|
||||||
});
|
});
|
||||||
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode || true) {
|
||||||
Logger.root.level = Level.ALL;
|
Logger.root.level = Level.ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -470,12 +472,14 @@ class MainScreenState extends State<MainScreen>
|
||||||
//Listen to URLs
|
//Listen to URLs
|
||||||
_urlLinkStream = linkStream.listen((String? link) {
|
_urlLinkStream = linkStream.listen((String? link) {
|
||||||
if (link == null) return;
|
if (link == null) return;
|
||||||
|
_logger.fine('received link: $link');
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
openScreenByURL(context, link);
|
openScreenByURL(context, link);
|
||||||
}, onError: (err) {});
|
}, onError: (err) {});
|
||||||
//Get initial link on cold start
|
//Get initial link on cold start
|
||||||
try {
|
try {
|
||||||
String? link = await getInitialLink();
|
String? link = await getInitialLink();
|
||||||
|
_logger.fine('initial link: $link');
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (link != null && link.length > 4) openScreenByURL(context, link);
|
if (link != null && link.length > 4) openScreenByURL(context, link);
|
||||||
|
|
|
@ -22,7 +22,7 @@ import '../api/definitions.dart';
|
||||||
import 'error.dart';
|
import 'error.dart';
|
||||||
|
|
||||||
FutureOr openScreenByURL(BuildContext context, String url) async {
|
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;
|
if (res == null) return;
|
||||||
|
|
||||||
switch (res.type) {
|
switch (res.type) {
|
||||||
|
@ -435,271 +435,476 @@ class SearchBrowseCard extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchResultsScreen extends StatelessWidget {
|
class SearchResultsScreen extends StatefulWidget {
|
||||||
final String? query;
|
final String? query;
|
||||||
final bool? offline;
|
final bool? offline;
|
||||||
|
|
||||||
const SearchResultsScreen(this.query, {super.key, this.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 {
|
Future _search() async {
|
||||||
if (offline ?? false) {
|
try {
|
||||||
return await downloadManager.search(query);
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(query!)),
|
appBar: AppBar(
|
||||||
body: FutureBuilder(
|
title: Text(widget.query!),
|
||||||
future: _search(),
|
bottom: PreferredSize(
|
||||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
preferredSize: Size.fromHeight(_results == null ? 0.0 : 50.0),
|
||||||
if (!snapshot.hasData) {
|
child: _results == null
|
||||||
return const Center(
|
? const SizedBox.shrink()
|
||||||
child: CircularProgressIndicator(),
|
: Expanded(
|
||||||
);
|
child: Padding(
|
||||||
}
|
padding: const EdgeInsets.symmetric(
|
||||||
if (snapshot.hasError) return const ErrorScreen();
|
horizontal: 16.0, vertical: 8.0),
|
||||||
|
child:
|
||||||
SearchResults results = snapshot.data;
|
ListView(scrollDirection: Axis.horizontal, children: [
|
||||||
|
if (_results!.tracks != null &&
|
||||||
if (results.empty) {
|
_results!.tracks!.isNotEmpty) ...[
|
||||||
return Center(
|
ActionChip(
|
||||||
child: Column(
|
elevation: 1.0,
|
||||||
mainAxisSize: MainAxisSize.min,
|
label: Text('Tracks'.i18n),
|
||||||
children: <Widget>[
|
onPressed: () => Scrollable.ensureVisible(
|
||||||
Icon(
|
_tracksKey.currentContext!,
|
||||||
Icons.warning,
|
duration: const Duration(milliseconds: 500))),
|
||||||
size: 64.sp,
|
const SizedBox(width: 8.0),
|
||||||
),
|
],
|
||||||
Text('No results!'.i18n)
|
if (_results!.albums != null &&
|
||||||
],
|
_results!.albums!.isNotEmpty) ...[
|
||||||
),
|
ActionChip(
|
||||||
);
|
elevation: 1.0,
|
||||||
}
|
label: Text('Albums'.i18n),
|
||||||
|
onPressed: () => Scrollable.ensureVisible(
|
||||||
return ListView(
|
_albumsKey.currentContext!,
|
||||||
children: <Widget>[
|
duration: const Duration(milliseconds: 500))),
|
||||||
if (results.tracks != null && results.tracks!.isNotEmpty) ...[
|
const SizedBox(width: 8.0),
|
||||||
Padding(
|
],
|
||||||
padding: const EdgeInsets.symmetric(
|
if (_results!.artists != null &&
|
||||||
horizontal: 16.0, vertical: 4.0),
|
_results!.artists!.isNotEmpty) ...[
|
||||||
child: Text(
|
ActionChip(
|
||||||
'Tracks'.i18n,
|
elevation: 1.0,
|
||||||
textAlign: TextAlign.left,
|
label: Text('Artists'.i18n),
|
||||||
style: const TextStyle(
|
onPressed: () => Scrollable.ensureVisible(
|
||||||
fontSize: 20.0, fontWeight: FontWeight.bold),
|
_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: () {
|
body: _error != null
|
||||||
cache.addToSearchHistory(track);
|
? ErrorScreen(message: _error.toString())
|
||||||
playerHelper.playSearchMixDeferred(track);
|
: _results == null
|
||||||
}, onSecondary: (details) {
|
? const Center(child: CircularProgressIndicator())
|
||||||
MenuSheet m = MenuSheet(context);
|
: Builder(
|
||||||
m.defaultTrackMenu(track, details: details);
|
builder: (context) {
|
||||||
}),
|
final results = _results!;
|
||||||
ListTile(
|
if (results.empty) {
|
||||||
title: Text('Show all tracks'.i18n),
|
return Center(
|
||||||
onTap: () {
|
child: Column(
|
||||||
Navigator.of(context).pushRoute(
|
mainAxisSize: MainAxisSize.min,
|
||||||
builder: (context) =>
|
children: <Widget>[
|
||||||
TrackListScreen(results.tracks, null));
|
Icon(
|
||||||
},
|
Icons.warning,
|
||||||
),
|
size: 64.sp,
|
||||||
const FreezerDivider(),
|
),
|
||||||
],
|
Text('No results!'.i18n)
|
||||||
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);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
),
|
|
||||||
),
|
return SingleChildScrollView(
|
||||||
const FreezerDivider()
|
child: Column(
|
||||||
],
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
if (results.playlists != null &&
|
children: <Widget>[
|
||||||
results.playlists!.isNotEmpty) ...[
|
if (results.topResult != null &&
|
||||||
const SizedBox(height: 8.0),
|
results.topResult!.isNotEmpty) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 4.0, horizontal: 16.0),
|
horizontal: 16.0, vertical: 4.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Playlists'.i18n,
|
'Top results'.i18n,
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20.0, fontWeight: FontWeight.bold),
|
fontSize: 20.0,
|
||||||
),
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
for (final playlist in results.playlists!
|
),
|
||||||
.getRange(0, min(results.playlists!.length, 3)))
|
Padding(
|
||||||
PlaylistTile(
|
padding: const EdgeInsets.symmetric(
|
||||||
playlist,
|
horizontal: 16.0),
|
||||||
onTap: () {
|
child: SingleChildScrollView(
|
||||||
cache.addToSearchHistory(playlist);
|
scrollDirection: Axis.horizontal,
|
||||||
Navigator.of(context).pushRoute(
|
child: Row(
|
||||||
builder: (context) => PlaylistDetails(playlist));
|
children: results.topResult!
|
||||||
},
|
.map(buildFromDeezerItem)
|
||||||
onSecondary: (details) {
|
.toList(growable: false),
|
||||||
MenuSheet m = MenuSheet(context);
|
),
|
||||||
m.defaultPlaylistMenu(playlist, details: details);
|
),
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
ListTile(
|
if (results.tracks != null &&
|
||||||
title: Text('Show all playlists'.i18n),
|
results.tracks!.isNotEmpty) ...[
|
||||||
onTap: () {
|
Padding(
|
||||||
Navigator.of(context).pushRoute(
|
key: _tracksKey,
|
||||||
builder: (context) =>
|
padding: const EdgeInsets.symmetric(
|
||||||
SearchResultPlaylists(results.playlists));
|
horizontal: 16.0, vertical: 4.0),
|
||||||
},
|
child: Text(
|
||||||
),
|
'Tracks'.i18n,
|
||||||
const FreezerDivider(),
|
textAlign: TextAlign.left,
|
||||||
],
|
style: const TextStyle(
|
||||||
if (results.shows != null && results.shows!.isNotEmpty) ...[
|
fontSize: 20.0,
|
||||||
const SizedBox(height: 8.0),
|
fontWeight: FontWeight.bold),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
),
|
||||||
vertical: 4.0, horizontal: 16.0),
|
for (final track in results.tracks!
|
||||||
child: Text(
|
.getRange(0, min(results.tracks!.length, 3)))
|
||||||
'Shows'.i18n,
|
TrackTile.fromTrack(track, onTap: () {
|
||||||
textAlign: TextAlign.left,
|
cache.addToSearchHistory(track);
|
||||||
style: const TextStyle(
|
playerHelper.playSearchMixDeferred(track);
|
||||||
fontSize: 20.0, fontWeight: FontWeight.bold),
|
}, onSecondary: (details) {
|
||||||
),
|
MenuSheet m = MenuSheet(context);
|
||||||
),
|
m.defaultTrackMenu(track, details: details);
|
||||||
for (final show in results.shows!
|
}),
|
||||||
.getRange(0, min(results.shows!.length, 3)))
|
ListTile(
|
||||||
ShowTile(
|
title: Text('Show all tracks'.i18n),
|
||||||
show,
|
onTap: () {
|
||||||
onTap: () async {
|
Navigator.of(context).pushRoute(
|
||||||
Navigator.of(context)
|
builder: (context) => TrackListScreen(
|
||||||
.pushRoute(builder: (context) => ShowScreen(show));
|
results.tracks, null));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
const FreezerDivider(),
|
||||||
title: Text('Show all shows'.i18n),
|
],
|
||||||
onTap: () {
|
if (results.albums != null &&
|
||||||
Navigator.of(context).pushRoute(
|
results.albums!.isNotEmpty) ...[
|
||||||
builder: (context) => ShowListScreen(results.shows));
|
const SizedBox(height: 8.0),
|
||||||
},
|
Padding(
|
||||||
),
|
key: _albumsKey,
|
||||||
const FreezerDivider()
|
padding: const EdgeInsets.symmetric(
|
||||||
],
|
horizontal: 16.0, vertical: 4.0),
|
||||||
if (results.episodes != null &&
|
child: Text(
|
||||||
results.episodes!.isNotEmpty) ...[
|
'Albums'.i18n,
|
||||||
const SizedBox(height: 8.0),
|
textAlign: TextAlign.left,
|
||||||
Padding(
|
style: const TextStyle(
|
||||||
padding: const EdgeInsets.symmetric(
|
fontSize: 20.0,
|
||||||
vertical: 4.0, horizontal: 16.0),
|
fontWeight: FontWeight.bold),
|
||||||
child: Text(
|
),
|
||||||
'Episodes'.i18n,
|
),
|
||||||
textAlign: TextAlign.left,
|
for (final album in results.albums!
|
||||||
style: const TextStyle(
|
.getRange(0, min(results.albums!.length, 3)))
|
||||||
fontSize: 20.0, fontWeight: FontWeight.bold),
|
AlbumTile(album, onSecondary: (details) {
|
||||||
),
|
MenuSheet m = MenuSheet(context);
|
||||||
),
|
m.defaultAlbumMenu(album, details: details);
|
||||||
for (final episode in results.episodes!
|
}, onTap: () {
|
||||||
.getRange(0, min(results.episodes!.length, 3)))
|
cache.addToSearchHistory(album);
|
||||||
ShowEpisodeTile(
|
Navigator.of(context).pushRoute(
|
||||||
episode,
|
builder: (context) =>
|
||||||
trailing: IconButton(
|
AlbumDetails(album));
|
||||||
icon: Icon(
|
}),
|
||||||
Icons.more_vert,
|
ListTile(
|
||||||
semanticLabel: "Options".i18n,
|
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));
|
|
||||||
})
|
|
||||||
]
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue