Pato05
f126ffef46
use get_url api by default, and fall back to old generation if get_url failed start to write a better cachemanager to implement in all systems write in more appropriate directories on windows and linux improve check for Connectivity by adding a fallback (needed for example on linux systems without NetworkManager) allow to dynamically change track quality without rebuilding the object
993 lines
32 KiB
Dart
993 lines
32 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:freezer/main.dart';
|
|
import 'package:freezer/ui/fancy_scaffold.dart';
|
|
import 'package:freezer/ui/player_bar.dart';
|
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:freezer/api/cache.dart';
|
|
import 'package:freezer/api/deezer.dart';
|
|
import 'package:freezer/api/download.dart';
|
|
import 'package:freezer/api/player/audio_handler.dart';
|
|
import 'package:freezer/ui/details_screens.dart';
|
|
import 'package:freezer/ui/error.dart';
|
|
import 'package:freezer/translations.i18n.dart';
|
|
import 'package:freezer/api/definitions.dart';
|
|
import 'package:freezer/ui/cached_image.dart';
|
|
import 'package:numberpicker/numberpicker.dart';
|
|
import 'package:share_plus/share_plus.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
class SliverTrackPersistentHeader extends SliverPersistentHeaderDelegate {
|
|
final Track track;
|
|
final double extent;
|
|
const SliverTrackPersistentHeader(this.track, {required this.extent});
|
|
|
|
@override
|
|
bool shouldRebuild(oldDelegate) => false;
|
|
|
|
@override
|
|
double get maxExtent => extent;
|
|
@override
|
|
double get minExtent => extent;
|
|
|
|
@override
|
|
Widget build(
|
|
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
|
return DecoratedBox(
|
|
decoration: BoxDecoration(color: Theme.of(context).cardColor),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const SizedBox(height: 16.0),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: <Widget>[
|
|
Semantics(
|
|
label: "Album art".i18n,
|
|
image: true,
|
|
child: CachedImage(
|
|
url: track.albumArt!.full,
|
|
height: 128,
|
|
width: 128,
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 240.0,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
Text(
|
|
track.title!,
|
|
maxLines: 1,
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 22.0, fontWeight: FontWeight.bold),
|
|
),
|
|
Text(
|
|
track.artistString,
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
style: const TextStyle(fontSize: 20.0),
|
|
),
|
|
const SizedBox(height: 8.0),
|
|
Text(
|
|
track.album!.title!,
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
),
|
|
Text(track.durationString)
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16.0),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MenuSheetOption {
|
|
final Widget label;
|
|
final Widget? icon;
|
|
final VoidCallback onTap;
|
|
|
|
const MenuSheetOption(
|
|
this.label, {
|
|
required this.onTap,
|
|
this.icon,
|
|
});
|
|
}
|
|
|
|
class MenuSheet {
|
|
BuildContext context;
|
|
Function? navigateCallback;
|
|
|
|
MenuSheet(this.context, {this.navigateCallback});
|
|
|
|
void _showContextMenu(List<MenuSheetOption> options,
|
|
{required TapDownDetails details}) {
|
|
final overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
|
|
final actualPosition = overlay.globalToLocal(details.globalPosition);
|
|
showMenu(
|
|
elevation: 4.0,
|
|
context: context,
|
|
constraints: const BoxConstraints(maxWidth: 300.0),
|
|
position:
|
|
RelativeRect.fromSize(actualPosition & Size.zero, overlay.size),
|
|
items: options
|
|
.map((option) => PopupMenuItem(
|
|
onTap: option.onTap,
|
|
child: option.icon == null
|
|
? option.label
|
|
: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
option.icon!,
|
|
const SizedBox(width: 8.0),
|
|
Flexible(child: option.label),
|
|
])))
|
|
.toList(growable: false));
|
|
}
|
|
|
|
//===================
|
|
// DEFAULT
|
|
//===================
|
|
|
|
void show(List<MenuSheetOption> options, {TapDownDetails? details}) {
|
|
if (details != null) {
|
|
_showContextMenu(options, details: details);
|
|
return;
|
|
}
|
|
|
|
showModalBottomSheet(
|
|
isScrollControlled: false, // true,
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return SafeArea(
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
maxHeight: (MediaQuery.of(context).orientation ==
|
|
Orientation.landscape)
|
|
? 220
|
|
: 350,
|
|
),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
children: options
|
|
.map((option) => ListTile(
|
|
title: option.label,
|
|
leading: option.icon,
|
|
onTap: option.onTap,
|
|
))
|
|
.toList(growable: false)),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
//===================
|
|
// TRACK
|
|
//===================
|
|
|
|
void showWithTrack(Track track, List<MenuSheetOption> options,
|
|
{TapDownDetails? details}) {
|
|
if (details != null) {
|
|
_showContextMenu(options, details: details);
|
|
return;
|
|
}
|
|
|
|
showModalBottomSheet(
|
|
backgroundColor: Colors.transparent,
|
|
context: context,
|
|
isScrollControlled: true,
|
|
enableDrag: false,
|
|
showDragHandle: false,
|
|
useSafeArea: true,
|
|
elevation: 0.0,
|
|
builder: (BuildContext context) {
|
|
return DraggableScrollableSheet(
|
|
initialChildSize: 0.5,
|
|
minChildSize: 0.45,
|
|
maxChildSize: 0.75,
|
|
builder: (context, scrollController) => Material(
|
|
type: MaterialType.card,
|
|
clipBehavior: Clip.antiAlias,
|
|
borderRadius:
|
|
const BorderRadius.vertical(top: Radius.circular(20.0)),
|
|
child: CustomScrollView(
|
|
controller: scrollController,
|
|
slivers: [
|
|
SliverPersistentHeader(
|
|
pinned: true,
|
|
delegate: SliverTrackPersistentHeader(track,
|
|
extent: 128.0 + 16.0 + 16.0)),
|
|
SliverList(
|
|
delegate: SliverChildListDelegate.fixed(options
|
|
.map((option) => ListTile(
|
|
title: option.label,
|
|
leading: option.icon,
|
|
onTap: option.onTap,
|
|
))
|
|
.toList(growable: false))),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
//Default track options
|
|
void defaultTrackMenu(
|
|
Track track, {
|
|
List<MenuSheetOption> options = const [],
|
|
Function? onRemove,
|
|
TapDownDetails? details,
|
|
}) {
|
|
showWithTrack(
|
|
track,
|
|
<MenuSheetOption>[
|
|
addToQueueNext(track),
|
|
addToQueue(track),
|
|
(cache.checkTrackFavorite(track))
|
|
? removeFavoriteTrack(track, onUpdate: onRemove)
|
|
: addTrackFavorite(track),
|
|
addToPlaylist(track),
|
|
downloadTrack(track),
|
|
offlineTrack(track),
|
|
shareTile('track', track.id),
|
|
playMix(track),
|
|
showAlbum(track.album!),
|
|
...List.generate(
|
|
track.artists!.length, (i) => showArtist(track.artists![i])),
|
|
...options
|
|
],
|
|
details: details);
|
|
}
|
|
|
|
//===================
|
|
// TRACK OPTIONS
|
|
//===================
|
|
|
|
MenuSheetOption addToQueueNext(Track t) =>
|
|
MenuSheetOption(Text('Play next'.i18n),
|
|
icon: const Icon(Icons.playlist_play), onTap: () async {
|
|
//-1 = next
|
|
await audioHandler.insertQueueItem(-1, await t.toMediaItem());
|
|
_close();
|
|
});
|
|
|
|
MenuSheetOption addToQueue(Track t) =>
|
|
MenuSheetOption(Text('Add to queue'.i18n),
|
|
icon: const Icon(Icons.playlist_add), onTap: () async {
|
|
await audioHandler.addQueueItem(await t.toMediaItem());
|
|
_close();
|
|
});
|
|
|
|
MenuSheetOption addTrackFavorite(Track t) =>
|
|
MenuSheetOption(Text('Add track to favorites'.i18n),
|
|
icon: const Icon(Icons.favorite), onTap: () async {
|
|
await deezerAPI.addFavoriteTrack(t.id);
|
|
//Make track offline, if favorites are offline
|
|
Playlist p = Playlist(id: deezerAPI.favoritesPlaylistId!);
|
|
if (await downloadManager.checkOffline(playlist: p)) {
|
|
downloadManager.addOfflinePlaylist(p);
|
|
}
|
|
ScaffoldMessenger.of(context).snack('Added to library'.i18n);
|
|
//Add to cache
|
|
cache.libraryTracks.add(t.id);
|
|
|
|
_close();
|
|
});
|
|
|
|
MenuSheetOption downloadTrack(Track t) => MenuSheetOption(
|
|
Text('Download'.i18n),
|
|
icon: const Icon(Icons.file_download),
|
|
onTap: () async {
|
|
if (await downloadManager.addOfflineTrack(t,
|
|
private: false, context: context, isSingleton: true) !=
|
|
false) showDownloadStartedToast();
|
|
_close();
|
|
},
|
|
);
|
|
|
|
MenuSheetOption addToPlaylist(Track t) => MenuSheetOption(
|
|
Text('Add to playlist'.i18n),
|
|
icon: const Icon(Icons.playlist_add),
|
|
onTap: () async {
|
|
//Show dialog to pick playlist
|
|
await showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return SelectPlaylistDialog(
|
|
track: t,
|
|
callback: (Playlist p) async {
|
|
await deezerAPI.addToPlaylist(t.id, p.id);
|
|
//Update the playlist if offline
|
|
if (await downloadManager.checkOffline(playlist: p)) {
|
|
downloadManager.addOfflinePlaylist(p);
|
|
}
|
|
ScaffoldMessenger.of(context)
|
|
.snack("${"Track added to".i18n} ${p.title}");
|
|
});
|
|
});
|
|
_close();
|
|
},
|
|
);
|
|
|
|
MenuSheetOption removeFromPlaylist(Track t, Playlist? p) => MenuSheetOption(
|
|
Text('Remove from playlist'.i18n),
|
|
icon: const Icon(Icons.delete),
|
|
onTap: () async {
|
|
await deezerAPI.removeFromPlaylist(t.id, p!.id);
|
|
ScaffoldMessenger.of(context)
|
|
.snack('${'Track removed from'.i18n} ${p.title}');
|
|
_close();
|
|
},
|
|
);
|
|
|
|
MenuSheetOption removeFavoriteTrack(Track t, {onUpdate}) => MenuSheetOption(
|
|
Text('Remove favorite'.i18n),
|
|
icon: const Icon(Icons.delete),
|
|
onTap: () async {
|
|
await deezerAPI.removeFavorite(t.id);
|
|
//Check if favorites playlist is offline, update it
|
|
Playlist p = Playlist(id: deezerAPI.favoritesPlaylistId!);
|
|
if (await downloadManager.checkOffline(playlist: p)) {
|
|
await downloadManager.addOfflinePlaylist(p);
|
|
}
|
|
//Remove from cache
|
|
cache.libraryTracks.removeWhere((i) => i == t.id);
|
|
ScaffoldMessenger.of(context)
|
|
.snack('Track removed from library'.i18n);
|
|
if (onUpdate != null) onUpdate();
|
|
_close();
|
|
},
|
|
);
|
|
|
|
//Redirect to artist page (ie from track)
|
|
MenuSheetOption showArtist(Artist a) => MenuSheetOption(
|
|
Text(
|
|
'${'Go to'.i18n} ${a.name}',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
icon: const Icon(Icons.recent_actors),
|
|
onTap: () {
|
|
_close();
|
|
navigatorKey.currentState!
|
|
.push(MaterialPageRoute(builder: (context) => ArtistDetails(a)));
|
|
|
|
if (navigateCallback != null) {
|
|
navigateCallback!();
|
|
}
|
|
},
|
|
);
|
|
|
|
MenuSheetOption showAlbum(Album a) => MenuSheetOption(
|
|
Text(
|
|
'${'Go to'.i18n} ${a.title}',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
icon: const Icon(Icons.album),
|
|
onTap: () {
|
|
_close();
|
|
navigatorKey.currentState!
|
|
.push(MaterialPageRoute(builder: (context) => AlbumDetails(a)));
|
|
|
|
if (navigateCallback != null) {
|
|
navigateCallback!();
|
|
}
|
|
},
|
|
);
|
|
|
|
MenuSheetOption playMix(Track track) => MenuSheetOption(
|
|
Text('Play mix'.i18n),
|
|
icon: const Icon(Icons.online_prediction),
|
|
onTap: () async {
|
|
playerHelper.playMix(track.id, track.title!);
|
|
_close();
|
|
},
|
|
);
|
|
|
|
MenuSheetOption offlineTrack(Track track) => MenuSheetOption(
|
|
FutureBuilder(
|
|
future: downloadManager.checkOffline(track: track),
|
|
builder: (context, snapshot) {
|
|
bool isOffline = snapshot.data ?? track.offline ?? false;
|
|
return Text(isOffline ? 'Remove offline'.i18n : 'Offline'.i18n);
|
|
}),
|
|
icon: const Icon(Icons.offline_pin), onTap: () async {
|
|
if (await downloadManager.checkOffline(track: track)) {
|
|
await downloadManager.removeOfflineTracks([track]);
|
|
ScaffoldMessenger.of(context)
|
|
.snack("Track removed from offline!".i18n);
|
|
} else {
|
|
await downloadManager.addOfflineTrack(track,
|
|
private: true, context: context);
|
|
}
|
|
_close();
|
|
});
|
|
|
|
//===================
|
|
// ALBUM
|
|
//===================
|
|
|
|
//Default album options
|
|
void defaultAlbumMenu(Album album,
|
|
{List<MenuSheetOption> options = const [],
|
|
Function? onRemove,
|
|
TapDownDetails? details}) {
|
|
show([
|
|
album.library!
|
|
? removeAlbum(album, onRemove: onRemove)
|
|
: libraryAlbum(album),
|
|
downloadAlbum(album),
|
|
offlineAlbum(album),
|
|
shareTile('album', album.id),
|
|
...options
|
|
]);
|
|
}
|
|
|
|
//===================
|
|
// ALBUM OPTIONS
|
|
//===================
|
|
|
|
MenuSheetOption downloadAlbum(Album a) =>
|
|
MenuSheetOption(Text('Download'.i18n),
|
|
icon: const Icon(Icons.file_download), onTap: () async {
|
|
_close();
|
|
if (await downloadManager.addOfflineAlbum(a,
|
|
private: false, context: context) !=
|
|
false) showDownloadStartedToast();
|
|
});
|
|
|
|
MenuSheetOption offlineAlbum(Album a) => MenuSheetOption(
|
|
Text('Make offline'.i18n),
|
|
icon: const Icon(Icons.offline_pin),
|
|
onTap: () async {
|
|
await deezerAPI.addFavoriteAlbum(a.id);
|
|
await downloadManager.addOfflineAlbum(a, private: true);
|
|
_close();
|
|
showDownloadStartedToast();
|
|
},
|
|
);
|
|
|
|
MenuSheetOption libraryAlbum(Album a) => MenuSheetOption(
|
|
Text('Add to library'.i18n),
|
|
icon: const Icon(Icons.library_music),
|
|
onTap: () async {
|
|
await deezerAPI.addFavoriteAlbum(a.id);
|
|
ScaffoldMessenger.of(context).snack('Added to library'.i18n);
|
|
_close();
|
|
},
|
|
);
|
|
|
|
//Remove album from favorites
|
|
MenuSheetOption removeAlbum(Album a, {Function? onRemove}) => MenuSheetOption(
|
|
Text('Remove album'.i18n),
|
|
icon: const Icon(Icons.delete),
|
|
onTap: () async {
|
|
await deezerAPI.removeAlbum(a.id);
|
|
await downloadManager.removeOfflineAlbum(a.id);
|
|
ScaffoldMessenger.of(context).snack('Album removed'.i18n);
|
|
if (onRemove != null) onRemove();
|
|
_close();
|
|
},
|
|
);
|
|
|
|
//===================
|
|
// ARTIST
|
|
//===================
|
|
|
|
void defaultArtistMenu(Artist artist,
|
|
{List<MenuSheetOption> options = const [],
|
|
Function? onRemove,
|
|
TapDownDetails? details}) {
|
|
show(details: details, [
|
|
artist.library!
|
|
? removeArtist(artist, onRemove: onRemove)
|
|
: favoriteArtist(artist),
|
|
shareTile('artist', artist.id),
|
|
...options
|
|
]);
|
|
}
|
|
|
|
//===================
|
|
// ARTIST OPTIONS
|
|
//===================
|
|
|
|
MenuSheetOption removeArtist(Artist a, {Function? onRemove}) =>
|
|
MenuSheetOption(
|
|
Text('Remove from favorites'.i18n),
|
|
icon: const Icon(Icons.delete),
|
|
onTap: () async {
|
|
await deezerAPI.removeArtist(a.id);
|
|
ScaffoldMessenger.of(context)
|
|
.snack('Artist removed from library'.i18n);
|
|
if (onRemove != null) onRemove();
|
|
_close();
|
|
},
|
|
);
|
|
|
|
MenuSheetOption favoriteArtist(Artist a) => MenuSheetOption(
|
|
Text('Add to favorites'.i18n),
|
|
icon: const Icon(Icons.favorite),
|
|
onTap: () async {
|
|
await deezerAPI.addFavoriteArtist(a.id);
|
|
ScaffoldMessenger.of(context).snack('Added to library'.i18n);
|
|
_close();
|
|
},
|
|
);
|
|
|
|
//===================
|
|
// PLAYLIST
|
|
//===================
|
|
|
|
void defaultPlaylistMenu(Playlist playlist,
|
|
{List<MenuSheetOption> options = const [],
|
|
Function? onRemove,
|
|
Function? onUpdate,
|
|
TapDownDetails? details}) {
|
|
show(details: details, [
|
|
if (playlist.library != null)
|
|
playlist.library!
|
|
? removePlaylistLibrary(playlist, onRemove: onRemove)
|
|
: addPlaylistLibrary(playlist),
|
|
addPlaylistOffline(playlist),
|
|
downloadPlaylist(playlist),
|
|
shareTile('playlist', playlist.id),
|
|
if (playlist.user!.id == deezerAPI.userId)
|
|
editPlaylist(playlist, onUpdate: onUpdate),
|
|
...options
|
|
]);
|
|
}
|
|
|
|
//===================
|
|
// PLAYLIST OPTIONS
|
|
//===================
|
|
|
|
MenuSheetOption removePlaylistLibrary(Playlist p, {Function? onRemove}) =>
|
|
MenuSheetOption(
|
|
Text('Remove from library'.i18n),
|
|
icon: const Icon(Icons.delete),
|
|
onTap: () async {
|
|
if (p.user!.id!.trim() == deezerAPI.userId) {
|
|
//Delete playlist if own
|
|
await deezerAPI.deletePlaylist(p.id);
|
|
} else {
|
|
//Just remove from library
|
|
await deezerAPI.removePlaylist(p.id);
|
|
}
|
|
downloadManager.removeOfflinePlaylist(p.id);
|
|
if (onRemove != null) onRemove();
|
|
_close();
|
|
},
|
|
);
|
|
|
|
MenuSheetOption addPlaylistLibrary(Playlist p) => MenuSheetOption(
|
|
Text('Add playlist to library'.i18n),
|
|
icon: const Icon(Icons.favorite),
|
|
onTap: () async {
|
|
await deezerAPI.addPlaylist(p.id);
|
|
ScaffoldMessenger.of(context).snack('Added playlist to library'.i18n);
|
|
_close();
|
|
},
|
|
);
|
|
|
|
MenuSheetOption addPlaylistOffline(Playlist p) => MenuSheetOption(
|
|
Text('Make playlist offline'.i18n),
|
|
icon: const Icon(Icons.offline_pin),
|
|
onTap: () async {
|
|
//Add to library
|
|
await deezerAPI.addPlaylist(p.id);
|
|
downloadManager.addOfflinePlaylist(p, private: true);
|
|
_close();
|
|
showDownloadStartedToast();
|
|
},
|
|
);
|
|
|
|
MenuSheetOption downloadPlaylist(Playlist p) => MenuSheetOption(
|
|
Text('Download playlist'.i18n),
|
|
icon: const Icon(Icons.file_download),
|
|
onTap: () async {
|
|
_close();
|
|
if (await downloadManager.addOfflinePlaylist(p,
|
|
private: false, context: context) !=
|
|
false) showDownloadStartedToast();
|
|
},
|
|
);
|
|
|
|
MenuSheetOption editPlaylist(Playlist p, {Function? onUpdate}) =>
|
|
MenuSheetOption(
|
|
Text('Edit playlist'.i18n),
|
|
icon: const Icon(Icons.edit),
|
|
onTap: () async {
|
|
await showDialog(
|
|
context: context,
|
|
builder: (context) => CreatePlaylistDialog(playlist: p));
|
|
_close();
|
|
if (onUpdate != null) onUpdate();
|
|
},
|
|
);
|
|
|
|
//===================
|
|
// SHOW/EPISODE
|
|
//===================
|
|
|
|
defaultShowEpisodeMenu(Show s, ShowEpisode e,
|
|
{List<MenuSheetOption> options = const [], TapDownDetails? details}) {
|
|
show(details: details, [
|
|
shareTile('episode', e.id),
|
|
shareShow(s.id),
|
|
downloadExternalEpisode(e),
|
|
...options
|
|
]);
|
|
}
|
|
|
|
MenuSheetOption shareShow(String? id) => MenuSheetOption(
|
|
Text('Share show'.i18n),
|
|
icon: const Icon(Icons.share),
|
|
onTap: () async {
|
|
Share.share('https://deezer.com/show/$id');
|
|
},
|
|
);
|
|
|
|
//Open direct download link in browser
|
|
MenuSheetOption downloadExternalEpisode(ShowEpisode e) => MenuSheetOption(
|
|
Text('Download externally'.i18n),
|
|
icon: const Icon(Icons.file_download),
|
|
onTap: () async {
|
|
launchUrl(Uri.parse(e.url!));
|
|
},
|
|
);
|
|
|
|
//===================
|
|
// OTHER
|
|
//===================
|
|
|
|
showDownloadStartedToast() {
|
|
ScaffoldMessenger.of(context).snack('Downloads added!'.i18n);
|
|
}
|
|
|
|
//Create playlist
|
|
Future createPlaylist() async {
|
|
await showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return const CreatePlaylistDialog();
|
|
});
|
|
}
|
|
|
|
MenuSheetOption shareTile(String type, String? id) => MenuSheetOption(
|
|
Text('Share'.i18n),
|
|
icon: const Icon(Icons.share),
|
|
onTap: () async {
|
|
Share.share('https://deezer.com/$type/$id');
|
|
},
|
|
);
|
|
|
|
MenuSheetOption sleepTimer() => MenuSheetOption(
|
|
Text('Sleep timer'.i18n),
|
|
icon: const Icon(Icons.access_time),
|
|
onTap: () async {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return const SleepTimerDialog();
|
|
});
|
|
},
|
|
);
|
|
|
|
MenuSheetOption wakelock() => MenuSheetOption(
|
|
Text('Keep the screen on'.i18n),
|
|
icon: const Icon(Icons.screen_lock_portrait),
|
|
onTap: () async {
|
|
_close();
|
|
//Enable
|
|
if (!cache.wakelock) {
|
|
WakelockPlus.enable();
|
|
ScaffoldMessenger.of(context).snack('Wakelock enabled!'.i18n);
|
|
cache.wakelock = true;
|
|
return;
|
|
}
|
|
//Disable
|
|
WakelockPlus.disable();
|
|
ScaffoldMessenger.of(context).snack('Wakelock disabled!'.i18n);
|
|
cache.wakelock = false;
|
|
},
|
|
);
|
|
|
|
void _close() {
|
|
FancyScaffold.of(context)!.dragController.fling(velocity: -1.0);
|
|
// Navigator.of(context).pop();
|
|
}
|
|
}
|
|
|
|
class SleepTimerDialog extends StatefulWidget {
|
|
const SleepTimerDialog({super.key});
|
|
|
|
@override
|
|
State<SleepTimerDialog> createState() => _SleepTimerDialogState();
|
|
}
|
|
|
|
class _SleepTimerDialogState extends State<SleepTimerDialog> {
|
|
int hours = 0;
|
|
int minutes = 30;
|
|
|
|
String _endTime() {
|
|
return '${cache.sleepTimerTime!.hour.toString().padLeft(2, '0')}:${cache.sleepTimerTime!.minute.toString().padLeft(2, '0')}';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Sleep timer'.i18n),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text('Hours:'.i18n),
|
|
NumberPicker(
|
|
value: hours,
|
|
minValue: 0,
|
|
maxValue: 69,
|
|
onChanged: (v) => setState(() => hours = v),
|
|
),
|
|
],
|
|
),
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text('Minutes:'.i18n),
|
|
NumberPicker(
|
|
value: minutes,
|
|
minValue: 0,
|
|
maxValue: 60,
|
|
onChanged: (v) => setState(() => minutes = v),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
Container(height: 4.0),
|
|
if (cache.sleepTimerTime != null)
|
|
Text(
|
|
'${'Current timer ends at'.i18n}: ${_endTime()}',
|
|
textAlign: TextAlign.center,
|
|
)
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
child: Text('Dismiss'.i18n),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
if (cache.sleepTimer != null)
|
|
TextButton(
|
|
child: Text('Cancel current timer'.i18n),
|
|
onPressed: () {
|
|
cache.sleepTimer!.cancel();
|
|
cache.sleepTimer = null;
|
|
cache.sleepTimerTime = null;
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
TextButton(
|
|
child: Text('Save'.i18n),
|
|
onPressed: () {
|
|
Duration duration = Duration(hours: hours, minutes: minutes);
|
|
if (cache.sleepTimer != null) {
|
|
cache.sleepTimer!.cancel();
|
|
}
|
|
//Create timer
|
|
cache.sleepTimer =
|
|
Stream.fromFuture(Future.delayed(duration)).listen((_) {
|
|
audioHandler.pause();
|
|
cache.sleepTimer!.cancel();
|
|
cache.sleepTimerTime = null;
|
|
cache.sleepTimer = null;
|
|
});
|
|
cache.sleepTimerTime = DateTime.now().add(duration);
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class SelectPlaylistDialog extends StatefulWidget {
|
|
final Track? track;
|
|
final Function? callback;
|
|
const SelectPlaylistDialog({this.track, this.callback, Key? key})
|
|
: super(key: key);
|
|
|
|
@override
|
|
State<SelectPlaylistDialog> createState() => _SelectPlaylistDialogState();
|
|
}
|
|
|
|
class _SelectPlaylistDialogState extends State<SelectPlaylistDialog> {
|
|
bool createNew = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
//Create new playlist
|
|
if (createNew) {
|
|
if (widget.track == null) {
|
|
return const CreatePlaylistDialog();
|
|
}
|
|
return CreatePlaylistDialog(tracks: [widget.track]);
|
|
}
|
|
|
|
return AlertDialog(
|
|
title: Text('Select playlist'.i18n),
|
|
content: FutureBuilder(
|
|
future: deezerAPI.getPlaylists(),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.hasError) {
|
|
const SizedBox(
|
|
height: 100,
|
|
child: ErrorScreen(),
|
|
);
|
|
}
|
|
if (snapshot.connectionState != ConnectionState.done) {
|
|
return const SizedBox(
|
|
height: 100,
|
|
child: Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
);
|
|
}
|
|
|
|
List<Playlist> playlists = snapshot.data!;
|
|
return SingleChildScrollView(
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
...List.generate(
|
|
playlists.length,
|
|
(i) => ListTile(
|
|
title: Text(playlists[i].title!),
|
|
leading: CachedImage(
|
|
url: playlists[i].image!.thumb,
|
|
),
|
|
onTap: () {
|
|
if (widget.callback != null) {
|
|
widget.callback!(playlists[i]);
|
|
}
|
|
Navigator.of(context).pop();
|
|
},
|
|
)),
|
|
ListTile(
|
|
title: Text('Create new playlist'.i18n),
|
|
leading: const Icon(Icons.add),
|
|
onTap: () async {
|
|
setState(() {
|
|
createNew = true;
|
|
});
|
|
},
|
|
)
|
|
]),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class CreatePlaylistDialog extends StatefulWidget {
|
|
final List<Track?>? tracks;
|
|
//If playlist not null, update
|
|
final Playlist? playlist;
|
|
const CreatePlaylistDialog({this.tracks, this.playlist, Key? key})
|
|
: super(key: key);
|
|
|
|
@override
|
|
State<CreatePlaylistDialog> createState() => _CreatePlaylistDialogState();
|
|
}
|
|
|
|
class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
|
|
int? _playlistType = 1;
|
|
String _title = '';
|
|
String _description = '';
|
|
TextEditingController? _titleController;
|
|
TextEditingController? _descController;
|
|
|
|
//Create or edit mode
|
|
bool get edit => widget.playlist != null;
|
|
|
|
@override
|
|
void initState() {
|
|
//Edit playlist mode
|
|
if (edit) {
|
|
_titleController = TextEditingController(text: widget.playlist!.title);
|
|
_descController =
|
|
TextEditingController(text: widget.playlist!.description);
|
|
}
|
|
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text(edit ? 'Edit playlist'.i18n : 'Create playlist'.i18n),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
TextField(
|
|
decoration: InputDecoration(labelText: 'Title'.i18n),
|
|
controller: _titleController ?? TextEditingController(),
|
|
onChanged: (String s) => _title = s,
|
|
),
|
|
TextField(
|
|
onChanged: (String s) => _description = s,
|
|
controller: _descController ?? TextEditingController(),
|
|
decoration: InputDecoration(labelText: 'Description'.i18n),
|
|
),
|
|
Container(
|
|
height: 4.0,
|
|
),
|
|
DropdownButton<int>(
|
|
value: _playlistType,
|
|
onChanged: (int? v) {
|
|
setState(() => _playlistType = v);
|
|
},
|
|
items: [
|
|
DropdownMenuItem<int>(
|
|
value: 1,
|
|
child: Text('Private'.i18n),
|
|
),
|
|
DropdownMenuItem<int>(
|
|
value: 2,
|
|
child: Text('Collaborative'.i18n),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
child: Text('Cancel'.i18n),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
TextButton(
|
|
child: Text(edit ? 'Update'.i18n : 'Create'.i18n),
|
|
onPressed: () async {
|
|
if (edit) {
|
|
//Update
|
|
await deezerAPI.updatePlaylist(widget.playlist!.id,
|
|
_titleController!.value.text, _descController!.value.text,
|
|
status: _playlistType);
|
|
ScaffoldMessenger.of(context).snack('Playlist updated!'.i18n);
|
|
} else {
|
|
List<String> tracks = [];
|
|
if (widget.tracks != null) {
|
|
tracks = widget.tracks!.map<String>((t) => t!.id).toList();
|
|
}
|
|
await deezerAPI.createPlaylist(_title,
|
|
status: _playlistType,
|
|
description: _description,
|
|
trackIds: tracks);
|
|
ScaffoldMessenger.of(context).snack('Playlist created!'.i18n);
|
|
}
|
|
Navigator.of(context).pop();
|
|
},
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|