Pato05
2862c9ec05
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
1233 lines
43 KiB
Dart
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,
|
|
);
|
|
},
|
|
)),
|
|
);
|
|
}
|
|
}
|