982 lines
32 KiB
Dart
982 lines
32 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:freezer/main.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 {
|
|
final BuildContext context;
|
|
final VoidCallback? navigateCallback;
|
|
|
|
MenuSheet(this.context, {this.navigateCallback});
|
|
|
|
void _showContextMenu(List<MenuSheetOption> options,
|
|
{required TapUpDetails details}) {
|
|
final overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
|
|
final actualPosition = overlay.globalToLocal(details.globalPosition);
|
|
showMenu(
|
|
clipBehavior: Clip.antiAlias,
|
|
elevation: 4.0,
|
|
context: context,
|
|
constraints: const BoxConstraints(maxWidth: 300.0),
|
|
position:
|
|
RelativeRect.fromSize(actualPosition & Size.zero, overlay.size),
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(28.0))),
|
|
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, {TapUpDetails? details}) {
|
|
if (details != null) {
|
|
_showContextMenu(options, details: details);
|
|
return;
|
|
}
|
|
|
|
showModalBottomSheet(
|
|
isScrollControlled: false, // true,
|
|
context: context,
|
|
useSafeArea: true,
|
|
builder: (BuildContext context) {
|
|
return 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.call();
|
|
Navigator.pop(context);
|
|
},
|
|
))
|
|
.toList(growable: false)),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
//===================
|
|
// TRACK
|
|
//===================
|
|
|
|
void showWithTrack(Track track, List<MenuSheetOption> options,
|
|
{TapUpDetails? details}) {
|
|
if (details != null) {
|
|
_showContextMenu(options, details: details);
|
|
return;
|
|
}
|
|
|
|
showModalBottomSheet(
|
|
backgroundColor: Colors.transparent,
|
|
context: context,
|
|
isScrollControlled: true,
|
|
enableDrag: false,
|
|
showDragHandle: false,
|
|
elevation: 0.0,
|
|
builder: (BuildContext context) {
|
|
return DraggableScrollableSheet(
|
|
initialChildSize: 0.5,
|
|
minChildSize: 0.45,
|
|
maxChildSize: 0.95,
|
|
builder: (context, scrollController) => Material(
|
|
type: MaterialType.card,
|
|
clipBehavior: Clip.antiAlias,
|
|
borderRadius:
|
|
const BorderRadius.vertical(top: Radius.circular(20.0)),
|
|
child: SafeArea(
|
|
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.call();
|
|
Navigator.pop(context);
|
|
},
|
|
))
|
|
.toList(growable: false))),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
//Default track options
|
|
void defaultTrackMenu(
|
|
Track track, {
|
|
List<MenuSheetOption> options = const [],
|
|
List<MenuSheetOption> optionsTop = const [],
|
|
Function? onRemove,
|
|
TapUpDetails? details,
|
|
}) {
|
|
showWithTrack(
|
|
track,
|
|
<MenuSheetOption>[
|
|
...optionsTop,
|
|
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());
|
|
});
|
|
|
|
MenuSheetOption addToQueue(Track t) =>
|
|
MenuSheetOption(Text('Add to queue'.i18n),
|
|
icon: const Icon(Icons.playlist_add), onTap: () async {
|
|
await audioHandler.addQueueItem(await t.toMediaItem());
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
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();
|
|
},
|
|
);
|
|
|
|
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}");
|
|
});
|
|
});
|
|
},
|
|
);
|
|
|
|
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}');
|
|
},
|
|
);
|
|
|
|
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();
|
|
},
|
|
);
|
|
|
|
//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: () {
|
|
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: () {
|
|
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 {
|
|
// I couldn't find this API request within the Deezer app, but the
|
|
// same button uses the getSearchTrackMix API call, so let's use that
|
|
// instead.
|
|
|
|
// playerHelper.playMix(track.id, track.title!);
|
|
|
|
playerHelper.playSearchMix(track.id, track.title!);
|
|
},
|
|
);
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
//===================
|
|
// ALBUM
|
|
//===================
|
|
|
|
//Default album options
|
|
void defaultAlbumMenu(Album album,
|
|
{List<MenuSheetOption> options = const [],
|
|
Function? onRemove,
|
|
TapUpDetails? 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 {
|
|
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);
|
|
|
|
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);
|
|
},
|
|
);
|
|
|
|
//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();
|
|
},
|
|
);
|
|
|
|
//===================
|
|
// ARTIST
|
|
//===================
|
|
|
|
void defaultArtistMenu(Artist artist,
|
|
{List<MenuSheetOption> options = const [],
|
|
Function? onRemove,
|
|
TapUpDetails? 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();
|
|
},
|
|
);
|
|
|
|
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);
|
|
},
|
|
);
|
|
|
|
//===================
|
|
// PLAYLIST
|
|
//===================
|
|
|
|
void defaultPlaylistMenu(Playlist playlist,
|
|
{List<MenuSheetOption> options = const [],
|
|
Function? onRemove,
|
|
Function? onUpdate,
|
|
TapUpDetails? 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();
|
|
},
|
|
);
|
|
|
|
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);
|
|
},
|
|
);
|
|
|
|
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);
|
|
|
|
showDownloadStartedToast();
|
|
},
|
|
);
|
|
|
|
MenuSheetOption downloadPlaylist(Playlist p) => MenuSheetOption(
|
|
Text('Download playlist'.i18n),
|
|
icon: const Icon(Icons.file_download),
|
|
onTap: () async {
|
|
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));
|
|
|
|
if (onUpdate != null) onUpdate();
|
|
},
|
|
);
|
|
|
|
//===================
|
|
// SHOW/EPISODE
|
|
//===================
|
|
|
|
defaultShowEpisodeMenu(Show s, ShowEpisode e,
|
|
{List<MenuSheetOption> options = const [], TapUpDetails? 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 {
|
|
//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;
|
|
},
|
|
);
|
|
}
|
|
|
|
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();
|
|
},
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|