freezer/lib/ui/library.dart

1251 lines
41 KiB
Dart
Raw Normal View History

2020-06-23 19:23:12 +00:00
import 'package:connectivity/connectivity.dart';
import 'package:flutter/material.dart';
2020-11-28 21:32:17 +00:00
import 'package:fluttericon/font_awesome5_icons.dart';
2020-06-23 19:23:12 +00:00
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/cache.dart';
2020-06-23 19:23:12 +00:00
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/importer.dart';
2020-06-23 19:23:12 +00:00
import 'package:freezer/api/player.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/downloads_screen.dart';
import 'package:freezer/ui/elements.dart';
2020-06-23 19:23:12 +00:00
import 'package:freezer/ui/error.dart';
2020-06-25 12:28:56 +00:00
import 'package:freezer/ui/importer_screen.dart';
2020-06-23 19:23:12 +00:00
import 'package:freezer/ui/tiles.dart';
import 'package:freezer/translations.i18n.dart';
2020-11-01 16:47:04 +00:00
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
2020-06-23 19:23:12 +00:00
import 'menu.dart';
import 'settings_screen.dart';
import '../api/download.dart';
class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Size get preferredSize => AppBar().preferredSize;
@override
Widget build(BuildContext context) {
return FreezerAppBar(
'Library'.i18n,
2020-06-23 19:23:12 +00:00
actions: <Widget>[
IconButton(
icon: Icon(
Icons.file_download,
semanticLabel: "Download".i18n,
),
2020-06-23 19:23:12 +00:00
onPressed: () {
2021-11-01 16:41:25 +00:00
Navigator.of(context)
.pushRoute(builder: (context) => DownloadsScreen());
2020-06-23 19:23:12 +00:00
},
),
IconButton(
icon: Icon(
Icons.settings,
semanticLabel: "Settings".i18n,
),
2020-06-23 19:23:12 +00:00
onPressed: () {
2021-11-01 16:41:25 +00:00
Navigator.of(context)
.pushRoute(builder: (context) => SettingsScreen());
2020-06-23 19:23:12 +00:00
},
),
],
);
}
}
class LibraryScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: LibraryAppBar(),
body: ListView(
children: <Widget>[
Container(
height: 4.0,
),
2021-11-01 16:41:25 +00:00
if (!downloadManager.running && downloadManager.queueSize! > 0)
2020-06-23 19:23:12 +00:00
ListTile(
title: Text('Downloads'.i18n),
leading: LeadingIcon(Icons.file_download, color: Colors.grey),
subtitle: Text(
'Downloading is currently stopped, click here to resume.'
.i18n),
2020-06-23 19:23:12 +00:00
onTap: () {
downloadManager.start();
2021-11-01 16:41:25 +00:00
Navigator.of(context)
.pushRoute(builder: (context) => DownloadsScreen());
2020-06-23 19:23:12 +00:00
},
),
ListTile(
title: Text('Shuffle'.i18n),
leading: LeadingIcon(Icons.shuffle, color: Color(0xffeca704)),
onTap: () async {
2021-09-01 12:38:32 +00:00
List<Track> tracks = (await deezerAPI.libraryShuffle())!;
playerHelper.playFromTrackList(
tracks,
tracks[0].id,
QueueSource(
id: 'libraryshuffle',
source: 'libraryshuffle',
text: 'Library shuffle'.i18n));
},
),
FreezerDivider(),
2020-06-23 19:23:12 +00:00
ListTile(
title: Text('Tracks'.i18n),
leading: LeadingIcon(Icons.audiotrack, color: Color(0xffbe3266)),
2020-06-23 19:23:12 +00:00
onTap: () {
2021-11-01 16:41:25 +00:00
Navigator.of(context)
.pushRoute(builder: (context) => LibraryTracks());
2020-06-23 19:23:12 +00:00
},
),
ListTile(
title: Text('Albums'.i18n),
leading: LeadingIcon(Icons.album, color: Color(0xff4b2e7e)),
2020-06-23 19:23:12 +00:00
onTap: () {
2021-11-01 16:41:25 +00:00
Navigator.of(context)
.pushRoute(builder: (context) => LibraryAlbums());
2020-06-23 19:23:12 +00:00
},
),
ListTile(
title: Text('Artists'.i18n),
leading: LeadingIcon(Icons.recent_actors, color: Color(0xff384697)),
2020-06-23 19:23:12 +00:00
onTap: () {
2021-11-01 16:41:25 +00:00
Navigator.of(context)
.pushRoute(builder: (context) => LibraryArtists());
2020-06-23 19:23:12 +00:00
},
),
ListTile(
title: Text('Playlists'.i18n),
leading: LeadingIcon(Icons.playlist_play, color: Color(0xff0880b5)),
2020-06-23 19:23:12 +00:00
onTap: () {
2021-11-01 16:41:25 +00:00
Navigator.of(context)
.pushRoute(builder: (context) => LibraryPlaylists());
2020-06-23 19:23:12 +00:00
},
),
FreezerDivider(),
ListTile(
title: Text('History'.i18n),
leading: LeadingIcon(Icons.history, color: Color(0xff009a85)),
onTap: () {
2021-11-01 16:41:25 +00:00
Navigator.of(context)
.pushRoute(builder: (context) => HistoryScreen());
},
),
FreezerDivider(),
2020-06-25 12:28:56 +00:00
ListTile(
title: Text('Import'.i18n),
leading: LeadingIcon(Icons.import_export, color: Color(0xff2ba766)),
subtitle: Text('Import playlists from Spotify'.i18n),
2020-06-25 12:28:56 +00:00
onTap: () {
//Show progress
if (importer.done || importer.busy) {
2021-11-01 16:41:25 +00:00
Navigator.of(context)
.pushRoute(builder: (context) => ImporterStatusScreen());
2020-06-25 12:28:56 +00:00
return;
}
//Pick importer dialog
showDialog(
context: context,
builder: (context) => SimpleDialog(
title: Text('Importer'.i18n),
children: [
ListTile(
leading: Icon(FontAwesome5.spotify),
title: Text('Spotify v1'.i18n),
subtitle: Text(
'Import Spotify playlists up to 100 tracks without any login.'
.i18n),
onTap: () {
Navigator.of(context).pop();
2021-11-01 16:41:25 +00:00
Navigator.of(context).pushRoute(
builder: (context) => SpotifyImporterV1());
},
),
ListTile(
leading: Icon(FontAwesome5.spotify),
title: Text('Spotify v2'.i18n),
subtitle: Text(
'Import any Spotify playlist, import from own Spotify library. Requires free account.'
.i18n),
onTap: () {
Navigator.of(context).pop();
2021-11-01 16:41:25 +00:00
Navigator.of(context).pushRoute(
builder: (context) => SpotifyImporterV2());
},
)
],
));
2020-06-25 12:28:56 +00:00
},
),
2020-06-23 19:23:12 +00:00
ExpansionTile(
title: Text('Statistics'.i18n),
leading: LeadingIcon(Icons.insert_chart, color: Colors.grey),
2020-06-23 19:23:12 +00:00
children: <Widget>[
FutureBuilder(
future: downloadManager.getStats(),
builder: (context, snapshot) {
if (snapshot.hasError) return ErrorScreen();
if (!snapshot.hasData)
return Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[CircularProgressIndicator()],
),
);
2021-09-01 12:38:32 +00:00
List<String> data = snapshot.data! as List<String>;
2020-06-23 19:23:12 +00:00
return Column(
children: <Widget>[
ListTile(
title: Text('Offline tracks'.i18n),
2020-06-23 19:23:12 +00:00
leading: Icon(Icons.audiotrack),
trailing: Text(data[0]),
),
ListTile(
title: Text('Offline albums'.i18n),
2020-06-23 19:23:12 +00:00
leading: Icon(Icons.album),
trailing: Text(data[1]),
),
ListTile(
title: Text('Offline playlists'.i18n),
2020-06-23 19:23:12 +00:00
leading: Icon(Icons.playlist_add),
trailing: Text(data[2]),
),
ListTile(
title: Text('Offline size'.i18n),
2020-06-23 19:23:12 +00:00
leading: Icon(Icons.sd_card),
trailing: Text(data[3]),
),
ListTile(
title: Text('Free space'.i18n),
2020-06-23 19:23:12 +00:00
leading: Icon(Icons.disc_full),
trailing: Text(data[4]),
),
],
);
},
)
],
)
],
),
);
}
}
class LibraryTracks extends StatefulWidget {
@override
_LibraryTracksState createState() => _LibraryTracksState();
}
class _LibraryTracksState extends State<LibraryTracks> {
bool _loading = false;
2020-10-10 20:51:20 +00:00
bool _loadingTracks = false;
2020-06-23 19:23:12 +00:00
ScrollController _scrollController = ScrollController();
2021-09-01 12:38:32 +00:00
List<Track?>? tracks = [];
List<Track?> allTracks = [];
int? trackCount;
Sorting? _sort = Sorting(sourceType: SortSourceTypes.TRACKS);
2020-06-23 19:23:12 +00:00
Playlist get _playlist => Playlist(id: deezerAPI.favoritesPlaylistId);
List<Track> get _sorted {
2021-09-01 12:38:32 +00:00
List<Track> tcopy = List.from(tracks!);
tcopy.sort((a, b) => a.addedDate!.compareTo(b.addedDate!));
switch (_sort!.type) {
case SortType.ALPHABETIC:
2021-09-01 12:38:32 +00:00
tcopy.sort((a, b) => a.title!.compareTo(b.title!));
2020-11-28 21:32:17 +00:00
break;
case SortType.ARTIST:
2021-09-01 12:38:32 +00:00
tcopy.sort((a, b) => a.artists![0].name!
.toLowerCase()
2021-09-01 12:38:32 +00:00
.compareTo(b.artists![0].name!.toLowerCase()));
2020-11-28 21:32:17 +00:00
break;
case SortType.DEFAULT:
default:
2020-11-28 21:32:17 +00:00
break;
}
2020-11-28 21:32:17 +00:00
//Reverse
2021-09-01 12:38:32 +00:00
if (_sort!.reverse!) return tcopy.reversed.toList();
2020-11-28 21:32:17 +00:00
return tcopy;
}
Future _reverse() async {
2021-09-01 12:38:32 +00:00
setState(() => _sort!.reverse = !_sort!.reverse!);
2020-11-28 21:32:17 +00:00
//Save sorting in cache
2021-09-01 12:38:32 +00:00
int? index = Sorting.index(SortSourceTypes.TRACKS);
2020-11-28 21:32:17 +00:00
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
//Preload for sorting
2021-09-01 12:38:32 +00:00
if (tracks!.length < (trackCount ?? 0)) _loadFull();
}
2020-06-23 19:23:12 +00:00
Future _load() async {
//Already loaded
2021-09-01 12:38:32 +00:00
if (trackCount != null && tracks!.length >= trackCount!) {
//Update tracks cache if fully loaded
if (cache.libraryTracks == null ||
2021-09-01 12:38:32 +00:00
cache.libraryTracks!.length != trackCount) {
setState(() {
2021-09-01 12:38:32 +00:00
cache.libraryTracks = tracks!.map((t) => t!.id).toList();
});
await cache.save();
}
return;
}
2020-06-23 19:23:12 +00:00
ConnectivityResult connectivity = await Connectivity().checkConnectivity();
if (connectivity != ConnectivityResult.none) {
setState(() => _loading = true);
2021-09-01 12:38:32 +00:00
int pos = tracks!.length;
2021-09-01 12:38:32 +00:00
if (trackCount == null || tracks!.length == 0) {
//Load tracks as a playlist
2021-09-01 12:38:32 +00:00
Playlist? favPlaylist;
try {
favPlaylist = await deezerAPI.playlist(deezerAPI.favoritesPlaylistId);
} catch (e) {}
//Error loading
if (favPlaylist == null) {
setState(() => _loading = false);
return;
}
//Update
setState(() {
2021-09-01 12:38:32 +00:00
trackCount = favPlaylist!.trackCount;
if (tracks!.length == 0) tracks = favPlaylist.tracks;
_makeFavorite();
_loading = false;
});
return;
}
2020-06-23 19:23:12 +00:00
//Load another page of tracks from deezer
2020-10-10 20:51:20 +00:00
if (_loadingTracks) return;
_loadingTracks = true;
2021-09-01 12:38:32 +00:00
List<Track>? _t;
2020-06-23 19:23:12 +00:00
try {
_t = await deezerAPI.playlistTracksPage(
deezerAPI.favoritesPlaylistId, pos);
2020-06-23 19:23:12 +00:00
} catch (e) {}
//On error load offline
if (_t == null) {
await _loadOffline();
return;
}
setState(() {
2021-09-01 12:38:32 +00:00
tracks!.addAll(_t!);
_makeFavorite();
2020-06-23 19:23:12 +00:00
_loading = false;
2020-10-10 20:51:20 +00:00
_loadingTracks = false;
2020-06-23 19:23:12 +00:00
});
}
}
//Load all tracks
Future _loadFull() async {
2021-09-01 12:38:32 +00:00
if (tracks!.length == 0 || tracks!.length < (trackCount ?? 0)) {
Playlist? p;
try {
p = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
} catch (e) {}
if (p != null) {
setState(() {
2021-09-01 12:38:32 +00:00
tracks = p!.tracks;
trackCount = p.trackCount;
2020-11-28 21:32:17 +00:00
_sort = _sort;
});
}
}
}
2020-06-23 19:23:12 +00:00
Future _loadOffline() async {
2021-09-01 12:38:32 +00:00
Playlist? p =
await downloadManager.getPlaylist(deezerAPI.favoritesPlaylistId);
if (p != null)
setState(() {
tracks = p.tracks;
});
2020-06-23 19:23:12 +00:00
}
Future _loadAllOffline() async {
2020-06-23 19:23:12 +00:00
List tracks = await downloadManager.allOfflineTracks();
setState(() {
2021-09-01 12:38:32 +00:00
allTracks = tracks as List<Track?>;
2020-06-23 19:23:12 +00:00
});
}
//Update tracks with favorite true
void _makeFavorite() {
2021-09-01 12:38:32 +00:00
for (int i = 0; i < tracks!.length; i++) tracks![i]!.favorite = true;
}
2020-06-23 19:23:12 +00:00
@override
void initState() {
_scrollController.addListener(() {
//Load more tracks on scroll
double off = _scrollController.position.maxScrollExtent * 0.90;
if (_scrollController.position.pixels > off) _load();
});
_load();
//Load all offline tracks
_loadAllOffline();
2020-06-23 19:23:12 +00:00
2020-11-28 21:32:17 +00:00
//Load sorting
2021-09-01 12:38:32 +00:00
int? index = Sorting.index(SortSourceTypes.TRACKS);
if (index != null) setState(() => _sort = cache.sorts[index]);
2020-11-28 21:32:17 +00:00
2021-09-01 12:38:32 +00:00
if (_sort!.type != SortType.DEFAULT || _sort!.reverse!) _loadFull();
2020-06-23 19:23:12 +00:00
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar(
'Tracks'.i18n,
actions: [
IconButton(
icon: Icon(
2021-09-01 12:38:32 +00:00
_sort!.reverse!
? FontAwesome5.sort_alpha_up
: FontAwesome5.sort_alpha_down,
2021-09-01 12:38:32 +00:00
semanticLabel: _sort!.reverse!
? "Sort descending".i18n
: "Sort ascending".i18n,
),
onPressed: () async {
await _reverse();
}),
PopupMenuButton(
child: Icon(
Icons.sort,
size: 32.0,
semanticLabel: "Sort".i18n,
),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (SortType s) async {
//Preload for sorting
2021-09-01 12:38:32 +00:00
if (tracks!.length < (trackCount ?? 0)) await _loadFull();
2021-09-01 12:38:32 +00:00
setState(() => _sort!.type = s);
//Save sorting in cache
2021-09-01 12:38:32 +00:00
int? index = Sorting.index(SortSourceTypes.TRACKS);
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_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()),
),
],
2020-06-23 19:23:12 +00:00
),
Container(width: 8.0),
],
),
body: DraggableScrollbar.rrect(
controller: _scrollController,
backgroundColor: Theme.of(context).primaryColor,
child: ListView(
controller: _scrollController,
children: <Widget>[
Container(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
MakePlaylistOffline(_playlist),
TextButton(
child: Row(
children: <Widget>[
Icon(
Icons.file_download,
size: 32.0,
),
Container(
width: 4,
),
Text('Download'.i18n)
],
),
onPressed: () async {
if (await downloadManager.addOfflinePlaylist(_playlist,
private: false, context: context) !=
false)
MenuSheet(context).showDownloadStartedToast();
},
)
],
)),
FreezerDivider(),
//Loved tracks
2021-09-01 12:38:32 +00:00
...List.generate(tracks!.length, (i) {
Track? t = (tracks!.length == (trackCount ?? 0))
? _sorted[i]
2021-09-01 12:38:32 +00:00
: tracks![i];
return TrackTile(
2021-11-01 16:41:25 +00:00
t!,
onTap: () {
playerHelper.playFromTrackList(
2021-09-01 12:38:32 +00:00
(tracks!.length == (trackCount ?? 0))
? _sorted
2021-09-01 12:38:32 +00:00
: tracks!,
2021-11-01 16:41:25 +00:00
t.id,
QueueSource(
id: deezerAPI.favoritesPlaylistId,
text: 'Favorites'.i18n,
source: 'playlist'));
},
onHold: () {
MenuSheet m = MenuSheet(context);
2021-11-01 16:41:25 +00:00
m.defaultTrackMenu(t, onRemove: () {
setState(() {
2021-09-01 12:38:32 +00:00
tracks!.removeWhere((track) => t.id == track!.id);
});
2020-11-01 16:47:04 +00:00
});
},
2020-11-01 16:47:04 +00:00
);
}),
if (_loading)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: CircularProgressIndicator(),
)
],
),
FreezerDivider(),
Text(
'All offline tracks'.i18n,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
Container(
height: 8,
),
...List.generate(allTracks.length, (i) {
2021-09-01 12:38:32 +00:00
Track? t = allTracks[i];
return TrackTile(
2021-11-01 16:41:25 +00:00
t!,
onTap: () {
playerHelper.playFromTrackList(
allTracks,
2021-11-01 16:41:25 +00:00
t.id,
QueueSource(
id: 'allTracks',
text: 'All offline tracks'.i18n,
source: 'offline'));
},
onHold: () {
2021-11-01 16:41:25 +00:00
MenuSheet(context).defaultTrackMenu(t);
},
);
})
],
)));
2020-06-23 19:23:12 +00:00
}
}
class LibraryAlbums extends StatefulWidget {
@override
_LibraryAlbumsState createState() => _LibraryAlbumsState();
}
class _LibraryAlbumsState extends State<LibraryAlbums> {
2021-09-01 12:38:32 +00:00
List<Album>? _albums;
Sorting? _sort = Sorting(sourceType: SortSourceTypes.ALBUMS);
2020-11-01 16:47:04 +00:00
ScrollController _scrollController = ScrollController();
List<Album> get _sorted {
2021-09-01 12:38:32 +00:00
List<Album> albums = List.from(_albums!);
albums.sort((a, b) => a.favoriteDate!.compareTo(b.favoriteDate!));
switch (_sort!.type) {
2020-11-28 21:32:17 +00:00
case SortType.DEFAULT:
break;
case SortType.ALPHABETIC:
albums.sort(
2021-09-01 12:38:32 +00:00
(a, b) => a.title!.toLowerCase().compareTo(b.title!.toLowerCase()));
2020-11-28 21:32:17 +00:00
break;
case SortType.ARTIST:
2021-09-01 12:38:32 +00:00
albums.sort((a, b) => a.artists![0].name!
.toLowerCase()
2021-09-01 12:38:32 +00:00
.compareTo(b.artists![0].name!.toLowerCase()));
2020-11-28 21:32:17 +00:00
break;
case SortType.RELEASE_DATE:
2021-09-01 12:38:32 +00:00
albums.sort((a, b) => DateTime.parse(a.releaseDate!)
.compareTo(DateTime.parse(b.releaseDate!)));
break;
default:
2020-11-28 21:32:17 +00:00
break;
}
2020-11-28 21:32:17 +00:00
//Reverse
2021-09-01 12:38:32 +00:00
if (_sort!.reverse!) return albums.reversed.toList();
return albums;
}
2020-06-23 19:23:12 +00:00
Future _load() async {
if (settings.offlineMode) return;
try {
List<Album> albums = await deezerAPI.getAlbums();
setState(() => _albums = albums);
} catch (e) {}
}
@override
void initState() {
_load();
2020-11-28 21:32:17 +00:00
//Load sorting
2021-09-01 12:38:32 +00:00
int? index = Sorting.index(SortSourceTypes.ALBUMS);
if (index != null) _sort = cache.sorts[index];
2020-11-28 21:32:17 +00:00
2020-06-23 19:23:12 +00:00
super.initState();
}
2020-11-28 21:32:17 +00:00
Future _reverse() async {
2021-09-01 12:38:32 +00:00
setState(() => _sort!.reverse = !_sort!.reverse!);
2020-11-28 21:32:17 +00:00
//Save sorting in cache
2021-09-01 12:38:32 +00:00
int? index = Sorting.index(SortSourceTypes.ALBUMS);
2020-11-28 21:32:17 +00:00
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
}
2020-06-23 19:23:12 +00:00
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar(
'Albums'.i18n,
actions: [
IconButton(
icon: Icon(
2021-09-01 12:38:32 +00:00
_sort!.reverse!
? FontAwesome5.sort_alpha_up
: FontAwesome5.sort_alpha_down,
2021-09-01 12:38:32 +00:00
semanticLabel: _sort!.reverse!
? "Sort descending".i18n
: "Sort ascending".i18n,
),
onPressed: () => _reverse(),
),
PopupMenuButton(
color: Theme.of(context).scaffoldBackgroundColor,
child: Icon(Icons.sort, size: 32.0),
onSelected: (SortType s) async {
2021-09-01 12:38:32 +00:00
setState(() => _sort!.type = s);
//Save to cache
2021-09-01 12:38:32 +00:00
int? index = Sorting.index(SortSourceTypes.ALBUMS);
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.RELEASE_DATE,
child: Text('Release date'.i18n, style: popupMenuTextStyle()),
),
],
),
Container(width: 8.0),
],
),
body: DraggableScrollbar.rrect(
2020-11-01 16:47:04 +00:00
controller: _scrollController,
backgroundColor: Theme.of(context).primaryColor,
child: ListView(
controller: _scrollController,
children: <Widget>[
Container(
height: 8.0,
2020-11-01 16:47:04 +00:00
),
if (!settings.offlineMode && _albums == null)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[CircularProgressIndicator()],
),
if (_albums != null)
2021-09-01 12:38:32 +00:00
...List.generate(_albums!.length, (int i) {
Album a = _sorted[i];
return AlbumTile(
a,
onTap: () {
2021-11-01 16:41:25 +00:00
Navigator.of(context)
.pushRoute(builder: (context) => AlbumDetails(a));
},
onHold: () async {
MenuSheet m = MenuSheet(context);
m.defaultAlbumMenu(a, onRemove: () {
2021-09-01 12:38:32 +00:00
setState(() => _albums!.remove(a));
});
},
);
}),
FutureBuilder(
future: downloadManager.getOfflineAlbums(),
builder: (context, snapshot) {
if (snapshot.hasError ||
!snapshot.hasData ||
2021-09-01 12:38:32 +00:00
(snapshot.data! as List).length == 0)
return Container(
height: 0,
width: 0,
2020-11-01 16:47:04 +00:00
);
2021-09-01 12:38:32 +00:00
List<Album> albums = snapshot.data as List<Album>;
return Column(
children: <Widget>[
FreezerDivider(),
Text(
'Offline albums'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 24.0),
2020-11-01 16:47:04 +00:00
),
...List.generate(albums.length, (i) {
Album a = albums[i];
return AlbumTile(
a,
onTap: () {
2021-11-01 16:41:25 +00:00
Navigator.of(context).pushRoute(
builder: (context) => AlbumDetails(a));
},
onHold: () async {
MenuSheet m = MenuSheet(context);
m.defaultAlbumMenu(a, onRemove: () {
setState(() {
albums.remove(a);
2021-09-01 12:38:32 +00:00
_albums!.remove(a);
});
2020-11-01 16:47:04 +00:00
});
},
);
})
],
);
},
)
],
),
));
2020-06-23 19:23:12 +00:00
}
}
class LibraryArtists extends StatefulWidget {
@override
_LibraryArtistsState createState() => _LibraryArtistsState();
}
class _LibraryArtistsState extends State<LibraryArtists> {
2021-09-01 12:38:32 +00:00
late List<Artist> _artists;
Sorting? _sort = Sorting(sourceType: SortSourceTypes.ARTISTS);
bool _loading = true;
bool _error = false;
2020-11-01 16:47:04 +00:00
ScrollController _scrollController = ScrollController();
List<Artist> get _sorted {
List<Artist> artists = List.from(_artists);
2021-09-01 12:38:32 +00:00
artists.sort((a, b) => a.favoriteDate!.compareTo(b.favoriteDate!));
switch (_sort!.type) {
2020-11-28 21:32:17 +00:00
case SortType.DEFAULT:
break;
case SortType.POPULARITY:
2021-09-01 12:38:32 +00:00
artists.sort((a, b) => b.fans! - a.fans!);
2020-11-28 21:32:17 +00:00
break;
case SortType.ALPHABETIC:
artists.sort(
2021-09-01 12:38:32 +00:00
(a, b) => a.name!.toLowerCase().compareTo(b.name!.toLowerCase()));
break;
default:
2020-11-28 21:32:17 +00:00
break;
}
2020-11-28 21:32:17 +00:00
//Reverse
2021-09-01 12:38:32 +00:00
if (_sort!.reverse!) return artists.reversed.toList();
return artists;
}
//Load data
Future _load() async {
setState(() => _loading = true);
//Fetch
2021-09-01 12:38:32 +00:00
List<Artist>? data;
try {
data = await deezerAPI.getArtists();
} catch (e) {}
//Update UI
setState(() {
if (data != null) {
_artists = data;
} else {
_error = true;
}
_loading = false;
});
}
2020-11-28 21:32:17 +00:00
Future _reverse() async {
2021-09-01 12:38:32 +00:00
setState(() => _sort!.reverse = !_sort!.reverse!);
2020-11-28 21:32:17 +00:00
//Save sorting in cache
2021-09-01 12:38:32 +00:00
int? index = Sorting.index(SortSourceTypes.ARTISTS);
2020-11-28 21:32:17 +00:00
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
}
@override
void initState() {
2020-11-28 21:32:17 +00:00
//Restore sort
2021-09-01 12:38:32 +00:00
int? index = Sorting.index(SortSourceTypes.ARTISTS);
if (index != null) _sort = cache.sorts[index];
2020-11-28 21:32:17 +00:00
_load();
super.initState();
}
2020-06-23 19:23:12 +00:00
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar(
'Artists'.i18n,
actions: [
IconButton(
icon: Icon(
2021-09-01 12:38:32 +00:00
_sort!.reverse!
? FontAwesome5.sort_alpha_up
: FontAwesome5.sort_alpha_down,
2021-09-01 12:38:32 +00:00
semanticLabel: _sort!.reverse!
? "Sort descending".i18n
: "Sort ascending".i18n,
),
onPressed: () => _reverse(),
),
PopupMenuButton(
child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (SortType s) async {
2021-09-01 12:38:32 +00:00
setState(() => _sort!.type = s);
//Save
2021-09-01 12:38:32 +00:00
int? index = Sorting.index(SortSourceTypes.ARTISTS);
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.POPULARITY,
child: Text('Popularity'.i18n, style: popupMenuTextStyle()),
),
],
),
Container(width: 8.0),
],
),
body: DraggableScrollbar.rrect(
2020-11-01 16:47:04 +00:00
controller: _scrollController,
backgroundColor: Theme.of(context).primaryColor,
child: ListView(
controller: _scrollController,
children: <Widget>[
if (_loading)
Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [CircularProgressIndicator()],
),
2020-11-01 16:47:04 +00:00
),
if (_error) Center(child: ErrorScreen()),
if (!_loading && !_error)
...List.generate(_artists.length, (i) {
Artist a = _sorted[i];
return ArtistHorizontalTile(
a,
onTap: () {
2021-11-01 16:41:25 +00:00
Navigator.of(context)
.pushRoute(builder: (context) => ArtistDetails(a));
},
onHold: () {
MenuSheet m = MenuSheet(context);
m.defaultArtistMenu(a, onRemove: () {
setState(() {
_artists.remove(a);
});
2020-11-01 16:47:04 +00:00
});
},
);
}),
],
),
));
2020-06-23 19:23:12 +00:00
}
}
class LibraryPlaylists extends StatefulWidget {
@override
_LibraryPlaylistsState createState() => _LibraryPlaylistsState();
}
class _LibraryPlaylistsState extends State<LibraryPlaylists> {
2021-09-01 12:38:32 +00:00
List<Playlist>? _playlists;
Sorting? _sort = Sorting(sourceType: SortSourceTypes.PLAYLISTS);
2020-11-01 16:47:04 +00:00
ScrollController _scrollController = ScrollController();
String _filter = '';
List<Playlist> get _sorted {
2021-09-01 12:38:32 +00:00
List<Playlist> playlists = List.from(_playlists!
.where((p) => p.title!.toLowerCase().contains(_filter.toLowerCase())));
switch (_sort!.type) {
2020-11-28 21:32:17 +00:00
case SortType.DEFAULT:
break;
case SortType.USER:
2021-09-01 12:38:32 +00:00
playlists.sort((a, b) => (a.user!.name ?? deezerAPI.userName)!
.toLowerCase()
2021-09-01 12:38:32 +00:00
.compareTo((b.user!.name ?? deezerAPI.userName)!.toLowerCase()));
2020-11-28 21:32:17 +00:00
break;
case SortType.TRACK_COUNT:
2021-09-01 12:38:32 +00:00
playlists.sort((a, b) => b.trackCount! - a.trackCount!);
2020-11-28 21:32:17 +00:00
break;
case SortType.ALPHABETIC:
playlists.sort(
2021-09-01 12:38:32 +00:00
(a, b) => a.title!.toLowerCase().compareTo(b.title!.toLowerCase()));
break;
default:
2020-11-28 21:32:17 +00:00
break;
}
2021-09-01 12:38:32 +00:00
if (_sort!.reverse!) return playlists.reversed.toList();
return playlists;
}
2020-06-23 19:23:12 +00:00
Future _load() async {
if (!settings.offlineMode) {
try {
2021-09-01 12:38:32 +00:00
List<Playlist>? playlists = await deezerAPI.getPlaylists();
2020-06-23 19:23:12 +00:00
setState(() => _playlists = playlists);
} catch (e) {}
}
}
2020-11-28 21:32:17 +00:00
Future _reverse() async {
2021-09-01 12:38:32 +00:00
setState(() => _sort!.reverse = !_sort!.reverse!);
2020-11-28 21:32:17 +00:00
//Save sorting in cache
2021-09-01 12:38:32 +00:00
int? index = Sorting.index(SortSourceTypes.PLAYLISTS);
2020-11-28 21:32:17 +00:00
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
}
2020-06-23 19:23:12 +00:00
@override
void initState() {
2020-11-28 21:32:17 +00:00
//Restore sort
2021-09-01 12:38:32 +00:00
int? index = Sorting.index(SortSourceTypes.PLAYLISTS);
if (index != null) _sort = cache.sorts[index];
2020-11-28 21:32:17 +00:00
2020-06-23 19:23:12 +00:00
_load();
super.initState();
}
Playlist get favoritesPlaylist => Playlist(
id: deezerAPI.favoritesPlaylistId,
title: 'Favorites'.i18n,
user: User(name: deezerAPI.userName),
image: ImageDetails(thumbUrl: 'assets/favorites_thumb.jpg'),
tracks: [],
trackCount: 1,
duration: Duration(seconds: 0));
2020-06-23 19:23:12 +00:00
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar(
'Playlists'.i18n,
actions: [
IconButton(
icon: Icon(
2021-09-01 12:38:32 +00:00
_sort!.reverse!
? FontAwesome5.sort_alpha_up
: FontAwesome5.sort_alpha_down,
2021-09-01 12:38:32 +00:00
semanticLabel: _sort!.reverse!
? "Sort descending".i18n
: "Sort ascending".i18n,
),
onPressed: () => _reverse(),
),
PopupMenuButton(
child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (SortType s) async {
2021-09-01 12:38:32 +00:00
setState(() => _sort!.type = s);
//Save to cache
2021-09-01 12:38:32 +00:00
int? index = Sorting.index(SortSourceTypes.PLAYLISTS);
if (index == null)
cache.sorts.add(_sort);
else
cache.sorts[index] = _sort;
await cache.save();
2020-11-01 16:47:04 +00:00
},
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
PopupMenuItem(
value: SortType.DEFAULT,
child: Text('Default'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.USER,
child: Text('User'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.TRACK_COUNT,
child: Text('Track count'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.ALPHABETIC,
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
),
],
2020-06-23 19:23:12 +00:00
),
Container(width: 8.0),
],
),
body: DraggableScrollbar.rrect(
controller: _scrollController,
backgroundColor: Theme.of(context).primaryColor,
child: ListView(
controller: _scrollController,
children: <Widget>[
//Search
Padding(
padding: EdgeInsets.all(8.0),
child: TextField(
onChanged: (String s) => setState(() => _filter = s),
decoration: InputDecoration(
labelText: 'Search'.i18n,
fillColor: Theme.of(context).bottomAppBarColor,
filled: true,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey)),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey)),
)),
),
ListTile(
title: Text('Create new playlist'.i18n),
leading:
LeadingIcon(Icons.playlist_add, color: Color(0xff009a85)),
onTap: () async {
if (settings.offlineMode) {
Fluttertoast.showToast(
msg: 'Cannot create playlists in offline mode'.i18n,
gravity: ToastGravity.BOTTOM);
return;
}
MenuSheet m = MenuSheet(context);
await m.createPlaylist();
await _load();
},
2020-11-01 16:47:04 +00:00
),
FreezerDivider(),
2020-06-23 19:23:12 +00:00
if (!settings.offlineMode && _playlists == null)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(),
],
),
2020-06-23 19:23:12 +00:00
//Favorites playlist
PlaylistTile(
favoritesPlaylist,
onTap: () async {
2021-11-01 16:41:25 +00:00
Navigator.of(context).pushRoute(
builder: (context) => PlaylistDetails(favoritesPlaylist));
},
onHold: () {
MenuSheet m = MenuSheet(context);
favoritesPlaylist.library = true;
m.defaultPlaylistMenu(favoritesPlaylist);
},
),
2020-11-01 16:47:04 +00:00
if (_playlists != null)
...List.generate(_sorted.length, (int i) {
2021-09-01 12:38:32 +00:00
Playlist p = _sorted[i];
return PlaylistTile(
p,
2021-11-01 16:41:25 +00:00
onTap: () => Navigator.of(context)
.pushRoute(builder: (context) => PlaylistDetails(p)),
onHold: () {
MenuSheet m = MenuSheet(context);
m.defaultPlaylistMenu(p, onRemove: () {
2021-09-01 12:38:32 +00:00
setState(() => _playlists!.remove(p));
}, onUpdate: () {
_load();
});
},
);
}),
2020-11-01 16:47:04 +00:00
FutureBuilder(
future: downloadManager.getOfflinePlaylists(),
builder: (context, snapshot) {
if (snapshot.hasError || !snapshot.hasData)
return Container(
height: 0,
width: 0,
);
2021-09-01 12:38:32 +00:00
if ((snapshot.data! as List).length == 0)
return Container(
height: 0,
width: 0,
);
2021-09-01 12:38:32 +00:00
List<Playlist> playlists = snapshot.data! as List<Playlist>;
return Column(
children: <Widget>[
FreezerDivider(),
Text(
'Offline playlists'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24.0, fontWeight: FontWeight.bold),
2020-11-01 16:47:04 +00:00
),
...List.generate(playlists.length, (i) {
Playlist p = playlists[i];
return PlaylistTile(
p,
2021-11-01 16:41:25 +00:00
onTap: () => Navigator.of(context).pushRoute(
builder: (context) => PlaylistDetails(p)),
onHold: () {
MenuSheet m = MenuSheet(context);
m.defaultPlaylistMenu(p, onRemove: () {
setState(() {
playlists.remove(p);
2021-09-01 12:38:32 +00:00
_playlists!.remove(p);
});
2020-11-01 16:47:04 +00:00
});
},
);
})
],
);
},
)
],
),
));
2020-06-23 19:23:12 +00:00
}
}
class HistoryScreen extends StatefulWidget {
@override
_HistoryScreenState createState() => _HistoryScreenState();
}
class _HistoryScreenState extends State<HistoryScreen> {
2020-11-01 16:47:04 +00:00
ScrollController _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar(
'History'.i18n,
actions: [
IconButton(
icon: Icon(
Icons.delete_sweep,
semanticLabel: "Clear all".i18n,
),
onPressed: () {
setState(() => cache.history = []);
cache.save();
},
)
],
),
2020-11-01 16:47:04 +00:00
body: DraggableScrollbar.rrect(
controller: _scrollController,
backgroundColor: Theme.of(context).primaryColor,
child: ListView.builder(
controller: _scrollController,
2021-09-01 12:38:32 +00:00
itemCount: cache.history.length,
itemBuilder: (BuildContext context, int i) {
Track t = cache.history[cache.history.length - i - 1];
return TrackTile(
t,
onTap: () {
playerHelper.playFromTrackList(
cache.history.reversed.toList(),
t.id,
QueueSource(
id: null, text: 'History'.i18n, source: 'history'));
},
onHold: () {
MenuSheet m = MenuSheet(context);
m.defaultTrackMenu(t);
},
);
},
)),
);
}
}