Pato05
2a5a51e43f
fix lyrics add right click action to AlbumCard add desktop file script for linux automated tarball creation for linux don't preload old queue
862 lines
32 KiB
Dart
862 lines
32 KiB
Dart
import 'dart:async';
|
|
import 'dart:math';
|
|
|
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:freezer/api/cache.dart';
|
|
import 'package:freezer/api/download.dart';
|
|
import 'package:freezer/api/player/audio_handler.dart';
|
|
import 'package:freezer/icons.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, optionsTop: [
|
|
MenuSheetOption(Text('Play'.i18n),
|
|
icon: const Icon(Icons.play_arrow),
|
|
onTap: () => playerHelper.playSearchMixDeferred(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);
|
|
if (p.tracks == null || p.tracks!.isEmpty) {
|
|
ScaffoldMessenger.of(context)
|
|
.snack('The playlist is empty or private.'.i18n);
|
|
return;
|
|
}
|
|
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);
|
|
CancelToken? _searchCancelToken;
|
|
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;
|
|
_searchCancelToken?.cancel();
|
|
|
|
//Load
|
|
final List<String>? suggestions;
|
|
try {
|
|
_searchCancelToken = CancelToken();
|
|
suggestions = await deezerAPI.searchSuggestions(_controller.text,
|
|
cancelToken: _searchCancelToken);
|
|
} on DioException catch (e) {
|
|
if (e.type != DioExceptionType.cancel) rethrow;
|
|
return;
|
|
} catch (e) {
|
|
print(e);
|
|
return;
|
|
}
|
|
|
|
_loading.value = false;
|
|
if (suggestions != null) _suggestions.value = suggestions;
|
|
}
|
|
|
|
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: 1), () {
|
|
_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(FreezerIcons.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) {
|
|
switch (cache.searchHistory[i]) {
|
|
case final Track data:
|
|
return TrackTile.fromTrack(
|
|
data,
|
|
onTap: () {
|
|
final queue = cache.searchHistory
|
|
.whereType<Track>()
|
|
.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 final Album data:
|
|
return AlbumTile(
|
|
data,
|
|
onTap: () {
|
|
Navigator.of(context).pushRoute(
|
|
builder: (context) => AlbumDetails(data));
|
|
},
|
|
onSecondary: (details) => MenuSheet(context)
|
|
.defaultAlbumMenu(data, details: details),
|
|
trailing: _removeHistoryItemWidget(i),
|
|
);
|
|
case final Artist data:
|
|
return ArtistHorizontalTile(
|
|
data,
|
|
onTap: () {
|
|
Navigator.of(context).pushRoute(
|
|
builder: (context) =>
|
|
ArtistDetails(data));
|
|
},
|
|
onHold: () =>
|
|
MenuSheet(context).defaultArtistMenu(data),
|
|
trailing: _removeHistoryItemWidget(i),
|
|
);
|
|
case final Playlist data:
|
|
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.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);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
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: () {
|
|
if (queueSource == null) {
|
|
playerHelper.playSearchMixDeferred(t);
|
|
return;
|
|
}
|
|
|
|
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));
|
|
},
|
|
);
|
|
},
|
|
));
|
|
}
|
|
}
|