Initial commit

This commit is contained in:
exttex 2020-06-23 21:23:12 +02:00
commit ed087bc583
123 changed files with 10390 additions and 0 deletions

203
lib/ui/cached_image.dart Normal file
View 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
View 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),
)
],
);
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
),
),
),
),
)
);
}
}