freezer/lib/ui/details_screens.dart
Pato05 2862c9ec05
remove browser login for desktop
restore translations functionality
make scrollViews handle mouse pointers like touch, so that pull to refresh functionality is available
exit app if opening cache or settings fails (another instance running)
remove draggable_scrollbar and use builtin widget instead
fix email login
better way to manage lyrics (less updates and lookups in the lyrics List)
fix player_screen on mobile (too big -> just average :))
right click: use TapUp events instead
desktop: show context menu on triple dots button also
avoid showing connection error if the homepage is cached and available offline
i'm probably forgetting something idk
2023-10-25 00:32:28 +02:00

1289 lines
47 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:fluttericon/font_awesome5_icons.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/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,
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.max,
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);
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;
});
//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.tight(
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!
? FontAwesome5.sort_alpha_up
: FontAwesome5.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);
},
);
})
],
),
);
}
}