freezer/lib/ui/library.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

1233 lines
43 KiB
Dart

import 'package:connectivity_plus/connectivity_plus.dart';
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/definitions.dart';
import 'package:freezer/api/importer.dart';
import 'package:freezer/api/player/audio_handler.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/importer_screen.dart';
import 'package:freezer/ui/tiles.dart';
import 'package:freezer/translations.i18n.dart';
import 'menu.dart';
import '../api/download.dart';
class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
const LibraryAppBar({super.key});
@override
Size get preferredSize => AppBar().preferredSize;
@override
Widget build(BuildContext context) {
return AppBar(
title: Text('Library'.i18n),
actions: <Widget>[
IconButton(
icon: Icon(
Icons.file_download,
semanticLabel: "Download".i18n,
),
onPressed: () => Navigator.pushNamed(context, '/downloads'),
),
IconButton(
icon: Icon(
Icons.settings,
semanticLabel: "Settings".i18n,
),
onPressed: () => Navigator.pushNamed(context, '/settings'),
),
],
);
}
}
class LibraryScreen extends StatelessWidget {
const LibraryScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const LibraryAppBar(),
body: ListView(
children: <Widget>[
const SizedBox(height: 4.0),
if (!downloadManager.running && downloadManager.queueSize! > 0)
ListTile(
title: Text('Downloads'.i18n),
leading:
const LeadingIcon(Icons.file_download, color: Colors.grey),
subtitle: Text(
'Downloading is currently stopped, click here to resume.'
.i18n),
onTap: () {
downloadManager.start();
Navigator.pushNamed(context, '/downloads');
},
),
ListTile(
title: Text('Shuffle'.i18n),
leading: const LeadingIcon(Icons.shuffle, color: Color(0xffeca704)),
onTap: () async {
List<Track> tracks = (await deezerAPI.libraryShuffle())!;
playerHelper.playFromTrackList(
tracks,
tracks[0].id,
QueueSource(
id: 'libraryshuffle',
source: 'libraryshuffle',
text: 'Library shuffle'.i18n));
},
),
const FreezerDivider(),
ListTile(
title: Text('Tracks'.i18n),
leading:
const LeadingIcon(Icons.audiotrack, color: Color(0xffbe3266)),
onTap: () => Navigator.pushNamed(context, '/library/tracks'),
),
ListTile(
title: Text('Albums'.i18n),
leading: const LeadingIcon(Icons.album, color: Color(0xff4b2e7e)),
onTap: () => Navigator.pushNamed(context, '/library/albums'),
),
ListTile(
title: Text('Artists'.i18n),
leading: const LeadingIcon(Icons.recent_actors,
color: Color(0xff384697)),
onTap: () => Navigator.pushNamed(context, '/library/artists'),
),
ListTile(
title: Text('Playlists'.i18n),
leading: const LeadingIcon(Icons.playlist_play,
color: Color(0xff0880b5)),
onTap: () => Navigator.pushNamed(context, '/library/playlists'),
),
const FreezerDivider(),
ListTile(
title: Text('History'.i18n),
leading: const LeadingIcon(Icons.history, color: Color(0xff009a85)),
onTap: () => Navigator.pushNamed(context, '/library/history'),
),
const FreezerDivider(),
ListTile(
title: Text('Import'.i18n),
leading: const LeadingIcon(Icons.import_export,
color: Color(0xff2ba766)),
subtitle: Text('Import playlists from Spotify'.i18n),
onTap: () {
//Show progress
if (importer.done || importer.busy) {
Navigator.of(context).pushRoute(
builder: (context) => const ImporterStatusScreen());
return;
}
Navigator.of(context).pushNamed('/spotify-importer');
//Pick importer dialog (removed as ImporterV1 is broken)
// showDialog(
// context: context,
// builder: (context) => SimpleDialog(
// title: Text('Importer'.i18n),
// children: [
// ListTile(
// leading: const 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();
// Navigator.of(context).pushRoute(
// builder: (context) =>
// const SpotifyImporterV1());
// },
// ),
// ListTile(
// leading: const 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();
// Navigator.of(context).pushRoute(
// builder: (context) =>
// const SpotifyImporterV2());
// },
// )
// ],
// ));
},
),
if (DownloadManager.isSupported)
ExpansionTile(
title: Text('Statistics'.i18n),
leading:
const LeadingIcon(Icons.insert_chart, color: Colors.grey),
children: <Widget>[
FutureBuilder(
future: downloadManager.getStats(),
builder: (context, snapshot) {
if (snapshot.hasError) return const ErrorScreen();
if (!snapshot.hasData) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[CircularProgressIndicator()],
),
);
}
List<String> data = snapshot.data!;
return Column(
children: <Widget>[
ListTile(
title: Text('Offline tracks'.i18n),
leading: const Icon(Icons.audiotrack),
trailing: Text(data[0]),
),
ListTile(
title: Text('Offline albums'.i18n),
leading: const Icon(Icons.album),
trailing: Text(data[1]),
),
ListTile(
title: Text('Offline playlists'.i18n),
leading: const Icon(Icons.playlist_add),
trailing: Text(data[2]),
),
ListTile(
title: Text('Offline size'.i18n),
leading: const Icon(Icons.sd_card),
trailing: Text(data[3]),
),
ListTile(
title: Text('Free space'.i18n),
leading: const Icon(Icons.disc_full),
trailing: Text(data[4]),
),
],
);
},
)
],
)
],
),
);
}
}
class LibraryTracks extends StatefulWidget {
const LibraryTracks({super.key});
@override
State<LibraryTracks> createState() => _LibraryTracksState();
}
class _LibraryTracksState extends State<LibraryTracks> {
bool _loading = false;
bool _loadingTracks = false;
final ScrollController _scrollController = ScrollController();
List<Track> tracks = [];
List<Track?> allTracks = [];
int? trackCount;
Sorting? _sort = Sorting(sourceType: SortSourceTypes.TRACKS);
Playlist get _playlist => Playlist(id: deezerAPI.favoritesPlaylistId!);
List<Track> get _sorted {
List<Track> tcopy = List.from(tracks);
tcopy.sort((a, b) => a.addedDate!.compareTo(b.addedDate!));
switch (_sort!.type) {
case SortType.ALPHABETIC:
tcopy.sort((a, b) => a.title!.compareTo(b.title!));
break;
case SortType.ARTIST:
tcopy.sort((a, b) => a.artists![0].name!
.toLowerCase()
.compareTo(b.artists![0].name!.toLowerCase()));
break;
case SortType.DEFAULT:
default:
break;
}
//Reverse
if (_sort!.reverse!) return tcopy.reversed.toList();
return tcopy;
}
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 (tracks.length < (trackCount ?? 0)) _loadFull();
}
Future _load() async {
//Already loaded
if (trackCount != null && tracks.length >= trackCount!) {
//Update tracks cache if fully loaded
if (cache.libraryTracks.length != trackCount) {
cache.libraryTracks = tracks.map((t) => t.id).toList();
await cache.save();
}
return;
}
ConnectivityResult connectivity = await Connectivity().checkConnectivity();
if (connectivity != ConnectivityResult.none) {
setState(() => _loading = true);
int pos = tracks.length;
if (trackCount == null || tracks.isEmpty) {
//Load tracks as a playlist
Playlist? favPlaylist;
try {
favPlaylist = await deezerAPI.playlist(deezerAPI.favoritesPlaylistId);
} catch (e) {}
//Error loading
if (favPlaylist == null) {
setState(() => _loading = false);
return;
}
//Update
if (mounted) {
setState(() {
trackCount = favPlaylist!.trackCount;
if (tracks.isEmpty && favPlaylist.tracks != null) {
tracks = favPlaylist.tracks!;
}
_makeFavorite();
_loading = false;
});
}
return;
}
//Load another page of tracks from deezer
if (_loadingTracks) return;
_loadingTracks = true;
List<Track>? t;
try {
t = await deezerAPI.playlistTracksPage(
deezerAPI.favoritesPlaylistId, pos);
} catch (e) {}
//On error load offline
if (t == null) {
await _loadOffline();
return;
}
setState(() {
tracks.addAll(t!);
_makeFavorite();
_loading = false;
_loadingTracks = false;
});
}
}
//Load all tracks
Future _loadFull() async {
if (tracks.isEmpty || tracks.length < (trackCount ?? 0)) {
Playlist? p;
try {
p = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
} catch (e) {}
if (p != null) {
setState(() {
tracks.addAll(p!.tracks!);
trackCount = p.trackCount;
_sort = _sort;
});
if (cache.libraryTracks.length != trackCount) {
cache.libraryTracks = tracks.map((t) => t.id).toList();
await cache.save();
}
}
}
}
Future<void> _loadOffline() async {
if (deezerAPI.favoritesPlaylistId == null) return;
Playlist? p =
await downloadManager.getPlaylistFromId(deezerAPI.favoritesPlaylistId!);
if (p != null) {
setState(() {
tracks.addAll(p.tracks!);
});
}
}
Future _loadAllOffline() async {
List<Track?> tracks = await downloadManager.allOfflineTracks();
setState(() {
allTracks = tracks;
});
}
//Update tracks with favorite true
void _makeFavorite() {
for (int i = 0; i < tracks.length; i++) {
tracks[i].favorite = true;
}
}
@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();
//Load sorting
int? index = Sorting.index(SortSourceTypes.TRACKS);
if (index != null) setState(() => _sort = cache.sorts[index]);
if (_sort!.type != SortType.DEFAULT || _sort!.reverse!) _loadFull();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Tracks'.i18n),
actions: [
IconButton(
icon: Icon(
_sort!.reverse!
? FontAwesome5.sort_alpha_up
: FontAwesome5.sort_alpha_down,
semanticLabel: _sort!.reverse!
? "Sort descending".i18n
: "Sort ascending".i18n,
),
onPressed: () async {
await _reverse();
}),
PopupMenuButton(
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (SortType s) async {
//Preload for sorting
if (tracks.length < (trackCount ?? 0)) await _loadFull();
setState(() => _sort!.type = s);
//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();
},
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()),
),
],
child: Icon(
Icons.sort,
size: 32.0,
semanticLabel: "Sort".i18n,
),
),
const SizedBox(width: 8.0),
],
),
body: _loading && allTracks.isEmpty
? const Center(child: CircularProgressIndicator())
: Scrollbar(
interactive: true,
controller: _scrollController,
thickness: 8.0,
child: ListView(
controller: _scrollController,
children: <Widget>[
if (!_loading)
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
MakePlaylistOffline(_playlist),
TextButton.icon(
icon: const Icon(Icons.file_download),
label: Text('Download'.i18n),
onPressed: () async {
if (await downloadManager.addOfflinePlaylist(
_playlist,
private: false,
context: context) !=
false) {
MenuSheet(context).showDownloadStartedToast();
}
},
)
]),
const FreezerDivider(),
//Loved tracks
if (_loading)
const Center(child: CircularProgressIndicator()),
...List.generate(tracks.length, (i) {
Track? t = (tracks.length == (trackCount ?? 0))
? _sorted[i]
: tracks[i];
return TrackTile.fromTrack(
t,
onTap: () {
playerHelper.playFromTrackList(
(tracks.length == (trackCount ?? 0))
? _sorted
: tracks,
t.id,
QueueSource(
id: deezerAPI.favoritesPlaylistId,
text: 'Favorites'.i18n,
source: 'playlist'));
},
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultTrackMenu(t, details: details, onRemove: () {
setState(() {
tracks.removeWhere((track) => t.id == track.id);
});
});
},
);
}),
const FreezerDivider(),
Text(
'All offline tracks'.i18n,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8.0),
for (final track in allTracks)
TrackTile.fromTrack(track!, onTap: () {
playerHelper.playFromTrackList(
allTracks,
track.id,
QueueSource(
id: 'allTracks',
text: 'All offline tracks'.i18n,
source: 'offline'));
}, onSecondary: (details) {
MenuSheet(context)
.defaultTrackMenu(track, details: details);
}),
],
)));
}
}
class LibraryAlbums extends StatefulWidget {
const LibraryAlbums({super.key});
@override
State<LibraryAlbums> createState() => _LibraryAlbumsState();
}
class _LibraryAlbumsState extends State<LibraryAlbums> {
List<Album>? _albums;
Sorting? _sort = Sorting(sourceType: SortSourceTypes.ALBUMS);
final ScrollController _scrollController = ScrollController();
List<Album> get _sorted {
List<Album> albums = List.from(_albums!);
albums.sort((a, b) => a.favoriteDate!.compareTo(b.favoriteDate!));
switch (_sort!.type) {
case SortType.DEFAULT:
break;
case SortType.ALPHABETIC:
albums.sort(
(a, b) => a.title!.toLowerCase().compareTo(b.title!.toLowerCase()));
break;
case SortType.ARTIST:
albums.sort((a, b) => a.artists![0].name!
.toLowerCase()
.compareTo(b.artists![0].name!.toLowerCase()));
break;
case SortType.RELEASE_DATE:
albums.sort((a, b) => DateTime.parse(a.releaseDate!)
.compareTo(DateTime.parse(b.releaseDate!)));
break;
default:
break;
}
//Reverse
if (_sort!.reverse!) return albums.reversed.toList();
return albums;
}
Future _load() async {
if (settings.offlineMode) return;
try {
List<Album> albums = await deezerAPI.getAlbums();
setState(() => _albums = albums);
} catch (e) {}
}
@override
void initState() {
_load();
//Load sorting
int? index = Sorting.index(SortSourceTypes.ALBUMS);
if (index != null) _sort = cache.sorts[index];
super.initState();
}
Future _reverse() async {
setState(() => _sort!.reverse = !_sort!.reverse!);
//Save sorting in cache
int? index = Sorting.index(SortSourceTypes.ALBUMS);
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Albums'.i18n),
actions: [
IconButton(
icon: Icon(
_sort!.reverse!
? FontAwesome5.sort_alpha_up
: FontAwesome5.sort_alpha_down,
semanticLabel: _sort!.reverse!
? "Sort descending".i18n
: "Sort ascending".i18n,
),
onPressed: () => _reverse(),
),
PopupMenuButton(
color: Theme.of(context).scaffoldBackgroundColor,
child: const Icon(Icons.sort, size: 32.0),
onSelected: (SortType s) async {
setState(() => _sort!.type = s);
//Save to cache
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()),
),
],
),
const SizedBox(width: 8.0),
],
),
body: !settings.offlineMode && _albums == null
? const Center(child: CircularProgressIndicator())
: Scrollbar(
interactive: true,
controller: _scrollController,
thickness: 8.0,
child: ListView(
controller: _scrollController,
children: <Widget>[
const SizedBox(height: 8.0),
if (_albums != null)
...List.generate(_albums!.length, (int i) {
Album a = _sorted[i];
return AlbumTile(
a,
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) => AlbumDetails(a));
},
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultAlbumMenu(
a,
details: details,
onRemove: () {
setState(() => _albums!.remove(a));
},
);
},
);
}),
FutureBuilder(
future: downloadManager.getOfflineAlbums(),
builder: (context, snapshot) {
if (snapshot.hasError ||
!snapshot.hasData ||
(snapshot.data!).isEmpty) {
return const SizedBox(
height: 0,
width: 0,
);
}
List<Album> albums = snapshot.data as List<Album>;
return Column(
children: <Widget>[
const FreezerDivider(),
Text(
'Offline albums'.i18n,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 24.0),
),
...List.generate(albums.length, (i) {
Album a = albums[i];
return AlbumTile(
a,
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) => AlbumDetails(a));
},
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultAlbumMenu(a, details: details,
onRemove: () {
setState(() {
albums.remove(a);
_albums!.remove(a);
});
});
},
);
})
],
);
},
)
],
),
));
}
}
class LibraryArtists extends StatefulWidget {
const LibraryArtists({super.key});
@override
State<LibraryArtists> createState() => _LibraryArtistsState();
}
class _LibraryArtistsState extends State<LibraryArtists> {
late List<Artist> _artists;
Sorting? _sort = Sorting(sourceType: SortSourceTypes.ARTISTS);
bool _loading = true;
bool _error = false;
final ScrollController _scrollController = ScrollController();
List<Artist> get _sorted {
List<Artist> artists = List.from(_artists);
artists.sort((a, b) => a.favoriteDate!.compareTo(b.favoriteDate!));
switch (_sort!.type) {
case SortType.DEFAULT:
break;
case SortType.POPULARITY:
artists.sort((a, b) => b.fans! - a.fans!);
break;
case SortType.ALPHABETIC:
artists.sort(
(a, b) => a.name!.toLowerCase().compareTo(b.name!.toLowerCase()));
break;
default:
break;
}
//Reverse
if (_sort!.reverse!) return artists.reversed.toList();
return artists;
}
//Load data
Future _load() async {
setState(() => _loading = true);
//Fetch
List<Artist>? data;
try {
data = await deezerAPI.getArtists();
} catch (e) {}
//Update UI
setState(() {
if (data != null) {
_artists = data;
} else {
_error = true;
}
_loading = false;
});
}
Future _reverse() async {
setState(() => _sort!.reverse = !_sort!.reverse!);
//Save sorting in cache
int? index = Sorting.index(SortSourceTypes.ARTISTS);
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
}
@override
void initState() {
//Restore sort
int? index = Sorting.index(SortSourceTypes.ARTISTS);
if (index != null) _sort = cache.sorts[index];
_load();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Artists'.i18n),
actions: [
IconButton(
icon: Icon(
_sort!.reverse!
? FontAwesome5.sort_alpha_up
: FontAwesome5.sort_alpha_down,
semanticLabel: _sort!.reverse!
? "Sort descending".i18n
: "Sort ascending".i18n,
),
onPressed: () => _reverse(),
),
PopupMenuButton(
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (SortType s) async {
setState(() => _sort!.type = s);
//Save
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()),
),
],
child: const Icon(Icons.sort, size: 32.0),
),
const SizedBox(width: 8.0),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _error
? const Center(child: ErrorScreen())
: Scrollbar(
interactive: true,
controller: _scrollController,
thickness: 8.0,
child: ListView(
controller: _scrollController,
children: <Widget>[
for (final artist in _sorted)
ArtistHorizontalTile(
artist,
onTap: () {
Navigator.of(context).pushRoute(
builder: (context) => ArtistDetails(artist));
},
onHold: () {
MenuSheet m = MenuSheet(context);
m.defaultArtistMenu(artist, onRemove: () {
setState(() {
_artists.remove(artist);
});
});
},
),
],
),
));
}
}
class LibraryPlaylists extends StatefulWidget {
const LibraryPlaylists({super.key});
@override
State<LibraryPlaylists> createState() => _LibraryPlaylistsState();
}
class _LibraryPlaylistsState extends State<LibraryPlaylists> {
List<Playlist>? _playlists;
Sorting? _sort = Sorting(sourceType: SortSourceTypes.PLAYLISTS);
final ScrollController _scrollController = ScrollController();
String _filter = '';
List<Playlist> get _sorted {
List<Playlist> playlists = List.from(_playlists!
.where((p) => p.title!.toLowerCase().contains(_filter.toLowerCase())));
switch (_sort!.type) {
case SortType.DEFAULT:
break;
case SortType.USER:
playlists.sort((a, b) => (a.user!.name ?? deezerAPI.userName)!
.toLowerCase()
.compareTo((b.user!.name ?? deezerAPI.userName)!.toLowerCase()));
break;
case SortType.TRACK_COUNT:
playlists.sort((a, b) => b.trackCount! - a.trackCount!);
break;
case SortType.ALPHABETIC:
playlists.sort(
(a, b) => a.title!.toLowerCase().compareTo(b.title!.toLowerCase()));
break;
default:
break;
}
if (_sort!.reverse!) return playlists.reversed.toList();
return playlists;
}
Future _load() async {
if (!settings.offlineMode) {
try {
final List<Playlist> playlists = await deezerAPI.getPlaylists();
setState(() => _playlists = playlists);
return;
} catch (e) {}
}
}
Future _reverse() async {
setState(() => _sort!.reverse = !_sort!.reverse!);
//Save sorting in cache
int? index = Sorting.index(SortSourceTypes.PLAYLISTS);
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
}
@override
void initState() {
//Restore sort
int? index = Sorting.index(SortSourceTypes.PLAYLISTS);
if (index != null) _sort = cache.sorts[index];
_load();
super.initState();
}
Playlist get favoritesPlaylist => Playlist(
id: deezerAPI.favoritesPlaylistId!,
title: 'Favorites'.i18n,
user: User(name: deezerAPI.userName),
image: UrlImageDetails.single('assets/favorites_thumb.jpg'),
tracks: [],
trackCount: 1,
duration: const Duration(seconds: 0));
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Playlists'.i18n),
actions: [
IconButton(
icon: Icon(
_sort!.reverse!
? FontAwesome5.sort_alpha_up
: FontAwesome5.sort_alpha_down,
semanticLabel: _sort!.reverse!
? "Sort descending".i18n
: "Sort ascending".i18n,
),
onPressed: () => _reverse(),
),
PopupMenuButton(
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (SortType s) async {
setState(() => _sort!.type = s);
//Save to cache
int? index = Sorting.index(SortSourceTypes.PLAYLISTS);
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.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()),
),
],
child: const Icon(Icons.sort, size: 32.0),
),
Container(width: 8.0),
],
),
body: Scrollbar(
interactive: true,
controller: _scrollController,
thickness: 8.0,
child: ListView(
controller: _scrollController,
children: <Widget>[
//Search
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
onChanged: (String s) => setState(() => _filter = s),
decoration: InputDecoration(
labelText: 'Search'.i18n,
filled: true,
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey)),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey)),
)),
),
ListTile(
title: Text('Create new playlist'.i18n),
leading: const LeadingIcon(Icons.playlist_add,
color: Color(0xff009a85)),
onTap: () async {
if (settings.offlineMode) {
ScaffoldMessenger.of(context)
.snack('Cannot create playlists in offline mode'.i18n);
return;
}
MenuSheet m = MenuSheet(context);
await m.createPlaylist();
await _load();
},
),
const FreezerDivider(),
if (!settings.offlineMode && _playlists == null)
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(),
],
),
//Favorites playlist
PlaylistTile(
favoritesPlaylist,
onTap: () async {
Navigator.of(context).pushRoute(
builder: (context) => PlaylistDetails(favoritesPlaylist));
},
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
favoritesPlaylist.library = true;
m.defaultPlaylistMenu(favoritesPlaylist, details: details);
},
),
if (_playlists != null)
...List.generate(_sorted.length, (int i) {
Playlist p = _sorted[i];
return PlaylistTile(
p,
onTap: () => Navigator.of(context)
.pushRoute(builder: (context) => PlaylistDetails(p)),
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultPlaylistMenu(p, details: details, onRemove: () {
setState(() => _playlists!.remove(p));
}, onUpdate: () {
_load();
});
},
);
}),
FutureBuilder(
future: downloadManager.getOfflinePlaylists(),
builder: (context, AsyncSnapshot<List<Playlist>> snapshot) {
if (snapshot.hasError ||
!snapshot.hasData ||
snapshot.data!.isEmpty) {
return const SizedBox.shrink();
}
List<Playlist> playlists = snapshot.data!;
return Column(
children: <Widget>[
const FreezerDivider(),
Text(
'Offline playlists'.i18n,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 24.0, fontWeight: FontWeight.bold),
),
...List.generate(playlists.length, (i) {
Playlist p = playlists[i];
return PlaylistTile(
p,
onTap: () => Navigator.of(context).pushRoute(
builder: (context) => PlaylistDetails(p)),
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultPlaylistMenu(p, details: details,
onRemove: () {
setState(() {
playlists.remove(p);
_playlists!.remove(p);
});
});
},
);
})
],
);
},
)
],
),
));
}
}
class HistoryScreen extends StatefulWidget {
const HistoryScreen({super.key});
@override
State<HistoryScreen> createState() => _HistoryScreenState();
}
class _HistoryScreenState extends State<HistoryScreen> {
final ScrollController _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('History'.i18n),
actions: [
IconButton(
icon: Icon(
Icons.delete_sweep,
semanticLabel: "Clear all".i18n,
),
onPressed: () {
setState(() => cache.history = []);
cache.save();
},
)
],
),
body: Scrollbar(
interactive: true,
controller: _scrollController,
thickness: 8.0,
child: ListView.builder(
controller: _scrollController,
itemCount: cache.history.length,
itemBuilder: (BuildContext context, int i) {
Track t = cache.history[cache.history.length - i - 1];
return TrackTile.fromTrack(
t,
onTap: () {
playerHelper.playFromTrackList(
cache.history.reversed.toList(),
t.id,
QueueSource(
id: null, text: 'History'.i18n, source: 'history'));
},
onSecondary: (details) {
MenuSheet m = MenuSheet(context);
m.defaultTrackMenu(t, details: details);
},
checkTrackOffline: false,
);
},
)),
);
}
}