Initial commit
This commit is contained in:
commit
ed087bc583
123 changed files with 10390 additions and 0 deletions
203
lib/ui/cached_image.dart
Normal file
203
lib/ui/cached_image.dart
Normal file
|
@ -0,0 +1,203 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
ImagesDatabase imagesDatabase = ImagesDatabase();
|
||||
|
||||
class ImagesDatabase {
|
||||
|
||||
/*
|
||||
images.db:
|
||||
Table: images
|
||||
Fields:
|
||||
id - id
|
||||
name - md5 hash of url. also filename
|
||||
url - url
|
||||
permanent - 0/1 - if image is cached or offline
|
||||
*/
|
||||
|
||||
|
||||
Database db;
|
||||
String imagesPath;
|
||||
|
||||
//Prepare database
|
||||
Future init() async {
|
||||
String dir = await getDatabasesPath();
|
||||
String path = p.join(dir, 'images.db');
|
||||
db = await openDatabase(
|
||||
path,
|
||||
version: 1,
|
||||
singleInstance: false,
|
||||
onCreate: (Database db, int version) async {
|
||||
//Create table on db created
|
||||
await db.execute('CREATE TABLE images (id INTEGER PRIMARY KEY, name TEXT, url TEXT, permanent INTEGER)');
|
||||
}
|
||||
);
|
||||
//Prepare folders
|
||||
imagesPath = p.join((await getApplicationDocumentsDirectory()).path, 'images/');
|
||||
Directory imagesDir = Directory(imagesPath);
|
||||
await imagesDir.create(recursive: true);
|
||||
}
|
||||
|
||||
String getPath(String name) {
|
||||
return p.join(imagesPath, name);
|
||||
}
|
||||
|
||||
//Get image url/path, cache it
|
||||
Future<String> getImage(String url, {bool permanent = false}) async {
|
||||
//Already file
|
||||
if (!url.startsWith('http')) {
|
||||
url = url.replaceFirst('file://', '');
|
||||
if (!permanent) return url;
|
||||
//Update in db to permanent
|
||||
String name = p.basenameWithoutExtension(url);
|
||||
await db.rawUpdate('UPDATE images SET permanent == 1 WHERE name == ?', [name]);
|
||||
}
|
||||
//Filename = md5 hash
|
||||
String hash = md5.convert(utf8.encode(url)).toString();
|
||||
List<Map> results = await db.rawQuery('SELECT * FROM images WHERE name == ?', [hash]);
|
||||
String path = getPath(hash);
|
||||
if (results.length > 0) {
|
||||
//Image in database
|
||||
return path;
|
||||
}
|
||||
//Save image
|
||||
Dio dio = Dio();
|
||||
try {
|
||||
await dio.download(url, path);
|
||||
await db.insert('images', {'url': url, 'name': hash, 'permanent': permanent?1:0});
|
||||
return path;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PaletteGenerator> getPaletteGenerator(String url) async {
|
||||
String path = await getImage(url);
|
||||
//Get image provider
|
||||
ImageProvider provider = AssetImage('assets/cover.jpg');
|
||||
if (path != null) {
|
||||
provider = FileImage(File(path));
|
||||
}
|
||||
PaletteGenerator paletteGenerator = await PaletteGenerator.fromImageProvider(provider);
|
||||
return paletteGenerator;
|
||||
}
|
||||
|
||||
//Get primary color from album art
|
||||
Future<Color> getPrimaryColor(String url) async {
|
||||
PaletteGenerator paletteGenerator = await getPaletteGenerator(url);
|
||||
return paletteGenerator.colors.first;
|
||||
}
|
||||
|
||||
//Check if is dark
|
||||
Future<bool> isDark(String url) async {
|
||||
PaletteGenerator paletteGenerator = await getPaletteGenerator(url);
|
||||
return paletteGenerator.colors.first.computeLuminance() > 0.5 ? false : true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class CachedImage extends StatefulWidget {
|
||||
|
||||
final String url;
|
||||
final double width;
|
||||
final double height;
|
||||
final bool circular;
|
||||
|
||||
const CachedImage({Key key, this.url, this.height, this.width, this.circular = false}): super(key: key);
|
||||
|
||||
@override
|
||||
_CachedImageState createState() => _CachedImageState();
|
||||
}
|
||||
|
||||
class _CachedImageState extends State<CachedImage> {
|
||||
|
||||
final ImageProvider _placeholder = AssetImage('assets/cover.jpg');
|
||||
ImageProvider _image = AssetImage('assets/cover.jpg');
|
||||
double _opacity = 0.0;
|
||||
bool _disposed = false;
|
||||
|
||||
Future<ImageProvider> _getImage() async {
|
||||
//Image already path
|
||||
if (!widget.url.startsWith('http')) {
|
||||
//Remove file://, if used in audio_service
|
||||
if (widget.url.startsWith('/')) return FileImage(File(widget.url));
|
||||
return FileImage(File(widget.url.replaceFirst('file://', '')));
|
||||
}
|
||||
//Load image from db
|
||||
String path = await imagesDatabase.getImage(widget.url);
|
||||
if (path == null) return _placeholder;
|
||||
return FileImage(File(path));
|
||||
}
|
||||
|
||||
//Load image and fade
|
||||
void _load() async {
|
||||
ImageProvider image = await _getImage();
|
||||
if (_disposed) return;
|
||||
setState(() {
|
||||
_image = image;
|
||||
_opacity = 1.0;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_load();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CachedImage oldWidget) {
|
||||
_load();
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
widget.circular ?
|
||||
CircleAvatar(
|
||||
radius: (widget.width??widget.height),
|
||||
backgroundImage: _placeholder,
|
||||
):
|
||||
Image(
|
||||
image: _placeholder,
|
||||
height: widget.height,
|
||||
width: widget.width,
|
||||
),
|
||||
AnimatedOpacity(
|
||||
duration: Duration(milliseconds: 250),
|
||||
opacity: _opacity,
|
||||
child: widget.circular ?
|
||||
CircleAvatar(
|
||||
radius: (widget.width??widget.height),
|
||||
backgroundImage: _image,
|
||||
):
|
||||
Image(
|
||||
image: _image,
|
||||
height: widget.height,
|
||||
width: widget.width,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
697
lib/ui/details_screens.dart
Normal file
697
lib/ui/details_screens.dart
Normal file
|
@ -0,0 +1,697 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/search.dart';
|
||||
|
||||
import '../api/definitions.dart';
|
||||
import 'player_bar.dart';
|
||||
import 'cached_image.dart';
|
||||
import 'tiles.dart';
|
||||
import 'menu.dart';
|
||||
|
||||
class AlbumDetails extends StatelessWidget {
|
||||
|
||||
Album album;
|
||||
|
||||
AlbumDetails(this.album);
|
||||
|
||||
Future _loadAlbum() async {
|
||||
//Get album from API, if doesn't have tracks
|
||||
if (this.album.tracks == null || this.album.tracks.length == 0) {
|
||||
this.album = await deezerAPI.album(album.id);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: FutureBuilder(
|
||||
future: _loadAlbum(),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
|
||||
//Wait for data
|
||||
if (snapshot.connectionState != ConnectionState.done) return Center(child: CircularProgressIndicator(),);
|
||||
//On error
|
||||
if (snapshot.hasError) return ErrorScreen();
|
||||
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
//Album art, title, artists
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(height: 8.0,),
|
||||
CachedImage(
|
||||
url: album.art.full,
|
||||
height: 256.0,
|
||||
),
|
||||
Container(height: 8,),
|
||||
Text(
|
||||
album.title,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
Text(
|
||||
album.artistString,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
color: Theme.of(context).primaryColor
|
||||
),
|
||||
),
|
||||
Container(height: 8.0,),
|
||||
],
|
||||
),
|
||||
),
|
||||
//Details
|
||||
Card(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.audiotrack, size: 32.0,),
|
||||
Container(width: 8.0, height: 42.0,), //Height to adjust card height
|
||||
Text(
|
||||
album.tracks.length.toString(),
|
||||
style: TextStyle(fontSize: 16.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.timelapse, size: 32.0,),
|
||||
Container(width: 8.0,),
|
||||
Text(
|
||||
album.durationString,
|
||||
style: TextStyle(fontSize: 16.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.people, size: 32.0,),
|
||||
Container(width: 8.0,),
|
||||
Text(
|
||||
album.fansString,
|
||||
style: TextStyle(fontSize: 16.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
//Options (offline, download...)
|
||||
Card(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.favorite, size: 32),
|
||||
Container(width: 4,),
|
||||
Text('Library')
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
await deezerAPI.addFavoriteAlbum(album.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added to library',
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
},
|
||||
),
|
||||
MakeAlbumOffline(album: album),
|
||||
FlatButton(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.file_download, size: 32.0,),
|
||||
Container(width: 4,),
|
||||
Text('Download')
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
downloadManager.addOfflineAlbum(album, private: false);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
...List.generate(album.tracks.length, (i) {
|
||||
Track t = album.tracks[i];
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
playerHelper.playFromAlbum(album, t.id);
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t);
|
||||
}
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MakeAlbumOffline extends StatefulWidget {
|
||||
|
||||
Album album;
|
||||
MakeAlbumOffline({Key key, this.album}): super(key: key);
|
||||
|
||||
@override
|
||||
_MakeAlbumOfflineState createState() => _MakeAlbumOfflineState();
|
||||
}
|
||||
|
||||
class _MakeAlbumOfflineState extends State<MakeAlbumOffline> {
|
||||
|
||||
bool _offline = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
downloadManager.checkOffline(album: widget.album).then((v) {
|
||||
setState(() {
|
||||
_offline = v;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Switch(
|
||||
value: _offline,
|
||||
onChanged: (v) async {
|
||||
if (v) {
|
||||
//Add to offline
|
||||
await deezerAPI.addFavoriteAlbum(widget.album.id);
|
||||
downloadManager.addOfflineAlbum(widget.album, private: true);
|
||||
setState(() {
|
||||
_offline = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
downloadManager.removeOfflineAlbum(widget.album.id);
|
||||
setState(() {
|
||||
_offline = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Container(width: 4.0,),
|
||||
Text(
|
||||
'Offline',
|
||||
style: TextStyle(fontSize: 16),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ArtistDetails extends StatelessWidget {
|
||||
|
||||
Artist artist;
|
||||
ArtistDetails(this.artist);
|
||||
|
||||
Future _loadArtist() async {
|
||||
//Load artist from api if no albums
|
||||
if ((this.artist.albums??[]).length == 0) {
|
||||
this.artist = await deezerAPI.artist(artist.id);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: FutureBuilder(
|
||||
future: _loadArtist(),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
//Error / not done
|
||||
if (snapshot.hasError) return ErrorScreen();
|
||||
if (snapshot.connectionState != ConnectionState.done) return Center(child: CircularProgressIndicator(),);
|
||||
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
Card(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
CachedImage(
|
||||
url: artist.picture.full,
|
||||
height: 200,
|
||||
),
|
||||
Container(
|
||||
width: 200.0,
|
||||
height: 220,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
artist.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 4,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Container(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.people,
|
||||
size: 32.0,
|
||||
),
|
||||
Container(
|
||||
width: 8,
|
||||
),
|
||||
Text(
|
||||
artist.fansString,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
height: 4.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(Icons.album, size: 32.0),
|
||||
Container(
|
||||
width: 8.0,
|
||||
),
|
||||
Text(
|
||||
artist.albumCount.toString(),
|
||||
style: TextStyle(fontSize: 16),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(height: 4.0,),
|
||||
Card(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.favorite, size: 32),
|
||||
Container(width: 4,),
|
||||
Text('Library')
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
await deezerAPI.addFavoriteArtist(artist.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added to library',
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(height: 16.0,),
|
||||
//Top tracks
|
||||
Text(
|
||||
'Top Tracks',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22.0
|
||||
),
|
||||
),
|
||||
Container(height: 4.0),
|
||||
...List.generate(5, (i) {
|
||||
if (artist.topTracks.length <= i) return Container(height: 0, width: 0,);
|
||||
Track t = artist.topTracks[i];
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
playerHelper.playFromTopTracks(
|
||||
artist.topTracks,
|
||||
t.id,
|
||||
artist
|
||||
);
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet mi = MenuSheet(context);
|
||||
mi.defaultTrackMenu(t);
|
||||
},
|
||||
);
|
||||
}),
|
||||
ListTile(
|
||||
title: Text('Show more tracks'),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => TrackListScreen(artist.topTracks, QueueSource(
|
||||
id: artist.id,
|
||||
text: 'Top ${artist.name}',
|
||||
source: 'topTracks'
|
||||
)))
|
||||
);
|
||||
}
|
||||
),
|
||||
Divider(),
|
||||
//Albums
|
||||
Text(
|
||||
'Top Albums',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22.0
|
||||
),
|
||||
),
|
||||
...List.generate(artist.albums.length, (i) {
|
||||
Album a = artist.albums[i];
|
||||
return AlbumTile(
|
||||
a,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => AlbumDetails(a))
|
||||
);
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultAlbumMenu(
|
||||
a
|
||||
);
|
||||
},
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PlaylistDetails extends StatefulWidget {
|
||||
|
||||
Playlist playlist;
|
||||
PlaylistDetails(this.playlist, {Key key}): super(key: key);
|
||||
|
||||
@override
|
||||
_PlaylistDetailsState createState() => _PlaylistDetailsState();
|
||||
}
|
||||
|
||||
class _PlaylistDetailsState extends State<PlaylistDetails> {
|
||||
|
||||
Playlist playlist;
|
||||
bool _loading = false;
|
||||
bool _error = false;
|
||||
ScrollController _scrollController = ScrollController();
|
||||
|
||||
//Load tracks from api
|
||||
void _load() async {
|
||||
if (playlist.tracks.length < playlist.trackCount && !_loading) {
|
||||
setState(() => _loading = true);
|
||||
int pos = playlist.tracks.length;
|
||||
//Get another page of tracks
|
||||
List<Track> tracks;
|
||||
try {
|
||||
tracks = await deezerAPI.playlistTracksPage(playlist.id, pos);
|
||||
} catch (e) {
|
||||
setState(() => _error = true);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
playlist.tracks.addAll(tracks);
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
playlist = widget.playlist;
|
||||
//If scrolled past 90% load next tracks
|
||||
_scrollController.addListener(() {
|
||||
double off = _scrollController.position.maxScrollExtent * 0.90;
|
||||
if (_scrollController.position.pixels > off) {
|
||||
_load();
|
||||
}
|
||||
});
|
||||
//Load if no tracks
|
||||
if (playlist.tracks.length == 0) {
|
||||
//Get correct metadata
|
||||
deezerAPI.playlist(playlist.id)
|
||||
.catchError((e) => setState(() => _error = true))
|
||||
.then((Playlist p) {
|
||||
if (p == null) return;
|
||||
setState(() {
|
||||
playlist = p;
|
||||
});
|
||||
//Load tracks
|
||||
_load();
|
||||
});
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ListView(
|
||||
controller: _scrollController,
|
||||
children: <Widget>[
|
||||
Container(height: 4.0,),
|
||||
Card(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
CachedImage(
|
||||
url: playlist.image.full,
|
||||
height: 180.0,
|
||||
),
|
||||
Container(
|
||||
width: 180,
|
||||
height: 200, //Card padding
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
playlist.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
Text(
|
||||
playlist.user.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 18.0
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.audiotrack,
|
||||
size: 32.0,
|
||||
),
|
||||
Container(width: 8.0,),
|
||||
Text(playlist.trackCount.toString(), style: TextStyle(fontSize: 16),)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.timelapse,
|
||||
size: 32.0,
|
||||
),
|
||||
Container(width: 8.0,),
|
||||
Text(playlist.durationString, style: TextStyle(fontSize: 16),)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(height: 4.0,),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
playlist.description ?? '',
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
Card(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.favorite, size: 32),
|
||||
Container(width: 4,),
|
||||
Text('Library')
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
await deezerAPI.addFavoriteAlbum(playlist.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added to library',
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
},
|
||||
),
|
||||
MakePlaylistOffline(playlist),
|
||||
FlatButton(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.file_download, size: 32.0,),
|
||||
Container(width: 4,),
|
||||
Text('Download')
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
downloadManager.addOfflinePlaylist(playlist, private: false);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
...List.generate(playlist.tracks.length, (i) {
|
||||
Track t = playlist.tracks[i];
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
playerHelper.playFromPlaylist(playlist, t.id);
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t, options: [
|
||||
(playlist.user.id == deezerAPI.userId) ? m.removeFromPlaylist(t, playlist) : Container(width: 0, height: 0,)
|
||||
]);
|
||||
}
|
||||
);
|
||||
}),
|
||||
if (_loading)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
),
|
||||
if (_error)
|
||||
ErrorScreen()
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MakePlaylistOffline extends StatefulWidget {
|
||||
Playlist playlist;
|
||||
MakePlaylistOffline(this.playlist, {Key key}): super(key: key);
|
||||
|
||||
@override
|
||||
_MakePlaylistOfflineState createState() => _MakePlaylistOfflineState();
|
||||
}
|
||||
|
||||
class _MakePlaylistOfflineState extends State<MakePlaylistOffline> {
|
||||
bool _offline = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
downloadManager.checkOffline(playlist: widget.playlist).then((v) {
|
||||
setState(() {
|
||||
_offline = v;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Switch(
|
||||
value: _offline,
|
||||
onChanged: (v) async {
|
||||
if (v) {
|
||||
//Add to offline
|
||||
if (widget.playlist.user != null && widget.playlist.user.id != deezerAPI.userId)
|
||||
await deezerAPI.addPlaylist(widget.playlist.id);
|
||||
downloadManager.addOfflinePlaylist(widget.playlist, private: true);
|
||||
setState(() {
|
||||
_offline = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
downloadManager.removeOfflinePlaylist(widget.playlist.id);
|
||||
setState(() {
|
||||
_offline = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Container(width: 4.0,),
|
||||
Text(
|
||||
'Offline',
|
||||
style: TextStyle(fontSize: 16),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
113
lib/ui/downloads_screen.dart
Normal file
113
lib/ui/downloads_screen.dart
Normal file
|
@ -0,0 +1,113 @@
|
|||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'cached_image.dart';
|
||||
import '../api/download.dart';
|
||||
|
||||
|
||||
class DownloadTile extends StatelessWidget {
|
||||
|
||||
final Download download;
|
||||
DownloadTile(this.download);
|
||||
|
||||
String get subtitle {
|
||||
switch (download.state) {
|
||||
case DownloadState.NONE: return '';
|
||||
case DownloadState.DOWNLOADING:
|
||||
return '${filesize(download.received)} / ${filesize(download.total)}';
|
||||
case DownloadState.POST:
|
||||
return 'Post processing...';
|
||||
case DownloadState.DONE:
|
||||
return 'Done'; //Shouldn't be visible
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
Widget get progressBar {
|
||||
switch (download.state) {
|
||||
case DownloadState.DOWNLOADING:
|
||||
return LinearProgressIndicator(value: download.received / download.total);
|
||||
case DownloadState.POST:
|
||||
return LinearProgressIndicator();
|
||||
default:
|
||||
return Container(height: 0, width: 0,);
|
||||
}
|
||||
}
|
||||
|
||||
Widget get trailing {
|
||||
if (download.private) {
|
||||
return Icon(Icons.offline_pin);
|
||||
}
|
||||
return Icon(Icons.sd_card);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(download.track.title),
|
||||
subtitle: Text(subtitle),
|
||||
leading: CachedImage(
|
||||
url: download.track.albumArt.thumb,
|
||||
),
|
||||
trailing: trailing,
|
||||
),
|
||||
progressBar
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadsScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Downloads'),
|
||||
),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
StreamBuilder(
|
||||
stream: Stream.periodic(Duration(milliseconds: 500)), //Periodic to get current download progress
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
|
||||
if (downloadManager.queue.length == 0)
|
||||
return Container(width: 0, height: 0,);
|
||||
|
||||
return Column(
|
||||
children: List.generate(downloadManager.queue.length, (i) {
|
||||
return DownloadTile(downloadManager.queue[i]);
|
||||
})
|
||||
);
|
||||
},
|
||||
),
|
||||
FutureBuilder(
|
||||
future: downloadManager.getFinishedDownloads(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data.length == 0) return Container(height: 0, width: 0,);
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Divider(),
|
||||
Text(
|
||||
'History',
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
...List.generate(snapshot.data.length, (i) {
|
||||
Download d = snapshot.data[i];
|
||||
return DownloadTile(d);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
26
lib/ui/error.dart
Normal file
26
lib/ui/error.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ErrorScreen extends StatelessWidget {
|
||||
|
||||
final String message;
|
||||
|
||||
ErrorScreen({this.message});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.error,
|
||||
color: Colors.red,
|
||||
size: 64.0,
|
||||
),
|
||||
Container(height: 4.0,),
|
||||
Text(message ?? 'Please check your connection and try again later...')
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
224
lib/ui/home_screen.dart
Normal file
224
lib/ui/home_screen.dart
Normal file
|
@ -0,0 +1,224 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/menu.dart';
|
||||
import 'tiles.dart';
|
||||
import 'details_screens.dart';
|
||||
import '../settings.dart';
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
Container(height: 16.0,),
|
||||
FreezerTitle(),
|
||||
Container(height: 16.0,),
|
||||
HomePageScreen()
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FreezerTitle extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
'freezer',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Jost',
|
||||
fontSize: 75,
|
||||
fontStyle: FontStyle.italic,
|
||||
letterSpacing: 7
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class HomePageScreen extends StatefulWidget {
|
||||
|
||||
final HomePage homePage;
|
||||
final DeezerChannel channel;
|
||||
HomePageScreen({this.homePage, this.channel, Key key}): super(key: key);
|
||||
|
||||
@override
|
||||
_HomePageScreenState createState() => _HomePageScreenState();
|
||||
}
|
||||
|
||||
class _HomePageScreenState extends State<HomePageScreen> {
|
||||
|
||||
HomePage _homePage;
|
||||
bool _cancel = false;
|
||||
bool _error = false;
|
||||
|
||||
void _loadChannel() async {
|
||||
HomePage _hp;
|
||||
//Fetch channel from api
|
||||
try {
|
||||
_hp = await deezerAPI.getChannel(widget.channel.target);
|
||||
} catch (e) {}
|
||||
if (_hp == null) {
|
||||
//On error
|
||||
setState(() => _error = true);
|
||||
return;
|
||||
}
|
||||
setState(() => _homePage = _hp);
|
||||
}
|
||||
void _loadHomePage() async {
|
||||
//Load local
|
||||
try {
|
||||
HomePage _hp = await HomePage().load();
|
||||
setState(() => _homePage = _hp);
|
||||
} catch (e) {}
|
||||
//On background load from API
|
||||
try {
|
||||
if (settings.offlineMode) return;
|
||||
HomePage _hp = await deezerAPI.homePage();
|
||||
if (_hp != null) {
|
||||
if (_cancel) return;
|
||||
if (_hp.sections.length == 0) return;
|
||||
setState(() => _homePage = _hp);
|
||||
//Save to cache
|
||||
await _homePage.save();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
void _load() {
|
||||
if (widget.channel != null) {
|
||||
_loadChannel();
|
||||
return;
|
||||
}
|
||||
if (widget.channel == null && widget.homePage == null) {
|
||||
_loadHomePage();
|
||||
return;
|
||||
}
|
||||
if (widget.homePage.sections == null || widget.homePage.sections.length == 0) {
|
||||
_loadHomePage();
|
||||
return;
|
||||
}
|
||||
//Already have data
|
||||
setState(() => _homePage = widget.homePage);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_load();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cancel = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_homePage == null)
|
||||
return Center(child: CircularProgressIndicator(),);
|
||||
if (_error)
|
||||
return ErrorScreen();
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
...List.generate(_homePage.sections.length, (i) {
|
||||
HomePageSection section = _homePage.sections[i];
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
child: Text(
|
||||
section.title,
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 24.0),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0)
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List<Widget>.generate(section.items.length, (i) {
|
||||
HomePageItem item = section.items[i];
|
||||
|
||||
switch (item.type) {
|
||||
case HomePageItemType.SMARTTRACKLIST:
|
||||
return SmartTrackListTile(
|
||||
item.value,
|
||||
onTap: () {
|
||||
playerHelper.playFromSmartTrackList(item.value);
|
||||
},
|
||||
);
|
||||
case HomePageItemType.ALBUM:
|
||||
return AlbumCard(
|
||||
item.value,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => AlbumDetails(item.value)
|
||||
));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultAlbumMenu(item.value);
|
||||
},
|
||||
);
|
||||
case HomePageItemType.ARTIST:
|
||||
return ArtistTile(
|
||||
item.value,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ArtistDetails(item.value)
|
||||
));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultArtistMenu(item.value);
|
||||
},
|
||||
);
|
||||
case HomePageItemType.PLAYLIST:
|
||||
return PlaylistCardTile(
|
||||
item.value,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => PlaylistDetails(item.value)
|
||||
));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(item.value);
|
||||
},
|
||||
);
|
||||
case HomePageItemType.CHANNEL:
|
||||
return ChannelTile(
|
||||
item.value,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: AppBar(title: Text(item.value.title.toString()),),
|
||||
body: HomePageScreen(channel: item.value,),
|
||||
)
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
return Container(height: 0, width: 0);
|
||||
}),
|
||||
),
|
||||
),
|
||||
Container(height: 16.0,)
|
||||
],
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
610
lib/ui/library.dart
Normal file
610
lib/ui/library.dart
Normal file
|
@ -0,0 +1,610 @@
|
|||
import 'package:connectivity/connectivity.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:freezer/ui/details_screens.dart';
|
||||
import 'package:freezer/ui/downloads_screen.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/tiles.dart';
|
||||
|
||||
import 'menu.dart';
|
||||
import 'settings_screen.dart';
|
||||
import 'player_bar.dart';
|
||||
import '../api/download.dart';
|
||||
|
||||
class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
@override
|
||||
Size get preferredSize => AppBar().preferredSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
title: Text('Library'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.file_download),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => DownloadsScreen())
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.settings),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => SettingsScreen())
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LibraryScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: LibraryAppBar(),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
Container(height: 4.0,),
|
||||
if (downloadManager.stopped)
|
||||
ListTile(
|
||||
title: Text('Downloads'),
|
||||
leading: Icon(Icons.file_download),
|
||||
subtitle: Text('Downloading is currently stopped, click here to resume.'),
|
||||
onTap: () {
|
||||
downloadManager.updateQueue();
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => DownloadsScreen()
|
||||
));
|
||||
},
|
||||
),
|
||||
//Dirty if to not use columns
|
||||
if (downloadManager.stopped)
|
||||
Divider(),
|
||||
|
||||
ListTile(
|
||||
title: Text('Tracks'),
|
||||
leading: Icon(Icons.audiotrack),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LibraryTracks())
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Albums'),
|
||||
leading: Icon(Icons.album),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LibraryAlbums())
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Artists'),
|
||||
leading: Icon(Icons.recent_actors),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LibraryArtists())
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Playlists'),
|
||||
leading: Icon(Icons.playlist_play),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LibraryPlaylists())
|
||||
);
|
||||
},
|
||||
),
|
||||
ExpansionTile(
|
||||
title: Text('Statistics'),
|
||||
leading: Icon(Icons.insert_chart),
|
||||
children: <Widget>[
|
||||
FutureBuilder(
|
||||
future: downloadManager.getStats(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return ErrorScreen();
|
||||
if (!snapshot.hasData) return 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'),
|
||||
leading: Icon(Icons.audiotrack),
|
||||
trailing: Text(data[0]),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Offline albums'),
|
||||
leading: Icon(Icons.album),
|
||||
trailing: Text(data[1]),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Offline playlists'),
|
||||
leading: Icon(Icons.playlist_add),
|
||||
trailing: Text(data[2]),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Offline size'),
|
||||
leading: Icon(Icons.sd_card),
|
||||
trailing: Text(data[3]),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Free space'),
|
||||
leading: Icon(Icons.disc_full),
|
||||
trailing: Text(data[4]),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LibraryTracks extends StatefulWidget {
|
||||
@override
|
||||
_LibraryTracksState createState() => _LibraryTracksState();
|
||||
}
|
||||
|
||||
class _LibraryTracksState extends State<LibraryTracks> {
|
||||
|
||||
bool _loading = false;
|
||||
ScrollController _scrollController = ScrollController();
|
||||
List<Track> tracks = [];
|
||||
List<Track> allTracks = [];
|
||||
|
||||
Playlist get _playlist => Playlist(id: deezerAPI.favoritesPlaylistId);
|
||||
|
||||
Future _load() async {
|
||||
ConnectivityResult connectivity = await Connectivity().checkConnectivity();
|
||||
if (connectivity != ConnectivityResult.none) {
|
||||
setState(() => _loading = true);
|
||||
int pos = tracks.length;
|
||||
//Load another page of tracks from deezer
|
||||
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);
|
||||
_loading = false;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Future _loadOffline() async {
|
||||
Playlist p = await downloadManager.getPlaylist(deezerAPI.favoritesPlaylistId);
|
||||
if (p != null) setState(() {
|
||||
tracks = p.tracks;
|
||||
});
|
||||
}
|
||||
|
||||
Future _loadAll() async {
|
||||
List tracks = await downloadManager.allOfflineTracks();
|
||||
setState(() {
|
||||
allTracks = tracks;
|
||||
});
|
||||
}
|
||||
|
||||
@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 tracks
|
||||
_loadAll();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Tracks'),),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(height: 8.0,),
|
||||
Text(
|
||||
'Loved tracks',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
MakePlaylistOffline(_playlist),
|
||||
FlatButton(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.file_download, size: 32.0,),
|
||||
Container(width: 4,),
|
||||
Text('Download')
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
downloadManager.addOfflinePlaylist(_playlist, private: false);
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
//Loved tracks
|
||||
...List.generate(tracks.length, (i) {
|
||||
Track t = tracks[i];
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
playerHelper.playFromTrackList(tracks, t.id, QueueSource(
|
||||
id: deezerAPI.favoritesPlaylistId,
|
||||
text: 'Favorites',
|
||||
source: 'playlist'
|
||||
));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(
|
||||
t,
|
||||
onRemove: () {
|
||||
setState(() {
|
||||
tracks.removeWhere((track) => t.id == track.id);
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
if (_loading)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
],
|
||||
),
|
||||
Divider(),
|
||||
Text(
|
||||
'All offline tracks',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
Container(height: 8,),
|
||||
...List.generate(allTracks.length, (i) {
|
||||
Track t = allTracks[i];
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
playerHelper.playFromTrackList(allTracks, t.id, QueueSource(
|
||||
id: 'allTracks',
|
||||
text: 'All offline tracks',
|
||||
source: 'offline'
|
||||
));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t);
|
||||
},
|
||||
);
|
||||
})
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LibraryAlbums extends StatefulWidget {
|
||||
@override
|
||||
_LibraryAlbumsState createState() => _LibraryAlbumsState();
|
||||
}
|
||||
|
||||
class _LibraryAlbumsState extends State<LibraryAlbums> {
|
||||
|
||||
List<Album> _albums;
|
||||
|
||||
Future _load() async {
|
||||
if (settings.offlineMode) return;
|
||||
try {
|
||||
List<Album> albums = await deezerAPI.getAlbums();
|
||||
setState(() => _albums = albums);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_load();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Albums'),),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
Container(height: 8.0,),
|
||||
if (!settings.offlineMode && _albums == null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
),
|
||||
|
||||
if (_albums != null)
|
||||
...List.generate(_albums.length, (int i) {
|
||||
Album a = _albums[i];
|
||||
return AlbumTile(
|
||||
a,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => AlbumDetails(a))
|
||||
);
|
||||
},
|
||||
onHold: () async {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultAlbumMenu(a, onRemove: () {
|
||||
setState(() => _albums.remove(a));
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
FutureBuilder(
|
||||
future: downloadManager.getOfflineAlbums(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError || !snapshot.hasData || snapshot.data.length == 0) return Container(height: 0, width: 0,);
|
||||
|
||||
List<Album> albums = snapshot.data;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Divider(),
|
||||
Text(
|
||||
'Offline albums',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24.0
|
||||
),
|
||||
),
|
||||
...List.generate(albums.length, (i) {
|
||||
Album a = albums[i];
|
||||
return AlbumTile(
|
||||
a,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => AlbumDetails(a))
|
||||
);
|
||||
},
|
||||
onHold: () async {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultAlbumMenu(a, onRemove: () {
|
||||
setState(() {
|
||||
albums.remove(a);
|
||||
_albums.remove(a);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LibraryArtists extends StatefulWidget {
|
||||
@override
|
||||
_LibraryArtistsState createState() => _LibraryArtistsState();
|
||||
}
|
||||
|
||||
class _LibraryArtistsState extends State<LibraryArtists> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Artists'),),
|
||||
body: FutureBuilder(
|
||||
future: deezerAPI.getArtists(),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
|
||||
if (snapshot.hasError) return ErrorScreen();
|
||||
if (!snapshot.hasData) return Center(child: CircularProgressIndicator(),);
|
||||
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
...List.generate(snapshot.data.length, (i) {
|
||||
Artist a = snapshot.data[i];
|
||||
return ArtistHorizontalTile(
|
||||
a,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => ArtistDetails(a))
|
||||
);
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultArtistMenu(a, onRemove: () {
|
||||
setState(() => {});
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LibraryPlaylists extends StatefulWidget {
|
||||
@override
|
||||
_LibraryPlaylistsState createState() => _LibraryPlaylistsState();
|
||||
}
|
||||
|
||||
class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
||||
|
||||
List<Playlist> _playlists;
|
||||
|
||||
Future _load() async {
|
||||
if (!settings.offlineMode) {
|
||||
try {
|
||||
List<Playlist> playlists = await deezerAPI.getPlaylists();
|
||||
setState(() => _playlists = playlists);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_load();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Playlists'),),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Create new playlist'),
|
||||
leading: Icon(Icons.playlist_add),
|
||||
onTap: () {
|
||||
if (settings.offlineMode) {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Cannot create playlists in offline mode',
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
return;
|
||||
}
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.createPlaylist();
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
|
||||
if (!settings.offlineMode && _playlists == null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
|
||||
if (_playlists != null)
|
||||
...List.generate(_playlists.length, (int i) {
|
||||
Playlist p = _playlists[i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => PlaylistDetails(p)
|
||||
)),
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p, onRemove: () {
|
||||
setState(() => _playlists.remove(p));
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
FutureBuilder(
|
||||
future: downloadManager.getOfflinePlaylists(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError || !snapshot.hasData) return Container(height: 0, width: 0,);
|
||||
if (snapshot.data.length == 0) return Container(height: 0, width: 0,);
|
||||
|
||||
List<Playlist> playlists = snapshot.data;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Divider(),
|
||||
Text(
|
||||
'Offline playlists',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
...List.generate(playlists.length, (i) {
|
||||
Playlist p = playlists[i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => PlaylistDetails(p)
|
||||
)),
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p, onRemove: () {
|
||||
setState(() {
|
||||
playlists.remove(p);
|
||||
_playlists.remove(p);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
254
lib/ui/login_screen.dart
Normal file
254
lib/ui/login_screen.dart
Normal file
|
@ -0,0 +1,254 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/main.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
|
||||
import '../settings.dart';
|
||||
import '../api/definitions.dart';
|
||||
import 'home_screen.dart';
|
||||
|
||||
class LoginWidget extends StatefulWidget {
|
||||
|
||||
Function callback;
|
||||
LoginWidget({this.callback, Key key}): super(key: key);
|
||||
|
||||
@override
|
||||
_LoginWidgetState createState() => _LoginWidgetState();
|
||||
}
|
||||
|
||||
class _LoginWidgetState extends State<LoginWidget> {
|
||||
|
||||
String _arl;
|
||||
|
||||
//Initialize deezer etc
|
||||
Future _init() async {
|
||||
deezerAPI.arl = settings.arl;
|
||||
await playerHelper.start();
|
||||
|
||||
//Pre-cache homepage
|
||||
if (!await HomePage().exists()) {
|
||||
await deezerAPI.authorize();
|
||||
settings.offlineMode = false;
|
||||
HomePage hp = await deezerAPI.homePage();
|
||||
await hp.save();
|
||||
}
|
||||
}
|
||||
//Call _init()
|
||||
void _start() async {
|
||||
if (settings.arl != null) {
|
||||
_init().then((_) {
|
||||
if (widget.callback != null) widget.callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LoginWidget oldWidget) {
|
||||
_start();
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_start();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void errorDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Error'),
|
||||
content: Text('Error logging in! Please check your token and internet connection and try again.'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: Text('Dismiss'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void _update() async {
|
||||
setState(() => {});
|
||||
|
||||
//Try logging in
|
||||
try {
|
||||
deezerAPI.arl = settings.arl;
|
||||
bool resp = await deezerAPI.authorize();
|
||||
if (resp == false) { //false, not null
|
||||
setState(() => settings.arl = null);
|
||||
errorDialog();
|
||||
}
|
||||
//On error show dialog and reset to null
|
||||
} catch (e) {
|
||||
setState(() => settings.arl = null);
|
||||
errorDialog();
|
||||
}
|
||||
|
||||
await settings.save();
|
||||
_start();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
//If arl non null, show loading
|
||||
if (settings.arl != null)
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
|
||||
if (settings.arl == null)
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
Container(height: 16.0,),
|
||||
Text(
|
||||
'Welcome to',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0
|
||||
),
|
||||
),
|
||||
FreezerTitle(),
|
||||
Container(height: 8.0,),
|
||||
Text(
|
||||
"Please login using your Deezer account.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0
|
||||
),
|
||||
),
|
||||
Container(height: 16.0,),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: OutlineButton(
|
||||
child: Text('Login using browser'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LoginBrowser(_update))
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: OutlineButton(
|
||||
child: Text('Login using token'),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Enter ARL'),
|
||||
content: Container(
|
||||
child: TextField(
|
||||
onChanged: (String s) => _arl = s,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Token (ARL)'
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: Text('Save'),
|
||||
onPressed: () {
|
||||
settings.arl = _arl;
|
||||
Navigator.of(context).pop();
|
||||
_update();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(height: 16.0,),
|
||||
Text(
|
||||
"If you don't have account, you can register on deezer.com for free.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: OutlineButton(
|
||||
child: Text('Open in browser'),
|
||||
onPressed: () {
|
||||
InAppBrowser.openWithSystemBrowser(url: 'https://deezer.com/register');
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(height: 8.0,),
|
||||
Divider(),
|
||||
Container(height: 8.0,),
|
||||
Text(
|
||||
"By using this app, you don't agree with the Deezer ToS",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LoginBrowser extends StatelessWidget {
|
||||
|
||||
Function updateParent;
|
||||
LoginBrowser(this.updateParent);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Container(
|
||||
|
||||
child: InAppWebView(
|
||||
initialUrl: 'https://deezer.com/login',
|
||||
onLoadStart: (InAppWebViewController controller, String url) async {
|
||||
//Parse arl from url
|
||||
if (url.startsWith('intent://deezer.page.link')) {
|
||||
try {
|
||||
//Parse url
|
||||
Uri uri = Uri.parse(url);
|
||||
//Actual url is in `link` query parameter
|
||||
Uri linkUri = Uri.parse(uri.queryParameters['link']);
|
||||
String arl = linkUri.queryParameters['arl'];
|
||||
if (arl != null) {
|
||||
settings.arl = arl;
|
||||
Navigator.of(context).pop();
|
||||
updateParent();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
615
lib/ui/menu.dart
Normal file
615
lib/ui/menu.dart
Normal file
|
@ -0,0 +1,615 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/ui/details_screens.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
|
||||
import '../api/definitions.dart';
|
||||
import '../api/player.dart';
|
||||
import 'cached_image.dart';
|
||||
|
||||
class MenuSheet {
|
||||
|
||||
BuildContext context;
|
||||
|
||||
MenuSheet(this.context);
|
||||
|
||||
//===================
|
||||
// DEFAULT
|
||||
//===================
|
||||
|
||||
void show(List<Widget> options) {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: (MediaQuery.of(context).orientation == Orientation.landscape)?220:350,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: options
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
//===================
|
||||
// TRACK
|
||||
//===================
|
||||
|
||||
void showWithTrack(Track track, List<Widget> options) {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(height: 16.0,),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
CachedImage(
|
||||
url: track.albumArt.full,
|
||||
height: 128,
|
||||
width: 128,
|
||||
),
|
||||
Container(
|
||||
width: 240.0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
track.title,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 22.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
Text(
|
||||
track.artistString,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0
|
||||
),
|
||||
),
|
||||
Container(height: 8.0,),
|
||||
Text(
|
||||
track.album.title,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
Text(
|
||||
track.durationString
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(height: 16.0,),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: (MediaQuery.of(context).orientation == Orientation.landscape)?220:350,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: options
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
//Default track options
|
||||
void defaultTrackMenu(Track track, {List<Widget> options = const [], Function onRemove}) {
|
||||
showWithTrack(track, [
|
||||
addToQueueNext(track),
|
||||
addToQueue(track),
|
||||
(track.favorite??false)?removeFavoriteTrack(track, onUpdate: onRemove):addTrackFavorite(track),
|
||||
addToPlaylist(track),
|
||||
downloadTrack(track),
|
||||
showAlbum(track.album),
|
||||
...List.generate(track.artists.length, (i) => showArtist(track.artists[i])),
|
||||
...options
|
||||
]);
|
||||
}
|
||||
|
||||
//===================
|
||||
// TRACK OPTIONS
|
||||
//===================
|
||||
|
||||
Widget addToQueueNext(Track t) => ListTile(
|
||||
title: Text('Play next'),
|
||||
leading: Icon(Icons.playlist_play),
|
||||
onTap: () async {
|
||||
if (playerHelper.queueIndex == -1) {
|
||||
//First track
|
||||
await AudioService.addQueueItem(t.toMediaItem());
|
||||
await AudioService.play();
|
||||
} else {
|
||||
//Normal
|
||||
await AudioService.addQueueItemAt(
|
||||
t.toMediaItem(), playerHelper.queueIndex + 1);
|
||||
}
|
||||
_close();
|
||||
});
|
||||
|
||||
Widget addToQueue(Track t) => ListTile(
|
||||
title: Text('Add to queue'),
|
||||
leading: Icon(Icons.playlist_add),
|
||||
onTap: () async {
|
||||
await AudioService.addQueueItem(t.toMediaItem());
|
||||
_close();
|
||||
}
|
||||
);
|
||||
|
||||
Widget addTrackFavorite(Track t) => ListTile(
|
||||
title: Text('Add track to favorites'),
|
||||
leading: 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);
|
||||
}
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added to library!',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT
|
||||
);
|
||||
_close();
|
||||
}
|
||||
);
|
||||
|
||||
Widget downloadTrack(Track t) => ListTile(
|
||||
title: Text('Download'),
|
||||
leading: Icon(Icons.file_download),
|
||||
onTap: () async {
|
||||
await downloadManager.addOfflineTrack(t, private: false);
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
||||
Widget addToPlaylist(Track t) => ListTile(
|
||||
title: Text('Add to playlist'),
|
||||
leading: Icon(Icons.playlist_add),
|
||||
onTap: () async {
|
||||
|
||||
Playlist p;
|
||||
|
||||
//Show dialog to pick playlist
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Select playlist'),
|
||||
content: FutureBuilder(
|
||||
future: deezerAPI.getPlaylists(),
|
||||
builder: (context, snapshot) {
|
||||
|
||||
if (snapshot.hasError) SizedBox(
|
||||
height: 100,
|
||||
child: ErrorScreen(),
|
||||
);
|
||||
if (snapshot.connectionState != ConnectionState.done) return 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: () {
|
||||
p = playlists[i];
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)),
|
||||
ListTile(
|
||||
title: Text('Create new playlist'),
|
||||
leading: Icon(Icons.add),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => CreatePlaylistDialog(tracks: [t],)
|
||||
);
|
||||
},
|
||||
)
|
||||
]
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
//Add to playlist, show toast
|
||||
if (p != null) {
|
||||
await deezerAPI.addToPlaylist(t.id, p.id);
|
||||
//Update the playlist if offline
|
||||
if (await downloadManager.checkOffline(playlist: p)) {
|
||||
downloadManager.addOfflinePlaylist(p);
|
||||
}
|
||||
Fluttertoast.showToast(
|
||||
msg: "Track added to ${p.title}",
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
||||
Widget removeFromPlaylist(Track t, Playlist p) => ListTile(
|
||||
title: Text('Remove from playlist'),
|
||||
leading: Icon(Icons.delete),
|
||||
onTap: () async {
|
||||
await deezerAPI.removeFromPlaylist(t.id, p.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Track removed from ${p.title}',
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
||||
Widget removeFavoriteTrack(Track t, {onUpdate}) => ListTile(
|
||||
title: Text('Remove favorite'),
|
||||
leading: 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);
|
||||
}
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Track removed from library',
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
onUpdate();
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
||||
//Redirect to artist page (ie from track)
|
||||
Widget showArtist(Artist a) => ListTile(
|
||||
title: Text(
|
||||
'Go to ${a.name}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
leading: Icon(Icons.recent_actors),
|
||||
onTap: () {
|
||||
_close();
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => ArtistDetails(a))
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Widget showAlbum(Album a) => ListTile(
|
||||
title: Text(
|
||||
'Go to ${a.title}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
leading: Icon(Icons.album),
|
||||
onTap: () {
|
||||
_close();
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => AlbumDetails(a))
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
//===================
|
||||
// ALBUM
|
||||
//===================
|
||||
|
||||
//Default album options
|
||||
void defaultAlbumMenu(Album album, {List<Widget> options = const [], Function onRemove}) {
|
||||
show([
|
||||
album.library?removeAlbum(album, onRemove: onRemove):libraryAlbum(album),
|
||||
downloadAlbum(album),
|
||||
offlineAlbum(album),
|
||||
...options
|
||||
]);
|
||||
}
|
||||
|
||||
//===================
|
||||
// ALBUM OPTIONS
|
||||
//===================
|
||||
|
||||
Widget downloadAlbum(Album a) => ListTile(
|
||||
title: Text('Download'),
|
||||
leading: Icon(Icons.file_download),
|
||||
onTap: () async {
|
||||
await downloadManager.addOfflineAlbum(a, private: false);
|
||||
_close();
|
||||
}
|
||||
);
|
||||
|
||||
Widget offlineAlbum(Album a) => ListTile(
|
||||
title: Text('Make offline'),
|
||||
leading: Icon(Icons.offline_pin),
|
||||
onTap: () async {
|
||||
await deezerAPI.addFavoriteAlbum(a.id);
|
||||
await downloadManager.addOfflineAlbum(a, private: true);
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
||||
Widget libraryAlbum(Album a) => ListTile(
|
||||
title: Text('Add to library'),
|
||||
leading: Icon(Icons.library_music),
|
||||
onTap: () async {
|
||||
await deezerAPI.addFavoriteAlbum(a.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added to library',
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
||||
//Remove album from favorites
|
||||
Widget removeAlbum(Album a, {Function onRemove}) => ListTile(
|
||||
title: Text('Remove album'),
|
||||
leading: Icon(Icons.delete),
|
||||
onTap: () async {
|
||||
await deezerAPI.removeAlbum(a.id);
|
||||
await downloadManager.removeOfflineAlbum(a.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Album removed',
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
if (onRemove != null) onRemove();
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
||||
//===================
|
||||
// ARTIST
|
||||
//===================
|
||||
|
||||
void defaultArtistMenu(Artist artist, {List<Widget> options = const [], Function onRemove}) {
|
||||
show([
|
||||
artist.library?removeArtist(artist, onRemove: onRemove):favoriteArtist(artist),
|
||||
...options
|
||||
]);
|
||||
}
|
||||
|
||||
//===================
|
||||
// ARTIST OPTIONS
|
||||
//===================
|
||||
|
||||
Widget removeArtist(Artist a, {Function onRemove}) => ListTile(
|
||||
title: Text('Remove from favorites'),
|
||||
leading: Icon(Icons.delete),
|
||||
onTap: () async {
|
||||
await deezerAPI.removeArtist(a.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Artist removed from library',
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
if (onRemove != null) onRemove();
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
||||
Widget favoriteArtist(Artist a) => ListTile(
|
||||
title: Text('Add to favorites'),
|
||||
leading: Icon(Icons.favorite),
|
||||
onTap: () async {
|
||||
await deezerAPI.addFavoriteArtist(a.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added to library',
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
||||
//===================
|
||||
// PLAYLIST
|
||||
//===================
|
||||
|
||||
void defaultPlaylistMenu(Playlist playlist, {List<Widget> options = const [], Function onRemove}) {
|
||||
show([
|
||||
playlist.library?removePlaylistLibrary(playlist, onRemove: onRemove):addPlaylistLibrary(playlist),
|
||||
addPlaylistOffline(playlist),
|
||||
downloadPlaylist(playlist),
|
||||
...options
|
||||
]);
|
||||
}
|
||||
|
||||
//===================
|
||||
// PLAYLIST OPTIONS
|
||||
//===================
|
||||
|
||||
Widget removePlaylistLibrary(Playlist p, {Function onRemove}) => ListTile(
|
||||
title: Text('Remove from library'),
|
||||
leading: 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();
|
||||
},
|
||||
);
|
||||
|
||||
Widget addPlaylistLibrary(Playlist p) => ListTile(
|
||||
title: Text('Add playlist to library'),
|
||||
leading: Icon(Icons.favorite),
|
||||
onTap: () async {
|
||||
await deezerAPI.addPlaylist(p.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added playlist to library',
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
||||
Widget addPlaylistOffline(Playlist p) => ListTile(
|
||||
title: Text('Make playlist offline'),
|
||||
leading: Icon(Icons.offline_pin),
|
||||
onTap: () async {
|
||||
//Add to library
|
||||
await deezerAPI.addPlaylist(p.id);
|
||||
downloadManager.addOfflinePlaylist(p, private: true);
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
||||
Widget downloadPlaylist(Playlist p) => ListTile(
|
||||
title: Text('Download playlist'),
|
||||
leading: Icon(Icons.file_download),
|
||||
onTap: () async {
|
||||
downloadManager.addOfflinePlaylist(p, private: false);
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
//===================
|
||||
// OTHER
|
||||
//===================
|
||||
|
||||
//Create playlist
|
||||
void createPlaylist() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CreatePlaylistDialog();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _close() => Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
|
||||
class CreatePlaylistDialog extends StatefulWidget {
|
||||
|
||||
final List<Track> tracks;
|
||||
CreatePlaylistDialog({this.tracks, Key key}): super(key: key);
|
||||
|
||||
@override
|
||||
_CreatePlaylistDialogState createState() => _CreatePlaylistDialogState();
|
||||
}
|
||||
|
||||
class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
|
||||
|
||||
int _playlistType = 1;
|
||||
String _title = '';
|
||||
String _description = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Create playlist'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Title'
|
||||
),
|
||||
onChanged: (String s) => _title = s,
|
||||
),
|
||||
TextField(
|
||||
onChanged: (String s) => _description = s,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Description'
|
||||
),
|
||||
),
|
||||
Container(height: 4.0,),
|
||||
DropdownButton<int>(
|
||||
value: _playlistType,
|
||||
onChanged: (int v) {
|
||||
setState(() => _playlistType = v);
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem<int>(
|
||||
value: 1,
|
||||
child: Text('Private'),
|
||||
),
|
||||
DropdownMenuItem<int>(
|
||||
value: 2,
|
||||
child: Text('Collaborative'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Create'),
|
||||
onPressed: () async {
|
||||
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
|
||||
);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Playlist created!',
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
160
lib/ui/player_bar.dart
Normal file
160
lib/ui/player_bar.dart
Normal file
|
@ -0,0 +1,160 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
|
||||
import '../api/player.dart';
|
||||
import 'cached_image.dart';
|
||||
import 'player_screen.dart';
|
||||
|
||||
class PlayerBar extends StatelessWidget {
|
||||
double get progress {
|
||||
if (AudioService.playbackState == null) return 0.0;
|
||||
if (AudioService.currentMediaItem == null) return 0.0;
|
||||
if (AudioService.currentMediaItem.duration.inSeconds == 0) return 0.0; //Division by 0
|
||||
return AudioService.playbackState.currentPosition.inSeconds / AudioService.currentMediaItem.duration.inSeconds;
|
||||
}
|
||||
|
||||
double iconSize = 32;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: Stream.periodic(Duration(milliseconds: 250)),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
if (AudioService.currentMediaItem == null) return Container(width: 0, height: 0,);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => PlayerScreen())),
|
||||
leading: CachedImage(
|
||||
width: 50,
|
||||
height: 50,
|
||||
url: AudioService.currentMediaItem.artUri,
|
||||
),
|
||||
title: Text(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
AudioService.currentMediaItem.displaySubtitle,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
PrevNextButton(iconSize, prev: true, hidePrev: true,),
|
||||
PlayPauseButton(iconSize),
|
||||
PrevNextButton(iconSize)
|
||||
],
|
||||
)
|
||||
),
|
||||
Container(
|
||||
height: 3.0,
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
value: progress,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PrevNextButton extends StatelessWidget {
|
||||
|
||||
final double size;
|
||||
final bool prev;
|
||||
final bool hidePrev;
|
||||
int i;
|
||||
PrevNextButton(this.size, {this.prev = false, this.hidePrev = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!prev) {
|
||||
if (playerHelper.queueIndex == (AudioService.queue??[]).length - 1) {
|
||||
return IconButton(
|
||||
icon: Icon(Icons.skip_next),
|
||||
iconSize: size,
|
||||
onPressed: null,
|
||||
);
|
||||
}
|
||||
return IconButton(
|
||||
icon: Icon(Icons.skip_next),
|
||||
iconSize: size,
|
||||
onPressed: () => AudioService.skipToNext(),
|
||||
);
|
||||
}
|
||||
if (prev) {
|
||||
if (i == 0) {
|
||||
if (hidePrev) {
|
||||
return Container(height: 0, width: 0,);
|
||||
}
|
||||
return IconButton(
|
||||
icon: Icon(Icons.skip_previous),
|
||||
iconSize: size,
|
||||
onPressed: null,
|
||||
);
|
||||
}
|
||||
return IconButton(
|
||||
icon: Icon(Icons.skip_previous),
|
||||
iconSize: size,
|
||||
onPressed: () => AudioService.skipToPrevious(),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class PlayPauseButton extends StatelessWidget {
|
||||
|
||||
final double size;
|
||||
PlayPauseButton(this.size);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
//Playing
|
||||
if (AudioService.playbackState?.playing??false) {
|
||||
return IconButton(
|
||||
iconSize: this.size,
|
||||
icon: Icon(Icons.pause),
|
||||
onPressed: () => AudioService.pause()
|
||||
);
|
||||
}
|
||||
|
||||
//Paused
|
||||
if ((!AudioService.playbackState.playing &&
|
||||
AudioService.playbackState.processingState == AudioProcessingState.ready) ||
|
||||
//None state (stopped)
|
||||
AudioService.playbackState.processingState == AudioProcessingState.none) {
|
||||
return IconButton(
|
||||
iconSize: this.size,
|
||||
icon: Icon(Icons.play_arrow),
|
||||
onPressed: () => AudioService.play()
|
||||
);
|
||||
}
|
||||
|
||||
switch (AudioService.playbackState.processingState) {
|
||||
//Stopped/Error
|
||||
case AudioProcessingState.error:
|
||||
case AudioProcessingState.none:
|
||||
case AudioProcessingState.stopped:
|
||||
return Container(width: this.size, height: this.size);
|
||||
//Loading, connecting, rewinding...
|
||||
default:
|
||||
return Container(
|
||||
width: this.size,
|
||||
height: this.size,
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
581
lib/ui/player_screen.dart
Normal file
581
lib/ui/player_screen.dart
Normal file
|
@ -0,0 +1,581 @@
|
|||
import 'dart:ui';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/ui/menu.dart';
|
||||
import 'package:freezer/ui/tiles.dart';
|
||||
|
||||
import 'cached_image.dart';
|
||||
import '../api/definitions.dart';
|
||||
import 'player_bar.dart';
|
||||
|
||||
|
||||
|
||||
class PlayerScreen extends StatefulWidget {
|
||||
@override
|
||||
_PlayerScreenState createState() => _PlayerScreenState();
|
||||
}
|
||||
|
||||
class _PlayerScreenState extends State<PlayerScreen> {
|
||||
|
||||
double iconSize = 48;
|
||||
bool _lyrics = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: StreamBuilder(
|
||||
stream: AudioService.playbackStateStream,
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
|
||||
//Disable lyrics when skipping songs, loading
|
||||
PlaybackState s = snapshot.data;
|
||||
if (s != null && s.processingState != AudioProcessingState.ready && s.processingState != AudioProcessingState.buffering) _lyrics = false;
|
||||
|
||||
return OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
//Landscape
|
||||
if (orientation == Orientation.landscape) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 32, 16, 8),
|
||||
child: Container(
|
||||
width: 320,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
CachedImage(
|
||||
url: AudioService.currentMediaItem.artUri,
|
||||
),
|
||||
if (_lyrics) LyricsWidget(
|
||||
artUri: AudioService.currentMediaItem.artUri,
|
||||
trackId: AudioService.currentMediaItem.id,
|
||||
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
||||
height: 320.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 2 - 32,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(8, 42, 8, 0),
|
||||
child: Container(
|
||||
width: 300,
|
||||
child: PlayerScreenTopRow(),
|
||||
)
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
Container(height: 4,),
|
||||
Text(
|
||||
AudioService.currentMediaItem.displaySubtitle,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
width: 320,
|
||||
child: SeekBar(),
|
||||
),
|
||||
Container(
|
||||
width: 320,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
PrevNextButton(iconSize, prev: true,),
|
||||
PlayPauseButton(iconSize),
|
||||
PrevNextButton(iconSize)
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(8, 0, 8, 16),
|
||||
child: Container(
|
||||
width: 300,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.subtitles),
|
||||
onPressed: () {
|
||||
setState(() => _lyrics = !_lyrics);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
AudioService.currentMediaItem.extras['qualityString']
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//Portrait
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(28, 28, 28, 0),
|
||||
child: PlayerScreenTopRow()
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Container(
|
||||
height: 360,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
CachedImage(
|
||||
url: AudioService.currentMediaItem.artUri,
|
||||
),
|
||||
if (_lyrics) LyricsWidget(
|
||||
artUri: AudioService.currentMediaItem.artUri,
|
||||
trackId: AudioService.currentMediaItem.id,
|
||||
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
||||
height: 360.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
Container(height: 4,),
|
||||
Text(
|
||||
AudioService.currentMediaItem.displaySubtitle,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SeekBar(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
PrevNextButton(iconSize, prev: true,),
|
||||
PlayPauseButton(iconSize),
|
||||
PrevNextButton(iconSize)
|
||||
],
|
||||
),
|
||||
//Container(height: 8.0,),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.subtitles),
|
||||
onPressed: () {
|
||||
setState(() => _lyrics = !_lyrics);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
AudioService.currentMediaItem.extras['qualityString']
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LyricsWidget extends StatefulWidget {
|
||||
|
||||
final Lyrics lyrics;
|
||||
final String trackId;
|
||||
final String artUri;
|
||||
final double height;
|
||||
LyricsWidget({this.artUri, this.lyrics, this.trackId, this.height, Key key}): super(key: key);
|
||||
|
||||
@override
|
||||
_LyricsWidgetState createState() => _LyricsWidgetState();
|
||||
}
|
||||
|
||||
class _LyricsWidgetState extends State<LyricsWidget> {
|
||||
|
||||
bool _loading = true;
|
||||
Lyrics _l;
|
||||
Color _textColor = Colors.black;
|
||||
ScrollController _scrollController = ScrollController();
|
||||
Timer _timer;
|
||||
int _currentIndex;
|
||||
double _boxHeight;
|
||||
|
||||
Future _load() async {
|
||||
//Get text color by album art (black or white)
|
||||
if (widget.artUri != null) {
|
||||
bool bw = await imagesDatabase.isDark(widget.artUri);
|
||||
if (bw != null) setState(() => _textColor = bw?Colors.white:Colors.black);
|
||||
}
|
||||
|
||||
if (widget.lyrics.lyrics == null || widget.lyrics.lyrics.length == 0) {
|
||||
//Load from api
|
||||
try {
|
||||
_l = await deezerAPI.lyrics(widget.trackId);
|
||||
setState(() => _loading = false);
|
||||
} catch (e) {
|
||||
//Error Lyrics
|
||||
setState(() => _l = Lyrics().error);
|
||||
}
|
||||
} else {
|
||||
//Use provided lyrics
|
||||
_l = widget.lyrics;
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
this._boxHeight = widget.height??400.0;
|
||||
_load();
|
||||
Timer.periodic(Duration(milliseconds: 500), (timer) {
|
||||
_timer = timer;
|
||||
if (_loading) return;
|
||||
//Update index of current lyric
|
||||
setState(() {
|
||||
_currentIndex = _l.lyrics.lastIndexWhere((l) => l.offset <= AudioService.playbackState.currentPosition);
|
||||
});
|
||||
//Scroll to current lyric
|
||||
if (_currentIndex <= 0) return;
|
||||
_scrollController.animateTo(
|
||||
(_boxHeight * _currentIndex),
|
||||
duration: Duration(milliseconds: 250),
|
||||
curve: Curves.ease
|
||||
);
|
||||
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_timer != null) _timer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: _boxHeight,
|
||||
width: _boxHeight,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 7.0,
|
||||
sigmaY: 7.0
|
||||
),
|
||||
child: Container(
|
||||
child: _loading?
|
||||
Center(child: CircularProgressIndicator(),) :
|
||||
SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
child: Column(
|
||||
children: List.generate(_l.lyrics.length, (i) {
|
||||
return Container(
|
||||
height: _boxHeight,
|
||||
child: Center(
|
||||
child: Text(
|
||||
_l.lyrics[i].text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: _textColor,
|
||||
fontSize: 40.0,
|
||||
fontWeight: (_currentIndex == i)?FontWeight.bold:FontWeight.normal
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//Top row containing QueueSource, queue...
|
||||
class PlayerScreenTopRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Playing from: ' + playerHelper.queueSource.text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(fontSize: 16.0),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
RepeatButton(),
|
||||
Container(width: 16.0,),
|
||||
InkWell(
|
||||
child: Icon(Icons.menu),
|
||||
onTap: (){
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => QueueScreen()
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class RepeatButton extends StatefulWidget {
|
||||
@override
|
||||
_RepeatButtonState createState() => _RepeatButtonState();
|
||||
}
|
||||
|
||||
class _RepeatButtonState extends State<RepeatButton> {
|
||||
|
||||
Icon get icon {
|
||||
switch (playerHelper.repeatType) {
|
||||
case RepeatType.NONE:
|
||||
return Icon(Icons.repeat);
|
||||
case RepeatType.LIST:
|
||||
return Icon(
|
||||
Icons.repeat,
|
||||
color: Theme.of(context).primaryColor,
|
||||
);
|
||||
case RepeatType.TRACK:
|
||||
return Icon(
|
||||
Icons.repeat_one,
|
||||
color: Theme.of(context).primaryColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
await playerHelper.changeRepeat();
|
||||
setState(() {});
|
||||
},
|
||||
child: icon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class SeekBar extends StatefulWidget {
|
||||
@override
|
||||
_SeekBarState createState() => _SeekBarState();
|
||||
}
|
||||
|
||||
class _SeekBarState extends State<SeekBar> {
|
||||
|
||||
bool _seeking = false;
|
||||
double _pos;
|
||||
|
||||
double get position {
|
||||
if (_seeking) return _pos;
|
||||
if (AudioService.playbackState == null) return 0.0;
|
||||
double p = AudioService.playbackState.currentPosition.inMilliseconds.toDouble()??0.0;
|
||||
if (p > duration) return duration;
|
||||
return p;
|
||||
}
|
||||
|
||||
//Duration to mm:ss
|
||||
String _timeString(double pos) {
|
||||
Duration d = Duration(milliseconds: pos.toInt());
|
||||
return "${d.inMinutes}:${d.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
}
|
||||
|
||||
double get duration {
|
||||
if (AudioService.currentMediaItem == null) return 1.0;
|
||||
return AudioService.currentMediaItem.duration.inMilliseconds.toDouble();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: Stream.periodic(Duration(milliseconds: 250)),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 0.0, horizontal: 24.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
_timeString(position),
|
||||
style: TextStyle(
|
||||
fontSize: 14.0
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_timeString(duration),
|
||||
style: TextStyle(
|
||||
fontSize: 14.0
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 32.0,
|
||||
child: Slider(
|
||||
value: position,
|
||||
max: duration,
|
||||
onChangeStart: (double d) {
|
||||
setState(() {
|
||||
_seeking = true;
|
||||
_pos = d;
|
||||
});
|
||||
},
|
||||
onChanged: (double d) {
|
||||
setState(() {
|
||||
_pos = d;
|
||||
});
|
||||
},
|
||||
onChangeEnd: (double d) async {
|
||||
await AudioService.seekTo(Duration(milliseconds: d.round()));
|
||||
setState(() {
|
||||
_pos = d;
|
||||
_seeking = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class QueueScreen extends StatefulWidget {
|
||||
@override
|
||||
_QueueScreenState createState() => _QueueScreenState();
|
||||
}
|
||||
|
||||
class _QueueScreenState extends State<QueueScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Queue'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.shuffle),
|
||||
onPressed: () async {
|
||||
await AudioService.customAction('shuffleQueue');
|
||||
setState(() => {});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemCount: AudioService.queue.length,
|
||||
itemBuilder: (context, i) {
|
||||
Track t = Track.fromMediaItem(AudioService.queue[i]);
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () async {
|
||||
await AudioService.playFromMediaId(t.id);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
387
lib/ui/search.dart
Normal file
387
lib/ui/search.dart
Normal file
|
@ -0,0 +1,387 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/ui/details_screens.dart';
|
||||
import 'package:freezer/ui/menu.dart';
|
||||
|
||||
import 'tiles.dart';
|
||||
import '../api/deezer.dart';
|
||||
import '../api/definitions.dart';
|
||||
import '../settings.dart';
|
||||
import 'error.dart';
|
||||
|
||||
class SearchScreen extends StatefulWidget {
|
||||
@override
|
||||
_SearchScreenState createState() => _SearchScreenState();
|
||||
}
|
||||
|
||||
class _SearchScreenState extends State<SearchScreen> {
|
||||
|
||||
String _query;
|
||||
bool _offline = settings.offlineMode;
|
||||
|
||||
void _submit(BuildContext context, {String query}) {
|
||||
if (query != null) _query = query;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => SearchResultsScreen(_query, offline: _offline,))
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Search'),),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
Container(height: 16.0),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (String s) => _query = s,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Search'
|
||||
),
|
||||
onSubmitted: (String s) => _submit(context, query: s),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.search),
|
||||
onPressed: () => _submit(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Offline search'),
|
||||
leading: Switch(
|
||||
value: _offline,
|
||||
onChanged: (v) {
|
||||
if (settings.offlineMode) {
|
||||
setState(() => _offline = true);
|
||||
} else {
|
||||
setState(() => _offline = v);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class SearchResultsScreen extends StatelessWidget {
|
||||
|
||||
final String query;
|
||||
final bool offline;
|
||||
|
||||
SearchResultsScreen(this.query, {this.offline});
|
||||
|
||||
Future _search() async {
|
||||
if (offline??false) {
|
||||
return await downloadManager.search(query);
|
||||
}
|
||||
return await deezerAPI.search(query);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Search Results'),
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: _search(),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
|
||||
if (!snapshot.hasData) return Center(child: CircularProgressIndicator(),);
|
||||
if (snapshot.hasError) return ErrorScreen();
|
||||
|
||||
SearchResults results = snapshot.data;
|
||||
|
||||
if (results.empty)
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.warning,
|
||||
size: 64,
|
||||
),
|
||||
Text('No results!')
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
//Tracks
|
||||
List<Widget> tracks = [];
|
||||
if (results.tracks != null && results.tracks.length != 0) {
|
||||
tracks = [
|
||||
Text(
|
||||
'Tracks',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0
|
||||
),
|
||||
),
|
||||
...List.generate(3, (i) {
|
||||
if (results.tracks.length <= i) return Container(width: 0, height: 0,);
|
||||
Track t = results.tracks[i];
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
playerHelper.playFromTrackList(results.tracks, t.id, QueueSource(
|
||||
text: 'Search',
|
||||
id: query,
|
||||
source: 'search'
|
||||
));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t);
|
||||
},
|
||||
);
|
||||
}),
|
||||
ListTile(
|
||||
title: Text('Show all tracks'),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => TrackListScreen(results.tracks, QueueSource(
|
||||
id: query,
|
||||
source: 'search',
|
||||
text: 'Search'
|
||||
)))
|
||||
);
|
||||
},
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
//Albums
|
||||
List<Widget> albums = [];
|
||||
if (results.albums != null && results.albums.length != 0) {
|
||||
albums = [
|
||||
Text(
|
||||
'Albums',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0
|
||||
),
|
||||
),
|
||||
...List.generate(3, (i) {
|
||||
if (results.albums.length <= i) return Container(height: 0, width: 0,);
|
||||
Album a = results.albums[i];
|
||||
return AlbumTile(
|
||||
a,
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultAlbumMenu(a);
|
||||
},
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => AlbumDetails(a))
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
ListTile(
|
||||
title: Text('Show all albums'),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => AlbumListScreen(results.albums))
|
||||
);
|
||||
},
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
//Artists
|
||||
List<Widget> artists = [];
|
||||
if (results.artists != null && results.artists.length != 0) {
|
||||
artists = [
|
||||
Text(
|
||||
'Artists',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0
|
||||
),
|
||||
),
|
||||
Container(height: 4),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(results.artists.length, (int i) {
|
||||
Artist a = results.artists[i];
|
||||
return ArtistTile(
|
||||
a,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => ArtistDetails(a))
|
||||
);
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultArtistMenu(a);
|
||||
},
|
||||
);
|
||||
}),
|
||||
)
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
//Playlists
|
||||
List<Widget> playlists = [];
|
||||
if (results.playlists != null && results.playlists.length != 0) {
|
||||
playlists = [
|
||||
Text(
|
||||
'Playlists',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0
|
||||
),
|
||||
),
|
||||
...List.generate(3, (i) {
|
||||
if (results.playlists.length <= i) return Container(height: 0, width: 0,);
|
||||
Playlist p = results.playlists[i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => PlaylistDetails(p))
|
||||
);
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p);
|
||||
},
|
||||
);
|
||||
}),
|
||||
ListTile(
|
||||
title: Text('Show all playlists'),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => SearchResultPlaylists(results.playlists))
|
||||
);
|
||||
},
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
Container(height: 8.0,),
|
||||
...tracks,
|
||||
Container(height: 8.0,),
|
||||
...albums,
|
||||
Container(height: 8.0,),
|
||||
...artists,
|
||||
Container(height: 8.0,),
|
||||
...playlists
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//List all tracks
|
||||
class TrackListScreen extends StatelessWidget {
|
||||
|
||||
final QueueSource queueSource;
|
||||
final List<Track> tracks;
|
||||
|
||||
TrackListScreen(this.tracks, this.queueSource);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Tracks'),),
|
||||
body: ListView.builder(
|
||||
itemCount: tracks.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
Track t = tracks[i];
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
playerHelper.playFromTrackList(tracks, t.id, queueSource);
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//List all albums
|
||||
class AlbumListScreen extends StatelessWidget {
|
||||
|
||||
final List<Album> albums;
|
||||
AlbumListScreen(this.albums);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Albums'),),
|
||||
body: ListView.builder(
|
||||
itemCount: albums.length,
|
||||
itemBuilder: (context, i) {
|
||||
Album a = albums[i];
|
||||
return AlbumTile(
|
||||
a,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => AlbumDetails(a))
|
||||
);
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultAlbumMenu(a);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SearchResultPlaylists extends StatelessWidget {
|
||||
|
||||
final List<Playlist> playlists;
|
||||
SearchResultPlaylists(this.playlists);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Playlists'),),
|
||||
body: ListView.builder(
|
||||
itemCount: playlists.length,
|
||||
itemBuilder: (context, i) {
|
||||
Playlist p = playlists[i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => PlaylistDetails(p))
|
||||
);
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
655
lib/ui/settings_screen.dart
Normal file
655
lib/ui/settings_screen.dart
Normal file
|
@ -0,0 +1,655 @@
|
|||
import 'package:country_pickers/country.dart';
|
||||
import 'package:country_pickers/country_picker_dialog.dart';
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/player_bar.dart';
|
||||
import 'package:language_pickers/language_pickers.dart';
|
||||
import 'package:language_pickers/languages.dart';
|
||||
import 'package:package_info/package_info.dart';
|
||||
import 'package:path_provider_ex/path_provider_ex.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import '../settings.dart';
|
||||
import '../main.dart';
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
@override
|
||||
_SettingsScreenState createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
String _about = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//Load about text
|
||||
PackageInfo.fromPlatform().then((PackageInfo info) {
|
||||
setState(() {
|
||||
_about = '${info.appName} ${info.version}';
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Settings'),),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('General'),
|
||||
leading: Icon(Icons.settings),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => GeneralSettings()
|
||||
)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Appearance'),
|
||||
leading: Icon(Icons.color_lens),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => AppearanceSettings())
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Quality'),
|
||||
leading: Icon(Icons.high_quality),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => QualitySettings())
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Deezer'),
|
||||
leading: Icon(Icons.equalizer),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => DeezerSettings()
|
||||
)),
|
||||
),
|
||||
Divider(),
|
||||
Text(
|
||||
_about,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppearanceSettings extends StatefulWidget {
|
||||
@override
|
||||
_AppearanceSettingsState createState() => _AppearanceSettingsState();
|
||||
}
|
||||
|
||||
class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Appearance'),),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Theme'),
|
||||
subtitle: Text('Currently: ${settings.theme.toString().split('.').last}'),
|
||||
leading: Icon(Icons.color_lens),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SimpleDialog(
|
||||
title: Text('Select theme'),
|
||||
children: <Widget>[
|
||||
SimpleDialogOption(
|
||||
child: Text('Light (default)'),
|
||||
onPressed: () {
|
||||
setState(() => settings.theme = Themes.Light);
|
||||
settings.save();
|
||||
updateTheme();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
SimpleDialogOption(
|
||||
child: Text('Dark'),
|
||||
onPressed: () {
|
||||
setState(() => settings.theme = Themes.Dark);
|
||||
settings.save();
|
||||
updateTheme();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
SimpleDialogOption(
|
||||
child: Text('Black (AMOLED)'),
|
||||
onPressed: () {
|
||||
setState(() => settings.theme = Themes.Black);
|
||||
settings.save();
|
||||
updateTheme();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Primary color'),
|
||||
leading: Icon(Icons.format_paint),
|
||||
subtitle: Text(
|
||||
'Selected color',
|
||||
style: TextStyle(
|
||||
color: settings.primaryColor
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Primary color'),
|
||||
content: Container(
|
||||
height: 200,
|
||||
child: MaterialColorPicker(
|
||||
allowShades: false,
|
||||
selectedColor: settings.primaryColor,
|
||||
onMainColorChange: (ColorSwatch color) {
|
||||
setState(() {
|
||||
settings.primaryColor = color;
|
||||
});
|
||||
settings.save();
|
||||
updateTheme();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Use album art primary color'),
|
||||
subtitle: Text('Warning: might be buggy'),
|
||||
leading: Switch(
|
||||
value: settings.useArtColor,
|
||||
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class QualitySettings extends StatefulWidget {
|
||||
@override
|
||||
_QualitySettingsState createState() => _QualitySettingsState();
|
||||
}
|
||||
|
||||
class _QualitySettingsState extends State<QualitySettings> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Quality'),),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Mobile streaming'),
|
||||
leading: Icon(Icons.network_cell),
|
||||
),
|
||||
QualityPicker('mobile'),
|
||||
Divider(),
|
||||
ListTile(
|
||||
title: Text('Wifi streaming'),
|
||||
leading: Icon(Icons.network_wifi),
|
||||
),
|
||||
QualityPicker('wifi'),
|
||||
Divider(),
|
||||
ListTile(
|
||||
title: Text('Offline'),
|
||||
leading: Icon(Icons.offline_pin),
|
||||
),
|
||||
QualityPicker('offline'),
|
||||
Divider(),
|
||||
ListTile(
|
||||
title: Text('External downloads'),
|
||||
leading: Icon(Icons.file_download),
|
||||
),
|
||||
QualityPicker('download'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class QualityPicker extends StatefulWidget {
|
||||
|
||||
final String field;
|
||||
QualityPicker(this.field, {Key key}): super(key: key);
|
||||
|
||||
@override
|
||||
_QualityPickerState createState() => _QualityPickerState();
|
||||
}
|
||||
|
||||
class _QualityPickerState extends State<QualityPicker> {
|
||||
|
||||
AudioQuality _quality;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_getQuality();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
//Get current quality
|
||||
void _getQuality() {
|
||||
switch (widget.field) {
|
||||
case 'mobile':
|
||||
_quality = settings.mobileQuality; break;
|
||||
case 'wifi':
|
||||
_quality = settings.wifiQuality; break;
|
||||
case 'download':
|
||||
_quality = settings.downloadQuality; break;
|
||||
case 'offline':
|
||||
_quality = settings.offlineQuality; break;
|
||||
}
|
||||
}
|
||||
|
||||
//Update quality in settings
|
||||
void _updateQuality(AudioQuality q) {
|
||||
setState(() {
|
||||
_quality = q;
|
||||
});
|
||||
switch (widget.field) {
|
||||
case 'mobile':
|
||||
settings.mobileQuality = _quality; break;
|
||||
case 'wifi':
|
||||
settings.wifiQuality = _quality; break;
|
||||
case 'download':
|
||||
settings.downloadQuality = _quality; break;
|
||||
case 'offline':
|
||||
settings.offlineQuality = _quality; break;
|
||||
}
|
||||
settings.updateAudioServiceQuality();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
//Save
|
||||
settings.updateAudioServiceQuality();
|
||||
settings.save();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('MP3 128kbps'),
|
||||
leading: Radio(
|
||||
groupValue: _quality,
|
||||
value: AudioQuality.MP3_128,
|
||||
onChanged: (q) => _updateQuality(q),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('MP3 320kbps'),
|
||||
leading: Radio(
|
||||
groupValue: _quality,
|
||||
value: AudioQuality.MP3_320,
|
||||
onChanged: (q) => _updateQuality(q),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('FLAC'),
|
||||
leading: Radio(
|
||||
groupValue: _quality,
|
||||
value: AudioQuality.FLAC,
|
||||
onChanged: (q) => _updateQuality(q),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DeezerSettings extends StatefulWidget {
|
||||
@override
|
||||
_DeezerSettingsState createState() => _DeezerSettingsState();
|
||||
}
|
||||
|
||||
class _DeezerSettingsState extends State<DeezerSettings> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Deezer'),),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Content language'),
|
||||
subtitle: Text('Not app language, used in headers. Now: ${settings.deezerLanguage}'),
|
||||
leading: Icon(Icons.language),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => LanguagePickerDialog(
|
||||
titlePadding: EdgeInsets.all(8.0),
|
||||
isSearchable: true,
|
||||
title: Text('Select language'),
|
||||
onValuePicked: (Language language) {
|
||||
setState(() => settings.deezerLanguage = language.isoCode);
|
||||
settings.save();
|
||||
},
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Content country'),
|
||||
subtitle: Text('Country used in headers. Now: ${settings.deezerCountry}'),
|
||||
leading: Icon(Icons.vpn_lock),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => CountryPickerDialog(
|
||||
titlePadding: EdgeInsets.all(8.0),
|
||||
isSearchable: true,
|
||||
onValuePicked: (Country country) {
|
||||
setState(() => settings.deezerCountry = country.isoCode);
|
||||
settings.save();
|
||||
},
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Log tracks'),
|
||||
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'),
|
||||
leading: Checkbox(
|
||||
value: settings.logListen,
|
||||
onChanged: (bool v) {
|
||||
setState(() => settings.logListen = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeneralSettings extends StatefulWidget {
|
||||
@override
|
||||
_GeneralSettingsState createState() => _GeneralSettingsState();
|
||||
}
|
||||
|
||||
class _GeneralSettingsState extends State<GeneralSettings> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('General'),),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Offline mode'),
|
||||
subtitle: Text('Will be overwritten on start.'),
|
||||
leading: Switch(
|
||||
value: settings.offlineMode,
|
||||
onChanged: (bool v) {
|
||||
if (v) {
|
||||
setState(() => settings.offlineMode = true);
|
||||
return;
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
deezerAPI.authorize().then((v) {
|
||||
if (v) {
|
||||
setState(() => settings.offlineMode = false);
|
||||
} else {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Error logging in, check your internet connections.',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT
|
||||
);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
return AlertDialog(
|
||||
title: Text('Logging in...'),
|
||||
content: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Download path'),
|
||||
leading: Icon(Icons.folder),
|
||||
subtitle: Text(settings.downloadPath),
|
||||
onTap: () async {
|
||||
//Check permissions
|
||||
if (!(await Permission.storage.request().isGranted)) return;
|
||||
//Navigate
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => DirectoryPicker(settings.downloadPath, onSelect: (String p) {
|
||||
setState(() => settings.downloadPath = p);
|
||||
},)
|
||||
));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Downloads naming'),
|
||||
leading: Icon(Icons.text_format),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SimpleDialog(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Default naming'),
|
||||
subtitle: Text('01. Title'),
|
||||
onTap: () {
|
||||
settings.downloadNaming = DownloadNaming.DEFAULT;
|
||||
Navigator.of(context).pop();
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Standalone naming'),
|
||||
subtitle: Text('Artist - Title'),
|
||||
onTap: () {
|
||||
settings.downloadNaming = DownloadNaming.STANDALONE;
|
||||
Navigator.of(context).pop();
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create download folder structure'),
|
||||
subtitle: Text('Artist/Album/Track'),
|
||||
leading: Switch(
|
||||
value: settings.downloadFolderStructure,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.downloadFolderStructure = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DirectoryPicker extends StatefulWidget {
|
||||
|
||||
final String initialPath;
|
||||
final Function onSelect;
|
||||
DirectoryPicker(this.initialPath, {this.onSelect, Key key}): super(key: key);
|
||||
|
||||
@override
|
||||
_DirectoryPickerState createState() => _DirectoryPickerState();
|
||||
}
|
||||
|
||||
class _DirectoryPickerState extends State<DirectoryPicker> {
|
||||
|
||||
String _path;
|
||||
String _previous;
|
||||
String _root;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_path = widget.initialPath;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Pick-a-Path'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.sd_card),
|
||||
onPressed: () {
|
||||
String path = '';
|
||||
//Chose storage
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Select storage'),
|
||||
content: FutureBuilder(
|
||||
future: PathProviderEx.getStorageInfo(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return ErrorScreen();
|
||||
if (!snapshot.hasData) return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
),
|
||||
);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
...List.generate(snapshot.data.length, (i) {
|
||||
StorageInfo si = snapshot.data[i];
|
||||
return ListTile(
|
||||
title: Text(si.rootDir),
|
||||
leading: Icon(Icons.sd_card),
|
||||
trailing: Text(filesize(si.availableBytes)),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_path = si.appFilesDir;
|
||||
//Android 5+ blocks sd card, so this prevents going outside
|
||||
//app data dir, until permission request fix.
|
||||
_root = si.rootDir;
|
||||
if (i != 0) _root = si.appFilesDir;
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: Icon(Icons.done),
|
||||
onPressed: () {
|
||||
//When folder confirmed
|
||||
if (widget.onSelect != null) widget.onSelect(_path);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: Directory(_path).list().toList(),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
|
||||
//On error go to last good path
|
||||
if (snapshot.hasError) Future.delayed(Duration(milliseconds: 50), () => setState(() => _path = _previous));
|
||||
if (!snapshot.hasData) return Center(child: CircularProgressIndicator(),);
|
||||
|
||||
List<FileSystemEntity> data = snapshot.data;
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(_path),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Go up'),
|
||||
leading: Icon(Icons.arrow_upward),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (_root == _path) {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Permission denied',
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
return;
|
||||
}
|
||||
_previous = _path;
|
||||
_path = Directory(_path).parent.path;
|
||||
});
|
||||
},
|
||||
),
|
||||
...List.generate(data.length, (i) {
|
||||
FileSystemEntity f = data[i];
|
||||
if (f is Directory) {
|
||||
return ListTile(
|
||||
title: Text(f.path.split('/').last),
|
||||
leading: Icon(Icons.folder),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_previous = _path;
|
||||
_path = f.path;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
return Container(height: 0, width: 0,);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
363
lib/ui/tiles.dart
Normal file
363
lib/ui/tiles.dart
Normal file
|
@ -0,0 +1,363 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../api/definitions.dart';
|
||||
import 'cached_image.dart';
|
||||
|
||||
class TrackTile extends StatefulWidget {
|
||||
|
||||
final Track track;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Widget trailing;
|
||||
|
||||
TrackTile(this.track, {this.onTap, this.onHold, this.trailing, Key key}): super(key: key);
|
||||
|
||||
@override
|
||||
_TrackTileState createState() => _TrackTileState();
|
||||
}
|
||||
|
||||
class _TrackTileState extends State<TrackTile> {
|
||||
|
||||
StreamSubscription _subscription;
|
||||
|
||||
bool get nowPlaying {
|
||||
if (AudioService.currentMediaItem == null) return false;
|
||||
return AudioService.currentMediaItem.id == widget.track.id;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//Listen to media item changes, update text color if currently playing
|
||||
_subscription = AudioService.currentMediaItemStream.listen((event) {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_subscription != null) _subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
widget.track.title,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: nowPlaying?Theme.of(context).primaryColor:null
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
widget.track.artistString,
|
||||
maxLines: 1,
|
||||
),
|
||||
leading: CachedImage(
|
||||
url: widget.track.albumArt.thumb,
|
||||
),
|
||||
onTap: widget.onTap,
|
||||
onLongPress: widget.onHold,
|
||||
trailing: widget.trailing,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumTile extends StatelessWidget {
|
||||
|
||||
final Album album;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Widget trailing;
|
||||
|
||||
AlbumTile(this.album, {this.onTap, this.onHold, this.trailing});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
album.title,
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
album.artistString,
|
||||
maxLines: 1,
|
||||
),
|
||||
leading: CachedImage(
|
||||
url: album.art.thumb,
|
||||
),
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistTile extends StatelessWidget {
|
||||
|
||||
final Artist artist;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
|
||||
ArtistTile(this.artist, {this.onTap, this.onHold});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 150,
|
||||
child: Card(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(height: 4,),
|
||||
CachedImage(
|
||||
url: artist.picture.thumb,
|
||||
circular: true,
|
||||
width: 64,
|
||||
),
|
||||
Container(height: 4,),
|
||||
Text(
|
||||
artist.name,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0
|
||||
),
|
||||
),
|
||||
Container(height: 4,),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistTile extends StatelessWidget {
|
||||
|
||||
final Playlist playlist;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Widget trailing;
|
||||
|
||||
PlaylistTile(this.playlist, {this.onHold, this.onTap, this.trailing});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
playlist.title,
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
playlist.user.name,
|
||||
maxLines: 1,
|
||||
),
|
||||
leading: CachedImage(
|
||||
url: playlist.image.thumb,
|
||||
),
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistHorizontalTile extends StatelessWidget {
|
||||
|
||||
final Artist artist;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Widget trailing;
|
||||
|
||||
ArtistHorizontalTile(this.artist, {this.onHold, this.onTap, this.trailing});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
artist.name,
|
||||
maxLines: 1,
|
||||
),
|
||||
leading: CachedImage(
|
||||
url: artist.picture.thumb,
|
||||
circular: true,
|
||||
),
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistCardTile extends StatelessWidget {
|
||||
|
||||
final Playlist playlist;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
PlaylistCardTile(this.playlist, {this.onTap, this.onHold});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CachedImage(
|
||||
url: playlist.image.thumb,
|
||||
width: 128,
|
||||
height: 128,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 144,
|
||||
child: Text(
|
||||
playlist.title,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 16.0),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SmartTrackListTile extends StatelessWidget {
|
||||
|
||||
final SmartTrackList smartTrackList;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
SmartTrackListTile(this.smartTrackList, {this.onHold, this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: CachedImage(
|
||||
width: 128,
|
||||
height: 128,
|
||||
url: smartTrackList.cover.thumb,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 144.0,
|
||||
child: Text(
|
||||
smartTrackList.title,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumCard extends StatelessWidget {
|
||||
|
||||
final Album album;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
|
||||
AlbumCard(this.album, {this.onTap, this.onHold});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: CachedImage(
|
||||
width: 128.0,
|
||||
height: 128.0,
|
||||
url: album.art.thumb,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 144.0,
|
||||
child: Text(
|
||||
album.title,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelTile extends StatelessWidget {
|
||||
|
||||
final DeezerChannel channel;
|
||||
final Function onTap;
|
||||
ChannelTile(this.channel, {this.onTap});
|
||||
|
||||
Color _textColor() {
|
||||
double luminance = channel.backgroundColor.computeLuminance();
|
||||
return (luminance>0.5)?Colors.black:Colors.white;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: channel.backgroundColor,
|
||||
child: InkWell(
|
||||
onTap: this.onTap,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 75,
|
||||
child: Center(
|
||||
child: Text(
|
||||
channel.title,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _textColor()
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue