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: [ 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: [ 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 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: [ 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: [CircularProgressIndicator()], ), ); } List data = snapshot.data!; return Column( children: [ 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 createState() => _LibraryTracksState(); } class _LibraryTracksState extends State { bool _loading = false; bool _loadingTracks = false; final ScrollController _scrollController = ScrollController(); List tracks = []; List allTracks = []; int? trackCount; Sorting? _sort = Sorting(sourceType: SortSourceTypes.TRACKS); Playlist get _playlist => Playlist(id: deezerAPI.favoritesPlaylistId!); List get _sorted { List 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? 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 _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 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) => >[ 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: [ if (!_loading) Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ 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 createState() => _LibraryAlbumsState(); } class _LibraryAlbumsState extends State { List? _albums; Sorting? _sort = Sorting(sourceType: SortSourceTypes.ALBUMS); final ScrollController _scrollController = ScrollController(); List get _sorted { List 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 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) => >[ 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: [ 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 albums = snapshot.data as List; return Column( children: [ 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 createState() => _LibraryArtistsState(); } class _LibraryArtistsState extends State { late List _artists; Sorting? _sort = Sorting(sourceType: SortSourceTypes.ARTISTS); bool _loading = true; bool _error = false; final ScrollController _scrollController = ScrollController(); List get _sorted { List 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? 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) => >[ 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: [ 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 createState() => _LibraryPlaylistsState(); } class _LibraryPlaylistsState extends State { List? _playlists; Sorting? _sort = Sorting(sourceType: SortSourceTypes.PLAYLISTS); final ScrollController _scrollController = ScrollController(); String _filter = ''; List get _sorted { List 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 (cache.favoritePlaylists != null) { setState(() => _playlists = cache.favoritePlaylists!.value.values.toList(growable: false)); if (DateTime.now().difference(cache.favoritePlaylists!.updatedAt) < const Duration(hours: 1)) return; } if (!settings.offlineMode) { try { final List playlists = await deezerAPI.getPlaylists(); setState(() => _playlists = playlists); if (cache.favoritePlaylists == null) { cache.favoritePlaylists = CacheEntry({for (final p in playlists) p.id: p}); } else { // update non-destructively final oldEntry = cache.favoritePlaylists!.value; final newEntry = {}; for (final playlist in playlists) { if (oldEntry.containsKey(playlist.id)) { newEntry[playlist.id] = oldEntry[playlist.id]!; } else { newEntry[playlist.id] = playlist; } } } await cache.save(); 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) => >[ 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: [ //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: [ 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> snapshot) { if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { return const SizedBox.shrink(); } List playlists = snapshot.data!; return Column( children: [ 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 createState() => _HistoryScreenState(); } class _HistoryScreenState extends State { 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, ); }, )), ); } }