Pato05
f126ffef46
use get_url api by default, and fall back to old generation if get_url failed start to write a better cachemanager to implement in all systems write in more appropriate directories on windows and linux improve check for Connectivity by adding a fallback (needed for example on linux systems without NetworkManager) allow to dynamically change track quality without rebuilding the object
851 lines
32 KiB
Dart
851 lines
32 KiB
Dart
import 'dart:async';
|
|
import 'dart:math';
|
|
|
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:fluttericon/typicons_icons.dart';
|
|
import 'package:freezer/api/cache.dart';
|
|
import 'package:freezer/api/download.dart';
|
|
import 'package:freezer/api/player/audio_handler.dart';
|
|
import 'package:freezer/notifiers/list_notifier.dart';
|
|
import 'package:freezer/ui/details_screens.dart';
|
|
import 'package:freezer/ui/elements.dart';
|
|
import 'package:freezer/ui/menu.dart';
|
|
import 'package:freezer/translations.i18n.dart';
|
|
|
|
import 'tiles.dart';
|
|
import '../api/deezer.dart';
|
|
import '../api/definitions.dart';
|
|
import 'error.dart';
|
|
|
|
FutureOr openScreenByURL(BuildContext context, String url) async {
|
|
DeezerLinkResponse? res = await deezerAPI.parseLink(url);
|
|
if (res == null) return;
|
|
|
|
switch (res.type) {
|
|
case DeezerLinkType.TRACK:
|
|
Track t = await deezerAPI.track(res.id);
|
|
MenuSheet(context).defaultTrackMenu(t);
|
|
break;
|
|
case DeezerLinkType.ALBUM:
|
|
Album a = await deezerAPI.album(res.id);
|
|
return Navigator.of(context)
|
|
.push(MaterialPageRoute(builder: (context) => AlbumDetails(a)));
|
|
case DeezerLinkType.ARTIST:
|
|
Artist a = await deezerAPI.artist(res.id);
|
|
return Navigator.of(context)
|
|
.push(MaterialPageRoute(builder: (context) => ArtistDetails(a)));
|
|
case DeezerLinkType.PLAYLIST:
|
|
Playlist p = await deezerAPI.playlist(res.id);
|
|
return Navigator.of(context)
|
|
.push(MaterialPageRoute(builder: (context) => PlaylistDetails(p)));
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
class SearchScreen extends StatefulWidget {
|
|
final String? defaultText;
|
|
const SearchScreen({this.defaultText, Key? key}) : super(key: key);
|
|
@override
|
|
State<SearchScreen> createState() => _SearchScreenState();
|
|
}
|
|
|
|
class _SearchScreenState extends State<SearchScreen> {
|
|
bool _offline = false;
|
|
late final _controller = TextEditingController(text: widget.defaultText);
|
|
final _suggestions = ListNotifier<String>([]);
|
|
final _showingSuggestions = ValueNotifier(false);
|
|
final _loading = ValueNotifier(false);
|
|
Timer? _searchTimer;
|
|
final _focus = FocusNode();
|
|
final _textFieldFocusNode = FocusNode();
|
|
|
|
Future<void> _submit() async {
|
|
// dismiss keyboard
|
|
_textFieldFocusNode.unfocus();
|
|
|
|
if (_controller.text.isEmpty) return;
|
|
|
|
//URL
|
|
if (_controller.text.startsWith('http')) {
|
|
_loading.value = true;
|
|
try {
|
|
final f = openScreenByURL(context, _controller.text);
|
|
if (f is Future) {
|
|
f.whenComplete(() => _textFieldFocusNode.requestFocus());
|
|
}
|
|
} catch (e) {}
|
|
_loading.value = false;
|
|
return;
|
|
}
|
|
|
|
Navigator.of(context)
|
|
.pushRoute(
|
|
builder: (context) => SearchResultsScreen(
|
|
_controller.text,
|
|
offline: _offline,
|
|
))
|
|
.whenComplete(() => _textFieldFocusNode.requestFocus());
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
//Check for connectivity and enable offline mode
|
|
Connectivity().checkConnectivity().then((res) {
|
|
if (res == ConnectivityResult.none) {
|
|
setState(() {
|
|
_offline = true;
|
|
});
|
|
}
|
|
});
|
|
_suggestions.addListener(
|
|
() => _showingSuggestions.value = _suggestions.value.isNotEmpty);
|
|
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_focus.dispose();
|
|
_textFieldFocusNode.dispose();
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
//Load search suggestions
|
|
Future<void> _loadSuggestions() async {
|
|
if (_controller.text.isEmpty ||
|
|
_controller.text.length < 2 ||
|
|
_controller.text.startsWith('http')) return;
|
|
_loading.value = true;
|
|
//Load
|
|
List<String>? sugg;
|
|
try {
|
|
sugg = await deezerAPI.searchSuggestions(_controller.text);
|
|
} catch (e) {
|
|
print(e);
|
|
}
|
|
_loading.value = false;
|
|
|
|
if (sugg != null) _suggestions.value = sugg;
|
|
}
|
|
|
|
Widget _removeHistoryItemWidget(int index) {
|
|
return IconButton(
|
|
icon: Icon(
|
|
Icons.close,
|
|
semanticLabel: "Remove".i18n,
|
|
),
|
|
onPressed: () async {
|
|
cache.searchHistory.removeAt(index);
|
|
setState(() {});
|
|
await cache.save();
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
child: Scaffold(
|
|
appBar: PreferredSize(
|
|
preferredSize: const Size.fromHeight(76.0),
|
|
child: Column(
|
|
children: [
|
|
Expanded(
|
|
child: ListTile(
|
|
focusColor: Theme.of(context).listTileTheme.tileColor ??
|
|
Theme.of(context).colorScheme.background,
|
|
mouseCursor: MaterialStateMouseCursor.textable,
|
|
leading: Navigator.canPop(context)
|
|
? IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () => Navigator.pop(context))
|
|
: const Icon(Icons.search),
|
|
trailing: ValueListenableBuilder<TextEditingValue>(
|
|
valueListenable: _controller,
|
|
builder: (context, value, child) =>
|
|
value.text.isEmpty ? const SizedBox() : child!,
|
|
child: IconButton(
|
|
icon: Icon(
|
|
Icons.clear,
|
|
semanticLabel: "Clear".i18n,
|
|
),
|
|
onPressed: () {
|
|
_suggestions.clear();
|
|
_controller.clear();
|
|
})),
|
|
title: RawKeyboardListener(
|
|
focusNode: _focus,
|
|
onKey: (event) {
|
|
// For Android TV: quit search textfield
|
|
if (event is RawKeyUpEvent) {
|
|
LogicalKeyboardKey key = event.data.logicalKey;
|
|
if (key == LogicalKeyboardKey.arrowDown) {
|
|
_textFieldFocusNode.unfocus();
|
|
}
|
|
}
|
|
},
|
|
child: TextField(
|
|
onChanged: (query) {
|
|
if (query.isEmpty) {
|
|
_suggestions.clear();
|
|
} else {
|
|
_searchTimer ??= Timer(
|
|
const Duration(milliseconds: 300), () {
|
|
_searchTimer = null;
|
|
_loadSuggestions();
|
|
});
|
|
}
|
|
},
|
|
focusNode: _textFieldFocusNode,
|
|
autofocus: true,
|
|
decoration: InputDecoration(
|
|
hintText: 'Search or paste URL'.i18n,
|
|
border: InputBorder.none,
|
|
),
|
|
controller: _controller,
|
|
onSubmitted: (String s) => _submit(),
|
|
)),
|
|
onTap: () => _textFieldFocusNode.requestFocus()),
|
|
),
|
|
SizedBox(
|
|
height: 3.0,
|
|
child: ValueListenableBuilder<bool>(
|
|
valueListenable: _loading,
|
|
builder: (context, loading, _) => loading
|
|
? const LinearProgressIndicator()
|
|
: const SizedBox()),
|
|
)
|
|
],
|
|
)),
|
|
body: FocusScope(
|
|
child: ListView(
|
|
children: <Widget>[
|
|
ListTile(
|
|
title: Text('Offline search'.i18n),
|
|
leading: const Icon(Icons.offline_pin),
|
|
trailing: Switch(
|
|
value: _offline,
|
|
onChanged: (v) {
|
|
setState(() => _offline = !_offline);
|
|
},
|
|
),
|
|
),
|
|
const FreezerDivider(),
|
|
ValueListenableBuilder<bool>(
|
|
valueListenable: _showingSuggestions,
|
|
builder: (context, showingSuggestions, child) =>
|
|
showingSuggestions ? const SizedBox() : child!,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 8.0, horizontal: 16.0),
|
|
child: Text(
|
|
'Quick access'.i18n,
|
|
style: const TextStyle(
|
|
fontSize: 20.0, fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
SearchBrowseCard(
|
|
color: const Color(0xff11b192),
|
|
text: 'Flow'.i18n,
|
|
icon: const Icon(Typicons.waves),
|
|
onTap: () async {
|
|
await playerHelper.playFromSmartTrackList(
|
|
SmartTrackList(id: 'flow'));
|
|
},
|
|
),
|
|
if (cache.searchHistory.isNotEmpty) ...[
|
|
const FreezerDivider(),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 8.0, horizontal: 16.0),
|
|
child: Text(
|
|
'History'.i18n,
|
|
style: const TextStyle(
|
|
fontSize: 20.0, fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
...List.generate(min(cache.searchHistory.length, 10),
|
|
(int i) {
|
|
final data = cache.searchHistory[i].data;
|
|
switch (cache.searchHistory[i].type) {
|
|
case SearchHistoryItemType.track:
|
|
return TrackTile.fromTrack(
|
|
data,
|
|
onTap: () {
|
|
List<Track?> queue = cache.searchHistory
|
|
.where((h) =>
|
|
h.type == SearchHistoryItemType.track)
|
|
.map<Track>((t) => t.data)
|
|
.toList();
|
|
playerHelper.playFromTrackList(
|
|
queue,
|
|
data.id,
|
|
QueueSource(
|
|
text: 'Search history'.i18n,
|
|
source: 'searchhistory',
|
|
id: 'searchhistory'));
|
|
},
|
|
onSecondary: (details) => MenuSheet(context)
|
|
.defaultTrackMenu(data, details: details),
|
|
trailing: _removeHistoryItemWidget(i),
|
|
);
|
|
case SearchHistoryItemType.album:
|
|
return AlbumTile(
|
|
data,
|
|
onTap: () {
|
|
Navigator.of(context).pushRoute(
|
|
builder: (context) => AlbumDetails(data));
|
|
},
|
|
onSecondary: (details) => MenuSheet(context)
|
|
.defaultAlbumMenu(data, details: details),
|
|
trailing: _removeHistoryItemWidget(i),
|
|
);
|
|
case SearchHistoryItemType.artist:
|
|
return ArtistHorizontalTile(
|
|
data,
|
|
onTap: () {
|
|
Navigator.of(context).pushRoute(
|
|
builder: (context) =>
|
|
ArtistDetails(data));
|
|
},
|
|
onHold: () =>
|
|
MenuSheet(context).defaultArtistMenu(data),
|
|
trailing: _removeHistoryItemWidget(i),
|
|
);
|
|
case SearchHistoryItemType.playlist:
|
|
return PlaylistTile(
|
|
data,
|
|
onTap: () {
|
|
Navigator.of(context).pushRoute(
|
|
builder: (context) =>
|
|
PlaylistDetails(data));
|
|
},
|
|
onSecondary: (details) => MenuSheet(context)
|
|
.defaultPlaylistMenu(data,
|
|
details: details),
|
|
trailing: _removeHistoryItemWidget(i),
|
|
);
|
|
default:
|
|
return const SizedBox();
|
|
}
|
|
}),
|
|
if (cache.searchHistory.isNotEmpty)
|
|
ListTile(
|
|
title: Text('Clear search history'.i18n),
|
|
leading: const Icon(Icons.clear_all),
|
|
onTap: () {
|
|
cache.searchHistory.clear();
|
|
cache.save();
|
|
setState(() {});
|
|
},
|
|
),
|
|
],
|
|
])),
|
|
|
|
//Suggestions
|
|
ValueListenableBuilder<List<String>>(
|
|
valueListenable: _suggestions,
|
|
builder: (context, suggestions, _) => Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: List.generate(
|
|
suggestions.length,
|
|
(index) => ListTile(
|
|
title: Text(suggestions[index]),
|
|
leading: const Icon(Icons.search),
|
|
onTap: () {
|
|
setState(() =>
|
|
_controller.text = suggestions[index]);
|
|
_submit();
|
|
},
|
|
)))),
|
|
],
|
|
)),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class SearchBrowseCard extends StatelessWidget {
|
|
final Color color;
|
|
final Widget? icon;
|
|
final Function onTap;
|
|
final String text;
|
|
const SearchBrowseCard(
|
|
{super.key,
|
|
required this.color,
|
|
required this.onTap,
|
|
required this.text,
|
|
this.icon});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
color: color,
|
|
child: InkWell(
|
|
onTap: onTap as void Function()?,
|
|
child: SizedBox(
|
|
width: MediaQuery.of(context).size.width / 2 - 32,
|
|
height: 75,
|
|
child: Center(
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (icon != null) icon!,
|
|
if (icon != null) Container(width: 8.0),
|
|
Text(
|
|
text,
|
|
textAlign: TextAlign.center,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 18.0,
|
|
fontWeight: FontWeight.bold,
|
|
color: (color.computeLuminance() > 0.5)
|
|
? Colors.black
|
|
: Colors.white),
|
|
),
|
|
],
|
|
)),
|
|
),
|
|
));
|
|
}
|
|
}
|
|
|
|
class SearchResultsScreen extends StatelessWidget {
|
|
final String? query;
|
|
final bool? offline;
|
|
|
|
const SearchResultsScreen(this.query, {super.key, this.offline});
|
|
|
|
Future _search() async {
|
|
if (offline ?? false) {
|
|
return await downloadManager.search(query);
|
|
}
|
|
return await deezerAPI.search(query);
|
|
}
|
|
|
|
@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),
|
|
),
|
|
),
|
|
for (final track in results.tracks!
|
|
.getRange(0, min(results.tracks!.length, 3)))
|
|
TrackTile.fromTrack(track, onTap: () {
|
|
cache.addToSearchHistory(track);
|
|
playerHelper.playFromTrackList(
|
|
results.tracks!,
|
|
track.id,
|
|
QueueSource(
|
|
text: 'Search'.i18n,
|
|
id: query,
|
|
source: 'search'));
|
|
}, 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,
|
|
QueueSource(
|
|
id: query,
|
|
source: 'search',
|
|
text: 'Search'.i18n)));
|
|
},
|
|
),
|
|
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);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
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,
|
|
),
|
|
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));
|
|
})
|
|
]
|
|
],
|
|
);
|
|
},
|
|
));
|
|
}
|
|
}
|
|
|
|
//List all tracks
|
|
class TrackListScreen extends StatelessWidget {
|
|
final QueueSource queueSource;
|
|
final List<Track?>? tracks;
|
|
|
|
const TrackListScreen(this.tracks, this.queueSource, {super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text('Tracks'.i18n)),
|
|
body: ListView.builder(
|
|
itemCount: tracks!.length,
|
|
itemBuilder: (BuildContext context, int i) {
|
|
Track t = tracks![i]!;
|
|
return TrackTile.fromTrack(
|
|
t,
|
|
onTap: () {
|
|
playerHelper.playFromTrackList(tracks!, t.id, queueSource);
|
|
},
|
|
onSecondary: (details) {
|
|
MenuSheet m = MenuSheet(context);
|
|
m.defaultTrackMenu(t, details: details);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
//List all albums
|
|
class AlbumListScreen extends StatelessWidget {
|
|
final List<Album?>? albums;
|
|
const AlbumListScreen(this.albums, {super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text('Albums'.i18n)),
|
|
body: ListView.builder(
|
|
itemCount: albums!.length,
|
|
itemBuilder: (context, i) {
|
|
Album? a = albums![i];
|
|
return AlbumTile(
|
|
a,
|
|
onTap: () {
|
|
Navigator.of(context)
|
|
.pushRoute(builder: (context) => AlbumDetails(a));
|
|
},
|
|
onSecondary: (details) {
|
|
MenuSheet m = MenuSheet(context);
|
|
m.defaultAlbumMenu(a!, details: details);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class SearchResultPlaylists extends StatelessWidget {
|
|
final List<Playlist?>? playlists;
|
|
const SearchResultPlaylists(this.playlists, {super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text('Playlists'.i18n)),
|
|
body: ListView.builder(
|
|
itemCount: playlists!.length,
|
|
itemBuilder: (context, i) {
|
|
Playlist? p = playlists![i];
|
|
return PlaylistTile(
|
|
p,
|
|
onTap: () {
|
|
Navigator.of(context)
|
|
.pushRoute(builder: (context) => PlaylistDetails(p));
|
|
},
|
|
onSecondary: (details) {
|
|
MenuSheet m = MenuSheet(context);
|
|
m.defaultPlaylistMenu(p!, details: details);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ShowListScreen extends StatelessWidget {
|
|
final List<Show>? shows;
|
|
const ShowListScreen(this.shows, {super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text('Shows'.i18n)),
|
|
body: ListView.builder(
|
|
itemCount: shows!.length,
|
|
itemBuilder: (context, i) {
|
|
Show s = shows![i];
|
|
return ShowTile(
|
|
s,
|
|
onTap: () {
|
|
Navigator.of(context)
|
|
.push(MaterialPageRoute(builder: (context) => ShowScreen(s)));
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class EpisodeListScreen extends StatelessWidget {
|
|
final List<ShowEpisode>? episodes;
|
|
const EpisodeListScreen(this.episodes, {super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text('Episodes'.i18n)),
|
|
body: ListView.builder(
|
|
itemCount: episodes!.length,
|
|
itemBuilder: (context, i) {
|
|
ShowEpisode e = episodes![i];
|
|
return ShowEpisodeTile(
|
|
e,
|
|
trailing: IconButton(
|
|
icon: Icon(
|
|
Icons.more_vert,
|
|
semanticLabel: "Options".i18n,
|
|
),
|
|
onPressed: () {
|
|
MenuSheet m = MenuSheet(context);
|
|
m.defaultShowEpisodeMenu(e.show!, e);
|
|
},
|
|
),
|
|
onTap: () async {
|
|
//Load entire show, then play
|
|
List<ShowEpisode> episodes =
|
|
(await deezerAPI.allShowEpisodes(e.show!.id))!;
|
|
await playerHelper.playShowEpisode(e.show!, episodes,
|
|
index: episodes.indexWhere((ep) => e.id == ep.id));
|
|
},
|
|
);
|
|
},
|
|
));
|
|
}
|
|
}
|