import 'dart:async'; import 'dart:math'; import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.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.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 createState() => _AlbumDetailsState(); } class _AlbumDetailsState extends State { 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( body: _error ? const ErrorScreen() : _loading ? const Center(child: CircularProgressIndicator()) : ListView( children: [ //Album art, title, artists Container( color: Theme.of(context).scaffoldBackgroundColor, child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8.0), ConstrainedBox( constraints: BoxConstraints.loose( MediaQuery.of(context).size / 3), 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: [ Row( children: [ 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: [ Icon( Icons.timelapse, size: 32.0, semanticLabel: "Duration".i18n, ), Container( width: 8.0, ), Text( album!.durationString, style: const TextStyle(fontSize: 16.0), ) ], ), Row( children: [ 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: [ 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 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 createState() => _MakeAlbumOfflineState(); } class _MakeAlbumOfflineState extends State { 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: [ 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 createState() => _ArtistDetailsState(); } class _ArtistDetailsState extends State { late final Future _future; void initState() { FutureOr future = _loadArtist(widget.artist); if (future is Artist) { _future = Future.value(widget.artist); } else { _future = future; } super.initState(); } FutureOr _loadArtist(Artist artist) { //Load artist from api if no albums if ((artist.albums ?? []).isEmpty) { return deezerAPI.artist(artist.id); } return artist; } @override Widget build(BuildContext context) { return Scaffold( body: FutureBuilder( 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: [ const SizedBox(height: 4.0), Padding( padding: const EdgeInsets.all(16.0), child: SizedBox( height: MediaQuery.of(context).size.height / 3, child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Flexible( child: ZoomableImage( url: widget.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: [ 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: [ 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: [ 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: [ TextButton( child: Row( children: [ const Icon(Icons.favorite, size: 32), const SizedBox(width: 4.0), Text('Library'.i18n) ], ), onPressed: () async { await deezerAPI.addFavoriteArtist(widget.artist.id); ScaffoldMessenger.of(context) .snack('Added to library'.i18n); }, ), if ((artist.radio ?? false)) TextButton( child: Row( children: [ const Icon(Icons.radio, size: 32), const SizedBox(width: 4.0), Text('Radio'.i18n) ], ), onPressed: () async { List 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 createState() => _DiscographyScreenState(); } class _DiscographyScreenState extends State { Artist? artist; bool _loading = false; bool _error = false; final List _controllers = [ ScrollController(), ScrollController(), ScrollController() ]; Future _load() async { if (artist!.albums!.length >= artist!.albumCount! || _loading) return; setState(() => _loading = true); //Fetch data List? 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 createState() => _PlaylistDetailsState(); } class _PlaylistDetailsState extends State { Playlist? playlist; bool _loading = false; bool _error = false; Sorting? _sort; final ScrollController _scrollController = ScrollController(); //Get sorted playlist List get sorted { List 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? 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( body: DraggableScrollbar.rrect( controller: _scrollController, backgroundColor: Theme.of(context).primaryColor, child: ListView( controller: _scrollController, children: [ 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: [ 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: [ 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: [ 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: [ 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: [ 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) => >[ 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(), ...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: [ (playlist!.user!.id == deezerAPI.userId) ? m.removeFromPlaylist(t, playlist) : const SizedBox( width: 0, height: 0, ) ]); }); }), if (_loading) const Padding( padding: EdgeInsets.symmetric(vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [CircularProgressIndicator()], ), ), if (_error) const ErrorScreen() ], ), )); } } class MakePlaylistOffline extends StatefulWidget { final Playlist? playlist; const MakePlaylistOffline(this.playlist, {Key? key}) : super(key: key); @override State createState() => _MakePlaylistOfflineState(); } class _MakePlaylistOfflineState extends State { bool _offline = false; @override void initState() { downloadManager.checkOffline(playlist: widget.playlist).then((v) { setState(() { _offline = v; }); }); super.initState(); } @override Widget build(BuildContext context) { return Row( children: [ 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 createState() => _ShowScreenState(); } class _ShowScreenState extends State { Show? _show; bool _loading = true; bool _error = false; List? _episodes; Future _load() async { //Fetch List? 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: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.max, children: [ CachedImage( url: _show!.art!.full, rounded: true, width: MediaQuery.of(context).size.width / 2 - 16, ), SizedBox( width: MediaQuery.of(context).size.width / 2 - 16, child: Column( mainAxisSize: MainAxisSize.max, children: [ Text(_show!.name!, maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: const TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold)), Container(height: 8.0), Text( _show!.description!, maxLines: 6, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16.0), ) ], ), ) ], ), ), Container(height: 4.0), const FreezerDivider(), //Error if (_error) const ErrorScreen(), //Loading if (_loading) const Padding( padding: EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [CircularProgressIndicator()], ), ), //Data if (!_loading && !_error) ...List.generate(_episodes!.length, (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(_show!, e); }, ), onTap: () async { await playerHelper.playShowEpisode(_show!, _episodes!, index: i); }, ); }) ], ), ); } }