freezer/lib/ui/details_screens.dart

1300 lines
47 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/api/player/audio_handler.dart';
import 'package:freezer/icons.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/search.dart';
import 'package:freezer/translations.i18n.dart';
import '../api/definitions.dart';
import 'cached_image.dart';
import 'tiles.dart';
import 'menu.dart';
class AlbumDetails extends StatefulWidget {
final Album? album;
const AlbumDetails(this.album, {Key? key}) : super(key: key);
@override
State<AlbumDetails> createState() => _AlbumDetailsState();
}
class _AlbumDetailsState extends State<AlbumDetails> {
Album? album;
bool _loading = true;
bool _error = false;
Future _loadAlbum() async {
//Get album from API, if doesn't have tracks
if (album!.tracks == null || album!.tracks!.isEmpty) {
try {
Album a = await deezerAPI.album(album!.id);
//Preserve library
a.library = album!.library;
setState(() => album = a);
} catch (e) {
setState(() => _error = true);
}
}
setState(() => _loading = false);
}
//Get count of CDs in album
int? get cdCount {
int? c = 1;
for (Track? t in album!.tracks!) {
if ((t!.diskNumber ?? 1) > c!) c = t.diskNumber;
}
return c;
}
@override
void initState() {
album = widget.album;
_loadAlbum();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(album!.title ?? ''),
),
body: _error
? const ErrorScreen()
: _loading
? const Center(child: CircularProgressIndicator())
: ListView(
children: <Widget>[
//Album art, title, artists
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const SizedBox(height: 8.0),
ConstrainedBox(
constraints: BoxConstraints.loose(
MediaQuery.of(context).size / 2.5),
child: ZoomableImage(
url: album!.art!.full,
rounded: true,
),
),
const SizedBox(height: 8.0),
Text(
album!.title!,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: const TextStyle(
fontSize: 20.0, fontWeight: FontWeight.bold),
),
Text(
album!.artistString,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: TextStyle(
fontSize: 16.0,
color: Theme.of(context).primaryColor),
),
const SizedBox(height: 4.0),
if (album!.releaseDate != null &&
album!.releaseDate!.length >= 4)
Text(
album!.releaseDate!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.0,
color: Theme.of(context).disabledColor),
),
const SizedBox(height: 8.0),
],
),
const FreezerDivider(),
//Details
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
children: <Widget>[
Icon(
Icons.audiotrack,
size: 32.0,
semanticLabel: "Tracks".i18n,
),
const SizedBox(
width: 8.0,
height: 42.0,
), //Height to adjust card height
Text(
album!.tracks!.length.toString(),
style: const TextStyle(fontSize: 16.0),
)
],
),
Row(
children: <Widget>[
Icon(
Icons.timelapse,
size: 32.0,
semanticLabel: "Duration".i18n,
),
Container(
width: 8.0,
),
Text(
album!.durationString,
style: const TextStyle(fontSize: 16.0),
)
],
),
Row(
children: <Widget>[
Icon(Icons.people,
size: 32.0, semanticLabel: "Fans".i18n),
Container(
width: 8.0,
),
Text(
album!.fansString,
style: const TextStyle(fontSize: 16.0),
)
],
),
],
),
const FreezerDivider(),
//Options (offline, download...)
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
TextButton.icon(
icon: Icon((album!.library ?? false)
? Icons.favorite
: Icons.favorite_border),
label: Text('Library'.i18n),
onPressed: () async {
//Add to library
if (!album!.library!) {
await deezerAPI.addFavoriteAlbum(album!.id);
ScaffoldMessenger.of(context)
.snack('Added to library'.i18n);
setState(() => album!.library = true);
return;
}
//Remove
await deezerAPI.removeAlbum(album!.id);
ScaffoldMessenger.of(context)
.snack('Album removed from library!'.i18n);
setState(() => album!.library = false);
},
),
MakeAlbumOffline(album: album),
TextButton.icon(
icon: const Icon(Icons.file_download),
label: Text('Download'.i18n),
onPressed: () async {
if (await downloadManager.addOfflineAlbum(album,
private: false, context: context) !=
false) {
MenuSheet(context).showDownloadStartedToast();
}
},
)
],
),
const FreezerDivider(),
...List.generate(cdCount!, (cdi) {
List<Track?> tracks = album!.tracks!
.where((t) => (t.diskNumber ?? 1) == cdi + 1)
.toList();
return Column(
children: [
Padding(
padding:
const EdgeInsets.symmetric(vertical: 4.0),
child: Text(
'${'Disk'.i18n.toUpperCase()} ${cdi + 1}',
style: const TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.w300),
),
),
...List.generate(
tracks.length,
(i) =>
TrackTile.fromTrack(tracks[i]!, onTap: () {
playerHelper.playFromAlbum(
album!, tracks[i]!.id);
}, onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultTrackMenu(tracks[i]!,
details: details);
}))
],
);
}),
],
));
}
}
class MakeAlbumOffline extends StatefulWidget {
final Album? album;
const MakeAlbumOffline({Key? key, this.album}) : super(key: key);
@override
State<MakeAlbumOffline> createState() => _MakeAlbumOfflineState();
}
class _MakeAlbumOfflineState extends State<MakeAlbumOffline> {
bool _offline = false;
@override
void initState() {
downloadManager.checkOffline(album: widget.album).then((v) {
setState(() {
_offline = v;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Switch(
value: _offline,
onChanged: (v) async {
if (v) {
//Add to offline
await deezerAPI.addFavoriteAlbum(widget.album!.id);
downloadManager.addOfflineAlbum(widget.album, private: true);
MenuSheet(context).showDownloadStartedToast();
setState(() {
_offline = true;
});
return;
}
downloadManager.removeOfflineAlbum(widget.album!.id);
ScaffoldMessenger.of(context)
.snack("Removed album from offline!".i18n);
setState(() {
_offline = false;
});
},
),
const SizedBox(width: 4.0),
Text(
'Offline'.i18n,
style: const TextStyle(fontSize: 16),
)
],
);
}
}
class ArtistDetails extends StatefulWidget {
final Artist artist;
const ArtistDetails(this.artist, {super.key});
@override
State<ArtistDetails> createState() => _ArtistDetailsState();
}
class _ArtistDetailsState extends State<ArtistDetails> {
late final Future<Artist> _future;
@override
void initState() {
_future = _loadArtist(widget.artist);
super.initState();
}
Future<Artist> _loadArtist(Artist artist) async {
//Load artist from api if no albums
if ((artist.albums ?? []).isEmpty) {
return await deezerAPI.artist(artist.id);
}
return artist;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.artist.name ?? '')),
body: FutureBuilder<Artist>(
future: _future,
builder: (BuildContext context, snapshot) {
//Error / not done
if (snapshot.hasError) return const ErrorScreen();
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
final artist = snapshot.data!;
return ListView(
children: <Widget>[
const SizedBox(height: 4.0),
Padding(
padding: const EdgeInsets.all(16.0),
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height / 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Flexible(
child: ZoomableImage(
url: artist.picture!.full,
rounded: true,
),
),
SizedBox(
width: min(
MediaQuery.of(context).size.width / 16, 60.0)),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
artist.name!,
overflow: TextOverflow.ellipsis,
maxLines: 4,
style: const TextStyle(
fontSize: 24.0, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8.0),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
Icons.people,
size: 32.0,
semanticLabel: "Fans".i18n,
),
const SizedBox(width: 8.0),
Text(
artist.fansString,
style: const TextStyle(fontSize: 16),
),
],
),
const SizedBox(height: 4.0),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
Icons.album,
size: 32.0,
semanticLabel: "Albums".i18n,
),
const SizedBox(width: 8.0),
Text(
widget.artist.albumCount.toString(),
style: const TextStyle(fontSize: 16),
)
],
)
],
),
),
],
),
),
),
const FreezerDivider(),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
TextButton.icon(
icon: const Icon(Icons.favorite),
label: Text('Library'.i18n),
onPressed: () async {
await deezerAPI.addFavoriteArtist(widget.artist.id);
ScaffoldMessenger.of(context)
.snack('Added to library'.i18n);
},
),
if ((artist.radio ?? false))
TextButton.icon(
icon: const Icon(Icons.radio),
label: Text('Radio'.i18n),
onPressed: () async {
List<Track> tracks =
(await deezerAPI.smartRadio(artist.id))!;
playerHelper.playFromTrackList(
tracks,
tracks[0].id,
QueueSource(
id: artist.id,
text: '${'Radio'.i18n} ${artist.name}',
source: 'smartradio'));
},
)
],
),
const FreezerDivider(),
const SizedBox(height: 12.0),
//Highlight
if (artist.highlight != null)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 2.0),
child: Text(
artist.highlight!.title!,
textAlign: TextAlign.left,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 20.0),
),
),
if (artist.highlight!.type == ArtistHighlightType.ALBUM)
AlbumTile(
artist.highlight!.data,
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) =>
AlbumDetails(artist.highlight!.data));
},
),
const SizedBox(height: 8.0)
],
),
//Top tracks
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 2.0),
child: Text(
'Top Tracks'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 20.0),
),
),
const SizedBox(height: 4.0),
...List.generate(5, (i) {
if (artist.topTracks!.length <= i) {
return const SizedBox(height: 0.0, width: 0.0);
}
Track t = artist.topTracks![i];
return TrackTile.fromTrack(
t,
onTap: () {
playerHelper.playFromTopTracks(
artist.topTracks!, t.id, artist);
},
onSecondary: (details) {
MenuSheet mi = MenuSheet(context);
mi.defaultTrackMenu(t, details: details);
},
);
}),
ListTile(
title: Text('Show more tracks'.i18n),
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) => TrackListScreen(
artist.topTracks,
QueueSource(
id: artist.id,
text: '${'Top'.i18n}${artist.name}',
source: 'topTracks')));
}),
const FreezerDivider(),
//Albums
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Text(
'Top Albums'.i18n,
textAlign: TextAlign.left,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 20.0),
),
),
...List.generate(
artist.albums!.length > 10 ? 11 : artist.albums!.length + 1,
(i) {
//Show discography
if (i == 10 || i == artist.albums!.length) {
return ListTile(
title: Text('Show all albums'.i18n),
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) => DiscographyScreen(
artist: artist,
));
});
}
//Top albums
Album a = artist.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 DiscographyScreen extends StatefulWidget {
final Artist? artist;
const DiscographyScreen({required this.artist, Key? key}) : super(key: key);
@override
State<DiscographyScreen> createState() => _DiscographyScreenState();
}
class _DiscographyScreenState extends State<DiscographyScreen> {
Artist? artist;
bool _loading = false;
bool _error = false;
final List<ScrollController> _controllers = [
ScrollController(),
ScrollController(),
ScrollController()
];
Future _load() async {
if (artist!.albums!.length >= artist!.albumCount! || _loading) return;
setState(() => _loading = true);
//Fetch data
List<Album>? data;
try {
data = await deezerAPI.discographyPage(artist!.id,
start: artist!.albums!.length);
} catch (e) {
setState(() {
_error = true;
_loading = false;
});
return;
}
//Save
setState(() {
artist!.albums!.addAll(data!);
_loading = false;
});
}
//Get album tile
Widget _tile(Album a) => AlbumTile(
a,
onTap: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => AlbumDetails(a))),
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultAlbumMenu(a, details: details);
},
);
Widget get _loadingWidget {
if (_loading) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [CircularProgressIndicator()],
),
);
}
//Error
if (_error) return const ErrorScreen();
//Success
return const SizedBox(width: 0.0, height: 0.0);
}
@override
void initState() {
artist = widget.artist;
//Lazy loading scroll
for (var _c in _controllers) {
_c.addListener(() {
double off = _c.position.maxScrollExtent * 0.85;
if (_c.position.pixels > off) _load();
});
}
super.initState();
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Builder(builder: (BuildContext context) {
final TabController tabController = DefaultTabController.of(context);
tabController.addListener(() {
if (!tabController.indexIsChanging) {
//Load data if empty tabs
int nSingles = artist!.albums!
.where((a) => a.type == AlbumType.SINGLE)
.length;
int nFeatures = artist!.albums!
.where((a) => a.type == AlbumType.FEATURED)
.length;
if ((nSingles == 0 || nFeatures == 0) && !_loading) _load();
}
});
return Scaffold(
appBar: AppBar(
title: Text('Discography'.i18n),
bottom: TabBar(
tabs: [
Tab(
icon: Icon(
Icons.album,
semanticLabel: "Albums".i18n,
)),
Tab(
icon: Icon(Icons.audiotrack,
semanticLabel: "Singles".i18n)),
Tab(
icon: Icon(
Icons.recent_actors,
semanticLabel: "Featured".i18n,
))
],
),
toolbarHeight: 100.0,
),
body: TabBarView(
children: [
//Albums
ListView.builder(
controller: _controllers[0],
itemCount: artist!.albums!.length + 1,
itemBuilder: (context, i) {
if (i == artist!.albums!.length) return _loadingWidget;
if (artist!.albums![i].type == AlbumType.ALBUM) {
return _tile(artist!.albums![i]);
}
return const SizedBox(width: 0.0, height: 0.0);
},
),
//Singles
ListView.builder(
controller: _controllers[1],
itemCount: artist!.albums!.length + 1,
itemBuilder: (context, i) {
if (i == artist!.albums!.length) return _loadingWidget;
if (artist!.albums![i].type == AlbumType.SINGLE) {
return _tile(artist!.albums![i]);
}
return const SizedBox(width: 0.0, height: 0.0);
},
),
//Featured
ListView.builder(
controller: _controllers[2],
itemCount: artist!.albums!.length + 1,
itemBuilder: (context, i) {
if (i == artist!.albums!.length) return _loadingWidget;
if (artist!.albums![i].type == AlbumType.FEATURED) {
return _tile(artist!.albums![i]);
}
return const SizedBox(width: 0.0, height: 0.0);
},
),
],
),
);
}));
}
}
class PlaylistDetails extends StatefulWidget {
final Playlist? playlist;
const PlaylistDetails(this.playlist, {Key? key}) : super(key: key);
@override
State<PlaylistDetails> createState() => _PlaylistDetailsState();
}
class _PlaylistDetailsState extends State<PlaylistDetails> {
Playlist? playlist;
bool _loading = false;
bool _error = false;
Sorting? _sort;
final ScrollController _scrollController = ScrollController();
//Get sorted playlist
List<Track> get sorted {
List<Track> tracks = List.from(playlist!.tracks ?? []);
switch (_sort!.type) {
case SortType.ALPHABETIC:
tracks.sort((a, b) => a.title!.compareTo(b.title!));
break;
case SortType.ARTIST:
tracks.sort((a, b) => a.artists![0].name!
.toLowerCase()
.compareTo(b.artists![0].name!.toLowerCase()));
break;
case SortType.DATE_ADDED:
tracks.sort((a, b) => (a.addedDate ?? 0) - (b.addedDate ?? 0));
break;
case SortType.DEFAULT:
default:
break;
}
//Reverse
if (_sort!.reverse!) return tracks.reversed.toList();
return tracks;
}
//Load tracks from api
void _load() async {
if (playlist!.tracks!.length <
(playlist!.trackCount ?? playlist!.tracks!.length) &&
!_loading) {
setState(() => _loading = true);
int pos = playlist!.tracks!.length;
//Get another page of tracks
List<Track>? tracks;
try {
tracks = await deezerAPI.playlistTracksPage(playlist!.id, pos);
} catch (e) {
setState(() {
_error = true;
_loading = false;
});
return;
}
if (tracks == null || tracks.isEmpty) {
setState(() => _error = true);
return;
}
setState(() {
playlist!.tracks!.addAll(tracks!);
_loading = false;
});
}
}
//Load cached playlist sorting
void _restoreSort() async {
//Find index
int? index = Sorting.index(SortSourceTypes.PLAYLIST, id: playlist!.id);
if (index == null) return;
//Preload tracks
if (playlist!.tracks!.length < playlist!.trackCount!) {
playlist = await deezerAPI.fullPlaylist(playlist!.id);
}
setState(() => _sort = cache.sorts[index]);
}
Future _reverse() async {
setState(() => _sort!.reverse = !_sort!.reverse!);
//Save sorting in cache
int? index = Sorting.index(SortSourceTypes.TRACKS);
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
//Preload for sorting
if (playlist!.tracks!.length < playlist!.trackCount!) {
playlist = await deezerAPI.fullPlaylist(playlist!.id);
}
}
@override
void initState() {
playlist = widget.playlist;
_sort = Sorting(sourceType: SortSourceTypes.PLAYLIST, id: playlist!.id);
//If scrolled past 90% load next tracks
_scrollController.addListener(() {
double off = _scrollController.position.maxScrollExtent * 0.90;
if (_scrollController.position.pixels > off) {
_load();
}
});
//Load if no tracks
if (playlist!.tracks!.isEmpty) {
//Get correct metadata
deezerAPI.playlist(playlist!.id).then((Playlist p) {
setState(() {
playlist = p;
});
// update cache
cache.favoritePlaylists?.value[playlist!.id] = p;
//Load tracks
_load();
}).catchError((e) {
setState(() => _error = true);
});
}
_restoreSort();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(playlist!.title ?? '')),
body: Scrollbar(
interactive: true,
controller: _scrollController,
thickness: 8.0,
child: ListView(
controller: _scrollController,
children: <Widget>[
const SizedBox(height: 4.0),
ConstrainedBox(
constraints: BoxConstraints.loose(
Size.fromHeight(MediaQuery.of(context).size.height / 3)),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 4.0, horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Flexible(
child: CachedImage(
url: playlist!.image!.full,
rounded: true,
fullThumb: true,
),
),
SizedBox(
width: min(
MediaQuery.of(context).size.width / 16, 60.0)),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
playlist!.title!,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,
maxLines: 3,
style: const TextStyle(
fontSize: 20.0, fontWeight: FontWeight.bold),
),
Text(
playlist!.user!.name ?? '',
overflow: TextOverflow.ellipsis,
maxLines: 2,
textAlign: TextAlign.start,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontSize: 17.0),
),
const SizedBox(height: 16.0),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
Icons.audiotrack,
size: 20.0,
semanticLabel: "Tracks".i18n,
),
const SizedBox(width: 8.0),
Text(
(playlist!.trackCount ??
playlist!.tracks!.length)
.toString(),
style: const TextStyle(fontSize: 16),
)
],
),
const SizedBox(height: 6.0),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
Icons.timelapse,
size: 32.0,
semanticLabel: "Duration".i18n,
),
const SizedBox(width: 8.0),
Text(
playlist!.durationString,
style: const TextStyle(fontSize: 16),
)
],
),
],
),
),
],
),
),
),
if (playlist!.description != null &&
playlist!.description!.isNotEmpty)
const FreezerDivider(),
if (playlist!.description != null &&
playlist!.description!.isNotEmpty)
Padding(
padding: const EdgeInsets.all(6.0),
child: Text(
playlist!.description ?? '',
maxLines: 4,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16.0),
),
),
const FreezerDivider(),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
MakePlaylistOffline(playlist),
if (playlist!.user!.name != deezerAPI.userName)
IconButton(
icon: Icon(
playlist!.library!
? Icons.favorite
: Icons.favorite_outline,
size: 32,
semanticLabel:
playlist!.library! ? "Unlove".i18n : "Love".i18n,
),
onPressed: () async {
//Add to library
if (!playlist!.library!) {
await deezerAPI.addPlaylist(playlist!.id);
ScaffoldMessenger.of(context)
.snack('Added to library'.i18n);
setState(() => playlist!.library = true);
return;
}
//Remove
await deezerAPI.removePlaylist(playlist!.id);
ScaffoldMessenger.of(context)
.snack('Playlist removed from library!'.i18n);
setState(() => playlist!.library = false);
},
),
IconButton(
icon: Icon(
Icons.file_download,
size: 32.0,
semanticLabel: "Download".i18n,
),
onPressed: () async {
if (await downloadManager.addOfflinePlaylist(playlist,
private: false, context: context) !=
false) MenuSheet(context).showDownloadStartedToast();
},
),
PopupMenuButton(
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (SortType s) async {
if (playlist!.tracks!.length < playlist!.trackCount!) {
//Preload whole playlist
playlist = await deezerAPI.fullPlaylist(playlist!.id);
}
setState(() => _sort!.type = s);
//Save sort type to cache
int? index = Sorting.index(SortSourceTypes.PLAYLIST,
id: playlist!.id);
if (index == null) {
cache.sorts.add(_sort);
} else {
cache.sorts[index] = _sort;
}
await cache.save();
},
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
PopupMenuItem(
value: SortType.DEFAULT,
child:
Text('Default'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.ALPHABETIC,
child: Text('Alphabetic'.i18n,
style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.ARTIST,
child: Text('Artist'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.DATE_ADDED,
child: Text('Date added'.i18n,
style: popupMenuTextStyle()),
),
],
child: Icon(
Icons.sort,
size: 32.0,
semanticLabel: "Sort playlist".i18n,
),
),
IconButton(
icon: Icon(
_sort!.reverse!
? FreezerIcons.sort_alpha_up
: FreezerIcons.sort_alpha_down,
semanticLabel: _sort!.reverse!
? "Sort descending".i18n
: "Sort ascending".i18n,
),
onPressed: () => _reverse(),
),
Container(width: 4.0)
],
),
const FreezerDivider(),
if (playlist!.tracks!.isEmpty)
const Center(child: CircularProgressIndicator()),
...List.generate(playlist!.tracks!.length, (i) {
Track t = sorted[i];
return TrackTile.fromTrack(t, onTap: () {
Playlist p = Playlist(
title: playlist!.title, id: playlist!.id, tracks: sorted);
playerHelper.playFromPlaylist(p, t.id);
}, onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultTrackMenu(t, details: details, options: [
if (playlist!.user!.id == deezerAPI.userId)
m.removeFromPlaylist(t, playlist)
]);
});
}),
if (_loading)
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[CircularProgressIndicator()],
),
),
if (_error) const ErrorScreen()
],
),
));
}
}
class MakePlaylistOffline extends StatefulWidget {
final Playlist? playlist;
const MakePlaylistOffline(this.playlist, {Key? key}) : super(key: key);
@override
State<MakePlaylistOffline> createState() => _MakePlaylistOfflineState();
}
class _MakePlaylistOfflineState extends State<MakePlaylistOffline> {
bool _offline = false;
@override
void initState() {
downloadManager.checkOffline(playlist: widget.playlist).then((v) {
if (mounted) {
setState(() {
_offline = v;
});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Switch(
value: _offline,
onChanged: (v) async {
if (v) {
//Add to offline
if (widget.playlist!.user != null &&
widget.playlist!.user!.id != deezerAPI.userId) {
await deezerAPI.addPlaylist(widget.playlist!.id);
}
downloadManager.addOfflinePlaylist(widget.playlist,
private: true);
MenuSheet(context).showDownloadStartedToast();
setState(() {
_offline = true;
});
return;
}
downloadManager.removeOfflinePlaylist(widget.playlist!.id);
ScaffoldMessenger.of(context)
.snack("Playlist removed from offline!".i18n);
setState(() {
_offline = false;
});
},
),
const SizedBox(width: 4.0),
Text(
'Offline'.i18n,
style: const TextStyle(fontSize: 16),
)
],
);
}
}
class ShowScreen extends StatefulWidget {
final Show? show;
const ShowScreen(this.show, {Key? key}) : super(key: key);
@override
State<ShowScreen> createState() => _ShowScreenState();
}
class _ShowScreenState extends State<ShowScreen> {
Show? _show;
bool _loading = true;
bool _error = false;
List<ShowEpisode>? _episodes;
Future _load() async {
//Fetch
List<ShowEpisode>? e;
try {
e = await deezerAPI.allShowEpisodes(_show!.id);
} catch (e) {
setState(() {
_loading = false;
_error = true;
});
return;
}
setState(() {
_episodes = e;
_loading = false;
});
}
@override
void initState() {
_show = widget.show;
_load();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(_show!.name!)),
body: ListView(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height / 3),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
child: AspectRatio(
aspectRatio: 1.0,
child: CachedImage(
url: _show!.art!.full,
rounded: true,
),
),
),
SizedBox(
width: min(MediaQuery.of(context).size.width / 16, 60.0)),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_show!.name!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 20.0, fontWeight: FontWeight.bold)),
const SizedBox(height: 16.0),
Text(
_show!.description!,
maxLines: 6,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 16.0),
)
],
),
)
],
),
),
),
const SizedBox(height: 4.0),
const FreezerDivider(),
//Error
if (_error) const ErrorScreen(),
//Loading
if (_loading) const Center(child: CircularProgressIndicator()),
//Data
if (!_loading && !_error)
...List.generate(_episodes!.length, (i) {
ShowEpisode e = _episodes![i];
return ShowEpisodeTile(
e,
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultShowEpisodeMenu(_show!, e, details: details);
},
onTap: () async {
await playerHelper.playShowEpisode(_show!, _episodes!,
index: i);
},
);
})
],
),
);
}
}